[{"content":"准备 SSH 密钥 GitHub Actions 要通过 SSH 上传文件到服务器，需要 一个公私钥对：\n在本地生成 SSH 密钥（推荐单独为部署创建，不覆盖现有 SSH）： 1 ssh-keygen -t rsa -b 4096 -f ~/.ssh/myblog_deploy_key -f ~/.ssh/myblog_deploy_key → 保存路径和文件名 会生成两个文件： 1 2 ~/.ssh/myblog_deploy_key # 私钥 ~/.ssh/myblog_deploy_key.pub # 公钥 注意不要设置 passphrase，GitHub Actions 无法交互输入密码 把公钥上传到阿里云服务器 假设服务器 IP：123.45.67.89，用户名 root 或普通用户：\n1 2 3 4 5 ssh root@123.45.67.89 mkdir -p ~/.ssh chmod 700 ~/.ssh echo \u0026#34;\u0026lt;myblog_deploy_key.pub 内容\u0026gt;\u0026#34; \u0026gt;\u0026gt; ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys 这样 GitHub Actions 用私钥就可以 SSH 登录服务器，无需密码。 私钥内容放到 GitHub Secrets 打开 GitHub 仓库 → Settings → Secrets and variables → Actions → New repository secret Secret 名称：ALI_KEY 内容就是 ~/.ssh/myblog_deploy_key 文件里的完整内容（保持换行） 注意：不要在 public key 中放入 GitHub Secrets，只放私钥。\n或者可以直接将本地的公钥上传到服务器，私钥放在github Secrets中\n阿里云服务器初始化 假设服务器全新（Ubuntu 22.04 举例）：\n更新系统 1 sudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y 安装必需工具 1 sudo apt install -y git rsync curl 创建博客目录 1 2 sudo mkdir -p /var/www/myblog sudo chown -R $USER:$USER /var/www/myblog 允许防火墙端口 80/443 1 2 3 sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable Hugo 构建部署准备 安装 Hugo Extended（支持 PaperMod SCSS） 1 2 3 4 5 6 7 wget https://github.com/gohugoio/hugo/releases/download/v0.115.0/hugo_extended_0.157.0_Linux-64bit.tar.gz tar -xzf hugo_extended_0.157.0_Linux-64bit.tar.gz sudo mv hugo /usr/local/bin/ hugo version # 输出下列 hugo v0.157.0-7747abbb316b03c8f353fd3be62d5011fa883ee6+extended linux/amd64 BuildDate=2026-02-25T16:38:33Z VendorInfo=gohugoio 安装docker https://ktzxy.top/posts/tech/docker/ivzblwvqy0/\n创建docker-compose 1 2 3 4 5 6 7 8 9 10 11 12 13 14 version: \u0026#39;3.8\u0026#39; services: nginx: image: nginx:alpine container_name: myblog_nginx restart: always ports: - \u0026#34;80:80\u0026#34; - \u0026#34;443:443\u0026#34; volumes: - ./public:/usr/share/nginx/html:ro - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro - /etc/letsencrypt:/etc/letsencrypt:ro 配置 ssl 安装 Nginx + Certbot 1 sudo apt install -y nginx certbot python3-certbot-nginx 申请 SSL 证书（Let’s Encrypt） 使用 certbot 自动签发并配置：\n1 sudo certbot --nginx -d your-domain.com -d www.your-domain.com 按照提示：\n输入邮箱 同意条款 选择是否强制 HTTPS（选 2 强制跳转） 成功后会自动生成：\n1 /etc/letsencrypt/live/your-domain.com/ 自动续期（重要） 测试自动续期：\n1 sudo certbot renew --dry-run Certbot 会自动加入系统定时任务，无需手动操作。\n创建 Nginx 站点配置 创建文件：\n1 sudo vim /var/www/myblog/nginx.conf 写入下面完整配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 # ============================== # HTTP 自动跳转 HTTPS # ============================== server { listen 80; server_name your-domain.com www.your-domain.com; return 301 https://$host$request_uri; } # ============================== # HTTPS 主站配置 # ============================== server { listen 443 ssl http2; server_name your-domain.com www.your-domain.com; # ⚠ 关键修改 root /usr/share/nginx/html; index index.html; ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_session_timeout 1d; ssl_session_cache shared:SSL:10m; add_header Strict-Transport-Security \u0026#34;max-age=63072000; includeSubDomains; preload\u0026#34; always; add_header X-Frame-Options SAMEORIGIN; add_header X-Content-Type-Options nosniff; add_header Referrer-Policy no-referrer-when-downgrade; gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; gzip_min_length 1024; location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|webp|woff2?)$ { expires 30d; add_header Cache-Control \u0026#34;public, no-transform\u0026#34;; } location / { try_files $uri $uri/ =404; } location ~ /\\. { deny all; } access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; } 启用站点 1 2 3 sudo ln -s /etc/nginx/sites-available/your-domain.com /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl restart nginx 最终检查 查看状态：\n1 sudo systemctl status nginx 访问：\n1 https://your-domain.com 应该看到你的 Hugo 博客。\nhugo配置 hugo.toml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 baseURL = \u0026#39;https://ktzxy.top/\u0026#39; languageCode = \u0026#34;zh-cn\u0026#34; timeZone = \u0026#34;Asia/Shanghai\u0026#34; title = \u0026#34;🏘️Home\u0026#34; copyright = \u0026#34;[©2024 Blue Eucalyptus\u0026#39;s Blog](https://ktzxy.top/)\u0026#34; theme = \u0026#34;PaperMod\u0026#34; enableRobotsTXT = true # 生成 robots.txt buildDrafts = false # 是否构建草稿 buildFuture = false # 是否构建未来内容 buildExpired = false # 是否构建过期内容 canonifyURLs = true enableGitInfo = true # 输出 JSON 用于搜索 [outputs] home = [\u0026#34;HTML\u0026#34;, \u0026#34;RSS\u0026#34;, \u0026#34;JSON\u0026#34;] [permalinks] posts = \u0026#34;/posts/:slug/\u0026#34; [params] author = \u0026#34;Atticus Wilde\u0026#34; # 作者名称 enableThemeToggle = true # 夜间模式开关 enableFontSize = true # 字号切换 showReadingTime = true # 显示阅读时间 showWordCount = true # 显示文章字数 disableIntegrity = true # 关闭 SRI 验证 disableLiveReload = true # 构建时关闭 LiveReload enableSearch = true # 启用搜索功能 enableInlineShortcodes = true # 允许短代码 hasCJKLanguage = true # 中文/日文/韩文字符支持 ShowCodeCopyButtons = true # 代码复制按钮 ShowRssButtonInSectionTermList = true # RSS按钮 enableEmoji = true # 启用 emoji 表情 pygmentsUseClasses = true # 高亮使用 class 而不是 inline 样式 # 标签和首页显示配置 ShowBreadCrumbs = true # 面包屑导航 ShowPostNavLinks = true # 文章上下页导航 ShowShareButtons = true # 分享按钮 UseHugoToc = true # 启用 Hugo 内置目录 ShowToc = true # 页面显示目录 TocOpen = false # 目录默认折叠 disableFingerprinting = false #启用资源指纹识别 comments = true #启用评论 ShowAllPagesInArchive = true # 文章归档显示 defaultTheme = \u0026#34;auto\u0026#34; # 自动适配系统暗黑模式。 # Fuse.js 搜索选项 [params.fuseOpts] isCaseSensitive = false shouldSort = true location = 0 distance = 1000 threshold = 0.4 # 0.0 意味着完全精确匹配 minMatchCharLength = 2 # 原为 1，避免单字搜索刷屏 includeMatches = true keys = [\u0026#34;title\u0026#34;, \u0026#34;tags\u0026#34;, \u0026#34;summary\u0026#34;] [params.cover] hidden = true # 默认隐藏 hiddenInList = true hiddenInSingle = true [params.giscus] repo = \u0026#34;ktzxy/BlueEucalyptusBlog\u0026#34; repoId = \u0026#34;R_kgDORa2bBQ\u0026#34; category = \u0026#34;General\u0026#34; categoryId = \u0026#34;DIC_kwDORa2bBc4C3yKa\u0026#34; mapping = \u0026#34;pathname\u0026#34; reactionsEnabled = \u0026#34;1\u0026#34; inputPosition = \u0026#34;top\u0026#34; theme = \u0026#34;preferred_color_scheme\u0026#34; lang = \u0026#34;zh-CN\u0026#34; loading = \u0026#34;lazy\u0026#34; # 🖼 头像模式配置（参考 RexBlog） [params.profileMode] enabled = true # 启用头像模式 title = \u0026#34;蓝桉の博客\u0026#34; subtitle = \u0026#34;行至水穷处，坐看云起时；山海自远，心自从容。\u0026#34; imageUrl = \u0026#34;./images/profile.png\u0026#34; # 头像路径 imageWidth = 120 imageHeight = 120 imageTitle = \u0026#34;头像\u0026#34; buttons = [ { name = \u0026#34;Posts\u0026#34;, url = \u0026#34;/posts/\u0026#34; }, { name = \u0026#34;Tags\u0026#34;, url = \u0026#34;/tags/\u0026#34; }, { name = \u0026#34;Archives\u0026#34;, url = \u0026#34;/archives/\u0026#34; }, { name = \u0026#34;About\u0026#34;, url = \u0026#34;/about/\u0026#34; } ] # 📂 社交图标（可自由添加） [[params.socialIcons]] name = \u0026#34;github\u0026#34; url = \u0026#34;https://github.com/ktzxy\u0026#34; [[params.socialIcons]] name = \u0026#34;email\u0026#34; url = \u0026#34;mailto:kt_zxh@163.com\u0026#34; [[params.socialIcons]] name = \u0026#34;csdn\u0026#34; url = \u0026#34;https://blog.csdn.net/qq_46087070?type=blog\u0026#34; # 菜单 [menu] [[menu.main]] identifier = \u0026#34;search\u0026#34; name = \u0026#34;Search\u0026#34; url = \u0026#34;/search/\u0026#34; weight = 2 # Markdown/数学公式支持 [markup] [markup.goldmark] [markup.goldmark.extensions] passthrough.delimiters.block = [[\u0026#34;\\\\[\u0026#34;,\u0026#34;\\\\]\u0026#34;], [\u0026#34;$$\u0026#34;,\u0026#34;$$\u0026#34;]] passthrough.delimiters.inline = [[\u0026#34;\\\\(\u0026#34;,\u0026#34;\\\\)\u0026#34;], [\u0026#34;$\u0026#34;,\u0026#34;$\u0026#34;]] passthrough.enable = true [markup.highlight] style = \u0026#34;github-dark\u0026#34; lineNos = true codeFences = true guessSyntax = true # Sitemap优化 [sitemap] changefreq = \u0026#34;weekly\u0026#34; priority = 0.5 filename = \u0026#34;sitemap.xml\u0026#34; # 性能优化 [minify] disableXML = true minifyOutput = true 搜索 myblog\\content\\search.md\n1 2 3 4 5 6 7 8 +++ draft = false layout = \u0026#34;search\u0026#34; title = \u0026#39;Search\u0026#39; summary = \u0026#34;search\u0026#34; placeholder = \u0026#34;Search posts...\u0026#34; type = \u0026#34;page\u0026#34; +++ 1 2 3 4 5 6 7 8 9 10 # Fuse.js 搜索选项 [params.fuseOpts] isCaseSensitive = false shouldSort = true location = 0 distance = 1000 threshold = 0.4 # 0.0 意味着完全精确匹配 minMatchCharLength = 2 # 原为 1，避免单字搜索刷屏 includeMatches = true keys = [\u0026#34;title\u0026#34;, \u0026#34;tags\u0026#34;, \u0026#34;summary\u0026#34;] 归档 myblog\\content\\archives.md\n1 2 3 4 5 6 7 +++ draft = false title = \u0026#39;Archives\u0026#39; layout = \u0026#34;archives\u0026#34; url = \u0026#34;/archives/\u0026#34; type = \u0026#34;page\u0026#34; +++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 /* ========================================== Hugo「留白诗行」归档样式 风格：侘寂 · 诗意 · 静谧高贵 ========================================== */ .archive { position: relative; max-width: 800px; /* 控制宽度，避免过宽失去聚焦感 */ margin: 0 auto; padding: 80px 40px; font-family: -apple-system, BlinkMacSystemFont, \u0026#34;Segoe UI\u0026#34;, Roboto, \u0026#34;Helvetica Neue\u0026#34;, Arial, sans-serif; color: #222; line-height: 1.6; } /* ========================================== 年份标题 - 如画卷开篇 ========================================== */ .archive-year-header { position: relative; display: inline-block; margin: 60px 0 40px 0; font-size: 2.2rem; font-weight: 700; letter-spacing: -0.5px; color: #111; } /* 年份旁的小计数 */ .archive-year-header span { font-size: 0.9rem; color: #999; font-weight: 400; margin-left: 8px; vertical-align: super; /* 上标效果 */ opacity: 0.7; } /* ========================================== 月份标题 - 如章节分隔 ========================================== */ .archive-month-header { position: relative; display: inline-block; margin: 40px 0 20px 0; font-size: 1.3rem; font-weight: 600; color: #333; } /* 月份旁的小计数 */ .archive-month-header span { font-size: 0.8rem; color: #aaa; font-weight: 400; margin-left: 6px; vertical-align: super; opacity: 0.6; } /* ========================================== 文章列表 - 诗行式排列 ========================================== */ .archive-posts { display: flex; flex-direction: column; gap: 28px; /* 增大间距，营造呼吸感 */ margin-bottom: 60px; } .archive-entry { position: relative; display: flex; flex-direction: column; gap: 6px; padding: 16px 0; transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); /* 初始状态：轻微透明，营造“未展开”的静谧感 */ opacity: 0.9; } /* Hover 效果：完全显现 + 微移 + 墨迹晕染 */ .archive-entry:hover { opacity: 1; transform: translateX(-4px); /* 向左微移，靠近年份/月份 */ } /* ========================================== 标题部分 - 如诗句主体 ========================================== */ .archive-entry-link { font-size: 1.1rem; font-weight: 500; color: #222; text-decoration: none; line-height: 1.4; /* 关键：禁止换行，超长省略 */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color 0.2s ease; } .archive-entry:hover .archive-entry-link { color: #000; } /* ========================================== 元信息部分 - 如题跋小字 ========================================== */ .archive-entry-meta { font-size: 0.8rem; color: #999; font-weight: 400; line-height: 1.3; letter-spacing: 0.3px; } /* ========================================== 「墨迹晕染」Hover 特效（可选） 使用伪元素模拟毛笔蘸水扩散 ========================================== */ .archive-entry::before { content: \u0026#39;\u0026#39;; position: absolute; top: 50%; left: 0; width: 0; height: 0; background: radial-gradient(circle, rgba(100, 100, 100, 0.05) 0%, transparent 70%); border-radius: 50%; transform: translateY(-50%) scale(0); transition: all 0.5s cubic-bezier(0.25, 0.8, 0.25, 1); z-index: 0; pointer-events: none; } .archive-entry:hover::before { width: 200px; height: 200px; transform: translateY(-50%) scale(1); } /* ========================================== 响应式优化 ========================================== */ @media (max-width: 768px) { .archive { padding: 40px 20px; } .archive-year-header { font-size: 1.8rem; margin: 40px 0 30px 0; } .archive-month-header { font-size: 1.2rem; margin: 30px 0 15px 0; } .archive-posts { gap: 20px; } .archive-entry-link { font-size: 1rem; white-space: normal; /* 手机端允许换行 */ overflow: visible; text-overflow: clip; } .archive-entry-meta { font-size: 0.75rem; } .archive-entry:hover { transform: none; } .archive-entry::before { display: none; /* 移动端关闭晕染特效 */ } } 标签 myblog\\assets\\css\\extended\\tags.css\ncss样式必须放在该路径下，自动加载不用引入\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 /* ========================================== Hugo 极简意境标签样式 - 紧凑版 (Compact \u0026amp; Zen) 风格参考：PaperMod / 现代杂志风 + 智能填充 ========================================== */ /* 标签容器 */ .terms-tags { display: flex; flex-wrap: wrap; gap: 8px; /* 缩小间距，更紧凑 */ margin-top: 24px; padding: 0; list-style: none; /* 关键：允许子项自由伸缩，自动填充满行 */ justify-content: flex-start; } /* 单个标签项 */ .terms-tags li { margin: 0; /* 不允许换行中断，由父容器控制 */ } /* 标签主体链接 */ .terms-tags li a { position: relative; display: inline-flex; align-items: center; /* 核心改动：动态宽度 + 最小限制 */ padding: 6px 14px; min-width: 50px; /* 防止过窄 */ max-width: 100%; /* 防止溢出 */ font-size: 0.85rem; font-weight: 500; color: #333; text-decoration: none; letter-spacing: 0.3px; border-radius: 99px; border: 1px solid #eaeaea; background-color: #fff; transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); /* 🔥 关键：允许内容撑开，但不强制等宽 */ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } /* Hover 动效：反色填充 */ .terms-tags li a:hover { background-color: #222; border-color: #222; color: #fff; transform: translateY(-1px); box-shadow: 0 3px 8px rgba(0, 0, 0, 0.06); } /* ========================================== 数量角标优化 (Badge Style) ========================================== */ .terms-tags li a sup { position: static; display: inline-flex; align-items: center; justify-content: center; margin-left: 6px; min-width: 16px; height: 16px; padding: 0 5px; font-size: 0.7rem; font-weight: 600; line-height: 1; color: #999; background-color: #f5f5f5; border-radius: 99px; transition: all 0.3s ease; } /* Hover 时角标也跟随反色 */ .terms-tags li a:hover sup { background-color: rgba(255, 255, 255, 0.2); color: #fff; } /* ========================================== 响应式适配 ========================================== */ @media (max-width: 768px) { .terms-tags { gap: 6px; } .terms-tags li a { padding: 5px 12px; font-size: 0.8rem; min-width: 45px; } .terms-tags li a sup { min-width: 14px; height: 14px; font-size: 0.65rem; margin-left: 4px; padding: 0 4px; } } 首页 myblog\\assets\\css\\extended\\homepage.css\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 /* ========================= Emperor Supreme Edition ========================= */ :root { --bg-main: #f5f5f7; --text-main: #1d1d1f; --text-sub: #6e6e73; --accent: #111; } html[data-theme=\u0026#34;dark\u0026#34;] { --bg-main: #000; --text-main: #f5f5f7; --text-sub: #8e8e93; --accent: #fff; } /* 背景纯净 */ body.list { background: var(--bg-main); } /* Hero结构 */ .profile { min-height: 95vh; display: flex; align-items: center; justify-content: center; text-align: center; padding: 0 20px; } /* 去卡片 */ .profile_inner { background: none; box-shadow: none; padding: 0; max-width: 720px; } /* 头像 */ .profile img { width: 120px; height: 120px; border-radius: 50%; margin-bottom: 40px; filter: grayscale(10%); transition: transform .4s ease; } .profile img:hover { transform: scale(1.04); } /* 标题（稳重，不夸张） */ .profile h1 { font-size: clamp(36px, 4vw, 52px); font-weight: 600; letter-spacing: -0.5px; line-height: 1.2; color: var(--text-main); margin-bottom: 22px; } /* 副标题 */ .profile span { display: block; font-size: 18px; line-height: 1.8; color: var(--text-sub); max-width: 600px; margin: 0 auto; } /* 按钮区域 */ .buttons { margin-top: 40px; display: flex; justify-content: center; gap: 20px; flex-wrap: wrap; } /* 统一按钮 */ .button { min-width: 130px; text-align: center; padding: 12px 28px; font-size: 16px; font-weight: 500; border-radius: 999px; border: 1px solid var(--accent); background: transparent; color: var(--accent); transition: all .3s ease; } .button:hover { background: var(--accent); color: var(--bg-main); transform: scale(1.05); } /* 社交图标 */ .social-icons { margin-top: 40px; opacity: 0.65; } .social-icons a:hover { opacity: 1; } 关于我 myblog\\content\\about.md\n1 2 3 4 5 6 7 8 +++ draft = false title = \u0026#39;About\u0026#39; description = \u0026#34;技术为术，道在其外；笃行不怠，静水深流。\u0026#34; layout = \u0026#34;about\u0026#34; showToc = false +++ ........ 评论配置 安装 Giscus App 直接打开这个页面：\n👉 https://github.com/apps/giscus\n这是 GitHub 的 App 页面。\n右上角会看到：\n1 Install 点击：\n1 Install 然后选择：\n1 Only select repositories 选择你的博客仓库，例如：\n1 myblog 最后点击：\n1 Install 安装完成。\n开启 GitHub Discussions 进入你的博客仓库：\n1 Settings 找到：\n1 Features 勾选：\n1 Discussions 保存。\n此时仓库顶部会出现：\n1 Code | Issues | Pull requests | Actions | Discussions 生成 Giscus 配置 打开：\n👉 https://giscus.app\n填写：\n1 2 Repository 你的用户名/仓库名 例如：\n1 AtticusWilde/myblog 如果安装成功，会自动检测到仓库。\n然后选择：\n1 2 Discussion Category → General 映射方式推荐：\n1 pathname 下面就会生成 配置代码。\n把配置写入 Hugo PaperMod config.toml 添加 Giscus\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [params] comments = true [params.giscus] repo = \u0026#34;你的用户名/仓库名\u0026#34; repoId = \u0026#34;R_kgDOxxxx\u0026#34; category = \u0026#34;General\u0026#34; categoryId = \u0026#34;DIC_kwDOxxxx\u0026#34; mapping = \u0026#34;pathname\u0026#34; strict = \u0026#34;0\u0026#34; reactionsEnabled = \u0026#34;1\u0026#34; emitMetadata = \u0026#34;0\u0026#34; inputPosition = \u0026#34;top\u0026#34; theme = \u0026#34;preferred_color_scheme\u0026#34; lang = \u0026#34;zh-CN\u0026#34; myblog\\layouts\\partials\\comments.html\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 {{ if .Site.Params.comments }} \u0026lt;div class=\u0026#34;comments\u0026#34;\u0026gt; \u0026lt;!-- Giscus 评论 --\u0026gt; \u0026lt;script src=\u0026#34;https://giscus.app/client.js\u0026#34; data-repo=\u0026#34;{{ .Site.Params.giscus.repo }}\u0026#34; data-repo-id=\u0026#34;{{ .Site.Params.giscus.repoId }}\u0026#34; data-category=\u0026#34;{{ .Site.Params.giscus.category }}\u0026#34; data-category-id=\u0026#34;{{ .Site.Params.giscus.categoryId }}\u0026#34; data-mapping=\u0026#34;pathname\u0026#34; data-reactions-enabled=\u0026#34;1\u0026#34; data-input-position=\u0026#34;top\u0026#34; data-theme=\u0026#34;preferred_color_scheme\u0026#34; data-lang=\u0026#34;zh-CN\u0026#34; crossorigin=\u0026#34;anonymous\u0026#34; async \u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/div\u0026gt; {{ end }} myblog\\layouts\\partials\\post_footer.html\n1 {{ partial \u0026#34;comments.html\u0026#34; . }} myblog\\assets\\css\\extended\\comments.css\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 /* ================================ Giscus 评论区高级美化（PaperMod 专用） ================================ */ /* 评论整体容器 */ .giscus { margin-top: 60px; padding: 25px; border-radius: 16px; background: linear-gradient(145deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.04)); backdrop-filter: blur(10px); border: 1px solid var(--border); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.05); transition: all 0.3s ease; } /* hover 微动 */ .giscus:hover { transform: translateY(-3px); box-shadow: 0 12px 30px rgba(0, 0, 0, 0.12); } /* 评论标题 */ .giscus::before { content: \u0026#34;💬 评论区\u0026#34;; display: block; font-size: 18px; font-weight: 600; margin-bottom: 15px; color: var(--primary); } /* 评论 iframe */ .giscus iframe { border-radius: 12px; } /* 评论区加载动画 */ .giscus:empty { height: 120px; display: flex; align-items: center; justify-content: center; } .giscus:empty::after { content: \u0026#34;评论加载中...\u0026#34;; color: var(--secondary); font-size: 14px; animation: fade 1.2s infinite; } @keyframes fade { 0% { opacity: .3 } 50% { opacity: 1 } 100% { opacity: .3 } } /* 暗黑模式优化 */ .dark .giscus { background: linear-gradient(145deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.05)); border: 1px solid rgba(255, 255, 255, 0.08); } /* 评论区间距优化 */ .post-content+.giscus { margin-top: 80px; } 卸载主题 1 2 3 4 5 6 7 8 # 1. 从 .gitmodules 文件中移除记录 git submodule deinit -f themes/PaperMod-PE # 2. 从 Git 缓存中移除该模块 git rm -f themes/PaperMod-PE # 3. 删除 .git/modules 中残留的配置 (如果存在) Remove-Item -Recurse -Force .git/modules/themes/PaperMod-PE -ErrorAction SilentlyContinue 添加文章版权声明 参考 PaperMod 添加文章版权声明 | Tofuwine\u0026rsquo;s Blog\n创建 copyright.html 文件：\nmyblog\\layouts\\partials\\copyright.html\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;div class=\u0026#34;pe-copyright\u0026#34;\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;blockquote\u0026gt; {{ if .Param \u0026#34;reposted\u0026#34; }} \u0026lt;p\u0026gt;本文为转载内容，原文信息如下：\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;原文标题：{{- .Param \u0026#34;repostedTitle\u0026#34; -}}\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;原文作者：{{- .Param \u0026#34;repostedAuthor\u0026#34; -}}\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;原文链接：\u0026lt;a href=\u0026#34;{{- .Param \u0026#34;repostedLink\u0026#34; -}}\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;{{- .Param \u0026#34;repostedLink\u0026#34; -}}\u0026lt;/a\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;如有侵权，请\u0026lt;a href=\u0026#34;mailto://{{ .Param \u0026#34;contactEmail\u0026#34; }}\u0026#34;\u0026gt;联系作者\u0026lt;/a\u0026gt;删除。\u0026lt;/p\u0026gt; {{ else }} \u0026lt;p\u0026gt;本文为原创内容，版权归作者所有。如需转载，请在文章中声明本文标题及链接。\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;文章标题：{{ .Title }} —— {{ .Param \u0026#34;author\u0026#34; }}\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;文章链接：\u0026lt;a href=\u0026#34;{{ .Permalink }}\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;{{ .Permalink }}\u0026lt;/a\u0026gt;\u0026lt;/p\u0026gt; \u0026lt;p\u0026gt;许可协议：\u0026lt;a href=\u0026#34;{{- .Param \u0026#34;licenseLink\u0026#34; -}}\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;{{- .Param \u0026#34;licenseName\u0026#34; -}}\u0026lt;/a\u0026gt;\u0026lt;/p\u0026gt; {{ end }} \u0026lt;/blockquote\u0026gt; \u0026lt;/div\u0026gt; copyright.css 文件：\nmyblog\\assets\\css\\extended\\copyright.css\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 .pe-copyright { margin-top: 20px; font-size: 14px; } .pe-copyright hr { border-style: dashed; color: #e26c56; } .pe-copyright blockquote { margin: 10px 0; padding: 0 10px; border-inline-start: 3px solid #e26c56; } .pe-copyright a { box-shadow: 0 1px; box-decoration-break: clone; -webkit-box-decoration-break: clone; } 在 footer 节点上添加如下内容：\n这里我是将主题single.html copy一份到myblog\\layouts_default\\single.html\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 {{- define \u0026#34;main\u0026#34; }} \u0026lt;article class=\u0026#34;post-single\u0026#34;\u0026gt; \u0026lt;header class=\u0026#34;post-header\u0026#34;\u0026gt; {{ partial \u0026#34;breadcrumbs.html\u0026#34; . }} \u0026lt;h1 class=\u0026#34;post-title entry-hint-parent\u0026#34;\u0026gt; {{ .Title }} {{- if .Draft }} \u0026lt;span class=\u0026#34;entry-hint\u0026#34; title=\u0026#34;Draft\u0026#34;\u0026gt; \u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; height=\u0026#34;35\u0026#34; viewBox=\u0026#34;0 -960 960 960\u0026#34; fill=\u0026#34;currentColor\u0026#34; \u0026gt; \u0026lt;path d=\u0026#34;M160-410v-60h300v60H160Zm0-165v-60h470v60H160Zm0-165v-60h470v60H160Zm360 580v-123l221-220q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q9 9 13 20t4 22q0 11-4.5 22.5T862.09-380L643-160H520Zm300-263-37-37 37 37ZM580-220h38l121-122-18-19-19-18-122 121v38Zm141-141-19-18 37 37-18-19Z\u0026#34; /\u0026gt; \u0026lt;/svg\u0026gt; \u0026lt;/span\u0026gt; {{- end }} \u0026lt;/h1\u0026gt; {{- if .Description }} \u0026lt;div class=\u0026#34;post-description\u0026#34;\u0026gt;{{ .Description }}\u0026lt;/div\u0026gt; {{- end }} {{- if not (.Param \u0026#34;hideMeta\u0026#34;) }} \u0026lt;div class=\u0026#34;post-meta\u0026#34;\u0026gt; {{- partial \u0026#34;post_meta.html\u0026#34; . -}} {{- partial \u0026#34;translation_list.html\u0026#34; . -}} {{- partial \u0026#34;edit_post.html\u0026#34; . -}} {{- partial \u0026#34;post_canonical.html\u0026#34; . -}} \u0026lt;/div\u0026gt; {{- end }} \u0026lt;/header\u0026gt; {{- $isHidden := (.Param \u0026#34;cover.hiddenInSingle\u0026#34;) | default (.Param \u0026#34;cover.hidden\u0026#34;) | default false }} {{- partial \u0026#34;cover.html\u0026#34; (dict \u0026#34;cxt\u0026#34; . \u0026#34;IsSingle\u0026#34; true \u0026#34;isHidden\u0026#34; $isHidden) }} {{- if (.Param \u0026#34;ShowToc\u0026#34;) }} {{- partial \u0026#34;toc.html\u0026#34; . }} {{- end }} {{- if .Content }} \u0026lt;div class=\u0026#34;post-content\u0026#34;\u0026gt; {{- if not (.Param \u0026#34;disableAnchoredHeadings\u0026#34;) }} {{- partial \u0026#34;anchored_headings.html\u0026#34; .Content -}} {{- else }}{{ .Content }}{{ end }} \u0026lt;/div\u0026gt; {{- end }} \u0026lt;!-- Copyright --\u0026gt; {{ if .Param \u0026#34;enableCopyright\u0026#34; }} {{ partial \u0026#34;copyright.html\u0026#34; . }} {{ end }} \u0026lt;footer class=\u0026#34;post-footer\u0026#34;\u0026gt; {{- $tags := .Language.Params.Taxonomies.tag | default \u0026#34;tags\u0026#34; }} \u0026lt;ul class=\u0026#34;post-tags\u0026#34;\u0026gt; {{- range ($.GetTerms $tags) }} \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;{{ .Permalink }}\u0026#34;\u0026gt;{{ .LinkTitle }}\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; {{- end }} \u0026lt;/ul\u0026gt; {{- if (.Param \u0026#34;ShowPostNavLinks\u0026#34;) }} {{- partial \u0026#34;post_nav_links.html\u0026#34; . }} {{- end }} {{- if (and site.Params.ShowShareButtons (ne .Params.disableShare true)) }} {{- partial \u0026#34;share_icons.html\u0026#34; . -}} {{- end }} \u0026lt;/footer\u0026gt; {{- if (.Param \u0026#34;comments\u0026#34;) }} {{- partial \u0026#34;comments.html\u0026#34; . }} {{- end }} \u0026lt;/article\u0026gt; {{- end }}{{/* end main */}} hugo.toml配置\n1 2 3 4 5 [params] # copyright enableCopyright = true licenseLink = \u0026#34;https://creativecommons.org/licenses/by-nc/4.0/\u0026#34; licenseName = \u0026#34;CC BY-NC 4.0\u0026#34; 转载文章\n1 2 3 4 5 6 7 +++ reposted: true repostedTitle: \u0026#34;修改为原文章标题\u0026#34; repostedAuthor: \u0026#34;修改为原文章作者名\u0026#34; repostedLink: \u0026#34;修改为原文章链接\u0026#34; contactEmail: your email +++ 添加赞赏 创建 reward.html 文件：\nmyblog\\layouts\\partials\\reward.html\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 {{- $button := .Params.rewardButton | default \u0026#34;\u0026#34; }} {{- $subtitle := .Params.rewardSubtitle | default \u0026#34;如果本文对您有所帮助，欢迎打赏支持作者！\u0026#34; }} {{- $title := .Params.rewardTitle | default \u0026#34;赞赏作者\u0026#34; }} \u0026lt;div class=\u0026#34;pe-reward-wrap\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;pe-reward-card\u0026#34;\u0026gt; \u0026lt;!-- 标题 --\u0026gt; \u0026lt;h3 class=\u0026#34;pe-reward-title\u0026#34;\u0026gt;{{ $title }}\u0026lt;/h3\u0026gt; \u0026lt;!-- 副标题 --\u0026gt; {{- if not .Params.hideRewardSubtitle }} \u0026lt;p class=\u0026#34;pe-reward-subtitle\u0026#34;\u0026gt;{{ $subtitle }}\u0026lt;/p\u0026gt; {{ end }} \u0026lt;!-- 按钮区域：改为扁平风格 --\u0026gt; \u0026lt;div class=\u0026#34;pe-reward-btn-container\u0026#34;\u0026gt; \u0026lt;a href=\u0026#34;javascript:void(0);\u0026#34; class=\u0026#34;pe-reward-trigger\u0026#34; onclick=\u0026#34; document .querySelector(\u0026#39;.pe-reward-overlay\u0026#39;) .classList.remove(\u0026#39;hidden\u0026#39;) \u0026#34; \u0026gt; {{ if eq $button \u0026#34;\u0026#34; }} \u0026lt;!-- 默认图标：缩小尺寸 --\u0026gt; \u0026lt;svg viewBox=\u0026#34;0 0 1024 1024\u0026#34; version=\u0026#34;1.1\u0026#34; xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; width=\u0026#34;18\u0026#34; height=\u0026#34;18\u0026#34; \u0026gt; \u0026lt;path d=\u0026#34;M835.52 337.824a159.04 159.04 0 0 1 124.544 59.52 155.904 155.904 0 0 1 30.4 134.08l-80.896 341.632a157.952 157.952 0 0 1-56.224 87.68 160.544 160.544 0 0 1-98.688 33.952H166.72c-75.712 0-137.44-60.96-137.44-136.256v-363.84c0-75.296 61.76-136.288 137.44-136.288h80.704c50.176 0 71.232-12.48 85.792-36.544 10.368-17.12 17.472-41.376 23.04-76 1.728-10.656 2.88-19.392 5.408-38.72 11.712-90.944 21.888-124.736 62.528-155.168 25.504-19.072 56.768-25.984 90.72-20.992 64.672 9.504 115.936 43.52 145.824 97.6 23.36 42.272 32.64 95.584 27.904 154.24-1.056 12.832-2.752 25.888-5.12 38.912-0.64 3.68-1.856 9.024-3.712 16.192h155.68z m-261.472 80l14.72-51.104c9.472-32.704 15.04-53.376 16.064-59.2 1.92-10.56 3.264-21.024 4.096-31.296 3.584-43.84-3.04-81.6-18.208-109.056-17.472-31.68-46.88-51.2-87.392-57.12-13.696-2.016-23.456 0.128-31.168 5.888-15.808 11.84-22.4 33.792-31.136 101.312-2.56 20.16-3.84 29.44-5.76 41.248-7.04 43.872-16.704 76.8-33.536 104.64-28.736 47.52-75.424 75.2-154.24 75.2H166.72c-31.744 0-57.44 25.376-57.44 56.224v363.872c0 30.88 25.696 56.256 57.44 56.256h587.904c17.824 0 35.424-6.08 49.344-16.96 13.856-10.848 23.68-26.24 27.712-43.104l80.896-341.6a75.904 75.904 0 0 0-14.944-65.6 79.04 79.04 0 0 0-62.144-29.6H574.08z m-212.8 205.984l97.28 72.64 242.24-209.28s16.224-13.888 30.4-3.008c4.256 3.264 9.12 12.512-1.856 27.008l-252.896 277.856s-19.392 24.864-42.4-0.288l-109.12-138.208s-12.96-18.688 3.264-29.92c5.44-3.744 17.888-9.6 33.12 3.2z\u0026#34; fill=\u0026#34;currentColor\u0026#34; \u0026gt;\u0026lt;/path\u0026gt; \u0026lt;/svg\u0026gt; {{ else }} {{ $button | safeHTML }} {{ end }} \u0026lt;span class=\u0026#34;pe-reward-btn-text\u0026#34;\u0026gt;打赏作者\u0026lt;/span\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;!-- 遮罩层 (保持不变) --\u0026gt; \u0026lt;div class=\u0026#34;pe-reward-overlay hidden\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;pe-reward-qr-wrap\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;pe-reward-img-group\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;pe-reward-item\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;pe-reward-qr-box\u0026#34;\u0026gt; {{- $wechatImg := .Site.Params.WechatPay }} {{- if and $wechatImg (not (hasPrefix $wechatImg \u0026#34;http\u0026#34;)) }} {{- $wechatImg = urls.JoinPath $.Site.BaseURL $wechatImg }} {{- end }} \u0026lt;img src=\u0026#34;{{ $wechatImg }}\u0026#34; alt=\u0026#34;WeChat Pay\u0026#34; onerror=\u0026#34;this.style.display = \u0026#39;none\u0026#39;\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;p\u0026gt;微信\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;pe-reward-item\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;pe-reward-qr-box\u0026#34;\u0026gt; {{- $alipayImg := .Site.Params.Alipay }} {{- if and $alipayImg (not (hasPrefix $alipayImg \u0026#34;http\u0026#34;)) }} {{- $alipayImg = urls.JoinPath $.Site.BaseURL $alipayImg }} {{- end }} \u0026lt;img src=\u0026#34;{{ $alipayImg }}\u0026#34; alt=\u0026#34;Alipay\u0026#34; onerror=\u0026#34;this.style.display = \u0026#39;none\u0026#39;\u0026#34; /\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;p\u0026gt;支付宝\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;span class=\u0026#34;pe-reward-close\u0026#34; onclick=\u0026#34;this.closest(\u0026#39;.pe-reward-overlay\u0026#39;).classList.add(\u0026#39;hidden\u0026#39;)\u0026#34; \u0026gt;\u0026amp;times;\u0026lt;/span \u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt; document .querySelector(\u0026#34;.pe-reward-overlay\u0026#34;) .addEventListener(\u0026#34;click\u0026#34;, function (evt) { if ( evt.target === this || evt.target.classList.contains(\u0026#34;pe-reward-qr-wrap\u0026#34;) ) { this.classList.add(\u0026#34;hidden\u0026#34;); } }); \u0026lt;/script\u0026gt; 添加赞赏按钮及其遮罩层样式。创建 reward.css 文件：\nmyblog\\assets\\css\\extended\\reward.css\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 /* 外层包裹 */ .pe-reward-wrap { margin: 2rem 0; /* 减小上下间距 */ width: 100%; } /* 卡片容器 */ .pe-reward-card { background-color: var(--theme); border-radius: 12px; padding: 2rem 1.5rem; /* 减小内边距 */ text-align: center; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); position: relative; } /* 标题 */ .pe-reward-title { font-size: 1.2rem; /* 稍微调小标题 */ font-weight: 700; margin-bottom: 0.5rem; color: var(--body-font-color); } /* 副标题 */ .pe-reward-subtitle { font-size: 0.9rem; color: var(--secondary-font-color); margin-bottom: 1.5rem; line-height: 1.5; } /* 按钮容器 */ .pe-reward-btn-container { display: flex; justify-content: center; align-items: center; } /* --- 核心修改：扁平长方形按钮 --- */ .pe-reward-trigger { display: inline-flex; /* 改为行内弹性布局，让图标和文字横向排列 */ flex-direction: row; /* 横向排列 */ align-items: center; justify-content: center; padding: 0.5rem 1.2rem; /* 扁平的内边距 */ height: auto; /* 高度自适应 */ min-width: 120px; /* 最小宽度 */ background-color: #ff5e5e; /* 纯色背景，去掉渐变 */ color: #fff; border-radius: 50px; /* 胶囊形状 (长方形圆角) */ font-size: 0.9rem; /* 字体调小 */ font-weight: 500; text-decoration: none; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 2px 8px rgba(255, 94, 94, 0.3); /* 轻微阴影 */ border: 1px solid transparent; } /* 悬停效果 */ .pe-reward-trigger:hover { background-color: #ff4040; transform: translateY(-2px); /* 轻微上浮 */ box-shadow: 0 4px 12px rgba(255, 94, 94, 0.4); } .pe-reward-trigger svg { width: 16px; /* 图标缩小 */ height: 16px; fill: currentColor; margin-right: 6px; /* 图标和文字之间的间距 */ margin-bottom: 0; /* 移除之前的底部间距 */ } .pe-reward-btn-text { line-height: 1; white-space: nowrap; /* 防止文字换行 */ } /* --- 遮罩层与二维码 (保持简洁) --- */ .pe-reward-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); z-index: 9999; display: flex; justify-content: center; align-items: center; } .pe-reward-overlay.hidden { display: none; } .pe-reward-qr-wrap { background-color: var(--theme); padding: 2rem; border-radius: 12px; position: relative; max-width: 90%; width: 450px; /* 稍微调窄一点 */ text-align: center; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); } .pe-reward-img-group { display: flex; justify-content: center; gap: 1.5rem; flex-wrap: wrap; } .pe-reward-item { display: flex; flex-direction: column; align-items: center; } .pe-reward-qr-box { width: 140px; height: 140px; border: 1px solid #eee; padding: 8px; border-radius: 8px; display: flex; align-items: center; justify-content: center; background: #fff; } .pe-reward-qr-box img { max-width: 100%; max-height: 100%; object-fit: contain; display: block; } .pe-reward-item p { margin-top: 0.6rem; font-size: 0.85rem; color: var(--body-font-color); } .pe-reward-close { position: absolute; top: 8px; right: 12px; font-size: 1.8rem; line-height: 1; color: #999; cursor: pointer; } .pe-reward-close:hover { color: #333; } /* 移动端适配 */ @media screen and (max-width: 568px) { .pe-reward-card { padding: 1.5rem 1rem; } .pe-reward-qr-wrap { width: 85%; padding: 1.5rem; } .pe-reward-qr-box { width: 110px; height: 110px; } } 将赞赏按钮添加到文章章末 在 footer 节点上添加如下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 {{- define \u0026#34;main\u0026#34; }} \u0026lt;article class=\u0026#34;post-single\u0026#34;\u0026gt; \u0026lt;header class=\u0026#34;post-header\u0026#34;\u0026gt; {{ partial \u0026#34;breadcrumbs.html\u0026#34; . }} \u0026lt;h1 class=\u0026#34;post-title entry-hint-parent\u0026#34;\u0026gt; {{ .Title }} {{- if .Draft }} \u0026lt;span class=\u0026#34;entry-hint\u0026#34; title=\u0026#34;Draft\u0026#34;\u0026gt; \u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; height=\u0026#34;35\u0026#34; viewBox=\u0026#34;0 -960 960 960\u0026#34; fill=\u0026#34;currentColor\u0026#34; \u0026gt; \u0026lt;path d=\u0026#34;M160-410v-60h300v60H160Zm0-165v-60h470v60H160Zm0-165v-60h470v60H160Zm360 580v-123l221-220q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q9 9 13 20t4 22q0 11-4.5 22.5T862.09-380L643-160H520Zm300-263-37-37 37 37ZM580-220h38l121-122-18-19-19-18-122 121v38Zm141-141-19-18 37 37-18-19Z\u0026#34; /\u0026gt; \u0026lt;/svg\u0026gt; \u0026lt;/span\u0026gt; {{- end }} \u0026lt;/h1\u0026gt; {{- if .Description }} \u0026lt;div class=\u0026#34;post-description\u0026#34;\u0026gt;{{ .Description }}\u0026lt;/div\u0026gt; {{- end }} {{- if not (.Param \u0026#34;hideMeta\u0026#34;) }} \u0026lt;div class=\u0026#34;post-meta\u0026#34;\u0026gt; {{- partial \u0026#34;post_meta.html\u0026#34; . -}} {{- partial \u0026#34;translation_list.html\u0026#34; . -}} {{- partial \u0026#34;edit_post.html\u0026#34; . -}} {{- partial \u0026#34;post_canonical.html\u0026#34; . -}} \u0026lt;/div\u0026gt; {{- end }} \u0026lt;/header\u0026gt; {{- $isHidden := (.Param \u0026#34;cover.hiddenInSingle\u0026#34;) | default (.Param \u0026#34;cover.hidden\u0026#34;) | default false }} {{- partial \u0026#34;cover.html\u0026#34; (dict \u0026#34;cxt\u0026#34; . \u0026#34;IsSingle\u0026#34; true \u0026#34;isHidden\u0026#34; $isHidden) }} {{- if (.Param \u0026#34;ShowToc\u0026#34;) }} {{- partial \u0026#34;toc.html\u0026#34; . }} {{- end }} {{- if .Content }} \u0026lt;div class=\u0026#34;post-content\u0026#34;\u0026gt; {{- if not (.Param \u0026#34;disableAnchoredHeadings\u0026#34;) }} {{- partial \u0026#34;anchored_headings.html\u0026#34; .Content -}} {{- else }}{{ .Content }}{{ end }} \u0026lt;/div\u0026gt; {{- end }} \u0026lt;!-- Copyright --\u0026gt; {{ if .Param \u0026#34;enableCopyright\u0026#34; }} {{ partial \u0026#34;copyright.html\u0026#34; . }} {{ end }} \u0026lt;!-- Reward --\u0026gt; {{ if .Param \u0026#34;enableReward\u0026#34; }} {{ partial \u0026#34;reward.html\u0026#34; . }} {{ end }} \u0026lt;footer class=\u0026#34;post-footer\u0026#34;\u0026gt; {{- $tags := .Language.Params.Taxonomies.tag | default \u0026#34;tags\u0026#34; }} \u0026lt;ul class=\u0026#34;post-tags\u0026#34;\u0026gt; {{- range ($.GetTerms $tags) }} \u0026lt;li\u0026gt;\u0026lt;a href=\u0026#34;{{ .Permalink }}\u0026#34;\u0026gt;{{ .LinkTitle }}\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; {{- end }} \u0026lt;/ul\u0026gt; {{- if (.Param \u0026#34;ShowPostNavLinks\u0026#34;) }} {{- partial \u0026#34;post_nav_links.html\u0026#34; . }} {{- end }} {{- if (and site.Params.ShowShareButtons (ne .Params.disableShare true)) }} {{- partial \u0026#34;share_icons.html\u0026#34; . -}} {{- end }} \u0026lt;/footer\u0026gt; {{- if (.Param \u0026#34;comments\u0026#34;) }} {{- partial \u0026#34;comments.html\u0026#34; . }} {{- end }} \u0026lt;/article\u0026gt; {{- end }}{{/* end main */}} 启用赞赏 在 hugo 配置文件中添加以下配置：\n1 2 3 4 5 6 7 8 9 10 11 [params] # 启用赞赏功能 enableReward = true # 赞赏按钮显示字符 (可在文章 `frontmatter` 中设置)，默认为赞赏图标 # rewardButton = 赞赏 # 赞赏描述 (可在文章 `frontmatter` 中设置) rewardDescription = \u0026#34;如果本文对你有所帮助，可以点击上方按钮请作者喝杯咖啡！\u0026#34; # 设置微信收款码图片 (路径相对于博客工程的 static 目录。如下文件在 `static/images/wechat_pay.jpg`) WechatPay = \u0026#34;images/wechat_pay.webp\u0026#34; # 设置支付宝收款码图片 (路径相对于博客工程的 static 目录。如下文件在 `static/images/alipay.jpg`) Alipay = \u0026#34;images/alipay.webp\u0026#34; 文章页展示所属系列的文章列表 PaperMod 文章页展示所属系列的文章列表 | loyayz\n新建模板\nmyblog\\layouts\\partials\\series-posts.html\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 {{- if .Params.series }} \u0026lt;div class=\u0026#34;x-post-series\u0026#34;\u0026gt; {{- range .Params.series }} {{- $seriesName := . }} {{- /* 获取当前系列下的所有页面 */}} {{- $pages := where $.Site.RegularPages \u0026#34;Params.series\u0026#34; \u0026#34;intersect\u0026#34; (slice $seriesName) }} {{- if $pages }} \u0026lt;details open\u0026gt; \u0026lt;summary\u0026gt; \u0026lt;strong\u0026gt;📚 系列文章：{{ $seriesName }}\u0026lt;/strong\u0026gt; \u0026lt;span style=\u0026#34;font-size: 0.8em; color: var(--secondary)\u0026#34; \u0026gt;({{ len $pages }}篇)\u0026lt;/span \u0026gt; \u0026lt;/summary\u0026gt; \u0026lt;ol\u0026gt; {{- /* 核心逻辑：自定义排序 1. 提取标题开头的数字部分 2. 转换为整数进行排序 3. 如果没数字，则排到最后 */}} {{- $sortedPages := sort $pages \u0026#34;Params.weight\u0026#34; }} {{- /* 如果没有设置 weight 参数，我们尝试按标题数字排序 */}} {{- /* 注意：Hugo 原生 sort 很难直接解析字符串中的数字，这里用一种变通方法：*/}} {{- /* 我们假设用户会在 front matter 中设置 weight，或者我们手动处理 */}} {{- /* 【更简单的方案】：利用 Hugo 的 \u0026#34;ByParam\u0026#34; 或手动构建排序列表 由于 Hugo 模板语言限制，最稳健的方法是要求用户在 Front Matter 中设置 weight 或者我们这里写一段稍微复杂的逻辑来提取数字。 下面这段代码尝试提取标题第一个连续数字作为排序依据 */}} {{- /* 初始化一个空切片用于存储带排序键的对象 */}} {{- $listWithSortKey := slice }} {{- range $pages }} {{- $title := .LinkTitle }} {{- $sortKey := 9999 }} {{- /* 默认最大值，如果没有数字排最后 */}} {{- /* 尝试提取标题开头的数字 (支持 \u0026#34;1 \u0026#34;, \u0026#34;1.\u0026#34;, \u0026#34;01 \u0026#34;, \u0026#34;01.\u0026#34; 等格式) */}} {{- $matched := findRE \u0026#34;^\\\\s*(\\\\d+)\u0026#34; $title }} {{- if $matched }} {{- $numStr := index $matched 0 }} {{- /* 去掉可能存在的非数字字符并转为 int */}} {{- $cleanNum := replaceRE \u0026#34;[^0-9]\u0026#34; \u0026#34;\u0026#34; $numStr }} {{- if $cleanNum }} {{- $sortKey = int $cleanNum }} {{- end }} {{- end }} {{- /* 将页面和排序键打包 */}} {{- $item := dict \u0026#34;Page\u0026#34; . \u0026#34;SortKey\u0026#34; $sortKey }} {{- $listWithSortKey = $listWithSortKey | append $item }} {{- end }} {{- /* 对打包后的列表按 SortKey 排序 */}} {{- $sortedList := sort $listWithSortKey \u0026#34;SortKey\u0026#34; }} {{- /* 遍历排序后的列表渲染 */}} {{- range $sortedList }} {{- $p := .Page }} \u0026lt;li style=\u0026#34;margin-bottom: 8px\u0026#34;\u0026gt; \u0026lt;a href=\u0026#34;{{ $p.Permalink }}\u0026#34; style=\u0026#34;text-decoration: none; color: var(--primary)\u0026#34; \u0026gt; {{- /* 正则替换：去掉开头的 \u0026#34;数字 + 可选点号 + 可选空格\u0026#34; */}} {{- $cleanTitle := replaceRE \u0026#34;^\\\\s*\\\\d+\\\\.?\\\\s*\u0026#34; \u0026#34;\u0026#34; $p.LinkTitle }} {{ $cleanTitle }} \u0026lt;/a\u0026gt; {{- if eq $p $ }} \u0026lt;span style=\u0026#34;color: var(--accent); font-weight: bold\u0026#34;\u0026gt; \u0026amp;lt;-- 当前阅读\u0026lt;/span \u0026gt; {{- end }} \u0026lt;sub style=\u0026#34; display: block; font-size: 0.75em; color: var(--secondary); margin-top: 2px; \u0026#34; \u0026gt; {{ $p.Date.Format \u0026#34;2006-01-02\u0026#34; }} \u0026lt;/sub\u0026gt; \u0026lt;/li\u0026gt; {{- end }} \u0026lt;/ol\u0026gt; \u0026lt;/details\u0026gt; {{- end }} {{- end }} \u0026lt;/div\u0026gt; {{- end }} 添加样式\nmyblog\\assets\\css\\extended\\series-posts.css\n1 2 3 .x-post-series { padding: 16px 0; } 编辑文章模板\nmyblog\\layouts_default\\single.html\n1 2 3 4 5 6 7 8 \u0026lt;footer class=\u0026#34;post-footer\u0026#34;\u0026gt; \u0026lt;!-- 添加下面这2行 --\u0026gt; {{- if and site.Params.ShowSeriesInPost (ne .Params.showSeries false ) }} {{- partial \u0026#34;series-posts.html\u0026#34; . -}} {{- end }} {{- $tags := .Language.Params.Taxonomies.tag | default \u0026#34;tags\u0026#34; }} 配置\n1 2 3 params: # 是否在文章页显示所属系列的文章列表 ShowSeriesInPost = true 设置文章所属系列\n1 2 3 4 5 6 7 8 9 10 11 12 13 +++ date = \u0026#39;2026-01-04T11:02:13+08:00\u0026#39; draft = false title = \u0026#39;hugo博客部署\u0026#39; slug = \u0026#34;4163nwhh20\u0026#34; description = \u0026#34;使用docker部署hugo，部署到云服务器\u0026#34; summary = \u0026#34;使用docker部署hugo，部署到云服务器\u0026#34; categories = [\u0026#34;📒博客部署\u0026#34;] tags = [\u0026#34;hugo\u0026#34;] comments = true series = [\u0026#34;博客部署美化系列\u0026#34;] showSeries= true +++ 搜索页展示系列列表 myblog\\layouts_default\\search.html\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 {{- define \u0026#34;main\u0026#34; }} ....... \u0026lt;div id=\u0026#34;searchbox\u0026#34;\u0026gt; \u0026lt;input id=\u0026#34;searchInput\u0026#34; autofocus placeholder=\u0026#34;{{ .Params.placeholder | default (printf \u0026#34;%s ↵\u0026#34; .Title) }}\u0026#34; aria-label=\u0026#34;search\u0026#34; type=\u0026#34;search\u0026#34; autocomplete=\u0026#34;off\u0026#34; maxlength=\u0026#34;64\u0026#34;\u0026gt; \u0026lt;ul id=\u0026#34;searchResults\u0026#34; aria-label=\u0026#34;search results\u0026#34;\u0026gt;\u0026lt;/ul\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;!-- 搜索页加系列文章 --\u0026gt; {{- if not (.Param \u0026#34;hideSeries\u0026#34;) }} {{- /* 1. 安全获取系列分类数据 */}} {{- $taxonomies := .Site.Taxonomies.series }} {{- /* 2. 增加防御性判断：确保 $taxonomies 不是 nil 且有内容 */}} {{- if and $taxonomies (gt (len $taxonomies) 0) }} \u0026lt;h2 style=\u0026#34;margin-top: 32px\u0026#34;\u0026gt;{{- (.Param \u0026#34;seriesTitle\u0026#34;) | default \u0026#34;📚 系列专栏\u0026#34; }}\u0026lt;/h2\u0026gt; \u0026lt;ul class=\u0026#34;terms-tags\u0026#34;\u0026gt; {{- range $name, $value := $taxonomies }} {{- $count := .Count }} {{- /* 尝试获取系列页面，如果不存在则使用默认链接 */}} {{- $seriesPage := site.GetPage (printf \u0026#34;/series/%s\u0026#34; $name) }} {{- $href := cond $seriesPage $seriesPage.Permalink (printf \u0026#34;/series/%s\u0026#34; $name) }} {{- $displayName := cond $seriesPage $seriesPage.Name $name }} \u0026lt;li\u0026gt; \u0026lt;a href=\u0026#34;{{ $href }}\u0026#34;\u0026gt; {{ $displayName }} \u0026lt;sup\u0026gt;\u0026lt;strong\u0026gt;{{ $count }}\u0026lt;/strong\u0026gt;\u0026lt;/sup\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/li\u0026gt; {{- end }} \u0026lt;/ul\u0026gt; {{- end }} {{- end }} #添加上面一段代码 {{- end }}{{/* end main */}} hugo.toml\n1 2 3 [taxonomies] tag = \u0026#34;tags\u0026#34; series = \u0026#34;series\u0026#34; 搜索页展示标签列表 myblog\\layouts_default\\search.html\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 {{- define \u0026#34;main\u0026#34; }} ....... \u0026lt;!-- 搜索页加标签列表 --\u0026gt; {{- if not (.Param \u0026#34;hideTags\u0026#34;) }} {{- /* 1. 安全获取标签分类数据 */}} {{- $taxonomies := .Site.Taxonomies.tags }} {{- /* 2. 防御性判断：确保数据存在且不为空 */}} {{- if and $taxonomies (gt (len $taxonomies) 0) }} \u0026lt;h2 style=\u0026#34;margin-top: 32px\u0026#34;\u0026gt;{{- (.Param \u0026#34;tagsTitle\u0026#34;) | default \u0026#34;🏷️ 热门标签\u0026#34; }}\u0026lt;/h2\u0026gt; \u0026lt;ul class=\u0026#34;terms-tags\u0026#34;\u0026gt; {{- range $name, $value := $taxonomies }} {{- $count := .Count }} {{- /* 尝试获取标签页面，如果不存在则使用默认链接 */}} {{- $tagPage := site.GetPage (printf \u0026#34;/tags/%s\u0026#34; $name) }} {{- $href := cond $tagPage $tagPage.Permalink (printf \u0026#34;/tags/%s\u0026#34; $name) }} {{- $displayName := cond $tagPage $tagPage.Name $name }} \u0026lt;li\u0026gt; \u0026lt;a href=\u0026#34;{{ $href }}\u0026#34;\u0026gt; {{ $displayName }} \u0026lt;sup\u0026gt;\u0026lt;strong\u0026gt;{{ $count }}\u0026lt;/strong\u0026gt;\u0026lt;/sup\u0026gt; \u0026lt;/a\u0026gt; \u0026lt;/li\u0026gt; {{- end }} \u0026lt;/ul\u0026gt; {{- end }} {{- end }} {{- end }}{{/* end main */}} 文章页底部添加面包屑导航 myblog\\layouts_default\\single.html\n1 2 3 4 5 6 7 8 \u0026lt;footer class=\u0026#34;post-footer\u0026#34;\u0026gt; \u0026lt;!-- 添加下面这行 --\u0026gt; {{ partial \u0026#34;breadcrumbs.html\u0026#34; . }} {{- if (.Param \u0026#34;ShowPostNavLinks\u0026#34;) }} {{- partial \u0026#34;post_nav_links.html\u0026#34; . }} {{- end }} \u0026lt;/footer\u0026gt; 1 2 3 4 5 # 随便哪个样式页面加入 /* 文章页面底部的面包屑居右 */ .post-footer \u0026gt; .breadcrumbs { justify-content: flex-end; } 列表文章标题后添加标识 自动为列表中的文章添加对应的标识：[置顶]、[转载]\nmyblog\\layouts_default\\list.html\n替换整个 \u0026lt;header class=\u0026quot;entry-header\u0026quot;\u0026gt; 块：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 \u0026lt;header class=\u0026#34;entry-header\u0026#34;\u0026gt; \u0026lt;h2 class=\u0026#34;entry-hint-parent\u0026#34;\u0026gt; {{- .Title }} {{- /* 1. 置顶标识 (权重为 1) */}} {{- if eq .Weight 1 }} \u0026lt;sup\u0026gt; \u0026lt;span class=\u0026#34;x-entry-istop\u0026#34; style=\u0026#34;font-size: 0.6em; font-weight: bold; color: var(--accent); margin-left: 6px;\u0026#34;\u0026gt;[置顶]\u0026lt;/span\u0026gt; \u0026lt;/sup\u0026gt; {{- end }} {{- /* 2. 转载标识 (front matter 中 outer: true) */}} {{- if .Param \u0026#34;outer\u0026#34; }} \u0026lt;sup\u0026gt; \u0026lt;span class=\u0026#34;x-entry-isouter\u0026#34; style=\u0026#34;font-size: 0.6em; font-weight: bold; color: var(--secondary); margin-left: 6px;\u0026#34;\u0026gt;[转载]\u0026lt;/span\u0026gt; \u0026lt;/sup\u0026gt; {{- end }} {{- /* 3. 草稿标识 (保留原有图标逻辑) */}} {{- if .Draft }} \u0026lt;span class=\u0026#34;entry-hint\u0026#34; title=\u0026#34;Draft\u0026#34; style=\u0026#34;margin-left: 6px; vertical-align: middle;\u0026#34;\u0026gt; \u0026lt;svg xmlns=\u0026#34;http://www.w3.org/2000/svg\u0026#34; height=\u0026#34;18\u0026#34; viewBox=\u0026#34;0 -960 960 960\u0026#34; fill=\u0026#34;currentColor\u0026#34;\u0026gt; \u0026lt;path d=\u0026#34;M160-410v-60h300v60H160Zm0-165v-60h470v60H160Zm0-165v-60h470v60H160Zm360 580v-123l221-220q9-9 20-13t22-4q12 0 23 4.5t20 13.5l37 37q9 9 13 20t4 22q0 11-4.5 22.5T862.09-380L643-160H520Zm300-263-37-37 37 37ZM580-220h38l121-122-18-19-19-18-122 121v38Zm141-141-19-18 37 37-18-19Z\u0026#34; /\u0026gt; \u0026lt;/svg\u0026gt; \u0026lt;/span\u0026gt; {{- end }} \u0026lt;/h2\u0026gt; \u0026lt;/header\u0026gt; 1 2 3 4 5 6 7 8 9 +++ title = \u0026#34;测试\u0026#34; date = 2026-03-03 draft = false # 1 表示置顶 weight = 1 # true 表示是转载 outer = true +++ 文章目录级别设置 hugo.toml\n1 2 3 4 [markup.tableOfContents] startLevel = 1 # 从 h1 开始 endLevel = 3 # 到 h3 结束 (可根据需要调整，比如 4, 5, 6) ordered = false # 是否使用有序列表 (1. 2. 3.)，false 为无序列表 添加瞬间 创建数据文件 (数据源) 在项目根目录下创建 data 文件夹（如果不存在），并在其中创建 moments.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 格式说明： # - date: 时间 (ISO 8601 格式) # tags: [标签列表] # content: | (多行内容，支持 Markdown) - date: 2024-03-19T16:30:00+08:00 tags: [\u0026#34;日常\u0026#34;, \u0026#34;心情\u0026#34;] content: | 这是第一条瞬间！🎉 采用单文件模式，所有数据都在这一个 yaml 文件里。 支持代码块： ```go println(\u0026#34;Hello World\u0026#34;) date: 2024-03-18T10:00:00+08:00 tags: [\u0026ldquo;技术\u0026rdquo;, \u0026ldquo;Hugo\u0026rdquo;] content: \u0026ldquo;只需复制粘贴一段 YAML 配置，就能新增一条瞬间，太方便了！☕️\u0026rdquo;\ndate: 2024-03-15T09:20:00+08:00 content: \u0026ldquo;没有标签的瞬间也是可以的。\u0026rdquo;\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ### 创建页面入口配置 创建瞬间页面的配置文件，定义标题和布局参数。 **路径：** `content/moments/_index.md` ```shell +++ title = \u0026#34;🌟 瞬间\u0026#34; description = \u0026#34;记录生活中的点滴灵感与碎片化思考\u0026#34; date = 2024-01-01T00:00:00+08:00 draft = false [build] render = \u0026#34;always\u0026#34; [params] ShowToc = false ShowShareButtons = false DateFormat = \u0026#34;2006-01-02 15:04\u0026#34; +++ 创建页面模板 创建渲染逻辑，读取 YAML 数据并生成 HTML。\n路径： layouts/moments/list.html\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 {{- define \u0026#34;main\u0026#34; }} {{- $dateFormat := .Params.DateFormat | default site.Params.DateFormat | default \u0026#34;2006-01-02 15:04\u0026#34; }} {{- $allMoments := site.Data.moments }} {{- /* 按时间倒序排序：最新的在前面 */}} {{- $sortedMoments := sort $allMoments \u0026#34;date\u0026#34; \u0026#34;desc\u0026#34; }} \u0026lt;article class=\u0026#34;post-single\u0026#34;\u0026gt; \u0026lt;header class=\u0026#34;page-header\u0026#34;\u0026gt; \u0026lt;h1\u0026gt;{{ .Title }}\u0026lt;/h1\u0026gt; {{- if .Description }} \u0026lt;div class=\u0026#34;post-description\u0026#34;\u0026gt; {{ .Description }} \u0026lt;/div\u0026gt; {{- end }} \u0026lt;/header\u0026gt; \u0026lt;div class=\u0026#34;post-content\u0026#34;\u0026gt; {{- if not $sortedMoments }} \u0026lt;div style=\u0026#34;text-align: center; padding: 4rem 0; color: var(--secondary);\u0026#34;\u0026gt; \u0026lt;p\u0026gt;暂无瞬间，快去 \u0026lt;code\u0026gt;data/moments.yml\u0026lt;/code\u0026gt; 添加第一条吧！✨\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; {{- else }} \u0026lt;ul class=\u0026#34;pe-moments\u0026#34;\u0026gt; {{- range $moment := $sortedMoments }} \u0026lt;li class=\u0026#34;pe-moment\u0026#34;\u0026gt; \u0026lt;!-- 1. 头像区域 (直接读取 hugo.toml 配置) --\u0026gt; \u0026lt;div class=\u0026#34;pe-moment-avatar\u0026#34;\u0026gt; {{- $avatarUrl := \u0026#34;\u0026#34; }} {{- /* 第一步：直接尝试从 hugo.toml (site.Params) 读取 */}} {{- if site.Params.avatar }} {{- $avatarUrl = site.Params.avatar }} {{- /* 第二步：如果 tom l没配，再尝试从 content/_index.md 读取 (兼容旧模式) */}} {{- else }} {{- with site.GetPage \u0026#34;/_index.md\u0026#34; }} {{- if .Params.avatar }} {{- $avatarUrl = .Params.avatar }} {{- end }} {{- end }} {{- end }} {{- /* 渲染结果 */}} {{- if $avatarUrl }} \u0026lt;img src=\u0026#34;{{ $avatarUrl | relURL }}\u0026#34; alt=\u0026#34;Avatar\u0026#34;\u0026gt; {{- else }} \u0026lt;div class=\u0026#34;pe-moment-avatar-placeholder\u0026#34;\u0026gt; {{- with site.Params.author }}{{ substr . 0 1 | upper }}{{ else }}M{{ end }} \u0026lt;/div\u0026gt; {{- end }} \u0026lt;/div\u0026gt; \u0026lt;!-- 2. 内容主体 --\u0026gt; \u0026lt;div class=\u0026#34;pe-moment-body\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;pe-moment-content\u0026#34;\u0026gt; {{- /* 渲染 Markdown 内容 */}} {{- $moment.content | markdownify }} \u0026lt;/div\u0026gt; \u0026lt;!-- 3. 标签 --\u0026gt; {{- if $moment.tags }} \u0026lt;div class=\u0026#34;pe-moment-tags\u0026#34;\u0026gt; {{- range $tag := $moment.tags }} \u0026lt;a href=\u0026#34;{{ \u0026#34;/tags/\u0026#34; | relURL }}{{ $tag | urlize }}/\u0026#34; class=\u0026#34;pe-moment-tag\u0026#34;\u0026gt;{{ $tag }}\u0026lt;/a\u0026gt; {{- end }} \u0026lt;/div\u0026gt; {{- end }} \u0026lt;!-- 4. 时间 --\u0026gt; \u0026lt;div class=\u0026#34;pe-moment-meta\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;pe-moment-time\u0026#34;\u0026gt; {{- time.Format $dateFormat (time $moment.date) }} \u0026lt;/span\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/li\u0026gt; {{- end }} \u0026lt;/ul\u0026gt; {{- end }} \u0026lt;/div\u0026gt; \u0026lt;/article\u0026gt; {{- end }} 添加样式 (CSS) 让瞬间页面拥有朋友圈/便签风格的样式，并适配深色模式。\n路径： assets/css/extended/moments.css\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 /* 容器 */ .pe-moments { list-style: none; padding: 0; margin: 0; } /* 单条瞬间卡片 */ .pe-moment { display: flex; gap: 1.2rem; padding: 1.5rem 0; border-bottom: 1px solid var(--border-color, #eaeaea); align-items: flex-start; animation: fadeIn 0.5s ease; } .pe-moments li:last-child { border-bottom: none; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } /* 头像 */ .pe-moment-avatar img { width: 3.2rem; height: 3.2rem; border-radius: 50%; object-fit: cover; border: 2px solid var(--border-color, #eaeaea); background-color: var(--code-bg, #f5f5f5); } .pe-moment-avatar-placeholder { width: 3.2rem; height: 3.2rem; border-radius: 50%; background-color: var(--primary, #333); color: var(--theme, #fff); display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 1.2rem; border: 2px solid var(--border-color, #eaeaea); } /* 内容区 */ .pe-moment-body { flex: 1; min-width: 0; } .pe-moment-content { font-size: 1rem; line-height: 1.7; color: var(--content-color, #333); margin-bottom: 0.8rem; word-wrap: break-word; } /* 标签 */ .pe-moment-tags { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.8rem; } .pe-moment-tag { font-size: 0.75rem; padding: 0.2rem 0.6rem; background-color: var(--code-bg, #f5f5f5); color: var(--secondary, #666); border-radius: 4px; text-decoration: none; transition: all 0.2s; } .pe-moment-tag:hover { background-color: var(--primary, #333); color: var(--theme, #fff); } /* 时间 */ .pe-moment-meta { font-size: 0.85rem; color: var(--secondary, #999); font-style: italic; } /* 移动端适配 */ @media screen and (max-width: 768px) { .pe-moment { gap: 0.8rem; padding: 1.2rem 0; } .pe-moment-avatar img, .pe-moment-avatar-placeholder { width: 2.5rem; height: 2.5rem; font-size: 1rem; } } 添加到导航菜单 (可选) 为了让访客能方便地找到“瞬间”页面，建议在导航栏添加链接。\n编辑 hugo.toml)：\n1 2 3 4 [[menu.main]] name = \u0026#34;瞬间\u0026#34; url = \u0026#34;/moments/\u0026#34; weight = 5 # 调整权重以控制显示顺序 头像配置\nhugo.toml\n1 2 3 4 5 6 7 8 9 [params] # 其他配置... # 添加作者名字（可选，用于显示首字母 fallback） author = \u0026#34;YourName\u0026#34; # 【关键】添加头像路径 # 注意：这里写的是相对路径，去掉 \u0026#34;static\u0026#34; 前缀 avatar = \u0026#34;images/profile.png\u0026#34; 目录导航右侧显示 修改 single.html 布局，把 TOC 从文章开头移到右侧悬浮位置\n1 2 3 4 5 6 7 8 9 10 11 12 # 删除35行左右的下列代码 {{- if (.Param \u0026#34;ShowToc\u0026#34;) }} {{- partial \u0026#34;toc.html\u0026#34; . }} {{- end }} #在文章末尾添加 TOC 容器（在第 66 行 \u0026lt;/article\u0026gt; 之后，{{- end }} 之前） {{- if (.Param \u0026#34;ShowToc\u0026#34;) }} \u0026lt;aside class=\u0026#34;toc-sidebar\u0026#34;\u0026gt; {{- partial \u0026#34;toc.html\u0026#34; . }} \u0026lt;/aside\u0026gt; {{- end }} 创建 CSS 样式文件\nmyblog\\assets\\css\\extended\\toc-sidebar.css\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 /* 右侧悬浮目录容器 */ .toc-sidebar { position: fixed; top: calc(var(--header-height) + var(--gap)); right: 5px; width: 300px; max-height: calc(100vh - var(--header-height) - var(--gap) * 2 - var(--footer-height)); overflow-y: auto; z-index: 10; padding: var(--gap); } .toc-sidebar .toc { margin-bottom: 0; border: 1px solid var(--border); background: var(--entry); border-radius: var(--radius); padding: 0.4em; position: sticky; top: 0; } [data-theme=\u0026#34;dark\u0026#34;] .toc-sidebar .toc { background: var(--entry); } .toc-sidebar .toc details summary { cursor: zoom-in; margin-inline-start: 10px; user-select: none; font-weight: 600; font-size: 14px; } .toc-sidebar .toc details[open] summary { cursor: zoom-out; } .toc-sidebar .toc .details { display: inline; font-weight: 600; color: var(--primary); } .toc-sidebar .toc .inner { margin: 10px 15px; padding: 0 5px; opacity: 0.9; } .toc-sidebar .toc li ul { margin-inline-start: var(--gap); } .toc-sidebar .toc a { display: block; padding: 4px 0; color: var(--secondary); font-size: 13px; line-height: 1.4; text-decoration: none; transition: color 0.2s ease; } .toc-sidebar .toc a:hover { color: var(--primary); text-decoration: underline; } .toc-sidebar::-webkit-scrollbar { width: 4px; } .toc-sidebar::-webkit-scrollbar-thumb { background: var(--tertiary); border-radius: 2px; } .toc-sidebar::-webkit-scrollbar-track { background: transparent; } /* 屏幕宽度小于 1200px 时，取消悬浮，放在文章顶部 */ @media screen and (max-width: 1200px) { .toc-sidebar { position: static; width: 100%; max-width: 720px; margin: 0 auto var(--content-gap) auto; padding: 0; } .toc-sidebar .toc { position: static; } } /* 移动端隐藏目录 */ @media screen and (max-width: 768px) { .toc-sidebar { display: none; } } /* 当前激活的目录项高亮 */ .toc-sidebar .toc a.active { color: var(--primary); font-weight: 600; position: relative; } /* 添加箭头指示 */ .toc-sidebar .toc a.active::before { content: \u0026#39;➤\u0026#39;; position: absolute; left: -12px; top: 50%; transform: translateY(-50%); font-size: 14px; font-weight: bold; color: var(--primary); } /* 悬停时也显示箭头（可选） */ .toc-sidebar .toc a:hover::before { content: \u0026#39;→\u0026#39;; position: absolute; left: -12px; top: 50%; transform: translateY(-50%); font-size: 14px; color: var(--secondary); } /* 确保有足够空间显示箭头 */ .toc-sidebar .toc a { padding-left: 15px; } 创建 JavaScript 文件实现滚动监听\nmyblog\\layouts\\partials\\toc-highlight.html\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 \u0026lt;script\u0026gt; function initTocHighlight() { const headings = document.querySelectorAll(\u0026#39;.post-content h1, .post-content h2, .post-content h3, .post-content h4, .post-content h5, .post-content h6\u0026#39;); const tocLinks = document.querySelectorAll(\u0026#39;.toc-sidebar .toc a\u0026#39;); if (headings.length === 0 || tocLinks.length === 0) return; const observerOptions = { root: null, rootMargin: \u0026#39;-20% 0px -80% 0px\u0026#39;, threshold: 0 }; const observer = new IntersectionObserver((entries) =\u0026gt; { entries.forEach(entry =\u0026gt; { if (entry.isIntersecting) { const id = entry.target.getAttribute(\u0026#39;id\u0026#39;); if (id) { tocLinks.forEach(link =\u0026gt; { link.classList.remove(\u0026#39;active\u0026#39;); if (link.getAttribute(\u0026#39;href\u0026#39;) === \u0026#39;#\u0026#39; + id) { link.classList.add(\u0026#39;active\u0026#39;); link.scrollIntoView({ behavior: \u0026#39;smooth\u0026#39;, block: \u0026#39;nearest\u0026#39; }); } }); } } }); }, observerOptions); headings.forEach(heading =\u0026gt; observer.observe(heading)); } if (document.readyState === \u0026#39;loading\u0026#39;) { document.addEventListener(\u0026#39;DOMContentLoaded\u0026#39;, initTocHighlight); } else { initTocHighlight(); } \u0026lt;/script\u0026gt; 复制主题下 toc.html 到 layouts/partials/ myblog\\layouts\\partials\\toc.html\n1 2 3 4 ........ \u0026lt;/div\u0026gt; \u0026lt;script\u0026gt;initTocHighlight()\u0026lt;/script\u0026gt; # 在最后添加该行 {{- end }} 修改配置，改为true，点击文章自动展开目录导航\n1 TocOpen = true # 目录默认折叠 ","permalink":"https://ktzxy.top/posts/4163nwhh20/","summary":"使用docker部署hugo，部署到云服务器","title":"hugo博客部署"},{"content":"推送镜像到公有仓库 一、确认本地镜像 你现在有：\n1 2 REPOSITORY TAG IMAGE ID mariadb 10.6 eb40c69647cc 目标仓库：\n1 crpi-7jaksta3zhkkymyt.cn-shenzhen.personal.cr.aliyuncs.com/common_imgage/im1 二、登录阿里云镜像仓库 执行：\n1 docker login --username=AtticusWilde crpi-7jaksta3zhkkymyt.cn-shenzhen.personal.cr.aliyuncs.com 输入：\n1 阿里云容器镜像服务密码 成功会看到：\n1 Login Succeeded 三、给镜像打 Tag 将本地镜像打上阿里云仓库标签：\n1 2 docker tag eb40c69647cc \\ crpi-7jaksta3zhkkymyt.cn-shenzhen.personal.cr.aliyuncs.com/common_imgage/mariadb:10.6 或者直接：\n1 2 docker tag mariadb:10.6 \\ crpi-7jaksta3zhkkymyt.cn-shenzhen.personal.cr.aliyuncs.com/common_imgage/mariadb:10.6 查看：\n1 docker images 会出现：\n1 2 3 REPOSITORY TAG mariadb 10.6 crpi-7jaksta3zhkkymyt.cn-shenzhen.personal.cr.aliyuncs.com/common_imgage/mariadb 10.6 四、推送镜像 执行：\n1 docker push crpi-7jaksta3zhkkymyt.cn-shenzhen.personal.cr.aliyuncs.com/common_imgage/mariadb:10.6 会看到：\n1 2 Pushing layer... Pushed 完成后镜像就存在 阿里云仓库。\n五、服务器拉取镜像 以后服务器就可以直接：\n1 docker pull crpi-7jaksta3zhkkymyt.cn-shenzhen.personal.cr.aliyuncs.com/common_imgage/mariadb:10.6 不会再受 DockerHub 网络问题影响。\n六、docker-compose 使用私有镜像 把 compose 改成：\n1 2 db: image: crpi-7jaksta3zhkkymyt.cn-shenzhen.personal.cr.aliyuncs.com/common_imgage/mariadb:10.6 七、推荐镜像仓库结构（运维规范） 建议不要叫 im1 这种名字。\n推荐：\n1 2 3 4 5 6 7 common_image ├ nginx │ └ alpine ├ redis │ └ 7-alpine └ mariadb └ 10.6 推送示例：\n1 2 3 4 docker tag nginx:alpine \\ crpi-7jaksta3zhkkymyt.cn-shenzhen.personal.cr.aliyuncs.com/common_image/nginx:alpine docker push crpi-7jaksta3zhkkymyt.cn-shenzhen.personal.cr.aliyuncs.com/common_image/nginx:alpine 八、运维常用批量推送脚本（推荐） 如果你本地有多个镜像：\n1 docker images 例如：\n1 2 3 nginx:alpine redis:7-alpine mariadb:10.6 可以写脚本：\n1 2 3 4 5 6 7 8 9 10 vim push.sh REG=crpi-7jaksta3zhkkymyt.cn-shenzhen.personal.cr.aliyuncs.com/common_imgage docker tag nginx:alpine $REG/nginx:alpine docker tag redis:7-alpine $REG/redis:7-alpine docker tag mariadb:10.6 $REG/mariadb:10.6 docker push $REG/nginx:alpine docker push $REG/redis:7-alpine docker push $REG/mariadb:10.6 执行：\n1 bash push.sh 全自动脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 [root@192 ~]# cat push.sh #!/bin/bash # ========================================== # DockerHub 镜像同步到阿里云私有仓库 # Author : AtticusWilde # Date : 2026-03-13 # ========================================== set -e # 私有仓库 REG=\u0026#34;crpi-7jaksta3zhkkymyt.cn-shenzhen.personal.cr.aliyuncs.com/common_imgage\u0026#34; # 最大重试次数 RETRY=3 # 镜像列表 IMAGES=( mariadb:10.11 postgres:15-alpine postgres:16-alpine php:8.2-fpm php:8.3-fpm node:20-alpine node:18-alpine python:3.11-alpine mongo:7 mysql:8.0 memcached:alpine rabbitmq:3-management golang:1.22-alpine openjdk:17-jdk openjdk:21-jdk alpine:3.19 portainer/portainer-ce:latest bitnami/kafka:latest minio/minio:latest jenkins/jenkins:lts gitlab/gitlab-ce:latest prom/prometheus:latest grafana/grafana:latest grafana/loki:latest docker.elastic.co/elasticsearch/elasticsearch:8.12.0 docker.elastic.co/kibana/kibana:8.12.0 fluent/fluentd:latest nicolaka/netshoot:latest joyqi/typecho:latest ghost:latest ) echo \u0026#34;======================================\u0026#34; echo \u0026#34;Docker 镜像同步脚本启动\u0026#34; echo \u0026#34;目标仓库: $REG\u0026#34; echo \u0026#34;======================================\u0026#34; # 登录仓库 docker login --username=AtticusWilde crpi-7jaksta3zhkkymyt.cn-shenzhen.personal.cr.aliyuncs.com sync_image(){ IMG=$1 echo \u0026#34;\u0026#34; echo \u0026#34;--------------------------------------\u0026#34; echo \u0026#34;处理镜像: $IMG\u0026#34; echo \u0026#34;--------------------------------------\u0026#34; # 获取tag TAG=$(echo $IMG | awk -F: \u0026#39;{print $2}\u0026#39;) if [ -z \u0026#34;$TAG\u0026#34; ]; then TAG=\u0026#34;latest\u0026#34; IMG=\u0026#34;$IMG:$TAG\u0026#34; fi # 获取镜像名（去掉 registry） NAME=$(echo $IMG | awk -F/ \u0026#39;{print $NF}\u0026#39; | cut -d\u0026#39;:\u0026#39; -f1) TARGET=\u0026#34;$REG/$NAME:$TAG\u0026#34; echo \u0026#34;目标镜像: $TARGET\u0026#34; # pull 重试机制 for ((i=1;i\u0026lt;=RETRY;i++)) do echo \u0026#34;拉取镜像 (尝试 $i/$RETRY)...\u0026#34; if docker pull $IMG; then echo \u0026#34;Pull 成功\u0026#34; break fi if [ $i -eq $RETRY ]; then echo \u0026#34;Pull 失败，跳过 $IMG\u0026#34; return fi sleep 3 done # 打 tag docker tag $IMG $TARGET # push 重试 for ((i=1;i\u0026lt;=RETRY;i++)) do echo \u0026#34;推送镜像 (尝试 $i/$RETRY)...\u0026#34; if docker push $TARGET; then echo \u0026#34;Push 成功\u0026#34; break fi if [ $i -eq $RETRY ]; then echo \u0026#34;Push 失败: $TARGET\u0026#34; return fi sleep 3 done echo \u0026#34;镜像同步完成: $TARGET\u0026#34; } # 遍历镜像 for img in \u0026#34;${IMAGES[@]}\u0026#34; do sync_image $img done echo \u0026#34;\u0026#34; echo \u0026#34;======================================\u0026#34; echo \u0026#34;所有镜像同步完成\u0026#34; echo \u0026#34;======================================\u0026#34; 清理已有 Docker 日志 如果日志已经很大：\n查看：\n1 du -sh /var/lib/docker/containers/* 清理：\n1 truncate -s 0 /var/lib/docker/containers/*/*-json.log 不会影响容器运行。\n","permalink":"https://ktzxy.top/posts/8ne7cjwth5/","summary":"推送镜像到公有仓库","title":"推送镜像到公有仓库"},{"content":"宿主机vpn代理虚拟机拉取镜像 1 2 3 4 5 6 7 8 9 10 11 #桥接模式 vim /etc/systemd/system/docker.service.d/proxy.conf [Service] # 将 192.168.31.99 替换为你真实的宿主机 IP Environment=\u0026#34;HTTP_PROXY=http://宿主机ip:7897\u0026#34; #宿主机ip+vpn端口 Environment=\u0026#34;HTTPS_PROXY=http://宿主机ip:7897\u0026#34; # 本地地址不走代理，防止死循环 Environment=\u0026#34;NO_PROXY=localhost,127.0.0.1,.local,.internal,192.168.31.0/24\u0026#34; sudo systemctl daemon-reload sudo systemctl restart docker 宿主vpn系统代理，局域网开启\n","permalink":"https://ktzxy.top/posts/kithu0hblk/","summary":"虚拟机设置vpn代理拉取镜像","title":"宿主机vpn代理虚拟机拉取镜像"},{"content":"《生产事故排障手册》 第一部分：运维排障思维模型 1.1 生产排障黄金流程 1 2 3 生产排障黄金六步法 1. 发现告警 → 2. 初步评估 → 3. 紧急止血 → 4. 定位根因 5. 彻底修复 → 6. 复盘预防 步骤1：发现告警（0-5分钟）\n监控平台触发告警（Zabbix/Prometheus/云监控） 用户反馈/客服转告 业务指标异常（订单下跌、响应时间增长） 步骤2：初步评估（5-10分钟）\n影响范围评估：单服务/多服务/全站 影响程度评估：部分功能/核心功能/完全不可用 紧急程度分级：P0（全站不可用）/P1（核心功能）/P2（部分功能） 步骤3：紧急止血（10-30分钟）\n回滚最近变更 重启故障服务 切换备用链路 限流降级 步骤4：定位根因（30分钟-2小时）\n收集证据（日志、监控、链路追踪） 分层排查（网络→系统→应用→数据） 复现问题（测试环境验证） 步骤5：彻底修复（2-24小时）\n代码修复 配置调整 架构优化 容量扩容 步骤6：复盘预防（24-72小时）\n编写事故报告（Postmortem） 制定改进措施（Action Items） 更新监控告警 完善应急预案 1.2 故障定位四层模型 1 2 3 4 5 6 7 8 应用层 (Application) 代码逻辑 | 配置错误 | 依赖服务 | 业务异常 | 缓存失效 系统层 (System) CPU过载 | 内存泄漏 | 磁盘满 | 文件句柄 | 进程崩溃 网络层 (Network) DNS故障 | 路由异常 | 防火墙 | 负载均衡 | 带宽耗尽 硬件层 (Hardware) 磁盘损坏 | 内存故障 | 网卡异常 | 电源问题 | 机房断电 排查顺序：自底向上\n先确认硬件/基础设施是否正常 再检查网络连通性 然后查看系统资源 最后定位应用问题 1.3 SRE排障方法论 Google SRE核心原则：\n原则 说明 实践 消除琐事 减少重复性手工操作 自动化运维脚本 拥抱风险 接受一定程度的故障 错误预算(Error Budget) 监控优先 用数据驱动决策 四黄金信号监控 自动化修复 故障自愈 自动扩容/重启 简化设计 降低系统复杂度 微服务拆分 四黄金信号监控：\n延迟(Latency)：请求处理时间 流量(Traffic)：请求量/QPS 错误(Errors)：错误率/失败请求 饱和度(Saturation)：资源使用率 1.4 变更管理原则 变更三要素：\n1 2 3 1. 谁操作：明确责任人，禁止无授权变更 2. 何时操作：避开业务高峰，选择低峰期 3. 如何回滚：必须有回滚预案，15分钟内可执行 变更检查清单：\n变更方案已评审 回滚方案已验证 监控告警已配置 相关人员已通知 备份已完成 测试环境已验证 第二部分：Linux生产排障命令大全 2.1 CPU排障 常用命令：\n命令 用途 示例 top 实时查看CPU占用 top -d 1 htop 交互式进程查看 htop vmstat 系统整体状态 vmstat 1 5 mpstat CPU详细统计 mpstat -P ALL 1 pidstat 进程级统计 pidstat -u 1 perf 性能分析 perf top CPU 100%排查流程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 找出占用CPU最高的进程 top -b -n 1 | head -20 # 2. 查看该进程的线程 top -H -p \u0026lt;PID\u0026gt; # 3. 将线程ID转换为16进制 printf \u0026#34;%x\\n\u0026#34; \u0026lt;TID\u0026gt; # 4. 查看该线程的堆栈 jstack \u0026lt;PID\u0026gt; | grep -A 30 \u0026lt;16进制TID\u0026gt; # 5. 如果是Java应用，查看GC情况 jstat -gc \u0026lt;PID\u0026gt; 1000 10 2.2 内存排障 常用命令：\n命令 用途 示例 free -h 查看内存使用 free -h vmstat 内存交换情况 vmstat 1 5 pmap 进程内存映射 pmap -x \u0026lt;PID\u0026gt; smem 共享内存分析 smem -r slabtop 内核缓存查看 slabtop 内存泄漏排查流程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 查看整体内存使用 free -h # 2. 查看是否有swap使用 vmstat 1 5 | grep -E \u0026#34;swpd|si|so\u0026#34; # 3. 找出内存占用最高的进程 ps -eo pid,ppid,cmd,%mem,%cpu --sort=-%mem | head -20 # 4. 查看进程详细内存 cat /proc/\u0026lt;PID\u0026gt;/status | grep -E \u0026#34;VmRSS|VmSize|VmSwap\u0026#34; # 5. 检查是否有OOM Killer dmesg | grep -i \u0026#34;oom\\|killed\u0026#34; OOM Killer分析：\n1 2 3 4 5 6 7 8 9 # 查看OOM日志 grep -i \u0026#34;out of memory\u0026#34; /var/log/messages grep -i \u0026#34;killed process\u0026#34; /var/log/syslog # 查看进程OOM分数 cat /proc/\u0026lt;PID\u0026gt;/oom_score_adj # 调整OOM优先级（分数越低越不容易被杀） echo -500 \u0026gt; /proc/\u0026lt;PID\u0026gt;/oom_score_adj 2.3 IO排障 常用命令：\n命令 用途 示例 iostat 磁盘IO统计 iostat -xz 1 iotop 进程IO监控 iotop -o pidstat 进程IO统计 pidstat -d 1 lsof 文件打开情况 lsof +D /var/log du 磁盘空间分析 du -sh /* IO瓶颈排查流程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 1. 查看整体IO情况 iostat -xz 1 5 # 关键字段说明： # %util: 磁盘利用率，\u0026gt;80%表示瓶颈 # await: 平均等待时间，\u0026gt;10ms表示慢 # svctm: 服务时间 # 2. 找出IO高的进程 iotop -o -b -n 5 # 3. 查看进程打开的文件 lsof -p \u0026lt;PID\u0026gt; | grep REG # 4. 查看磁盘空间 df -hT # 5. 查看inode使用 df -i 2.4 文件句柄排障 常用命令：\n命令 用途 示例 ulimit -n 查看当前限制 ulimit -n lsof 查看打开文件 lsof -n ls /proc/\u0026lt;PID\u0026gt;/fd 进程文件描述符 ls /proc//fd 文件句柄耗尽排查：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 1. 查看系统级限制 cat /proc/sys/fs/file-max # 2. 查看当前使用 cat /proc/sys/fs/file-nr # 3. 查看进程级限制 ulimit -n # 4. 找出打开文件最多的进程 for i in /proc/[0-9]*/fd; do echo $(ls $i | wc -l) $i; done | sort -rn | head -20 # 5. 临时提高限制 ulimit -n 65535 # 6. 永久修改（/etc/security/limits.conf） echo \u0026#34;* soft nofile 65535\u0026#34; \u0026gt;\u0026gt; /etc/security/limits.conf echo \u0026#34;* hard nofile 65535\u0026#34; \u0026gt;\u0026gt; /etc/security/limits.conf 2.5 inode排障 inode耗尽现象：\n磁盘空间未满但无法创建文件 报错\u0026quot;no space left on device\u0026quot;但df -h显示有空间 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 查看inode使用 df -i # 2. 找出inode使用最高的目录 for i in /*; do echo $(find $i 2\u0026gt;/dev/null | wc -l) $i; done | sort -rn | head -10 # 3. 找出小文件最多的目录 find /var -type f -size -1k | wc -l # 4. 清理小文件（谨慎操作） find /tmp -type f -atime +7 -delete 2.6 网络排障 常用命令：\n命令 用途 示例 ping 基础连通性 ping -c 4 目标IP telnet 端口连通性 telnet IP 端口 nc 网络调试 nc -zv IP 端口 netstat 网络连接 netstat -tuln ss 套接字统计 ss -s tcpdump 抓包分析 tcpdump -i any port 80 traceroute 路由追踪 traceroute 目标IP mtr 持续路由追踪 mtr 目标IP 网络连接排查流程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 检查本地端口监听 netstat -tuln | grep 端口号 # 2. 检查连接状态 netstat -an | grep ESTABLISHED | wc -l netstat -an | grep TIME_WAIT | wc -l # 3. 检查防火墙规则 iptables -L -n | grep 端口号 firewall-cmd --list-ports # 4. 抓包分析 tcpdump -i eth0 -w capture.pcap port 8080 # 5. 分析TCP状态 ss -s 2.7 进程排障 常用命令：\n命令 用途 示例 ps 进程查看 ps auxf pgrep 按名查找进程 pgrep -f java kill 终止进程 kill -9 PID pkill 按名终止进程 pkill -f 进程名 strace 系统调用追踪 strace -p PID 僵尸进程处理：\n1 2 3 4 5 6 7 8 9 10 # 1. 查找僵尸进程 ps aux | grep defunct # 2. 查找父进程 ps -o ppid= -p \u0026lt;僵尸进程PID\u0026gt; # 3. 终止父进程（谨慎） kill -9 \u0026lt;父进程PID\u0026gt; # 4. 如果父进程是init，只能重启系统 2.8 日志排障 常用命令：\n命令 用途 示例 journalctl 系统日志 journalctl -u 服务名 dmesg 内核日志 `dmesg tail 实时查看日志 tail -f /var/log/messages grep 日志搜索 grep -i error /var/log/* awk 日志分析 awk '{print $1}' access.log 日志分析技巧：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 统计错误数量 grep -c \u0026#34;ERROR\u0026#34; /var/log/app.log # 2. 查看最近100行 tail -n 100 /var/log/app.log # 3. 实时过滤关键字 tail -f /var/log/app.log | grep -i \u0026#34;exception\u0026#34; # 4. 统计访问IP排行 awk \u0026#39;{print $1}\u0026#39; access.log | sort | uniq -c | sort -rn | head -10 # 5. 统计状态码分布 awk \u0026#39;{print $9}\u0026#39; access.log | sort | uniq -c 第三部分：网络排障手册 3.1 TCP连接问题 常见状态及含义：\n状态 含义 正常数量 ESTABLISHED 已建立连接 根据业务量 TIME_WAIT 主动关闭等待 \u0026lt;10000 CLOSE_WAIT 被动关闭等待 \u0026lt;100 SYN_RECV 半连接 \u0026lt;1000 FIN_WAIT 关闭中 \u0026lt;1000 TIME_WAIT过多处理：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 1. 查看TIME_WAIT数量 netstat -n | grep TIME_WAIT | wc -l # 2. 查看TIME_WAIT详情 netstat -n | grep TIME_WAIT # 3. 临时优化（立即回收） echo 1 \u0026gt; /proc/sys/net/ipv4/tcp_tw_reuse # 4. 缩短TIME_WAIT时间 echo 30 \u0026gt; /proc/sys/net/ipv4/tcp_fin_timeout # 5. 扩大本地端口范围 echo \u0026#34;1024 65535\u0026#34; \u0026gt; /proc/sys/net/ipv4/ip_local_port_range # 6. 永久修改（/etc/sysctl.conf） net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 30 net.ipv4.ip_local_port_range = 1024 65535 3.2 SYN Flood攻击 现象：\n大量SYN_RECV状态连接 正常连接无法建立 服务器响应变慢 排查命令：\n1 2 3 4 5 6 7 8 # 1. 查看SYN_RECV数量 netstat -n | grep SYN_RECV | wc -l # 2. 查看来源IP分布 netstat -n | grep SYN_RECV | awk \u0026#39;{print $5}\u0026#39; | cut -d: -f1 | sort | uniq -c | sort -rn | head -10 # 3. 抓包分析 tcpdump -i eth0 \u0026#39;tcp[tcpflags] \u0026amp; tcp-syn != 0\u0026#39; -w syn.pcap 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 开启SYN Cookie echo 1 \u0026gt; /proc/sys/net/ipv4/tcp_syncookies # 2. 增加半连接队列 echo 2048 \u0026gt; /proc/sys/net/ipv4/tcp_max_syn_backlog # 3. 限制SYN速率（iptables） iptables -A INPUT -p tcp --syn -m limit --limit 1/s --limit-burst 3 -j ACCEPT # 4. 使用防火墙防护 iptables -A INPUT -p tcp --syn -m recent --name synflood --rcheck --seconds 60 --hitcount 20 -j DROP 3.3 DNS故障 现象：\n域名无法解析 解析速度慢 解析结果错误 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 1. 测试DNS解析 nslookup 域名 dig 域名 host 域名 # 2. 查看本地DNS配置 cat /etc/resolv.conf # 3. 测试DNS响应时间 time nslookup 域名 # 4. 查看DNS缓存 systemctl restart nscd # 清除缓存 # 5. 测试多个DNS服务器 dig @8.8.8.8 域名 dig @114.114.114.114 域名 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 修改DNS配置 echo \u0026#34;nameserver 8.8.8.8\u0026#34; \u0026gt; /etc/resolv.conf echo \u0026#34;nameserver 114.114.114.114\u0026#34; \u0026gt;\u0026gt; /etc/resolv.conf # 2. 配置本地hosts echo \u0026#34;1.2.3.4 域名\u0026#34; \u0026gt;\u0026gt; /etc/hosts # 3. 安装本地DNS缓存 yum install nscd -y systemctl enable nscd systemctl start nscd 3.4 路由问题 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 查看路由表 route -n ip route show # 2. 追踪路由 traceroute 目标IP mtr 目标IP # 3. 查看网卡配置 ip addr show ifconfig -a # 4. 测试连通性 ping -c 4 目标IP ping -I 网卡名 目标IP 常见问题及解决：\n1 2 3 4 5 6 7 8 9 # 问题1：默认网关错误 ip route del default ip route add default via 正确网关IP # 问题2：多网卡路由冲突 ip route add 目标网段 via 网关 dev 网卡名 # 问题3：路由表满 ip route flush cache 3.5 交换机环路 现象：\n网络广播风暴 交换机CPU 100% 网络时断时续 排查方法：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 查看交换机端口流量 show interface counters # 2. 查看MAC地址表 show mac address-table # 3. 检查STP状态 show spanning-tree # 4. 查看广播包数量 show interface | include broadcast 解决方案：\n启用STP（生成树协议） 配置端口环路检测 限制广播速率 物理排查网线连接 第四部分：Docker生产事故 4.1 overlay2磁盘爆满 事故现象：\ndf -h显示/var/lib/docker使用率100% 容器无法启动 镜像无法拉取 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 查看Docker磁盘使用 docker system df # 2. 查看各容器大小 docker ps -s # 3. 查看镜像大小 docker images # 4. 查看overlay2目录 du -sh /var/lib/docker/overlay2/* # 5. 查看悬空镜像 docker images -f \u0026#34;dangling=true\u0026#34; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 1. 清理悬空镜像 docker image prune -f # 2. 清理未使用容器 docker container prune -f # 3. 清理未使用卷 docker volume prune -f # 4. 清理所有未使用资源 docker system prune -a -f # 5. 限制容器日志大小（daemon.json） { \u0026#34;log-driver\u0026#34;: \u0026#34;json-file\u0026#34;, \u0026#34;log-opts\u0026#34;: { \u0026#34;max-size\u0026#34;: \u0026#34;100m\u0026#34;, \u0026#34;max-file\u0026#34;: \u0026#34;3\u0026#34; } } # 6. 迁移Docker数据目录 systemctl stop docker rsync -avz /var/lib/docker /新路径/ 修改/etc/docker/daemon.json systemctl start docker 预防措施：\n配置日志轮转 定期清理无用资源 监控磁盘使用率（\u0026gt;80%告警） 使用独立分区挂载/var/lib/docker 4.2 容器OOM 事故现象：\n容器频繁重启 docker ps显示容器状态为Exited 日志中有\u0026quot;OOMKilled\u0026quot; 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 查看容器退出原因 docker inspect 容器ID | grep -i \u0026#34;oom\\|exit\u0026#34; # 2. 查看系统OOM日志 dmesg | grep -i \u0026#34;oom\\|killed\u0026#34; # 3. 查看容器内存限制 docker inspect 容器ID | grep -i \u0026#34;memory\u0026#34; # 4. 查看容器内存使用 docker stats 容器ID 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 # 1. 调整内存限制 docker run -m 512m --memory-reservation=256m 镜像名 # 2. 优化应用内存使用 # Java应用：-Xmx256m -Xms128m # Node应用：--max-old-space-size=256 # 3. 增加Swap（不推荐生产） docker run --memory=512m --memory-swap=1g 镜像名 # 4. 调整OOM优先级 docker run --oom-score-adj=-500 镜像名 预防措施：\n设置合理的内存限制 应用层做好内存管理 监控容器内存使用 配置自动扩容 4.3 镜像拉取失败 事故现象：\ndocker pull超时或失败 Pod状态为ImagePullBackOff 网络错误 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 测试网络连通性 ping registry.docker.com # 2. 测试DNS解析 nslookup registry.docker.com # 3. 手动拉取测试 docker pull 镜像名:标签 # 4. 查看Docker日志 journalctl -u docker -n 100 # 5. 查看代理配置 env | grep -i proxy 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 1. 配置镜像加速器（国内） cat \u0026gt; /etc/docker/daemon.json \u0026lt;\u0026lt; EOF { \u0026#34;registry-mirrors\u0026#34;: [ \u0026#34;https://registry.cn-hangzhou.aliyuncs.com\u0026#34;, \u0026#34;https://mirror.ccs.tencentyun.com\u0026#34; ] } EOF systemctl daemon-reload systemctl restart docker # 2. 配置代理 mkdir -p /etc/systemd/system/docker.service.d cat \u0026gt; /etc/systemd/system/docker.service.d/http-proxy.conf \u0026lt;\u0026lt; EOF [Service] Environment=\u0026#34;HTTP_PROXY=http://代理IP:端口\u0026#34; Environment=\u0026#34;HTTPS_PROXY=http://代理IP:端口\u0026#34; EOF systemctl daemon-reload systemctl restart docker # 3. 使用私有仓库 docker login 私有仓库地址 docker pull 私有仓库/镜像名 4.4 容器频繁重启 事故现象：\ndocker ps -a显示多次重启 服务不稳定 日志中有大量重启记录 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 查看容器重启次数 docker inspect 容器ID | grep -i \u0026#34;restart\u0026#34; # 2. 查看容器日志 docker logs --tail 200 容器ID # 3. 查看容器资源 docker stats 容器ID # 4. 查看容器健康检查 docker inspect 容器ID | grep -A 10 \u0026#34;Health\u0026#34; 常见原因及解决：\n原因 排查方法 解决方案 应用崩溃 查看日志 修复代码/配置 资源不足 docker stats 增加资源限制 端口冲突 netstat -tuln 修改端口映射 依赖未就绪 检查启动顺序 添加等待逻辑 健康检查失败 docker inspect 调整检查参数 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 调整重启策略 docker update --restart=on-failure:3 容器ID # 2. 添加健康检查 docker run --health-cmd \u0026#34;curl -f http://localhost:8080\u0026#34; \\ --health-interval 30s \\ --health-timeout 10s \\ --health-retries 3 镜像名 # 3. 添加启动等待 # 使用wait-for-it.sh等待依赖服务 4.5 网络隔离问题 事故现象：\n容器间无法通信 容器无法访问外网 跨主机通信失败 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 查看网络列表 docker network ls # 2. 查看网络详情 docker network inspect 网络名 # 3. 查看容器IP docker inspect 容器ID | grep IPAddress # 4. 测试容器间连通性 docker exec 容器1 ping 容器2IP # 5. 查看iptables规则 iptables -L -n | grep docker 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 创建自定义网络 docker network create --driver bridge mynet # 2. 将容器加入同一网络 docker network connect mynet 容器ID # 3. 检查防火墙 systemctl status firewalld firewall-cmd --list-all # 4. 重启Docker网络 systemctl restart docker # 5. 跨主机网络（使用overlay） docker network create --driver overlay --attachable myoverlay 第五部分：Kubernetes生产事故 5.1 Pod Pending 事故现象：\nkubectl get pods显示Pending Pod长时间无法启动 调度失败 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 查看Pod详细状态 kubectl describe pod Pod名 -n 命名空间 # 2. 查看节点资源 kubectl describe node 节点名 # 3. 查看节点状态 kubectl get nodes -o wide # 4. 查看调度事件 kubectl get events -n 命名空间 --sort-by=\u0026#39;.lastTimestamp\u0026#39; # 5. 查看资源配额 kubectl describe quota -n 命名空间 常见原因及解决：\n原因 现象 解决方案 资源不足 Insufficient cpu/memory 扩容节点或调整request 节点污点 Taints not tolerated 添加toleration 节点选择器 NodeSelector不匹配 修改nodeSelector 亲和性 Affinity不满足 调整affinity配置 PVC未绑定 Volume pending 检查StorageClass 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 1. 查看调度失败原因 kubectl describe pod Pod名 | grep -A 5 \u0026#34;Events\u0026#34; # 2. 临时调整资源 kubectl set resources deployment/服务名 \\ --requests=cpu=100m,memory=128Mi \\ --limits=cpu=500m,memory=512Mi # 3. 查看节点可调度资源 kubectl top nodes # 4. 驱逐低优先级Pod kubectl drain 节点名 --ignore-daemonsets --delete-local-data 5.2 Pod CrashLoopBackOff 事故现象：\nPod状态为CrashLoopBackOff 容器反复重启 RESTARTS计数持续增长 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 查看Pod状态 kubectl get pods -n 命名空间 # 2. 查看Pod详情 kubectl describe pod Pod名 -n 命名空间 # 3. 查看容器日志 kubectl logs Pod名 -n 命名空间 kubectl logs Pod名 -n 命名空间 --previous # 查看上次崩溃日志 # 4. 进入容器调试 kubectl exec -it Pod名 -n 命名空间 -- /bin/bash # 5. 查看事件 kubectl get events -n 命名空间 --field-selector involvedObject.name=Pod名 常见原因及解决：\n原因 排查方法 解决方案 应用启动失败 查看日志 修复配置/代码 探针失败 describe查看Events 调整探针参数 配置错误 检查ConfigMap 修正配置 依赖服务不可用 检查网络连接 等待依赖就绪 资源不足 查看OOM 增加资源限制 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 调整存活探针 kubectl edit deployment/服务名 -n 命名空间 # 修改： # initialDelaySeconds: 30 # 延长初始延迟 # failureThreshold: 5 # 增加失败阈值 # periodSeconds: 10 # 延长检查间隔 # 2. 回滚到稳定版本 kubectl rollout undo deployment/服务名 -n 命名空间 # 3. 删除Pod让K8s重建 kubectl delete pod Pod名 -n 命名空间 # 4. 临时禁用探针（调试用） kubectl edit deployment/服务名 -n 命名空间 # 注释掉livenessProbe和readinessProbe 5.3 CNI网络异常 事故现象：\nPod无法跨节点通信 Service无法访问 网络插件Pod异常 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 查看CNI Pod状态 kubectl get pods -n kube-system | grep -E \u0026#34;flannel|calico|weave\u0026#34; # 2. 查看CNI日志 kubectl logs -n kube-system CNI-Pod名 # 3. 测试Pod间连通性 kubectl exec Pod1 -- ping Pod2IP # 4. 查看网络策略 kubectl get networkpolicy -A # 5. 查看iptables规则 iptables -L -n | grep -E \u0026#34;KUBE|CALICO\u0026#34; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 1. 重启CNI Pod kubectl delete pod -n kube-system -l k8s-app=calico-node # 2. 重新安装CNI kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml # 3. 检查节点网络 ip addr show ip route show # 4. 清理残留网络配置 # 谨慎操作，可能需要重启节点 rm -rf /var/lib/cni/* 5.4 etcd崩溃 事故现象：\nkubectl命令无响应或超时 API Server报错 集群无法调度 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 1. 查看etcd Pod状态 kubectl get pods -n kube-system | grep etcd # 2. 查看etcd日志 kubectl logs -n kube-system etcd-节点名 # 3. 检查etcd健康 ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \\ --cacert=/etc/kubernetes/pki/etcd/ca.crt \\ --cert=/etc/kubernetes/pki/etcd/healthcheck-client.crt \\ --key=/etc/kubernetes/pki/etcd/healthcheck-client.key \\ endpoint health # 4. 查看etcd成员 ETCDCTL_API=3 etcdctl member list # 5. 查看etcd数据大小 ETCDCTL_API=3 etcdctl --write-out=table endpoint status 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 1. 重启etcd kubectl delete pod -n kube-system etcd-节点名 # 2. 从备份恢复 ETCDCTL_API=3 etcdctl snapshot restore 备份文件 \\ --data-dir=/var/lib/etcd-restore # 3. 清理过期数据 ETCDCTL_API=3 etcdctl compaction --revision=修订版本 ETCDCTL_API=3 etcdctl defrag # 4. 扩容etcd集群 # 添加新成员，确保奇数节点 预防措施：\n定期备份etcd数据 监控etcd延迟和存储大小 保持3/5/7奇数节点 使用SSD存储 5.5 kubelet失联 事故现象：\n节点状态为NotReady Pod无法调度到该节点 节点上的Pod状态异常 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 查看节点状态 kubectl get nodes kubectl describe node 节点名 # 2. 查看kubelet状态 systemctl status kubelet # 3. 查看kubelet日志 journalctl -u kubelet -n 200 # 4. 检查证书 ls -la /etc/kubernetes/pki/kubelet* openssl x509 -in /etc/kubernetes/pki/kubelet.crt -text -noout # 5. 检查配置文件 cat /etc/kubernetes/kubelet.conf 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 重启kubelet systemctl restart kubelet # 2. 更新证书 # 证书过期需要重新签发 kubeadm certs renew kubelet-client-current systemctl restart kubelet # 3. 检查资源 free -h df -h # 4. 重新加入集群 kubeadm reset kubeadm join 控制节点IP:6443 --token XXX --discovery-token-ca-cert-hash XXX 5.6 Service访问失败 事故现象：\nClusterIP无法访问 NodePort不通 LoadBalancer不分配IP 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 重启kubelet systemctl restart kubelet # 2. 更新证书 # 证书过期需要重新签发 kubeadm certs renew kubelet-client-current systemctl restart kubelet # 3. 检查资源 free -h df -h # 4. 重新加入集群 kubeadm reset kubeadm join 控制节点IP:6443 --token XXX --discovery-token-ca-cert-hash XXX 常见原因及解决：\n原因 排查方法 解决方案 标签不匹配 对比Pod和Service标签 修正labelSelector 端口不匹配 检查targetPort 修正端口配置 Endpoints为空 kubectl get endpoints 检查Pod就绪状态 kube-proxy异常 查看kube-proxy日志 重启kube-proxy 网络策略限制 kubectl get networkpolicy 调整网络策略 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 修正标签选择器 kubectl edit svc 服务名 -n 命名空间 # 2. 重启kube-proxy kubectl delete pod -n kube-system -l k8s-app=kube-proxy # 3. 检查iptables规则 iptables -L -n | grep 服务IP # 4. 使用headless Service调试 # 将clusterIP改为None，直接访问Pod IP 第六部分：数据库事故 6.1 MySQL锁表 事故现象：\n查询超时 应用报\u0026quot;Lock wait timeout\u0026quot; 数据库响应慢 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 -- 1. 查看锁等待 SELECT * FROM information_schema.INNODB_LOCK_WAITS; -- 2. 查看正在运行的事务 SELECT * FROM information_schema.INNODB_TRX; -- 3. 查看锁信息 SELECT * FROM performance_schema.data_locks; -- 4. 查看进程列表 SHOW PROCESSLIST; -- 5. 查看表锁 SHOW OPEN TABLES WHERE In_use \u0026gt; 0; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 -- 1. 终止阻塞的会话 KILL 进程ID; -- 2. 查看并终止长时间运行的事务 SELECT CONCAT(\u0026#39;KILL \u0026#39;, id, \u0026#39;;\u0026#39;) FROM information_schema.processlist WHERE command = \u0026#39;Sleep\u0026#39; AND time \u0026gt; 300; -- 3. 优化锁等待超时 SET GLOBAL innodb_lock_wait_timeout = 50; -- 4. 查看锁等待图 SELECT blocking_locks.lock_id AS blocked_lock_id, blocking_locks.lock_mode AS blocked_lock_mode, blocking_trx.trx_id AS blocked_trx_id, blocking_trx.trx_query AS blocked_query FROM performance_schema.data_lock_waits JOIN performance_schema.data_locks blocking_locks ON data_lock_waits.blocking_engine_lock_id = blocking_locks.engine_lock_id JOIN performance_schema.innodb_trx blocking_trx ON blocking_locks.engine_transaction_id = blocking_trx.trx_id; 预防措施：\n避免长事务 合理设计索引 使用合适的隔离级别 定期分析慢查询 6.2 主从延迟 事故现象：\n从库查询数据不一致 应用读写分离异常 监控显示延迟增长 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- 1. 查看主从状态 SHOW SLAVE STATUS\\G -- 关键字段： -- Slave_IO_Running: Yes/No -- Slave_SQL_Running: Yes/No -- Seconds_Behind_Master: 延迟秒数 -- Last_Error: 错误信息 -- 2. 查看主库位置 SHOW MASTER STATUS\\G -- 3. 查看从库位置 SHOW SLAVE STATUS\\G -- Relay_Log_Pos, Exec_Master_Log_Pos -- 4. 查看复制线程 SHOW PROCESSLIST; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- 1. 跳过错误（谨慎使用） STOP SLAVE; SET GLOBAL sql_slave_skip_counter = 1; START SLAVE; -- 2. 并行复制优化（MySQL 5.7+） SET GLOBAL slave_parallel_workers = 4; SET GLOBAL slave_parallel_type = \u0026#39;LOGICAL_CLOCK\u0026#39;; -- 3. 优化从库配置 -- my.cnf [mysqld] slave_parallel_workers = 4 binlog_format = ROW slave_preserve_commit_order = ON -- 4. 重新同步（最后手段） -- 从主库重新导数据 预防措施：\n使用行级复制（binlog_format=ROW） 开启并行复制 避免从库执行大事务 监控延迟告警（\u0026gt;30秒） 6.3 慢SQL拖垮系统 事故现象：\nCPU 100% 连接数爆满 响应时间增长 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -- 1. 开启慢查询日志 SET GLOBAL slow_query_log = \u0026#39;ON\u0026#39;; SET GLOBAL long_query_time = 1; -- 2. 查看慢查询 SELECT * FROM mysql.slow_log; -- 3. 查看当前运行查询 SHOW FULL PROCESSLIST; -- 4. 查看查询执行计划 EXPLAIN SELECT * FROM 表 WHERE 条件; -- 5. 查看索引使用情况 SELECT * FROM sys.schema_unused_indexes; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 -- 1. 终止慢查询 KILL 进程ID; -- 2. 添加索引 ALTER TABLE 表名 ADD INDEX 索引名 (字段名); -- 3. 优化查询 -- 避免SELECT * -- 避免在索引列上使用函数 -- 避免LIKE \u0026#39;%前缀\u0026#39; -- 4. 限制查询资源 SET GLOBAL max_execution_time = 30000; -- 30秒超时 -- 5. 使用查询缓存（MySQL 5.7） SET GLOBAL query_cache_size = 64M; 预防措施：\n代码审查SQL 上线前EXPLAIN分析 慢查询监控告警 定期优化索引 6.4 连接池耗尽 事故现象：\n应用报\u0026quot;Cannot get connection\u0026quot; 数据库连接数达到上限 新请求无法获取连接 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -- 1. 查看当前连接数 SHOW STATUS LIKE \u0026#39;Threads_connected\u0026#39;; -- 2. 查看最大连接数 SHOW VARIABLES LIKE \u0026#39;max_connections\u0026#39;; -- 3. 查看各用户连接数 SELECT user, host, COUNT(*) FROM information_schema.processlist GROUP BY user, host; -- 4. 查看连接状态 SHOW STATUS LIKE \u0026#39;Connections\u0026#39;; SHOW STATUS LIKE \u0026#39;Aborted_connects\u0026#39;; -- 5. 应用层查看连接池 -- 根据连接池类型查看监控 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- 1. 临时增加连接数 SET GLOBAL max_connections = 500; -- 2. 终止空闲连接 SELECT CONCAT(\u0026#39;KILL \u0026#39;, id, \u0026#39;;\u0026#39;) FROM information_schema.processlist WHERE command = \u0026#39;Sleep\u0026#39; AND time \u0026gt; 300; -- 3. 调整应用连接池配置 # 示例（HikariCP） spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.idle-timeout=600000 -- 4. 优化查询减少连接占用 -- 减少长事务 -- 及时关闭连接 预防措施：\n合理设置连接池大小 监控连接使用率 设置连接超时 使用连接池监控 6.5 binlog爆仓 事故现象：\n磁盘空间耗尽 数据库无法写入 主从复制中断 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 -- 1. 查看binlog列表 SHOW BINARY LOGS; -- 2. 查看binlog大小 SELECT SUM(file_size) FROM (SHOW BINARY LOGS) AS logs; -- 3. 查看binlog配置 SHOW VARIABLES LIKE \u0026#39;binlog%\u0026#39;; -- 4. 查看当前binlog SHOW MASTER STATUS; -- 5. 系统层查看 du -sh /var/lib/mysql/*.bin 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 -- 1. 清理旧binlog PURGE BINARY LOGS BEFORE \u0026#39;2024-01-01 00:00:00\u0026#39;; PURGE BINARY LOGS TO \u0026#39;mysql-bin.000100\u0026#39;; -- 2. 调整binlog保留策略 -- my.cnf [mysqld] binlog_expire_logs_seconds = 604800 # 7天 max_binlog_size = 1073741824 # 1GB -- 3. 临时关闭binlog（谨慎） SET SESSION sql_log_bin = 0; -- 4. 从库跳过binlog -- 从库不需要binlog可关闭 [mysqld] skip-log-bin 预防措施：\n设置合理的过期时间 监控binlog大小 定期清理 独立分区存储binlog 第七部分：缓存事故 7.1 Redis雪崩 事故现象：\n大量缓存同时过期 数据库压力激增 系统响应变慢或崩溃 原因分析：\n同一时间大量key过期 Redis服务宕机 缓存未命中穿透到数据库 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 随机过期时间 import random expire_time = base_expire + random.randint(0, 300) # 基础时间+随机0-5分钟 # 2. 热点数据永不过期 # 对核心数据设置较长过期时间或永不过期 # 3. 限流降级 # 使用令牌桶或信号量限制并发查询 # 4. 多级缓存 # 本地缓存 + Redis + 数据库 # 5. 缓存预热 # 系统启动时预加载热点数据 预防措施：\n设置随机过期时间 热点数据单独处理 监控缓存命中率 配置熔断降级 7.2 Redis击穿 事故现象：\n单个热点key过期 大量请求同时访问该key 数据库瞬间压力增大 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 1. 互斥锁 def get_data(key): data = redis.get(key) if data is None: if redis.setnx(f\u0026#34;lock:{key}\u0026#34;, 1): try: data = db.query(key) redis.set(key, data, expire=300) finally: redis.delete(f\u0026#34;lock:{key}\u0026#34;) else: time.sleep(0.1) return get_data(key) return data # 2. 逻辑过期 # 不设置物理过期，在value中存储过期时间 # 异步更新缓存 # 3. 永不过期 + 异步更新 # 热点数据永不过期，后台定时更新 7.3 Redis穿透 事故现象：\n查询不存在的数据 请求直接打到数据库 恶意攻击场景 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 1. 缓存空值 def get_data(key): data = redis.get(key) if data == \u0026#34;NULL\u0026#34;: # 特殊标记 return None if data is None: data = db.query(key) if data is None: redis.setex(key, 60, \u0026#34;NULL\u0026#34;) # 缓存空值60秒 return None redis.setex(key, 300, data) return data # 2. 布隆过滤器 from pybloom import BloomFilter bf = BloomFilter(capacity=1000000, error_rate=0.001) def query(key): if key not in bf: # 肯定不存在 return None # 可能存在，继续查询 # 3. 参数校验 # 对请求参数进行合法性校验 7.4 Redis主从切换失败 事故现象：\n主节点宕机 从节点未自动提升 服务不可用 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 # 1. 查看Redis节点状态 redis-cli -h 主节点IP INFO replication # 2. 查看Sentinel状态 redis-cli -h SentinelIP INFO sentinel # 3. 查看Sentinel监控 redis-cli -h SentinelIP SENTINEL masters redis-cli -h SentinelIP SENTINEL slaves 主节点名 # 4. 测试连接 redis-cli -h 节点IP ping 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 手动提升从节点 redis-cli -h 从节点IP SLAVEOF NO ONE # 2. 检查Sentinel配置 # sentinel.conf sentinel monitor mymaster 主IP 6379 2 sentinel down-after-milliseconds mymaster 5000 sentinel failover-timeout mymaster 60000 sentinel parallel-syncs mymaster 1 # 3. 重启Sentinel systemctl restart redis-sentinel # 4. 检查网络连通性 ping 节点IP telnet 节点IP 26379 预防措施：\n配置至少3个Sentinel 设置合理的超时时间 定期演练故障切换 监控主从状态 第八部分：消息队列事故 8.1 Kafka积压 事故现象：\n消费延迟持续增长 Lag值不断增大 消息处理不及时 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 1. 查看消费组状态 kafka-consumer-groups.sh --bootstrap-server broker:9092 --describe --group 消费组名 # 关键字段： # LAG: 积压消息数 # CURRENT-OFFSET: 当前消费位置 # LOG-END-OFFSET: 最新消息位置 # 2. 查看Topic详情 kafka-topics.sh --bootstrap-server broker:9092 --describe --topic Topic名 # 3. 查看消费者延迟 kafka-consumer-groups.sh --bootstrap-server broker:9092 \\ --describe --group 消费组名 --verbose # 4. 查看Broker状态 kafka-broker-api-versions.sh --bootstrap-server broker:9092 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 1. 增加消费者实例 # 扩展消费者组，增加并行度 # 2. 增加Partition kafka-topics.sh --bootstrap-server broker:9092 \\ --alter --topic Topic名 --partitions 新数量 # 3. 优化消费逻辑 # 批量处理消息 # 异步处理非关键逻辑 # 4. 临时跳过消息（谨慎） # 重置消费位点 kafka-consumer-groups.sh --bootstrap-server broker:9092 \\ --group 消费组名 --topic Topic名 --reset-offsets --to-latest --execute # 5. 扩容Broker # 增加Broker节点，重新分配Partition 预防措施：\n监控Lag指标（\u0026gt;10000告警） 合理设置Partition数量 消费者自动扩缩容 设置消息保留策略 8.2 RabbitMQ堆积 事故现象：\n队列消息数持续增长 消费者处理不过来 内存/磁盘使用率增长 排查命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 查看队列状态 rabbitmqctl list_queues name messages consumers # 2. 查看队列详情 rabbitmqctl list_queues name messages_ready messages_unacknowledged # 3. 查看连接 rabbitmqctl list_connections # 4. 查看通道 rabbitmqctl list_channels # 5. 管理界面 # http://MQ_IP:15672 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 增加消费者 # 启动更多消费者实例 # 2. 调整prefetch_count # 消费者配置：channel.basic_qos(prefetch_count=10) # 3. 清理积压消息 # 创建临时队列，消费后丢弃 rabbitmqadmin purge queue 队列名 # 4. 扩容集群 # 增加RabbitMQ节点 # 5. 优化消息处理 # 批量确认 # 异步处理 预防措施：\n监控队列长度 设置队列最大长度 配置死信队列 定期清理无用队列 8.3 消息重复消费 事故现象：\n业务数据重复 订单重复创建 金额重复扣减 原因分析：\n消费者处理完未确认 网络抖动导致重投 消费者重启 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 1. 幂等性设计 def process_message(msg_id, data): # 检查是否已处理 if redis.exists(f\u0026#34;processed:{msg_id}\u0026#34;): return # 处理业务 do_something(data) # 标记已处理 redis.setex(f\u0026#34;processed:{msg_id}\u0026#34;, 86400, 1) # 2. 数据库唯一约束 # 使用业务主键作为唯一索引 # 3. 状态机控制 # 订单状态：待支付-\u0026gt;已支付，不允许重复支付 # 4. 消息去重表 CREATE TABLE message_dedup ( msg_id VARCHAR(64) PRIMARY KEY, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); 预防措施：\n业务层实现幂等 消息ID全局唯一 记录已处理消息 定期清理去重表 第九部分：真实互联网事故复盘（100个） 事故1：GitLab数据库误删事故（2017） 事故现象：\nGitLab.com服务完全不可用 6小时数据丢失 30万项目受影响 事故原因：\n运维人员误执行rm -rf删除生产数据库目录 备份系统失效（5个备份机制全部失败） 权限管理不当（生产环境可直接删除） 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 发现服务不可用 curl -I https://gitlab.com # 返回500 # 2. 检查数据库状态 systemctl status postgresql # 服务停止 # 3. 检查数据目录 ls -la /var/opt/gitlab/postgresql/data # 目录为空 # 4. 尝试恢复备份 # 发现所有备份均不可用 使用命令：\n1 2 3 4 5 6 7 # 检查备份状态 ls -la /backup/gitlab/ pg_restore -l 备份文件 # 检查磁盘 df -h lsof | grep deleted 解决方案：\n从最近的可用备份恢复（损失6小时数据） 手动重建缺失数据 逐步恢复服务 预防措施：\n实施最小权限原则 多重备份验证机制 生产环境操作审批流程 定期备份恢复演练 经验总结：\n备份必须定期验证可用性 生产环境禁止直接操作 关键操作需要双人复核 事故2：AWS S3全球宕机（2017） 事故现象：\nS3服务中断4小时 大量依赖S3的服务不可用 估计损失1.5亿美元 事故原因：\n运维人员调试计费系统时输入错误命令 误删除了更多服务器而非预期 S3索引系统级联故障 排查过程：\n1 2 3 4 5 6 7 8 9 10 # 1. 监控告警 S3请求错误率飙升 API响应时间超时 # 2. 日志分析 AWS CloudTrail审计日志 S3访问日志 # 3. 系统状态检查 检查S3各组件健康状态 解决方案：\n隔离故障组件 重建索引系统 逐步恢复服务 预防措施：\n命令执行前验证 限制批量操作权限 实施变更冻结窗口 自动化安全检测 经验总结：\n自动化脚本需要安全限制 关键系统需要隔离保护 故障恢复需要优先级排序 事故3：Facebook BGP事故（2021） 事故现象：\nFacebook、Instagram、WhatsApp全球中断6小时 DNS无法解析 内部系统无法访问 事故原因：\nBGP路由更新错误 所有Facebook IP从互联网路由表撤回 内部DNS依赖外部网络 排查过程：\n1 2 3 4 5 6 7 8 9 10 # 1. 外部检测 ping facebook.com # 无法解析 traceroute facebook.com # 路由中断 # 2. BGP检查 show ip bgp summary show ip route facebook网段 # 3. DNS检查 nslookup facebook.com # 无响应 解决方案：\n工程师物理进入数据中心 手动修复BGP配置 恢复路由广播 逐步恢复服务 预防措施：\nBGP变更需要多重审批 内部系统不依赖外部网络 建立带外管理通道 定期BGP配置审计 经验总结：\n网络变更风险极高 需要带外管理通道 DNS架构需要冗余设计 事故4：Cloudflare CPU 100%事故 事故现象：\nCloudflare全球服务性能下降 网站访问变慢 API响应超时 事故原因：\nWAF规则更新引入正则表达式灾难性回溯 单个请求消耗大量CPU 级联影响所有边缘节点 排查过程：\n1 2 3 4 5 6 7 8 9 10 # 1. 监控发现 CPU使用率告警 请求延迟增长 # 2. 日志分析 grep \u0026#34;WAF\u0026#34; /var/log/cloudflare/ 分析高CPU请求特征 # 3. 性能分析 perf top # 查看热点函数 解决方案：\n回滚WAF规则 优化正则表达式 添加请求限制 逐步恢复 预防措施：\n规则变更前性能测试 正则表达式复杂度限制 灰度发布机制 实时监控CPU使用 经验总结：\n正则表达式可能成为DoS向量 变更需要性能基准测试 需要快速回滚机制 事故5：GitHub数据库复制事故 事故现象：\nGitHub服务中断24秒 部分用户看到旧数据 写入操作失败 事故原因：\n数据库主从切换触发 复制延迟导致数据不一致 应用层未正确处理切换 排查过程：\n1 2 3 4 5 6 7 8 -- 1. 检查复制状态 SHOW SLAVE STATUS\\G -- 2. 检查延迟 SELECT TIMESTAMPDIFF(SECOND, last_executed_time, NOW()); -- 3. 检查连接 SHOW PROCESSLIST; 解决方案：\n等待复制完成 验证数据一致性 恢复服务 修复应用重试逻辑 预防措施：\n监控复制延迟 应用层实现重试机制 读写分离容错设计 定期故障演练 经验总结：\n数据库切换需要应用配合 复制延迟需要监控 短暂中断也需要复盘 事故6：Slack数据库过载 事故现象：\nSlack消息发送失败 频道加载缓慢 部分功能不可用 事故原因：\n数据库连接池耗尽 慢查询拖垮数据库 级联故障影响所有服务 排查过程：\n1 2 3 4 5 6 7 8 -- 1. 检查连接数 SHOW STATUS LIKE \u0026#39;Threads_connected\u0026#39;; -- 2. 检查慢查询 SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 10; -- 3. 检查锁等待 SELECT * FROM information_schema.innodb_lock_waits; 解决方案：\n终止慢查询 增加数据库连接 优化问题SQL 实施限流 预防措施：\n连接池监控告警 慢查询自动分析 SQL上线审查 服务降级机制 经验总结：\n数据库是单点故障高发区 连接池需要合理配置 慢查询需要主动发现 事故7：Zoom DNS故障 事故现象：\nZoom会议无法加入 域名解析失败 新用户无法注册 事故原因：\nDNS配置错误 证书更新导致DNS记录失效 缓存传播延迟 排查过程：\n1 2 3 4 5 6 7 8 9 # 1. 测试DNS解析 nslookup zoom.us dig zoom.us # 2. 检查DNS配置 cat /etc/resolv.conf # 3. 检查证书 openssl s_client -connect zoom.us:443 解决方案：\n修正DNS记录 清除DNS缓存 等待全球传播 通知用户 预防措施：\nDNS变更提前通知 多DNS提供商冗余 TTL设置合理 变更验证流程 经验总结：\nDNS变更影响范围广 需要足够的传播时间 多提供商提高可用性 事故8：Kubernetes etcd崩溃 事故现象：\nkubectl命令无响应 Pod无法调度 集群管理功能失效 事故原因：\netcd磁盘IO延迟高 Leader选举频繁 集群无法达成共识 排查过程：\n1 2 3 4 5 6 7 8 # 1. 检查etcd状态 ETCDCTL_API=3 etcdctl endpoint health # 2. 检查延迟 ETCDCTL_API=3 etcdctl endpoint status --write-out=table # 3. 检查磁盘 iostat -x 1 解决方案：\n更换SSD磁盘 优化etcd配置 减少etcd负载 增加etcd节点 预防措施：\netcd使用SSD 监控etcd延迟 定期备份 限制etcd存储大小 经验总结：\netcd对磁盘IO敏感 需要专用存储 定期维护很重要 事故9：ELK日志拖垮生产 事故现象：\n生产服务响应变慢 磁盘空间耗尽 内存使用率飙升 事故原因：\n日志量激增（异常循环打印） Elasticsearch索引过多 Logstash处理不过来 排查过程：\n1 2 3 4 5 6 7 8 9 # 1. 检查磁盘 df -h du -sh /var/log/* # 2. 检查ES集群 curl localhost:9200/_cluster/health # 3. 检查日志量 wc -l /var/log/app/*.log 解决方案：\n修复异常日志 清理旧索引 限制日志级别 增加ES节点 预防措施：\n日志级别动态调整 索引生命周期管理 日志量监控告警 采样记录日志 经验总结：\n日志系统可能成为故障源 需要限制日志量 定期清理很重要 事故10：CDN缓存雪崩 事故现象：\n网站访问极慢 源站压力激增 图片/静态资源加载失败 事故原因：\nCDN证书过期 缓存同时失效 大量请求打到源站 排查过程：\n1 2 3 4 5 6 7 8 9 # 1. 检查CDN状态 curl -I https://cdn.域名/资源 # 2. 检查证书 openssl s_client -connect cdn.域名:443 # 3. 检查源站 top netstat -an | grep ESTABLISHED | wc -l 解决方案：\n更新CDN证书 源站限流保护 启用备用CDN 预热缓存 预防措施：\n证书过期监控 多CDN冗余 缓存预热机制 源站保护策略 经验总结：\nCDN不是完全可靠 需要多供应商 证书管理要自动化 事故11：TLS证书过期事故 事故现象：\n用户访问HTTPS网站显示\u0026quot;证书已过期\u0026quot;警告 移动端APP无法连接API 浏览器显示红色安全警告 业务流量下降80% 事故原因：\nSSL证书有效期1年，到期未续费 证书管理依赖人工记忆，无自动化监控 运维人员离职交接遗漏 证书部署在多台服务器，部分遗漏更新 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # 1. 用户反馈后验证证书状态 openssl s_client -connect www.example.com:443 \u0026lt;/dev/null 2\u0026gt;/dev/null | openssl x509 -noout -dates # 输出显示： # notBefore=Jan 15 00:00:00 2024 GMT # notAfter=Jan 15 23:59:59 2025 GMT # 已过期 # 2. 检查所有服务器证书 for server in web1 web2 web3 web4; do echo \u0026#34;=== $server ===\u0026#34; openssl s_client -connect $server:443 \u0026lt;/dev/null 2\u0026gt;/dev/null | \\ openssl x509 -noout -enddate done # 3. 查找证书文件位置 find /etc -name \u0026#34;*.crt\u0026#34; -o -name \u0026#34;*.pem\u0026#34; 2\u0026gt;/dev/null find /opt -name \u0026#34;*.crt\u0026#34; -o -name \u0026#34;*.pem\u0026#34; 2\u0026gt;/dev/null # 4. 检查证书到期时间 for cert in $(find /etc/ssl -name \u0026#34;*.crt\u0026#34; 2\u0026gt;/dev/null); do echo \u0026#34;=== $cert ===\u0026#34; openssl x509 -in $cert -noout -enddate done # 5. 检查Nginx配置 grep -r \u0026#34;ssl_certificate\u0026#34; /etc/nginx/ # 6. 查看证书剩余天数 openssl x509 -in /etc/ssl/certs/server.crt -noout -checkend 86400 # 返回1表示24小时内过期，返回0表示未过期 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 检查证书详细信息 openssl x509 -in certificate.crt -text -noout # 检查证书链 openssl verify -CAfile ca-bundle.crt server.crt # 在线检查证书 curl -vI https://www.example.com 2\u0026gt;\u0026amp;1 | grep -i \u0026#34;expire\\|subject\u0026#34; # 监控脚本示例 #!/bin/bash CERT_FILE=\u0026#34;/etc/ssl/certs/server.crt\u0026#34; EXPIRE_DAYS=30 EXPIRE_DATE=$(openssl x509 -in $CERT_FILE -noout -enddate | cut -d= -f2) EXPIRE_EPOCH=$(date -d \u0026#34;$EXPIRE_DATE\u0026#34; +%s) CURRENT_EPOCH=$(date +%s) DAYS_LEFT=$(( ($EXPIRE_EPOCH - $CURRENT_EPOCH) / 86400 )) if [ $DAYS_LEFT -lt $EXPIRE_DAYS ]; then echo \u0026#34;WARNING: Certificate expires in $DAYS_LEFT days\u0026#34; # 发送告警 curl -X POST -d \u0026#34;证书将在$DAYS_LEFT天后过期\u0026#34; http://alert-server/webhook fi 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 1. 紧急申请新证书（Let\u0026#39;s Encrypt） certbot certonly --webroot -w /var/www/html -d www.example.com # 2. 替换证书文件 cp /etc/letsencrypt/live/www.example.com/fullchain.pem /etc/ssl/certs/server.crt cp /etc/letsencrypt/live/www.example.com/privkey.pem /etc/ssl/private/server.key # 3. 重新加载Nginx nginx -t # 先测试配置 systemctl reload nginx # 4. 验证新证书 openssl s_client -connect www.example.com:443 \u0026lt;/dev/null 2\u0026gt;/dev/null | \\ openssl x509 -noout -dates # 5. 清理旧证书 rm -f /etc/ssl/certs/server.crt.old # 6. 重启相关服务 systemctl restart php-fpm systemctl restart tomcat 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 # 1. 配置自动续期（crontab） 0 0 1 * * /usr/bin/certbot renew --quiet \u0026amp;\u0026amp; systemctl reload nginx # 2. 添加证书监控（Prometheus） # 使用ssl_exporter监控证书到期 # 配置告警规则（Prometheus Alertmanager） groups: - name: certificate rules: - alert: SSLCertificateExpiringSoon expr: ssl_cert_expiry_days \u0026lt; 30 for: 1h labels: severity: warning annotations: summary: \u0026#34;SSL证书即将过期\u0026#34; description: \u0026#34;{{ $labels.instance }} 证书将在{{ $value }}天后过期\u0026#34; - alert: SSLCertificateExpired expr: ssl_cert_expiry_days \u0026lt; 0 for: 5m labels: severity: critical annotations: summary: \u0026#34;SSL证书已过期\u0026#34; description: \u0026#34;{{ $labels.instance }} 证书已过期\u0026#34; # 3. 证书管理台账 # 建立Excel/数据库记录所有证书信息 | 域名 | 证书类型 | 到期时间 | 负责人 | 部署位置 | |------|----------|----------|--------|----------| | www.example.com | DV | 2026-01-15 | 张三 | web1-4 | # 4. 使用证书管理服务 # 如：HashiCorp Vault、AWS ACM、阿里云SSL服务 事故12：Kafka消息堆积事故 事故现象：\n订单处理延迟从秒级增长到小时级 用户反馈下单后长时间未收到确认 监控显示Kafka Lag持续增长 消费者组积压消息超过1000万条 事故原因：\n消费者服务BUG导致处理速度下降90% 新增业务逻辑增加数据库写入，单条处理时间从10ms增至500ms 消费者实例数量未随业务量增长 缺少Lag监控告警机制 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # 1. 查看消费组状态 kafka-consumer-groups.sh --bootstrap-server kafka-broker:9092 \\ --describe --group order-consumer-group # 输出示例： # TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG # order-topic 0 1000000 5000000 4000000 # order-topic 1 1200000 5200000 4000000 # order-topic 2 1100000 5100000 4000000 # 2. 查看消费者日志 tail -f /var/log/order-consumer/app.log | grep -i \u0026#34;process\\|error\u0026#34; # 3. 检查消费者JVM状态 jps -l | grep order-consumer jstat -gc \u0026lt;PID\u0026gt; 1000 10 # 查看GC情况 jstack \u0026lt;PID\u0026gt; | grep -A 30 \u0026#34;RUNNABLE\u0026#34; # 查看线程状态 # 4. 查看Kafka Broker状态 kafka-broker-api-versions.sh --bootstrap-server kafka-broker:9092 # 5. 检查Topic配置 kafka-topics.sh --bootstrap-server kafka-broker:9092 \\ --describe --topic order-topic # 6. 查看消息生产速率 kafka-run-class.sh kafka.tools.JmxTool \\ --object-name kafka.server:type=BrokerTopicMetrics,name=MessagesInPerSec \\ --jmx-url service:jmx:rmi:///jndi/rmi://kafka-broker:9999/jmxrmi 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 持续监控Lag变化 watch -n 5 \u0026#39;kafka-consumer-groups.sh --bootstrap-server kafka-broker:9092 \\ --describe --group order-consumer-group | grep -v \u0026#34;^$\u0026#34;\u0026#39; # 查看消费者延迟趋势 kafka-consumer-groups.sh --bootstrap-server kafka-broker:9092 \\ --describe --group order-consumer-group --verbose # 检查Kafka磁盘使用 df -h /var/lib/kafka du -sh /var/lib/kafka/logs/* # 查看Kafka控制器状态 echo \u0026#34;dump\u0026#34; | nc kafka-broker 9092 | grep -i \u0026#34;controller\u0026#34; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # 1. 紧急扩容消费者实例 # 从3个实例扩展到10个实例 kubectl scale deployment order-consumer --replicas=10 # 2. 增加Topic Partition数量 kafka-topics.sh --bootstrap-server kafka-broker:9092 \\ --alter --topic order-topic --partitions 20 # 3. 优化消费者配置 # consumer.properties fetch.min.bytes=1048576 # 增加拉取批量 fetch.max.wait.ms=500 # 增加等待时间 max.poll.records=1000 # 增加单次拉取数量 max.partition.fetch.bytes=10485760 # 4. 临时跳过非关键消息（谨慎使用） kafka-consumer-groups.sh --bootstrap-server kafka-broker:9092 \\ --group order-consumer-group --topic order-topic \\ --reset-offsets --to-current --execute # 5. 优化业务逻辑 # 将同步写入改为异步批量写入 # 原代码：每条消息单独插入数据库 # 优化后：每100条批量插入 # 6. 增加数据库连接池 spring.datasource.hikari.maximum-pool-size=50 # 7. 启用消费者并行处理 # 使用线程池处理消息 ExecutorService executor = Executors.newFixedThreadPool(10); 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 # 1. 配置Lag监控告警（Prometheus + Kafka Exporter） # 告警规则 - alert: KafkaConsumerLagHigh expr: kafka_consumer_group_lag \u0026gt; 100000 for: 5m labels: severity: warning annotations: summary: \u0026#34;Kafka消费延迟过高\u0026#34; description: \u0026#34;消费组{{ $labels.group }} 延迟{{ $value }}条\u0026#34; - alert: KafkaConsumerLagCritical expr: kafka_consumer_group_lag \u0026gt; 1000000 for: 2m labels: severity: critical annotations: summary: \u0026#34;Kafka消费严重延迟\u0026#34; description: \u0026#34;消费组{{ $labels.group }} 延迟{{ $value }}条\u0026#34; # 2. 消费者自动扩缩容 # 基于Lag指标自动扩容 apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: order-consumer-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: order-consumer minReplicas: 3 maxReplicas: 20 metrics: - type: External external: metric: name: kafka_consumer_lag target: type: AverageValue averageValue: 10000 # 3. 代码层面优化 # 添加处理超时限制 @KafkaListener(topics = \u0026#34;order-topic\u0026#34;) public void listen(ConsumerRecord\u0026lt;String, String\u0026gt; record) { try { processWithTimeout(record, 5, TimeUnit.SECONDS); } catch (TimeoutException e) { // 发送死信队列 sendToDeadLetterQueue(record); } } # 4. 定期压测 # 模拟高峰流量验证消费者处理能力 经验总结：\nKafka Lag必须实时监控告警 消费者处理能力要预留50%余量 业务变更必须评估对消费速度的影响 建立消息积压应急预案（扩容、限流、降级） 重要消息需要死信队列机制 事故13：Nginx连接耗尽事故 事故现象：\n用户访问网站返回502 Bad Gateway Nginx错误日志显示\u0026quot;connect() failed (110: Connection timed out)\u0026quot; 新请求无法建立连接 监控显示Nginx活跃连接数达到上限 事故原因：\n后端服务响应变慢，连接占用时间增长 Nginx worker_connections配置过小（1024） 系统文件句柄限制过低 突发流量超过预期3倍 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 # 1. 查看Nginx状态 curl http://localhost/nginx_status # 输出： # Active connections: 1024 # server accepts handled requests # 100000 100000 500000 # Reading: 100 Writing: 900 Waiting: 24 # 2. 查看Nginx错误日志 tail -f /var/log/nginx/error.log | grep -i \u0026#34;connect\\|limit\u0026#34; # 典型错误： # 2024/01/15 10:30:00 [error] connect() failed (110: Connection timed out) # 2024/01/15 10:30:01 [emerg] worker_connections are not enough # 3. 检查Nginx配置 cat /etc/nginx/nginx.conf | grep -i \u0026#34;worker_connections\\|worker_processes\u0026#34; # 4. 查看系统文件句柄 ulimit -n cat /proc/sys/fs/file-nr # 5. 查看Nginx进程打开的文件数 ls /proc/$(cat /var/run/nginx.pid)/fd | wc -l # 6. 查看后端服务连接 netstat -an | grep :8080 | wc -l netstat -an | grep :8080 | grep ESTABLISHED | wc -l # 7. 查看TIME_WAIT连接 netstat -n | grep TIME_WAIT | wc -l # 8. 检查后端服务状态 systemctl status tomcat top -p $(pgrep -f java) 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 实时查看Nginx连接状态 watch -n 1 \u0026#39;curl -s http://localhost/nginx_status\u0026#39; # 查看各状态连接数 netstat -n | awk \u0026#39;/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}\u0026#39; # 查看Nginx进程资源 ps aux | grep nginx cat /proc/$(cat /var/run/nginx.pid)/limits # 抓包分析连接问题 tcpdump -i any port 80 -w nginx_capture.pcap # 分析慢请求 awk \u0026#39;{if($9 \u0026gt;= 500) print $0}\u0026#39; /var/log/nginx/access.log | head -20 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 # 1. 紧急调整Nginx配置 # /etc/nginx/nginx.conf worker_processes auto; events { worker_connections 65535; # 从1024增加到65535 multi_accept on; use epoll; } http { # 调整连接超时 keepalive_timeout 65; client_body_timeout 60; send_timeout 60; # 调整上游连接 upstream backend { server 127.0.0.1:8080; keepalive 100; # 长连接池 } server { location / { proxy_connect_timeout 60; proxy_send_timeout 60; proxy_read_timeout 60; proxy_http_version 1.1; proxy_set_header Connection \u0026#34;\u0026#34;; } } } # 2. 调整系统文件句柄限制 # /etc/security/limits.conf * soft nofile 65535 * hard nofile 65535 root soft nofile 65535 root hard nofile 65535 # /etc/sysctl.conf fs.file-max = 2097152 fs.nr_open = 2097152 # 应用配置 sysctl -p ulimit -n 65535 # 3. 优化TCP参数 # /etc/sysctl.conf net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 30 net.ipv4.ip_local_port_range = 1024 65535 net.core.somaxconn = 65535 net.ipv4.tcp_max_syn_backlog = 65535 sysctl -p # 4. 重启Nginx nginx -t systemctl reload nginx # 5. 扩容后端服务 kubectl scale deployment backend --replicas=5 # 6. 添加限流保护 # /etc/nginx/nginx.conf limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s; server { location / { limit_req zone=one burst=20 nodelay; } } 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 1. 配置连接数监控 # Prometheus Nginx Exporter # 告警规则 - alert: NginxActiveConnectionsHigh expr: nginx_connections_active \u0026gt; 5000 for: 2m labels: severity: warning annotations: summary: \u0026#34;Nginx活跃连接数过高\u0026#34; # 2. 压力测试 # 定期使用ab/wrk进行压测 ab -n 10000 -c 100 http://localhost/ # 3. 容量规划 # 根据业务量预估连接数，预留50%余量 # 公式：连接数 = QPS * 平均响应时间 * 1.5 # 4. 优雅降级 # 配置限流和熔断 location /api/ { limit_req zone=api burst=50; proxy_connect_timeout 5s; error_page 502 503 504 /50x.html; } 经验总结：\nworker_connections要根据业务量合理设置 系统文件句柄限制必须调整 后端服务响应时间直接影响连接占用 必须配置连接数监控告警 限流保护是最后一道防线 事故14：TIME_WAIT风暴事故 事故现象：\n服务器无法建立新连接 应用报\u0026quot;Cannot assign requested address\u0026quot; 端口耗尽，服务不可用 netstat显示大量TIME_WAIT状态 事故原因：\n短连接场景（如HTTP请求）频繁建立关闭 服务器主动关闭连接，产生大量TIME_WAIT 本地端口范围过小 TIME_WAIT等待时间过长（默认120秒） 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 # 1. 查看TIME_WAIT数量 netstat -n | grep TIME_WAIT | wc -l # 输出：25000（正常应\u0026lt;10000） # 2. 查看TIME_WAIT详情 netstat -n | grep TIME_WAIT | head -20 # 输出示例： # tcp 0 0 192.168.1.10:45000 10.0.0.1:80 TIME_WAIT # tcp 0 0 192.168.1.10:45001 10.0.0.1:80 TIME_WAIT # 3. 查看各状态连接分布 netstat -n | awk \u0026#39;/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}\u0026#39; # 输出： # TIME_WAIT 25000 # ESTABLISHED 500 # CLOSE_WAIT 100 # 4. 查看本地端口范围 cat /proc/sys/net/ipv4/ip_local_port_range # 输出：32768 61000（约28000个端口） # 5. 查看TIME_WAIT等待时间 cat /proc/sys/net/ipv4/tcp_fin_timeout # 输出：60（秒） # 6. 查看端口耗尽情况 netstat -n | grep 192.168.1.10 | awk \u0026#39;{print $4}\u0026#39; | \\ cut -d: -f2 | sort -n | uniq | wc -l # 7. 检查应用连接池配置 # 查看是否有连接复用 grep -r \u0026#34;keepalive\\|pool\u0026#34; /etc/application/ 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 持续监控TIME_WAIT变化 watch -n 5 \u0026#39;netstat -n | grep TIME_WAIT | wc -l\u0026#39; # 查看TIME_WAIT连接的目标分布 netstat -n | grep TIME_WAIT | awk \u0026#39;{print $5}\u0026#39; | \\ cut -d: -f1 | sort | uniq -c | sort -rn | head -10 # 查看端口使用情况 ss -s # 输出： # TCP: 30000 (estab 500, closed 29000, orphaned 0, synrecv 0, timewait 25000) # 检查是否有连接泄漏 lsof -i -n | grep -i wait 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 # 1. 开启TIME_WAIT复用（立即生效） echo 1 \u0026gt; /proc/sys/net/ipv4/tcp_tw_reuse # 2. 缩短TIME_WAIT等待时间 echo 30 \u0026gt; /proc/sys/net/ipv4/tcp_fin_timeout # 3. 扩大本地端口范围 echo \u0026#34;1024 65535\u0026#34; \u0026gt; /proc/sys/net/ipv4/ip_local_port_range # 4. 永久配置（/etc/sysctl.conf） cat \u0026gt;\u0026gt; /etc/sysctl.conf \u0026lt;\u0026lt; EOF net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 30 net.ipv4.ip_local_port_range = 1024 65535 net.ipv4.tcp_max_tw_buckets = 20000 EOF sysctl -p # 5. 应用层优化（使用长连接） # Python示例 import requests session = requests.Session() # 复用连接 for i in range(1000): session.get(\u0026#39;http://backend/api\u0026#39;) # Java示例（HttpClient） CloseableHttpClient httpClient = HttpClients.custom() .setMaxConnTotal(200) .setMaxConnPerRoute(50) .setConnectionTimeToLive(30, TimeUnit.SECONDS) .build(); # 6. Nginx配置长连接 upstream backend { server 127.0.0.1:8080; keepalive 100; # 保持100个长连接 } # 7. 数据库连接池 # 避免频繁创建数据库连接 spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=5 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 1. 监控TIME_WAIT数量 # Prometheus Node Exporter + 告警 - alert: TimeWaitConnectionsHigh expr: node_netstat_Tcp_TimeWait \u0026gt; 10000 for: 5m labels: severity: warning annotations: summary: \u0026#34;TIME_WAIT连接数过高\u0026#34; # 2. 应用连接池规范 # 所有外部调用必须使用连接池 # HTTP、数据库、Redis等 # 3. 架构优化 # 减少不必要的网络调用 # 使用本地缓存 # 批量处理减少请求次数 # 4. 定期巡检 # 每周检查连接状态 netstat -n | awk \u0026#39;/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}\u0026#39; 经验总结：\nTIME_WAIT是TCP正常状态，但过多会影响性能 开启tcp_tw_reuse是最有效的解决方案 应用层必须使用连接池 短连接场景要特别关注端口耗尽 监控要包含各TCP状态分布 事故15：文件句柄耗尽事故 事故现象：\n应用报\u0026quot;Too many open files\u0026quot; 无法创建新连接 日志无法写入 服务部分功能失效 事故原因：\n连接泄漏，打开的文件/Socket未关闭 系统文件句柄限制过低（默认1024） 日志文件未轮转，单个文件过大 进程打开大量小文件未释放 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # 1. 查看系统级文件句柄 cat /proc/sys/fs/file-max cat /proc/sys/fs/file-nr # 输出：100000 50000 100000（已用/空闲/最大） # 2. 查看进程级限制 ulimit -n # 输出：1024（过低） # 3. 查看具体进程打开的文件数 ls /proc/$(pgrep -f java)/fd | wc -l # 输出：5000 # 4. 找出打开文件最多的进程 for pid in /proc/[0-9]*/fd; do echo $(ls $pid 2\u0026gt;/dev/null | wc -l) $pid done | sort -rn | head -20 # 5. 查看进程打开的具体文件 lsof -p $(pgrep -f java) | head -50 # 6. 查看已删除但仍被占用的文件 lsof | grep deleted | head -20 # 7. 查看Socket连接 lsof -i -n | grep -i listen lsof -i -n | grep -i established | wc -l # 8. 查看日志文件 lsof | grep \u0026#34;\\.log\u0026#34; | wc -l 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 12 # 实时监控文件句柄使用 watch -n 5 \u0026#39;cat /proc/sys/fs/file-nr\u0026#39; # 查看各类型文件分布 lsof -p $(pgrep -f java) | awk \u0026#39;{print $5}\u0026#39; | sort | uniq -c | sort -rn # 查看网络连接数 lsof -i -n | awk \u0026#39;{print $8}\u0026#39; | sort | uniq -c | sort -rn # 检查文件描述符泄漏 strace -p $(pgrep -f java) -e open,close -o /tmp/fd_trace.log # 分析open和close是否配对 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 # 1. 临时提高进程限制 ulimit -n 65535 # 2. 永久修改系统限制 # /etc/security/limits.conf * soft nofile 65535 * hard nofile 65535 root soft nofile 65535 root hard nofile 65535 # /etc/sysctl.conf fs.file-max = 2097152 fs.nr_open = 2097152 sysctl -p # 3. 修改systemd服务限制 # /etc/systemd/system/application.service [Service] LimitNOFILE=65535 LimitNPROC=65535 systemctl daemon-reload systemctl restart application # 4. 修复代码泄漏 # Java示例 - 确保资源关闭 // 错误代码 FileInputStream fis = new FileInputStream(file); // 处理文件 // 忘记关闭 // 正确代码 try (FileInputStream fis = new FileInputStream(file)) { // 处理文件 } catch (IOException e) { // 处理异常 } # 5. 配置日志轮转 # /etc/logrotate.d/application /var/log/application/*.log { daily rotate 7 compress delaycompress missingok notifempty create 0640 app app postrotate kill -USR1 $(cat /var/run/application.pid) endscript } # 6. 重启服务释放泄漏资源 systemctl restart application # 7. 清理已删除但占用的文件 # 找到进程并重启 lsof | grep deleted | awk \u0026#39;{print $2}\u0026#39; | sort -u 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 1. 配置文件句柄监控 # Prometheus Node Exporter - alert: FileDescriptorsHigh expr: node_filefd_allocated / node_filefd_maximum \u0026gt; 0.8 for: 5m labels: severity: warning annotations: summary: \u0026#34;文件句柄使用率过高\u0026#34; # 2. 代码审查规范 # 所有资源打开必须有对应的关闭 # 使用try-with-resources（Java） # 使用context manager（Python） # 3. 定期巡检 # 每周检查文件句柄使用 cat /proc/sys/fs/file-nr # 4. 压力测试 # 模拟高并发验证文件句柄是否泄漏 经验总结：\n文件句柄耗尽是常见但可预防的事故 系统默认限制必须调整 代码资源管理是关键 日志轮转必须配置 监控要覆盖系统级和进程级 事故16：Docker overlay2爆盘事故 事故现象：\n容器无法启动 镜像无法拉取 Docker命令执行失败 df -h显示/var/lib/docker使用率100% 事故原因：\n容器日志未限制大小，单个日志文件达50GB 大量悬空镜像未清理 容器层数过多，overlay2叠加膨胀 未配置定期清理机制 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # 1. 查看Docker磁盘使用 docker system df # 输出： # TYPE TOTAL ACTIVE SIZE RECLAIMABLE # Images 50 10 20GB 15GB (75%) # Containers 100 20 5GB 4GB (80%) # Local Volumes 30 10 10GB 8GB (80%) # Build Cache - - 5GB 5GB (100%) # 2. 查看具体占用 du -sh /var/lib/docker/* # 输出： # 80G /var/lib/docker/overlay2 # 10G /var/lib/docker/containers # 5G /var/lib/docker/volumes # 3. 查看大日志文件 find /var/lib/docker/containers -name \u0026#34;*.log\u0026#34; -size +1G -exec ls -lh {} \\; # 4. 查看悬空镜像 docker images -f \u0026#34;dangling=true\u0026#34; # 5. 查看容器大小 docker ps -s --no-trunc # 6. 查看overlay2层 ls -la /var/lib/docker/overlay2/ | head -20 du -sh /var/lib/docker/overlay2/* | sort -rh | head -10 # 7. 查看Docker配置 cat /etc/docker/daemon.json 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 查看容器日志大小 for container in $(docker ps -q); do size=$(docker inspect $container --format=\u0026#39;{{.LogPath}}\u0026#39; | \\ xargs du -h 2\u0026gt;/dev/null | cut -f1) echo \u0026#34;$container: $size\u0026#34; done | sort -rh | head -10 # 查看镜像层数 docker history 镜像名 # 查看存储驱动 docker info | grep \u0026#34;Storage Driver\u0026#34; # 监控Docker磁盘使用 watch -n 10 \u0026#39;df -h /var/lib/docker\u0026#39; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 # 1. 紧急清理悬空资源 docker image prune -f # 清理悬空镜像 docker container prune -f # 清理停止的容器 docker volume prune -f # 清理未使用的卷 docker system prune -f # 清理所有未使用资源 # 2. 清理大日志文件 for log in $(find /var/lib/docker/containers -name \u0026#34;*.log\u0026#34; -size +1G); do echo \u0026#34;\u0026#34; \u0026gt; $log # 清空而非删除，避免影响正在写入的容器 done # 3. 配置日志限制（/etc/docker/daemon.json） { \u0026#34;log-driver\u0026#34;: \u0026#34;json-file\u0026#34;, \u0026#34;log-opts\u0026#34;: { \u0026#34;max-size\u0026#34;: \u0026#34;100m\u0026#34;, \u0026#34;max-file\u0026#34;: \u0026#34;3\u0026#34; }, \u0026#34;storage-opts\u0026#34;: [ \u0026#34;overlay2.override_kernel_check=true\u0026#34; ] } systemctl daemon-reload systemctl restart docker # 4. 迁移Docker数据目录 systemctl stop docker rsync -avz /var/lib/docker /data/docker/ # 修改/etc/docker/daemon.json { \u0026#34;data-root\u0026#34;: \u0026#34;/data/docker\u0026#34; } systemctl start docker # 5. 优化镜像构建（减少层数） # Dockerfile优化 # 错误：多层RUN RUN apt-get update RUN apt-get install -y package1 RUN apt-get install -y package2 # 正确：合并层 RUN apt-get update \u0026amp;\u0026amp; apt-get install -y package1 package2 \u0026amp;\u0026amp; \\ rm -rf /var/lib/apt/lists/* # 6. 定期清理脚本（crontab） 0 2 * * * /usr/bin/docker system prune -f --volumes # 7. 使用多阶段构建减少镜像大小 FROM golang:1.19 AS builder WORKDIR /app COPY . . RUN go build -o main . FROM alpine:latest COPY --from=builder /app/main /main CMD [\u0026#34;/main\u0026#34;] 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # 1. 配置磁盘监控告警 # Prometheus + Docker Exporter - alert: DockerDiskUsageHigh expr: docker_disk_usage / docker_disk_total \u0026gt; 0.8 for: 5m labels: severity: warning annotations: summary: \u0026#34;Docker磁盘使用率过高\u0026#34; # 2. 日志收集方案 # 使用 journald 或 syslog 驱动，配合ELK { \u0026#34;log-driver\u0026#34;: \u0026#34;syslog\u0026#34;, \u0026#34;log-opts\u0026#34;: { \u0026#34;syslog-address\u0026#34;: \u0026#34;udp://log-server:514\u0026#34; } } # 3. 镜像仓库管理 # 定期清理旧镜像 docker images | grep \u0026#34;weeks ago\\|months ago\u0026#34; | \\ awk \u0026#39;{print $3}\u0026#39; | xargs docker rmi -f # 4. 存储规划 # Docker数据目录独立分区 # 建议至少100GB空间 经验总结：\n日志限制是必须配置项 定期清理要自动化 镜像构建要优化层数 数据目录建议独立分区 监控要覆盖磁盘使用率 事故17：CI/CD误发布事故 事故现象：\n生产环境出现未测试功能 用户报告新功能BUG 数据库 schema 不兼容 服务间歇性失败 事故原因：\n开发人员直接推送代码到生产分支 缺少发布审批流程 自动化测试覆盖率不足 回滚机制不完善 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 1. 查看Git提交历史 git log production --since=\u0026#34;2024-01-15\u0026#34; --oneline # 2. 查看CI/CD流水线记录 # Jenkins/GitLab CI 构建历史 # 查看谁触发了发布 # 3. 对比发布前后差异 git diff 发布前commit 发布后commit --stat # 4. 查看部署记录 kubectl rollout history deployment/app kubectl rollout history deployment/app --revision=5 # 5. 检查配置变更 git diff 发布前commit 发布后commit -- config/ # 6. 查看应用日志 kubectl logs deployment/app --tail=1000 | grep -i \u0026#34;error\\|exception\u0026#34; # 7. 检查数据库变更 # 查看迁移脚本 ls -la db/migrations/ 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 # 查看当前部署版本 kubectl get deployment app -o jsonpath=\u0026#39;{.spec.template.metadata.annotations}\u0026#39; # 查看Pod重启次数 kubectl get pods | grep -v \u0026#34;1/1\u0026#34; # 检查服务健康 curl -f http://app-service/health || echo \u0026#34;Unhealthy\u0026#34; # 查看事件 kubectl get events --sort-by=\u0026#39;.lastTimestamp\u0026#39; | tail -20 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 1. 紧急回滚 kubectl rollout undo deployment/app # 或回滚到特定版本 kubectl rollout undo deployment/app --to-revision=4 # Jenkins回滚 # 点击\u0026#34;Revert\u0026#34;或执行回滚脚本 # 2. 验证回滚结果 kubectl rollout status deployment/app curl -f http://app-service/health # 3. 检查数据一致性 # 如有数据库变更，需要回滚数据 # 从备份恢复或执行回滚脚本 # 4. 通知相关人员 # 发送事故通知 # 更新状态页面 # 5. 修复问题后重新发布 # 走正常发布流程 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 # 1. 分支保护策略 # GitLab/GitHub分支保护 # - 禁止直接push到production # - 必须Merge Request # - 必须Code Review # - 必须CI通过 # 2. 发布审批流程 # Jenkins Pipeline示例 pipeline { agent any stages { stage(\u0026#39;Build\u0026#39;) { steps { sh \u0026#39;mvn package\u0026#39; } } stage(\u0026#39;Test\u0026#39;) { steps { sh \u0026#39;mvn test\u0026#39; } } stage(\u0026#39;Approve\u0026#39;) { steps { input message: \u0026#39;确认发布到生产？\u0026#39;, ok: \u0026#39;确认发布\u0026#39; } } stage(\u0026#39;Deploy\u0026#39;) { steps { sh \u0026#39;./deploy.sh production\u0026#39; } } } } # 3. 灰度发布 # 先发布10%流量 kubectl set image deployment/app app=app:new-version kubectl rollout pause deployment/app # 验证后继续 kubectl rollout resume deployment/app # 4. 自动化回滚 # 健康检查失败自动回滚 apiVersion: apps/v1 kind: Deployment metadata: name: app spec: strategy: rollingUpdate: maxSurge: 1 maxUnavailable: 0 minReadySeconds: 30 progressDeadlineSeconds: 300 # 5. 数据库变更规范 # - 向前兼容 # - 可回滚脚本 # - 分批次执行 经验总结：\n生产发布必须有审批流程 自动化测试是基本保障 灰度发布降低风险 回滚必须能在15分钟内完成 每次发布都要有回滚预案 事故18：数据库误删事故 事故现象：\n核心业务数据丢失 应用报\u0026quot;Table doesn\u0026rsquo;t exist\u0026quot; 用户数据无法查询 业务完全中断 事故原因：\n运维人员误执行DROP/TRUNCATE命令 生产环境权限过大 缺少操作审计 备份恢复验证不足 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 -- 1. 确认数据丢失 SHOW DATABASES; USE production_db; SHOW TABLES; -- 发现表不存在 -- 2. 查看操作日志 # MySQL审计日志 grep -i \u0026#34;drop\\|truncate\u0026#34; /var/log/mysql/audit.log # 3. 查看二进制日志 mysqlbinlog /var/lib/mysql/mysql-bin.000100 | \\ grep -i \u0026#34;drop\\|truncate\u0026#34; | head -20 -- 4. 查看进程历史 SELECT * FROM information_schema.processlist; -- 5. 确认备份状态 ls -la /backup/mysql/ mysql -e \u0026#34;SHOW BINARY LOGS;\u0026#34; -- 6. 检查binlog是否开启 SHOW VARIABLES LIKE \u0026#39;log_bin\u0026#39;; SHOW VARIABLES LIKE \u0026#39;binlog_format\u0026#39;; 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 # 查看binlog内容 mysqlbinlog --start-datetime=\u0026#34;2024-01-15 10:00:00\u0026#34; \\ --stop-datetime=\u0026#34;2024-01-15 11:00:00\u0026#34; \\ /var/lib/mysql/mysql-bin.000100 | less # 查找误操作时间点 mysqlbinlog /var/lib/mysql/mysql-bin.* | \\ grep -B 5 -A 5 \u0026#34;DROP TABLE\u0026#34; # 检查备份文件 ls -lth /backup/mysql/*.sql | head -5 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 1. 紧急止损 # 停止应用写入，防止数据进一步损坏 kubectl scale deployment app --replicas=0 # 2. 从备份恢复 # 找到最近的全量备份 BACKUP_FILE=$(ls -lt /backup/mysql/*.sql | head -1 | awk \u0026#39;{print $NF}\u0026#39;) # 恢复数据库 mysql -u root -p \u0026lt; $BACKUP_FILE # 3. 使用binlog恢复增量数据 # 找到误操作前的binlog位置 mysqlbinlog --start-datetime=\u0026#34;2024-01-15 00:00:00\u0026#34; \\ --stop-datetime=\u0026#34;2024-01-15 10:59:59\u0026#34; \\ /var/lib/mysql/mysql-bin.000100 | mysql -u root -p # 4. 验证数据 mysql -e \u0026#34;SELECT COUNT(*) FROM critical_table;\u0026#34; # 5. 恢复应用 kubectl scale deployment app --replicas=3 # 6. 通知用户 # 发送维护完成通知 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # 1. 权限最小化 # 生产环境禁止DROP/DELETE权限 # 创建只读账号用于查询 CREATE USER \u0026#39;readonly\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED BY \u0026#39;password\u0026#39;; GRANT SELECT ON production_db.* TO \u0026#39;readonly\u0026#39;@\u0026#39;%\u0026#39;; # 2. 操作审批 # 所有DDL操作需要审批 # 使用工单系统 # 3. 备份策略 # /etc/crontab # 每天全量备份 0 2 * * * mysqldump -u root -p --all-databases \u0026gt; /backup/mysql/all_$(date +\\%F).sql # 每小时增量备份 0 * * * * mysqladmin flush-logs # 4. 备份验证 # 每周恢复测试 # 验证备份可用性 # 5. 审计日志 # /etc/my.cnf [mysqld] audit_log=FORCE_LOG_PERMANENT audit_log_events=CONNECT,QUERY,TABLE_ACCESS # 6. 高危命令保护 # 使用SQL审核工具 # 如：Yearning、Archery 经验总结：\n生产环境权限必须最小化 高危操作需要双人复核 备份必须定期验证恢复 binlog是最后一道防线 审计日志必须开启 事故19：Redis缓存击穿事故 事故现象：\n某个热点商品页面响应极慢 数据库CPU 100% 应用大量超时 只有特定接口受影响 事故原因：\n热点key（如秒杀商品）突然过期 大量请求同时访问该key 请求穿透到数据库 数据库无法承受瞬时压力 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 1. 查看Redis状态 redis-cli INFO redis-cli INFO stats | grep -i \u0026#34;hit\\|miss\u0026#34; # 2. 查看热点key redis-cli --hotkeys # Redis 4.0+ # 3. 查看慢查询 redis-cli SLOWLOG GET 10 # 4. 查看数据库状态 mysql -e \u0026#34;SHOW PROCESSLIST;\u0026#34; | head -20 mysql -e \u0026#34;SHOW STATUS LIKE \u0026#39;Threads_connected\u0026#39;;\u0026#34; # 5. 查看应用日志 grep -i \u0026#34;timeout\\|slow\u0026#34; /var/log/app/*.log | tail -50 # 6. 监控缓存命中率 # Prometheus Redis Exporter redis_keyspace_hits / (redis_keyspace_hits + redis_keyspace_misses) 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 # 实时查看Redis命令 redis-cli MONITOR | head -50 # 查看大key redis-cli --bigkeys # 查看内存使用 redis-cli INFO memory | grep -i \u0026#34;used_memory\u0026#34; # 查看连接数 redis-cli CLIENT LIST | wc -l 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 # 1. 互斥锁方案（代码修复） import redis import time def get_product(product_id): key = f\u0026#34;product:{product_id}\u0026#34; data = redis_client.get(key) if data is None: # 尝试获取锁 lock_key = f\u0026#34;lock:{key}\u0026#34; if redis_client.set(lock_key, \u0026#34;1\u0026#34;, nx=True, ex=10): try: # 双重检查 data = redis_client.get(key) if data is None: # 从数据库查询 data = db.query_product(product_id) redis_client.setex(key, 300, data) finally: redis_client.delete(lock_key) else: # 等待后重试 time.sleep(0.1) return get_product(product_id) return data # 2. 逻辑过期方案 # 不设置物理过期，在value中存储过期时间 def get_product_with_logical_expire(product_id): key = f\u0026#34;product:{product_id}\u0026#34; data = redis_client.get(key) if data: data_dict = json.loads(data) if time.time() \u0026lt; data_dict[\u0026#39;expire_time\u0026#39;]: return data_dict[\u0026#39;data\u0026#39;] else: # 异步更新 asyncio.create_task(refresh_cache(product_id)) return data_dict[\u0026#39;data\u0026#39;] # 缓存未命中，重建 return rebuild_cache(product_id) # 3. 永不过期 + 异步更新 # 热点数据永不过期，后台定时更新 # 4. 限流保护 from flask_limiter import Limiter limiter = Limiter(app, key_func=get_remote_address) @app.route(\u0026#39;/product/\u0026lt;id\u0026gt;\u0026#39;) @limiter.limit(\u0026#34;100/minute\u0026#34;) def get_product(id): ... 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # 1. 热点key识别 # 定期分析Redis访问日志 # 识别访问频率最高的key # 2. 缓存策略 # 热点数据永不过期 # 设置随机过期时间 expire_time = base_expire + random.randint(0, 300) # 3. 多级缓存 # 本地缓存 + Redis + 数据库 from cachetools import TTLCache local_cache = TTLCache(maxsize=1000, ttl=60) # 4. 监控告警 # Prometheus告警规则 - alert: RedisHitRateLow expr: redis_keyspace_hits / (redis_keyspace_hits + redis_keyspace_misses) \u0026lt; 0.8 for: 5m labels: severity: warning - alert: DatabaseConnectionsHigh expr: mysql_threads_connected / mysql_max_connections \u0026gt; 0.8 for: 2m labels: severity: critical # 5. 压测验证 # 模拟热点key过期场景 # 验证系统承受能力 经验总结：\n热点key需要特殊保护 互斥锁是防止击穿的有效手段 缓存命中率必须监控 数据库要有熔断保护 定期识别和治理热点key 事故20：API限流失效事故 事故现象：\n接口被恶意刷取 系统资源耗尽 正常用户无法访问 产生大量异常费用（如短信、云资源） 事故原因：\n限流配置未生效 限流算法实现错误 分布式环境限流不同步 配置变更未验证 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 1. 查看API访问日志 awk \u0026#39;{print $1}\u0026#39; access.log | sort | uniq -c | sort -rn | head -10 # 发现单个IP请求量异常 # 2. 查看限流配置 cat /etc/nginx/nginx.conf | grep -A 10 \u0026#34;limit_req\u0026#34; # 3. 查看应用限流配置 grep -r \u0026#34;rate.limit\u0026#34; /app/config/ # 4. 检查限流中间件状态 # Redis限流计数器 redis-cli GET \u0026#34;rate_limit:api:login:192.168.1.100\u0026#34; # 5. 查看被限流的请求数 grep \u0026#34;429\u0026#34; access.log | wc -l # 输出：0（说明限流未生效） # 6. 检查配置变更历史 git log config/ --since=\u0026#34;2024-01-14\u0026#34; 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 # 实时查看API请求分布 tail -f access.log | awk \u0026#39;{print $1}\u0026#39; | sort | uniq -c | sort -rn # 查看限流日志 grep -i \u0026#34;rate.limit\\|throttle\u0026#34; /var/log/app/*.log # 检查Redis限流key redis-cli KEYS \u0026#34;rate_limit:*\u0026#34; | head -20 # 查看Nginx限流状态 nginx -T | grep -A 5 \u0026#34;limit_req_zone\u0026#34; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 # 1. Nginx限流（立即生效） # /etc/nginx/nginx.conf http { # 定义限流区域 limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; limit_req_zone $server_name zone=global:10m rate=1000r/s; server { location /api/ { limit_req zone=api burst=20 nodelay; limit_req_status 429; # 超过限制返回友好提示 error_page 429 /429.html; } location /api/login { # 登录接口更严格 limit_req zone=api burst=5 nodelay; } } } nginx -t \u0026amp;\u0026amp; systemctl reload nginx # 2. 应用层限流（Java + Redis） @Aspect @Component public class RateLimitAspect { @Autowired private RedisTemplate\u0026lt;String, String\u0026gt; redisTemplate; @Around(\u0026#34;@annotation(RateLimit)\u0026#34;) public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) { String key = \u0026#34;rate_limit:\u0026#34; + rateLimit.api() + \u0026#34;:\u0026#34; + getClientIP(); Long count = redisTemplate.opsForValue().increment(key); if (count == 1) { redisTemplate.expire(key, rateLimit.time(), TimeUnit.SECONDS); } if (count \u0026gt; rateLimit.maxRequests()) { throw new RateLimitException(\u0026#34;请求过于频繁\u0026#34;); } return joinPoint.proceed(); } } # 3. 分布式限流（Redis + Lua） local key = KEYS[1] local limit = tonumber(ARGV[1]) local expire = tonumber(ARGV[2]) local current = redis.call(\u0026#39;INCR\u0026#39;, key) if current == 1 then redis.call(\u0026#39;EXPIRE\u0026#39;, key, expire) end if current \u0026gt; limit then return 0 else return 1 end # 4. 紧急封禁恶意IP # iptables封禁 iptables -A INPUT -s 恶意IP -j DROP # Nginx封禁 echo \u0026#34;deny 恶意IP;\u0026#34; \u0026gt;\u0026gt; /etc/nginx/conf.d/block.conf nginx -s reload # 5. 添加验证码 # 对频繁请求要求验证码 if (request_count \u0026gt; threshold) { requireCaptcha = true; } 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # 1. 多层限流 # Nginx层 + 应用层 + 网关层 # 每层都有独立限流 # 2. 限流配置验证 # 变更后必须测试验证 # 自动化测试限流效果 # 3. 监控告警 # Prometheus告警 - alert: APIRateLimitTriggered expr: rate(http_requests_total{status=\u0026#34;429\u0026#34;}[5m]) \u0026gt; 100 for: 2m labels: severity: warning annotations: summary: \u0026#34;API限流触发频繁\u0026#34; # 4. 动态限流 # 根据系统负载动态调整限流阈值 if (cpu_usage \u0026gt; 80%) { rate_limit = rate_limit * 0.5; } # 5. 黑名单机制 # 自动识别并封禁恶意IP # 接入WAF防护 经验总结：\n限流必须多层防护 配置变更后必须验证 分布式限流需要原子操作 监控要覆盖429状态码 恶意IP要快速封禁 事故21：机房断电事故 事故现象：\n整个机房服务不可用 监控全部失联 用户无法访问任何服务 硬件设备关机 事故原因：\n市电供应中断 UPS电池老化失效 发电机启动失败 备用电源切换故障 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 确认断电范围 # 联系机房运维确认 # 查看监控最后上报时间 # 2. 检查UPS状态 # 通过带外管理查看 ipmitool -H BMC_IP -U admin -P password power status # 3. 检查服务器状态 # 通过IPMI查看 ipmitool -H BMC_IP sensor list | grep -i \u0026#34;power\\|voltage\u0026#34; # 4. 确认恢复时间 # 联系电力公司 # 评估发电机燃料 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 1. 启动应急预案 # 通知相关人员 # 更新状态页面 # 2. 等待电力恢复 # 监控UPS剩余时间 # 准备有序关机 # 3. 电力恢复后 # 按顺序启动设备 # 1. 网络设备（交换机、路由器） # 2. 存储设备 # 3. 数据库服务器 # 4. 应用服务器 # 4. 验证服务 for service in db cache app web; do systemctl status $service curl -f http://localhost/$service/health done # 5. 数据一致性检查 # 数据库主从状态 # 文件系统检查 fsck /dev/sda1 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 1. 双路市电 # 接入不同变电站 # 2. UPS定期维护 # 每季度检查电池 # 每年更换老化电池 # 3. 发电机保养 # 每月试运行 # 保证燃料充足 # 4. 多机房部署 # 异地灾备 # 流量自动切换 # 5. 监控告警 # UPS电量监控 # 电力质量监控 经验总结：\n电力是基础设施中的基础设施 UPS必须定期维护 多机房部署是最终保障 应急预案必须定期演练 带外管理通道必须独立 事故22：网络交换机故障事故 事故现象：\n部分服务器网络中断 网络延迟飙升 丢包率超过50% 跨机房通信失败 事故原因：\n交换机硬件故障 配置错误导致环路 固件BUG 端口过载 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 1. 测试网络连通性 ping 网关IP ping 同网段其他服务器 # 2. 查看网络接口 ip addr show ethtool eth0 # 3. 查看路由 ip route show traceroute 目标IP # 4. 登录交换机 ssh admin@switch-ip show interface status show mac address-table show spanning-tree # 5. 查看交换机日志 show logging show interface eth1/1 errors 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 1. 切换备用链路 # 修改路由 ip route change default via 备用网关 # 2. 重启交换机端口 # 交换机命令 configure terminal interface eth1/1 shutdown no shutdown # 3. 更换故障交换机 # 启用备用设备 # 恢复配置 # 4. 验证网络 ping -c 100 目标IP | grep \u0026#34;packet loss\u0026#34; mtr 目标IP # 5. 通知业务方 # 确认服务恢复 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 1. 交换机冗余 # 堆叠或VRRP # 双上行链路 # 2. STP配置 # 启用生成树协议 # 防止环路 # 3. 定期巡检 # 检查端口错误计数 # 检查CPU/内存使用 # 4. 固件升级 # 定期升级到稳定版本 # 关注厂商安全公告 # 5. 监控告警 # 端口状态监控 # 流量异常告警 经验总结：\n网络设备必须有冗余 配置变更需要审批 定期巡检很重要 备用设备要定期测试 厂商支持合同要有效 事故 23：负载均衡器宕机事故 事故现象：\n所有用户请求返回 502/503 错误 后端服务正常但无法访问 监控显示 LB 健康检查全部失败 业务完全中断 30 分钟 事故原因：\n负载均衡器硬件故障 主备切换机制失效 配置同步延迟 健康检查阈值设置过严 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 1. 确认 LB 状态 curl -I http://lb-vip/health # 返回：Connection refused # 2. 检查 LB 节点 ssh lb-master systemctl status haproxy systemctl status keepalived # 3. 查看 VIP 漂移状态 ip addr show | grep \u0026#34;inet.*vip\u0026#34; # 4. 检查后端服务 for backend in backend1 backend2 backend3; do curl -f http://$backend/health \u0026amp;\u0026amp; echo \u0026#34;$backend OK\u0026#34; || echo \u0026#34;$backend FAIL\u0026#34; done # 5. 查看 LB 日志 tail -f /var/log/haproxy.log | grep -i \u0026#34;error\\|down\u0026#34; # 6. 检查 keepalived 状态 systemctl status keepalived journalctl -u keepalived -n 50 # 7. 查看 VRRP 状态 ip addr show | grep -i \u0026#34;vrrp\\|master\\|backup\u0026#34; 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 查看 HAProxy 状态 echo \u0026#34;show stat\u0026#34; | socat stdio /var/run/haproxy.sock # 查看后端服务器状态 echo \u0026#34;show servers state\u0026#34; | socat stdio /var/run/haproxy.sock # 检查连接数 echo \u0026#34;show info\u0026#34; | socat stdio /var/run/haproxy.sock | grep -i \u0026#34;conn\\|curr\u0026#34; # 测试后端直连 curl -H \u0026#34;Host: www.example.com\u0026#34; http://backend1:80/ # 查看网络接口 ip -s link show eth0 ethtool -S eth0 | grep -i \u0026#34;drop\\|error\u0026#34; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 1. 紧急切换备用 LB # 在备用节点强制抢占 VIP systemctl stop keepalived echo \u0026#34;2\u0026#34; \u0026gt; /proc/sys/net/ipv4/conf/all/arp_ignore echo \u0026#34;1\u0026#34; \u0026gt; /proc/sys/net/ipv4/conf/all/arp_announce systemctl start keepalived # 2. 验证 VIP 漂移 ip addr show | grep \u0026#34;inet.*vip\u0026#34; ping -c 3 lb-vip # 3. 重启 HAProxy systemctl restart haproxy haproxy -c -f /etc/haproxy/haproxy.cfg # 先验证配置 # 4. 检查后端状态 echo \u0026#34;show servers state\u0026#34; | socat stdio /var/run/haproxy.sock # 5. 临时直连后端（应急） # 修改 DNS 或 hosts 绕过 LB echo \u0026#34;后端 IP www.example.com\u0026#34; \u0026gt;\u0026gt; /etc/hosts # 6. 修复主 LB 后恢复 # 等待主 LB 恢复后重新加入集群 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 # 1. 双活 LB 架构 # 至少 2 台 LB，使用 keepalived 实现 VIP 漂移 # 2. 健康检查优化 # /etc/haproxy/haproxy.cfg backend app_servers balance roundrobin option httpchk GET /health default-server inter 5s fall 3 rise 2 # 调整检查频率和阈值 server backend1 192.168.1.10:80 check server backend2 192.168.1.11:80 check server backend3 192.168.1.12:80 check # 3. 监控告警 # Prometheus HAProxy Exporter - alert: HAProxyBackendDown expr: haproxy_backend_up == 0 for: 1m labels: severity: critical annotations: summary: \u0026#34;HAProxy 后端服务器宕机\u0026#34; - alert: HAProxyFrontendDown expr: haproxy_frontend_up == 0 for: 1m labels: severity: critical annotations: summary: \u0026#34;HAProxy 前端服务宕机\u0026#34; # 4. 定期演练 # 每月进行 LB 故障切换演练 # 验证 VIP 漂移时间\u0026lt;10 秒 # 5. 多层 LB # DNS LB + 硬件 LB + 软件 LB # 逐层降级保护 经验总结：\nLB 是单点故障高发区，必须冗余 健康检查阈值要合理，避免误判 VIP 漂移时间要监控（目标\u0026lt;10 秒） 定期故障切换演练必不可少 准备直连后端的应急方案 事故 24：防火墙规则错误事故 事故现象：\n部分服务突然无法访问 跨网段通信中断 监控显示连接被拒绝 应用报\u0026quot;Connection refused\u0026quot; 事故原因：\n防火墙规则更新错误 新规则覆盖了原有允许规则 规则顺序错误 未测试直接应用到生产 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # 1. 测试连通性 telnet 目标 IP 端口 nc -zv 目标 IP 端口 # 2. 查看防火墙状态 systemctl status firewalld systemctl status iptables # 3. 查看当前规则 iptables -L -n -v firewall-cmd --list-all # 4. 查看规则历史记录 # CentOS/RHEL cat /etc/sysconfig/iptables # Ubuntu/Debian cat /etc/iptables/rules.v4 # 5. 测试规则匹配 iptables -L -n -v | grep -i \u0026#34;目标端口\u0026#34; # 6. 查看被拒绝的连接 dmesg | grep -i \u0026#34;iptables\\|firewall\u0026#34; journalctl -k | grep -i \u0026#34;dropped\u0026#34; # 7. 临时关闭防火墙测试（谨慎） systemctl stop firewalld # 测试连通性 # 立即恢复 systemctl start firewalld 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 实时查看防火墙日志 tail -f /var/log/messages | grep -i \u0026#34;firewall\\|iptables\u0026#34; # 查看规则匹配计数 iptables -L -n -v --line-numbers # 测试特定规则 iptables -C INPUT -p tcp --dport 8080 -j ACCEPT # 检查规则是否存在 # 查看 NAT 规则 iptables -t nat -L -n -v # 查看连接追踪 conntrack -L | head -20 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # 1. 紧急恢复（备份规则） # 如果有备份 iptables-restore \u0026lt; /backup/iptables.rules.$(date -d \u0026#34;1 hour ago\u0026#34; +%Y%m%d) # 2. 删除错误规则 iptables -L -n --line-numbers iptables -D INPUT 5 # 删除第 5 条规则 # 3. 添加正确规则 iptables -A INPUT -p tcp --dport 8080 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT # 4. firewalld 修复 firewall-cmd --permanent --add-port=8080/tcp firewall-cmd --permanent --add-port=443/tcp firewall-cmd --reload # 5. 验证规则 iptables -L -n -v | grep -E \u0026#34;8080|443\u0026#34; nc -zv localhost 8080 # 6. 保存规则 # CentOS/RHEL service iptables save # Ubuntu/Debian iptables-save \u0026gt; /etc/iptables/rules.v4 # 7. 通知业务方验证 curl -f http://service:8080/health 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 1. 规则变更流程 # - 变更申请 # - 测试环境验证 # - 审批 # - 备份当前规则 # - 应用变更 # - 验证 # - 回滚预案 # 2. 规则备份自动化 # /etc/crontab 0 2 * * * /sbin/iptables-save \u0026gt; /backup/iptables/iptables.$(date +\\%Y\\%m\\%d).rules # 3. 规则管理工具 # 使用 Ansible/Puppet 管理防火墙规则 # 版本控制所有变更 # 4. 监控告警 # 监控防火墙规则变更 auditctl -w /etc/sysconfig/iptables -p wa -k iptables_change # 5. 默认策略 # 确保默认策略不会阻断所有流量 iptables -P INPUT ACCEPT # 测试期间 # 生产环境 iptables -P INPUT DROP # 但必须有允许规则 经验总结：\n防火墙变更必须备份当前规则 测试环境验证后再应用到生产 规则顺序很重要，从上到下匹配 监控规则变更审计日志 准备快速回滚脚本 事故 25：DNS 劫持事故 事故现象：\n用户访问域名被跳转到恶意网站 部分用户无法访问正常服务 DNS 解析结果不一致 安全团队收到钓鱼报告 事故原因：\n域名注册商账号被盗 DNS 记录被恶意修改 本地 DNS 服务器被入侵 中间人攻击 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # 1. 验证 DNS 解析 nslookup www.example.com dig www.example.com host www.example.com # 2. 从多个 DNS 服务器测试 dig @8.8.8.8 www.example.com dig @114.114.114.114 www.example.com dig @ns1.example.com www.example.com # 3. 检查 DNS 记录 dig www.example.com ANY dig example.com SOA dig example.com NS # 4. 查看本地 DNS 配置 cat /etc/resolv.conf nmcli dev show | grep DNS # 5. 检查 hosts 文件 cat /etc/hosts | grep -v \u0026#34;^#\u0026#34; # 6. 追踪 DNS 解析路径 dig +trace www.example.com # 7. 检查域名注册商 # 登录域名管理后台 # 查看 DNS 记录变更历史 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 批量测试多个 DNS 服务器 for dns in 8.8.8.8 1.1.1.1 114.114.114.114 223.5.5.5; do echo \u0026#34;=== $dns ===\u0026#34; dig @$dns www.example.com +short done # 检查 DNS 缓存 systemctl restart nscd # 清除缓存 systemctl restart systemd-resolved # 查看 DNS 查询日志 journalctl -u systemd-resolved -f # 检测 DNS 劫持 curl -s http://www.example.com | grep -i \u0026#34;title\u0026#34; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # 1. 紧急修改 DNS 记录 # 登录域名注册商后台 # 恢复正确的 A 记录 # www.example.com -\u0026gt; 正确 IP # 2. 降低 TTL 加速传播 # 将 TTL 从 24 小时改为 5 分钟 # 等待全球 DNS 缓存刷新 # 3. 通知用户 # 发送安全公告 # 提醒用户清除本地 DNS 缓存 # 4. 加强域名安全 # 启用域名锁定（Domain Lock） # 启用双因素认证 # 限制 DNS 修改权限 # 5. 部署 DNSSEC # 为域名启用 DNSSEC 签名 # 防止 DNS 记录篡改 # 6. 多 DNS 提供商 # 使用至少 2 家 DNS 服务商 # 如：Cloudflare + AWS Route53 # 7. 用户端建议 # 推荐使用可信 DNS（8.8.8.8、1.1.1.1） # 清除浏览器 DNS 缓存 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 1. 域名安全 # - 启用注册商锁定 # - 开启双因素认证 # - 限制管理员账号 # - 定期更换密码 # 2. DNS 监控 # 监控 DNS 记录变更 # 使用 DNS 监控服务（如 DNSWatch、SpyEye） # 3. DNSSEC 部署 # 为所有关键域名启用 DNSSEC # 定期更新密钥 # 4. 多 DNS 冗余 # 主 DNS：Cloudflare # 备 DNS：AWS Route53 # 自动故障切换 # 5. 用户教育 # 提醒用户注意钓鱼网站 # 提供官方 IP 备用访问方式 经验总结：\n域名账号安全至关重要 DNSSEC 可以有效防止篡改 多 DNS 提供商提高可用性 监控 DNS 记录变更 准备备用访问方式（IP 直连） 事故 26：BGP 路由泄露事故 事故现象：\n部分区域用户无法访问服务 网络路由异常 流量被错误路由到其他 AS 国际访问延迟飙升 事故原因：\n运营商 BGP 配置错误 AS 路径泄露 路由前缀宣告错误 恶意路由劫持 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 1. 检查本地路由 ip route show route -n # 2. 追踪路由路径 traceroute 目标 IP mtr 目标 IP # 3. 查看 BGP 状态（如有 BGP 路由器） show ip bgp summary show ip bgp 前缀 # 4. 使用在线工具检查 # bgp.he.net # bgpview.io # 查看 AS 路径和前缀宣告 # 5. 检查路由表 netstat -rn ip route show table all # 6. 测试不同区域访问 # 使用多地探测服务 # 如：ping.chinaz.com、boce.com 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 # 持续监控路由变化 watch -n 10 \u0026#39;mtr -r -c 10 目标 IP\u0026#39; # 查看路由缓存 ip route show cache # 检查多路径路由 ip route show | grep -i \u0026#34;multipath\u0026#34; # 查看 BGP 邻居（如有） show ip bgp neighbors 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 1. 联系运营商 # 报告路由异常 # 提供 AS 路径证据 # 要求修复 BGP 配置 # 2. 临时路由调整 # 修改本地路由 ip route add 目标网段 via 备用网关 # 3. 启用备用链路 # 切换到其他运营商线路 # 更新 DNS 解析 # 4. 使用 Anycast # 多地域部署相同 IP # 自动路由到最近节点 # 5. 监控路由变化 # 使用 BGP 监控服务 # 如：BGPMon、RIPE RIS # 6. 通知用户 # 发布服务状态公告 # 提供临时访问方式 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 1. BGP 安全 # 启用 RPKI（资源公钥基础设施） # 验证路由起源 # 2. 多运营商接入 # 至少 2 家运营商 # 自动故障切换 # 3. 路由监控 # 部署 BGP 监控 # 设置路由变更告警 # 4. Anycast 部署 # 关键服务使用 Anycast # 提高路由韧性 # 5. 运营商沟通 # 建立运营商紧急联系渠道 # 定期沟通网络状况 经验总结：\nBGP 问题通常需要运营商配合 多运营商接入降低风险 BGP 监控可以提前发现问题 Anycast 可以提高路由韧性 准备备用访问方案 事故 27：带宽耗尽事故 事故现象：\n网络响应极慢 丢包率超过 30% 监控显示带宽使用率 100% 用户访问超时 事故原因：\nDDoS 攻击 异常流量（如爬虫、扫描） 大文件未限制下载 视频/图片未压缩 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 1. 查看带宽使用 iftop -i eth0 nload eth0 vnstat -l # 2. 查看流量来源 tcpdump -i eth0 -nn -c 100 ntopng -i eth0 # 3. 查看连接分布 netstat -an | awk \u0026#39;{print $5}\u0026#39; | cut -d: -f1 | sort | uniq -c | sort -rn | head -20 # 4. 查看进程网络使用 nethogs eth0 # 5. 查看防火墙连接 conntrack -L | wc -l # 6. 检查异常流量 tcpdump -i eth0 -w capture.pcap # 用 Wireshark 分析 # 7. 查看 Web 访问日志 awk \u0026#39;{print $1}\u0026#39; access.log | sort | uniq -c | sort -rn | head -20 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 # 实时监控带宽 watch -n 1 \u0026#39;cat /proc/net/dev | grep eth0\u0026#39; # 查看各协议流量 ip -s link show eth0 # 查看 TCP 连接状态 ss -s # 查看网络错误 netstat -i 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 # 1. 紧急限流 # iptables 限流 iptables -A INPUT -p tcp --dport 80 -m limit --limit 100/s -j ACCEPT iptables -A INPUT -p tcp --dport 80 -j DROP # 2. 封禁异常 IP # 自动封禁高频访问 IP for ip in $(awk \u0026#39;{print $1}\u0026#39; access.log | sort | uniq -c | sort -rn | head -10 | awk \u0026#39;{print $2}\u0026#39;); do iptables -A INPUT -s $ip -j DROP done # 3. Nginx 限流 # /etc/nginx/nginx.conf limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s; server { location / { limit_req zone=one burst=20; limit_conn conn_limit 10; } } # 4. 启用 CDN # 将静态资源迁移到 CDN # 减少源站带宽压力 # 5. 联系运营商 # 申请临时带宽扩容 # 启用流量清洗服务 # 6. 优化资源 # 压缩图片和视频 # 启用 Gzip 压缩 # 配置浏览器缓存 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 1. 带宽监控 # Prometheus + Node Exporter - alert: BandwidthUsageHigh expr: rate(node_network_receive_bytes_total[5m]) / 1024 / 1024 \u0026gt; 100 # 100MB/s for: 5m labels: severity: warning # 2. DDoS 防护 # 接入云厂商 DDoS 防护 # 如：阿里云 DDoS 高防、腾讯云大禹 # 3. CDN 部署 # 静态资源全部走 CDN # 减少源站压力 # 4. 限流策略 # 多层限流（Nginx + 应用 + 网关） # 按 IP、用户、API 限流 # 5. 容量规划 # 根据业务量预估带宽需求 # 预留 50% 余量 经验总结：\n带宽耗尽通常是攻击或异常流量 快速识别并封禁异常 IP CDN 是降低带宽成本的有效手段 多层限流保护源站 DDoS 防护服务必不可少 事故 28：SSL 加速卡故障事故 事故现象：\nHTTPS 请求响应极慢 SSL 握手失败率飙升 CPU 使用率 100% 加密性能下降 90% 事故原因：\nSSL 加速卡硬件故障 驱动不兼容 证书配置错误 加密算法协商失败 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 1. 检查 SSL 加速卡状态 lspci | grep -i \u0026#34;ssl\\|crypto\u0026#34; dmesg | grep -i \u0026#34;ssl\\|crypto\u0026#34; # 2. 查看驱动状态 lsmod | grep -i \u0026#34;ssl\\|crypto\u0026#34; modinfo 驱动名 # 3. 测试 SSL 性能 openssl speed rsa openssl s_time -connect www.example.com:443 # 4. 检查证书 openssl s_client -connect www.example.com:443 \u0026lt;/dev/null 2\u0026gt;/dev/null | \\ openssl x509 -noout -dates # 5. 查看 Nginx SSL 配置 nginx -T | grep -i \u0026#34;ssl\u0026#34; # 6. 监控 SSL 错误 tail -f /var/log/nginx/error.log | grep -i \u0026#34;ssl\\|handshake\u0026#34; # 7. 测试不同加密套件 openssl s_client -connect www.example.com:443 -cipher \u0026#39;AES256-SHA\u0026#39; 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 # 查看 SSL 会话统计 openssl s_client -connect www.example.com:443 -stats # 测试 SSL 握手时间 time openssl s_client -connect www.example.com:443 \u0026lt;/dev/null # 查看硬件加速状态 cat /proc/crypto | grep -i \u0026#34;module\\|type\u0026#34; # 监控 CPU 使用 top -H -p $(pgrep -f nginx) 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # 1. 临时禁用硬件加速 # Nginx 配置 # ssl_session_cache off; # ssl_engine off; # 2. 重启 SSL 服务 systemctl restart nginx systemctl restart haproxy # 3. 更新驱动 # 下载最新驱动 # 重新编译内核模块 # 4. 优化 SSL 配置 # /etc/nginx/nginx.conf ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers \u0026#39;ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384\u0026#39;; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; # 5. 启用 OCSP Stapling ssl_stapling on; ssl_stapling_verify on; # 6. 替换故障硬件 # 联系厂商更换 SSL 加速卡 # 或改用软件 SSL 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 1. 硬件监控 # 监控 SSL 加速卡状态 # 设置故障告警 # 2. 软件降级方案 # 准备纯软件 SSL 配置 # 硬件故障时快速切换 # 3. 定期测试 # 每月测试 SSL 性能 # 验证硬件加速效果 # 4. 驱动管理 # 保持驱动最新版本 # 关注厂商安全公告 # 5. 证书管理 # 自动化证书续期 # 监控证书到期 经验总结：\nSSL 加速卡是性能瓶颈点 必须有软件降级方案 定期测试 SSL 性能 证书管理要自动化 关注驱动兼容性 事故 29：时间同步异常事故 事故现象：\n分布式系统数据不一致 日志时间混乱 证书验证失败 定时任务执行异常 事故原因：\nNTP 服务器不可达 时间漂移过大 时区配置错误 虚拟机时间不同步 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # 1. 检查当前时间 date timedatectl # 2. 检查 NTP 状态 systemctl status chronyd systemctl status ntpd # 3. 查看时间同步状态 chronyc tracking ntpq -p # 4. 检查时间偏移 chronyc sources -v ntpdate -q pool.ntp.org # 5. 查看系统日志 grep -i \u0026#34;time\\|ntp\\|clock\u0026#34; /var/log/messages # 6. 检查时区 timedatectl | grep \u0026#34;Time zone\u0026#34; ls -l /etc/localtime # 7. 对比多台服务器时间 for host in server1 server2 server3; do ssh $host \u0026#34;date\u0026#34; done 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 实时查看时间偏移 watch -n 5 \u0026#39;chronyc tracking | grep \u0026#34;System time\u0026#34;\u0026#39; # 手动同步时间 chronyc -a makestep ntpdate pool.ntp.org # 查看时间同步历史 chronyc sourcestats # 检查硬件时钟 hwclock --show hwclock --systohc 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # 1. 紧急时间同步 chronyc -a makestep # 或 ntpdate -s pool.ntp.org # 2. 重启时间服务 systemctl restart chronyd # 或 systemctl restart ntpd # 3. 配置 NTP 服务器 # /etc/chrony.conf server ntp.aliyun.com iburst server ntp.tencent.com iburst server pool.ntp.org iburst # 4. 配置时区 timedatectl set-timezone Asia/Shanghai ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime # 5. 虚拟机时间同步 # VMware Tools / VirtualBox Guest Additions # 启用主机时间同步 # 6. 验证同步 chronyc tracking date # 7. 通知业务方 # 确认服务恢复正常 # 检查日志时间 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 1. 多 NTP 源 # 配置至少 3 个 NTP 服务器 # 使用不同提供商 # 2. 监控告警 # Prometheus NTP Exporter - alert: NtpOffsetHigh expr: abs(ntp_offset_seconds) \u0026gt; 0.1 # 100ms for: 5m labels: severity: warning # 3. 定期巡检 # 每周检查时间同步状态 chronyc sources -v # 4. 虚拟机配置 # 启用主机时间同步 # 安装 Guest Tools # 5. 应用层容错 # 业务逻辑不依赖精确时间 # 使用逻辑时钟 事故 30：硬件磁盘损坏事故 事故现象：\n磁盘读写错误 文件系统只读 数据丢失 服务无法启动 事故原因：\n磁盘物理损坏 RAID 阵列降级 文件系统损坏 未及时更换故障盘 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # 1. 检查磁盘状态 smartctl -a /dev/sda smartctl -H /dev/sda # 2. 查看 RAID 状态 cat /proc/mdstat mdadm --detail /dev/md0 # 3. 检查文件系统 df -h mount | grep -i \u0026#34;ro\\|read-only\u0026#34; # 4. 查看系统日志 dmesg | grep -i \u0026#34;error\\|fail\\|sd\u0026#34; journalctl -k | grep -i \u0026#34;I/O error\u0026#34; # 5. 检查磁盘健康 badblocks -sv /dev/sda hdparm -t /dev/sda # 6. 查看磁盘 SMART 信息 smartctl -A /dev/sda | grep -i \u0026#34;reallocated\\|pending\\|uncorrectable\u0026#34; # 7. 检查 LVM 状态 pvdisplay vgdisplay lvdisplay 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 # 实时监控磁盘错误 watch -n 5 \u0026#39;dmesg | tail -20 | grep -i \u0026#34;error\u0026#34;\u0026#39; # 查看磁盘 IO 统计 iostat -x 1 # 查看磁盘温度 smartctl -A /dev/sda | grep -i \u0026#34;temperature\u0026#34; # 检查磁盘序列号 smartctl -i /dev/sda | grep \u0026#34;Serial Number\u0026#34; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # 1. 紧急数据备份 # 优先备份关键数据 rsync -avz /重要数据 /备份位置/ # 2. RAID 重建 # 更换故障磁盘 mdadm /dev/md0 --add /dev/sdX # 监控重建进度 watch cat /proc/mdstat # 3. 文件系统修复 # 先卸载 umount /dev/sda1 # 修复 fsck -y /dev/sda1 # 重新挂载 mount /dev/sda1 # 4. 更换磁盘 # 热插拔更换（如支持） # 或停机更换 # 5. 恢复数据 # 从备份恢复 # 或使用数据恢复工具 # 6. 验证服务 systemctl status 关键服务 curl -f http://localhost/health 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 1. RAID 配置 # 生产环境至少 RAID 1 或 RAID 5 # 关键数据 RAID 10 # 2. 磁盘监控 # Prometheus SMART Exporter - alert: DiskSmartFailure expr: smart_disk_failure == 1 for: 1m labels: severity: critical # 3. 定期巡检 # 每周检查 SMART 状态 smartctl -H /dev/sd[a-z] # 4. 备份策略 # 每天增量备份 # 每周全量备份 # 异地备份 # 5. 备件管理 # 保持适量备件库存 # 与供应商建立快速更换通道 经验总结：\n磁盘损坏是常见硬件故障 RAID 可以提供冗余保护 SMART 监控可以提前预警 备份是最后防线 备件管理很重要 事故 31：内存泄漏事故 事故现象：\n服务运行一段时间后变慢 内存使用率持续增长 OOM Killer 频繁触发 服务反复重启 事故原因：\n代码内存泄漏 缓存未设置上限 连接未正确关闭 第三方库内存问题 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 1. 查看内存使用 free -h cat /proc/meminfo | head -20 # 2. 查看进程内存 ps aux --sort=-%mem | head -20 # 3. 查看内存趋势 vmstat 1 10 sar -r 1 10 # 4. 检查 OOM 日志 dmesg | grep -i \u0026#34;oom\\|killed\u0026#34; grep -i \u0026#34;oom\u0026#34; /var/log/messages # 5. Java 应用内存分析 jmap -heap \u0026lt;PID\u0026gt; jstat -gc \u0026lt;PID\u0026gt; 1000 10 # 6. 查看内存映射 pmap -x \u0026lt;PID\u0026gt; | tail -20 # 7. 检查 Swap 使用 swapon -s vmstat 1 5 | grep -E \u0026#34;swpd|si|so\u0026#34; 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 # 实时监控内存 watch -n 5 \u0026#39;free -h\u0026#39; # 查看内存详细分布 cat /proc/meminfo # 查看进程内存详细 cat /proc/\u0026lt;PID\u0026gt;/status | grep -i \u0026#34;vm\u0026#34; # 内存泄漏检测工具 valgrind --leak-check=full ./program 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 # 1. 紧急重启服务 systemctl restart 服务名 # 2. 临时增加 Swap dd if=/dev/zero of=/swapfile bs=1G count=4 chmod 600 /swapfile mkswap /swapfile swapon /swapfile # 3. 调整 OOM 优先级 echo -500 \u0026gt; /proc/\u0026lt;PID\u0026gt;/oom_score_adj # 4. 限制内存使用 # systemd 服务限制 # /etc/systemd/system/服务.service [Service] MemoryLimit=2G # 5. 代码修复 # Java：分析 Heap Dump jmap -dump:format=b,file=heap.hprof \u0026lt;PID\u0026gt; # 用 MAT 分析 # Python：使用 tracemalloc import tracemalloc tracemalloc.start() # ... 代码 ... snapshot = tracemalloc.take_snapshot() for stat in snapshot.statistics(\u0026#39;lineno\u0026#39;)[:10]: print(stat) # 6. 配置缓存上限 # Redis：maxmemory 2gb # 应用：设置缓存大小限制 # 7. 监控告警 # 设置内存使用率告警（\u0026gt;80%） 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 1. 代码审查 # 重点关注内存分配和释放 # 使用静态分析工具 # 2. 内存监控 # Prometheus + Node Exporter - alert: MemoryUsageHigh expr: (node_memory_MemTotal - node_memory_MemAvailable) / node_memory_MemTotal \u0026gt; 0.8 for: 5m labels: severity: warning # 3. 定期重启 # 对于无法彻底修复的泄漏 # 设置定时重启（如每天凌晨） # 4. 压力测试 # 上线前进行内存压力测试 # 验证长时间运行稳定性 # 5. 资源限制 # 容器内存限制 # systemd 内存限制 经验总结：\n内存泄漏需要代码层面修复 监控可以提前发现问题 临时措施可以争取修复时间 压力测试很重要 资源限制防止影响其他服务 事故 32：线程池耗尽事故 事故现象：\n请求排队等待 响应时间飙升 应用报\u0026quot;Thread pool exhausted\u0026quot; 新请求被拒绝 事故原因：\n并发请求超过线程池容量 线程阻塞未释放 线程池配置过小 外部依赖响应慢 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 1. 查看应用日志 grep -i \u0026#34;thread\\|pool\\|exhausted\u0026#34; /var/log/app/*.log # 2. Java 线程分析 jps -l jstack \u0026lt;PID\u0026gt; | head -100 # 3. 查看线程状态 jstack \u0026lt;PID\u0026gt; | grep -E \u0026#34;RUNNABLE|BLOCKED|WAITING\u0026#34; | sort | uniq -c # 4. 查看线程池状态 # 应用暴露的监控端点 curl http://localhost:8080/actuator/metrics/thread.pool.size # 5. 系统线程查看 ps -eLo pid,tid,class,rtprio,ni,pri,psr,pcpu,stat,wchan:14,comm | grep 进程名 # 6. 查看连接等待 netstat -an | grep ESTABLISHED | wc -l # 7. 检查外部依赖 curl -w \u0026#34;%{time_total}\\n\u0026#34; -o /dev/null -s http://依赖服务/health 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 # 实时查看线程数 watch -n 5 \u0026#39;ps -eLo pid,comm,nlwp | grep 进程名\u0026#39; # 查看线程堆栈 jstack \u0026lt;PID\u0026gt; \u0026gt; thread_dump.txt # 分析阻塞线程 jstack \u0026lt;PID\u0026gt; | grep -A 30 \u0026#34;BLOCKED\u0026#34; # 查看 CPU 等待 top -H -p \u0026lt;PID\u0026gt; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 # 1. 紧急扩容线程池 # 应用配置调整 # application.yml spring: task: execution: pool: core-size: 20 max-size: 100 # 从 50 增加到 100 queue-capacity: 500 # 2. 重启服务 systemctl restart 服务名 # 3. 优化外部调用 # 增加超时时间 # 添加熔断机制 @HystrixCommand(fallbackMethod = \u0026#34;fallback\u0026#34;) public Response callExternal() { return externalService.call(); } # 4. 异步处理 # 将同步调用改为异步 CompletableFuture\u0026lt;Response\u0026gt; future = CompletableFuture.supplyAsync(() -\u0026gt; externalService.call()); # 5. 限流保护 # 限制并发请求数 Semaphore semaphore = new Semaphore(50); semaphore.acquire(); try { // 处理请求 } finally { semaphore.release(); } # 6. 监控告警 # 线程池使用率监控 - alert: ThreadPoolUsageHigh expr: thread_pool_active / thread_pool_max \u0026gt; 0.8 for: 5m labels: severity: warning 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 1. 合理配置线程池 # 根据业务量评估 # 公式：线程数 = CPU 核心数 * (1 + 等待时间/计算时间) # 2. 超时设置 # 所有外部调用设置超时 # 避免线程长期阻塞 # 3. 熔断降级 # 使用 Hystrix/Resilience4j # 外部服务故障时快速失败 # 4. 监控告警 # 线程池使用率监控 # 队列长度监控 # 5. 压力测试 # 模拟高并发验证线程池配置 经验总结：\n线程池配置要合理 外部调用必须设置超时 熔断机制很重要 监控线程池状态 压力测试验证配置 事故 33：死锁事故 事故现象：\n部分请求永久阻塞 数据库事务超时 应用无响应 CPU 使用率低但服务不可用 事故原因：\n代码锁顺序不一致 数据库行锁冲突 资源竞争 嵌套锁导致循环等待 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 1. Java 死锁检测 jps -l jstack \u0026lt;PID\u0026gt; | grep -A 50 \u0026#34;deadlock\u0026#34; # 2. 数据库锁检测 # MySQL SELECT * FROM information_schema.innodb_lock_waits; SELECT * FROM performance_schema.data_locks; # 3. 查看阻塞会话 SELECT blocking_locks.lock_id, blocking_trx.trx_query FROM performance_schema.data_lock_waits JOIN performance_schema.data_locks blocking_locks ON data_lock_waits.blocking_engine_lock_id = blocking_locks.engine_lock_id JOIN performance_schema.innodb_trx blocking_trx ON blocking_locks.engine_transaction_id = blocking_trx.trx_id; # 4. 应用日志 grep -i \u0026#34;deadlock\\|lock\\|timeout\u0026#34; /var/log/app/*.log # 5. 线程 dump 分析 jstack \u0026lt;PID\u0026gt; \u0026gt; thread_dump.txt # 用线程分析工具查看 # 6. 数据库进程 SHOW PROCESSLIST; 使用命令：\n1 2 3 4 5 6 7 8 # 持续监控死锁 watch -n 10 \u0026#39;jstack \u0026lt;PID\u0026gt; | grep -i \u0026#34;deadlock\u0026#34;\u0026#39; # 查看锁等待 jstack \u0026lt;PID\u0026gt; | grep -B 5 \u0026#34;waiting to lock\u0026#34; # 数据库锁监控 mysql -e \u0026#34;SHOW ENGINE INNODB STATUS\\G\u0026#34; | grep -A 20 \u0026#34;LATEST DETECTED DEADLOCK\u0026#34; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 # 1. 紧急终止死锁进程 # Java：重启应用 systemctl restart 服务名 # 数据库：终止阻塞会话 KILL 进程 ID; # 2. 代码修复 # 统一锁获取顺序 // 错误：不同顺序获取锁 synchronized(lockA) { synchronized(lockB) { } } synchronized(lockB) { synchronized(lockA) { } } // 正确：统一顺序 synchronized(lockA) { synchronized(lockB) { } } synchronized(lockA) { synchronized(lockB) { } } # 3. 数据库优化 # 避免长事务 # 合理设计索引 # 使用行锁而非表锁 # 4. 超时设置 # 设置锁超时 SET innodb_lock_wait_timeout = 50; # 5. 死锁检测 # 启用死锁检测 SET innodb_deadlock_detect = ON; # 6. 监控告警 # 死锁次数监控 - alert: DeadlockDetected expr: rate(mysql_innodb_deadlocks[5m]) \u0026gt; 0 for: 1m labels: severity: warning 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 1. 代码规范 # 统一锁获取顺序 # 避免嵌套锁 # 使用高级并发工具（如 ConcurrentHashMap） # 2. 数据库设计 # 合理设计索引 # 避免大事务 # 使用合适的隔离级别 # 3. 超时机制 # 设置锁超时 # 设置事务超时 # 4. 监控告警 # 死锁次数监控 # 锁等待时间监控 # 5. 定期分析 # 分析死锁日志 # 优化热点数据访问 经验总结：\n死锁需要代码层面修复 统一锁顺序是关键 数据库死锁要分析事务 超时机制可以减轻影响 监控死锁发生频率 事故 34：序列化异常事故 事故现象：\n分布式调用失败 缓存读取异常 消息队列消费失败 应用报\u0026quot;ClassNotFoundException\u0026quot; 事故原因：\n类版本不一致 serialVersionUID 变化 依赖库版本冲突 跨语言序列化问题 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 1. 查看应用日志 grep -i \u0026#34;serial\\|class\\|deserialize\u0026#34; /var/log/app/*.log # 2. 检查类版本 javap -serial 类名.class # 3. 查看依赖树 mvn dependency:tree gradle dependencies # 4. 检查序列化数据 # Redis 中查看 redis-cli GET key # 分析序列化格式 # 5. 对比服务版本 # 各微服务版本是否一致 curl http://service/actuator/info # 6. 检查消息队列 # 查看消息内容 kafka-console-consumer.sh --topic topic --from-beginning 使用命令：\n1 2 3 4 5 6 7 8 # 查看类信息 javap -v 类名.class | grep -i \u0026#34;serial\u0026#34; # 查看 JAR 包版本 jar tf 包名.jar | grep -i \u0026#34;manifest\u0026#34; # 检查运行时类 jmap -clstats \u0026lt;PID\u0026gt; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 1. 紧急回滚 # 回滚到兼容版本 kubectl rollout undo deployment/app # 2. 清理缓存 # 删除不兼容的序列化数据 redis-cli DEL key # 或清空缓存 redis-cli FLUSHDB # 3. 统一版本 # 确保所有服务使用相同版本 # 更新依赖 # 4. 修复 serialVersionUID // 保持兼容 private static final long serialVersionUID = 1L; # 5. 使用兼容序列化 // 使用 JSON 而非 Java 原生序列化 // 使用 Protobuf/Avro 等 schema 驱动 # 6. 数据迁移 # 编写数据迁移脚本 # 转换旧格式到新格式 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 1. 序列化规范 # 使用 JSON/Protobuf 等通用格式 # 避免 Java 原生序列化 # 2. 版本管理 # 微服务版本统一升级 # 使用 API 版本控制 # 3. 依赖管理 # 统一依赖版本 # 使用 BOM 管理 # 4. 兼容性测试 # 上线前测试序列化兼容 # 灰度发布验证 # 5. 缓存策略 # 缓存设置合理过期时间 # 避免长期存储序列化数据 经验总结：\n避免使用 Java 原生序列化 微服务版本要统一管理 缓存数据要有过期策略 序列化格式要向前兼容 灰度发布可以发现兼容问题 事故 35：配置热更新失败事故 事故现象：\n配置变更后服务异常 部分节点配置不一致 配置回滚失败 服务间歇性失败 事故原因：\n配置中心故障 配置推送失败 配置格式错误 配置校验缺失 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 1. 检查配置中心状态 curl http://config-server/actuator/health # 2. 查看配置推送日志 grep -i \u0026#34;config\\|refresh\u0026#34; /var/log/app/*.log # 3. 对比各节点配置 for node in node1 node2 node3; do echo \u0026#34;=== $node ===\u0026#34; curl http://$node:8080/actuator/configprops done # 4. 检查配置内容 # 配置中心后台查看 # 对比变更前后差异 # 5. 查看应用日志 grep -i \u0026#34;failed\\|error\u0026#34; /var/log/app/*.log | tail -50 # 6. 检查配置刷新 # Spring Cloud curl -X POST http://localhost:8080/actuator/refresh 使用命令：\n1 2 3 4 5 6 7 8 9 # 查看当前配置 curl http://localhost:8080/actuator/env # 查看配置历史 # 配置中心版本历史 git log config-repo # 测试配置 curl -X POST http://localhost:8080/actuator/refresh 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 1. 紧急回滚配置 # 配置中心回滚到上一版本 # 或手动修改配置 # 2. 重启服务 # 强制重新加载配置 systemctl restart 服务名 # 3. 修复配置 # 修正格式错误 # 补充缺失配置 # 4. 分批推送 # 先推送部分节点 # 验证后再全量推送 # 5. 本地配置兜底 # 保留本地配置文件 # 配置中心故障时使用 # 6. 验证配置 curl http://localhost:8080/actuator/env | grep 配置项 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 1. 配置校验 # 配置变更时自动校验格式 # 使用 JSON Schema 验证 # 2. 灰度推送 # 先推送 10% 节点 # 验证后再全量 # 3. 版本控制 # 配置存入 Git # 变更有记录可追溯 # 4. 配置备份 # 定期备份配置 # 支持快速回滚 # 5. 监控告警 # 配置推送失败告警 # 配置不一致告警 经验总结：\n配置变更要有校验 灰度推送降低风险 配置版本要可追溯 本地配置做兜底 监控配置推送状态 事故 36：分布式锁失效导致定时任务重复执行 事故现象：\n每日结算任务被执行了 3 次，导致用户利息多算 报表数据重复统计，总数翻倍 日志显示多个应用实例在同一时间进入了临界区代码 Redis 中锁 Key 不存在或已过期 事故原因：\n使用 SETNX 实现锁时未设置过期时间，死锁后无法释放 设置了过期时间，但业务执行时间 \u0026gt; 锁过期时间，锁自动释放，其他节点趁虚而入 主从切换导致锁丢失（Redis 异步复制，Master 挂掉时锁未同步到 Slave） 代码逻辑中未在 finally 块释放锁，异常发生时锁未释放 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 1. 检查 Redis 锁状态 redis-cli GET lock:job:daily_settlement # 返回 nil，说明锁已丢失 # 2. 查看应用日志 grep \u0026#34;Start processing job\u0026#34; app.log # 发现同一时间点，不同 IP 的实例都打印了该日志 # 3. 分析 Redis 慢日志 redis-cli SLOWLOG GET 10 # 检查是否有耗时操作导致锁获取延迟 # 4. 检查 Redis 集群状态 redis-cli CLUSTER INFO # 确认故障期间是否发生了主从切换 (cluster_known_nodes, cluster_slots_fail) # 5. 代码审查 # 检查锁的实现逻辑，是否使用了看门狗（WatchDog）机制 使用命令：\n1 2 3 4 5 # 模拟锁竞争测试 # 脚本 A：持锁 5 秒 redis-cli SETNX lock:test 1 \u0026amp;\u0026amp; redis-cli EXPIRE lock:test 5 # 脚本 B：1 秒后尝试获锁 sleep 1 \u0026amp;\u0026amp; redis-cli SETNX lock:test 1 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // 1. 引入看门狗机制（推荐 Redisson） RLock lock = redisson.getLock(\u0026#34;lock:job:daily_settlement\u0026#34;); // lock.lock() 默认开启看门狗，只要线程存活，锁会自动续期 lock.lock(); try { // 业务逻辑（即使超过 30 秒也不会丢锁） processSettlement(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } // 2. 使用 RedLock 算法（极端强一致场景） // 向 N/2+1 个独立的 Redis 节点申请锁，提高可靠性 // 注意：性能较低，仅用于金融级核心数据 // 3. 数据库唯一索引兜底 // 即使锁失效，DB 唯一约束也能防止重复插入 ALTER TABLE settlement_log ADD UNIQUE INDEX uk_job_date (job_name, exec_date); // 4. 幂等性设计 // 业务逻辑内部判断状态，若已处理则直接返回 if (alreadyProcessed(date)) return; 预防措施：\n框架选型：严禁手写简单的 setnx 逻辑，统一使用 Redisson、Curator 等成熟库。 双重保障：分布式锁 + 数据库唯一键/状态机，构建双重防线。 监控告警：监控任务执行次数，若单日执行\u0026gt;1 次立即告警。 故障演练：模拟 Redis 主从切换，验证锁是否丢失。 经验总结：\n任何分布式锁都不是 100% 可靠的，必须有业务层幂等兜底。 锁的过期时间必须大于业务最大执行时间，或使用自动续期。 Redis 主从异步复制天生存在锁丢失风险，核心业务需评估是否改用 ZooKeeper/Etcd。 事故 37：消息队列消费者阻塞导致积压 事故现象：\n消息队列 Lag（积压量）从 0 飙升到 500 万+ 实时订单状态更新延迟超过 2 小时 消费者进程存活，但日志不再滚动 磁盘空间告警（MQ Broker 存满） 事故原因：\n消费者代码中出现死循环或长时间 Thread.sleep 调用第三方接口超时（无超时配置），线程永久阻塞 某条“毒丸消息”（Poison Pill）导致反序列化失败，不断重试阻塞队列 数据库连接池耗尽，消费者等待 DB 连接 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 1. 查看消费组状态 kafka-consumer-groups.sh --bootstrap-server kafka:9092 --describe --group order-group # 观察 LAG 列持续增加，CURRENT-OFFSET 停滞 # 2. 检查消费者线程堆栈 jps -l jstack \u0026lt;consumer_pid\u0026gt; | grep -A 20 \u0026#34;RUNNABLE\\|BLOCKED\u0026#34; # 发现大量线程阻塞在 `SocketRead` 或 `Wait` 状态 # 3. 查看消费者日志 tail -f consumer.log | grep -i \u0026#34;error\\|exception\\|timeout\u0026#34; # 发现大量 \u0026#34;DeserializationException\u0026#34; 或 \u0026#34;ConnectionTimeout\u0026#34; # 4. 检查 DB 连接池 curl http://localhost:8080/actuator/metrics/hikaricp.connections.active # 发现连接数已满 # 5. 定位毒丸消息 # 查看具体卡住的消息内容 kafka-console-consumer.sh --topic order-topic --partition 0 --offset \u0026lt;offset\u0026gt; --max-messages 1 使用命令：\n1 2 3 # 跳过特定消息（紧急止损） # 将消费组 Offset 重置到下一条 kafka-consumer-groups.sh --bootstrap-server kafka:9092 --reset-offsets --to-offset \u0026lt;next_offset\u0026gt; --execute --group order-group --topic order-topic 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 1. 紧急扩容消费者 # 增加 Partition 数量（若允许重排序） kafka-topics.sh --alter --topic order-topic --partitions 48 # 扩容消费者实例 kubectl scale deployment consumer --replicas=48 # 2. 修复代码缺陷 # 添加超时配置：socket.timeout.ms=5000 # 捕获异常并记录错误日志，避免死循环 try { process(msg); } catch (Exception e) { log.error(\u0026#34;Process failed\u0026#34;, e); // 发送死信队列，不阻塞主流程 sendToDLQ(msg); ack.acknowledge(); } # 3. 处理毒丸消息 # 将坏消息手动移入死信队列（DLQ） # 跳过该 Offset，继续消费后续正常消息 # 4. 优化 DB 连接 # 扩容连接池，或优化慢 SQL 释放连接 预防措施：\n超时控制：所有外部调用（DB、RPC、HTTP）必须设置严格超时。 死信队列：消费失败 N 次后自动转入 DLQ，人工介入处理，不阻塞主队列。 监控告警：监控 Lag 增长率，设定阈值（如 Lag \u0026gt; 10000 持续 5 分钟）。 批量处理：采用批量拉取、批量处理模式，提升吞吐量。 经验总结：\n一条坏消息可以拖垮整个消费组，必须隔离处理。 消费者必须具备“快速失败”能力，不能无限等待。 扩容 Partition 是解决积压最快的手段，但需注意消息顺序性。 事故 38：第三方 API 超时拖垮整个系统 事故现象：\n核心下单接口响应时间从 200ms 变为 30s+ Tomcat 线程池全部耗尽，新请求被拒绝 故障源头是“物流查询”接口，该接口依赖的外部服务商挂了 整个电商网站不可用 事故原因：\n代码中同步调用第三方接口，且未设置超时时间（默认无限等待） 没有熔断机制，下游故障直接传导至上游 线程池未隔离，物流查询占用了所有 Tomcat 线程 重试策略不当，失败后立即重试，加剧负载 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 链路追踪分析 # SkyWalking 显示 Span 卡在 `LogisticsService.query`，耗时 30s+ # 2. 检查线程堆栈 jstack \u0026lt;pid\u0026gt; | grep -c \u0026#34;WAITING\u0026#34; # 发现 200 个线程全部阻塞在 HTTP Client 的 `read` 方法上 # 3. 测试第三方接口 curl -v --connect-timeout 5 https://logistics-api.com/query # 连接超时或无响应 # 4. 查看代码 grep -A 5 \u0026#34;httpClient.execute\u0026#34; LogisticsService.java # 发现未配置 `RequestConfig` 超时参数 使用命令：\n1 2 3 # 模拟外部依赖故障 # 使用 iptables 丢弃包 iptables -A OUTPUT -d \u0026lt;third_party_ip\u0026gt; -j DROP 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 1. 设置严格超时 RequestConfig config = RequestConfig.custom() .setConnectTimeout(3000) // 连接超时 3s .setSocketTimeout(3000) // 读取超时 3s .build(); // 2. 实施熔断降级 (Resilience4j/Sentinel) @CircuitBreaker(name = \u0026#34;logisticsService\u0026#34;, fallbackMethod = \u0026#34;fallbackQuery\u0026#34;) public LogisticsInfo query(String orderId) { return httpClient.execute(...); } // 降级逻辑：返回默认值或缓存数据 public LogisticsInfo fallbackQuery(String orderId, Exception e) { log.warn(\u0026#34;Logistics service unavailable, return default\u0026#34;, e); return new LogisticsInfo(\u0026#34;UNKNOWN\u0026#34;, \u0026#34;稍后查询\u0026#34;); } // 3. 线程池隔离 // 为物流查询分配独立线程池，不影响下单主流程 ExecutorService logisticsPool = Executors.newFixedThreadPool(10); // 4. 异步化 // 非核心路径改为异步回调，不阻塞主线程 CompletableFuture.supplyAsync(() -\u0026gt; query(orderId), logisticsPool); 预防措施：\n依赖治理：梳理所有外部依赖，区分核心与非核心。 三原则：所有外部调用必须遵循“超时、熔断、降级”三原则。 资源隔离：使用舱壁模式（Bulkhead），为不同依赖分配独立资源。 Mock 测试：定期模拟第三方故障，验证系统韧性。 经验总结：\n系统的可用性取决于最弱的那个外部依赖。 永远不要信任第三方服务，假设它们随时会挂。 同步变异步，阻塞变非阻塞，是解耦的关键。 事故 39：微服务雪崩（级联故障） 事故现象：\n积分服务响应慢，导致用户服务超时 用户服务超时，导致订单服务线程池满 订单服务不可用，导致网关所有请求 502 整个集群所有服务不可用，监控全线飘红 事故原因：\n缺乏熔断机制，故障沿调用链向上传播 超时时间设置不合理（上层 \u0026gt; 下层），导致等待时间叠加 重试风暴：上层不断重试下层，放大了故障流量 资源未隔离，单个慢服务拖垮共享线程池 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 分析调用拓扑 # 通过链路追踪图，找到故障传播路径：Gateway -\u0026gt; Order -\u0026gt; User -\u0026gt; Points # 2. 检查各服务线程池 for svc in gateway order user points; do curl http://$svc/actuator/metrics/thread.pool.active done # 发现所有服务线程池均满 # 3. 查看重试配置 grep -r \u0026#34;maxAttempts\u0026#34; config/ # 发现默认重试 3 次，且无退避策略 # 4. 检查超时设置 # 发现 Order 调用 User 超时设为 10s，User 调用 Points 设为 10s # 总等待时间可能达到 20s+ 使用命令：\n1 2 3 # 紧急限流 # 在网关层直接拒绝指向故障服务的流量 istioctl apply -f rate-limit-order.yaml 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 1. 调整超时层级 # 确保 上游超时 \u0026lt; 下游超时 # Gateway(3s) -\u0026gt; Order(2s) -\u0026gt; User(1s) -\u0026gt; Points(0.5s) # 2. 配置熔断器 # 当错误率 \u0026gt; 50% 或 响应时间 \u0026gt; 1s，自动熔断 CircuitBreakerConfig: failureRateThreshold: 50 waitDurationInOpenState: 30s slidingWindowSize: 100 # 3. 禁止盲目重试 # 仅对网络波动（503/Timeout）重试，且增加指数退避 RetryConfig: maxAttempts: 3 backoff: multiplier: 2 minDelay: 100ms # 4. 舱壁隔离 # 每个依赖使用独立线程池 @Bulkhead(name = \u0026#34;userService\u0026#34;, type = Bulkhead.Type.THREADPOOL) 预防措施：\n架构原则：遵循“依赖必熔断、调用必超时、资源必隔离”。 全链路压测：模拟下游故障，验证上游是否能快速失败。 监控大盘：建立依赖关系拓扑图，实时展示健康度。 降级预案：核心服务故障时，非核心功能自动降级（如不显示积分）。 经验总结：\n雪崩往往始于一个不起眼的边缘服务。 没有超时的调用就是定时炸弹。 重试必须谨慎，避免放大故障。 事故 40：服务注册中心过载导致注册失败 事故现象：\n新发布的 Pod 一直无法启动，报错 Registration failed 现有服务心跳超时，被注册中心误剔除，流量中断 注册中心（Eureka/Nacos）CPU 100%，接口响应极慢 K8s 弹性伸缩后，实例数激增导致注册风暴 事故原因：\n注册中心集群规模过小，无法承载突发注册请求 客户端心跳频率过高（默认 5s），产生大量网络 IO 大量临时测试实例未清理，占用内存和连接 注册中心未开启集群模式，单点性能瓶颈 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 1. 查看注册中心监控 # CPU, Memory, Network IO, Register QPS # 发现 Register 接口延迟高达 2s # 2. 检查客户端日志 grep \u0026#34;register failed\\|heartbeat timeout\u0026#34; app.log # 3. 统计实例数量 curl http://nacos:8848/nacos/v1/ns/instance/list?serviceName=all # 发现实例数超过 5000，包含大量测试环境实例 # 4. 检查网络 # 客户端到注册中心的网络带宽是否打满 使用命令：\n1 2 # 查看 Nacos 集群状态 curl http://nacos:8848/nacos/v1/ns/operator/metrics 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 1. 调整心跳频率 spring: cloud: nacos: discovery: heart-beat-interval: 10000 # 从 5s 改为 10s heart-beat-timeout: 30000 ip-delete-timeout: 90000 # 2. 扩容注册中心 # 增加节点，组建集群 # 使用负载均衡分发注册请求 # 3. 清理无效实例 # 下线测试环境实例 # 设置 TTL，自动清理僵尸节点 # 4. 开启客户端缓存 # 客户端本地缓存服务列表，减少拉取频率 预防措施：\n高可用部署：注册中心必须集群化，且具备自动扩缩容能力。 参数调优：根据规模调整心跳间隔，避免“心跳风暴”。 环境隔离：生产、测试环境注册中心物理隔离。 本地缓存：客户端必须具备本地缓存和降级能力。 经验总结：\n注册中心是微服务的“电话簿”，一旦失联，整个系统瘫痪。 心跳频率不是越快越好，需在实时性和负载间平衡。 客户端缓存是应对注册中心抖动的缓冲垫。 事故 41：分布式事务数据不一致（最终一致性失效） 事故现象：\n支付成功但订单状态未更新 库存扣减了但订单未生成 对账发现大量“长款”或“短款” 补偿任务未触发，或触发后执行失败 事故原因：\n本地消息表未写入成功，导致 MQ 消息未发送 MQ 消息丢失（未持久化/未 ACK） 补偿任务（Job）宕机或未配置 补偿逻辑未实现幂等，导致重复执行或执行失败 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 -- 1. 查询本地消息表 SELECT * FROM local_message_table WHERE status = \u0026#39;TO_SEND\u0026#39;; -- 2. 检查 MQ 状态 # 查看是否有消息积压或未 ACK kafka-consumer-groups.sh --describe --group tx-group -- 3. 检查补偿任务日志 grep \u0026#34;Compensation Job\u0026#34; scheduler.log | grep \u0026#34;Error\u0026#34; -- 4. 比对业务数据 # 支付表 vs 订单表 SELECT p.id, o.id FROM payment p LEFT JOIN orders o ON p.order_id = o.id WHERE o.id IS NULL; 使用命令：\n1 2 # 手动触发补偿 curl -X POST http://scheduler/api/compensate?orderId=123 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 1. 确保本地消息与业务事务原子性 @Transactional public void createOrder(Order order) { orderMapper.insert(order); // 在同一事务中写入本地消息表 messageMapper.insert(new Message(order.getId(), \u0026#34;CREATE_ORDER\u0026#34;)); } // 2. 可靠投递消息 // 定时任务扫描本地消息表，发送至 MQ @Scheduled(fixedDelay = 5000) public void scanAndSend() { List\u0026lt;Message\u0026gt; messages = messageMapper.findToSend(); for (Message m : messages) { kafkaTemplate.send(m.getTopic(), m.getBody()); // 发送成功后更新状态 messageMapper.updateStatus(m.getId(), \u0026#34;SENT\u0026#34;); } } // 3. 消费端幂等 // 利用数据库唯一键或状态机防止重复消费 预防措施：\n本地消息表：保证业务操作与消息记录在同一本地事务中。 定时对账：建立 T+1 或准实时对账系统，自动发现并修复差异。 消息可靠性：MQ 开启持久化，消费者手动 ACK。 幂等设计：补偿逻辑必须幂等。 经验总结：\n分布式事务没有银弹，最终一致性是主流方案。 本地消息表是实现最终一致性的经典模式。 对账系统是数据一致性的最后一道防线。 事故 42：数据库自增 ID 耗尽 事故现象：\n插入新订单时报错 Duplicate entry '2147483647' for key 'PRIMARY' 业务完全停止写入 表结构显示主键为 INT，值已达到最大值 扩展字段或分库分表未及时实施 事故原因：\n建表时使用 INT (4 字节) 而非 BIGINT (8 字节) 业务增长过快，ID 达到上限 (21 亿) 未提前规划 ID 生成策略（如雪花算法） 历史数据迁移导致 ID 跳跃式增长 排查过程：\n1 2 3 4 5 6 7 8 9 -- 1. 查看表结构 SHOW CREATE TABLE orders; -- 确认主键类型是否为 INT -- 2. 查看当前最大 ID SELECT MAX(id) FROM orders; -- 3. 检查自增步长 SHOW VARIABLES LIKE \u0026#39;auto_increment_%\u0026#39;; 使用命令：\n1 2 # 估算剩余时间 # (2147483647 - current_max) / daily_increase_rate 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 -- 1. 紧急修改字段类型（在线 DDL，需评估锁表风险） ALTER TABLE orders MODIFY COLUMN id BIGINT UNSIGNED NOT NULL; -- 注意：MySQL 5.6+ 支持 Online DDL，但仍需谨慎 -- 2. 重建表（如果 Online DDL 不可用） -- 创建新表 -\u0026gt; 双写 -\u0026gt; 数据迁移 -\u0026gt; 切换 -- 3. 切换 ID 生成策略 # 弃用数据库自增，改用应用层雪花算法 (Snowflake) # 或号段模式 (Leaf) -- 4. 分库分表 # 如果单表数据量过大，借此机会实施分库分表 预防措施：\n规范设计：核心业务主键一律使用 BIGINT。 监控预警：监控主键使用率，达到 80% 即告警。 ID 策略：高并发场景推荐使用雪花算法或号段模式。 容量规划：定期评估数据增长速度。 经验总结：\nINT 溢出是低级但致命的错误。 修改主键类型是高风险操作，必须在低峰期进行。 去中心化 ID 生成策略是趋势。 事故 43：长事务导致数据库锁等待超时 事故现象：\n大量接口报错 Lock wait timeout exceeded; try restarting transaction 数据库 CPU 不高，但连接数爆满 应用日志显示大量事务回滚 某个后台导出任务运行了 30 分钟未结束 事故原因：\n大事务：在一个事务中处理了海量数据（如全表更新、大文件导出） 事务中包含 RPC/HTTP 调用，外部服务慢导致事务挂起 未提交/未回滚：代码异常捕获后未正确处理事务 索引缺失：更新操作导致全表锁（Gap Lock） 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 -- 1. 查看长事务 SELECT * FROM information_schema.innodb_trx WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) \u0026gt; 60; -- 2. 查看锁等待 SELECT * FROM performance_schema.data_lock_waits; -- 3. 查看进程列表 SHOW PROCESSLIST; # 寻找 State 为 \u0026#39;updating\u0026#39;, \u0026#39;locking\u0026#39; 且 Time 很大的进程 -- 4. 分析 SQL # 检查大事务执行的 SQL 是否走了索引 EXPLAIN SELECT ...; 使用命令：\n1 2 # 紧急 Kill 长事务 KILL \u0026lt;thread_id\u0026gt;; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 1. 拆分大事务 // 将大批量操作拆分为小批次（如每 1000 条提交一次） for (List\u0026lt;Item\u0026gt; batch : batches) { transactionTemplate.execute(status -\u0026gt; { batchUpdate(batch); return null; }); } // 2. 移除事务中的远程调用 // 先查数据 -\u0026gt; 提交事务 -\u0026gt; 再调用 RPC -\u0026gt; 异步更新结果 @Transactional public void prepareData() { ... } // 非事务 public void callExternal() { ... } // 3. 优化索引 // 确保更新条件命中索引，避免 Gap Lock // 4. 设置事务超时 @Transactional(timeout = 30) public void businessMethod() { ... } 预防措施：\n事务规范：严禁在事务中进行 RPC/HTTP 调用。 小事务原则：事务粒度尽可能小，只包裹必要的 DB 操作。 监控告警：监控长事务数量，超过阈值立即告警。 SQL 审计：上线前审核 SQL 执行计划。 经验总结：\n长事务是数据库性能的杀手。 事务内调用外部服务是架构大忌。 快速失败比长时间等待更保护系统。 事故 44：分库分表路由规则配置错误 事故现象：\n查询特定用户数据返回空，但该数据实际存在 写入数据成功，但读取时提示“表不存在” 扩容后，旧数据无法访问 应用日志报错 Table 'db_0.t_order_9' doesn't exist 事故原因：\n分片算法配置错误（如模数与实际表数不一致） 扩容后未更新路由规则配置 分片键选择错误，导致路由计算偏差 配置文件未同步到所有应用实例 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 开启 SQL 打印 # 查看 ShardingSphere 实际路由到的物理表名 logging.level.org.apache.shardingsphere=DEBUG # 2. 手动计算路由 # 根据算法 hash(id) % N，手动计算应落在哪个表 # 直接登录该物理表查询验证 # 3. 检查配置文件 cat sharding-config.yaml # 核对 actual-data-nodes, algorithm-expression # 4. 检查版本一致性 # 确认所有 Pod 加载的配置版本一致 使用命令：\n1 2 3 4 -- 直接登录物理库验证 USE db_order_0; SHOW TABLES; SELECT * FROM t_order_1 WHERE id = 12345; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 修正路由配置 sharding-rule: tables: t_order: actual-data-nodes: ds$-\u0026gt;{0..3}.t_order$-\u0026gt;{0..7} # 修正表数量 table-strategy: inline: sharding-column: order_id algorithm-expression: t_order$-\u0026gt;{order_id % 8} # 2. 数据迁移 # 如果是因为扩容导致的，需执行数据迁移脚本 # 将旧表数据 re-sharding 到新表 # 3. 配置热更新 # 使用配置中心管理分片规则，支持动态刷新 预防措施：\n自动化测试：编写单元测试覆盖所有分片路由场景。 管理工具：提供可视化工具查询数据物理位置。 灰度发布：路由规则变更需灰度验证。 文档记录：详细记录分片算法和扩容历史。 经验总结：\n路由规则错误会导致数据“隐形”。 分库分表扩容是高风险操作，需精密计划。 配置一致性至关重要。 事故 45：数据迁移脚本 Bug 导致数据丢失 事故现象：\n新版本上线后，部分用户历史数据消失 数据迁移日志显示“成功”，但实际数据未转移 回滚代码后，数据仍无法恢复 发现迁移脚本中有 DELETE 或 TRUNCATE 误操作 事故原因：\n迁移脚本逻辑错误（如条件判断失误，删除了活跃数据） 未在测试环境进行全量数据演练 脚本未做备份直接执行 缺乏双人复核机制 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 检查迁移日志 cat migration.log | grep -i \u0026#34;delete\\|truncate\\|error\u0026#34; # 2. 对比数据量 SELECT COUNT(*) FROM old_table; SELECT COUNT(*) FROM new_table; # 发现数量严重不符 # 3. 检查 Binlog mysqlbinlog mysql-bin.000XXX | grep -i \u0026#34;delete\u0026#34; # 定位误删除的具体时间和 SQL # 4. 审查脚本代码 # 发现 WHERE 条件缺失或错误 使用命令：\n1 2 # 尝试从 Binlog 恢复 mysqlbinlog --start-datetime=\u0026#34;2024-03-24 10:00:00\u0026#34; --stop-datetime=\u0026#34;2024-03-24 10:10:00\u0026#34; mysql-bin.000XXX | mysql -u root -p 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 紧急止损 # 停止所有写入，防止数据进一步破坏 SET GLOBAL read_only = ON; # 2. 数据恢复 # 从最近的冷备份恢复 # 或使用 Binlog 回放误删除前的数据 # 3. 修复脚本 # 修正逻辑，增加 WHERE 条件 # 增加预检查（Pre-check）步骤 # 4. 重新迁移 # 在测试环境验证通过后，再次执行 预防措施：\n备份先行：执行任何 DDL/DML 前必须备份。 全量演练：在仿真环境使用生产数据量级进行演练。 可回滚设计：迁移脚本必须配套回滚脚本。 双人复核：高危脚本需经过架构师和 DBA 双重审查。 经验总结：\n数据迁移是最高风险的操作之一。 没有经过全量演练的迁移脚本不可信。 备份是最后的救命稻草。 事故 46：备份文件损坏且无法恢复 事故现象：\n数据库崩溃，尝试从备份恢复 gunzip 报错 invalid compressed data mysql 导入报错 Syntax error 或文件截断 追溯发现过去 7 天的备份全部损坏 事故原因：\n备份脚本只检查退出码，未校验文件完整性 磁盘静默错误（Bit Rot）导致文件损坏 备份过程中磁盘空间满，文件写入不完整 从未进行过恢复演练，盲目相信备份成功日志 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 尝试解压测试 gunzip -t backup_20240320.sql.gz # 报错：CRC check failed # 2. 检查磁盘健康 smartctl -a /dev/sdb # 发现 Reallocated_Sector_Ct 很高 # 3. 检查备份脚本 cat backup.sh # 发现缺少校验步骤 # 4. 追溯历史备份 for f in /backup/*.gz; do gunzip -t $f || echo \u0026#34;$f FAILED\u0026#34;; done 使用命令：\n1 2 3 # 生成并校验 MD5 md5sum backup.sql \u0026gt; backup.sql.md5 md5sum -c backup.sql.md5 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # 1. 尝试修复 # 使用 gzip -d -f 强制解压（可能丢失部分数据） # 或使用专业工具修复 # 2. 寻找替代备份 # 从异地灾备中心、从库、或更早的冷备中寻找可用文件 # 3. 重建备份体系 # 修改脚本：备份后立即校验 mysqldump ... | gzip \u0026gt; backup.sql.gz if [ $? -eq 0 ]; then md5sum backup.sql.gz \u0026gt; backup.sql.gz.md5 # 试恢复验证 gunzip -c backup.sql.gz | mysql --defaults-file=/dev/null -e \u0026#34;SELECT 1\u0026#34; if [ $? -eq 0 ]; then echo \u0026#34;Backup Verified OK\u0026#34; aws s3 cp backup.sql.gz s3://bucket/ else echo \u0026#34;Verification FAILED\u0026#34; exit 1 fi fi # 4. 实施 3-2-1 备份策略 # 3 份副本，2 种介质，1 个异地 预防措施：\n强制校验：备份后必须校验文件完整性和可恢复性。 定期演练：每周/月自动执行恢复演练。 多地冗余：避免单点存储风险。 监控告警：监控备份文件大小、校验和、执行时间。 经验总结：\n没有经过恢复验证的备份等于没有备份。 磁盘静默错误是隐形杀手，校验和必不可少。 自动化验证是备份系统的核心组件。 事故 47：测试数据污染生产环境 事故现象：\n生产库中出现大量名为 \u0026ldquo;test\u0026rdquo;, \u0026ldquo;aaa\u0026rdquo;, \u0026ldquo;111\u0026rdquo; 的用户 订单表中出现金额为 0 或负数的异常数据 短信发送记录显示发给了内部测试手机号 用户投诉收到奇怪的通知 事故原因：\n开发/测试人员误连生产数据库 配置文件中测试环境与生产环境地址混淆 缺乏权限控制，测试账号拥有生产写权限 自动化测试脚本未区分环境，直接执行 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 -- 1. 查询异常数据 SELECT * FROM users WHERE name LIKE \u0026#39;%test%\u0026#39; OR phone LIKE \u0026#39;1380000%\u0026#39;; -- 2. 检查连接日志 SELECT user, host, db FROM mysql.general_log WHERE command_type = \u0026#39;Connect\u0026#39;; # 查找来自测试网段的连接 -- 3. 审查操作记录 # 检查应用日志，确认是哪个服务写入的 -- 4. 检查配置文件 grep \u0026#34;jdbc.url\u0026#34; config-prod.yml 使用命令：\n1 2 3 # 紧急清理 DELETE FROM users WHERE name LIKE \u0026#39;%test%\u0026#39;; # 注意：需先备份，并确认无误删 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 紧急清理数据 # 备份异常数据后删除 mysqldump -t users -w \u0026#34;name LIKE \u0026#39;%test%\u0026#39;\u0026#34; \u0026gt; dirty_data_backup.sql DELETE FROM users WHERE name LIKE \u0026#39;%test%\u0026#39;; # 2. 网络隔离 # 生产数据库禁止从测试网段访问 # 配置安全组/防火墙白名单 # 3. 权限最小化 # 回收测试人员的生产写权限 # 测试账号只能访问测试库 # 4. 配置分离 # 使用配置中心区分环境，禁止硬编码 # 增加环境标识校验（启动时检查 ENV 变量） 预防措施：\n网络隔离：生产与测试网络物理或逻辑隔离。 权限管控：严格执行最小权限原则。 环境标识：应用启动时校验环境标签，防止误启动。 数据脱敏：生产数据导出到测试环境必须脱敏。 经验总结：\n人为误操作是数据安全的最大威胁。 网络隔离和权限控制是防止误操作的硬屏障。 永远不要信任人的自觉性，要靠制度和技术。 事故 48：敏感数据泄露（日志打印） 事故现象：\n安全团队通报：GitHub 上发现公司生产日志，包含用户明文密码、手机号 合规部门介入，面临法律风险 日志系统中可搜索到大量身份证号、银行卡号 原因是开发调试时打开了 DEBUG 日志，打印了完整对象 事故原因：\n代码中直接 log.info(\u0026quot;user: {}\u0026quot;, user)，未脱敏 生产环境日志级别配置为 DEBUG 日志采集后未进行二次脱敏 日志平台权限管控不严，全员可读 排查过程：\n1 2 3 4 5 6 7 8 9 # 1. 搜索敏感关键词 grep -r \u0026#34;password\\|id_card\\|phone\u0026#34; /var/log/app/ # 2. 检查日志配置 cat logback-spring.xml # 确认 root level 是否为 DEBUG # 3. 审查代码 grep -r \u0026#34;log.*user\u0026#34; src/ 使用命令：\n1 2 3 # 紧急清理日志 find /var/log/app -name \u0026#34;*.log\u0026#34; -exec shred -u {} \\; # 注意：需先确认是否影响排障 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 1. 代码脱敏 // 重写 toString 方法或使用脱敏工具 log.info(\u0026#34;user: {}\u0026#34;, MaskUtils.mask(user)); // 2. 调整日志级别 # 生产环境严禁 DEBUG/INFO 打印敏感字段 # 仅保留 ERROR 和必要的 WARN // 3. 日志采集脱敏 # 在 Filebeat/Fluentd 插件中配置正则替换 processors: - replace: field: message pattern: \u0026#34;(\\\\d{3})\\\\d{4}(\\\\d{4})\u0026#34; replacement: \u0026#34;$1****$2\u0026#34; // 4. 权限管控 # 日志平台设置 RBAC，敏感字段仅授权安全团队查看 预防措施：\n代码扫描：CI 流水线集成敏感信息扫描工具。 脱敏规范：制定严格的日志打印规范。 自动化脱敏：在采集层统一脱敏，作为最后一道防线。 审计监控：监控日志中敏感词的出现频率。 经验总结：\n日志是数据泄露的重灾区。 永远不要在日志中打印明文敏感信息。 脱敏必须多层防御（代码层 + 采集层）。 事故 49：数据同步延迟导致读写不一致 事故现象：\n用户刚修改个人资料，刷新页面仍显示旧数据 后台管理系统查不到刚创建的订单 主从数据库同步延迟（Seconds_Behind_Master）高达 300 秒 从库 CPU 100%，回放慢 事故原因：\n主库写入压力过大，产生大量 Binlog 从库硬件配置低，回放速度跟不上 大事务在主库执行，导致从库单线程回放阻塞 网络带宽瓶颈，Binlog 传输慢 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -- 1. 查看同步状态 SHOW SLAVE STATUS\\G # 关注 Seconds_Behind_Master, Slave_SQL_Running_State -- 2. 查看主库负载 SHOW PROCESSLIST; # 查找大事务或高频写入 -- 3. 查看从库负载 top iostat -x 1 # 确认是否是 IO 或 CPU 瓶颈 -- 4. 检查网络 iftop -i eth0 使用命令：\n1 2 3 4 # 跳过错误（谨慎） STOP SLAVE; SET GLOBAL sql_slave_skip_counter = 1; START SLAVE; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 紧急切换读主 # 修改应用配置，将读流量切回主库（牺牲主库性能保一致性） # 或通过中间件强制路由到主库 # 2. 优化从库 # 升级从库硬件（CPU/SSD） # 开启并行复制（MySQL 5.7+） SET GLOBAL slave_parallel_workers = 16; # 3. 消除大事务 # 在主库拆分大事务，减少从库回放阻塞 # 4. 架构调整 # 对于强一致场景，强制走主库 # 引入缓存层，写后更新缓存 预防措施：\n硬件对等：从库配置应尽量接近主库。 并行复制：开启多线程回放。 监控告警：监控同步延迟，超过阈值（如 10s）告警。 业务容忍：业务层需接受最终一致性，或强制读主。 经验总结：\n主从延迟是读写分离架构的固有痛点。 大事务是同步延迟的主要推手。 强一致场景不要依赖从库。 事故 50：归档策略错误导致活跃数据被误删 事故现象：\n用户查询半年前的订单，提示“数据不存在” 运营报表统计数据大幅减少 发现归档脚本将最近 3 个月的活跃数据也删除了 备份中也没有这些数据（因为归档后即删除） 事故原因：\n归档脚本的时间条件写错（如 date \u0026lt; NOW() - 3 MONTH 写成了 date \u0026gt; ... 或逻辑反了） 未区分“已完成”和“进行中”的状态，误删了长期未结单的活跃数据 脚本未经过测试直接在生产执行 删除前未做二次备份 排查过程：\n1 2 3 4 5 6 7 8 9 10 # 1. 检查归档脚本 cat archive_job.sh # 发现 SQL 逻辑错误：DELETE FROM orders WHERE create_time \u0026gt; \u0026#39;2023-01-01\u0026#39; (本应是 \u0026lt;) # 2. 检查执行日志 cat archive.log # 确认删除了多少行数据 # 3. 检查备份 # 确认删除前的备份是否可用 使用命令：\n1 2 3 # 尝试从 Binlog 恢复 mysqlbinlog --start-datetime=\u0026#34;...\u0026#34; --stop-datetime=\u0026#34;...\u0026#34; mysql-bin.xxx | grep -v \u0026#34;^DELETE\u0026#34; | mysql # 或者反向解析 Binlog 生成 INSERT 语句 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 紧急停止脚本 # Kill 正在运行的归档进程 # 2. 数据恢复 # 从冷备份恢复被误删的数据 # 或使用 Binlog 反向解析恢复 # 3. 修复脚本 # 修正时间逻辑 # 增加状态判断：AND status = \u0026#39;COMPLETED\u0026#39; # 增加 Dry-Run 模式：先 SELECT 确认数据范围，再 DELETE # 4. 增加保护机制 # 删除操作需二次确认 # 限制单次删除行数（如每次 1000 条） 预防措施：\nDry-Run 机制：归档脚本必须先执行 SELECT 预览，人工确认后再执行 DELETE。 状态过滤：严格限定归档数据的状体（如仅归档“已完成”且“超过 N 天”）。 备份先行：删除前必须备份待删除数据。 权限控制：生产环境删除权限需审批。 经验总结：\n删除操作是不可逆的高危动作。 归档逻辑必须经过严格测试和预览。 备份是防止误删的最后防线。 事故 51：DDoS 攻击事故 事故现象：\n带宽使用率 100% 服务响应极慢或不可用 防火墙日志显示大量异常连接 CDN 回源流量激增 事故原因：\n恶意流量攻击 反射放大攻击 CC 攻击 防护策略不足 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 1. 查看流量来源 iftop -i eth0 -n tcpdump -i eth0 -nn -c 1000 # 2. 分析攻击类型 # SYN Flood：netstat -n | grep SYN_RECV | wc -l # CC 攻击：awk \u0026#39;{print $1}\u0026#39; access.log | sort | uniq -c | sort -rn | head -10 # 3. 查看防火墙日志 tail -f /var/log/firewall.log | grep -i \u0026#34;drop\\|block\u0026#34; # 4. 检查 CDN 状态 # CDN 控制台查看攻击流量 # 5. 查看系统资源 top vmstat 1 5 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 1. 启用 DDoS 防护 # 云厂商 DDoS 高防 # 接入流量清洗服务 # 2. 封禁攻击 IP for ip in $(awk \u0026#39;{print $1}\u0026#39; access.log | sort | uniq -c | sort -rn | head -100 | awk \u0026#39;{print $2}\u0026#39;); do iptables -A INPUT -s $ip -j DROP done # 3. 限流保护 # Nginx 限流 limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s; # 4. 启用 CDN 防护 # 开启 CDN DDoS 防护 # 配置 WAF 规则 # 5. 联系运营商 # 申请上游流量清洗 预防措施：\n接入专业 DDoS 防护服务 配置多层限流 CDN 隐藏源站 IP 定期安全演练 事故 52：SQL 注入事故 事故现象：\n数据库异常查询 敏感数据泄露 应用报 SQL 语法错误 安全团队告警 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 查看数据库日志 grep -i \u0026#34;select\\|union\\|drop\u0026#34; /var/log/mysql/*.log # 2. 分析访问日志 grep -i \u0026#34;union\\|select\\|\u0026#39;\u0026#34; access.log # 3. 检查异常查询 mysql -e \u0026#34;SHOW PROCESSLIST;\u0026#34; | grep -i \u0026#34;sleep\\|benchmark\u0026#34; # 4. 查看数据变更 mysqlbinlog /var/lib/mysql/mysql-bin.* | grep -i \u0026#34;drop\\|delete\u0026#34; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 紧急修复代码 # 使用参数化查询 PreparedStatement ps = conn.prepareStatement(\u0026#34;SELECT * FROM users WHERE id = ?\u0026#34;); ps.setInt(1, userId); # 2. 修复漏洞 # 输入验证 # 特殊字符转义 # 3. 数据恢复 # 从备份恢复被篡改数据 # 4. 安全加固 # 部署 WAF # 最小权限原则 预防措施：\n使用参数化查询 输入验证和转义 部署 WAF 定期安全扫描 事故 53：缓存穿透导致数据库瞬间崩溃 事故现象：\n某个冷门商品详情页突然流量激增（疑似恶意攻击） 数据库 CPU 瞬间飙升至 100%，连接数爆满 缓存命中率跌至 0% 大量请求直接打到数据库，导致正常用户无法访问 事故原因：\n攻击者构造大量不存在的 Key（如 product_id = -1, -2, ...）发起请求 缓存中不存在这些 Key，请求直接穿透到数据库 数据库查询返回空，代码未将空结果写入缓存，导致每次请求都查库 缺乏参数校验和限流机制 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 查看缓存命中情况 redis-cli INFO stats | grep keyspace_hits # 发现 hits 极低，misses 极高 # 2. 分析请求 Key 分布 # 在应用层采样打印请求的 Key log.info(\u0026#34;Request Key: {}\u0026#34;, key); # 发现大量 Key 为负数或无规律字符串 # 3. 查看数据库慢查询 SHOW PROCESSLIST; # 发现大量相同的查询 `SELECT * FROM products WHERE id = -xxx` # 4. 检查应用日志 grep \u0026#34;Cache Miss\u0026#34; app.log | wc -l # 确认缓存未命中量异常 使用命令：\n1 2 # 实时监控 Redis 命中率 watch -n 1 \u0026#39;redis-cli INFO stats | grep -E \u0026#34;keyspace_hits|keyspace_misses\u0026#34;\u0026#39; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // 1. 缓存空对象（短期方案） // 即使数据库查不到，也将 null 值写入缓存，设置较短过期时间（如 5 分钟） Object val = redis.get(key); if (val == NOT_EXIST) { return null; // 直接返回 } if (val == null) { val = db.query(key); if (val == null) { redis.setex(key, 300, NULL_OBJECT); // 缓存空值 return null; } redis.setex(key, 3600, val); } // 2. 布隆过滤器（Bloom Filter，推荐方案） // 在请求到达缓存前，先通过布隆过滤器判断 Key 是否存在 if (!bloomFilter.mightContain(key)) { // 肯定不存在，直接拦截 return null; } // 可能存在，继续查缓存 // 3. 接口限流与校验 // 对 ID 参数进行合法性校验（如 ID 必须 \u0026gt; 0） // 网关层针对单一 IP 或用户进行限流 预防措施：\n参数校验：入口层严格校验参数合法性。 布隆过滤器：核心业务引入布隆过滤器拦截非法 Key。 空值缓存：对查询为空的结果进行短时缓存。 限流降级：网关层配置针对异常流量的自动限流规则。 经验总结：\n缓存穿透是恶意攻击的常用手段，必须防御。 布隆过滤器是解决大规模 Key 存在性判断的最优解。 永远不要信任前端传来的参数。 事故 54：缓存雪崩导致全站不可用 事故现象：\n某时刻开始，所有核心接口响应极慢，数据库负载激增 监控显示大量 Key 在同一时间过期 缓存服务器负载正常，但后端数据库不堪重负 故障发生在整点（如 10:00:00），恰逢大批量 Key 到期 事故原因：\n大量热点 Key 设置了相同的过期时间（如都在凌晨 2 点设置，24 小时后同时过期） 缓存服务集群部分节点宕机，导致剩余节点压力过大（连带雪崩） 业务重启，本地缓存清空，流量全部涌向远程缓存和 DB 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 # 1. 检查 Key 过期时间分布 # 抽样查看热点 Key 的 TTL redis-cli TTL hot_key_1 redis-cli TTL hot_key_2 # 发现大量 Key 的剩余时间几乎一致 # 2. 查看数据库负载曲线 # 对比 Key 过期时间点与 DB CPU 飙升时间点，是否重合 # 3. 检查缓存节点状态 redis-cli CLUSTER NODES # 确认是否有节点 Fail 或正在恢复 使用命令：\n1 2 # 模拟雪崩 # 脚本批量设置相同过期的 Key，然后等待同时过期 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 1. 随机化过期时间（核心方案） // 在原定过期时间基础上，增加一个随机值（如 1-5 分钟） int expireTime = 3600 + new Random().nextInt(300); redis.setex(key, expireTime, value); // 2. 多级缓存 // 本地缓存 (Guava/Caffeine) + 分布式缓存 (Redis) // 即使 Redis 雪崩，本地缓存能扛住一部分流量 // 3. 限流降级 // 检测到 DB 压力过大时，自动熔断非核心业务 // 返回默认值或“系统繁忙”提示 // 4. 预热机制 // 在大促或重启前，提前加载热点数据到缓存 预防措施：\n分散过期：严禁设置固定时间的过期策略，必须加随机值。 高可用架构：Redis 集群部署，避免单点故障引发连锁反应。 限流保护：数据库前端必须有强有力的限流层。 监控告警：监控缓存命中率骤降和 DB 负载突增。 经验总结：\n雪崩往往是“定时炸弹”，由设计缺陷引起。 随机性是防止集体失效的简单有效手段。 多级缓存是应对高并发的标配。 事故 55：缓存击穿（热点 Key 失效）事故 事故现象：\n某个超级热点商品（如秒杀品）在过期瞬间，数据库 QPS 瞬间打满 仅这一个 Key 失效，却拖垮了整个数据库 其他非热点业务受影响较小，但核心交易链路阻塞 事故原因：\n单个 Key 访问量极大（QPS \u0026gt; 5000） 该 Key 过期瞬间，海量并发请求同时发现缓存缺失 所有请求同时击穿到数据库，形成“惊群效应” 数据库无法承受瞬时高并发写入/读取 排查过程：\n1 2 3 4 5 6 7 8 9 10 # 1. 定位热点 Key redis-cli --hotkeys # 或使用监控工具查看 QPS 最高的 Key # 2. 分析故障时间点 # 确认故障发生时间是否与该 Key 的过期时间一致 # 3. 查看数据库锁等待 SHOW ENGINE INNODB STATUS; # 发现大量线程等待同一行记录的锁 使用命令：\n1 2 # 实时监控单个 Key 的访问频率 # 需借助 APM 工具或自定义监控 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 1. 互斥锁（Mutex Lock） // 只让一个线程去查库重建缓存，其他线程等待 String key = \u0026#34;product_1001\u0026#34;; String lockKey = \u0026#34;lock:\u0026#34; + key; if (redis.setIfAbsent(lockKey, \u0026#34;1\u0026#34;, 10)) { // 尝试获锁 try { // 双重检查 String val = redis.get(key); if (val != null) return val; val = db.query(key); redis.setex(key, 3600, val); return val; } finally { redis.delete(lockKey); // 释放锁 } } else { // 未获锁，休眠重试 Thread.sleep(50); return getFromCache(key); } // 2. 逻辑过期（永不过期） // Key 本身不设 TTL，但在 Value 中包含逻辑过期时间 // 异步线程发现逻辑过期后，后台启动重建，前台返回旧值 预防措施：\n热点探测：实时监控并识别热点 Key。 永不过期策略：对超级热点 Key 采用逻辑过期 + 异步更新。 互斥重建：确保同一时刻只有一个请求回源查库。 本地缓存：利用本地缓存挡在第一线。 经验总结：\n击穿是单点故障引发的系统性风险。 互斥锁是解决击穿的经典方案，但要注意死锁风险。 逻辑过期能极大提升用户体验（无阻塞）。 事故 56：Redis BigKey 导致网络阻塞与超时 事故现象：\nRedis 集群出现周期性卡顿，响应时间从 1ms 飙升至 500ms+ 监控显示网卡流量瞬间打满 部分请求超时，甚至触发主从切换 发现某个 Key 的大小达到 50MB+ 事故原因：\n设计了大 Value 结构（如一个 Hash 存了 100 万个字段，或 List 存了百万级元素） 业务方一次性读取/删除整个大 Key Redis 单线程处理大 Key 的序列化/反序列化/删除操作，阻塞后续命令 网络传输大数据包耗时过长 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 查找 BigKey redis-cli --bigkeys # 或使用 RDB 分析工具（rdb-tools） # 2. 监控网络流量 iftop -P -n # 观察是否有瞬间的大流量突发 # 3. 查看慢日志 redis-cli SLOWLOG GET 10 # 发现 `GET big_key` 或 `DEL big_key` 耗时极长 # 4. 分析内存分布 redis-cli MEMORY USAGE \u0026lt;key\u0026gt; 使用命令：\n1 2 3 # 渐进式删除大 Key（避免阻塞） # 编写 Lua 脚本或使用 UNLINK 命令（Redis 4.0+） UNLINK big_key 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 紧急处理 # 使用 UNLINK 命令异步删除大 Key（非阻塞） redis-cli UNLINK big_hash_key # 2. 拆分大 Key # 将一个大 Hash 拆分为多个小 Hash（如 user_info_1, user_info_2...） # 将一个大 List 拆分为多个小 List # 3. 优化读取逻辑 # 禁止一次性读取全部元素，改为分页/分批读取（HSCAN, ZSCAN） // 错误：HGETALL big_key // 正确：HSCAN big_key 0 COUNT 100 # 4. 压缩数据 # 对 Value 进行序列化压缩（如 Protobuf, Gzip）后再存入 预防措施：\n规范设计：制定 BigKey 标准（如 String \u0026gt; 10KB, Hash/List \u0026gt; 5000 元素）。 上线扫描：CI/CD 流程集成 BigKey 扫描，阻断大 Key 上线。 异步删除：代码中删除大 Key 必须使用 UNLINK 而非 DEL。 分批操作：读写大集合类型必须使用 SCAN 系列命令。 经验总结：\nBigKey 是 Redis 性能的隐形杀手。 单线程模型决定了 Redis 对大操作极其敏感。 拆分和异步是处理 BigKey 的核心原则。 事故 57：Linux 文件句柄耗尽（Too many open files） 事故现象：\n应用日志大量报错 java.io.IOException: Too many open files 无法建立新网络连接，无法打开新文件 Nginx 返回 502，数据库连接失败 系统负载不高，但服务不可用 事故原因：\nLinux 默认文件句柄限制过低（通常 1024） 高并发场景下，每个 TCP 连接、每个打开的文件都占用一个句柄 连接泄漏：代码中未关闭 InputStream/OutputStream/Socket 进程级限制未调整，仅调整了系统级限制 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 1. 查看系统级限制 ulimit -n cat /proc/sys/fs/file-max # 2. 查看进程级限制 cat /proc/\u0026lt;pid\u0026gt;/limits | grep \u0026#34;open files\u0026#34; # 3. 统计当前打开文件数 ls /proc/\u0026lt;pid\u0026gt;/fd | wc -l # 4. 查找泄漏源 lsof -p \u0026lt;pid\u0026gt; | wc -l lsof -p \u0026lt;pid\u0026gt; | grep IPv4 | head -20 使用命令：\n1 2 3 # 实时监控系统句柄使用率 watch -n 1 \u0026#39;cat /proc/sys/fs/file-nr\u0026#39; # 输出：已分配 未使用 最大限制 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 1. 临时调整（立即生效，重启失效） ulimit -n 65535 sysctl -w fs.file-max=2097152 # 2. 永久调整 # /etc/security/limits.conf * soft nofile 65535 * hard nofile 65535 # /etc/sysctl.conf fs.file-max = 2097152 net.core.somaxconn = 65535 net.ipv4.ip_local_port_range = 1024 65535 # 3. 修复代码泄漏 // 必须使用 try-with-resources try (InputStream is = new FileInputStream(\u0026#34;...\u0026#34;)) { // process } catch (...) { ... } // 显式关闭 Socket 和 Connection # 4. 重启服务 # 使配置生效 systemctl restart app 预防措施：\n基线检查：新服务器初始化脚本必须包含句柄数调优。 代码审查：重点检查 IO 流和网络连接的关闭逻辑。 监控告警：监控进程打开文件数，超过 80% 阈值告警。 连接池化：使用连接池复用 TCP 连接，减少句柄消耗。 经验总结：\n文件句柄耗尽是高并发系统的常见瓶颈。 操作系统默认配置绝不能满足生产需求。 资源泄漏是慢性毒药，必须通过代码规范杜绝。 事故 58：TCP TIME_WAIT 过多导致端口耗尽 事故现象：\n应用作为客户端主动发起大量短连接 报错 Cannot assign requested address 或 Connection reset by peer netstat 显示大量 TIME_WAIT 状态的连接 无法建立新的出站连接，导致外部调用失败 事故原因：\n短连接模式：每次请求都新建 TCP 连接，用完即断 客户端端口范围过小（默认 32768-60999） tcp_tw_reuse 未开启，导致端口回收慢 高并发场景下，端口消耗速度 \u0026gt; 回收速度（2MSL） 排查过程：\n1 2 3 4 5 6 7 8 9 10 # 1. 查看连接状态分布 netstat -n | awk \u0026#39;/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}\u0026#39; # 观察 TIME_WAIT 数量是否巨大（如 \u0026gt; 20000） # 2. 查看可用端口范围 sysctl net.ipv4.ip_local_port_range # 3. 查看 TIME_WAIT 回收配置 sysctl net.ipv4.tcp_tw_reuse sysctl net.ipv4.tcp_fin_timeout 使用命令：\n1 2 # 实时监控 TIME_WAIT 数量 watch -n 1 \u0026#34;netstat -n | grep TIME_WAIT | wc -l\u0026#34; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 1. 开启端口复用 sysctl -w net.ipv4.tcp_tw_reuse=1 # 允许将 TIME_WAIT sockets 重新用于新的 TCP 连接 # 2. 缩短 FIN_WAIT 时间 sysctl -w net.ipv4.tcp_fin_timeout=30 # 3. 扩大本地端口范围 sysctl -w net.ipv4.ip_local_port_range=\u0026#34;1024 65535\u0026#34; # 4. 启用长连接（根本解决） # 应用层使用 HTTP Keep-Alive 或连接池 # 避免频繁建立/断开 TCP 连接 HttpClient httpClient = HttpClients.custom() .setMaxConnTotal(200) .setMaxConnPerRoute(20) .evictIdleConnections(30, TimeUnit.SECONDS) .build(); 预防措施：\n长连接优先：尽可能使用连接池和 Keep-Alive。 内核调优：高并发客户端必须调整 tcp_tw_reuse 和端口范围。 架构优化：引入网关或代理层，收敛出站连接。 监控：监控 TIME_WAIT 数量和可用端口数。 经验总结：\nTIME_WAIT 是 TCP 协议的正常机制，但在高并发短连接场景下会成为瓶颈。 开启 tcp_tw_reuse 是安全且有效的优化手段。 长连接是解决端口耗尽的根本之道。 事故 59：RabbitMQ 消息积压与队列阻塞 事故现象：\n消息队列中积压数百万条消息 消费者消费速度极慢，甚至停止消费 生产者发送消息阻塞，报错 BLOCKED 内存告警，RabbitMQ 节点触发 Flow Control 事故原因：\n消费者处理逻辑复杂或存在 Bug，处理单条消息耗时过长 消费者预取数量（Prefetch Count）设置过大，导致消息堆积在客户端未处理 队列中存在大量持久化消息，磁盘 IO 成为瓶颈 生产者发送速度远超消费能力，且无背压机制 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 1. 查看队列状态 rabbitmqctl list_queues name messages consumers messages_ready messages_unacknowledged # 2. 查看连接和通道 rabbitmqctl list_connections state rabbitmqctl list_channels consumer_count msgs_unacknowledged # 3. 检查消费者日志 grep \u0026#34;Processing time\u0026#34; consumer.log # 发现平均处理时间从 10ms 变为 5s # 4. 查看 RabbitMQ 管理面板 # 观察 Publish/Consume Rate 曲线，以及 Memory/Disk 使用率 使用命令：\n1 2 # 查看节点状态 rabbitmqctl status 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 1. 调整预取数量（Prefetch Count） // 限制消费者一次只拉取少量消息（如 1 或 10） channel.basicQos(1); // 确保处理完一条 ACK 后，再拉取下一条，避免客户端堆积 # 2. 扩容消费者 # 增加消费者实例数量 # 如果队列允许多消费者，增加并发度 # 3. 优化消费逻辑 # 异步处理、批量处理 # 移除同步阻塞调用 # 4. 紧急清理（极端情况） # 如果消息不重要， purge 队列 rabbitmqctl purge_queue \u0026lt;queue_name\u0026gt; # 5. 磁盘优化 # 将队列设置为非持久化（如果允许丢失） # 或使用 SSD 磁盘 预防措施：\n合理 Prefetch：根据处理能力动态调整 basicQos。 监控积压：设置消息积压阈值告警。 死信队列：处理失败的消息及时转入 DLQ，避免阻塞主队列。 背压机制：生产者在队列达到一定长度时应暂停发送或降级。 经验总结：\n消费者处理能力决定了整个系统的吞吐量。 basicQos(1) 是保证公平分发和防止客户端积压的关键。 磁盘 IO 往往是持久化队列的性能瓶颈。 事故 60：ZooKeeper 会话超时导致集群脑裂 事故现象：\nHadoop/HBase/Kafka 集群频繁发生 Master 选举 服务间歇性不可用，数据短暂不一致 ZooKeeper 日志显示大量 SessionExpired 和 ConnectionLoss 客户端频繁重连，ZK 集群 CPU 飙升 事故原因：\nZK 集群负载过高，无法及时处理心跳 网络抖动或 GC 停顿（Stop-The-World）导致客户端未及时发送心跳 会话超时时间（sessionTimeout）设置过短 ZK 节点磁盘 IO 慢，导致事务日志写入延迟 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 查看 ZK 状态 echo stat | nc localhost 2181 # 查看 Mode, Latency, Connections # 2. 查看 ZK 日志 grep \u0026#34;SessionExpired\\|Expiring\u0026#34; zookeeper.out # 确认超时会话数量 # 3. 检查客户端 GC 日志 grep \u0026#34;Full GC\u0026#34; app.log # 确认是否因长时间 GC 导致心跳中断 # 4. 检查磁盘 IO iostat -x 1 # 确认 ZK 数据目录所在磁盘的 await 时间 使用命令：\n1 2 3 4 # 监控 ZK 延迟 echo ruok | nc localhost 2181 # 使用四字命令监控 echo mntr | nc localhost 2181 | grep latency 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 1. 调整超时参数 # 增加 sessionTimeout 和 connectionTimeout # 建议：sessionTimeout = 20s ~ 40s (原默认可能为 5s) // 客户端配置 new ZooKeeper(hosts, 30000, watcher); # 2. 优化 ZK 集群 # 独立部署 ZK 集群，不与大数据组件混部 # 使用 SSD 存储 ZK 数据（txnlog 和 snapshot） # 增加 ZK 节点数（奇数，如 5 节点） # 3. 优化客户端 GC # 调整 JVM 参数，减少 Full GC 频率和停顿时间 # 使用 G1 GC # 4. 网络优化 # 确保客户端与 ZK 集群网络低延迟、无丢包 预防措施：\n独立部署：ZK 作为核心协调服务，必须独立集群部署。 SSD 存储：ZK 对磁盘延迟极其敏感，必须使用 SSD。 参数调优：根据网络环境和 GC 情况合理设置超时时间。 监控：监控 ZK 的延迟、连接数和会话超时率。 经验总结：\nZK 是分布式系统的“心脏”，心跳停止意味着死亡。 GC 停顿是导致 ZK 会话超时的常见原因。 磁盘 IO 性能直接决定 ZK 的稳定性。 事故 61：JVM Full GC 停顿过长事故 事故现象：\n核心交易接口响应时间从 50ms 飙升至 5s+ 监控显示应用出现周期性“假死”（Stop-The-World） Tomcat/Jetty 线程池全部阻塞在 WAITING 状态 日志中出现大量 GC overhead limit exceeded 警告 事故原因：\n内存泄漏导致老年代（Old Gen）快速填满 大对象直接进入老年代，触发频繁 Full GC JVM 参数配置不当（堆大小、GC 算法选择错误） 代码中存在未关闭的资源（如 InputStream、Database Connection） 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 1. 确认 GC 频率和耗时 jstat -gcutil \u0026lt;PID\u0026gt; 1000 10 # 关注 FGC (Full GC 次数) 和 FGCT (Full GC 总时间) # 如果 FGC 在短时间内急剧增加，且 FGCT 占比高，确认为 Full GC 问题 # 2. 查看堆内存分布 jmap -heap \u0026lt;PID\u0026gt; # 观察 Old Gen 使用率是否接近 100% # 3. 导出堆转储文件（Heap Dump） # 注意：生产环境执行此命令会暂停服务，建议在流量低峰或备用节点执行 jmap -dump:format=b,file=heap_dump.hprof \u0026lt;PID\u0026gt; # 4. 实时查看 GC 日志（如果已开启） tail -f /var/log/app/gc.log | grep \u0026#34;Full GC\u0026#34; # 5. 查看线程状态，确认是否都在等待 GC jstack \u0026lt;PID\u0026gt; | grep -A 5 \u0026#34;VM Thread\u0026#34; jstack \u0026lt;PID\u0026gt; | grep -c \u0026#34;WAITING (on object monitor)\u0026#34; # 6. 分析大对象 jmap -histo:live \u0026lt;PID\u0026gt; | head -20 # 查看存活对象中占用内存最大的类 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 12 # 实时监控 GC 状态 watch -n 1 \u0026#39;jstat -gc \u0026lt;PID\u0026gt; | tail -1\u0026#39; # 强制触发 GC 测试（谨慎使用，会导致短暂停顿） jcmd \u0026lt;PID\u0026gt; GC.run # 查看 JVM 启动参数 jinfo -flags \u0026lt;PID\u0026gt; # 使用 Arthas 在线诊断（推荐，无需重启） java -jar arthas-boot.jar # 进入后执行：dashboard, memory, thread 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # 1. 紧急扩容（临时方案） # 如果是因为流量突增导致的内存不足，临时增加堆内存 # 修改启动脚本：-Xms4g -Xmx8g (原为 2g/4g) # 重启服务 # 2. 降级非核心业务 # 关闭耗内存的功能模块（如报表生成、大数据量导出） # 减少缓存加载量 # 3. 切换 GC 算法（中长期） # 从 CMS 切换到 G1 GC（JDK 8u20+ 推荐） -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:InitiatingHeapOccupancyPercent=45 # 4. 修复内存泄漏（根本解决） # 使用 MAT (Memory Analyzer Tool) 分析 heap_dump.hprof # 查找 Dominator Tree，定位占用内存最大的对象 # 常见泄漏点：静态集合类、ThreadLocal 未 remove、未关闭的流 # 5. 优化代码 // 错误示例：静态 List 无限增长 static List\u0026lt;String\u0026gt; cache = new ArrayList\u0026lt;\u0026gt;(); // 正确示例：使用有界缓存 static Map\u0026lt;String, String\u0026gt; cache = new LinkedHashMap\u0026lt;\u0026gt;(1000, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() \u0026gt; 1000; } }; 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 1. 完善监控告警 # Prometheus JMX Exporter - alert: JvmGcPauseHigh expr: rate(jvm_gc_collection_seconds_sum[5m]) / rate(jvm_gc_collection_seconds_count[5m]) \u0026gt; 0.5 for: 2m labels: severity: critical annotations: summary: \u0026#34;JVM GC 停顿时间过长\u0026#34; - alert: JvmOldGenUsageHigh expr: jvm_memory_used_bytes{area=\u0026#34;heap\u0026#34;, pool=\u0026#34;G1 Old Gen\u0026#34;} / jvm_memory_max_bytes{area=\u0026#34;heap\u0026#34;, pool=\u0026#34;G1 Old Gen\u0026#34;} \u0026gt; 0.85 for: 5m labels: severity: warning # 2. 规范开发流程 # 禁止使用 static 集合存储大量数据 # 必须使用 try-with-resources 关闭流 # 定期 Code Review 内存相关代码 # 3. 压测验证 # 上线前进行稳定性压测（Soak Testing），运行 24h+ 观察内存趋势 经验总结：\nFull GC 是 Java 应用的“癌症”，必须零容忍 堆 dump 分析是定位内存泄漏的金标准 G1 GC 在大堆场景下表现优于 CMS 监控要关注 GC 频率和停顿时间，而不仅仅是内存使用率 静态变量和 ThreadLocal 是泄漏高发区 事故 62：数据库连接池耗尽事故（进阶版） 事故现象：\n应用日志大量报错：CannotGetJdbcConnectionException: Could not get JDBC Connection 接口超时，前端显示“系统繁忙” 数据库侧连接数已满（max_connections） 应用侧连接池活跃连接数达到最大值 事故原因：\n慢 SQL 导致连接占用时间过长，无法及时释放 代码中存在事务未提交/回滚，连接被挂起 连接池配置过小，无法应对突发流量 外部依赖（如 Redis、第三方 API）超时，导致线程阻塞进而占用 DB 连接 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 1. 查看应用连接池状态 # HikariCP 监控端点 curl http://localhost:8080/actuator/metrics/hikaricp.connections.active curl http://localhost:8080/actuator/metrics/hikaricp.connections.pending # 2. 查看数据库当前连接 mysql -u root -p -e \u0026#34;SHOW PROCESSLIST;\u0026#34; | head -50 # 关注 State 列：Sleep, Sending data, Locked, Waiting for table metadata lock # 3. 统计各状态连接数 mysql -u root -p -e \u0026#34;SELECT COMMAND, STATE, COUNT(*) FROM information_schema.processlist GROUP BY COMMAND, STATE;\u0026#34; # 4. 查找长事务 mysql -u root -p -e \u0026#34;SELECT * FROM information_schema.innodb_trx WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) \u0026gt; 60;\u0026#34; # 5. 查看应用日志中的慢查询 grep \u0026#34;SlowQuery\u0026#34; /var/log/app/*.log | tail -20 # 6. 检查是否有元数据锁等待 mysql -u root -p -e \u0026#34;SELECT * FROM performance_schema.metadata_locks WHERE OWNER_THREAD_ID IS NOT NULL;\u0026#34; 使用命令：\n1 2 3 4 5 6 7 8 9 10 11 # 实时监控数据库连接数 watch -n 1 \u0026#39;mysql -u root -p -e \u0026#34;SHOW STATUS LIKE \\\u0026#34;Threads_connected\\\u0026#34;;\u0026#34;\u0026#39; # 查找阻塞其他会话的源头 SELECT blocking_locks.lock_id, blocking_trx.trx_mysql_thread_id, blocking_trx.trx_query FROM performance_schema.data_lock_waits JOIN performance_schema.data_locks blocking_locks ON data_lock_waits.blocking_engine_lock_id = blocking_locks.engine_lock_id JOIN performance_schema.innodb_trx blocking_trx ON blocking_locks.engine_transaction_id = blocking_trx.trx_id; # 查看连接池等待队列长度 jstat -gc \u0026lt;PID\u0026gt; # 辅助判断是否因 GC 导致处理慢 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # 1. 紧急 Kill 掉异常会话 # 找出执行时间超过 60 秒的查询 mysql -u root -p -e \u0026#34;SELECT CONCAT(\u0026#39;KILL \u0026#39;, id, \u0026#39;;\u0026#39;) FROM information_schema.processlist WHERE time \u0026gt; 60 AND command != \u0026#39;Sleep\u0026#39;;\u0026#34; \u0026gt; kill.sql source kill.sql # 2. 临时扩容连接池 # 动态调整（如果支持）或重启应用 # application.yml spring: datasource: hikari: maximum-pool-size: 50 -\u0026gt; 100 # 临时加倍 connection-timeout: 30000 -\u0026gt; 10000 # 缩短获取连接超时，快速失败 # 3. 限流保护 # 在网关层限制进入应用的流量，防止连接池瞬间被打满 # Nginx: limit_req zone=api burst=20; # 4. 优化慢 SQL # 针对 Processlist 中耗时最长的 SQL 添加索引或优化逻辑 # 5. 修复代码事务 # 确保所有事务最终都会 commit 或 rollback // 错误：捕获异常后未回滚 try { db.update(); } catch (Exception e) { log.error(e); // 缺少 transaction.rollback() } 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 连接池监控 # 告警规则：活跃连接数 \u0026gt; 80% 最大连接数 - alert: DbPoolUsageHigh expr: hikaricp_connections_active / hikaricp_connections_max \u0026gt; 0.8 for: 2m # 2. SQL 审计 # 开启慢查询日志，阈值设为 1s # 定期分析慢查询并优化 # 3. 事务规范 # 严禁在事务中进行 RPC 调用、HTTP 请求 # 事务粒度尽可能小 # 4. 熔断降级 # 当数据库响应变慢时，自动熔断非核心业务 经验总结：\n连接池耗尽通常是“果”，慢 SQL 或长事务是“因” 严禁在数据库事务内调用外部接口 快速失败（Fail Fast）比长时间等待更保护系统 监控要细化到连接池的活跃数、等待数和超时数 事故 63：分布式锁失效导致重复消费事故 事故现象：\n订单被重复发货 用户账户余额被重复扣减 库存出现负数 日志显示同一业务 ID 被多个实例同时处理 事故原因：\nRedis 锁过期时间设置过短，业务未执行完锁已释放 客户端宕机或 GC 停顿，导致无法及时续期（Lookaside Lock 问题） 主从切换导致锁丢失（Redis Async Replication 特性） 代码逻辑错误，锁 key 拼接不一致 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 1. 查看业务日志 grep \u0026#34;OrderID_12345\u0026#34; /var/log/app/*.log # 发现两个不同 IP 的实例在同一时间段内都获得了锁并执行了业务 # 2. 检查 Redis 锁 Key 状态 redis-cli GET lock:order:12345 # 返回 nil，说明锁已不存在或从未设置成功 # 3. 查看 Redis 慢日志 redis-cli SLOWLOG GET 10 # 检查是否有耗时操作导致锁获取延迟 # 4. 检查 Redis 集群状态 redis-cli CLUSTER INFO # 查看是否有主从切换发生 # 5. 代码审查 # 检查锁的 key 生成逻辑是否包含动态变量 # 检查锁的释放逻辑是否在 finally 块中 使用命令：\n1 2 3 4 5 6 7 8 # 模拟锁竞争测试 # 使用 redis-cli 手动测试 setnx 行为 redis-cli SETNX lock:test 1 redis-cli EXPIRE lock:test 5 redis-cli TTL lock:test # 查看 Redis 内存中锁的分布 redis-cli KEYS \u0026#34;lock:*\u0026#34; | head -20 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # 1. 引入看门狗机制（WatchDog） # 使用 Redisson 等成熟框架，自动为锁续期 RLock lock = redisson.getLock(\u0026#34;lock:order:12345\u0026#34;); lock.lock(); // 默认开启看门狗，只要线程还在运行，锁就不会过期 try { // 业务逻辑 } finally { lock.unlock(); } # 2. 使用 RedLock 算法（强一致性场景） # 向 N/2+1 个 Redis 节点申请锁，提高可靠性 # 注意：RedLock 性能较低，仅在极端一致要求下使用 # 3. 数据库唯一索引兜底 # 即使锁失效，数据库唯一约束也能防止重复插入 ALTER TABLE orders ADD UNIQUE INDEX uk_order_no (order_no); # 4. 幂等性设计 # 业务逻辑本身支持重复执行而不产生副作用 // 检查状态机 if (order.getStatus() != OrderStatus.PAID) { // 执行扣款 order.setStatus(OrderStatus.PAID); } else { // 直接返回成功 } 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 # 1. 框架选型 # 禁止手写 Redis setnx 逻辑，统一使用 Redisson/Zookeeper Curator # 2. 兜底策略 # 分布式锁只是第一道防线，数据库唯一索引和业务幂等性是最后一道防线 # 3. 监控告警 # 监控锁等待时间、锁冲突次数 # 监控业务重复执行指标（如单位时间内同一 ID 处理次数\u0026gt;1） # 4. 故障演练 # 模拟 Redis 主从切换，验证锁是否丢失 经验总结：\n不要信任单一的分布式锁，必须有兜底（DB 唯一键/幂等） 锁的过期时间必须大于业务执行时间，或使用自动续期 Redis 主从异步复制天生存在锁丢失风险，关键业务需评估 幂等性是分布式系统的基石 事故 64：微服务雪崩（级联故障）事故 事故现象：\nA 服务故障，导致调用 A 的 B、C 服务全部超时 进而导致调用 B、C 的网关 D 服务线程池耗尽 最终整个集群所有服务不可用 监控显示所有服务 RT 飙升，错误率 100% 事故原因：\n缺乏熔断机制，下游故障拖垮上游 线程池隔离失效，单个慢接口占满所有线程 超时时间设置不合理（层层叠加） 重试风暴（上游不断重试下游，加剧负载） 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 链路追踪分析 # 查看 SkyWalking/Zipkin/Jaeger 追踪图 # 发现根节点是某个基础服务（如用户中心）响应极慢 # 2. 检查线程池状态 for service in gateway order user; do curl http://$service/actuator/metrics/thread.pool.active done # 3. 查看依赖调用链 # 确认哪个下游服务成为了瓶颈 # 检查该服务的 CPU、内存、DB 连接 # 4. 检查重试配置 grep -r \u0026#34;RetryTemplate\\|maxAttempts\u0026#34; config/ # 发现配置了 3 次重试，且无退避策略 使用命令：\n1 2 3 4 5 6 # 快速定位故障源 # 查看各服务错误率 kubectl top pods | sort -rn # 查看调用延迟分布 istioctl proxy-config log deploy/gateway --level debug 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # 1. 紧急熔断 # 手动开启熔断开关（如果有配置中心） # 或直接下线故障服务，返回默认值 kubectl scale deployment user-service --replicas=0 # 2. 实施舱壁模式（Bulkhead） # 为不同依赖分配独立的线程池 // 错误：共用线程池 ExecutorService commonPool = ...; // 正确：隔离线程池 ExecutorService userPool = Executors.newFixedThreadPool(20); ExecutorService orderPool = Executors.newFixedThreadPool(50); # 3. 配置熔断器 // 使用 Resilience4j/Sentinel/Hystrix CircuitBreakerConfig config = CircuitBreakerConfig.custom() .failureRateThreshold(50) .waitDurationInOpenState(Duration.ofSeconds(30)) .build(); # 4. 设置合理超时 # 上游超时时间 \u0026lt; 下游超时时间 # 禁止无限制等待 # 5. 禁用盲目重试 # 仅在网络波动或 5xx 错误时重试 # 增加指数退避（Exponential Backoff） 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 架构原则 # 遵循“依赖必熔断、调用必超时、资源必隔离” # 2. 全链路压测 # 模拟下游故障，验证上游是否能快速失败 # 3. 监控大盘 # 建立依赖关系拓扑图，实时展示健康度 # 4. 降级预案 # 核心服务故障时，非核心功能自动降级（如推荐列表置空） 经验总结：\n雪崩往往始于一个不起眼的边缘服务 没有超时的调用就是定时炸弹 重试必须谨慎，避免放大故障 隔离是防止级联故障的唯一手段 事故 65：Kafka 消息积压（千万级）事故 事故现象：\n消费者 Lag 达到 2000 万+ 实时数据处理延迟从秒级变为小时级 磁盘空间告警（Kafka Broker 存满） 用户反馈订单状态长时间不更新 事故原因：\n消费者代码出现死循环或严重 BUG，处理速度趋近于 0 新增了一个极其耗时的同步 IO 操作（如调用慢 HTTP 接口） Partition 数量少于消费者实例数，导致负载不均 消息体过大，导致网络传输和反序列化耗时 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 1. 查看消费组 Lag kafka-consumer-groups.sh --bootstrap-server kafka:9092 --describe --group order-group # 观察到 LAG 持续上涨，CURRENT-OFFSET 几乎不动 # 2. 检查消费者日志 tail -f consumer.log | grep -i \u0026#34;error\\|exception\\|timeout\u0026#34; # 发现大量 `ConnectTimeoutException` # 3. 分析消费者线程 jstack \u0026lt;consumer_pid\u0026gt; | grep -A 20 \u0026#34;RUNNABLE\u0026#34; # 发现线程阻塞在 HTTP 请求上 # 4. 查看 Kafka Broker 负载 kafka-topics.sh --bootstrap-server kafka:9092 --describe --topic order-topic # 检查 Partition 分布是否均匀 # 5. 检查消息大小 kafka-run-class.sh kafka.tools.GetOffsetShell --broker-list kafka:9092 --topic order-topic 使用命令：\n1 2 3 4 5 # 实时监控 Lag 变化 watch -n 5 \u0026#39;kafka-consumer-groups.sh --bootstrap-server kafka:9092 --describe --group order-group | grep order-topic\u0026#39; # 查看消息生产速率 vs 消费速率 # 通过 JMX 监控 MessagesInPerSec 和 BytesInPerSec 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 1. 修复代码 BUG # 移除死循环，增加 HTTP 请求超时时间 # 将同步调用改为异步批量调用 # 2. 紧急扩容消费者 # 增加 Partition 数量（前提：Key 允许重排序） kafka-topics.sh --alter --topic order-topic --partitions 48 # 扩容消费者实例至 48 个 kubectl scale deployment consumer --replicas=48 # 3. 跳过非关键消息（极端情况） # 如果数据允许丢失，重置 Offset 到最新 kafka-consumer-groups.sh --reset-offsets --to-latest --execute --group order-group --topic order-topic # 4. 临时旁路处理 # 将消息转发到另一个高性能处理集群 # 或写入临时存储，后续离线处理 # 5. 优化处理逻辑 // 批量拉取，批量处理 List\u0026lt;ConsumerRecord\u0026gt; records = consumer.poll(Duration.ofMillis(1000)); batchProcess(records); // 一次性处理 1000 条 预防措施：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 监控告警 # Lag \u0026gt; 10000 持续 5 分钟即告警 # 消费速率骤降告警 # 2. 容量规划 # Partition 数量应预留 2-3 倍余量 # 消费者处理能力应大于生产峰值的 1.5 倍 # 3. 异常处理 # 单条消息处理失败不应阻塞整个批次 # 引入死信队列（DLQ） # 4. 压测 # 定期模拟高吞吐场景，验证消费能力 经验总结：\n消息积压是“慢性病”，发现时往往已经很严重 扩容 Partition 是解决积压的最快手段（但需注意顺序性） 批量处理能显著提升吞吐量 死信队列是防止单条坏消息卡死整个消费的利器 事故 66：内部 DNS 解析故障（CoreDNS 崩溃） 事故现象：\nK8s 集群内 Pod 之间无法通过 Service 域名通信 应用日志大量报错 UnknownHostException 或 Could not resolve host nslookup 在 Pod 内执行超时或返回 SERVFAIL 核心业务链路中断，但 IP 直连正常 事故原因：\nCoreDNS 副本数不足，无法承载突发查询流量 CoreDNS 配置错误（如 forward 指向了不可达的上游 DNS） Node 本地 kube-dns 缓存污染或 systemd-resolved 冲突 CoreDNS 进程内存泄漏导致 OOM Kill 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 1. 在 Pod 内测试解析 kubectl run -it --rm debug --image=busybox:1.28 --restart=Never -- nslookup kubernetes.default # 观察是否超时或报错 # 2. 检查 CoreDNS 状态 kubectl get pods -n kube-system -l k8s-app=kube-dns kubectl describe pod \u0026lt;coredns-pod\u0026gt; -n kube-system | grep -A 5 \u0026#34;Events\u0026#34; # 查看是否有 OOMKilled 或 CrashLoopBackOff # 3. 查看 CoreDNS 日志 kubectl logs -n kube-system -l k8s-app=kube-dns --tail=200 # 搜索 \u0026#34;error\u0026#34;, \u0026#34;timeout\u0026#34;, \u0026#34;refused\u0026#34; # 4. 检查 CoreDNS ConfigMap kubectl get configmap coredns -n kube-system -o yaml # 检查 forward 插件配置的上游 DNS 是否可达 # 5. 检查 Node 本地 DNS # 登录到节点，检查 /etc/resolv.conf 和 systemd-resolved 状态 systemctl status systemd-resolved resolvectl status 使用命令：\n1 2 3 4 5 6 7 8 # 批量测试所有 Node 的 DNS 连通性 for node in $(kubectl get nodes -o jsonpath=\u0026#39;{.items[*].metadata.name}\u0026#39;); do kubectl debug node/$node -it --image=busybox -- nslookup google.com done # 查看 CoreDNS 监控指标（Prometheus） rate(coredns_dns_request_duration_seconds_count[5m]) rate(coredns_dns_response_rcode_count_total[5m]) 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 1. 紧急扩容 CoreDNS kubectl scale deployment/coredns --replicas=5 -n kube-system # 2. 修复 ConfigMap 配置 # 修改上游 DNS 为可靠地址（如 8.8.8.8 或内网 DNS） kubectl edit configmap coredns -n kube-system # 修改后重启 CoreDNS kubectl rollout restart deployment/coredns -n kube-system # 3. 清理 Node 缓存 # 在受影响节点执行 systemctl restart systemd-resolved # 或重启 kubelet systemctl restart kubelet # 4. 临时绕过 DNS（极端情况） # 在应用中使用 IP 直连，或修改 /etc/hosts (不推荐长期) 预防措施：\n高可用部署：CoreDNS 至少部署 2-3 个副本，并设置反亲和性（Anti-Affinity）分散在不同节点。 资源限制：合理设置 CoreDNS 的 CPU/Memory Request/Limit，防止 OOM。 监控告警：监控 CoreDNS 的 QPS、延迟、错误率（RCODE）。 本地缓存：在应用侧或 Sidecar 中引入 DNS 缓存（如 NodeLocal DNSCache）。 经验总结：\nDNS 是 K8s 的神经系统，一旦瘫痪全场皆输。 NodeLocal DNSCache 能显著降低 CoreDNS 压力并提高解析速度。 上游 DNS 配置必须冗余且可靠。 事故 67：Elasticsearch 集群脑裂与数据丢失 事故现象：\nES 集群状态变红（Red），部分分片未分配 写入请求失败，报错 ClusterBlockException 发现两个 Master 节点同时存在（脑裂） 部分索引数据不一致或丢失 事故原因：\ndiscovery.zen.minimum_master_nodes (ES 6.x) 或 cluster.initial_master_nodes (ES 7.x+) 配置错误 网络抖动导致 Master 节点间心跳超时，触发重新选举 节点恢复时间设置过短，频繁发生角色切换 磁盘空间满导致分片无法分配 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 查看集群健康状态 curl -X GET \u0026#34;localhost:9200/_cluster/health?pretty\u0026#34; # 2. 查看节点角色 curl -X GET \u0026#34;localhost:9200/_cat/nodes?v\u0026amp;h=name,ip,master,node.role\u0026#34; # 观察是否有多个节点标记为 * (Master) # 3. 查看未分配分片原因 curl -X GET \u0026#34;localhost:9200/_cluster/allocation/explain?pretty\u0026#34; # 4. 检查日志 grep \u0026#34;MasterChanged\\|ClusterFormation\u0026#34; /var/log/elasticsearch/*.log # 查找频繁的 Master 切换记录 # 5. 检查磁盘使用率 curl -X GET \u0026#34;localhost:9200/_cat/allocation?v\u0026#34; 使用命令：\n1 2 3 4 5 6 7 # 强制指定主节点（紧急修复脑裂） # 在所有非主节点 elasticsearch.yml 中配置 cluster.initial_master_nodes: [\u0026#34;node-1\u0026#34;] # 然后重启非主节点 # 查看分片分布 curl -X GET \u0026#34;localhost:9200/_cat/shards?v\u0026amp;h=index,shard,prirep,state,node\u0026#34; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 1. 停止所有节点 systemctl stop elasticsearch # 2. 修复配置 # 确保 master_eligible 节点数为奇数 (3, 5, 7) # 设置 minimum_master_nodes = (N/2) + 1 (ES 6.x) # ES 7.x+ 只需在首次启动时正确设置 cluster.initial_master_nodes # 3. 清理脏数据（谨慎） # 删除每个节点 data 目录下的 metadata 文件（仅在全量备份后操作） rm -rf /var/lib/elasticsearch/nodes/*/metadata # 4. 按顺序启动 # 先启动原 Master 节点，待其完全启动后再启动其他节点 systemctl start elasticsearch # 在原 Master 上 # 等待集群绿色 # 启动其他节点 # 5. 重新分配分片 curl -X POST \u0026#34;localhost:9200/_cluster/reroute?retry_failed=true\u0026#34; 预防措施：\n奇数节点：Master 候选节点数量必须为奇数，避免票数平局。 网络优化：确保集群内网络低延迟、高带宽，避免心跳超时。 参数调优：调整 discovery.zen.ping_timeout 和 discovery.zen.fd.ping_interval 适应网络环境。 磁盘监控：设置磁盘水位线告警（85% 警告，90% 只读）。 经验总结：\n脑裂是分布式存储的大忌，配置法定人数（Quorum）是关键。 ES 7.x 后简化了配置，但仍需严格遵循首次引导规则。 定期快照（Snapshot）是数据安全的最后防线。 事故 68：Nginx 502 Bad Gateway (Upstream 连接耗尽) 事故现象：\n用户访问大量返回 502 Bad Gateway Nginx 错误日志显示 connect() failed (110: Connection timed out) 或 no live upstreams 后端服务（Tomcat/Go/Node）其实存活，但无法建立新连接 监控显示 Nginx 活跃连接数激增，后端连接池已满 事故原因：\nNginx upstream 未配置 keepalive，导致每次请求都新建 TCP 连接 后端服务连接池（如 Tomcat maxThreads）耗尽 Nginx worker_connections 设置过小 后端处理慢，导致连接占用时间过长，无法释放 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 1. 查看 Nginx 错误日志 tail -f /var/log/nginx/error.log | grep \u0026#34;upstream\u0026#34; # 2. 检查 Nginx 状态 curl http://localhost/nginx_status # 观察 Active connections 和 Waiting # 3. 检查后端连接 netstat -an | grep :8080 | wc -l netstat -an | grep :8080 | grep TIME_WAIT | wc -l # 4. 查看后端线程池 # Tomcat: JMX 或 access_log 分析响应时间 # Go/Node: 查看 Goroutine/EventLoop 阻塞情况 # 5. 检查 Nginx 配置 nginx -T | grep -A 10 \u0026#34;upstream\u0026#34; 使用命令：\n1 2 3 4 5 # 实时查看 Nginx 连接状态分布 watch -n 1 \u0026#39;netstat -n | awk \u0026#34;/^tcp/ {++S[\\$NF]} END {for(a in S) print a, S[a]}\u0026#34;\u0026#39; # 压测验证 ab -n 10000 -c 1000 http://nginx_ip/ 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 # 1. 开启长连接（关键） upstream backend { server 127.0.0.1:8080; keepalive 32; # 保持 32 个长连接 keepalive_requests 1000; keepalive_timeout 60s; } server { location / { proxy_pass http://backend; proxy_http_version 1.1; # 必须开启 HTTP/1.1 以支持 keepalive proxy_set_header Connection \u0026#34;\u0026#34;; # 清除 Connection: close proxy_connect_timeout 5s; proxy_send_timeout 10s; proxy_read_timeout 30s; } } # 2. 调整 Nginx 并发限制 events { worker_connections 65535; multi_accept on; } # 3. 优化后端连接池 # Tomcat: server.xml \u0026lt;Connector maxThreads=\u0026#34;500\u0026#34; minSpareThreads=\u0026#34;50\u0026#34; /\u0026gt; # 确保后端最大连接数 \u0026gt; Nginx keepalive 总数 # 4. 重启 Nginx nginx -t \u0026amp;\u0026amp; nginx -s reload 预防措施：\n长连接标配：Nginx 到后端必须配置 keepalive 和 proxy_http_version 1.1。 超时匹配：Nginx 超时时间应略大于后端处理最长耗时，避免误杀。 容量规划：根据 QPS 和平均响应时间计算所需连接数（QPS * RT）。 监控：监控 Nginx upstream 状态和后端的活跃连接数。 经验总结：\n502 往往不是后端挂了，而是连接建立失败了。 短连接是高并发系统的杀手，长连接是标配。 Nginx 配置中的 proxy_http_version 1.1 和 Connection \u0026quot;\u0026quot; 缺一不可。 事故 69：Linux 内核参数未优化导致高并发丢包 事故现象：\n高并发压测时，大量请求超时或连接重置（Connection Reset） netstat 显示大量 SYN_RECV 或 TIME_WAIT 系统日志 dmesg 出现 TCP: request_sock_TCP: Possible SYN flooding 带宽和 CPU 未满，但吞吐量上不去 事故原因：\nnet.core.somaxconn 太小，监听队列溢出 net.ipv4.tcp_max_syn_backlog 太小，SYN 队列溢出 net.ipv4.ip_local_port_range 范围过窄，端口耗尽 net.core.netdev_max_backlog 太小，网卡接收队列丢包 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 查看当前内核参数 sysctl net.core.somaxconn sysctl net.ipv4.tcp_max_syn_backlog sysctl net.ipv4.ip_local_port_range # 2. 查看丢包统计 netstat -s | grep -i \u0026#34;listen\u0026#34; # 查看 \u0026#34;times the listen queue of a socket overflowed\u0026#34; # 查看 \u0026#34;SYNs to LISTEN sockets dropped\u0026#34; # 3. 查看网卡丢包 ifconfig eth0 # 查看 RX dropped / TX dropped # 4. 查看系统日志 dmesg | grep -i \u0026#34;drop\\|overflow\\|flood\u0026#34; 使用命令：\n1 2 3 4 5 # 实时监控 SYN 队列溢出 watch -n 1 \u0026#39;netstat -s | grep \u0026#34;listen queue\u0026#34;\u0026#39; # 查看端口使用情况 ss -s 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # 1. 优化内核参数 (/etc/sysctl.conf) cat \u0026gt;\u0026gt; /etc/sysctl.conf \u0026lt;\u0026lt; EOF # 增加监听队列长度 net.core.somaxconn = 65535 net.ipv4.tcp_max_syn_backlog = 65535 # 扩大本地端口范围 net.ipv4.ip_local_port_range = 1024 65535 # 增加网卡接收队列 net.core.netdev_max_backlog = 250000 # 开启 SYN Cookies (防攻击) net.ipv4.tcp_syncookies = 1 # 允许重用 TIME_WAIT socket net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_fin_timeout = 30 # 增加文件句柄 fs.file-max = 2097152 EOF # 2. 生效配置 sysctl -p # 3. 调整用户限制 (/etc/security/limits.conf) * soft nofile 65535 * hard nofile 65535 # 4. 验证效果 # 重新压测，观察 netstat -s 中的 drop 计数是否不再增加 预防措施：\n基线检查：新服务器上线前必须执行内核参数优化脚本。 自动化运维：使用 Ansible/Puppet 统一管理内核参数。 压测前置：任何高并发上线前，必须在仿真环境进行内核级压测。 监控：监控 netstat -s 中的关键丢包指标。 经验总结：\nLinux 默认配置是为通用场景设计的，不适合高并发服务器。 监听队列溢出是隐蔽的杀手，往往被误认为是应用层问题。 内核参数调优是高并发系统的“入场券”。 事故 70：CI/CD 流水线阻塞导致发布窗口错过 事故现象：\n生产发布窗口仅剩 30 分钟，但流水线一直卡在 \u0026ldquo;Pending\u0026rdquo; 或 \u0026ldquo;Failed\u0026rdquo; 无法上线紧急 Bug 修复 多个团队排队等待 Runner 资源 最终被迫推迟发布，影响业务活动 事故原因：\nGitLab Runner/GitHub Actions Runner 资源不足（CPU/内存满） 依赖镜像拉取超时（Docker Hub 限流或网络问题） 测试用例不稳定（Flaky Tests）导致反复重试 构建缓存失效，每次全量编译耗时过长 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 1. 检查 Runner 状态 # GitLab: Admin Area -\u0026gt; Runners # 查看是否有 Available Runner，以及 Job 队列长度 # 2. 查看具体 Job 日志 # 卡在哪一步？Clone? Build? Test? Deploy? # 如果是 Pull Image 慢，检查网络 # 3. 检查 Runner 机器负载 top free -h df -h # 查看是否磁盘满或内存溢出 # 4. 分析构建时长趋势 # CI 平台提供的 Analytics 面板 # 对比历史构建时间，定位突增步骤 使用命令：\n1 2 3 4 5 6 7 8 # 清理 Docker 缓存（在 Runner 上） docker system prune -af # 手动注册临时 Runner gitlab-runner register --url https://gitlab.com/ --registration-token XXX # 测试镜像拉取速度 time docker pull alpine:latest 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 1. 紧急扩容 Runner # 动态启动 Spot 实例作为临时 Runner # 或在 K8s 中扩容 runner-deployment # 2. 优化镜像拉取 # 搭建私有镜像仓库（Harbor/Nexus）并配置代理加速 # 在 Runner 节点预拉取常用基础镜像 # 3. 修复不稳定测试 # 暂时跳过非核心的 Flaky Tests (添加 @ignore 标签) # 优先保证主流程通过 # 4. 启用构建缓存 # 配置 Docker Layer Caching # 配置 Maven/Gradle/NPM 依赖缓存 # 5. 并行化构建 # 将串行任务改为并行（Matrix Build） 预防措施：\n弹性 Runner：基于 K8s 或云自动伸缩组实现 Runner 弹性伸缩。 缓存策略：精细化配置依赖缓存和 Docker 层缓存。 测试治理：定期清理和修复 Flaky Tests，建立测试稳定性红线。 多源加速：配置多个镜像源，避免单点限流。 经验总结：\nCI/CD 效率直接影响业务迭代速度，是核心竞争力。 构建环境的稳定性往往被忽视，直到发布时才爆发。 缓存和并行是提升构建速度的两大法宝。 事故 71：分布式事务数据不一致（TCC 模式失效） 事故现象：\n支付成功但订单状态未更新 库存扣减了但订单未生成 对账发现大量“长款”或“短款” 尝试手动补偿时，发现 TCC 的 Cancel 接口幂等性失效，导致重复回滚 事故原因：\nTCC 第二阶段（Confirm/Cancel）网络超时，发起方未收到结果，未进行重试 Cancel 接口未实现幂等，重复调用导致数据错误 全局事务记录表（Log）写入失败，导致状态丢失 分支事务注册失败，协调者不知道有参与者 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 -- 1. 查询全局事务日志 SELECT * FROM distributed_transaction_log WHERE status != \u0026#39;COMMITTED\u0026#39; AND status != \u0026#39;ROLLBACKED\u0026#39;; -- 2. 检查分支事务状态 SELECT * FROM branch_transaction_log WHERE global_id = \u0026#39;xxx\u0026#39;; -- 3. 比对业务数据 -- 支付表 vs 订单表 vs 库存表 SELECT p.status, o.status, i.qty FROM payment p, orders o, inventory i WHERE p.order_id = o.id AND o.id = i.order_id AND p.id = \u0026#39;xxx\u0026#39;; -- 4. 查看应用日志 grep \u0026#34;TCC Confirm\\|TCC Cancel\u0026#34; app.log | grep \u0026#34;Error\\|Timeout\u0026#34; 使用命令：\n1 2 3 # 模拟 TCC 异常 # 使用 ChaosBlade 注入网络延迟或丢包 blade create network delay --time 3000 --interface eth0 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 1. 实现严格的幂等性 // Cancel 方法中先检查状态，如果已回滚则直接返回成功 public void cancel(String businessKey) { if (alreadyRolledBack(businessKey)) { return; // 幂等返回 } // 执行回滚逻辑 doRollback(businessKey); markAsRolledBack(businessKey); } // 2. 引入定时对账补偿任务 // 扫描超过 N 分钟仍处于 TRYING 状态的事务 @Scheduled(cron = \u0026#34;0 */5 * * * ?\u0026#34;) public void compensate() { List\u0026lt;Transaction\u0026gt; timeouts = txService.findTimeoutTransactions(); for (Transaction tx : timeouts) { // 查询各分支状态，决定 Confirm 还是 Cancel txManager.compensate(tx); } } // 3. 优化重试机制 // 使用指数退避策略重试 Confirm/Cancel // 最大重试次数设为 10 次，超过后转入人工处理队列 预防措施：\n最终一致性：接受短暂不一致，依靠对账和补偿机制保证最终一致。 幂等性设计：所有分布式操作（尤其是回滚）必须幂等。 事务日志持久化：在执行业务前先写日志，确保状态可恢复。 定期对账：建立 T+1 或准实时的对账系统，自动发现并修复差异。 经验总结：\n分布式事务没有银弹，TCC 复杂度高，需谨慎使用。 幂等性是分布式系统的基石，没有幂等就没有重试。 对账系统是数据一致性的最后一道防线。 事故 72：分库分表路由错误导致数据查不到 事故现象：\n用户反馈查不到自己的订单 后台管理系统搜索特定 ID 返回空 数据库中存在该数据，但应用层查不出 扩容迁移后，部分旧数据无法访问 事故原因：\n分片算法（Sharding Algorithm）变更，新旧数据路由规则不一致 分片键（Sharding Key）选择不当，导致查询走全路由（广播）被限流 扩容迁移过程中，数据双写不一致，旧库数据未清洗 路由配置文件中表名映射错误 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 开启 SQL 打印 # 查看 ShardingSphere/JDBC 实际路由到的物理表 logging.level.org.apache.shardingsphere=DEBUG # 2. 手动计算路由 # 根据分片算法（如 hash(id) % 4），手动计算 ID 应在哪个库哪张表 # 直接查询该物理表验证数据是否存在 # 3. 检查配置 cat sharding-config.yaml # 核对 binding-tables, broadcast-tables, sharding-column 配置 # 4. 检查迁移日志 # 确认数据同步任务是否完成，是否有报错跳过 使用命令：\n1 2 3 -- 直接登录物理库查询 USE db_order_0; SELECT * FROM t_order_1 WHERE order_id = \u0026#39;12345\u0026#39;; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 1. 修正路由配置 # 确保新旧数据兼容，或实施数据迁移 sharding-rule: tables: t_order: actual-data-nodes: ds$-\u0026gt;{0..1}.t_order$-\u0026gt;{0..3} table-strategy: inline: sharding-column: order_id algorithm-expression: t_order$-\u0026gt;{order_id % 4} # 2. 实施数据迁移（双写 + 校验 + 切换） # 阶段 1：双写新旧库 # 阶段 2：后台全量校验数据一致性 # 阶段 3：读取切换到新库 # 阶段 4：停止旧库写入 # 3. 紧急修复查询 # 对于少量查不到的数据，建立映射表（ID -\u0026gt; 物理库表） # 或在网关层做特殊路由转发 预防措施：\n分片键选择：尽量选择离散度高、查询常用的字段（如 UserID 而非 OrderID）。 兼容性设计：分片算法变更必须配合平滑迁移方案。 全链路测试：上线前在仿真环境构造海量数据，验证路由准确性。 管理工具：提供后台工具，支持按全局 ID 查询物理位置。 经验总结：\n分库分表是“不归路”，前期设计必须慎重。 数据迁移是高风险操作，必须有回滚和校验机制。 路由规则的错误往往是致命的，会导致数据“隐形”。 事故 73：缓存与数据库双写不一致（经典难题） 事故现象：\n用户修改资料后，前端仍显示旧数据 并发更新时，数据库是新值，缓存是旧值（或反之） 出现“脏读”，持续时间不定 事故原因：\n采用“先更库后删缓存”，并发下旧数据回填（读写竞争） 采用“先删缓存后更库”，延时双删失败 缓存过期时间设置过长，且无主动更新机制 消息队列消费失败，导致异步更新缓存丢失 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 复现并发场景 # 使用 JMeter 模拟高并发读写同一 Key # 2. 查看代码逻辑 # 确认是 Cache Aside, Read/Write Through 哪种模式 # 检查是否有延时双删，延迟时间是否合理 # 3. 检查 Binlog 监听 # Canal/Otter 是否正常消费，有无积压或报错 grep \u0026#34;parse binlog error\u0026#34; canal.log # 4. 对比数据 redis-cli GET user:1001 mysql -e \u0026#34;SELECT * FROM users WHERE id=1001\u0026#34; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // 方案 A：Cache Aside + 延时双删（改进版） public void update(User user) { // 1. 删除缓存 redis.delete(\u0026#34;user:\u0026#34; + user.getId()); // 2. 更新数据库 db.update(user); // 3. 延时再次删除（异步） // 休眠时间 \u0026gt; 读业务完成时间 CompletableFuture.runAsync(() -\u0026gt; { Thread.sleep(500); redis.delete(\u0026#34;user:\u0026#34; + user.getId()); }); } // 方案 B：监听 Binlog 异步更新（推荐） // 1. 只更新数据库 // 2. Canal 监听 Binlog 变化 // 3. 发送 MQ 消息 // 4. 消费者收到消息后删除/更新缓存 // 优点：解耦，保证最终一致性，无并发竞争 // 方案 C：分布式锁（强一致场景） // 更新时加锁，确保读写串行化（性能损耗大，慎用） RLock lock = redisson.getLock(\u0026#34;lock:user:\u0026#34; + id); lock.lock(); try { db.update(); redis.delete(); } finally { lock.unlock(); } 预防措施：\n首选方案：非强一致场景使用 Cache Aside + Binlog 异步删除。 短期过期：缓存必须设置过期时间，作为兜底。 读写分离容忍：业务层需接受秒级的数据不一致。 监控：监控缓存与 DB 的不一致率（通过抽样比对）。 经验总结：\n缓存一致性是 CAP 中的权衡，通常选择 AP（可用性 + 分区容错）。 “先更库后删缓存”在极高并发下仍有风险，Binlog 方案更稳健。 永远不要相信“绝对实时”的缓存。 事故 74：消息队列重复消费导致资损 事故现象：\n用户账户被重复充值 订单被重复发货 积分被多次累加 日志显示同一条 Message ID 被处理了多次 事故原因：\n消费者业务逻辑执行成功，但 ACK 发送失败（网络抖动/宕机） 消费者处理超时，MQ 认为消费失败并重投 代码中未做幂等判断，直接执行写入 手动重置 Offset 导致消息回溯 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 查看 MQ 控制台 # 检查消息重投记录（Retry Count） # 查看死信队列（DLQ）是否有相关消息 # 2. 分析业务日志 grep \u0026#34;MessageID: xxx\u0026#34; app.log # 发现同一条 ID 出现了两次 \u0026#34;Process Success\u0026#34; # 3. 检查 ACK 机制 # 确认是自动 ACK 还是手动 ACK # 如果是手动 ACK，是否在 catch 块中捕获了异常但未 ACK 或 NACK # 4. 检查数据库唯一键 # 为什么重复插入没有报错？（可能漏了唯一索引） 使用命令：\n1 2 3 4 5 # Kafka 查看消费进度 kafka-consumer-groups.sh --bootstrap-server kafka:9092 --describe --group order-group # RocketMQ 查询消息轨迹 mqadmin queryMsgById -n namesrv_addr -i MessageID 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 // 1. 实现幂等性（核心） // 方法 A：数据库唯一索引 ALTER TABLE payments ADD UNIQUE INDEX uk_msg_id (message_id); // 捕获 DuplicateKeyException 并忽略 // 方法 B：状态机 CAS UPDATE orders SET status = \u0026#39;PAID\u0026#39; WHERE id = 123 AND status = \u0026#39;UNPAID\u0026#39;; // 检查 affected rows，若为 0 说明已处理过 // 方法 C：Redis 防重表 String key = \u0026#34;processed:\u0026#34; + messageId; if (redis.setIfAbsent(key, \u0026#34;1\u0026#34;, 3600)) { processBusiness(); } else { // 重复消息，直接 ACK ack(); } // 2. 优化 ACK 时机 // 确保业务逻辑完全成功后再发送 ACK // 在 try-catch-finally 中妥善处理异常，避免无限重投 try { process(msg); ack.acknowledge(); } catch (Exception e) { log.error(\u0026#34;Process failed\u0026#34;, e); // 记录错误，发送告警，视情况 NACK 或丢弃 } 预防措施：\n设计原则：MQ 投递语义是 At-Least-Once，消费端必须实现幂等。 唯一键约束：数据库层面必须有唯一索引作为最后一道防线。 日志追踪：每条消息处理前后必须打印 MessageID，便于追溯。 死信处理：多次重试失败的消息应进入死信队列，人工介入。 经验总结：\n消息队列的“可靠性”是以“重复”为代价的。 幂等性不是可选项，是必选项。 数据库唯一索引是防止资损的最简单有效手段。 事故 75：服务注册中心过载导致服务发现失效 事故现象：\n新启动的服务实例无法注册到 Nacos/Eureka 现有服务心跳超时，被错误剔除，导致流量中断 客户端获取服务列表为空或过时 注册中心 CPU 100%，响应极慢 事故原因：\nK8s 弹性伸缩导致实例数瞬间激增，注册请求洪峰 客户端心跳频率过高（默认 5s），注册中心扛不住 注册中心集群节点过少，或未开启集群模式 大量无效服务（测试实例）占用资源 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 1. 查看注册中心监控 # CPU, Memory, Network IO, Request QPS # 关注 Register 和 Heartbeat 接口延迟 # 2. 检查客户端日志 grep \u0026#34;register failed\\|heartbeat failed\u0026#34; app.log # 3. 查看实例数量 curl http://nacos-server:8848/nacos/v1/ns/instance/list?serviceName=order-service # 统计实例数是否异常巨大 # 4. 检查网络 # 客户端到注册中心的网络是否有丢包或延迟 使用命令：\n1 2 3 4 5 # 查看 Nacos 集群状态 curl http://nacos-server:8848/nacos/v1/ns/operator/metrics # 查看 Eureka 状态 curl http://eureka-server:8761/eureka/apps 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 1. 调整心跳频率 # 客户端配置 spring: cloud: nacos: discovery: heart-beat-interval: 10000 # 从 5s 改为 10s heart-beat-timeout: 30000 ip-delete-timeout: 90000 # 2. 扩容注册中心 # 增加 Nacos/Eureka 节点，组建集群 # 使用负载均衡器分发注册请求 # 3. 开启客户端缓存 # 客户端本地缓存服务列表，减少拉取频率 # Nacos 默认开启，检查配置是否被禁用 # 4. 清理无效实例 # 下线测试环境实例 # 设置实例 TTL，自动清理长期无心跳的僵尸节点 # 5. 降级策略 # 当注册中心不可用时，使用本地缓存列表继续运行 预防措施：\n集群部署：注册中心必须集群化，且具备自动扩缩容能力。 参数调优：根据规模调整心跳间隔，避免“心跳风暴”。 多租户隔离：生产、测试环境注册中心物理隔离。 本地缓存：客户端必须具备本地缓存和降级能力。 经验总结：\n注册中心是微服务的“电话簿”，一旦失联，整个系统瘫痪。 心跳频率不是越快越好，需在实时性和负载间平衡。 客户端缓存是应对注册中心抖动的缓冲垫。 事故 76：API 网关限流配置误伤正常用户 事故现象：\n大量正常用户请求被返回 429 Too Many Requests 某地区或某运营商用户全部无法访问 业务量骤降 50%，但系统负载很低 客服接到大量投诉 事故原因：\n限流维度配置错误：按“出口 IP”限流，导致 NAT 后的所有用户共享配额 阈值设置过低：基于低估的 QPS 设定了激进的限流值 爬虫识别规则过严，误判正常浏览器指纹 配置发布未经过灰度，直接全量生效 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 # 1. 查看网关访问日志 awk \u0026#39;$9 == 429 {print $1}\u0026#39; access.log | sort | uniq -c | sort -rn | head -10 # 发现大量 429 来自同一个 IP（网关出口 IP 或 运营商网关 IP） # 2. 检查限流配置 cat gateway-config.yaml # 关注 limit_req_zone 的 key 设置 # 错误示例：limit_req_zone $server_name ... (全局限流) # 错误示例：limit_req_zone $binary_remote_addr ... (在网关层 $remote_addr 是用户 IP，但在某些架构下可能是 LB IP) # 3. 分析用户分布 # 确认被限流用户是否集中在特定特征（如特定 User-Agent, 特定地区） 使用命令：\n1 2 3 4 5 # 实时统计 429 来源 tail -f access.log | awk \u0026#39;$9==429 {print $1}\u0026#39; | sort | uniq -c | sort -rn # 测试限流规则 ab -n 1000 -c 50 http://gateway/api/test 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # 1. 修正限流维度 # 使用 X-Forwarded-For 获取真实用户 IP # 注意：需确保 X-Forwarded-For 可信，否则易被伪造 map $http_x_forwarded_for $client_real_ip { default $http_x_forwarded_for; \u0026#34;\u0026#34; $remote_addr; } # 取第一个 IP（最左侧是真实用户） set $real_ip $http_x_forwarded_for; # 使用 Lua 脚本提取第一个 IP 更稳妥 limit_req_zone $real_ip zone=user_limit:10m rate=10r/s; # 2. 调整阈值 # 基于历史峰值 QPS 的 1.5 倍设定 # 区分核心接口和非核心接口，设置不同阈值 # 3. 白名单机制 # 将内部 IP、合作伙伴 IP 加入白名单 geo $whitelist { default 0; 192.168.1.0/24 1; 10.0.0.0/8 1; } limit_req_zone $whitelist zone=white:10m rate=10000r/s; # 4. 紧急回滚 # 回退到上一版本配置 # 或临时关闭限流模块（风险较高） 预防措施：\n维度选择：尽量按 UserID、API Key 限流，避免按 IP（除非防攻击）。 动态配置：限流阈值应支持动态调整，无需重启。 灰度发布：限流规则变更必须先小流量验证。 友好提示：429 页面应提示用户“稍后重试”，而非冷冰冰的错误码。 经验总结：\n限流是保护伞，但也可能成为拦路虎。 NAT 环境下的 IP 限流极易误伤，需格外小心。 监控 429 比例，异常升高立即告警。 事故 77：链路追踪数据丢失导致排障困难 事故现象：\n发生故障时，SkyWalking/Zipkin 中找不到对应的 TraceID 调用链断裂，无法定位是哪个服务出错 采样率过低，关键错误请求未被记录 追踪数据延迟高达数小时，失去实时意义 事故原因：\n采样率设置过低（如 0.1%），漏掉了低频但关键的错误 Collector 接收端过载，丢弃了大量数据 Agent 版本与 Server 不兼容，数据上报失败 网络带宽限制，Trace 数据被限流 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 检查 Agent 日志 cat skywalking-agent.log | grep -i \u0026#34;report\\|error\u0026#34; # 查看是否有 \u0026#34;Queue full\u0026#34;, \u0026#34;Send failed\u0026#34; # 2. 检查 Collector 负载 # CPU, Memory, GC 情况 # 查看 Backend 存储（ES/H2）写入延迟 # 3. 验证采样配置 cat agent.config # sampling_rate 设置是多少？ # 4. 测试上报 # 手动发起一个请求，查看是否能实时出现在 UI 上 使用命令：\n1 2 3 # 查看 SkyWalking OAP 指标 # 监控 Buffer 队列大小 # 监控 Report 成功率 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 1. 动态调整采样率 # 正常时期：0.1% - 1% # 故障时期/慢请求：100% # SkyWalking 支持动态配置，通过 UI 或 API 调整 # 设置规则：如果 RT \u0026gt; 1s 或 Status \u0026gt;= 500，强制采样 100% # 2. 扩容 Collector # 增加 OAP 节点，水平扩展 # 优化存储后端（ES 分片策略，SSD 磁盘） # 3. 升级 Agent # 确保所有服务 Agent 版本与 Server 兼容 # 统一升级至最新稳定版 # 4. 异步上报与缓冲 # 增加 Agent 端缓冲队列大小 # 启用压缩传输 预防措施：\n智能采样：基于错误率和延迟动态调整采样率，而非固定比例。 独立集群：链路追踪系统应独立部署，避免受业务影响。 版本管理：严格管理 Agent 版本，定期升级。 数据保留：合理规划存储 retention，平衡成本与需求。 经验总结：\n链路追踪是微服务的“黑匣子”，关键时刻必须有用。 固定低采样率在排查偶发故障时毫无价值。 错误请求和慢请求必须 100% 采集。 事故 78：配置中心推送全量覆盖导致服务启动失败 事故现象：\n配置中心发布新版本后，所有服务重启失败 报错：Configuration property xxx not found 部分关键配置（如 DB 密码、Redis 地址）丢失 回滚配置后仍需重启服务才能恢复 事故原因：\n运维人员误操作：上传了空的或不完整的配置文件 发布脚本逻辑缺陷：用新文件全量覆盖旧文件，而非增量合并 配置中心缺乏 Diff 预览和校验机制 服务启动强依赖配置中心，无本地兜底 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 1. 查看配置中心历史版本 # 对比发布前后的配置内容 # 发现新版本配置项大幅减少 # 2. 检查服务启动日志 grep \u0026#34;Failed to load configuration\u0026#34; app.log # 3. 查看操作审计 # 谁在什么时间执行了 Publish 操作？ # 是否有审批记录？ # 4. 验证本地配置 # 检查容器内是否挂载了本地配置文件（如有） 使用命令：\n1 2 # 模拟配置拉取 curl http://config-server/app/profile/dev 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 1. 紧急回滚 # 在配置中心界面点击“回滚”到上一版本 # 通知所有服务重启（或触发 Refresh） # 2. 修复配置 # 补充缺失的配置项 # 重新发布 # 3. 增加校验机制 # 发布前自动校验配置格式（YAML/JSON 合法性） # 校验必填项是否存在 # 提供 Diff 预览，人工确认变更点 # 4. 本地兜底 # 应用启动时，若配置中心不可用或配置缺失，使用打包在 Jar 内的默认配置 # bootstrap.yml 中配置 fallback spring: cloud: config: fail-fast: false # 启动不阻断 fallback-to-application-properties: true 预防措施：\n权限控制：生产环境配置修改需双人复核（Four-eyes）。 增量更新：配置中心应支持增量 Patch，避免全量覆盖风险。 自动化测试：配置变更触发自动化测试，验证服务启动。 本地缓存：客户端缓存最新配置，断网也能启动。 经验总结：\n配置即代码，配置变更的风险不亚于代码发布。 全量覆盖是危险的操作模式，增量合并更安全。 兜底机制是防止配置错误的最后一道防线。 事故 79：容器镜像漏洞导致大规模挖矿入侵 事故现象：\n服务器 CPU 100%，风扇狂转 发现未知进程（如 kworker, systemd-update 伪装）占用 CPU 异常外连矿池端口（如 3333, 8888） 多个 Pod 同时中招，横向扩散 事故原因：\n使用了含有已知漏洞的基础镜像（如老旧的 CentOS, Ubuntu） 应用依赖库存在高危漏洞（如 Log4j, Fastjson） 镜像仓库未进行安全扫描 容器以 Root 权限运行，被攻破后可控制宿主机 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 查找异常进程 top -c ps -ef | grep -v grep | grep -E \u0026#34;mining|pool|stratum\u0026#34; # 2. 检查网络连接 netstat -antp | grep ESTABLISHED # 查找可疑的外连 IP # 3. 检查定时任务 crontab -l ls -la /var/spool/cron/ # 黑客常写入定时任务持久化 # 4. 扫描镜像 trivy image my-app:latest # 查看 CVE 漏洞报告 使用命令：\n1 2 3 # 查找被篡改的系统文件 rpm -Va # 或使用 chkrootkit, rkhunter 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # 1. 紧急隔离 # 断开受害节点网络 # 杀掉恶意进程 kill -9 \u0026lt;PID\u0026gt; # 2. 清理持久化 rm -f /var/spool/cron/root rm -f /tmp/.X11-unix/X0... # 常见隐藏路径 # 3. 重建容器 # 不要试图修复被黑的容器，直接删除 kubectl delete pod \u0026lt;malicious-pod\u0026gt; # 强制拉取新的安全镜像 # 4. 修复漏洞 # 升级基础镜像到最新版 # 升级应用依赖库 # 禁止容器以 Root 运行 securityContext: runAsNonRoot: true runAsUser: 1000 # 5. 全面扫描 # 对所有存量镜像进行漏洞扫描，修复高危漏洞 预防措施：\n镜像扫描：CI/CD 流水线集成 Trivy/Clair，阻断含高危漏洞的镜像上线。 最小权限：容器严禁以 Root 运行，使用只读文件系统。 网络策略：K8s NetworkPolicy 限制 Pod 出站流量，禁止访问矿池。 定期更新：建立基础镜像定期更新机制。 经验总结：\n镜像安全是容器安全的第一道门。 Root 权限是黑客的最爱，必须剥夺。 自动化扫描是发现漏洞的最高效手段。 事故 80：监控告警风暴导致运维瘫痪 (注：此事故已在 91-100 部分详细展开，此处略过，避免重复，重点放在架构类)\n事故 81：跨区域容灾切换失败（RPO 不达标） 事故现象：\n主区域（Region A）发生地震/断电，触发容灾切换 切换至备区（Region B）后，发现丢失了最近 30 分钟的数据 业务虽然恢复，但用户投诉订单消失、余额不对 切换过程耗时 2 小时，远超 RTO 目标（15 分钟） 事故原因：\n数据同步机制为异步复制，存在固有延迟 切换前未检查同步延迟（Lag），盲目切换 DNS 切换 TTL 设置过长，用户仍访问旧区 备区容量不足，切换后瞬间流量打垮备区 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 检查数据同步延迟 # MySQL: SHOW SLAVE STATUS\\G (Seconds_Behind_Master) # DTS/CDC: 查看同步延迟监控图表 # 2. 检查 DNS 生效情况 dig @8.8.8.8 www.example.com # 观察是否仍解析到旧区 IP # 3. 检查备区负载 kubectl top nodes -l region=backup # 观察 CPU/Mem 是否爆满 使用命令：\n1 2 # 模拟切换演练 # 定期执行真实的故障转移测试 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 紧急数据补救 # 尝试从主区残存节点导出最后时刻的 Binlog # 在备区重放，尽可能减少 RPO # 2. 优化切换流程 # 步骤 1：停止主区写入（强制只读） # 步骤 2：等待同步延迟为 0 # 步骤 3：提升备区为主 # 步骤 4：切换 DNS/GSLB # 3. 扩容备区 # 备区应具备与主区同等的处理能力 # 平时可承担读流量或离线任务 # 4. 缩短 DNS TTL # 平时设置较短 TTL（如 60s），便于快速切换 预防措施：\n定期演练：每季度进行一次真实的跨区切换演练，验证 RPO/RTO。 同步监控：实时监控同步延迟，超过阈值立即告警。 容量对等：备区资源必须充足，避免“切换即雪崩”。 自动化切换：尽可能使用自动化脚本或平台执行切换，减少人为失误。 经验总结：\n容灾不是买个产品就行，必须靠演练来验证。 RPO（数据丢失量）和 RTO（恢复时间）是容灾的核心指标。 异步复制必然有数据丢失风险，关键业务需评估是否上强一致方案。 事故 82：多活数据中心“脑裂”双写冲突 事故现象：\n两个机房（A/B）同时对外提供服务，且都可写 网络专线中断，两边各自独立运行 网络恢复后，同一用户在 A 改了密码，在 B 下了订单 数据合并时发生严重冲突，部分操作被覆盖 事故原因：\n架构设计为“双活”，但缺乏有效的冲突检测与解决机制 数据库层未做单元化（Sharding）隔离，允许跨库写入 仲裁机制失效，未能及时切断一方写入 排查过程：\n1 2 3 4 5 6 -- 1. 比对数据差异 -- 找出两边不一致的记录 SELECT * FROM users WHERE last_modified \u0026gt; \u0026#39;故障开始时间\u0026#39;; -- 2. 检查同步日志 -- 查看双向同步工具（如 DTS）的冲突报错 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 1. 紧急止血 # 立即将其中一个机房设为只读（Read-Only） # 停止双向同步，防止冲突扩散 # 2. 数据合并策略 # 策略 A：以时间戳最新为准（Last Write Wins），可能丢失部分数据 # 策略 B：人工介入，逐条核对关键数据 # 策略 C：业务层合并（如订单和改密互不影响，可共存） # 3. 架构改造 # 实施单元化架构（Cell-Based Architecture） # 用户 ID 取模，固定路由到特定机房，从根本上避免双写 # 只有元数据或全局配置才允许异地同步 预防措施：\n单元化部署：核心业务数据按用户维度拆分，单机房闭环，避免跨机房双写。 仲裁机制：部署第三方仲裁节点，网络分区时强制选主。 冲突检测：应用层引入版本号（Version）或向量时钟，检测并拒绝冲突写入。 慎用双活：除非业务极度敏感，否则优先采用主备模式。 经验总结：\n双活是架构的皇冠，也是深渊。没有单元化，双活就是灾难。 数据冲突的解决成本远高于预防成本。 大多数业务不需要真正的双活，主备 + 快速切换足矣。 事故 83：云服务商区域性故障（依赖风险） 事故现象：\n某云厂商（如 AWS us-east-1, 阿里云华东 1）整个 Region 不可用 EC2/ECS 无法启动，RDS 无法连接，S3/OSS 无法读写 业务完全停摆，持续数小时 社交媒体上该云厂商热搜第一 事故原因：\n云厂商内部基础设施故障（电力、网络、控制平面） 用户架构强依赖单一 Region，无跨 Region 容灾 即使有多 AZ（可用区），但该故障影响了整个 Region 的控制面 排查过程：\n1 2 3 4 5 6 7 8 9 10 # 1. 确认故障范围 # 查看云厂商 Status Page # 尝试登录控制台，是否卡顿或报错 # 2. 测试连通性 ping \u0026lt;region-endpoint\u0026gt; telnet \u0026lt;rds-endpoint\u0026gt; 3306 # 3. 评估影响 # 统计多少比例的业务部署在该 Region 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 启用多云/多 Region 灾备 # 如果有备 Region，立即切换 DNS 流量 # 激活备用数据库（如果是异步复制，接受数据丢失） # 2. 降级服务 # 挂出“维护中”页面 # 暂停非核心功能，保留只读模式（如果有本地缓存） # 3. 联系云厂商 # 提交最高级别工单 # 关注官方通报，预估恢复时间 预防措施：\n多云战略：核心业务部署在两个以上云厂商或 Region。 架构解耦：使用 Terraform/Crossplane 等工具屏蔽云厂商差异，便于迁移。 数据同步：建立跨 Region 的数据实时同步机制。 应急预案：明确在云厂商宕机时的手动切换流程。 经验总结：\n云厂商也会挂，不要把命脉完全交给别人。 单 Region 架构在云时代等同于“裸奔”。 多云成本高，但对于核心业务是必要的保险费。 事故 84：TLS 证书链不完整导致部分客户端失败 事故现象：\nPC 浏览器访问正常，但部分 Android 手机、iOS App、Java 客户端报错 错误信息：SSLHandshakeException: unable to find valid certification path SSL Labs 测试评级为 A- 或 B，提示 \u0026ldquo;Chain issues\u0026rdquo; 新用户无法注册，老用户不受影响（因为缓存了 Session） 事故原因：\n服务器配置只部署了叶子证书（Leaf Certificate），缺少中间证书（Intermediate CA） PC 操作系统根证书库全，能自动补全链条；移动端/老旧 JDK 根库不全，无法补全 证书申请后，未正确合并证书文件 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 使用 OpenSSL 验证 openssl s_client -connect www.example.com:443 -showcerts # 观察返回的证书链，是否只有 1 张证书？（正常应有 2-3 张） # 2. 在线工具检测 # 访问 SSL Labs (ssllabs.com) 输入域名检测 # 查看 \u0026#34;Chain issues\u0026#34; 详情 # 3. 检查服务器配置 cat /etc/nginx/ssl/server.crt # 确认是否包含了中间证书内容 使用命令：\n1 2 # 查看证书链详情 openssl s_client -connect www.example.com:443 \u0026lt;/dev/null 2\u0026gt;/dev/null | openssl x509 -noout -issuer 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 1. 获取中间证书 # 从证书颁发机构（CA）下载 Intermediate CA 证书 # 2. 合并证书 cat leaf_certificate.crt intermediate_ca.crt \u0026gt; fullchain.crt # 顺序必须是：叶子证书在前，中间证书在后 # 3. 部署并重启 cp fullchain.crt /etc/nginx/ssl/ nginx -t \u0026amp;\u0026amp; nginx -s reload # 4. 验证 # 再次使用 openssl 或 SSL Labs 验证，确保链完整 预防措施：\n自动化管理：使用 Certbot/Let\u0026rsquo;s Encrypt 自动部署，它们会自动处理链条。 全面测试：证书更新后，必须在多种设备（PC, Mobile, IoT）上测试。 监控：监控证书链完整性，而不仅仅是有效期。 经验总结：\n证书链不完整是低级但高发的错误，兼容性杀手。 永远不要手动拼接证书，尽量自动化。 移动端和老旧系统的根证书库远不如 PC 完善。 事故 85：NTP 时间同步漂移导致签名验证失败 事故现象：\nOAuth2 登录大面积失败，报错 Invalid Token 或 Token Expired API 签名验证失败，返回 403 Forbidden 分布式锁失效，日志时间混乱 数据库主从同步报错（GTID 依赖时间） 事故原因：\n服务器 NTP 服务未启动或配置错误 虚拟化环境（VM）时间漂移，未安装 Guest Tools 本地硬件时钟（RTC）电池没电，重启后时间重置 防火墙阻断了 NTP 端口（123/UDP） 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 检查系统时间 date # 对比标准时间（手机/原子钟） # 2. 检查 NTP 状态 chronyc tracking # 或 ntpq -p # 查看 Offset 是否过大（\u0026gt;100ms 甚至几秒） # 3. 检查虚拟机工具 # VMware Tools / VirtIO 是否运行 systemctl status vmtoolsd # 4. 检查网络 telnet ntp.aliyun.com 123 使用命令：\n1 2 3 4 # 强制同步 chronyc -a makestep # 或 ntpdate -u pool.ntp.org 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 立即同步时间 chronyc -a makestep # 注意：如果时间偏差太大（\u0026gt;1000s），可能需要手动调整或重启服务 # 2. 修复 NTP 配置 # /etc/chrony.conf server ntp.aliyun.com iburst server ntp.tencent.com iburst makestep 1.0 3 # 前 3 次更新无论偏差多大都步进 # 3. 启用虚拟机时间同步 # 确保 Host 时间准确，Guest 自动同步 Host # 或在 Guest 中独立运行 NTP # 4. 更换电池 # 物理机更换 CMOS 电池 预防措施：\n多重源：配置至少 3 个不同的 NTP 源。 监控：监控时间偏移量（Offset），超过 50ms 即告警。 容器时间：容器共享宿主机时间，确保宿主机准确。 应用容错：签名验证允许一定的时间窗口（如 ±5 分钟）。 经验总结：\n时间是分布式系统的基石，时间不准，一切逻辑崩塌。 虚拟机时间漂移是常见问题，必须专门处理。 监控时间偏移比监控时间本身更重要。 事故 86：K8s Etcd 数据损坏导致集群瘫痪 事故现象：\nkubectl get pods 无响应或报错 connection refused 所有 API Server 无法工作 Etcd 集群成员状态异常，Leader 选举失败 节点 NotReady，Pod 无法调度 事故原因：\nEtcd 数据目录所在磁盘 IO 延迟过高，导致心跳超时 Etcd 进程 OOM 或被 Kill 数据文件损坏（WAL/Snapshot 损坏） 误操作删除了 Etcd 数据目录 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 检查 Etcd 状态 etcdctl endpoint health etcdctl member list # 2. 查看 Etcd 日志 journalctl -u etcd -f # 搜索 \u0026#34;raft\u0026#34;, \u0026#34;snapshot\u0026#34;, \u0026#34;corrupted\u0026#34; # 3. 检查磁盘 IO iostat -x 1 # 关注 await 和 %util # 4. 检查资源 top -p $(pgrep etcd) 使用命令：\n1 2 # 查看 Etcd 数据库大小 etcdctl --write-out=table endpoint status 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 1. 尝试恢复集群 # 如果是单节点故障，移除坏节点，加入新节点 etcdctl member remove \u0026lt;id\u0026gt; etcdctl member add \u0026lt;new-node\u0026gt; --peer-urls=\u0026lt;url\u0026gt; # 2. 从快照恢复（终极手段） # 找到最近的快照 etcdctl snapshot restore backup.db \\ --data-dir=/var/lib/etcd.restore \\ --name=etcd-node \\ --initial-cluster=... # 3. 替换数据目录 # 停止 Etcd，替换为恢复的数据，重启 # 4. 重建集群 # 如果数据全丢，只能重建 K8s 集群，重新部署应用 预防措施：\nSSD 必备：Etcd 对磁盘 IO 极其敏感，必须使用 SSD。 定期备份：自动化定时备份 Etcd 快照（etcdctl snapshot save）。 资源隔离：Etcd 独占资源，不与业务混部。 监控：监控 Etcd 延迟（db_fsync_duration），超过 10ms 即告警。 经验总结：\nEtcd 是 K8s 的大脑，大脑坏了，肢体全瘫。 磁盘 IO 是 Etcd 的性能瓶颈，必须重视。 备份是唯一的救命稻草，且必须定期演练恢复。 事故 87：Service Mesh (Istio) 配置错误导致全站 503 事故现象：\n微服务间调用全部返回 503 Service Unavailable Sidecar 代理（Envoy）日志显示 No healthy upstream 虚拟服务（VirtualService）路由规则生效异常 回滚 Istio 配置后恢复 事故原因：\nVirtualService 配置了错误的路由规则（如死循环、匹配所有但无目的地） DestinationRule 连接池配置过小，导致连接耗尽 mTLS 策略冲突，证书验证失败 Istio Pilot 推送配置延迟或失败 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 1. 检查 Istio 配置 istioctl analyze # 自动检测配置错误 # 2. 查看 Envoy 配置 istioctl proxy-config routes \u0026lt;pod\u0026gt; istioctl proxy-config clusters \u0026lt;pod\u0026gt; # 3. 查看 Pilot 日志 kubectl logs -n istio-system -l istio=pilot --tail=100 # 4. 测试连通性 kubectl exec -it \u0026lt;pod\u0026gt; -- curl -v http://service-name 使用命令：\n1 2 # 查看 Envoy 访问日志 kubectl logs \u0026lt;pod\u0026gt; -c istio-proxy | grep \u0026#34;503\u0026#34; 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 1. 紧急回滚配置 kubectl apply -f virtual-service-backup.yaml # 2. 旁路 Mesh（逃生） # 临时移除 Sidecar 注入标签，让流量直连 kubectl label namespace default istio-injection- # 3. 修正配置 # 修复路由规则，确保有合法的 Subset 和 Host # 调整连接池参数 # 4. 逐步生效 # 使用 Istio 的 Canary 发布功能，先对少量流量生效 预防措施：\n配置校验：上线前使用 istioctl analyze 严格校验。 灰度发布：Mesh 配置变更必须灰度，严禁全量直接推。 逃生通道：保留一键关闭 Sidecar 或 bypass Mesh 的能力。 监控：监控 Envoy 的 5xx 比例和配置推送版本。 经验总结：\nService Mesh 增加了架构复杂度，配置错误影响面极大。 必须有“一键降级”回传统网络模式的能力。 配置即代码，版本控制和 Review 必不可少。 事故 88：自动扩缩容（HPA）失效导致服务雪崩 事故现象：\n流量洪峰到来，QPS 翻倍 Pod 数量保持不变，未触发扩容 现有 Pod CPU 100%，请求大量超时 手动扩容后迅速恢复 事故原因：\nMetrics Server 故障，无法提供 CPU/Memory 指标 HPA 配置的阈值过高（如 90%），触发滞后 自定义指标（Custom Metrics）采集失败 Pod Resource Request 设置不合理，导致计算出的利用率偏低 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 查看 HPA 状态 kubectl get hpa # 观察 CURRENT 指标是否为 \u0026lt;unknown\u0026gt; # 观察 REASON 列是否有报错 # 2. 检查 Metrics Server kubectl get pods -n kube-system -l k8s-app=metrics-server kubectl logs -n kube-system -l k8s-app=metrics-server # 3. 查看 Pod 资源请求 kubectl get pod \u0026lt;pod\u0026gt; -o jsonpath=\u0026#39;{.spec.containers[].resources}\u0026#39; # 4. 描述 HPA kubectl describe hpa \u0026lt;hpa-name\u0026gt; # 查看 Events，是否有 \u0026#34;FailedGetResourceMetric\u0026#34; 使用命令：\n1 2 # 手动测试指标获取 kubectl top pods 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # 1. 修复 Metrics Server # 重启 Metrics Server # 检查 API Server 聚合层配置 # 2. 紧急手动扩容 kubectl scale deployment app --replicas=20 # 3. 调整 HPA 配置 # 降低目标利用率（如 60%） # 调整缩放速度（scaleDown/stabilizationWindow） spec: metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60 behavior: scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 15 # 4. 修正 Resource Request # 确保 Request 值接近实际平均使用量，避免虚高或虚低 预防措施：\nMetrics 高可用：Metrics Server 自身需要高可用部署。 合理阈值：根据压测结果设置合理的扩缩容阈值和冷却时间。 混合策略：结合 CPU 和 自定义指标（如 QPS、Lag）进行扩缩容。 定期演练：模拟流量洪峰，验证 HPA 反应速度。 经验总结：\n自动扩缩容依赖准确的指标，指标挂了，扩缩容就瞎了。 Request 设置不当会导致 HPA 误判，需定期校准。 扩容速度要快，缩容速度要慢，防止抖动。 事故 89：日志采集器（Filebeat/Fluentd）拖垮生产 事故现象：\n应用服务器 CPU 飙升，Load Average 极高 磁盘 IO 等待（iowait）占满 业务日志写入变慢，甚至阻塞业务线程 日志采集端（ES/Kafka）接收不过来，反压导致采集器内存爆炸 事故原因：\n开启了 DEBUG 级别日志，日志量激增 100 倍 采集器配置不当，未限制读取速率或内存 单行日志过大（如打印了大对象 JSON），导致处理阻塞 下游（ES）写入慢，采集器缓冲队列满，占用大量内存 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 查看资源占用 top -c | grep filebeat du -sh /var/log/app/* # 2. 检查日志文件大小 ls -lh /var/log/app/*.log # 发现单个文件几分钟内增长到 GB 级 # 3. 查看采集器日志 /var/log/filebeat/filebeat # 搜索 \u0026#34;harvester\u0026#34;, \u0026#34;publisher\u0026#34;, \u0026#34;backoff\u0026#34; # 4. 检查下游状态 # ES 集群是否 Red/Yellow？写入延迟是否高？ 使用命令：\n1 2 3 4 5 # 统计日志行数 wc -l /var/log/app/*.log # 查看大行 awk \u0026#39;length($0)\u0026gt;10000\u0026#39; /var/log/app/app.log | head 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # 1. 紧急降级日志级别 # 通过配置中心动态将日志级别改为 ERROR # 或临时注释掉高频打印的代码 # 2. 限制采集器资源 # filebeat.yml processors: - add_host_metadata: ~ output.elasticsearch: bulk_max_size: 2048 # 减小批次 queue_mem_events: 4096 # 限制队列 filebeat.inputs: - type: log close_inactive: 5m clean_inactive: 72h max_bytes: 10MB # 限制单行最大字节 # 3. 清理磁盘 # 轮转或删除过大的日志文件 logrotate -f /etc/logrotate.d/app # 4. 扩容下游 # 增加 ES 节点或 Kafka Partition 预防措施：\n日志规范：生产环境严禁打印 DEBUG 日志和大对象。 资源限制：采集器必须限制 CPU 和内存使用。 异步写入：应用日志应采用异步追加，避免阻塞业务。 采样：对高频日志进行采样打印（如每 100 条打 1 条）。 经验总结：\n日志系统本是辅助，配置不当会变成杀手。 控制日志量是运维的基本功，失控的日志能写满磁盘。 采集器要有背压处理机制，不能无限吃内存。 事故 90：备份数据损坏且无验证（终极灾难复盘） (注：虽然事故 46 也涉及备份损坏，但事故 90 侧重于管理流程缺失和绝望场景下的应对，作为 100 个事故的压轴警示)\n事故现象：\n核心数据库遭遇勒索病毒加密，所有在线数据不可用 决定从冷备份恢复，发现最近 3 个月的磁带/云备份文件全部损坏 报错：Tape read error, Checksum mismatch, File header corrupted 公司面临停业风险，数据恢复希望渺茫 事故原因：\n盲目信任：备份系统常年显示“Success”，但从未进行过恢复演练 介质老化：磁带库机械故障或硬盘静默错误，未被监测 软件 Bug：备份软件版本存在已知 Bug，写入即损坏 流程缺失：没有“定期恢复验证”的强制性流程 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1. 尝试多种恢复手段 # 尝试不同版本的备份软件 # 尝试在不同硬件上读取磁带 # 2. 检查备份日志历史 # 发现过去一年的日志全是 \u0026#34;Backup completed successfully\u0026#34; # 但没有任何 \u0026#34;Verify\u0026#34; 或 \u0026#34;Restore Test\u0026#34; 的记录 # 3. 检查介质健康 smartctl -a /dev/st0 # 发现大量硬件错误 # 4. 追溯根源 # 询问运维团队：上次恢复演练是什么时候？ # 回答：三年前，或者“从来没做过” 使用命令：\n1 2 # 尝试强制读取（死马当活马医） dd if=/dev/st0 of=backup.img bs=64k conv=noerror,sync 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 1. 专业数据恢复 # 联系专业数据恢复公司（费用极高，成功率不确定） # 尝试芯片级恢复或磁信号重组 # 2. 拼凑数据 # 从各种零散来源收集数据： # - 从库的残存数据 # - 开发环境的导出文件 # - 第三方合作伙伴的数据副本 # - 用户本地的缓存数据 # 3. 业务重构 # 如果数据无法恢复，考虑业务重启 # 公开道歉，赔偿用户，重新开始 # 4. 重建备份体系（痛定思痛） # 引入自动化恢复演练系统 # 实施 3-2-1 备份策略 # 定期进行“消防演习” 预防措施：\n铁律：没有经过恢复验证的备份等于没有备份。 自动化演练：每周自动拉起临时环境，恢复最新备份，运行测试用例，成功后销毁。 多重校验：备份时生成 Checksum，恢复前校验 Checksum。 多地异构：使用不同介质（磁盘 + 磁带 + 云）、不同厂商、不同地点的备份。 经验总结：\n这是运维人员的噩梦，也是职业生涯的终点。 备份的成功不在于“写完”，而在于“能读”。 流程和文化比技术更重要：必须建立“怀疑一切，验证一切”的文化。 不要等到灾难发生才后悔没做演练。 事故 91：告警风暴导致“狼来了”事故 事故现象：\n监控平台在 5 分钟内发送 2000+ 条告警短信/电话 运维人员手机被打爆，被迫关机或静音 真正的核心故障（数据库宕机）被淹没在海量无关告警中 故障发现时间延迟 2 小时，造成重大资损 事故原因：\n监控粒度太细：每个 Pod、每个接口、每个实例都独立告警 缺乏告警收敛/抑制机制：一个节点宕机触发上百条关联告警 阈值设置过敏感：CPU 瞬间抖动 1% 也触发 P0 告警 无分级通知机制：所有告警无论轻重都打电话 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 统计告警来源 grep \u0026#34;ALERT\u0026#34; /var/log/alertmanager.log | awk \u0026#39;{print $NF}\u0026#39; | sort | uniq -c | sort -rn | head -20 # 发现 80% 告警来自同一个集群的 NodeReady 检查 # 2. 分析告警关联性 # 发现：NodeDown -\u0026gt; PodUnready -\u0026gt; ServiceEndpointMissing -\u0026gt; RequestTimeout -\u0026gt; 5xxError # 本质上是一个根因，却触发了 5 层告警 # 3. 查看通知渠道负载 # 短信网关返回 \u0026#34;Rate Limit Exceeded\u0026#34; # 电话队列堆积超过 500 通 # 4. 确认核心故障被忽略 # 在 2000 条告警中，只有一条是 \u0026#34;MySQL Master Down\u0026#34;，被挤到第 1583 条 使用命令：\n1 2 3 4 5 6 7 # 模拟告警风暴测试 for i in {1..1000}; do curl -X POST http://alertmanager/api/v1/alerts -d \u0026#34;[{\\\u0026#34;labels\\\u0026#34;:{\\\u0026#34;alertname\\\u0026#34;:\\\u0026#34;Test$i\\\u0026#34;}}]\u0026#34; done # 查看告警分组情况 curl http://alertmanager/api/v1/status 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # 1. 实施告警收敛（Alertmanager 配置） group_wait: 30s # 等待 30 秒收集同一组告警 group_interval: 5m # 5 分钟内相同告警只发一次 repeat_interval: 4h # 4 小时内不重复发送 # 按业务线、集群、严重级别分组 group_by: [\u0026#39;alertname\u0026#39;, \u0026#39;cluster\u0026#39;, \u0026#39;severity\u0026#39;] # 2. 设置抑制规则（Inhibition Rules） # 如果 \u0026#34;ClusterDown\u0026#34; 触发，则抑制该集群下所有 \u0026#34;PodDown\u0026#34;, \u0026#34;ServiceDown\u0026#34; 告警 inhibit_rules: - source_match: severity: \u0026#39;critical\u0026#39; alertname: \u0026#39;ClusterDown\u0026#39; target_match: alertname: \u0026#39;PodDown\u0026#39; equal: [\u0026#39;cluster\u0026#39;] # 3. 告警分级通知 # P0 (Critical): 电话 + 短信 + IM (仅核心指标：DB 宕机、全站不可用) # P1 (Warning): 短信 + IM (单服务异常、延迟高) # P2 (Info): 仅 IM (资源使用率高、非核心错误) # 4. 动态阈值与智能告警 # 引入机器学习算法（如 3-Sigma），识别异常波动而非固定阈值 # 只有持续 N 分钟异常才触发 # 5. 紧急止血 # 暂时关闭非核心告警通道 # 人工置顶核心故障工单 预防措施：\n告警黄金原则：每条告警都必须有对应的行动项（Actionable），否则就是噪音。 定期清洗：每月审查告警规则，删除从未触发或误报率高的规则。 值班制度：设立专门的 On-Call 轮值，避免多人同时接收告警。 故障演练：模拟大规模故障，验证告警收敛效果。 经验总结：\n告警不在多，而在准。一条准确的 P0 告警胜过一万条噪音。 必须建立告警抑制和收敛机制，防止“雪崩式”通知。 运维人员的注意力是最宝贵资源，不能被浪费。 事故 92：备份文件损坏且无验证事故 事故现象：\n生产数据库磁盘崩溃，数据全丢 紧急恢复时，发现最近 7 天的备份文件无法解压（CRC 校验失败） 追溯到 30 天前的备份，数据丢失一个月 公司面临巨额赔偿和信任危机 事故原因：\n备份脚本只负责“执行”，不负责“验证” 磁盘存在静默错误（Bit Rot），备份写入时已损坏 备份文件未做校验和（Checksum） 长期未进行恢复演练，盲目相信备份成功日志 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 尝试恢复 gunzip backup_20240320.sql.gz # 报错：gzip: stdin: invalid compressed data--format violated # 2. 检查备份日志 cat /var/log/backup.log | grep \u0026#34;Success\u0026#34; # 显示每天都是 \u0026#34;Backup completed successfully\u0026#34; # 但脚本只检查了 mysqldump 退出码，没检查文件完整性 # 3. 检查磁盘健康 smartctl -a /dev/sdb | grep -i \u0026#34;reallocated\\|pending\u0026#34; # 发现大量重映射扇区 # 4. 追溯历史备份 for f in /backup/*.gz; do gunzip -t $f \u0026amp;\u0026amp; echo \u0026#34;$f OK\u0026#34; || echo \u0026#34;$f FAILED\u0026#34;; done # 发现过去 30 天备份全部损坏 使用命令：\n1 2 3 4 5 6 # 生成并验证校验和 md5sum backup.sql \u0026gt; backup.sql.md5 md5sum -c backup.sql.md5 # 测试恢复（干跑） mysql --defaults-file=/dev/null --simulate-backup \u0026lt; backup.sql 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # 1. 紧急数据抢救 # 联系专业数据恢复公司尝试修复磁盘镜像 # 从只读从库（如果有）导出数据 # 2. 重建备份体系 # 修改备份脚本，增加校验步骤 mysqldump ... | gzip \u0026gt; backup.sql.gz if [ $? -eq 0 ]; then md5sum backup.sql.gz \u0026gt; backup.sql.gz.md5 # 尝试本地试恢复 gunzip -c backup.sql.gz | mysql --defaults-file=/dev/null -u root -e \u0026#34;SELECT 1\u0026#34; if [ $? -eq 0 ]; then echo \u0026#34;Backup Verified OK\u0026#34; # 上传到异地存储 aws s3 cp backup.sql.gz s3://bucket/backup/ else echo \u0026#34;Backup Verification FAILED!\u0026#34; exit 1 fi fi # 3. 引入多重备份 # 本地磁盘 + 异地对象存储 + 磁带库 # 不同存储介质降低同时损坏概率 # 4. 自动化恢复演练 # 每周日凌晨自动拉起临时实例，恢复最新备份，运行测试用例 # 成功后销毁实例，失败则立即告警 预防措施：\n铁律：没有经过恢复验证的备份 = 没有备份。 校验和：所有备份文件必须生成并保存 Checksum。 定期演练：至少每季度进行一次真实环境的恢复演练。 多地冗余：遵循 3-2-1 备份原则（3 份副本，2 种介质，1 个异地）。 经验总结：\n备份的成功不在于“写完”，而在于“能读”。 自动化验证是备份系统的核心组件，不可或缺。 灾难发生时，唯一能救你的是上周刚演练过的备份。 事故 93：灾备演练引发真实故障事故 事故现象：\n计划内进行“主备切换演练” 切换过程中，主库被意外清空，备库数据同步覆盖主库（空数据） 双向同步导致所有生产数据被擦除 演练变成真实灾难 事故原因：\n演练方案缺陷：未隔离生产流量，未停止双向同步 操作失误：在备库执行了 DROP DATABASE，触发了同步机制 权限过大：演练账号拥有生产库 DROP 权限 缺乏“熔断”机制：发现异常未及时停止同步 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -- 1. 检查数据状态 SHOW TABLES; -- 结果为空 -- 2. 查看 Binlog mysqlbinlog mysql-bin.000XXX | grep -i \u0026#34;DROP\u0026#34; -- 发现来自备库 IP 的 DROP 语句 -- 3. 检查同步状态 SHOW SLAVE STATUS\\G -- 发现同步仍在进行，且正在 replay 删除操作 -- 4. 追溯操作记录 audit_log | grep \u0026#34;DROP\u0026#34; -- 定位到演练人员执行的命令 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 紧急切断同步 STOP SLAVE; RESET SLAVE ALL; # 物理断开网络连接 # 2. 停止所有写入 # 开启全局只读 SET GLOBAL read_only = ON; # 3. 数据恢复 # 从异地冷备份恢复（参考事故 92） # 耗时 12 小时 # 4. 复盘与问责 # 暂停所有自动同步任务 # 审查所有高权限账号 预防措施：\n演练隔离：演练必须在完全隔离的环境或使用影子流量进行。 权限最小化：演练账号严禁拥有 DROP、TRUNCATE 等高危权限。 双人复核：高危操作必须双人确认（Four-eyes principle）。 同步单向化：生产环境严禁随意开启双向同步，除非有严格冲突检测。 预案审批：演练方案必须经过架构师和安全团队严格评审。 经验总结：\n演练是把双刃剑，准备不足的演练比不演练更危险。 生产环境的任何变更（包括演练）都要有“一键回滚”和“紧急熔断”。 敬畏生产环境，永远不要高估人的可靠性。 事故 94：第三方依赖服务故障（供应链风险） 事故现象：\n核心下单功能不可用 内部服务正常，但调用“短信验证码”接口超时 由于代码中未做降级，整个下单流程被阻塞 用户无法注册、无法登录、无法下单 事故原因：\n强依赖第三方服务（短信商），无备用方案 代码中同步调用第三方接口，且无超时限制（默认无限等待） 第三方服务商发生区域性故障 未配置熔断器，拖垮自身线程池 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 1. 链路追踪 # SkyWalking 显示 span 卡在 \u0026#34;SendSMS\u0026#34; 环节，耗时 \u0026gt; 60s # 2. 检查线程池 jstack \u0026lt;PID\u0026gt; | grep -c \u0026#34;WAITING\u0026#34; # 发现 200 个线程全部阻塞在 HTTP 请求上 # 3. 测试第三方接口 curl -v --connect-timeout 5 https://sms-provider.com/api # 连接超时 # 4. 查看代码 grep -A 10 \u0026#34;sendSms\u0026#34; OrderService.java # 发现无 timeout 配置，无 try-catch 降级逻辑 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // 1. 紧急热修复（Hotfix） // 通过配置中心动态开启“降级开关” if (config.isSmsFallbackEnabled()) { // 跳过短信验证，直接返回成功（仅限测试环境或白名单） // 或者切换到备用短信商 return sendSmsViaProviderB(phone, code); } else { return sendSmsViaProviderA(phone, code); } // 2. 代码重构（根本解决） // 添加超时 RequestConfig config = RequestConfig.custom() .setConnectTimeout(3000) .setSocketTimeout(3000) .build(); // 添加熔断 @CircuitBreaker(name = \u0026#34;smsService\u0026#34;, fallbackMethod = \u0026#34;smsFallback\u0026#34;) public void sendSms(...) { ... } public void smsFallback(...) { // 记录日志，发送异步消息稍后重试 // 或者提示用户“稍后重试” } // 3. 多活供应商 // 接入 2-3 家短信服务商，自动故障切换 预防措施：\n依赖治理：梳理所有外部依赖，区分核心与非核心。 超时与熔断：所有外部调用必须设置超时，必须配备熔断降级。 多供应商策略：关键依赖（短信、支付、地图）必须有 Plan B。 异步解耦：非实时依赖（如发短信、发邮件）改为异步消息队列处理。 经验总结：\n你的系统强度取决于最弱的那个第三方依赖。 永远不要信任外部服务，假设它们随时会挂。 降级不是失败，而是为了保住核心业务的生存。 事故 95：API 版本兼容导致客户端大面积崩溃 事故现象：\nAPP 新版本发布后，旧版本用户（占 60%）无法打开首页 后端接口返回字段结构变更，旧客户端解析崩溃（Crash） 应用商店评分骤降至 1 星 事故原因：\n后端接口修改未保持向后兼容（Breaking Change） 移除旧字段未通知前端 缺乏 API 版本管理策略 灰度发布范围过小，未能覆盖旧版本用户 排查过程：\n1 2 3 4 5 6 7 8 9 # 1. 查看崩溃日志 # Firebase/Crashlytics 显示：JsonParseException: Unrecognized field \u0026#34;new_field\u0026#34; # 2. 对比接口文档 # v1.0 接口返回 { \u0026#34;name\u0026#34;: \u0026#34;abc\u0026#34; } # v1.1 接口返回 { \u0026#34;userName\u0026#34;: \u0026#34;abc\u0026#34;, \u0026#34;version\u0026#34;: 2 } (字段名变了) # 3. 检查流量分布 # 发现 60% 请求来自旧版本 APP，全部报错 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 1. 紧急回滚接口 # 恢复旧版接口逻辑，兼容新旧字段 if (clientVersion \u0026lt; 2.0) { response.setName(response.getUserName()); } # 2. 实施 API 版本控制 # URL 路径版本化：/api/v1/users, /api/v2/users # Header 版本化：Accept-Version: 1.0 # 3. 强制升级策略 # 对于严重不兼容版本，通过接口返回强制升级标志 { \u0026#34;code\u0026#34;: 400, \u0026#34;msg\u0026#34;: \u0026#34;Please update app\u0026#34;, \u0026#34;forceUpdate\u0026#34;: true } # 4. 灰度发布 # 先对 5% 旧版本用户开放新接口，观察崩溃率 预防措施：\n兼容性原则：接口只能新增字段，严禁修改/删除旧字段。 版本管理：严格执行 API 版本控制，废弃接口需提前公告并保留过渡期。 契约测试：引入 Pact 等工具，确保前后端契约一致。 监控崩溃率：实时监控客户端崩溃率，异常升高自动回滚。 经验总结：\n后端接口的微小变动，可能导致百万级客户端崩溃。 兼容性是 API 设计的生命线。 永远不要低估旧版本用户的比例。 事故 96：数据库大版本升级失败事故 事故现象：\n计划内 MySQL 5.7 升级 8.0 升级后启动失败，报错字符集不兼容 回滚时发现数据目录已被修改，无法降回 5.7 业务中断 12 小时 事故原因：\n未在测试环境充分验证升级路径 忽略字符集、排序规则（Collation）变更 升级脚本未备份原数据目录 回滚方案未经过实操验证 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 12 # 1. 查看启动错误 tail -f /var/log/mysql/error.log # [ERROR] Table \u0026#39;db.table\u0026#39; uses an unknown collation \u0026#39;utf8mb4_0900_ai_ci\u0026#39; # 2. 检查数据文件 ls -la /var/lib/mysql/ # 发现 ibdata1 已被 8.0 格式修改 # 3. 尝试回滚 yum install mysql-5.7 systemctl start mysqld # 启动失败：InnoDB 版本不匹配 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 1. 紧急恢复 # 从升级前的冷备份恢复数据 # 重新部署 5.7 版本 # 2. 重新规划升级 # 在测试环境完整演练升级 + 回滚流程 # 修复所有兼容性警告（字符集、SQL 语法、保留字） # 3. 采用蓝绿部署 # 搭建一套新的 8.0 集群 # 通过 DTS/CDC 同步数据 # 验证无误后切换流量 # 保留旧集群作为回滚底牌 预防措施：\n充分测试：升级前必须在仿真环境进行全量回归测试。 备份先行：升级前必须进行全量备份，并验证可恢复。 蓝绿策略：核心数据库升级严禁原地升级，采用双集群切换。 回滚演练：必须验证“降级”路径是否可行。 经验总结：\n数据库升级是高风险操作，能不动就不动。 原地升级是运维的大忌，蓝绿切换才是正道。 回滚方案必须像升级方案一样被重视。 事故 97：网络分区（Split-Brain）导致数据不一致 事故现象：\n机房 A 与机房 B 之间光纤中断 两边集群都认为自己是 Master，继续接受写入 网络恢复后，数据严重冲突，部分数据丢失 事故原因：\n集群仲裁机制配置错误（允许少数派成为 Master） 未配置脑裂保护（Fencing） 应用层未处理写冲突 排查过程：\n1 2 3 4 5 6 7 # 1. 检查集群状态 # 机房 A: Role=Master, Quorum=Yes (误判) # 机房 B: Role=Master, Quorum=Yes (误判) # 2. 比对数据 # 发现同一 ID 的记录在两边有不同的值 # 时间戳显示在网络中断期间双方都有写入 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 # 1. 紧急停写 # 手动将其中一个机房设为只读 SET GLOBAL read_only = ON; # 2. 数据合并 # 以时间戳最新为准（可能丢失部分数据） # 或人工介入逐条核对 # 3. 修复配置 # 配置法定人数（Quorum）：N/2 + 1 # 启用 STONITH (Shoot The Other Node In The Head) 机制 # 当检测到分区时，强制 fencing 掉另一方（断电/断网） 预防措施：\n仲裁机制：严格配置 Quorum，确保只有多数派能当选 Master。 Fencing 机制：硬件或软件层面的强制隔离，防止双写。 应用层冲突检测：使用版本号或向量时钟（Vector Clock）检测冲突。 避免双活写：除非必要，否则采用主从模式，禁止双 Master 写入。 经验总结：\n网络分区是分布式系统的常态，必须按“必然发生”来设计。 CAP 定理中，涉及金钱数据通常选择 CP（一致性），牺牲可用性。 脑裂的代价远高于短暂的服务不可用。 事故 98：资源配额（Quota）耗尽导致新业务无法上线 事故现象：\n新微服务部署失败，Pod 一直 Pending 报错：Failed to create pod: exceeded quota 现有业务不受影响，但无法扩容 紧急会议发现 Namespace 配额已满 事故原因：\nK8s Namespace 配额（CPU/Memory/Pod 数）设置过小 历史遗留僵尸 Pod 占用配额 缺乏配额监控和预警 资源申请未规划，随意创建 排查过程：\n1 2 3 4 5 6 7 8 9 10 11 # 1. 查看配额状态 kubectl describe quota -n production # 输出：Used: 99%, Hard: 100% # 2. 查找占用资源大户 kubectl top pods -n production --sort-by=cpu # 发现几个测试 Pod 长期运行，占用大量资源 # 3. 检查僵尸资源 kubectl get pods -n production | grep \u0026#34;Completed\\|Error\u0026#34; # 发现 50 个已完成但未删除的 Pod 解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 1. 清理僵尸资源 kubectl delete pods --field-selector=status.phase==Succeeded -n production kubectl delete jobs --field-selector=status.successful=1 -n production # 2. 临时扩容配额 kubectl patch quota compute-resources -n production -p \u0026#39;{\u0026#34;spec\u0026#34;:{\u0026#34;hard\u0026#34;:{\u0026#34;cpu\u0026#34;:\u0026#34;200\u0026#34;, \u0026#34;memory\u0026#34;:\u0026#34;400Gi\u0026#34;}}}\u0026#39; # 3. 优化资源申请 # 调整 Requests/Limits，避免过度预留 resources: requests: cpu: \u0026#34;100m\u0026#34; memory: \u0026#34;128Mi\u0026#34; limits: cpu: \u0026#34;500m\u0026#34; memory: \u0026#34;512Mi\u0026#34; # 4. 建立配额管理流程 # 新业务上线需评估配额 # 定期审计资源使用率 预防措施：\n配额监控：监控配额使用率，\u0026gt;80% 即告警。 定期清理：自动化脚本定期清理 Completed/Error 状态的 Pod。 资源规划：根据实际负载动态调整 Requests/Limits。 多 Namespace 隔离：按业务线划分 Namespace，避免相互影响。 经验总结：\n配额是保护集群的盾牌，但也可能成为业务的枷锁。 僵尸资源是配额的隐形杀手。 资源管理需要精细化运营。 事故 99：级联故障（雪崩）终极复盘 事故现象：\n某边缘服务（如“头像上传”）响应变慢 导致调用它的“用户中心”线程阻塞 进而导致“订单服务”获取用户信息超时 最终导致“网关”所有连接耗尽，全站不可用 历时 4 小时才恢复 原因链条：\n诱因：对象存储（OSS）抖动，头像上传慢。 传播：用户中心未设置超时，线程池满。 放大：订单服务重试机制，流量翻倍。 爆发：网关连接池耗尽，拒绝所有请求。 失效：熔断器未配置或阈值过高，未起作用。 深度解决方案：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # 1. 全链路超时设置 # 每一层调用都必须有超时，且上层 \u0026lt; 下层 Gateway Timeout: 3s Order Service Timeout: 2s User Service Timeout: 1s OSS Timeout: 0.5s # 2. 舱壁隔离 # 为每个依赖分配独立线程池 # 头像上传失败不影响下单核心流程 # 3. 智能熔断 # 错误率 \u0026gt; 50% 或 响应时间 \u0026gt; 1s 自动熔断 # 熔断后直接返回默认值（如默认头像） # 4. 限流降级 # 网关层针对非核心接口限流 # 保护核心交易链路 # 5. 异步化 # 头像上传改为异步，不阻塞主流程 经验总结：\n雪崩始于微末，终于全局。 系统的韧性取决于最弱一环的防护能力。 超时、熔断、限流、降级是分布式系统的“四大金刚”，缺一不可。 定期进行混沌工程（Chaos Engineering）演练，主动注入故障。 事故 100：黑天鹅事件（未知故障） 事故现象：\n无任何预兆，系统突然全面瘫痪 监控全绿（因为监控也挂了）或全是乱码 日志无法写入，SSH 无法连接 所有已知排查手段失效 可能原因：\n底层虚拟化平台崩溃（云厂商底层故障） 机房整体断电/火灾/水灾 遭受国家级网络攻击（APT） 宇宙射线导致内存位翻转（极罕见但存在） 应对策略（非技术层面）：\n启动最高级别应急响应（War Room） 召集所有核心技术骨干 建立专用沟通频道（避开故障系统） 指定唯一指挥官（Commander） 信息收集与通报 联系云厂商/IDC 运营商确认基础设施状态 每 15 分钟向管理层和用户通报进度（即使暂无进展） 尝试带外管理（Out-of-Band） 通过 IPMI/iDRAC 物理重启服务器 切换至备用数据中心/云厂商 业务连续性计划（BCP） 启用手工操作流程（如线下记账） 挂出“维护中”公告，引导用户预期 事后深度复盘 不追究个人责任，聚焦流程改进 更新应急预案，填补盲区 经验总结：\n面对黑天鹅，技术往往无能为力，依靠的是流程、组织和预案。 透明的沟通比快速修复更能赢得信任。 永远保持谦卑，系统越复杂，未知风险越大。 韧性架构的目标不是永不故障，而是故障后能快速恢复。 ","permalink":"https://ktzxy.top/posts/e2e8bsjyo3/","summary":"总结运维生产事故中的问题","title":"《生产事故排障手册》"},{"content":"目录 一、字符串函数 二、日期函数 三、数学函数 四、聚合函数 五、类型转换函数 六、条件函数 七、NULL相关函数 八、分组与窗口函数 九、JSON处理函数 十、空间/地理函数 十一、错误处理与异常 十二、序列与自增 十三、批量插入与合并 十四、其它常用系统函数 十五、典型SQL语法差异补充 十六、特殊函数与高级用法补充 十七、三大数据库特有函数及特殊场景用法 十八、数据类型对照表 十九、迁移注意事项与常见问题 二十、性能优化建议 二十一、DDL/DML迁移脚本模板 二十二、迁移后测试与验证建议 版本兼容性说明：\nOracle 11g+ 支持大部分窗口函数，12c+支持FETCH分页、IDENTITY自增等。 SQL Server 2012+ 支持窗口函数、OFFSET分页、TRY_CAST等。 MySQL 8.0+ 支持窗口函数、正则、JSON_TABLE等，5.7+支持部分JSON函数。 如无特殊说明，示例均以主流新版本为准。 一、字符串函数‌ 函数功能 Oracle SQL Server MySQL 参数说明 版本要求 字符串连接 CONCAT(str1, str2) CONCAT(str1, str2) CONCAT(str1, str2, ...) Oracle最多2个参数，SQL Server/MySQL支持多个参数拼接 11g+ 子串截取 SUBSTR(str,start,len) SUBSTRING(str,start,len) SUBSTRING(str, start, len) 起始位置从1开始，len为截取长度 11g+ 字符串长度 LENGTH(str) LEN(str) LENGTH(str) 返回字符数（中文按1计算） 11g+ 去除首尾空格 TRIM(str) LTRIM(RTRIM(str)) TRIM(str) Oracle支持单边TRIM(LEADING/TRAILING)，MySQL同理 11g+ 大小写转换 LOWER(str) / UPPER(str) 同左 同左 全数据库通用 11g+ SQL示例对照 1. 字符串函数 Oracle：\n1 SELECT CONCAT(\u0026#39;A\u0026#39;, \u0026#39;B\u0026#39;), SUBSTR(\u0026#39;Hello\u0026#39;, 2, 3), LENGTH(\u0026#39;中国\u0026#39;), TRIM(\u0026#39; abc \u0026#39;), LOWER(\u0026#39;ABC\u0026#39;) FROM dual; SQL Server：\n1 SELECT CONCAT(\u0026#39;A\u0026#39;, \u0026#39;B\u0026#39;), SUBSTRING(\u0026#39;Hello\u0026#39;, 2, 3), LEN(\u0026#39;中国\u0026#39;), LTRIM(RTRIM(\u0026#39; abc \u0026#39;)), LOWER(\u0026#39;ABC\u0026#39;); MySQL：\n1 SELECT CONCAT(\u0026#39;A\u0026#39;, \u0026#39;B\u0026#39;), SUBSTRING(\u0026#39;Hello\u0026#39;, 2, 3), LENGTH(\u0026#39;中国\u0026#39;), TRIM(\u0026#39; abc \u0026#39;), LOWER(\u0026#39;ABC\u0026#39;); 二、日期函数‌ 函数功能 Oracle SQL Server MySQL 参数说明 版本要求 当前日期时间 SYSDATE GETDATE() NOW() 无参数，返回服务器当前时间 11g+ 日期格式化 TO_CHAR(date,'YYYY-MM-DD') CONVERT(VARCHAR, date, 120) DATE_FORMAT(date, '%Y-%m-%d') Oracle格式代码自由，SQL Server用风格代码，MySQL用格式字符串 11g+ 日期加减 date + N DATEADD(unit, N, date) DATE_ADD(date, INTERVAL N unit) Oracle直接加减天数，SQL Server/MySQL需指定单位 11g+ 提取日期部分 EXTRACT(YEAR FROM date) DATEPART(YEAR, date) YEAR(date) 支持year/month/day/hour等时间单位 11g+ SQL示例对照 2. 日期函数 Oracle：\n1 SELECT SYSDATE, TO_CHAR(SYSDATE, \u0026#39;YYYY-MM-DD\u0026#39;), SYSDATE + 1, EXTRACT(YEAR FROM SYSDATE) FROM dual; SQL Server：\n1 SELECT GETDATE(), CONVERT(VARCHAR, GETDATE(), 120), DATEADD(day, 1, GETDATE()), DATEPART(YEAR, GETDATE()); MySQL：\n1 SELECT NOW(), DATE_FORMAT(NOW(), \u0026#39;%Y-%m-%d\u0026#39;), DATE_ADD(NOW(), INTERVAL 1 DAY), YEAR(NOW()); 三、数学函数‌ 函数功能 Oracle SQL Server MySQL 参数说明 版本要求 绝对值 ABS(num) 同左 同左 全数据库通用 11g+ 向上取整 CEIL(num) CEILING(num) CEIL(num) 返回大于等于参数的最小整数 11g+ 向下取整 FLOOR(num) 同左 同左 返回小于等于参数的最大整数 11g+ 四舍五入 ROUND(num,精度) 同左 同左 参数1为数值，参数2为保留小数位数 11g+ SQL示例对照 3. 数学函数 Oracle：\n1 SELECT ABS(-5), CEIL(2.3), FLOOR(2.7), ROUND(3.1415, 2) FROM dual; SQL Server：\n1 SELECT ABS(-5), CEILING(2.3), FLOOR(2.7), ROUND(3.1415, 2); MySQL：\n1 SELECT ABS(-5), CEIL(2.3), FLOOR(2.7), ROUND(3.1415, 2); ‌四、聚合函数‌ ‌函数功能‌ ‌Oracle‌ ‌SQL Server‌ ‌参数说明‌ 版本要求 字符串聚合 LISTAGG(str,分隔符) WITHIN GROUP(...) STRING_AGG(str,分隔符) WITHIN GROUP(...) Oracle支持ON OVERFLOW TRUNCATE截断，SQL Server需手动处理超长‌13 11g+ 空值替换 NVL(expr1, expr2) ISNULL(expr1, expr2) 当expr1为NULL时返回expr2‌24 11g+ SQL示例对照 4. 聚合函数 Oracle：\n1 2 SELECT LISTAGG(ename, \u0026#39;,\u0026#39;) WITHIN GROUP (ORDER BY ename) AS names FROM emp; SELECT NVL(comm, 0) FROM emp; SQL Server：\n1 2 SELECT STRING_AGG(ename, \u0026#39;,\u0026#39;) WITHIN GROUP (ORDER BY ename) AS names FROM emp; SELECT ISNULL(comm, 0) FROM emp; MySQL：\n1 2 SELECT GROUP_CONCAT(ename ORDER BY ename SEPARATOR \u0026#39;,\u0026#39;) AS names FROM emp; SELECT IFNULL(comm, 0) FROM emp; ‌五、类型转换函数‌ ‌函数功能‌ ‌Oracle‌ ‌SQL Server‌ ‌参数说明‌ 版本要求 类型转换 TO_NUMBER(str) CAST(str AS NUMERIC) / CONVERT(NUMERIC, str) SQL Server推荐使用TRY_CAST避免转换失败‌45 11g+ 日期转字符串 TO_CHAR(date,格式) CONVERT(VARCHAR, date, 格式代码) Oracle格式如'YYYY-MM-DD'，SQL Server用数字代码如112‌25 11g+ SQL示例对照 5. 类型转换函数 Oracle：\n1 SELECT TO_NUMBER(\u0026#39;123\u0026#39;), TO_CHAR(SYSDATE, \u0026#39;YYYY-MM-DD\u0026#39;) FROM dual; SQL Server：\n1 SELECT CAST(\u0026#39;123\u0026#39; AS NUMERIC), CONVERT(VARCHAR, GETDATE(), 112); MySQL：\n1 SELECT CAST(\u0026#39;123\u0026#39; AS DECIMAL), DATE_FORMAT(NOW(), \u0026#39;%Y-%m-%d\u0026#39;); ‌六、条件函数‌ ‌函数功能‌ ‌Oracle‌ ‌SQL Server‌ ‌参数说明‌ 版本要求 条件判断 DECODE(expr, val1, res1, default) CASE WHEN expr = val1 THEN res1 ELSE default END Oracle特有DECODE，SQL Server用标准CASE‌24 11g+ 空值处理 NVL2(expr, res1, res2) COALESCE(expr, res1, res2) Oracle根据expr是否为NULL返回不同结果，SQL Server用多参数合并‌46 11g+ SQL示例对照 6. 条件函数 Oracle：\n1 2 SELECT DECODE(sex, \u0026#39;M\u0026#39;, \u0026#39;男\u0026#39;, \u0026#39;F\u0026#39;, \u0026#39;女\u0026#39;, \u0026#39;未知\u0026#39;) FROM person; SELECT NVL2(comm, \u0026#39;有提成\u0026#39;, \u0026#39;无提成\u0026#39;) FROM emp; SQL Server：\n1 2 SELECT CASE WHEN sex = \u0026#39;M\u0026#39; THEN \u0026#39;男\u0026#39; WHEN sex = \u0026#39;F\u0026#39; THEN \u0026#39;女\u0026#39; ELSE \u0026#39;未知\u0026#39; END FROM person; SELECT COALESCE(comm, \u0026#39;无提成\u0026#39;) FROM emp; MySQL：\n1 2 SELECT IF(sex = \u0026#39;M\u0026#39;, \u0026#39;男\u0026#39;, IF(sex = \u0026#39;F\u0026#39;, \u0026#39;女\u0026#39;, \u0026#39;未知\u0026#39;)) FROM person; SELECT IFNULL(comm, \u0026#39;无提成\u0026#39;) FROM emp; ‌七、NULL相关函数‌ ‌函数功能‌ ‌Oracle‌ ‌SQL Server‌ ‌参数说明‌ 版本要求 条件判断 NVL(expr1, expr2) ISNULL(expr1, expr2) 当expr1为NULL时返回expr2‌24 11g+ 空值处理 COALESCE(expr, res1, res2) COALESCE(expr, res1, res2) 合并多个参数，返回第一个非NULL值‌46 11g+ SQL示例对照 7. NULL相关函数 Oracle：\n1 SELECT NVL(comm, 0), COALESCE(comm, bonus, 0), CASE WHEN comm IS NULL THEN 1 ELSE 0 END FROM emp; SQL Server：\n1 SELECT ISNULL(comm, 0), COALESCE(comm, bonus, 0), CASE WHEN comm IS NULL THEN 1 ELSE 0 END FROM emp; MySQL：\n1 SELECT IFNULL(comm, 0), IFNULL(bonus, 0), IF(comm IS NULL, 1, 0) FROM emp; 八、分组与窗口函数 函数功能 Oracle SQL Server MySQL 参数说明 版本要求 行号 ROWNUM / ROW_NUMBER() OVER(...) ROW_NUMBER() OVER(...) ROW_NUMBER() OVER(...) 返回分组内行号 11g+ 累计/排名 RANK() OVER(...) / DENSE_RANK() OVER(...) 同左 同左 分组排名 11g+ 分组求和 SUM(col) OVER(PARTITION BY ...) 同左 同左 分组累计 11g+ 移动平均 AVG(col) OVER(ORDER BY ... ROWS BETWEEN N PRECEDING AND CURRENT ROW) 同左 同左 计算滑动窗口平均值 11g+ 首/末值 FIRST_VALUE(col) OVER(...) / LAST_VALUE(col) OVER(...) 同左 同左 获取分组内首/末值 11g+ 行间差值 LAG(col, N, default) OVER(...) / LEAD(col, N, default) OVER(...) 同左 同左 获取前/后N行的值 11g+ SQL示例对照 8. 分组与窗口函数 Oracle：\n1 2 SELECT ename, ROW_NUMBER() OVER(ORDER BY sal DESC) AS rn FROM emp; SELECT deptno, SUM(sal) OVER(PARTITION BY deptno) AS total_sal FROM emp; SQL Server：\n1 2 SELECT ename, ROW_NUMBER() OVER(ORDER BY sal DESC) AS rn FROM emp; SELECT deptno, SUM(sal) OVER(PARTITION BY deptno) AS total_sal FROM emp; MySQL：\n1 2 SELECT ename, ROW_NUMBER() OVER(ORDER BY sal DESC) AS rn FROM emp; SELECT deptno, SUM(sal) OVER(PARTITION BY deptno) AS total_sal FROM emp; 九、JSON处理函数 函数功能 Oracle SQL Server MySQL 参数说明 版本要求 解析JSON JSON_VALUE(json_col, '$.key') JSON_VALUE(json_col, '$.key') JSON_EXTRACT(json_col, '$.key') 提取JSON字段值 11g+ JSON对象转表 JSON_TABLE(json_col, '$.items[*]' COLUMNS(...)) OPENJSON(json_col) JSON_TABLE(json_col, '$') 拆分JSON数组为多行 11g+ 判断JSON有效 IS JSON ISJSON(json_col) ISJSON(json_col) 判断字符串是否为合法JSON 11g+ SQL示例对照 9. JSON处理函数 Oracle：\n1 SELECT JSON_VALUE(\u0026#39;{\u0026#34;a\u0026#34;:1}\u0026#39;, \u0026#39;$.a\u0026#39;) FROM dual; SQL Server：\n1 2 SELECT JSON_VALUE(\u0026#39;{\u0026#34;a\u0026#34;:1}\u0026#39;, \u0026#39;$.a\u0026#39;); SELECT * FROM OPENJSON(\u0026#39;[{\u0026#34;id\u0026#34;:1},{\u0026#34;id\u0026#34;:2}]\u0026#39;); MySQL：\n1 2 SELECT JSON_EXTRACT(\u0026#39;{\u0026#34;a\u0026#34;:1}\u0026#39;, \u0026#39;$.a\u0026#39;); SELECT * FROM JSON_TABLE(\u0026#39;[{\u0026#34;id\u0026#34;:1},{\u0026#34;id\u0026#34;:2}]\u0026#39;, \u0026#39;$\u0026#39;); 十、空间/地理函数 函数功能 Oracle SQL Server MySQL 参数说明 版本要求 创建点 SDO_GEOMETRY(...) geometry::STPointFromText(...) ST_GeomFromText(...) 创建空间点对象 11g+ 距离计算 SDO_GEOM.SDO_DISTANCE(...) geometry::STDistance(...) ST_Distance(...) 计算空间距离 11g+ 空间相交 SDO_RELATE(...) geometry::STIntersects(...) ST_Intersects(...) 判断空间对象是否相交 11g+ SQL示例对照 10. 空间/地理函数 Oracle：\n1 SELECT SDO_GEOMETRY(2001, 4326, SDO_POINT_TYPE(116.4, 39.9, NULL), NULL, NULL) FROM dual; SQL Server：\n1 SELECT geometry::STPointFromText(\u0026#39;POINT(116.4 39.9)\u0026#39;, 4326); MySQL：\n1 SELECT ST_GeomFromText(\u0026#39;POINT(116.4 39.9)\u0026#39;); 十一、错误处理与异常 函数功能 Oracle SQL Server MySQL 参数说明 版本要求 异常捕获 BEGIN ... EXCEPTION WHEN ... THEN ... END; BEGIN TRY ... END TRY BEGIN CATCH ... END CATCH BEGIN ... END TRY BEGIN CATCH ... END CATCH 均支持块级异常处理 11g+ 抛出异常 RAISE_APPLICATION_ERROR(-20001, 'msg') THROW 50001, 'msg', 1 THROW 50001, 'msg', 1 自定义错误抛出 11g+ SQL示例对照 11. 错误处理与异常 Oracle：\n1 2 3 4 5 6 BEGIN -- 代码 EXCEPTION WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE(\u0026#39;出错\u0026#39;); END; SQL Server：\n1 2 3 4 5 6 BEGIN TRY -- 代码 END TRY BEGIN CATCH PRINT \u0026#39;出错\u0026#39;; END CATCH; MySQL：\n1 2 3 4 5 6 BEGIN -- 代码 END TRY BEGIN CATCH SELECT \u0026#39;出错\u0026#39;; END CATCH; 十二、序列与自增 函数功能 Oracle SQL Server MySQL 参数说明 版本要求 创建序列 CREATE SEQUENCE seq_name ... CREATE SEQUENCE seq_name ... CREATE SEQUENCE seq_name ... 语法类似，参数略有差异 12c+ 获取下值 seq_name.NEXTVAL NEXT VALUE FOR seq_name LAST_INSERT_ID() 获取序列下一个值 12c+ 自增主键 GENERATED AS IDENTITY（12c+）/NUMBER GENERATED BY DEFAULT IDENTITY(1,1) AUTO_INCREMENT 字段属性定义自增 12c+ SQL示例对照 12. 序列与自增 Oracle：\n1 2 3 CREATE SEQUENCE seq_test; SELECT seq_test.NEXTVAL FROM dual; CREATE TABLE t1(id NUMBER GENERATED BY DEFAULT AS IDENTITY, name VARCHAR2(20)); SQL Server：\n1 2 3 CREATE SEQUENCE seq_test; SELECT NEXT VALUE FOR seq_test; CREATE TABLE t1(id INT IDENTITY(1,1), name VARCHAR(20)); MySQL：\n1 2 3 CREATE SEQUENCE seq_test; SELECT LAST_INSERT_ID(); CREATE TABLE t1(id INT AUTO_INCREMENT, name VARCHAR(20)); 十三、批量插入与合并 函数功能 Oracle SQL Server MySQL 参数说明 版本要求 批量插入 INSERT ALL ... / INSERT INTO ... SELECT ... INSERT INTO ... SELECT ... INSERT INTO ... SELECT ... 批量插入多行数据 11g+ 合并（UPSERT） MERGE INTO ... USING ... ON ... WHEN MATCHED THEN ... 同左 MERGE INTO ... ON ... WHEN MATCHED THEN ... 两者均支持标准MERGE语法 11g+ SQL示例对照 13. 批量插入与合并 Oracle：\n1 2 3 4 5 6 7 INSERT ALL INTO t1 VALUES(1, \u0026#39;A\u0026#39;) INTO t1 VALUES(2, \u0026#39;B\u0026#39;) SELECT * FROM dual; MERGE INTO t1 USING t2 ON (t1.id = t2.id) WHEN MATCHED THEN UPDATE SET t1.name = t2.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (t2.id, t2.name); SQL Server：\n1 2 3 4 INSERT INTO t1 (id, name) SELECT 1, \u0026#39;A\u0026#39; UNION ALL SELECT 2, \u0026#39;B\u0026#39;; MERGE t1 USING t2 ON t1.id = t2.id WHEN MATCHED THEN UPDATE SET t1.name = t2.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (t2.id, t2.name); MySQL：\n1 2 3 4 INSERT INTO t1 (id, name) VALUES (1, \u0026#39;A\u0026#39;), (2, \u0026#39;B\u0026#39;); MERGE t1 USING t2 ON t1.id = t2.id WHEN MATCHED THEN UPDATE SET t1.name = t2.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (t2.id, t2.name); 十四、其它常用系统函数 函数功能 Oracle SQL Server MySQL 参数说明 版本要求 获取服务器时间 SYSDATE / SYSTIMESTAMP GETDATE() / SYSDATETIME() NOW() 精度略有差异 11g+ 获取当前用户 USER / SYS_CONTEXT('USERENV','SESSION_USER') CURRENT_USER / SUSER_SNAME() CURRENT_USER() 获取当前登录用户 11g+ 获取主机名 SYS_CONTEXT('USERENV','HOST') HOST_NAME() @@SERVERNAME 获取主机名 11g+ SQL示例对照 14. 其它常用系统函数 Oracle：\n1 SELECT SYSDATE, USER, SYS_CONTEXT(\u0026#39;USERENV\u0026#39;,\u0026#39;HOST\u0026#39;) FROM dual; SQL Server：\n1 SELECT GETDATE(), CURRENT_USER, HOST_NAME(); MySQL：\n1 SELECT NOW(), CURRENT_USER, @@SERVERNAME; 十五、典型SQL语法差异补充 LIMIT/OFFSET分页： Oracle 12c+：SELECT ... OFFSET N ROWS FETCH NEXT M ROWS ONLY SQL Server 2012+：同上，老版本用ROW_NUMBER()子查询分页 | 11g+ | dual表： Oracle：SELECT 1 FROM dual SQL Server：SELECT 1（无需dual） | 11g+ | 字符串转义： Oracle：单引号用'' SQL Server：同上，但部分场景支持N'...'表示Unicode | 11g+ | 布尔类型： Oracle无布尔类型，常用NUMBER(1)或CHAR(1) SQL Server有BIT类型 | 11g+ | 注释： 单行：-- 多行：/* ... */ 两者通用 | 11g+ | 十六、特殊函数与高级用法补充 函数功能 Oracle SQL Server MySQL 参数说明 版本要求 分组统计 GROUP BY ROLLUP/CUBE GROUP BY ROLLUP/CUBE GROUP BY WITH ROLLUP 多维分组统计 11g+ 字符查找 INSTR(str, substr) CHARINDEX(substr, str) INSTR(str, substr) 返回子串位置 11g+ 正则匹配 REGEXP_LIKE(str, pattern) LIKE/PATINDEX REGEXP_LIKE(str, pattern)/REGEXP MySQL 8+支持标准正则 11g+ 批量更新 MERGE/UPDATE ... WHERE ... MERGE/UPDATE ... FROM ... UPDATE ... JOIN ... 多表批量更新 11g+ 加密解密 DBMS_CRYPTO ENCRYPTBYPASSPHRASE等 AES_ENCRYPT/AES_DECRYPT 内置加密函数 11g+ XML处理 XMLTYPE/EXTRACTVALUE FOR XML PATH ExtractValue/XPath 结构化数据处理 11g+ JSON处理 JSON_VALUE/JSON_TABLE JSON_VALUE/OPENJSON JSON_EXTRACT/JSON_TABLE 结构化数据处理 11g+ 全文检索 CONTAINS CONTAINS MATCH ... AGAINST 需建全文索引 11g+ 随机数 DBMS_RANDOM.VALUE RAND() RAND() 生成0-1随机数 11g+ UUID SYS_GUID() NEWID() UUID() 生成唯一标识符 11g+ 特殊函数SQL示例 分组统计：\nOracle：\n1 SELECT deptno, SUM(sal) FROM emp GROUP BY ROLLUP(deptno); SQL Server：\n1 SELECT deptno, SUM(sal) FROM emp GROUP BY ROLLUP(deptno); MySQL：\n1 SELECT deptno, SUM(sal) FROM emp GROUP BY deptno WITH ROLLUP; 正则匹配：\nOracle：\n1 SELECT * FROM t WHERE REGEXP_LIKE(name, \u0026#39;^A.*\u0026#39;); SQL Server：\n1 SELECT * FROM t WHERE name LIKE \u0026#39;A%\u0026#39;; MySQL：\n1 SELECT * FROM t WHERE name REGEXP \u0026#39;^A.*\u0026#39;; 批量更新：\nOracle：\n1 MERGE INTO t1 USING t2 ON (t1.id = t2.id) WHEN MATCHED THEN UPDATE SET t1.name = t2.name; SQL Server：\n1 UPDATE t1 SET t1.name = t2.name FROM t1 JOIN t2 ON t1.id = t2.id; MySQL：\n1 UPDATE t1 JOIN t2 ON t1.id = t2.id SET t1.name = t2.name; 加密解密：\nOracle：\n1 SELECT RAWTOHEX(DBMS_CRYPTO.ENCRYPT(UTL_I18N.STRING_TO_RAW(\u0026#39;abc\u0026#39;,\u0026#39;AL32UTF8\u0026#39;), 4353, UTL_I18N.STRING_TO_RAW(\u0026#39;key\u0026#39;,\u0026#39;AL32UTF8\u0026#39;)) ) FROM dual; SQL Server：\n1 SELECT ENCRYPTBYPASSPHRASE(\u0026#39;key\u0026#39;, \u0026#39;abc\u0026#39;); MySQL：\n1 SELECT AES_ENCRYPT(\u0026#39;abc\u0026#39;, \u0026#39;key\u0026#39;); UUID：\nOracle：\n1 SELECT SYS_GUID() FROM dual; SQL Server：\n1 SELECT NEWID(); MySQL：\n1 SELECT UUID(); 十七、三大数据库特有函数及特殊场景用法 场景/功能 Oracle特有 SQL Server特有 MySQL特有 说明 版本要求 分析函数扩展 RATIO_TO_REPORT(expr) OVER(...) NTILE(n) OVER(...) GROUP_CONCAT(expr) Oracle支持分布占比，SQL Server分桶，MySQL字符串聚合 11g+ 层次查询 CONNECT BY PRIOR CTE递归（WITH ... AS ...） WITH RECURSIVE ... 层级树结构遍历 11g+ 行转列 PIVOT/UNPIVOT PIVOT/UNPIVOT GROUP_CONCAT+CASE WHEN 交叉报表、动态列 11g+ 伪列 ROWNUM/ROWID/LEVEL %%physloc%%/$IDENTITY ROW_NUMBER() 伪列/物理定位 11g+ 序列号 SYS_GUID() NEWSEQUENTIALID() UUID_SHORT() 唯一标识生成 12c+ XML处理 XMLTYPE/EXTRACTVALUE FOR XML PATH ExtractValue/XPath 结构化数据处理 11g+ 分区表管理 DBMS_PART SWITCH PARTITION PARTITION BY 分区表相关管理 11g+ 空间分析 SDO_GEOM geometry::STBuffer() ST_Buffer() GIS空间分析 11g+ 计划/执行分析 EXPLAIN PLAN FOR ... SET SHOWPLAN_ALL ON EXPLAIN SQL执行计划 11g+ 触发器扩展 AFTER EACH ROW INSTEAD OF BEFORE/AFTER 触发器细粒度控制 11g+ 其他 DBMS_OUTPUT.PUT_LINE PRINT SELECT '...' 控制台输出 11g+ 特有函数与特殊场景SQL示例 Oracle：\n分布占比：\n1 SELECT deptno, sal, RATIO_TO_REPORT(sal) OVER(PARTITION BY deptno) AS sal_ratio FROM emp; 层次查询：\n1 SELECT empno, ename, mgr FROM emp CONNECT BY PRIOR empno = mgr START WITH mgr IS NULL; 伪列：\n1 2 SELECT ROWNUM, emp.* FROM emp; SELECT LEVEL FROM DUAL CONNECT BY LEVEL \u0026lt;= 5; 控制台输出：\n1 BEGIN DBMS_OUTPUT.PUT_LINE(\u0026#39;Hello Oracle!\u0026#39;); END; SQL Server：\n分桶：\n1 SELECT Name, Salary, NTILE(4) OVER(ORDER BY Salary) AS Quartile FROM Employees; 递归CTE：\n1 2 3 4 5 WITH OrgChart AS ( SELECT empid, mgrid FROM emp WHERE mgrid IS NULL UNION ALL SELECT e.empid, e.mgrid FROM emp e JOIN OrgChart o ON e.mgrid = o.empid ) SELECT * FROM OrgChart; 新顺序GUID：\n1 SELECT NEWSEQUENTIALID(); 控制台输出：\n1 PRINT \u0026#39;Hello SQL Server!\u0026#39;; MySQL：\n字符串聚合：\n1 SELECT GROUP_CONCAT(name ORDER BY id) FROM users; 递归查询：\n1 2 3 4 5 WITH RECURSIVE cte AS ( SELECT id, parent_id FROM tree WHERE parent_id IS NULL UNION ALL SELECT t.id, t.parent_id FROM tree t JOIN cte ON t.parent_id = cte.id ) SELECT * FROM cte; 短UUID：\n1 SELECT UUID_SHORT(); 控制台输出：\n1 SELECT \u0026#39;Hello MySQL!\u0026#39;; 十八、数据类型对照表 逻辑类型 Oracle SQL Server MySQL 说明 字符串 VARCHAR2(n), CHAR(n), CLOB VARCHAR(n), NVARCHAR(n), TEXT VARCHAR(n), CHAR(n), TEXT 长文本类型命名不同 数值 NUMBER(p,s), INTEGER INT, BIGINT, DECIMAL(p,s) INT, BIGINT, DECIMAL(p,s) 精度和范围略有差异 浮点 BINARY_FLOAT, BINARY_DOUBLE FLOAT, REAL FLOAT, DOUBLE 日期/时间 DATE, TIMESTAMP DATETIME, DATE, TIME DATETIME, DATE, TIME, TIMESTAMP Oracle无TIME类型 布尔 无（NUMBER(1)或CHAR(1)代替） BIT TINYINT(1), BOOL 二进制 BLOB, RAW VARBINARY, IMAGE BLOB, BINARY JSON 无（CLOB/JSON字段） NVARCHAR+约束/JSON JSON MySQL原生支持JSON 十九、迁移注意事项与常见问题 NULL处理差异：Oracle的空字符串视为NULL，MySQL区分空字符串和NULL。 分页语法：MySQL 8.0+、SQL Server 2012+、Oracle 12c+支持标准OFFSET/FETCH，老版本需ROWNUM/ROW_NUMBER等。 布尔类型：Oracle无原生布尔，需用NUMBER(1)或CHAR(1)；MySQL用TINYINT(1)。 字符串截断：SQL Server超长字符串需手动截断，Oracle可用SUBSTR。 时区/日期精度：Oracle的TIMESTAMP精度高，MySQL默认无时区。 函数参数顺序：如SUBSTR/substring参数顺序不同。 正则表达式：MySQL 8.0+支持标准正则，SQL Server仅LIKE/PATINDEX。 唯一约束/自增：Oracle 12c+支持IDENTITY，MySQL用AUTO_INCREMENT，SQL Server用IDENTITY。 批量插入/合并：Oracle支持INSERT ALL，MySQL用多VALUES，SQL Server用MERGE。 分区表/空间数据：三库分区和空间类型实现差异大，需单独迁移设计。 二十、性能优化建议 尽量使用索引字段做条件，避免全表扫描。 聚合/窗口函数大表上建议分批处理或加索引。 批量DML建议分批提交，防止锁表。 字符串聚合、JSON处理等大数据量下建议评估执行计划。 合理使用分区表、分区索引提升大表查询性能。 避免在WHERE子句中对字段做函数运算。 二十一、DDL/DML迁移脚本模板 表结构迁移：\nOracle：\n1 2 3 4 5 CREATE TABLE t1 ( id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name VARCHAR2(50), created_at DATE ); SQL Server：\n1 2 3 4 5 CREATE TABLE t1 ( id INT IDENTITY(1,1) PRIMARY KEY, name VARCHAR(50), created_at DATETIME ); MySQL：\n1 2 3 4 5 CREATE TABLE t1 ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50), created_at DATETIME ); 索引迁移：\nOracle：CREATE INDEX idx_name ON t1(name); SQL Server/MySQL：CREATE INDEX idx_name ON t1(name); 视图迁移：\nOracle/SQL Server/MySQL：CREATE VIEW v1 AS SELECT ... FROM ...; 存储过程/函数迁移：建议逐条分析，语法差异较大。\n二十二、迁移后测试与验证建议 数据量校验：\n1 SELECT COUNT(*) FROM t1; 聚合校验：\n1 SELECT SUM(amount), AVG(amount) FROM t1; 样本数据对比：\n1 SELECT * FROM t1 WHERE id IN (1,2,3); 主键/唯一约束校验：\n1 SELECT id, COUNT(*) FROM t1 GROUP BY id HAVING COUNT(*) \u0026gt; 1; NULL/空值校验：\n1 SELECT COUNT(*) FROM t1 WHERE col IS NULL; 性能对比：\n使用EXPLAIN/EXPLAIN PLAN/SHOWPLAN等分析SQL执行计划。 ","permalink":"https://ktzxy.top/posts/r2tu06odzl/","summary":"\u003ch2 id=\"目录\"\u003e目录\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"#%E4%B8%80%E5%AD%97%E7%AC%A6%E4%B8%B2%E5%87%BD%E6%95%B0\"\u003e一、字符串函数\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E4%BA%8C%E6%97%A5%E6%9C%9F%E5%87%BD%E6%95%B0\"\u003e二、日期函数\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E4%B8%89%E6%95%B0%E5%AD%A6%E5%87%BD%E6%95%B0\"\u003e三、数学函数\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E5%9B%9B%E8%81%9A%E5%90%88%E5%87%BD%E6%95%B0\"\u003e四、聚合函数\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E4%BA%94%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2%E5%87%BD%E6%95%B0\"\u003e五、类型转换函数\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E5%85%AD%E6%9D%A1%E4%BB%B6%E5%87%BD%E6%95%B0\"\u003e六、条件函数\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E4%B8%83null%E7%9B%B8%E5%85%B3%E5%87%BD%E6%95%B0\"\u003e七、NULL相关函数\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E5%85%AB%E5%88%86%E7%BB%84%E4%B8%8E%E7%AA%97%E5%8F%A3%E5%87%BD%E6%95%B0\"\u003e八、分组与窗口函数\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E4%B9%9Djson%E5%A4%84%E7%90%86%E5%87%BD%E6%95%B0\"\u003e九、JSON处理函数\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E5%8D%81%E7%A9%BA%E9%97%B4%E5%9C%B0%E7%90%86%E5%87%BD%E6%95%B0\"\u003e十、空间/地理函数\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E5%8D%81%E4%B8%80%E9%94%99%E8%AF%AF%E5%A4%84%E7%90%86%E4%B8%8E%E5%BC%82%E5%B8%B8\"\u003e十一、错误处理与异常\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E5%8D%81%E4%BA%8C%E5%BA%8F%E5%88%97%E4%B8%8E%E8%87%AA%E5%A2%9E\"\u003e十二、序列与自增\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E5%8D%81%E4%B8%89%E6%89%B9%E9%87%8F%E6%8F%92%E5%85%A5%E4%B8%8E%E5%90%88%E5%B9%B6\"\u003e十三、批量插入与合并\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E5%8D%81%E5%9B%9B%E5%85%B6%E5%AE%83%E5%B8%B8%E7%94%A8%E7%B3%BB%E7%BB%9F%E5%87%BD%E6%95%B0\"\u003e十四、其它常用系统函数\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E5%8D%81%E4%BA%94%E5%85%B8%E5%9E%8Bsql%E8%AF%AD%E6%B3%95%E5%B7%AE%E5%BC%82%E8%A1%A5%E5%85%85\"\u003e十五、典型SQL语法差异补充\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E5%8D%81%E5%85%AD%E7%89%B9%E6%AE%8A%E5%87%BD%E6%95%B0%E4%B8%8E%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95%E8%A1%A5%E5%85%85\"\u003e十六、特殊函数与高级用法补充\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E5%8D%81%E4%B8%83%E4%B8%89%E5%A4%A7%E6%95%B0%E6%8D%AE%E5%BA%93%E7%89%B9%E6%9C%89%E5%87%BD%E6%95%B0%E5%8F%8A%E7%89%B9%E6%AE%8A%E5%9C%BA%E6%99%AF%E7%94%A8%E6%B3%95\"\u003e十七、三大数据库特有函数及特殊场景用法\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E5%8D%81%E5%85%AB%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%AF%B9%E7%85%A7%E8%A1%A8\"\u003e十八、数据类型对照表\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E5%8D%81%E4%B9%9D%E8%BF%81%E7%A7%BB%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9%E4%B8%8E%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98\"\u003e十九、迁移注意事项与常见问题\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E4%BA%8C%E5%8D%81%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96%E5%BB%BA%E8%AE%AE\"\u003e二十、性能优化建议\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E4%BA%8C%E5%8D%81%E4%B8%80ddldml%E8%BF%81%E7%A7%BB%E8%84%9A%E6%9C%AC%E6%A8%A1%E6%9D%BF\"\u003e二十一、DDL/DML迁移脚本模板\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"#%E4%BA%8C%E5%8D%81%E4%BA%8C%E8%BF%81%E7%A7%BB%E5%90%8E%E6%B5%8B%E8%AF%95%E4%B8%8E%E9%AA%8C%E8%AF%81%E5%BB%BA%E8%AE%AE\"\u003e二十二、迁移后测试与验证建议\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e版本兼容性说明\u003c/strong\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eOracle 11g+ 支持大部分窗口函数，12c+支持FETCH分页、IDENTITY自增等。\u003c/li\u003e\n\u003cli\u003eSQL Server 2012+ 支持窗口函数、OFFSET分页、TRY_CAST等。\u003c/li\u003e\n\u003cli\u003eMySQL 8.0+ 支持窗口函数、正则、JSON_TABLE等，5.7+支持部分JSON函数。\u003c/li\u003e\n\u003cli\u003e如无特殊说明，示例均以主流新版本为准。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch3 id=\"一字符串函数\"\u003e\u003cstrong\u003e一、字符串函数\u003c/strong\u003e‌\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e\u003cstrong\u003e函数功能\u003c/strong\u003e\u003c/th\u003e\n          \u003cth\u003e\u003cstrong\u003eOracle\u003c/strong\u003e\u003c/th\u003e\n          \u003cth\u003e\u003cstrong\u003eSQL Server\u003c/strong\u003e\u003c/th\u003e\n          \u003cth\u003e\u003cstrong\u003eMySQL\u003c/strong\u003e\u003c/th\u003e\n          \u003cth\u003e\u003cstrong\u003e参数说明\u003c/strong\u003e\u003c/th\u003e\n          \u003cth\u003e\u003cstrong\u003e版本要求\u003c/strong\u003e\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e字符串连接\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eCONCAT(str1, str2)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eCONCAT(str1, str2)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eCONCAT(str1, str2, ...)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eOracle最多2个参数，SQL Server/MySQL支持多个参数拼接\u003c/td\u003e\n          \u003ctd\u003e11g+\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e子串截取\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eSUBSTR(str,start,len)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eSUBSTRING(str,start,len)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eSUBSTRING(str, start, len)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e起始位置从1开始，\u003ccode\u003elen\u003c/code\u003e为截取长度\u003c/td\u003e\n          \u003ctd\u003e11g+\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e字符串长度\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eLENGTH(str)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eLEN(str)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eLENGTH(str)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e返回字符数（中文按1计算）\u003c/td\u003e\n          \u003ctd\u003e11g+\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e去除首尾空格\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eTRIM(str)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eLTRIM(RTRIM(str))\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eTRIM(str)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003eOracle支持单边\u003ccode\u003eTRIM(LEADING/TRAILING)\u003c/code\u003e，MySQL同理\u003c/td\u003e\n          \u003ctd\u003e11g+\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e大小写转换\u003c/td\u003e\n          \u003ctd\u003e\u003ccode\u003eLOWER(str)\u003c/code\u003e / \u003ccode\u003eUPPER(str)\u003c/code\u003e\u003c/td\u003e\n          \u003ctd\u003e同左\u003c/td\u003e\n          \u003ctd\u003e同左\u003c/td\u003e\n          \u003ctd\u003e全数据库通用\u003c/td\u003e\n          \u003ctd\u003e11g+\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch3 id=\"sql示例对照\"\u003e\u003cstrong\u003eSQL示例对照\u003c/strong\u003e\u003c/h3\u003e\n\u003ch4 id=\"1-字符串函数\"\u003e1. 字符串函数\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eOracle\u003c/strong\u003e：\u003c/p\u003e","title":"Oracle\u0026\u0026SqlServer\u0026\u0026Mysql内置函数对比‌"},{"content":"ActiveMQ [TOC]\n入门概述 MQ 的产品种类和对比 kafka\n编程语言：scala。 大数据领域的主流MQ。\nrabbitmq\n编程语言：erlang 基于erlang语言，不好修改底层，不要查找问题的原因，不建议选用。\nrocketmq\n编程语言：java 适用于大型项目。适用于集群。\nactivemq\n编程语言：java 适用于中小型项目。\nMQ 的产生背景 系统之间直接调用存在的问题？ 微服务架构后，链式调用是我们在写程序时候的一般流程,为了完成一个整体功能会将其拆分成多个函数(或子模块)，比如模块A调用模块B,模块B调用模块C,模块C调用模块D。但在大型分布式应用中，系统间的RPC交互繁杂，一个功能背后要调用上百个接口并非不可能，从单机架构过渡到分布式微服务架构的通例。这些架构会有哪些问题？\n系统之间接口耦合比较严重\n每新增一个下游功能，都要对上游的相关接口进行改造； 举个例子：如果系统A要发送数据给系统B和系统C，发送给每个系统的数据可能有差异，因此系统A对要发送给每个系统的数据进行了组装，然后逐一发送； 当代码上线后又新增了一个需求：把数据也发送给D，新上了一个D系统也要接受A系统的数据，此时就需要修改A系统，让他感知到D系统的存在，同时把数据处理好再给D。在这个过程你会看到，每接入一个下游系统，都要对系统A进行代码改造，开发联调的效率很低。其整体架构如下\n面对大流量并发时，容易被冲垮\n每个接口模块的吞吐能力是有限的，这个上限能力如果是堤坝，当大流量（洪水）来临时，容易被冲垮。 举个例子秒杀业务：上游系统发起下单购买操作，就是下单一个操作，很快就完成。然而，下游系统要完成秒杀业务后面的所有逻辑（读取订单，库存检查，库存冻结，余额检查，余额冻结，订单生产，余额扣减，库存减少，生成流水，余额解冻，库存解冻）。\n等待同步存在性能问题\nRPC接口上基本都是同步调用，**整体的服务性能遵循 [ 木桶理论 ] **，即整体系统的耗时取决于链路中最慢的那个接口。比如A调用B/C/D都是50ms，但此时B又调用了B1，花费2000ms，那么直接就拖累了整个服务性能。\n根据上述的几个问题，在设计系统时可以明确要达到的目标： 1，要做到系统解耦，当新的模块接进来时，可以做到代码改动最小；能够解耦 2，设置流量缓冲池，可以让后端系统按照自身吞吐能力进行消费，不被冲垮；能削峰 3，强弱依赖梳理能将非关键调用链路的操作异步化并提升整体系统的吞吐能力；能够异步\nMQ 的主要作用 ==异步==。调用者无需等待。 ==解耦==。解决了系统之间耦合调用的问题。 ==削峰==。抵御洪峰流量，保护了主业务。 MQ 的定义 面向消息的中间件（Message-Oriented Middleware）MOM能够很好的解决以上问题。是指利用高效可靠的消息传递机制与平台无关的数据交流，并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型在分布式环境下提供应用解耦，弹性伸缩，冗余存储、流量削峰，异步通信，数据同步等功能。 大致的过程是这样的：发送者把消息发送给消息服务器，消息服务器将消息存放在若干队列/主题topic中，在合适的时候，消息服务器回将消息转发给接受者。在这个过程中，发送和接收是异步的，也就是发送无需等待，而且发送者和接受者的生命周期也没有必然的关系；尤其在发布pub/订阅sub模式下，也可以完成一对多的通信，即让一个消息有多个接受者。( 类似微信公众号 )\nMQ 的特点 采用异步处理模式\n消息发送者可以发送一个消息而无须等待响应。消息发送者将消息发送到一条虚拟的通道（主题或者队列）上； 消息接收者则订阅或者监听该爱通道。一条消息可能最终转发给一个或者多个消息接收者，这些消息接收者都无需对消息发送者做出同步回应。整个过程都是异步的。 案例： 也就是说，一个系统跟另一个系统之间进行通信的时候，假如系统A希望发送一个消息给系统B，让他去处理。但是系统A不关注系统B到底怎么处理或者有没有处理好，所以系统A把消息发送给MQ，然后就不管这条消息的“死活了”，接着系统B从MQ里面消费出来处理即可。至于怎么处理，是否处理完毕，什么时候处理，都是系统B的事儿，与系统A无关。\n应用系统之间解耦合\n发送者和接受者不必了解对方，只需要确认消息。 发送者和接受者不必同时在线。\nMQ的缺点\n两个系统之间不能同步调用，不能实时回复，不能响应某个调用的回复。\n安装 ActiveMQ 安装启动 ActiveMQ 官网下载\nhttps://activemq.apache.org/components/classic/download/\n上传压缩包\n上传压缩包到 Linux 系统的 opt 目录下。\n解压\ntar -zxf apache-activemq-5.16.2-bin.tar.gz 创建文件夹\nmkdir /usr/local/activemq\n移动解压文件夹\nmv apache-activemq-5.16.2 /usr/local/activemq/apache-activemq-5.16.2\n启动 ActiveMQ\ncd /usr/local/activemq/apache-activemq-5.16.2/bin 进入 bin mul\n./activemq start 启动，默认的启动端口 61616\n./activemq restart 重新启动\n./activemq stop 关闭\nActiveMQ 控制台 ActiveMQ 占用的端口\n后台端口：61616\n前台端口：8161\n打开 端口\nfirewall-cmd --permanent --add-port=8161/tcp 开放前台端口\nfirewall-cmd --permanent --add-port=61616/tcp 开放后台端口\nfirewall-cmd --reload 重新载入\n修改 conf 目录下的 jetty.xml 文件\n将 host 属性从 127.0.0.1 修改为 0.0.0.0\n浏览器访问 http://192.168.200.130:8161/\n默认用户名和密码都为 amind\n登录后的页面\nJava 编码实现 ActiveMQ 通信 IDEA 创建 Maven 工程 POM 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;artifactId\u0026gt;activemq\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;org.hong\u0026lt;/groupId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;artifactId\u0026gt;activemq-demo\u0026lt;/artifactId\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- activemq 所需要的jar 包--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.activemq\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;activemq-all\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.16.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- activemq 和 spring 整合的基础包--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.xbean\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;xbean-spring\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.18\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.18.20\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.7.30\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.13\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;ch.qos.logback\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;logback-classic\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; JMS编码总体规范 Destination是目的地。Destination分为两种：队列 ( 一对一 ) 和主题 ( 一对多 )。\n队列 ( Queue ) 案例 队列消息生产者的入门案例 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class JmsProduce { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue01\u0026#34;; public static void main(String[] args) throws JMSException { // 1.按照给定的url, 创建连接工厂, 使用默认的用户名和密码 ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); // 2.通过连接工厂获得连接connection并启动 Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); // 3.创建会话session // 3.1.参数一: 事务 参数二: 签收 Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); // 4.创建目的地(具体是队列还是主题topic) Queue queue = session.createQueue(QUEUE_NAME); // 5.创建消息的生产者 MessageProducer producer = session.createProducer(queue); for (int i = 0; i \u0026lt; 3; i++) { // 6.Session创建消息 TextMessage textMessage = session.createTextMessage(\u0026#34;msg---\u0026#34; + i);// 理解为一个字符串 // 7.MessageProducer发送消息给MQ producer.send(textMessage); } // 8.关闭资源 producer.close(); session.close(); connection.close(); System.out.println(\u0026#34;消息发布到MQ完成\u0026#34;); } } ActiveMQ 控制台 谷歌翻译：\nActiveMQ 控制台列说明 英文 解释 详细信息 Number Of Pending Messages 等待消费的消息 这个是未出队列的数量，公式=总接收数-总出队列数。 Number Of Consumers 消费者数量 消费者端的消费者数量。只计算当前为连接状态的。 Messages Enqueued 进队列的总消息量 包括出队列的。这个数只增不减。 Messages Dequeued 出队消息数 可以理解为是消费者消费掉的数量。 总结：\n当有一个消息进入这个队列时，等待消费的消息是1，进入队列的消息是1。 当消息消费后，等待消费的消息是0，进入队列的消息是1，出队列的消息是1。 当再来一条消息时，等待消费的消息是1，进入队列的消息就是2。\n队列消息消费者的入门案例 代码实现 介绍\n同步阻塞方式( receive() )。订阅者或接收者调用MessageConsumer的receive()方法来接收消息，receive方法在能够接收到消息之前 ( 或超时之前 ) 将一直阻塞。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class JmsConsumer { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue01\u0026#34;; public static void main(String[] args) throws JMSException { // 1.按照给定的url, 创建连接工厂, 使用默认的用户名和密码 ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); // 2.通过连接工厂获得连接connection并启动 Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); // 3.创建会话session // 3.1.参数一: 事务 参数二: 签收 Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); // 4.创建目的地(具体是队列还是主题topic) Queue queue = session.createQueue(QUEUE_NAME); // 5.创建消息消费者 MessageConsumer consumer = session.createConsumer(queue); while (true) { // 6.消费消息 TextMessage textMessage = (TextMessage) consumer.receive(); if(textMessage != null){ System.out.println(\u0026#34;消费者接收到消息:\u0026#34; + textMessage.getText()); }else{ break; } } // 7.关闭资源 consumer.close(); session.close(); connection.close(); } } 控制台 消息正常取出，但是程序并没有结束，依据在运行；由于我们调用的是 receive() 无参方法，消费者会一直等待，直到获取到消息，因此程序不会停止。\nActiveMQ 控制台 消息消费者数量为1。\nreceive 方法详解 receive() 空参方法 有消息取出消息；没有消息一直等待，直到有消息。\nreceive(long time) 带参方法 有消息取出消息；没有消息等待指定毫秒数，如果还是没有消息，返回 null。\nNumber Of Consumers 介绍 上面的案例：消费者端使用的是 receive() 空参方法，ActiveMQ 控制台中 Number Of Consumers 的值为1。\n现在强制结束消费者端的程序，再次查看 ActiveMQ 控制台中 Number Of Consumers 的值变成了0。\n异步监听式消费者（MessageListener） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; import java.io.IOException; public class JmsConsumer { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue01\u0026#34;; public static void main(String[] args) throws JMSException, IOException { // 1.按照给定的url, 创建连接工厂, 使用默认的用户名和密码 ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); // 2.通过连接工厂获得连接connection并启动 Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); // 3.创建会话session // 3.1.参数一: 事务 参数二: 签收 Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); // 4.创建目的地(具体是队列还是主题topic) Queue queue = session.createQueue(QUEUE_NAME); // 5.创建消息消费者 MessageConsumer consumer = session.createConsumer(queue); consumer.setMessageListener(message -\u0026gt; { // message就是监听器获得到的消息对象 if(message != null \u0026amp;\u0026amp; message instanceof TextMessage){ TextMessage textMessage = (TextMessage) message; try { System.out.println(\u0026#34;消费者接收到消息:\u0026#34; + textMessage.getText()); } catch (JMSException e) { e.printStackTrace(); } } }); // 让主线程不要结束。因为一旦主线程结束了，其他的线程（如此处的监听消息的线程）也都会被迫结束。 // 实际开发中，我们的程序会一直运行，这句代码都会省略。 System.in.read(); consumer.close(); session.close(); connection.close(); } } 队列消息（Queue）总结 两种消费方式 同步阻塞方式(receive)\n订阅者或接收者抵用MessageConsumer的receive()方法来接收消息，receive方法在能接收到消息之前（或超时之前）将一直阻塞。\n异步非阻塞方式（监听器onMessage()）\n订阅者或接收者通过MessageConsumer的setMessageListener(MessageListener listener)注册一个消息监听器，当消息到达之后，系统会自动调用监听器MessageListener的onMessage(Message message)方法。\n队列的特点 每个消息只能有一个消费者，类似1对1的关系。好比个人快递自己领自己的。 消息的生产者和消费者之间没有时间上的相关性。无论消费者在生产者发送消息的时候是否处于运行状态，消费者都可以提取消息。好比我们发送短信，发送者发送后不见得接收者会即收即看。 消息被消费后队列中不会再存储，所以消费者不会消费到已经被消费掉的消息。 消息消费情况 情况1：只启动消费者1。 结果：消费者1会消费所有的数据。\n情况2：先启动消费者1，再启动消费者2。 结果：消费者1消费所有的数据。消费者2不会消费到消息。\n情况3：生产者发布6条消息，在此之前已经启动了消费者1和消费者2。 结果：消费者1和消费者2平摊了消息。各自消费3条消息。\n疑问：怎么去将消费者1和消费者2不平均分摊呢？而是按照各自的消费能力去消费。我觉得，现在ActiveMQ就是这样的机制。\nJMS 编码步骤 创建 ConnectionFacory 通过 ConnectionFacory 创建 JMS Connection 启动 JMS Connection 通过 Connection 创建 JMS Session 创建 JMS Destination 创建 JMS Producer 或者创建 JMS Message 并设置 Destination 创建 JMS Consumer 或者是注册一个 JMS Message Listener 发送或者接收 JMS Message 关闭所有的 JMS 资源 ( Connection、Session、Producer、Consumer 等 ) 主题 ( Topic ) 案例 介绍 在发布订阅消息传递域中，目的地被称为主题（topic） 发布/订阅消息传递域的特点如下：\n生产者将消息发布到topic中，每个消息可以有多个消费者，属于1：N的关系；、 生产者和消费者之间有时间上的相关性。订阅某一个主题的消费者只能消费自它订阅之后发布的消息。 生产者生产时，topic不保存消息它是无状态的不落地，假如无人订阅就去生产，那就是一条废消息，所以，一般先启动消费者再启动生产者。 默认情况下如上所述，但是JMS规范允许客户创建持久订阅，这在一定程度上放松了时间上的相关性要求。持久订阅允许消费者消费它在未处于激活状态时发送的消息。一句话，好比我们的微信公众号订阅\n主题生产者入门案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class JmsProduceTopic { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String TOPIC_NAME = \u0026#34;topic01\u0026#34;; public static void main(String[] args) throws JMSException { // 1.按照给定的url, 创建连接工厂, 使用默认的用户名和密码 ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); // 2.通过连接工厂获得连接connection并启动 Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); // 3.创建会话session // 3.1.参数一: 事务 参数二: 签收 Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); // 4.创建目的地(具体是队列还是主题topic) Topic topic = session.createTopic(TOPIC_NAME); // 5.创建消息的生产者 MessageProducer producer = session.createProducer(topic); for (int i = 0; i \u0026lt; 3; i++) { // 6.Session创建消息 TextMessage textMessage = session.createTextMessage(\u0026#34;topicmsg---\u0026#34; + i);// 理解为一个字符串 // 7.MessageProducer发送消息给MQ producer.send(textMessage); } // 8.关闭资源 producer.close(); session.close(); connection.close(); System.out.println(\u0026#34;Topic消息发布到MQ完成\u0026#34;); } } 主题消费者入门案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; import java.io.IOException; public class JmsConsumerTopic { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String TOPIC_NAME = \u0026#34;topic01\u0026#34;; public static void main(String[] args) throws JMSException, IOException { // 1.按照给定的url, 创建连接工厂, 使用默认的用户名和密码 ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); // 2.通过连接工厂获得连接connection并启动 Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); // 3.创建会话session // 3.1.参数一: 事务 参数二: 签收 Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); // 4.创建目的地(具体是队列还是主题topic) Topic topic = session.createTopic(TOPIC_NAME); // 5.创建消息消费者 MessageConsumer consumer = session.createConsumer(topic); // 6.使用监听的方式消费消息 consumer.setMessageListener(message -\u0026gt; { // message就是监听器获得到的消息对象 if(message != null \u0026amp;\u0026amp; message instanceof TextMessage){ TextMessage textMessage = (TextMessage) message; try { System.out.println(\u0026#34;消费者接收到消息:\u0026#34; + textMessage.getText()); } catch (JMSException e) { e.printStackTrace(); } } }); // 7.关闭资源 System.in.read(); consumer.close(); session.close(); connection.close(); } } 测试 先启动消费者，再启动生产者 启动2个消费者 IDEA 设置一下启动项。\nActiveMQ 控制台\n可以看到 topic01 现在有2个消费者，正常；其他的主题是 ActiveMQ 自动生成的，不用管，后面再说。\n启动生产者 刚刚生成出来的消息立马被消费掉，正常。\n生产者控制台打印\n消息生产完成\n消费者控制台打印\n1号消费者和2号消费者都消费了3条消息\nActiveMQ 控制台\n先启动生产者，再启动消费者 启动生产者 启动消费者 控制台打印\n没有消费到任何消息\nActiveMQ 控制台\n出队消息为0\n队列和主题的比较 比较项目 Topic 模式队列 Queue 模式队列 工作模式 \u0026ldquo;订阅-发布\u0026rdquo; 模式，如果当前没有订阅者，消息将会被丢弃。如果有多个订阅者，那么这些订阅者都会收到消息 \u0026ldquo;负载均衡\u0026rdquo; 模式，如果当前没有消费者，消息也不会丢弃；如果有多个消费者，那么一条消息也只会发送给其中一个消费者，并且要求消费者ack消息。 有无状态 无状态 Queue数据默认会在MQ服务器上以文件行形式保存，比如 ActiveMQ 一般保存在 $AMQ_HOME\\data\\kr-store\\data 下面，也可以配置成 DB 存储。 传递完整性 如果没有订阅者，消息会被丢弃。 消息不会被丢弃。 处理效率 由于消息要按照订阅者的数量进行复制，所以处理性能会随着订阅者的增加而明显降低，并且还要结合不同的消息协议自身的性能差异。 由于一条消息只发送给一个消费者，所有就算消费者再多，性能也不会有明显的降低。当然不同消息协议的具体性能也是有差异的。 JMS 规范 JMS 是什么 什么是Java消息服务？ Java消息服务指的是两个应用程序之间进行异步通信的API，它为标准协议和消息服务提供了一组通用接口，包括创建、发送、读取消息等，用于支持Java应用程序开发。在JavaEE中，当两个应用程序使用JMS进行通信时，它们之间不是直接相连的，而是通过一个共同的消息收发服务组件关联起来以达到解耦/异步削峰的效果。\nJMS 的组成结构和特点 消息头 MS 的消息头有哪些属性：\nJMSDestination：消息目的地 JMSDeliveryMode：消息持久化模式 JMSExpiration：消息过期时间 JMSPriority：消息的优先级 JMSMessageID：消息的唯一标识符。后面我们会介绍如何解决幂等性。 说明： 消息的生产者可以 set 这些属性，消息的消费者可以 get 这些属性。这些属性在 send 方法里面也可以设置。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class JmsProduceTopic { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String TOPIC_NAME = \u0026#34;topic01\u0026#34;; public static void main(String[] args) throws JMSException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Topic topic = session.createTopic(TOPIC_NAME); MessageProducer producer = session.createProducer(topic); for (int i = 0; i \u0026lt; 3; i++) { TextMessage textMessage = session.createTextMessage(\u0026#34;topicmsg---\u0026#34; + i);// 理解为一个字符串 // 这里可以指定每个消息的目的地 textMessage.setJMSDestination(topic); /* * 持久模式和非持久模式。 * 一条持久性的消息：应该被传送“一次仅仅一次”，这就意味着如果JMS提供者出现故障，该消息并不会丢失，它会在服务器恢复之后再次传递。 * 一条非持久的消息：最多会传递一次，这意味着服务器出现故障，该消息将会永远丢失。 */ textMessage.setJMSDeliveryMode(DeliveryMode.NON_PERSISTENT); /* * 可以设置消息在一定时间后过期，默认是永不过期。 * 消息过期时间，等于Destination的send方法中的timeToLive值加上发送时刻的GMT时间值。 * 如果timeToLive值等于0，则JMSExpiration被设为0，表示该消息永不过期。 * 如果发送后，在消息过期时间之后还没有被发送到目的地，则该消息被清除。 */ textMessage.setJMSExpiration(1000); /* * 消息优先级，从0-9十个级别，0-4是普通消息5-9是加急消息。 * JMS不要求MQ严格按照这十个优先级发送消息但必须保证加急消息要先于普通消息到达。默认是4级。 */ textMessage.setJMSPriority(10); // 唯一标识每个消息的标识。MQ会给我们默认生成一个，我们也可以自己指定。 textMessage.setJMSMessageID(\u0026#34;ABCD\u0026#34;); // 上面有些属性在send方法里也能设置 producer.send(textMessage); } producer.close(); session.close(); connection.close(); System.out.println(\u0026#34;Topic消息发布到MQ完成\u0026#34;); } } 消息体 概述 消息体是具体封装的消息数据，一共有5中消息体格式；发送和接收的消息体类型必须一致。\n消息体格式 TextMessage：普通字符串消息，包含一个 String MapMessage：一个 Map 类型的消息，key 为 String 类型，值为 Java 的基本数据类型和 String 类型 BytesMessage：二进制数组消息，包含一个 byte[] StreamMessage：Java 数据流消息，用标准流操作来顺序的填充和读取。 ObjectMessage：对象消息，包含一个可序列化的 Java 对象 生产者代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class JmsProduce { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue01\u0026#34;; public static void main(String[] args) throws JMSException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue(QUEUE_NAME); MessageProducer producer = session.createProducer(queue); for (int i = 0; i \u0026lt; 3; i++) { // 发送TextMessage TextMessage textMessage = session.createTextMessage(\u0026#34;msg---\u0026#34; + i); producer.send(textMessage); // 发送MapMessage MapMessage mapMessage = session.createMapMessage(); mapMessage.setString(\u0026#34;k1\u0026#34;, \u0026#34;v\u0026#34; + i); // MQ中MapMessage中的key是可以重复的 producer.send(mapMessage); } producer.close(); session.close(); connection.close(); System.out.println(\u0026#34;消息发布到MQ完成\u0026#34;); } } 消费者代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; import java.io.IOException; public class JmsConsumer { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue01\u0026#34;; public static void main(String[] args) throws JMSException, IOException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue(QUEUE_NAME); MessageConsumer consumer = session.createConsumer(queue); consumer.setMessageListener(message -\u0026gt; { // 获取TextMessage对象 if(message != null \u0026amp;\u0026amp; message instanceof TextMessage){ TextMessage textMessage = (TextMessage) message; try { System.out.println(\u0026#34;消费者接收到消息:\u0026#34; + textMessage.getText()); } catch (JMSException e) { e.printStackTrace(); } } // 获取MapMessage对象 if(message != null \u0026amp;\u0026amp; message instanceof MapMessage){ MapMessage mapMessage = (MapMessage) message; try { System.out.println(\u0026#34;消费者接收到消息:\u0026#34; + mapMessage.getString(\u0026#34;k1\u0026#34;)); } catch (JMSException e) { e.printStackTrace(); } } }); System.in.read(); consumer.close(); session.close(); connection.close(); } } 测试 顺序任意，查看消费者控制台打印。\n虽然我们设置的键都是 k1，依旧取出了3个值，并且值没有重复。\n消息属性 如果需要除消息头字段之外的值，那么可以使用消息属性。他是识别/去重/重点标注等操作，非常有用的方法。 他们是以属性名和属性值对的形式制定的。可以将属性是为消息头得扩展，属性指定一些消息头没有包括的附加信息，比如可以在属性里指定消息选择器。消息的属性就像可以分配给一条消息的附加消息头一样。它们允许开发者添加有关消息的不透明附加信息。它们还用于暴露消息选择器在消息过滤时使用的数据。\n消息属性的 API：\nJMS 的可靠性 事务偏生产方，签收偏消费方。\nPERSISTENT 持久性 参数设置说明 可以设置消息生产者生产的消息整体是否持久化，或者设置消息是否持久化 ( 使用 JMS 消息头 )。默认是持久化的。\n非持久化 服务器宕机，消息不存在。\n1 2 // 生产者对象设置是否持久化 messageProducer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); 持久化 当服务器宕机，消息依然存在\n1 messageProducer.setDeliveryMode(DeliveryMode.PERSISTENT); 持久的 Queue 持久化消息这是队列的默认传送模式，此模式保证这些消息只被传送一次和成功使用一次。对于这些消息，可靠性是优先考虑的因素。\n可靠性的另一个重要方面是确保持久性消息传送至目标后，消息服务在向消费者传送它们之前不会丢失这些消息。\n持久的 Topic topic默认就是非持久化的，因为生产者生产消息时，消费者也要在线，这样消费者才能消费到消息。 topic消息持久化，只要消费者向MQ服务器注册过，所有生产者发布成功的消息，该消费者都能收到，不管是MQ服务器宕机还是消费者不在线。\n注意：\n一定要先运行一次消费者，等于向MQ注册，类似我订阅了这个主题。 然后再运行生产者发送消息。 之后无论消费者是否在线，都会收到消息。如果不在线的话，下次连接的时候，会把没有收过的消息都接收过来。 持久化topic生产者代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class JmsProduceTopicPersistence { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String TOPIC_NAME = \u0026#34;topic-Persistence\u0026#34;; public static void main(String[] args) throws JMSException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Topic topic = session.createTopic(TOPIC_NAME); MessageProducer producer = session.createProducer(topic); // 设置持久化topic producer.setDeliveryMode(DeliveryMode.PERSISTENT); for (int i = 0; i \u0026lt; 3; i++) { TextMessage textMessage = session.createTextMessage(\u0026#34;topicmsg---\u0026#34; + i); producer.send(textMessage); } producer.close(); session.close(); connection.close(); System.out.println(\u0026#34;TopicPersistence消息发布到MQ完成\u0026#34;); } } 持久化topic消费者代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; import java.io.IOException; public class JmsConsumerTopicPersistence { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String TOPIC_NAME = \u0026#34;topic-Persistence\u0026#34;; public static void main(String[] args) throws JMSException, IOException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); // 设置客户端ID。向MQ服务器注册自己的名称; 先设置再启动start connection.setClientID(\u0026#34;z3\u0026#34;); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Topic topic = session.createTopic(TOPIC_NAME); // 创建一个topic订阅者对象。参数一是topic，参数二是订阅名称 TopicSubscriber topicSubscriber = session.createDurableSubscriber(topic, \u0026#34;topicTest\u0026#34;); topicSubscriber.setMessageListener(message -\u0026gt; { if(message != null \u0026amp;\u0026amp; message instanceof TextMessage){ TextMessage textMessage = (TextMessage) message; try { System.out.println(\u0026#34;订阅者接收到消息:\u0026#34; + textMessage.getText()); } catch (JMSException e) { e.printStackTrace(); } } }); System.in.read(); topicSubscriber.close(); session.close(); connection.close(); } } 测试 先启动一次订阅者向 ActiveMQ 注册自己\n订阅者断开连接\n生产者生产消息\n消息生产后再次启动订阅者\n由于订阅者向MQ订阅了消息，虽然消息生产时订阅者不在线，但是等到订阅者再次上线后，订阅者依旧能接收到消息\nTransaction 事务 关闭事务 只要执行 send，就进入队列中。关闭事务，那第二个签收参数的设置需要有效\n开启事务 先执行 send 再执行 commit，消息才被真正的提交到队列中。消息需要批量发送，需要缓冲区处理。\n生产者案例 开启事务不提交 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class JmsProduceTx { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue01\u0026#34;; public static void main(String[] args) throws JMSException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); // 参数一设置为true, 开启事务 Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue(QUEUE_NAME); MessageProducer producer = session.createProducer(queue); for (int i = 0; i \u0026lt; 3; i++) { TextMessage textMessage = session.createTextMessage(\u0026#34;msg---\u0026#34; + i); producer.send(textMessage); } producer.close(); session.close(); connection.close(); System.out.println(\u0026#34;消息发布到MQ完成\u0026#34;); } } 运行程序观察 ActiveMQ 控制台\n消息并没有进入到队列中\n开启事务回滚 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class JmsProduceTx { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue01\u0026#34;; public static void main(String[] args) throws JMSException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); // 参数一设置为true, 开启事务 Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue(QUEUE_NAME); MessageProducer producer = session.createProducer(queue); for (int i = 0; i \u0026lt; 3; i++) { TextMessage textMessage = session.createTextMessage(\u0026#34;msg---\u0026#34; + i); producer.send(textMessage); } // 回滚事务 session.rollback(); producer.close(); session.close(); connection.close(); System.out.println(\u0026#34;消息发布到MQ完成\u0026#34;); } } 运行程序观察 ActiveMQ 控制台\n消息并没有进入到队列中\n开启事务提交 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; public class JmsProduceTx { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue01\u0026#34;; public static void main(String[] args) throws JMSException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); // 参数一设置为true, 开启事务 Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue(QUEUE_NAME); MessageProducer producer = session.createProducer(queue); for (int i = 0; i \u0026lt; 3; i++) { TextMessage textMessage = session.createTextMessage(\u0026#34;msg---\u0026#34; + i); producer.send(textMessage); } // 提交事务 session.commit(); producer.close(); session.close(); connection.close(); System.out.println(\u0026#34;消息发布到MQ完成\u0026#34;); } } 运行程序观察 ActiveMQ 控制台\n消费者案例 开启事务不提交 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; import java.io.IOException; public class JmsConsumerTx { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue01\u0026#34;; public static void main(String[] args) throws JMSException, IOException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); // 参数一设置为true, 开启事务 Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue(QUEUE_NAME); MessageConsumer consumer = session.createConsumer(queue); while (true) { TextMessage textMessage = (TextMessage) consumer.receive(4000L); if(textMessage != null){ System.out.println(\u0026#34;消费者接收到消息:\u0026#34; + textMessage.getText()); }else{ break; } } consumer.close(); session.close(); connection.close(); } } 运行程序，观察控制台打印和 ActiveMQ 控制台\n消息被消费，但是 ActiveMQ 的队列中，消息依旧存在，会造成重复消费\n开启事务回滚 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; import java.io.IOException; public class JmsConsumerTx { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue01\u0026#34;; public static void main(String[] args) throws JMSException, IOException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); // 参数一设置为true, 开启事务 Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue(QUEUE_NAME); MessageConsumer consumer = session.createConsumer(queue); while (true) { TextMessage textMessage = (TextMessage) consumer.receive(4000L); if(textMessage != null){ System.out.println(\u0026#34;消费者接收到消息:\u0026#34; + textMessage.getText()); }else{ break; } } // 回滚事务 session.rollback(); consumer.close(); session.close(); connection.close(); } } 运行程序，观察控制台打印和 ActiveMQ 控制台\n消息被消费，但是 ActiveMQ 的队列中，消息依旧存在，也会造成重复消费。\n开启事务提交 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; import java.io.IOException; public class JmsConsumerTx { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue01\u0026#34;; public static void main(String[] args) throws JMSException, IOException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); // 参数一设置为true, 开启事务 Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue(QUEUE_NAME); MessageConsumer consumer = session.createConsumer(queue); while (true) { TextMessage textMessage = (TextMessage) consumer.receive(4000L); if(textMessage != null){ System.out.println(\u0026#34;消费者接收到消息:\u0026#34; + textMessage.getText()); }else{ break; } } // 回滚事务 session.commit(); consumer.close(); session.close(); connection.close(); } } 运行程序，观察控制台打印和 ActiveMQ 控制台\n消息被消费，但是 ActiveMQ 的队列中，消息依旧存在，也会造成重复消费。\nAcknowledge 签收 非事务 自动签收 1 2 // Session.AUTO_ACKNOWLEDGE 自动签收 Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); 这里就不演示了，效果就于入门案例一样。没什么好说的。\n手动签收 1 2 // Session.CLIENT_ACKNOWLEDGE 手动签收 Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE); 实例代码\n故名思意，如果不签收消息不会出队列，会造成重复消费。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import javax.jms.*; import java.io.IOException; public class JmsConsumerTx { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue01\u0026#34;; public static void main(String[] args) throws JMSException, IOException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); // 参数二设置为2(Session.CLIENT_ACKNOWLEDGE), 代表手动签收 Session session = connection.createSession(false, Session.CLIENT_ACKNOWLEDGE); Queue queue = session.createQueue(QUEUE_NAME); MessageConsumer consumer = session.createConsumer(queue); while (true) { TextMessage textMessage = (TextMessage) consumer.receive(4000L); if(textMessage != null){ System.out.println(\u0026#34;消费者接收到消息:\u0026#34; + textMessage.getText()); // Message对象调用acknowledge()方法进行签收 textMessage.acknowledge(); }else{ break; } } consumer.close(); session.close(); connection.close(); } } 允许重复消息 1 Session.DUPS_OK_ACKNOWLEDGE 事务 事务开启后，只有 commit 才能将全部消息变为以消费。\n自己试试看把！\n签收和事务的关系 在事务性会话中，当一个事务被成功提交则消息被自动签收。如果事务回滚，则消息会被再次传送。 非事务性会话中，消息何时被确认取决于创建会话时的应答模式 ( acknowledgement mode ) 事务大于签收 ActiveMQ 的 Borker 简介 用 ActiveMQ Broker 作为独立的消息服务器来构建 JAVA 应用。ActiveMQ 也支持在 vm 中通信基于嵌入式的 broker，能够无缝的集成其他 Java 应用。\n说白了，Broker 启动就是实现了用代码的形式启动 ActiveMQ 将 MQ 嵌入到 Java 代码中，一边随时用随时启动，在用的时候再去启动这样能节省资源，也保证了可靠性。\nPOM 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;artifactId\u0026gt;activemq\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;org.hong\u0026lt;/groupId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;artifactId\u0026gt;activemq-demo\u0026lt;/artifactId\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- activemq 所需要的jar 包--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.activemq\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;activemq-all\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.16.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- activemq 和 spring 整合的基础包--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.xbean\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;xbean-spring\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.18\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 引入这个jar包是为了解决(Caused by: java.lang.ClassNotFoundException: com.fasterxml.jackson.databind.ObjectMapper)错误 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-databind\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.12.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.18.20\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.7.30\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.13\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;ch.qos.logback\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;logback-classic\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; 代码示例 启动这个程序就相当于启动了一个 ActiveMQ。\n1 2 3 4 5 6 7 8 9 10 11 12 13 package org.hong.activemq; import org.apache.activemq.broker.BrokerService; public class EmbedBroker { public static void main(String[] args) throws Exception { BrokerService brokerService = new BrokerService(); brokerService.setUseJmx(true); brokerService.addConnector(\u0026#34;tcp://localhost:61616\u0026#34;); brokerService.start(); System.in.read(); } } Spring 整合 ActiveMQ Maven 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;artifactId\u0026gt;activemq\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;org.hong\u0026lt;/groupId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;artifactId\u0026gt;activemq-demo\u0026lt;/artifactId\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- activemq 所需要的jar 包--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.activemq\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;activemq-all\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.16.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- activemq 和 spring 整合的基础包--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.xbean\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;xbean-spring\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.20\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-databind\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.12.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- activeMQ jms 的支持 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-jms\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.7\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- pool 池化包相关的支持 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.activemq\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;activemq-pool\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.16.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- aop 相关的支持 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.7\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-context\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.7\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.18.20\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.7.30\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.13\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;ch.qos.logback\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;logback-classic\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; application.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:context=\u0026#34;http://www.springframework.org/schema/context\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd\u0026#34;\u0026gt; \u0026lt;context:component-scan base-package=\u0026#34;org.hong.activemq.spring\u0026#34;/\u0026gt; \u0026lt;bean id=\u0026#34;jmsFactory\u0026#34; class=\u0026#34;org.apache.activemq.pool.PooledConnectionFactory\u0026#34; destroy-method=\u0026#34;stop\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;connectionFactory\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.apache.activemq.ActiveMQConnectionFactory\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;brokerURL\u0026#34; value=\u0026#34;tcp://192.168.200.130:61616\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;maxConnections\u0026#34; value=\u0026#34;100\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- 创建一个队列目的地 --\u0026gt; \u0026lt;bean id=\u0026#34;destinationQueue\u0026#34; class=\u0026#34;org.apache.activemq.command.ActiveMQQueue\u0026#34;\u0026gt; \u0026lt;constructor-arg index=\u0026#34;0\u0026#34; value=\u0026#34;spring-active-queue\u0026#34;\u0026gt;\u0026lt;/constructor-arg\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- jms 的工具类 --\u0026gt; \u0026lt;bean id=\u0026#34;jmsTemplate\u0026#34; class=\u0026#34;org.springframework.jms.core.JmsTemplate\u0026#34;\u0026gt; \u0026lt;!-- 注入连接工厂 --\u0026gt; \u0026lt;property name=\u0026#34;connectionFactory\u0026#34; ref=\u0026#34;jmsFactory\u0026#34;/\u0026gt; \u0026lt;!-- 注入默认的目的地 --\u0026gt; \u0026lt;property name=\u0026#34;defaultDestination\u0026#34; ref=\u0026#34;destinationQueue\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;messageConverter\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.jms.support.converter.SimpleMessageConverter\u0026#34;/\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/beans\u0026gt; 队列生产者示例 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package org.hong.activemq.spring; import org.apache.xbean.spring.context.ClassPathXmlApplicationContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jms.core.JmsTemplate; import org.springframework.stereotype.Service; @Service public class SpringMQProduce { @Autowired private JmsTemplate jmsTemplate; public static void main(String[] args) { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(\u0026#34;classpath:application.xml\u0026#34;); SpringMQProduce produce = context.getBean(SpringMQProduce.class); produce.sendTextMessage(\u0026#34;Spring 整合 ActiveMQ 案例\u0026#34;); System.out.println(\u0026#34;Send Tack Over\u0026#34;); } public void sendTextMessage(String text){ /* * 我们只需要将目的地和消息给JmsTemplate, Spring会将Session对象传递给我们, 我们根据Session创建Message对象并返回即可 * JmsTemplate会根据目的地自动创建对于的produce并在发送结束后自动关闭produce * 因为我们在配置文件中给JmsTemplate注入了一个默认的目的地, 因此可以不用指定目的地, JmsTemplate会使用默认的目的地。 */ jmsTemplate.send(session -\u0026gt; session.createTextMessage(text)); } } 测试 运行上面的程序\n队列消费者示例 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package org.hong.activemq.spring; import org.apache.xbean.spring.context.ClassPathXmlApplicationContext; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jms.core.JmsTemplate; import org.springframework.stereotype.Service; @Service public class SpringMQConsumer { @Autowired private JmsTemplate jmsTemplate; public static void main(String[] args) { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(\u0026#34;classpath:application.xml\u0026#34;); SpringMQConsumer consumer = context.getBean(SpringMQConsumer.class); Object messageValue = consumer.getMessageValue(); System.out.println(messageValue); } public Object getMessageValue(){ // 没有指定目的地, 使用默认的目的地 return jmsTemplate.receiveAndConvert(); } } 测试 运行上面的程序\n正常取出之前放入的消息，消息也被正常消费。\n主题示例 修改 application.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:context=\u0026#34;http://www.springframework.org/schema/context\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd\u0026#34;\u0026gt; \u0026lt;context:component-scan base-package=\u0026#34;org.hong.activemq.spring\u0026#34;/\u0026gt; \u0026lt;bean id=\u0026#34;jmsFactory\u0026#34; class=\u0026#34;org.apache.activemq.pool.PooledConnectionFactory\u0026#34; destroy-method=\u0026#34;stop\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;connectionFactory\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.apache.activemq.ActiveMQConnectionFactory\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;brokerURL\u0026#34; value=\u0026#34;tcp://192.168.200.130:61616\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;maxConnections\u0026#34; value=\u0026#34;100\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- 创建一个队列目的地 --\u0026gt; \u0026lt;bean id=\u0026#34;destinationQueue\u0026#34; class=\u0026#34;org.apache.activemq.command.ActiveMQQueue\u0026#34;\u0026gt; \u0026lt;constructor-arg index=\u0026#34;0\u0026#34; value=\u0026#34;spring-active-queue\u0026#34;\u0026gt;\u0026lt;/constructor-arg\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- ================== 添加了一个Topic目的地, 并修改了jmsTemplate的默认目的地 ======================== --\u0026gt; \u0026lt;!-- 创建一个主题目的地 --\u0026gt; \u0026lt;bean id=\u0026#34;destinationTopic\u0026#34; class=\u0026#34;org.apache.activemq.command.ActiveMQTopic\u0026#34;\u0026gt; \u0026lt;constructor-arg index=\u0026#34;0\u0026#34; value=\u0026#34;spring-active-topic\u0026#34;\u0026gt;\u0026lt;/constructor-arg\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- jms 的工具类 --\u0026gt; \u0026lt;bean id=\u0026#34;jmsTemplate\u0026#34; class=\u0026#34;org.springframework.jms.core.JmsTemplate\u0026#34;\u0026gt; \u0026lt;!-- 注入连接工厂 --\u0026gt; \u0026lt;property name=\u0026#34;connectionFactory\u0026#34; ref=\u0026#34;jmsFactory\u0026#34;/\u0026gt; \u0026lt;!-- 注入默认的目的地 --\u0026gt; \u0026lt;property name=\u0026#34;defaultDestination\u0026#34; ref=\u0026#34;destinationTopic\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;messageConverter\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.jms.support.converter.SimpleMessageConverter\u0026#34;/\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/beans\u0026gt; 代码无需修改 测试 先启动消费者再启动生产者。正常收到消息。\n监听器配置 修改 application.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:context=\u0026#34;http://www.springframework.org/schema/context\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd\u0026#34;\u0026gt; \u0026lt;context:component-scan base-package=\u0026#34;org.hong.activemq.spring\u0026#34;/\u0026gt; \u0026lt;bean id=\u0026#34;jmsFactory\u0026#34; class=\u0026#34;org.apache.activemq.pool.PooledConnectionFactory\u0026#34; destroy-method=\u0026#34;stop\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;connectionFactory\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.apache.activemq.ActiveMQConnectionFactory\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;brokerURL\u0026#34; value=\u0026#34;tcp://192.168.200.130:61616\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;maxConnections\u0026#34; value=\u0026#34;100\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- 创建一个队列目的地 --\u0026gt; \u0026lt;bean id=\u0026#34;destinationQueue\u0026#34; class=\u0026#34;org.apache.activemq.command.ActiveMQQueue\u0026#34;\u0026gt; \u0026lt;constructor-arg index=\u0026#34;0\u0026#34; value=\u0026#34;spring-active-queue\u0026#34;\u0026gt;\u0026lt;/constructor-arg\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- 创建一个主题目的地 --\u0026gt; \u0026lt;bean id=\u0026#34;destinationTopic\u0026#34; class=\u0026#34;org.apache.activemq.command.ActiveMQTopic\u0026#34;\u0026gt; \u0026lt;constructor-arg index=\u0026#34;0\u0026#34; value=\u0026#34;spring-active-topic\u0026#34;\u0026gt;\u0026lt;/constructor-arg\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- 向Spring容器中注册一个MQ的监听器 --\u0026gt; \u0026lt;bean id=\u0026#34;jmsContainer\u0026#34; class=\u0026#34;org.springframework.jms.listener.DefaultMessageListenerContainer\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;connectionFactory\u0026#34; ref=\u0026#34;jmsFactory\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;destination\u0026#34; ref=\u0026#34;destinationTopic\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;messageListener\u0026#34; ref=\u0026#34;myMessageListener\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- jms 的工具类 --\u0026gt; \u0026lt;bean id=\u0026#34;jmsTemplate\u0026#34; class=\u0026#34;org.springframework.jms.core.JmsTemplate\u0026#34;\u0026gt; \u0026lt;!-- 注入连接工厂 --\u0026gt; \u0026lt;property name=\u0026#34;connectionFactory\u0026#34; ref=\u0026#34;jmsFactory\u0026#34;/\u0026gt; \u0026lt;!-- 注入默认的目的地 --\u0026gt; \u0026lt;property name=\u0026#34;defaultDestination\u0026#34; ref=\u0026#34;destinationTopic\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;messageConverter\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.jms.support.converter.SimpleMessageConverter\u0026#34;/\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/beans\u0026gt; 自定义监听器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package org.hong.activemq.spring; import org.springframework.stereotype.Component; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageListener; import javax.jms.TextMessage; @Component public class MyMessageListener implements MessageListener { @Override public void onMessage(Message message) { if(message instanceof TextMessage){ TextMessage textMessage = (TextMessage)message; try { System.out.println(textMessage.getText()); } catch (JMSException e) { e.printStackTrace(); } } } } 测试 直接启动生产者\n当我们向 MQ 中放入消息时，我们注册的监听器立刻就感知到了新的消息，并取出来进行消费。\nSpringBoot 整合 ActiveMQ 队列 队列生产者 新建 Maven 工程 工程名：boot-mq-produce\n包名：org.hong.boot.activemq\nPOM 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-parent\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;version\u0026gt;2.2.2.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;org.hong\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;boot-mq-produce\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-activemq\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; YAML 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server: port: 7777 spring: activemq: # MQ服务器地址 broker-url: tcp://192.168.200.130:61616 # 用户名和密码 user: admin password: admin jms: # false=Queue true=Topic pub-sub-domain: false # 自己定义队列名称 myqueue: boot-activemq-queue 主启动 1 2 3 4 5 6 7 8 9 10 11 12 13 package org.hong.boot.activemq; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.jms.annotation.EnableJms; @SpringBootApplication @EnableJms // 开启JMS public class MainApp { public static void main(String[] args) { SpringApplication.run(MainApp.class, args); } } Config 配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package org.hong.boot.activemq.config; import org.apache.activemq.command.ActiveMQQueue; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.jms.Queue; @Configuration public class ConfigBean { @Value(\u0026#34;${myqueue}\u0026#34;) private String myQueue; @Bean public Queue queue(){ return new ActiveMQQueue(myQueue); } } 生产者 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package org.hong.boot.activemq.produce; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jms.core.JmsMessagingTemplate; import org.springframework.stereotype.Component; import javax.jms.Queue; @Component public class QueueProduce { @Autowired private JmsMessagingTemplate jmsMessagingTemplate;// JmsMessagingTemplate是JmsTemplate的加强版 @Autowired private Queue queue; public void produceMsg(){ jmsMessagingTemplate.convertAndSend(queue, \u0026#34;SpringBoot 整合 ActiveMQ\u0026#34;); } } 测试单元 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package org.hong.boot.activemq; import org.hong.boot.activemq.produce.QueueProduce; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @SpringBootTest(classes = MainApp.class) @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration public class TestActiveMQ { @Autowired private QueueProduce queueProduce; @Test void testQueueProduceSend(){ queueProduce.produceMsg(); } } ActiveMQ 控制台 定时发送 案例修改\n修改QueueProduce新增定时投递方法 1 2 3 4 5 6 // 间隔3秒定投 @Scheduled(fixedDelay = 3000) public void produceMsgScheduled(){ jmsMessagingTemplate.convertAndSend(queue, \u0026#34;Scheduled\u0026#34; + UUID.randomUUID().toString().substring(0, 6)); System.out.println(\u0026#34;produceMsgScheduled send ok\u0026#34;); } 主启动添加 @EnableScheduling 注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package org.hong.boot.activemq; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.jms.annotation.EnableJms; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableJms // 开启JMS @EnableScheduling // 开启Scheduled注解 public class MainApp { public static void main(String[] args) { SpringApplication.run(MainApp.class, args); } } 测试 直接运行主启动了，定时投递自动运行。自行观察控制台。\n队列消费者 新建 Maven 工程 工程名：boot-mq-consumer\n包名：org.hong.boot.activemq\nPOM 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-parent\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;version\u0026gt;2.2.2.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;org.hong\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;boot-mq-consumer\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-activemq\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; YAML 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server: port: 8888 spring: activemq: # MQ服务器地址 broker-url: tcp://192.168.200.130:61616 # 用户名和密码 user: admin password: admin jms: # false=Queue true=Topic pub-sub-domain: false # 自己定义队列名称 myqueue: boot-activemq-queue 主启动 1 2 3 4 5 6 7 8 9 10 11 12 13 package org.hong.boot.activemq; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.jms.annotation.EnableJms; @SpringBootApplication @EnableJms public class MainApp { public static void main(String[] args) { SpringApplication.run(MainApp.class, args); } } 消费者 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package org.hong.boot.activemq.consumer; import org.springframework.jms.annotation.JmsListener; import org.springframework.stereotype.Component; import javax.jms.JMSException; import javax.jms.TextMessage; @Component public class QueueConsumer { // 使用@JmsListener标注这个方法是监听方法, 并指定目的地 // 因为我们在application.yaml中配置的是队列模式, 因此SpringBoot会根据名字创建队列的目的地 @JmsListener(destination = \u0026#34;${myqueue}\u0026#34;) public void receive(TextMessage textMessage) throws JMSException { System.out.println(\u0026#34;消费者收到消息: \u0026#34; + textMessage.getText()); } } 发布订阅 主题生产者 新建 Maven 工程 工程名：boot-mq-topic-produce\n包名：org.hong.boot.activemq.topic\nPOM 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-parent\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;version\u0026gt;2.2.2.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;org.hong\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;boot-mq-topic-produce\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-activemq\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; YAML 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server: port: 6666 spring: activemq: # MQ服务器地址 broker-url: tcp://192.168.200.130:61616 # 用户名和密码 user: admin password: admin jms: # false=Queue true=Topic pub-sub-domain: true # 自己定义主题名称 mytopic: boot-activemq-topic 主启动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.hong.boot.topic; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.jms.annotation.EnableJms; @SpringBootApplication @EnableScheduling @EnableJms public class MainApp { public static void main(String[] args) { SpringApplication.run(MainApp.class, args); } } Config 配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package org.hong.boot.topic.config; import org.apache.activemq.command.ActiveMQTopic; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.jms.Topic; @Configuration public class ConfigBean { @Value(\u0026#34;${mytopic}\u0026#34;) private String myTopic; @Bean public Topic topic(){ return new ActiveMQTopic(myTopic); } } 生产者 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package org.hong.boot.topic.produce; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jms.core.JmsMessagingTemplate; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.jms.Topic; import java.util.UUID; @Component public class TopicProduce { @Autowired private JmsMessagingTemplate jmsMessagingTemplate; @Autowired private Topic topic; @Scheduled(fixedDelay = 3000) public void produceTopic(){ jmsMessagingTemplate.convertAndSend(topic, \u0026#34;主题消息发布\u0026#34; + UUID.randomUUID().toString().substring(0, 6)); } } 主题消费者 新建 Maven 工程 工程名：boot-mq-topic-consumer\n包名：org.hong.boot.activemq.topic\nPOM 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;artifactId\u0026gt;activemq\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;org.hong\u0026lt;/groupId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;org.hong\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;boot-mq-topic-consumer\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-activemq\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; YAML 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 server: port: 5555 spring: activemq: # MQ服务器地址 broker-url: tcp://192.168.200.130:61616 # 用户名和密码 user: admin password: admin jms: # false=Queue true=Topic pub-sub-domain: true # 自己定义主题名称 mytopic: boot-activemq-topic 主启动 1 2 3 4 5 6 7 8 9 10 11 12 13 package org.hong.boot.activemq.topic; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.jms.annotation.EnableJms; @SpringBootApplication @EnableJms public class MainApp { public static void main(String[] args) { SpringApplication.run(MainApp.class, args); } } 消费者 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package org.hong.boot.activemq.topic.consumer; import org.springframework.jms.annotation.JmsListener; import org.springframework.stereotype.Component; import javax.jms.JMSException; import javax.jms.TextMessage; @Component public class TopicConsumer { @JmsListener(destination = \u0026#34;${mytopic}\u0026#34;) public void receive(TextMessage textMessage) throws JMSException { System.out.println(\u0026#34;消费者收到订阅的主题: \u0026#34; + textMessage.getText()); } } 主题测试 先启动消费者再启动生产者\n启动多个消费者，IDEA 启动多个微服务。\n每隔3秒，消费者服务会消费一条消息。\nActiveMQ 的传输协议 简介 ActiveMQ 支持的 client-broker 通信协议有：TCP、NIO、UDP、SSL、Http(s)、VM。\n启动配置 Transport Connector 的文件在 ActiveMQ 安装目录的 conf/activemq.xml 中的 transportConnectors 标签之内。\n在上文给出的配置文件中。\nURI 描述消息的头部都是采用协议名称：\n描述 amqp 协议的监听端口时，采用的 URI 描述格式为 amqp://...。\n唯独在进行 openwire 协议描述时，URI 头却采用 tcp://。这是因为 ActiveMQ 中默认的消息协议就是 openwire。\n传输协议简介 Transmission Control Protocol ( TCP ) 这是默认的 Broker 配置，TCP 的 Client 监听端口 61616 在网络创数数据前，必须要序列化数据，消息是通过一个叫 wire protocol 来序列化成字节流。默认情况下 ActiveMQ 把 wire protocol 叫做 OpenWire，它的目的是促使网络上的效率和数据快速交互。 TCP 连接的 URI 形式如：tcp://hostname:port?key=value\u0026amp;key=value，后面是参数是可选的 TCP 传输的优点 TCP 协议传输可靠性高，稳定性强 高效性：字节流方式传递，效率很高 有效性、可用性：应用广泛，支持任何平台 关于 Transport 协议的可配置参数可以参考官网：http://activemq.apache.org/configuring-version-5-transports.html NEW I/O API Protocol ( NIO ) NIO 协议和 TCP 协议类似但是 NIO 更侧重于底层的访问操作。它允许开发人员对同一资源可有更多的 client 调用和服务端有更多的负载。 适合使用 NIO 协议的场景： 可能有大量的 Client 去连接 Broker，一般情况下，大量的 Client 去连接 Broker 是被操作系统的线程所限制的。因此 NIO 的实现比 TCP 需要更少的线程去运行，所以建议使用 NIO 协议 可能对于 Broker 有一个很迟钝的网络传输，NIO 比 TCP 提供更好的性能。 NIO 连接的 URI 形式：nio//hostname:port?key=value Transport Connector 配置示例，参考官网：http://activemq.apache.org/configuring-version-5-transports.html 1 2 3 \u0026lt;transportConnectors\u0026gt; \u0026lt;transportConnector name=\u0026#34;nio\u0026#34; uri=\u0026#34;nio://localhost:61618?trace=true\u0026#34;/\u0026gt; \u0026lt;/transportConnectors\u0026gt; NIO 案例演示 修改 activemq.xml 1 2 3 \u0026lt;transportConnectors\u0026gt; \u0026lt;transportConnector name=\u0026#34;nio\u0026#34; uri=\u0026#34;nio://0.0.0.0:61618?trace=true\u0026#34;/\u0026gt; \u0026lt;/transportConnectors\u0026gt; 如果不特别指定 ActiveMQ 的网络监听端口，那么这些端口都将使用过 BIO 网络 IO 默认。( OpenWire，STOMP，AMQP\u0026hellip;\u0026hellip;就是默认带的5个 )。所以为了首先提高单节点的网络吞吐性能，我们需要明确指定 Active 的网络 IO 模型。如下所示：URI 格式头以 nio 开头，表示这个端口使用以 TCP 协议为基础的 NIO 网络 IO 模型。( BIO：阻塞 IO，NIO：非阻塞 IO )\n修改完成后重启 ActiveMQ。\nActiveMQ 控制台 测试 修改入门案例代码，并且打开 61618 端口。\n1 private static final String ACTIVE_URL = \u0026#34;nio://192.168.200.130:61618\u0026#34;; 自行测试，肯定是没问题的。就算不修改连接地址，一样不受影响，之前的 tcp 协议依旧能过使用。\nNIO 增强 问题 URI 格式头以 nio 开头，表示这个端口使用以 TCP 协议为基础的 NIO 网络 IO 模型，但是这样的设置方式，只能使这个端口支持 Openwire 协议，我们这么让这个端口支持 NIO 网络 IO 模型，又让它支持多个协议呢？\n解决 使用 auto 关键字\n使用 + 符号来为端口设置多种特性\n1 \u0026lt;transportConnector name=\u0026#34;auto+nio\u0026#34; uri=\u0026#34;auto+nio://0.0.0.0:61618?maximumConnections=1000\u0026amp;amp;wireFormat.maxFrameSize=104857600\u0026amp;amp;org.apache.activemq.transport.nio.SelectorManager.corePoolSize=20\u0026amp;amp;org.apache.activemq.transport.nio.SelectorManager.maximumPoolSize=50\u0026#34;/\u0026gt; 测试 1 private static final String ACTIVE_URL = \u0026#34;nio://192.168.200.130:61618\u0026#34;; 1 private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61618\u0026#34;; 我们使用 tcp 或者 nio 协议访问 61618 端口进行测试，都不会有问题。\nActiveMQ 的消息存储和持久化 概述 为了避免意外宕机以后丢失信息，需要做到重启后可以恢复消息队列，消息系统一般都会采用持久化机制。ActiveMQ 的消息持久化机制有 JDBC、AMQ、KahanDB 和 LevelDB，无论使用哪种持久化方式，消息的存储逻辑都是一致的。\n就是在发送者将消息发送出去后，消息中心首先将消息存储到本地数据文件、内存数据库或者远程数据库等再试图将消息发送给接收者，成功则将消息从存储中删除，失败则继续尝试发送。\n消息中心启动以后首先要检查指定的存储位置，如果有未发送成功的消息，则需要把消息发送出去。\n持久化方式介绍 AMQ ( Message Store ) 基于文件的存储方式，是以前的默认消息存储，现在不用了。\nKahaDB 消息存储 ( 默认 ) 基于日志文件，从 ActiveMQ5.4 开始默认的持久化插件\nJDBC 消息存储 消息基于 JDBC 存储\nLevelDB 消息存储 这种文件系统是从 ActiveMQ5.8 之后引进的，它和 KahaDB 非常相似，也是基于文件的本地数据库存储形式，但是它提供比 KahaDB 更快的持久性。但它不使用自定义 B-Tree 来实现索引预写日志，而是使用基于 LevelDB 的索引\n默认配置\n1 2 3 \u0026lt;persistenceAdapter\u0026gt; \u0026lt;levelDB directory=\u0026#34;activemq-data\u0026#34;/\u0026gt; \u0026lt;/persistenceAdapter\u0026gt; JDBC Message store with ActiveMQ Journal 后面有\nJDBC 消息存储 拷贝一个 MySQL 数据库的驱动包到 lib 文件夹下\njdbcPersistenceAdapter 配置\n修改 activemq.xml 配置文件，修改如下：\n修改前\n1 2 3 \u0026lt;persistenceAdapter\u0026gt; \u0026lt;kahaDB directory=\u0026#34;${activemq.data}/kahadb\u0026#34;/\u0026gt; \u0026lt;/persistenceAdapter\u0026gt; 修改后\n1 2 3 \u0026lt;persistenceAdapter\u0026gt; \u0026lt;jdbcPersistenceAdapter dataSource=\u0026#34;#mysql-ds\u0026#34;/\u0026gt; \u0026lt;/persistenceAdapter\u0026gt; dataSource 指定将要引用的持久化数据库的 bean 名称，可以任意写，# 不能丢。\ncreateTablesOnStartup 是否在启动的时候创建数据库，默认值是 true，这样每次启动都会去创建数据表，一般是第一次启动的时候设置为 true 之后改为 false。\n数据库连接池配置\n修改 conf/activemq.xml 配置文件\n1 2 3 4 5 6 7 8 \u0026lt;bean id=\u0026#34;mysql-ds\u0026#34; class=\u0026#34;org.apache.commons.dbcp2.BasicDataSource\u0026#34; destroy-method=\u0026#34;close\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driverClassName\u0026#34; value=\u0026#34;com.mysql.jdbc.Driver\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;jdbc:mysql//192.168.200.1:3306/activemq?relaxAutoCommit=true\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;root\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;1234\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;maxTotal\u0026#34; value=\u0026#34;200\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;poolPreparedStatements\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 注意添加的位置。\n创建数据库和对应的表\n创建一个名为 activemq 的数据库\n1 create database activemq; 启动 ActiveMQ 后运行程序会自动创建表\n如果启动不了，可能是 MySQL 不允许外部连接\n1 2 3 -- \u0026#39;%\u0026#39;允许任意主机使用root用户名的1234密码进行连接 GRANT ALL PRIVILEGES ON *.* TO \u0026#39;root\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED BY \u0026#39;1234\u0026#39; WITH GRANT OPTION; FLUSH PRIVILEGES; Queue\n在没有消费者消费的情况下会将消息保存到 activemq_msgs 表中，只要有任意一个消费者已经消费过了，消费之后这些消息将会被立即删除\nTopic\n一般是先启动消费订阅然后再生产的情况下会将消息保存到 activemq_acks 表中。\n下划线坑爹\njava.lang.IllegalStateException:BeanFactory not initialized or already closed 这是因为你的操作系统的机器名中有 _ 符号。\nJDBC Message store with ActiveMQ Journal 简介 这种方式克服了 JDBC Store 的不足，JDBC 每次消息过来，都需要去写库和读库。ActiveMQ Journal 使用高速缓存写入技术，大大提高了性能。当消费者的消费速度能够及时跟上生产者消费的生产速度时，Journal 文件能够大大减少需要写入到 DB 中的消息。\n举个栗子\n生产者生产了 1000 条消息，这 1000 条消息会保存到 Journal 文件，如果消费者的消费是速度很快的情况下，在 Journal 文件还没有同步到 DB 之前，消费者已经消费了 90% 消息，那么这个时候只需要同步剩余的 10% 的消息到 DB。如果消费者的消费速度很慢，这个时候 Journal 文件可以使消息以批量方式写到 DB。\n配置 修改 conf/activemq.xml 文件\n修改前\n1 2 3 \u0026lt;persistenceAdapter\u0026gt; \u0026lt;jdbcPersistenceAdapter dataSource=\u0026#34;#mysql-ds\u0026#34;/\u0026gt; \u0026lt;/persistenceAdapter\u0026gt; 修改后\n1 2 3 4 5 6 7 8 9 \u0026lt;persistenceFactory\u0026gt; \u0026lt;journalPersistenceAdapterFactory journalLogFiles=\u0026#34;4\u0026#34; journalLogFileSize=\u0026#34;32768\u0026#34; useJournal=\u0026#34;true\u0026#34; useQuickJournal=\u0026#34;true\u0026#34; dataSource=\u0026#34;#mysql-ds\u0026#34; dataDirectory=\u0026#34;activemq-data\u0026#34;/\u0026gt; \u0026lt;/persistenceFactory\u0026gt; 高级特性 异步投递 概述 ActiveMQ 支持同步、异步两种方式的模式将消息发送到 broker，模式的选择对发送延时有巨大的影响。使用异步发送可以显著的提高发送的性能\nActiveMQ 默认使用异步发送的模式：除非明确指定使用同步发送的方式或者在未使用事务的前提下发送持久化的消息，这两种情况都是同步发送的。\n如果你没有使用事务且发送的使持久化的消息，每一次发送都是同步发送的且会阻塞 producer 直到 broker 返回一个确认，表示消息已经被安全的持久化到磁盘。确认机制提供了消息安全的保障，但同时会则色客户端带来了很大的延时。\n很多高性能的应用。允许在失败的情况下有少量的数据丢失。如果你的应用满足这个特点，你可以使用异步发送。\n异步发送\n它可以最大化 producer 端的发送效率。通常在发送消息量比较密集的情况下使用异步发送，它可以很大的提升 producer 性能。不过这也带来了额外的问题：\n需要消耗较多的 Client 端内存同时也会导致 broker 端性能消耗增加 不能有效的确保消息的发送成功。在 useAsyncSend = true 的情况下客户端需要容忍消息丢失的可能 三种开启方式 Connection URI\n1 new ActiveMQConnectionFacttory(\u0026#34;tcp://localhost:61616?jms.useAsyscSend=true\u0026#34;); ConnectionFatory\n1 ((ActiveMQConnectionFactory)connectionFactory).setUseAsyncSend(true); Connection\n1 ((ActiveMQConnection)connection).setUseAsyncSend(true); 异步投递如何确认发送成功 异步发送丢失消息的场景使：生产者设置 UseAsyncSend=true，使用 producer.send(msg) 持续发送消息。由于消息不阻塞，生产者会认为所有send 的消息均被成功发送至 MQ。如果 MQ 突然宕机，此时生产者端内容中尚未被发送至 MQ 的消息都会丢失。所以，正确的异步发送方法使需要接收回调的。\n代码示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.ActiveMQMessageProducer; import org.apache.activemq.AsyncCallback; import javax.jms.*; import java.util.UUID; public class JmsProduce { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue\u0026#34;; public static void main(String[] args) throws JMSException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue(QUEUE_NAME); // 消息生产者使用ActiveMQMessageProducer ActiveMQMessageProducer producer = (ActiveMQMessageProducer) session.createProducer(queue); producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); for (int i = 0; i \u0026lt; 3; i++) { TextMessage textMessage = session.createTextMessage(\u0026#34;msg---\u0026#34; + i); String id = UUID.randomUUID().toString(); textMessage.setJMSMessageID(id); producer.send(textMessage, new AsyncCallback() { // 成功的回调 @Override public void onSuccess() { System.out.println(id + \u0026#34;发送成功\u0026#34;); } // 失败的回调 @Override public void onException(JMSException e) { System.out.println(id + \u0026#34;发送失败\u0026#34;); } }); } producer.close(); session.close(); connection.close(); System.out.println(\u0026#34;消息发布到MQ完成\u0026#34;); } } 延迟投递和定时投递 修改 conf/activemq.xml\n新增 schedulerSupport=\u0026quot;true\u0026quot; 属性。\n四大属性\nProperty Name type description AMQ_SCHEDULED_DELAY long 延迟投递的时间 AMQ_SCHEDULED_PERIOD long 重复投递的的时间间隔 AMQ_SCHEDULED_REPEAT int 重复投递次数 AMQ_SCHEDULED_CRON String Cron 表达式 代码案例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.ScheduledMessage; import javax.jms.*; public class JmsProduce { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue\u0026#34;; public static void main(String[] args) throws JMSException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue(QUEUE_NAME); MessageProducer producer = session.createProducer(queue); producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); long delay = 3 * 1000; long period = 4 * 1000; int repeat = 5; for (int i = 0; i \u0026lt; 3; i++) { TextMessage textMessage = session.createTextMessage(\u0026#34;msg---\u0026#34; + i); textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_DELAY, delay); // 延迟投递的时间 textMessage.setLongProperty(ScheduledMessage.AMQ_SCHEDULED_PERIOD, period); // 重复投递的时间间隔 textMessage.setIntProperty(ScheduledMessage.AMQ_SCHEDULED_REPEAT, repeat); // 重复投递次数 producer.send(textMessage); } producer.close(); session.close(); connection.close(); System.out.println(\u0026#34;消息发布到MQ完成\u0026#34;); } } 消息重发 消息重发的情况\nClient 用了 Transactions 且在 session 中调用了 rollback() Client 用了 Transactions 且在调用 commit() 之前关闭或者没有 commit() Client 在 CLIENT_ACKNOWLEDGE 的传递模式下，在 sessino 中调用了 recover() 消息重发时间间隔和重发次数\n间隔：1\n次数：6\n测试\n消费者端开启事务，但是不提交事务，会造成重复消费，但是由于消息重发机制的存在，在进行第7次消费时将无法再消费到数据。\n消息去哪了？\n一个消息被 redelivedred 超过默认的最大重发次数时，消费端会给 MQ 发送一个 poison ack 表示这个消息有毒，告诉 broker 不要再发了。这个时候 broker 会把这个消息方法 DLQ ( 死信队列 )。\n自定义重发策略\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package org.hong.activemq; import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.RedeliveryPolicy; import javax.jms.*; import java.io.IOException; public class JmsConsumer { private static final String ACTIVE_URL = \u0026#34;tcp://192.168.200.130:61616\u0026#34;; private static final String QUEUE_NAME = \u0026#34;queue01\u0026#34;; public static void main(String[] args) throws JMSException, IOException { ActiveMQConnectionFactory activeMQConnectionFactory = new ActiveMQConnectionFactory(ACTIVE_URL); // 自定义重发策略 RedeliveryPolicy redeliveryPolicy = new RedeliveryPolicy(); redeliveryPolicy.setMaximumRedeliveries(3); activeMQConnectionFactory.setRedeliveryPolicy(redeliveryPolicy); Connection connection = activeMQConnectionFactory.createConnection(); connection.start(); Session session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); Queue queue = session.createQueue(QUEUE_NAME); MessageConsumer consumer = session.createConsumer(queue); while (true) { TextMessage textMessage = (TextMessage) consumer.receive(4000L); if(textMessage != null){ System.out.println(\u0026#34;消费者接收到消息:\u0026#34; + textMessage.getText()); }else{ break; } } // session.commit(); 不提交事务 consumer.close(); session.close(); connection.close(); } } 属性说明\ncollisionAvoidanceFactor：设置防止冲突访问的正负百分比，只用\n属性 默认值 描述 backOffMultiplier 5 重连时间间隔递增倍数，只有值大于1和启动 useExponentialBackOff 参数时才生效。 collisionAvoidanceFactor 0.15 设置防止冲突访问的正负百分比，只有启动 useCollisionAvoidance 参数时才生效。也就是延迟时间上再加一个时间波动范围。 initialRedeliveryDelay 1000L 初始重发延迟(以毫秒为单位)。 maximumRedeliveries 6 最大重传次数，达到最大重连次数后抛出异常。为-1时不限制次数，为0表示不进行重传。 maximumRedeliveryDelay -1 最大传送延迟，只在 useExponentialBackOff=true 时生效。 redeliveryDelay 1000L 重发延迟时间，当 initialRedeliveryDelay=0 时生效。 useCollisionAvoidance false 启用防止冲突功能。 useExponentialBackOff false 启动指数倍数递增的方式增加延迟时间。 死信队列 ActiveMQ 中引入了 死信队列(Dead Letter Queue)的概念。即一条消息在被重发了多次后，将会被 ActiveMQ 移入死信队列。开发人员可以在这个 Queue 中查看处理出错的消息，进行人工干预。\n","permalink":"https://ktzxy.top/posts/i6rr6qvjny/","summary":"ActiveMQ","title":"ActiveMQ"},{"content":"Golang的数据类型 概述 Go 语言中数据类型分为：基本数据类型和复合数据类型基本数据类型有：\n整型、浮点型、布尔型、字符串\n复合数据类型有：\n数组、切片、结构体、函数、map、通道（channel）、接口等。\n整型 整型的类型有很多中，包括 int8，int16，int32，int64。我们可以根据具体的情况来进行定义\n如果我们直接写 int也是可以的，它在不同的操作系统中，int的大小是不一样的\n32位操作系统：int -\u0026gt; int32 64位操作系统：int -\u0026gt; int64 可以通过unsafe.Sizeof 查看不同长度的整型，在内存里面的存储空间\n1 2 var num2 = 12 fmt.Println(unsafe.Sizeof(num2)) 类型转换 通过在变量前面添加指定类型，就可以进行强制类型转换\n1 2 3 4 var a1 int16 = 10 var a2 int32 = 12 var a3 = int32(a1) + a2 fmt.Println(a3) 注意，高位转低位的时候，需要注意，会存在精度丢失，比如上述16转8位的时候，就丢失了\n1 2 var n1 int16 = 130 fmt.Println(int8(n1)) // 变成 -126 数字字面量语法 Go1.13版本之后，引入了数字字面量语法，这样便于开发者以二进制、八进制或十六进制浮点数的格式定义数字，例如：\n1 2 v := 0b00101101 // 代表二进制的101101 v：= Oo377 // 代表八进制的377 进制转换 1 2 3 4 5 6 7 8 9 10 11 var number = 17 // 原样输出 fmt.Printf(\u0026#34;%v\\n\u0026#34;, number) // 十进制输出 fmt.Printf(\u0026#34;%d\\n\u0026#34;, number) // 以八进制输出 fmt.Printf(\u0026#34;%o\\n\u0026#34;, number) // 以二进制输出 fmt.Printf(\u0026#34;%b\\n\u0026#34;, number) // 以十六进制输出 fmt.Printf(\u0026#34;%x\\n\u0026#34;, number) 浮点型 Go语言支持两种浮点型数：float32和float64。这两种浮点型数据格式遵循IEEE754标准：\nfloat32的浮点数的最大范围约为3.4e38，可以使用常量定义：math.MaxFloat32。float64的浮点数的最大范围约为1.8e308，可以使用一个常量定义：math.MaxFloat64\n打印浮点数时，可以使用fmt包配合动词%f，代码如下：\n1 2 3 4 5 var pi = math.Pi // 打印浮点类型，默认小数点6位 fmt.Printf(\u0026#34;%f\\n\u0026#34;, pi) // 打印浮点类型，打印小数点后2位 fmt.Printf(\u0026#34;%.2f\\n\u0026#34;, pi) Golang中精度丢失的问题 几乎所有的编程语言都有精度丢失的问题，这是典型的二进制浮点数精度损失问题，在定长条件下，二进制小数和十进制小数互转可能有精度丢失\n1 2 d := 1129.6 fmt.Println(d*100) //输出112959.99999999 解决方法，使用第三方包来解决精度损失的问题\nhttp://github.com/shopspring/decimal\n布尔类型 定义\n1 2 3 4 5 6 var fl = false if f1 { fmt.Println(\u0026#34;true\u0026#34;) } else { fmt.Println(\u0026#34;false\u0026#34;) } 字符串类型 Go 语言中的字符串以原生数据类型出现，使用字符串就像使用其他原生数据类型（int、bool、float32、float64等）一样。Go语言里的字符串的内部实现使用UTF-8编码。字符串的值为双引号（\u0026quot;）中的内容，可以在Go语言的源码中直接添加非ASCll码字符，例如：\n1 2 s1 := \u0026#34;hello\u0026#34; s1 := \u0026#34;你好\u0026#34; 如果想要定义多行字符串，可以使用反引号\n1 2 3 var str = `第一行 第二行` fmt.Println(str) 字符串常见操作 len(str)：求长度 +或fmt.Sprintf：拼接字符串 strings.Split：分割 strings.contains：判断是否包含 strings.HasPrefix，strings.HasSuffix：前缀/后缀判断 strings.Index()，strings.LastIndex()：子串出现的位置 strings.Join()：join操作 strings.Index()：判断在字符串中的位置 byte 和 rune类型 组成每个字符串的元素叫做 “字符”，可以通过遍历字符串元素获得字符。字符用单引号 \u0026rsquo;\u0026rsquo; 包裹起来\nGo语言中的字符有以下两种类型\nuint8类型：或者叫byte型，代表了ACII码的一个字符 rune类型：代表一个UTF-8字符 当需要处理中文，日文或者其他复合字符时，则需要用到rune类型，rune类型实际上是一个int32\nGo使用了特殊的rune类型来处理Unicode，让基于Unicode的文本处理更为方便，也可以使用byte型进行默认字符串处理，性能和扩展性都有照顾。\n需要注意的是，在go语言中，一个汉字占用3个字节（utf-8），一个字母占用1个字节\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package main import \u0026#34;fmt\u0026#34; func main() { var a byte = \u0026#39;a\u0026#39; // 输出的是ASCII码值，也就是说当我们直接输出byte（字符）的时候，输出的是这个字符对应的码值 fmt.Println(a) // 输出的是字符 fmt.Printf(\u0026#34;%c\u0026#34;, a) // for循环打印字符串里面的字符 // 通过len来循环的，相当于打印的是ASCII码 s := \u0026#34;你好 golang\u0026#34; for i := 0; i \u0026lt; len(s); i++ { fmt.Printf(\u0026#34;%v(%c)\\t\u0026#34;, s[i], s[i]) } // 通过rune打印的是 utf-8字符 for index, v := range s { fmt.Println(index, v) } } 修改字符串 要修改字符串，需要先将其转换成[]rune 或 []byte类型，完成后在转换成string，无论哪种转换都会重新分配内存，并复制字节数组\n转换为 []byte 类型\n1 2 3 4 5 // 字符串转换 s1 := \u0026#34;big\u0026#34; byteS1 := []byte(s1) byteS1[0] = \u0026#39;p\u0026#39; fmt.Println(string(byteS1)) 转换为rune类型\n1 2 3 4 5 // rune类型 s2 := \u0026#34;你好golang\u0026#34; byteS2 := []rune(s2) byteS2[0] = \u0026#39;我\u0026#39; fmt.Println(string(byteS2)) 基本数据类型转换 数值类型转换 1 2 3 4 5 6 7 8 9 // 整型和浮点型之间转换 var aa int8 = 20 var bb int16 = 40 fmt.Println(int16(aa) + bb) // 建议整型转换成浮点型 var cc int8 = 20 var dd float32 = 40 fmt.Println(float32(cc) + dd) 建议从低位转换成高位，这样可以避免\n转换成字符串类型 第一种方式，就是通过 fmt.Sprintf()来转换\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 字符串类型转换 var i int = 20 var f float64 = 12.456 var t bool = true var b byte = \u0026#39;a\u0026#39; str1 := fmt.Sprintf(\u0026#34;%d\u0026#34;, i) fmt.Printf(\u0026#34;类型：%v-%T \\n\u0026#34;, str1, str1) str2 := fmt.Sprintf(\u0026#34;%f\u0026#34;, f) fmt.Printf(\u0026#34;类型：%v-%T \\n\u0026#34;, str2, str2) str3 := fmt.Sprintf(\u0026#34;%t\u0026#34;, t) fmt.Printf(\u0026#34;类型：%v-%T \\n\u0026#34;, str3, str3) str4 := fmt.Sprintf(\u0026#34;%c\u0026#34;, b) fmt.Printf(\u0026#34;类型：%v-%T \\n\u0026#34;, str4, str4) 第二种方法就是通过strconv包里面的集中转换方法进行转换\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // int类型转换str类型 var num1 int64 = 20 s1 := strconv.FormatInt(num1, 10) fmt.Printf(\u0026#34;转换：%v - %T\u0026#34;, s1, s1) // float类型转换成string类型 var num2 float64 = 3.1415926 /* 参数1：要转换的值 参数2：格式化类型 \u0026#39;f\u0026#39;表示float，\u0026#39;b\u0026#39;表示二进制，‘e’表示 十进制 参数3：表示保留的小数点，-1表示不对小数点格式化 参数4：格式化的类型，传入64位 或者 32位 */ s2 := strconv.FormatFloat(num2, \u0026#39;f\u0026#39;, -1, 64) fmt.Printf(\u0026#34;转换：%v-%T\u0026#34;, s2, s2) 字符串转换成int 和 float类型 1 2 3 4 5 6 7 str := \u0026#34;10\u0026#34; // 第一个参数：需要转换的数，第二个参数：进制， 参数三：32位或64位 num,_ = strconv.ParseInt(str, 10, 64) // 转换成float类型 str2 := \u0026#34;3.141592654\u0026#34; num,_ = strconv.ParseFloat(str2, 10) ","permalink":"https://ktzxy.top/posts/sztlmhcgvb/","summary":"3 Go的数据类型","title":"3 Go的数据类型"},{"content":"Create_Sync_Job_Template 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 /* ================================================================================ 脚本名称：SQL Server Agent 作业创建模板 (高频同步专用) 功能描述：创建一个定时执行存储过程的作业，支持自定义频率、重试机制和超时控制。 适用场景：数据同步、定时报表、批量处理等。 【修改指南】 下次使用时，只需修改脚本顶部的 \u0026#34;【配置区域】\u0026#34; 中的变量值即可。 无需修改下方的逻辑代码，除非需要改变作业的高级行为。 ================================================================================ */ USE msdb; GO -- ============================================= -- 【配置区域】 (下次修改请只改这里) -- ============================================= DECLARE -- 1. 作业名称 (建议包含业务含义和频率，如：Job_Sync_Sale_10Min) @JobName NVARCHAR(128) = N\u0026#39;Job_Sync_Sale_Every10Min\u0026#39;, -- 2. 目标数据库名称 (存储过程所在的数据库) @DatabaseName NVARCHAR(128) = N\u0026#39;zyscm_dherp\u0026#39;, -- 3. 要执行的存储过程名称 (可带参数，如: \u0026#39;dbo.sync_sale_to_middle @param=1\u0026#39;) @StoredProcedure NVARCHAR(200) = N\u0026#39;dbo.sync_sale_to_middle\u0026#39;, -- 4. 作业所有者 (通常留空使用当前登录用户，或指定为 \u0026#39;sa\u0026#39;) @OwnerLogin NVARCHAR(128) = N\u0026#39;\u0026#39;, -- 5. 调度频率设置 @FreqType INT = 4, -- 4=每天, 3=每周, 2=一次性 @FreqSubdayType INT = 4, -- 4=分钟, 8=小时, 1=一次 @FreqSubdayInterval INT = 10, -- 间隔数值 (例如：每10分钟，每2小时) -- 6. 开始运行时间 (格式：HHMMSS，例如 080000 = 早上8点) @ActiveStartTime INT = 000000, -- 7. 失败重试设置 @RetryAttempts INT = 1, -- 失败后重试次数 @RetryInterval INT = 1; -- 重试间隔 (分钟) -- ============================================= -- 【逻辑区域】 (通常不需要修改) -- ============================================= DECLARE @ReturnCode INT; DECLARE @JobId UNIQUEIDENTIFIER; DECLARE @StepName NVARCHAR(128) = N\u0026#39;Execute Sync Step\u0026#39;; DECLARE @ScheduleName NVARCHAR(128) = N\u0026#39;Schedule_\u0026#39; + CAST(@FreqSubdayInterval AS VARCHAR) + \u0026#39;_Mins\u0026#39;; DECLARE @Command NVARCHAR(MAX); DECLARE @FinalOwner NVARCHAR(128); -- 自动处理所有者：如果未指定，则使用当前用户 IF ISNULL(@OwnerLogin, \u0026#39;\u0026#39;) = \u0026#39;\u0026#39; SET @FinalOwner = SUSER_SNAME(); ELSE SET @FinalOwner = @OwnerLogin; -- 构建执行命令 SET @Command = N\u0026#39;USE [\u0026#39; + @DatabaseName + N\u0026#39;]; EXEC \u0026#39; + @StoredProcedure + N\u0026#39;;\u0026#39;; PRINT \u0026#39;正在配置作业: \u0026#39; + @JobName; PRINT \u0026#39;目标数据库: \u0026#39; + @DatabaseName; PRINT \u0026#39;执行命令: \u0026#39; + @Command; -- 1. 删除已存在的同名作业 (防止冲突) IF EXISTS (SELECT job_id FROM msdb.dbo.sysjobs WHERE name = @JobName) BEGIN EXEC msdb.dbo.sp_delete_job @job_name = @JobName, @delete_unused_schedule = 1; PRINT \u0026#39;\u0026gt;\u0026gt;\u0026gt; 已删除旧作业: \u0026#39; + @JobName; END -- 2. 创建新作业 EXEC @ReturnCode = msdb.dbo.sp_add_job @job_name = @JobName, @enabled = 1, -- 1=启用, 0=禁用 @notify_level_eventlog = 2, -- 2=失败时记录到Windows事件日志 @notify_level_email = 0, -- 0=不发送邮件 (如需邮件通知需配置数据库邮件) @notify_level_netsend = 0, @notify_level_page = 0, @delete_level = 0, -- 0=作业完成后不删除 @description = N\u0026#39;自动生成的同步作业 - 频率:\u0026#39; + CAST(@FreqSubdayInterval AS VARCHAR) + \u0026#39;分钟\u0026#39;, @category_name = N\u0026#39;[Uncategorized (Local)]\u0026#39;, @owner_login_name = @FinalOwner, @job_id = @JobId OUTPUT; IF (@@ERROR \u0026lt;\u0026gt; 0 OR @ReturnCode \u0026lt;\u0026gt; 0) BEGIN PRINT \u0026#39;!!! 创建作业失败\u0026#39;; GOTO QuitWithRollback; END -- 3. 添加作业步骤 EXEC @ReturnCode = msdb.dbo.sp_add_jobstep @job_id = @JobId, @step_name = @StepName, @step_id = 1, @cmdexec_success_code = 0, @on_success_action = 1, -- 1=结束作业 (成功) @on_success_step_id = 0, @on_fail_action = 2, -- 2=结束作业 (失败) - 如果需要重试，主要靠下面的重试参数 @on_fail_step_id = 0, @retry_attempts = @RetryAttempts, @retry_interval = @RetryInterval, @os_run_priority = 0, @subsystem = N\u0026#39;TSQL\u0026#39;, @command = @Command, @database_name = @DatabaseName, -- 关键：确保上下文在正确的数据库 @flags = 0; IF (@@ERROR \u0026lt;\u0026gt; 0 OR @ReturnCode \u0026lt;\u0026gt; 0) BEGIN PRINT \u0026#39;!!! 创建作业步骤失败\u0026#39;; GOTO QuitWithRollback; END -- 4. 添加调度计划 EXEC @ReturnCode = msdb.dbo.sp_add_jobschedule @job_id = @JobId, @name = @ScheduleName, @enabled = 1, @freq_type = @FreqType, @freq_interval = 1, @freq_subday_type = @FreqSubdayType, @freq_subday_interval = @FreqSubdayInterval, @freq_relative_interval = 0, @freq_recurrence_factor = 1, @active_start_date = CONVERT(VARCHAR(8), GETDATE(), 112), -- 从今天开始 @active_end_date = 99991231, @active_start_time = @ActiveStartTime, @active_end_time = 235959; IF (@@ERROR \u0026lt;\u0026gt; 0 OR @ReturnCode \u0026lt;\u0026gt; 0) BEGIN PRINT \u0026#39;!!! 创建调度计划失败\u0026#39;; GOTO QuitWithRollback; END -- 5. 将作业关联到本地服务器 EXEC @ReturnCode = msdb.dbo.sp_add_jobserver @job_id = @JobId, @server_name = N\u0026#39;(local)\u0026#39;; IF (@@ERROR \u0026lt;\u0026gt; 0 OR @ReturnCode \u0026lt;\u0026gt; 0) BEGIN PRINT \u0026#39;!!! 关联服务器失败\u0026#39;; GOTO QuitWithRollback; END PRINT \u0026#39;=========================================\u0026#39;; PRINT \u0026#39;\u0026gt;\u0026gt;\u0026gt; 作业创建成功!\u0026#39;; PRINT \u0026#39;\u0026gt;\u0026gt;\u0026gt; 名称: \u0026#39; + @JobName; PRINT \u0026#39;\u0026gt;\u0026gt;\u0026gt; 频率: 每 \u0026#39; + CAST(@FreqSubdayInterval AS VARCHAR) + \u0026#39; 分钟\u0026#39;; PRINT \u0026#39;\u0026gt;\u0026gt;\u0026gt; 下次运行时间请查看 SQL Agent 作业历史记录\u0026#39;; PRINT \u0026#39;=========================================\u0026#39;; GOTO EndRoutine; QuitWithRollback: IF (@JobId IS NOT NULL) BEGIN EXEC msdb.dbo.sp_delete_job @job_id = @JobId; PRINT \u0026#39;\u0026gt;\u0026gt;\u0026gt; 已回滚并删除部分创建的作业\u0026#39;; END EndRoutine: GO 作业清理某表日志 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 /* =============================================================================== 脚本功能：创建 SQL Server Agent 作业，自动清理 wtps_middledb..sync_log 的历史日志 清理策略：保留最近 7 天数据，每天凌晨 02:00 执行 =============================================================================== */ USE msdb; GO -- ================= 配置区域 (请在此处修改) ================= DECLARE @dbName NVARCHAR(128) = N\u0026#39;wtps_middledb\u0026#39;; DECLARE @tableName NVARCHAR(128) = N\u0026#39;sync_log\u0026#39;; -- 【重要】请将 \u0026#39;log_time\u0026#39; 修改为你表中真实的时间字段名！ DECLARE @timeColumn NVARCHAR(128) = N\u0026#39;end_time\u0026#39;; DECLARE @retainDays INT = 7; -- 保留天数 DECLARE @jobName NVARCHAR(128) = N\u0026#39;Job_Cleanup_SyncLog\u0026#39;; DECLARE @scheduleName NVARCHAR(128) = N\u0026#39;Schedule_3Daily_2AM\u0026#39;; -- =========================================================== BEGIN TRY DECLARE @jobDescription NVARCHAR(512); SET @jobDescription = N\u0026#39;自动清理 \u0026#39; + @dbName + \u0026#39;.\u0026#39; + @tableName + N\u0026#39; 中 \u0026#39; + CAST(@retainDays AS NVARCHAR(10)) + N\u0026#39; 天前的日志\u0026#39;; -- 1. 如果作业已存在，先删除以避免冲突 IF EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = @jobName) BEGIN EXEC msdb.dbo.sp_delete_job @job_name = @jobName; PRINT N\u0026#39;✓ 已删除存在的旧作业：\u0026#39; + @jobName; END -- 2. 创建新作业 DECLARE @jobId UNIQUEIDENTIFIER; EXEC msdb.dbo.sp_add_job @job_name = @jobName, @enabled = 1, @description = @jobDescription, @start_step_id = 1, @job_id = @jobId OUTPUT; PRINT N\u0026#39;✓ 作业创建成功：\u0026#39; + @jobName; -- 3. 构建删除逻辑的 T-SQL 命令 DECLARE @sqlCommand NVARCHAR(MAX); SET @sqlCommand = N\u0026#39; USE [\u0026#39; + @dbName + N\u0026#39;]; SET NOCOUNT ON; DECLARE @CutoffDate DATETIME = DATEADD(DAY, -\u0026#39; + CAST(@retainDays AS NVARCHAR) + N\u0026#39;, GETDATE()); DECLARE @DeletedCount INT = 0; -- 执行删除 DELETE FROM [\u0026#39; + @tableName + N\u0026#39;] WHERE [\u0026#39; + @timeColumn + N\u0026#39;] \u0026lt; @CutoffDate; SET @DeletedCount = @@ROWCOUNT; -- 输出信息到作业历史日志 PRINT N\u0026#39;\u0026#39;[清理完成] 表：\u0026#39; + @tableName + N\u0026#39; | 截止期限：\u0026#39;\u0026#39; + CONVERT(NVARCHAR(19), @CutoffDate, 120) + N\u0026#39;\u0026#39; | 删除行数：\u0026#39;\u0026#39; + CAST(@DeletedCount AS NVARCHAR(20)); \u0026#39;; -- 4. 添加作业步骤 EXEC msdb.dbo.sp_add_jobstep @job_id = @jobId, @step_name = N\u0026#39;Delete Old Logs Step\u0026#39;, @subsystem = N\u0026#39;TSQL\u0026#39;, @command = @sqlCommand, @database_name = @dbName, @on_fail_action = 2; -- 失败时停止并报告 PRINT N\u0026#39;✓ 作业步骤已配置\u0026#39;; -- 5. 创建调度计划 (每天 02:00:00) -- 如果计划已存在则复用，否则创建 DECLARE @scheduleId INT; IF EXISTS (SELECT 1 FROM msdb.dbo.sysschedules WHERE name = @scheduleName) BEGIN SELECT @scheduleId = schedule_id FROM msdb.dbo.sysschedules WHERE name = @scheduleName; END ELSE BEGIN EXEC msdb.dbo.sp_add_schedule @schedule_name = @scheduleName, @freq_type = 4, -- 每天 @freq_interval = 3, -- 每隔 3 天 @active_start_time = 20000, -- 02:00:00 (HHMMSS) @schedule_id = @scheduleId OUTPUT; END -- 6. 关联作业与计划 EXEC msdb.dbo.sp_attach_schedule @job_id = @jobId, @schedule_id = @scheduleId; -- 7. 关联作业到当前服务器 EXEC msdb.dbo.sp_add_jobserver @job_id = @jobId, @server_name = @@SERVERNAME; PRINT N\u0026#39;========================================\u0026#39;; PRINT N\u0026#39;🎉 作业部署成功！\u0026#39;; PRINT N\u0026#39;作业名称：\u0026#39; + @jobName; PRINT N\u0026#39;目标对象：\u0026#39; + @dbName + \u0026#39;..\u0026#39; + @tableName; PRINT N\u0026#39;时间字段：\u0026#39; + @timeColumn; PRINT N\u0026#39;保留策略：最近 \u0026#39; + CAST(@retainDays AS NVARCHAR) + N\u0026#39; 天\u0026#39;; PRINT N\u0026#39;执行频率：每天凌晨 02:00\u0026#39;; PRINT N\u0026#39;========================================\u0026#39;; PRINT N\u0026#39;⚠️ 下一步操作建议：\u0026#39;; PRINT N\u0026#39;请确保字段 [\u0026#39; + @timeColumn + N\u0026#39;] 上有索引，否则删除大量数据时会锁表！\u0026#39;; PRINT N\u0026#39;执行以下 SQL 创建索引（如果尚未存在）：\u0026#39;; PRINT N\u0026#39;CREATE INDEX IX_\u0026#39; + @tableName + \u0026#39;_Time ON \u0026#39; + @dbName + \u0026#39;.dbo.\u0026#39; + @tableName + \u0026#39;(\u0026#39; + @timeColumn + \u0026#39;);\u0026#39;; PRINT N\u0026#39;========================================\u0026#39;; END TRY BEGIN CATCH PRINT N\u0026#39;❌ 发生错误：\u0026#39; + ERROR_MESSAGE(); PRINT N\u0026#39;错误号：\u0026#39; + CAST(ERROR_NUMBER() AS NVARCHAR); -- 如果出错，尝试回滚可能创建的半成品的作业 IF EXISTS (SELECT 1 FROM msdb.dbo.sysjobs WHERE name = @jobName) EXEC msdb.dbo.sp_delete_job @job_name = @jobName; END CATCH GO ","permalink":"https://ktzxy.top/posts/xl3qr9l97w/","summary":"Create_Sync_Job_Template 脚本用于在 SQL Server 中创建一个同步作业模板，支持自定义频率、重试机制和超时控制。通过修改配置区域内的变量值，可以快速定制化适用于不同场景的定时任务。此脚本特别适合于高频数据同步需求，如每10分钟执行一次","title":"Create_Sync_Job_Template"},{"content":"Go操作Redis 来源 https://www.liwenzhou.com/posts/Go/go_redis/\ncache缓存 简单的队列 排行榜 介绍 Redis是一个开源的内存数据库，Redis提供了多种不同类型的数据结构，很多业务场景下的问题都可以很自然地映射到这些数据结构上。除此之外，通过复制、持久化和客户端分片等特性，我们可以很方便地将Redis扩展成一个能够包含数百GB数据、每秒处理上百万次请求的系统。\nRedis支持的数据结构 Redis支持诸如字符串（strings）、哈希（hashes）、列表（lists）、集合（sets）、带范围查询的排序集合（sorted sets）、位图（bitmaps）、hyperloglogs、带半径查询和流的地理空间索引等数据结构（geospatial indexes）。\nRedis应用场景 缓存系统，减轻主数据库（MySQL）的压力。 计数场景，比如微博、抖音中的关注数和粉丝数。 热门排行榜，需要排序的场景特别适合使用ZSET。 利用LIST可以实现队列的功能。 Redis与Memcached比较 Memcached的值只支持简单的字符串，Redis支持更丰富的数据结构，Redis的性能比Memcached好很多，Redis支持RDB持久化和AOF持久化，Redis支持master/slave模式。\n准备Redis环境 这里直接使用Docker启动一个redis环境，方便学习使用。\ndocker启动一个名为redis507的5.0.7版本的redis server示例：\n1 docker run --name redis507 -p 6379:6379 -d redis:5.0.7 **注意：**此处的版本、容器名和端口号请根据自己需要设置。\n启动一个redis-cli连接上面的redis server:\n1 docker run -it --network host --rm redis:5.0.7 redis-cli go-redis库 安装 区别于另一个比较常用的Go语言redis client库：redigo，我们这里采用https://github.com/go-redis/redis连接Redis数据库并进行操作，因为go-redis支持连接哨兵及集群模式的Redis。\n使用以下命令下载并安装:\n1 go get -u github.com/go-redis/redis 连接 普通连接 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 声明一个全局的rdb变量 var rdb *redis.Client // 初始化连接 func initClient() (err error) { rdb = redis.NewClient(\u0026amp;redis.Options{ Addr: \u0026#34;localhost:6379\u0026#34;, Password: \u0026#34;\u0026#34;, // no password set DB: 0, // use default DB }) _, err = rdb.Ping().Result() if err != nil { return err } return nil } 连接Redis哨兵模式 1 2 3 4 5 6 7 8 9 10 11 func initClient()(err error){ rdb := redis.NewFailoverClient(\u0026amp;redis.FailoverOptions{ MasterName: \u0026#34;master\u0026#34;, SentinelAddrs: []string{\u0026#34;x.x.x.x:26379\u0026#34;, \u0026#34;xx.xx.xx.xx:26379\u0026#34;, \u0026#34;xxx.xxx.xxx.xxx:26379\u0026#34;}, }) _, err = rdb.Ping().Result() if err != nil { return err } return nil } 连接Redis集群 1 2 3 4 5 6 7 8 9 10 func initClient()(err error){ rdb := redis.NewClusterClient(\u0026amp;redis.ClusterOptions{ Addrs: []string{\u0026#34;:7000\u0026#34;, \u0026#34;:7001\u0026#34;, \u0026#34;:7002\u0026#34;, \u0026#34;:7003\u0026#34;, \u0026#34;:7004\u0026#34;, \u0026#34;:7005\u0026#34;}, }) _, err = rdb.Ping().Result() if err != nil { return err } return nil } 基本使用 set/get示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func redisExample() { err := rdb.Set(\u0026#34;score\u0026#34;, 100, 0).Err() if err != nil { fmt.Printf(\u0026#34;set score failed, err:%v\\n\u0026#34;, err) return } val, err := rdb.Get(\u0026#34;score\u0026#34;).Result() if err != nil { fmt.Printf(\u0026#34;get score failed, err:%v\\n\u0026#34;, err) return } fmt.Println(\u0026#34;score\u0026#34;, val) val2, err := rdb.Get(\u0026#34;name\u0026#34;).Result() if err == redis.Nil { fmt.Println(\u0026#34;name does not exist\u0026#34;) } else if err != nil { fmt.Printf(\u0026#34;get name failed, err:%v\\n\u0026#34;, err) return } else { fmt.Println(\u0026#34;name\u0026#34;, val2) } } zset示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 func redisExample2() { zsetKey := \u0026#34;language_rank\u0026#34; languages := []redis.Z{ redis.Z{Score: 90.0, Member: \u0026#34;Golang\u0026#34;}, redis.Z{Score: 98.0, Member: \u0026#34;Java\u0026#34;}, redis.Z{Score: 95.0, Member: \u0026#34;Python\u0026#34;}, redis.Z{Score: 97.0, Member: \u0026#34;JavaScript\u0026#34;}, redis.Z{Score: 99.0, Member: \u0026#34;C/C++\u0026#34;}, } // ZADD num, err := rdb.ZAdd(zsetKey, languages...).Result() if err != nil { fmt.Printf(\u0026#34;zadd failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;zadd %d succ.\\n\u0026#34;, num) // 把Golang的分数加10 newScore, err := rdb.ZIncrBy(zsetKey, 10.0, \u0026#34;Golang\u0026#34;).Result() if err != nil { fmt.Printf(\u0026#34;zincrby failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;Golang\u0026#39;s score is %f now.\\n\u0026#34;, newScore) // 取分数最高的3个 ret, err := rdb.ZRevRangeWithScores(zsetKey, 0, 2).Result() if err != nil { fmt.Printf(\u0026#34;zrevrange failed, err:%v\\n\u0026#34;, err) return } for _, z := range ret { fmt.Println(z.Member, z.Score) } // 取95~100分的 op := redis.ZRangeBy{ Min: \u0026#34;95\u0026#34;, Max: \u0026#34;100\u0026#34;, } ret, err = rdb.ZRangeByScoreWithScores(zsetKey, op).Result() if err != nil { fmt.Printf(\u0026#34;zrangebyscore failed, err:%v\\n\u0026#34;, err) return } for _, z := range ret { fmt.Println(z.Member, z.Score) } } 输出结果如下：\n1 2 3 4 5 6 7 8 9 10 $ ./06redis_demo zadd 0 succ. Golang\u0026#39;s score is 100.000000 now. Golang 100 C/C++ 99 Java 98 JavaScript 97 Java 98 C/C++ 99 Golang 100 Pipeline Pipeline 主要是一种网络优化。它本质上意味着客户端缓冲一堆命令并一次性将它们发送到服务器。这些命令不能保证在事务中执行。这样做的好处是节省了每个命令的网络往返时间（RTT）。\nPipeline 基本示例如下：\n1 2 3 4 5 6 7 pipe := rdb.Pipeline() incr := pipe.Incr(\u0026#34;pipeline_counter\u0026#34;) pipe.Expire(\u0026#34;pipeline_counter\u0026#34;, time.Hour) _, err := pipe.Exec() fmt.Println(incr.Val(), err) 上面的代码相当于将以下两个命令一次发给redis server端执行，与不使用Pipeline相比能减少一次RTT。\n1 2 INCR pipeline_counter EXPIRE pipeline_counts 3600 也可以使用Pipelined：\n1 2 3 4 5 6 7 var incr *redis.IntCmd _, err := rdb.Pipelined(func(pipe redis.Pipeliner) error { incr = pipe.Incr(\u0026#34;pipelined_counter\u0026#34;) pipe.Expire(\u0026#34;pipelined_counter\u0026#34;, time.Hour) return nil }) fmt.Println(incr.Val(), err) 在某些场景下，当我们有多条命令要执行时，就可以考虑使用pipeline来优化。\n事务 Redis是单线程的，因此单个命令始终是原子的，但是来自不同客户端的两个给定命令可以依次执行，例如在它们之间交替执行。但是，Multi/exec能够确保在multi/exec两个语句之间的命令之间没有其他客户端正在执行命令。\n在这种场景我们需要使用TxPipeline。TxPipeline总体上类似于上面的Pipeline，但是它内部会使用MULTI/EXEC包裹排队的命令。例如：\n1 2 3 4 5 6 7 pipe := rdb.TxPipeline() incr := pipe.Incr(\u0026#34;tx_pipeline_counter\u0026#34;) pipe.Expire(\u0026#34;tx_pipeline_counter\u0026#34;, time.Hour) _, err := pipe.Exec() fmt.Println(incr.Val(), err) 上面代码相当于在一个RTT下执行了下面的redis命令：\n1 2 3 4 MULTI INCR pipeline_counter EXPIRE pipeline_counts 3600 EXEC 还有一个与上文类似的TxPipelined方法，使用方法如下：\n1 2 3 4 5 6 7 var incr *redis.IntCmd _, err := rdb.TxPipelined(func(pipe redis.Pipeliner) error { incr = pipe.Incr(\u0026#34;tx_pipelined_counter\u0026#34;) pipe.Expire(\u0026#34;tx_pipelined_counter\u0026#34;, time.Hour) return nil }) fmt.Println(incr.Val(), err) Watch 在某些场景下，我们除了要使用MULTI/EXEC命令外，还需要配合使用WATCH命令。在用户使用WATCH命令监视某个键之后，直到该用户执行EXEC命令的这段时间里，如果有其他用户抢先对被监视的键进行了替换、更新、删除等操作，那么当用户尝试执行EXEC的时候，事务将失败并返回一个错误，用户可以根据这个错误选择重试事务或者放弃事务。\n1 Watch(fn func(*Tx) error, keys ...string) error Watch方法接收一个函数和一个或多个key作为参数。基本使用示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 // 监视watch_count的值，并在值不变的前提下将其值+1 key := \u0026#34;watch_count\u0026#34; err = client.Watch(func(tx *redis.Tx) error { n, err := tx.Get(key).Int() if err != nil \u0026amp;\u0026amp; err != redis.Nil { return err } _, err = tx.Pipelined(func(pipe redis.Pipeliner) error { pipe.Set(key, n+1, 0) return nil }) return err }, key) 最后看一个官方文档中使用GET和SET命令以事务方式递增Key的值的示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 const routineCount = 100 increment := func(key string) error { txf := func(tx *redis.Tx) error { // 获得当前值或零值 n, err := tx.Get(key).Int() if err != nil \u0026amp;\u0026amp; err != redis.Nil { return err } // 实际操作（乐观锁定中的本地操作） n++ // 仅在监视的Key保持不变的情况下运行 _, err = tx.Pipelined(func(pipe redis.Pipeliner) error { // pipe 处理错误情况 pipe.Set(key, n, 0) return nil }) return err } for retries := routineCount; retries \u0026gt; 0; retries-- { err := rdb.Watch(txf, key) if err != redis.TxFailedErr { return err } // 乐观锁丢失 } return errors.New(\u0026#34;increment reached maximum number of retries\u0026#34;) } var wg sync.WaitGroup wg.Add(routineCount) for i := 0; i \u0026lt; routineCount; i++ { go func() { defer wg.Done() if err := increment(\u0026#34;counter3\u0026#34;); err != nil { fmt.Println(\u0026#34;increment error:\u0026#34;, err) } }() } wg.Wait() n, err := rdb.Get(\u0026#34;counter3\u0026#34;).Int() fmt.Println(\u0026#34;ended with\u0026#34;, n, err) 更多详情请查阅文档。\n","permalink":"https://ktzxy.top/posts/xqqps1xp7u/","summary":"Go操作Redis","title":"Go操作Redis"},{"content":" 函数调用在MySQL内部分为确定性函数和不确定性函数。如果一个函数，对于给定的固定参数值，多次调用，返回的结果值不同，那么这样的函数就称之为不确定性函数，比如RAND(), UUID()。返回的结果值相同，则为确定性函数，比如POW(1,2)。\n1. 确定性函数与不确定性函数的主要区别 先看一个例子，表结构如下：\nCREATE TABLE t (id INT NOT NULL PRIMARY KEY, col_a VARCHAR(100));\n两个查询SQL，第一个SQL使用确定性函数POW()，第二个SQL使用不确定性函数RAND()，如下：\n（1）SELECT * FROM t WHERE id = POW(1,2); （2）SELECT * FROM t WHERE id = FLOOR(1 + RAND() * 49);\n对于第一个SQL，POW(1,2) 函数返回的是一个常量值，因此这个SQL最多返回一条记录。 对于第二个SQL，RAND()是一个不确定函数，对于不确定函数，表t中的每一行记录与条件进行匹配时，都会对where条件的值重新进行计算，因此第二个SQL有可能返回0条，1条或者多条记录。 使用不确定性函数，无法走索引，大部分场景都是全表扫描，性能低下。 2. 不确定性函数的影响 不确定性函数不能返回一个常量值，优化器无法采用合适的优化策略，比如索引查找，导致全表扫描，影响性能。 在InnoDB表中，对于只有一行记录的匹配，不确定性函数可能会将单行的行锁升级成一个范围锁。 在基于statement的binlog格式复制时，使用不确定性函数会导致主从数据不一致。 3. 不确定性函数的优化 使用变量来存储不确定函数的返回值，优化器在处理变量时，因为其值已经确定，可以当作一个常量来处理。\nSET @keyval = FLOOR(1 + RAND() * 49); UPDATE t SET col_a = some_expr WHERE id = @keyval;\n将不确定函数转移到业务代码中，业务执行不确定函数，将返回的结果(常量)作为参数，拼接到SQL中，提高SQL性能。\n","permalink":"https://ktzxy.top/posts/xsfeqahdn7/","summary":"MySQL性能优化 函数调用优化","title":"MySQL性能优化 函数调用优化"},{"content":"vscode上传代码到仓库 |Git |Permission denied 错误问题描述 1 Starting ssh-agent on Windows 10 fails: \u0026#34;unable to start ssh-agent service, error :1058\u0026#34; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 OpenSSH_for_Windows_8.1p1, LibreSSL 3.0.2 debug1: Connecting to github.com [] port 22. debug1: Connection established. debug1: identity file C:\\\\Users\\\\zhaoyu/.ssh/id_rsa type 0 debug1: identity file C:\\\\Users\\\\zhaoyu/.ssh/id_rsa-cert type -1 debug1: identity file C:\\\\Users\\\\zhaoyu/.ssh/id_dsa type -1 debug1: identity file C:\\\\Users\\\\zhaoyu/.ssh/id_dsa-cert type -1 debug1: identity file C:\\\\Users\\\\zhaoyu/.ssh/id_ecdsa type -1 debug1: identity file C:\\\\Users\\\\zhaoyu/.ssh/id_ecdsa-cert type -1 debug1: identity file C:\\\\Users\\\\zhaoyu/.ssh/id_ed25519 type 3 debug1: identity file C:\\\\Users\\\\zhaoyu/.ssh/id_ed25519-cert type -1 debug1: identity file C:\\\\Users\\\\zhaoyu/.ssh/id_xmss type -1 debug1: identity file C:\\\\Users\\\\zhaoyu/.ssh/id_xmss-cert type -1 debug1: Local version string SSH-2.0-OpenSSH_for_Windows_8.1 ... debug1: Authentications that can continue: publickey debug1: No more authentication methods to try. git@github.com: Permission denied (publickey). 解决办法参考 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 PS E:\\myblog_hexo\\lanan\u0026gt; Get-Service ssh-agent Status Name DisplayName ------ ---- ----------- Stopped ssh-agent OpenSSH Authentication Agent PS E:\\myblog_hexo\\lanan\u0026gt; Get-Service ssh-agent | Select StartType StartType --------- Disabled PS E:\\myblog_hexo\\lanan\u0026gt; Get-Service -Name ssh-agent | Set-Service -StartupType Manual PS E:\\myblog_hexo\\lanan\u0026gt; Get-Service ssh-agent | Select StartType StartType --------- Manual ","permalink":"https://ktzxy.top/posts/ef4hfr6xaz/","summary":"Git |Permission denied","title":"Git |Permission denied"},{"content":"1.温度转换 1 2 3 4 5 6 7 8 9 10 11 12 #include\u0026lt;stdio.h\u0026gt; /* 有人用温度计测量出用华氏温度98°F，现在要求用C语言实现把它转换为以摄氏法表示的温度。 摄氏度等于九分之五乘以华氏度减去32的积 */ int main(){ float f_Degree,centigrade; // f_Degree表示华氏温度， centigrade表示摄氏法 f_Degree=98.0; centigrade=(5.0/9)*(f_Degree-32); printf(\u0026#34;华氏度98的摄氏度为：%f\\n\u0026#34;,centigrade); return 0; } 2.字母大小写转换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include\u0026lt;stdio.h\u0026gt; /* 大写字母转换为小写，小写字母转换为大写 */ int main(){ char ch; printf(\u0026#34;请输入一个字母：\u0026#34;); scanf(\u0026#34;%c\u0026#34;,\u0026amp;ch); if((ch\u0026lt;=\u0026#39;z\u0026#39; \u0026amp;\u0026amp; ch\u0026gt;=\u0026#39;a\u0026#39;) || (ch\u0026lt;=\u0026#39;Z\u0026#39; \u0026amp;\u0026amp; ch\u0026gt;=\u0026#39;A\u0026#39;) ){ if(ch\u0026lt;=\u0026#39;z\u0026#39; \u0026amp;\u0026amp; ch\u0026gt;=\u0026#39;a\u0026#39;){ ch-=32; printf(\u0026#34;%c\u0026#34;,ch); }else{ ch+=32; printf(\u0026#34;%c\u0026#34;,ch); } }else{ printf(\u0026#34;输入有误！\u0026#34;); } return 0; } 3.三目运算实现判断大写 1 2 3 4 5 6 7 8 9 10 11 12 13 #include\u0026lt;stdio.h\u0026gt; /* 输入一个字符，判别它是否为大写字母，如果是，将它转换成小写，如果不是，不转换。 然后输出最后得到的字符，要求使用三目运算符。 */ int main(){ char character_Big,character_Small; printf(\u0026#34;请输入字母：\u0026#34;); scanf(\u0026#34;%c\u0026#34;,\u0026amp;character_Big); character_Small=(character_Big\u0026lt;=\u0026#39;Z\u0026#39;\u0026amp;\u0026amp;character_Big\u0026gt;=\u0026#39;A\u0026#39;)?(character_Big+32):character_Big; printf(\u0026#34;%c\u0026#34;,character_Small); return 0; } 4.判断某年是否为闰年 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include\u0026lt;stdio.h\u0026gt; /* C语言实现判断某一年是否是闰年。 */ int main(){ int year; printf(\u0026#34;请输入年份：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;year); if((year%100!=0 \u0026amp;\u0026amp; year%4==0) || year%400==0){ printf(\u0026#34;%d是闰年\u0026#34;,year); }else{ printf(\u0026#34;%d是平年\u0026#34;,year); } return 0; } 5.求1+2+……+100的和 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include\u0026lt;stdio.h\u0026gt; /* C语言实现求1+2+3+……+100的和，要求分别用while、do while、for循环实现。 */ //for循环 int main() { int i,sum=0; for(i=1;i\u0026lt;101;i++) { sum=sum+i; } printf(\u0026#34;%d\u0026#34;,sum); return 0; } //while循环 int main() { int i=1,sum=0; while(i\u0026lt;101) { sum=sum+i; i=i+1; } printf(\u0026#34;%d\u0026#34;,sum); return 0; } //do while循环 int main() { int i=1,sum=0; do{ sum=sum+i; i=i+1; }while(i\u0026lt;101); printf(\u0026#34;%d\u0026#34;,sum); return 0; } 6.统计捐款人数及捐款 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include\u0026lt;stdio.h\u0026gt; /* 在全系1000个学生中，征集慈善捐款，当总数达到10万元时就结束， 统计此时的捐款人数，以及平均每人捐款的数目。 */ int main() { float amount,aver,total; float sum=100000; int i; for(i=0,total=0;i\u0026lt;1001;i++){ printf(\u0026#34;第%d个人捐款钱数：\u0026#34;,i+1); scanf(\u0026#34;%f\u0026#34;,\u0026amp;amount); total+=amount; if(total\u0026gt;sum) break; } aver=total/i; printf(\u0026#34;第%d个人捐款之后达到10万+\\n平均每人捐款：%5.2f\\n\u0026#34;,i,aver); return 0; } 7.100-200之间不能被3整除的数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include\u0026lt;stdio.h\u0026gt; /* C语言实现统计100~200之间的不能被3整除的数。 */ int main(){ int i; for(i=100;i\u0026lt;=200;i++){ if(i%3==0) continue; printf(\u0026#34;%d\\t\u0026#34;,i); } printf(\u0026#34;\\n\u0026#34;); return 0; } 8.输出4*5的矩阵 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include\u0026lt;stdio.h\u0026gt; /* C语言实现输出4*5的矩阵。 */ int main(){ for(int i=1;i\u0026lt;5;i++){ for(int j=1;j\u0026lt;6;j++){ printf(\u0026#34;%d\\t\u0026#34;,i*j); } printf(\u0026#34;\\n\u0026#34;); } return 0; } 9.输出斐波那契前30列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include\u0026lt;stdio.h\u0026gt; /* 求Fibonacci数列的前40个数。这个数列有以下特点：第1,2两个数为1,1,。 从第三个数开始，该数是其前两个数之和。（斐波那契不死神兔） */ int main(){ int row,i,f1,f2,f3; f1=1,f2=1; printf(\u0026#34;请输入行数：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;row); printf(\u0026#34;%d\\n%d\\n\u0026#34;,f1,f2); for(i=1;i\u0026lt;row-1;i++){ f3=f1+f2; printf(\u0026#34;%d\\n\u0026#34;,f3); f1=f2; f2=f3; } return 0; } 10.判断是否素数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include\u0026lt;stdio.h\u0026gt; /* C语言实现输入一个大于3的整数n，判断他是否为素数（质数）。 解题思路：本题采用的算法是，让n被i除，如果number能被2~（number-1）之中的任何一个整数整除， 则表示number肯定不是素数，不必再继续被后面的整数除，因此，可以提前结束循环。 */ int main(){ int i,number; printf(\u0026#34;请输入一个整数：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;number); for(i=2;i\u0026lt;=number-1;i++){ if(number%i==0) break; } if(i\u0026lt;number){ printf(\u0026#34;%d不是素数\u0026#34;,number); }else{ printf(\u0026#34;%d是素数\u0026#34;,number); } return 0; } 11. 素数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include\u0026lt;stdio.h\u0026gt; #include\u0026lt;math.h\u0026gt; /* C语言编程实现输出100~200之间的素数。 */ int main(){ int i,j,count; for(i=100;i\u0026lt;=200;i++){ for(j=2;j\u0026lt;sqrt(i);j++){ if(i%j==0) break; } if(i%j!=0){ count++; printf(\u0026#34;%d\\n\u0026#34;,i); } } printf(\u0026#34;total = %d\\n\u0026#34;,count); return 0; } //输入一个大于3的整数n，判定它是否为素数。 #include\u0026lt;stdio.h\u0026gt; /* 输入一个大于3的整数n，判定它是否为素数 */ int main(){ int i,n; printf(\u0026#34;请输入一个整数：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); for(i=2;i\u0026lt;n;i++){ if(n%i==0) break; } if(i\u0026lt;n) printf(\u0026#34;%d 不是素数\\n\u0026#34;,n); else printf(\u0026#34;%d 是素数\\n\u0026#34;,n); return 0; } //改进 #include\u0026lt;stdio.h\u0026gt; #include\u0026lt;math.h\u0026gt; /* 输入一个大于3的整数n，判定它是否为素数 */ int main(){ int i,n,k; printf(\u0026#34;请输入一个整数：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); k=sqrt(n); for(i=2;i\u0026lt;=k;i++){ if(n%i==0) break; } if(i\u0026lt;k) printf(\u0026#34;%d 不是素数\\n\u0026#34;,n); else printf(\u0026#34;%d 是素数\\n\u0026#34;,n); return 0; } 12.九九乘法表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include\u0026lt;stdio.h\u0026gt; /* C语言编程实现九九乘法表，样式要求,常规，长方形、右三角形、左三角形。 */ //常规 (左三角形) /* int main(){ for(int i=1;i\u0026lt;=9;i++){ for(int j=1;j\u0026lt;=i;j++){ printf(\u0026#34;%d*%d=%d\\t\u0026#34;,i,j,i*j); } printf(\u0026#34;\\n\u0026#34;); } return 0; } */ //长方形 /* int main(){ for(int i=1;i\u0026lt;=9;i++){ for(int j=1;j\u0026lt;=9;j++){ printf(\u0026#34;%d*%d=%2d\\t\u0026#34;,i,j,i*j); } printf(\u0026#34;\\n\u0026#34;); } return 0; } */ //右三角形 int main(){ for(int i=1;i\u0026lt;=9;i++){ for(int j=1;j\u0026lt;=9;j++){ if(j\u0026lt;i) { printf(\u0026#34; \u0026#34;); } else{ printf(\u0026#34;%d*%d=%2d \u0026#34;,i,j,i*j); } } printf(\u0026#34;\\n\u0026#34;); } return 0; } 13.求特定规律的和 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include\u0026lt;stdio.h\u0026gt; /* C语言实现求 （1+2+3….+100）+（1*1+2*2+….50*50）+（1/1+1/2+…1/10） */ int main(){ float sum1=0,sum2=0,sum3=0,sum; for(int i=1;i\u0026lt;=100;i++){ sum1+=i; } for(int j=1;j\u0026lt;=50;j++){ sum2+=j*j; } for(int k=1;k\u0026lt;=10;k++){ sum3+=1/k; } sum=sum1+sum2+sum3; printf(\u0026#34;（1+2+3…+100）+（1*1+2*2+…50*50）+（1/1+1/2+…+1/10）=\u0026#34;); printf(\u0026#34;%.2f\\n\u0026#34;,sum); return 0; } 14.打印菱形 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include\u0026lt;stdio.h\u0026gt; /* C语言实现打印菱形。 */ int main(){ int i,j,k; for(i=0;i\u0026lt;4;i++){ for(j=0;j\u0026lt;=2-i;j++){ printf(\u0026#34; \u0026#34;); } for(k=0;k\u0026lt;=2*i;k++){ printf(\u0026#34;*\u0026#34;); } printf(\u0026#34;\\n\u0026#34;); } for(i=0;i\u0026lt;=2;i++){ for(j=0;j\u0026lt;=i;j++){ printf(\u0026#34; \u0026#34;); } for(k=0;k\u0026lt;=4-2*i;k++){ printf(\u0026#34;*\u0026#34;); } printf(\u0026#34;\\n\u0026#34;); } return 0; } 15.冒泡排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include\u0026lt;stdio.h\u0026gt; /* C语言实现从小到大对10个数进行排序，要求使用冒泡排序实现。 */ int main(){ int arr[10]; int temp,i,j; printf(\u0026#34;请输入十个数:\u0026#34;); for(i=0;i\u0026lt;10;i++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;arr[i]); } //排序 for(i=0;i\u0026lt;9;i++){ for(j=0;j\u0026lt;9-i;j++){ if(arr[j]\u0026gt;arr[j+1]){ //如果前一个数比后一个数大 temp=arr[j]; //把小的数赋值给前面，大的数赋值给后面 arr[j]=arr[j+1]; arr[j+1]=temp; } } } printf(\u0026#34;按照从小到大的顺序排序:\u0026#34;); for(i=0;i\u0026lt;10;i++){ printf(\u0026#34;%d \u0026#34;,arr[i]); } printf(\u0026#34;\\n\u0026#34;); return 0; } 16.选择排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include\u0026lt;stdio.h\u0026gt; #define N 10 /* 选择法排序 */ int main(){ void sort(int arr[N],int n); int a[N]; printf(\u0026#34;请输入十个数:\u0026#34;); for(int i=0;i\u0026lt;N;i++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;a[i]); } sort(a,N); printf(\u0026#34;按照从小到大的顺序排序:\u0026#34;); for(int i=0;i\u0026lt;N;i++){ printf(\u0026#34;%d \u0026#34;,a[i]); } return 0; } void sort(int arr[N],int n){ int i,j,k,t; for(i=0;i\u0026lt;n-1;i++){ k=i; for(j=i+1;j\u0026lt;n;j++){ if(arr[j]\u0026lt;arr[k]) k=j; } t=arr[k]; arr[k]=arr[i]; arr[i]=t; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include\u0026lt;stdio.h\u0026gt; /* 用选择法对数组中10个整数按由小到大排序 所谓选择法就是先将10个数中最小的数与a[0]对换；再将a[1]~a[9]中最小的数与a[1]对换...每比较一轮， 找出一个未经排序的数中最小的一个。共比较9轮 */ int main(){ int a[10]; int i,j,k,t; for(int i=0;i\u0026lt;10;i++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;a[i]); } for(i=0;i\u0026lt;9;i++){ k=i; for(j=i+1;j\u0026lt;10;j++){ if(a[j]\u0026lt;a[k]) k=j; } t=a[k]; a[k]=a[i]; a[i]=t; } for(i=0;i\u0026lt;10;i++){ printf(\u0026#34;%d \u0026#34;,a[i]); } return 0; } 17.二维数组行列元素互换 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include\u0026lt;stdio.h\u0026gt; /* C语言实现将一个二维数组行和列的元素互换，存到另一个二维数组中。 */ int main(){ int a[2][3]={{1,2,3},{4,5,6}}; int b[3][2]; int i,j; printf(\u0026#34;横向数组的序列：\\n\u0026#34;); for(i=0;i\u0026lt;2;i++){ for(j=0;j\u0026lt;3;j++){ printf(\u0026#34;%d \u0026#34;,a[i][j]); b[j][i]=a[i][j]; } printf(\u0026#34;\\n\u0026#34;); } printf(\u0026#34;纵向数组的序列:\\n\u0026#34;); for(i=0;i\u0026lt;3;i++){ for(j=0;j\u0026lt;2;j++){ printf(\u0026#34;%d \u0026#34;,b[i][j]); } printf(\u0026#34;\\n\u0026#34;); } return 0; } 18.求3*4的矩阵最大数及行号列号 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include\u0026lt;stdio.h\u0026gt; /* C语言实现求3*4的矩阵中制最大的那个元素的值，以及其所在的行号列号。 */ int main(){ int row=0,colum=0; int a[3][4]={{1,2,3,4},{9,8,7,6},{-10,10,-5,2}}; int max=a[0][0]; for(int i=0;i\u0026lt;3;i++){ for(int j=0;j\u0026lt;4;j++){ if(a[i][j]\u0026gt;max) { max=a[i][j]; row = i; colum = j; } } } printf(\u0026#34;max=%d\\nrow=%d\\ncolum=%d\\n\u0026#34;,max,row,colum); return 0; } 19. C语言实现输出杨辉三角。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include\u0026lt;stdio.h\u0026gt; /* C语言实现输出杨辉三角。 */ int main(){ int i,j; //定义二维数组 int a[10][10]; for(i=0;i\u0026lt;10;i++){ a[i][i]=1; //给二维数组的每一行的最后一个赋值为1 a[i][0]=1; //第二维数组的每一行的开头赋值为1 } for(i=2;i\u0026lt;10;i++){ for(j=1;j\u0026lt;=i-1;j++){ a[i][j]=a[i-1][j-1]+a[i-1][j]; } } for(i=0;i\u0026lt;10;i++){ for(j=0;j\u0026lt;=i;j++){ printf(\u0026#34;%6d\u0026#34;,a[i][j]); } printf(\u0026#34;\\n\u0026#34;); } return 0; } 20.函数实现比大小 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include\u0026lt;stdio.h\u0026gt; /* 输入两个整数，要求输出其中值较大者。要求用函数来找到大数。 */ int main(){ int max(int x,int y); int a,b; printf(\u0026#34;请输入两个整数：\\n\u0026#34;); scanf(\u0026#34;%d,%d\u0026#34;,\u0026amp;a,\u0026amp;b); printf(\u0026#34;max=%d\\n\u0026#34;,max(a,b)); return 0; } int max(int x,int y){ return x\u0026gt;y?x:y; } 21.函数求年龄 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include\u0026lt;stdio.h\u0026gt; /* 有5个美女坐在一起，问第5个美女多少岁，她说比第4个美女大2岁；问第4个美女多少岁，她说比第3个美女大2岁； 问第3个美女多少岁，她说比第2个美女大2岁；问第2个美女多少岁，她说比第一个大2岁。 最后问第1个美女，她说10岁。请问第2、3、4、5个美女多少岁？要求用C语言编程实现。 */ int main(){ int age(int temp); int number; printf(\u0026#34;输入想知道的第几个孩子：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;number); printf(\u0026#34;第%d个学生的年龄是%d岁\\n\u0026#34;,number,age(number)); return 0; } int age(int temp){ return (temp==1)?10:age(temp-1)+2; } 22.递归求n的阶乘 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include\u0026lt;stdio.h\u0026gt; /* C语言求n！，要求用递归实现。 */ int main(){ int factorial(int number); int number; printf(\u0026#34;输入要求阶乘的数：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;number); printf(\u0026#34;%d!=%d\u0026#34;,number,factorial(number)); return 0; } int factorial(int number){ int temp; if(number\u0026lt;0){ printf(\u0026#34;错误数据请，输入大于0的数！\u0026#34;); }else if(number==0 || number==1){ temp=1; }else{ temp=factorial(number-1)*number; } return temp; } **23.求平均分及第n个人成绩 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include\u0026lt;stdio.h\u0026gt; /* 有一个班，3个学生，各学习4门课，C语言编程实现计算总平均分数以及第n个学生的成绩，要求使用指针。 */ int main(){ void average(float *p,int n); void search_Grade(float (*p)[4],int n); float score[3][4]={{70,86,93,80},{85,90,75,82},{90,100,96,98}}; average(*score,12); search_Grade(score,2); return 0; } //自定义求平均成绩函数 void average(float *p,int n){ float *p1; float sum=0,aver; p1=p+n-1; for(;p\u0026lt;=p1;p++){ sum+=(*p); } aver=sum/n; printf(\u0026#34;平均数是:%.2f\u0026#34;,aver); printf(\u0026#34;\\n\u0026#34;); } //自定义求第n个学生成绩函数 void search_Grade(float (*p)[4],int n){ printf(\u0026#34;第%d个学生的成绩是:\u0026#34;,n+1); for(int i=0;i\u0026lt;4;i++){ printf(\u0026#34;%5.2f \u0026#34;,*(*(p+n)+i)); } } **24.输出平均成绩最高学生的信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include\u0026lt;stdio.h\u0026gt; #define N 3 /* 有n个结构体变量，内含学生的学号，学号，和三门成绩。要求输出平均成绩最高学生的信息 （包括学号、姓名、三门课程成绩和平均成绩）n个学生的成绩，要求使用指针。 */ struct Student{ int num; char name[20]; float score[N]; float aver; }; int main(){ void input(struct Student s[]); struct Student max(struct Student s[]); void print(struct Student stu); struct Student s[N],*p=s; input(p); print(max(p)); return 0; } //自定义输入函数 void input(struct Student s[]){ printf(\u0026#34;请输入各学生的信息：学号、姓名、三门课成绩:\\n\u0026#34;); for(int i=0;i\u0026lt;N;i++){ scanf(\u0026#34;%d %s %f %f %f\u0026#34;,\u0026amp;s[i].num,s[i].name,\u0026amp;s[i].score[0],\u0026amp;s[i].score[1],\u0026amp;s[i].score[2]); s[i].aver=(s[i].score[0]+s[i].score[1]+s[i].score[2])/N; } } //自定义求最大值 struct Student max(struct Student s[]){ int i,m=0; for(i=0;i\u0026lt;N;i++){ if(s[i].aver\u0026gt;s[m].aver) m=i; } return s[m]; }; //自定义打印函数 void print(struct Student stu){ printf(\u0026#34;\\n成绩最高的学生是：\\n\u0026#34;); printf(\u0026#34;学号；%d\\n姓名；%s\\n三门课成绩：%5.1f,%5.1f,%5.1f\\n平均成绩：%6.2f\\n\u0026#34;, stu.num,stu.name,stu.score[0],stu.score[1],stu.score[2],stu.aver); } 25.用指针字符串a复制为b并输出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include\u0026lt;stdio.h\u0026gt; /* C语言实现将字符串a复制为b，然后输出b，要求使用指针。 */ int main(){ char a[]=\u0026#34;I Love You\u0026#34;; char b[20]; int i; for(i=0;*(a+i)!=\u0026#39;\\0\u0026#39;;i++){ *(b+i)=*(a+i); } *(b+i)=\u0026#39;\\0\u0026#39;; printf(\u0026#34;字符串a是:%s\\n\u0026#34;,a); printf(\u0026#34;单个输出字符b：\u0026#34;); for(i=0;b[i]!=\u0026#39;\\0\u0026#39;;i++){ printf(\u0026#34;%c\u0026#34;,*(b+i)); } printf(\u0026#34;\\n\u0026#34;); return 0; } 26.统计投票的结果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include\u0026lt;stdio.h\u0026gt; #include\u0026lt;string.h\u0026gt; /* 有三个候选人，每个选民只能投给一个人，要求用C语言编一个统计选票的程序， 先后输入备选人的的名字，最后输出各人的得票结果。 */ struct People{ char name[20]; int number; }leader[3]={{\u0026#34;zhang\u0026#34;,0},{\u0026#34;li\u0026#34;,0},{\u0026#34;sun\u0026#34;,0}}; int main(){ int i,j; char leader_name[20]; for(i=0;i\u0026lt;10;i++){ printf(\u0026#34;请输入人名\\n\u0026#34;); scanf(\u0026#34;%s\u0026#34;,leader_name); for(j=0;j\u0026lt;3;j++){ if(strcmp(leader_name,leader[j].name)==0){ leader[j].number++; } } } printf(\u0026#34;结果是：\\n\u0026#34;); for(i=0;i\u0026lt;3;i++){ printf(\u0026#34;%s票数：%d\\n\u0026#34;,leader[i].name,leader[i].number); } return 0; } 27.按成绩高低输出学生信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include\u0026lt;stdio.h\u0026gt; /* 有n个学生的信息（包括学号、姓名、成绩），C语言编程实现按照成绩的高低顺序输出学生的信息。 */ struct Student{ int num; char name[10]; float score; }; int main(){ struct Student stu[5]={{10010,\u0026#34;Tom\u0026#34;,78},{10011,\u0026#34;Jon\u0026#34;,98.5},{10012,\u0026#34;Lisi\u0026#34;,100},{10013,\u0026#34;zhangsan\u0026#34;,99},{10014,\u0026#34;wangwu\u0026#34;,10}}; struct Student t; int i,j,k; printf(\u0026#34;成绩由大到小排序：\\n\u0026#34;); //用选择法排序 for(i=0;i\u0026lt;4;i++){ k=i; for(j=i+1;j\u0026lt;5;j++){ if(stu[j].score\u0026gt;stu[k].score) k=j; } t=stu[k]; stu[k]=stu[i]; stu[i]=t; } for(i=0;i\u0026lt;5;i++){ printf(\u0026#34;%d,%10s,%6.2f分\\n\u0026#34;,stu[i].num,stu[i].name,stu[i].score); } return 0; } 28.指向结构体变量的指针变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include\u0026lt;stdio.h\u0026gt; #include\u0026lt;string.h\u0026gt; /* C语言实现通过指向结构体变量的指针变量变量输出结构体变量中的信息。 */ struct Student{ int num; char name[10]; char sex; float score; }; int main(){ struct Student stu; struct Student *p; p=\u0026amp;stu; stu.num=10010; strcpy(stu.name,\u0026#34;zhangsan\u0026#34;); stu.sex=\u0026#39;M\u0026#39;; stu.score=100; printf(\u0026#34;学号是：%d\\n名字是%s\\n性别是：%c\\n成绩是：%f\\n\u0026#34;,stu.num,stu.name,stu.sex,stu.score); printf(\u0026#34;\\n\u0026#34;); printf(\u0026#34;学号是：%d\\n名字是%s\\n性别是：%c\\n成绩是：%f\\n\u0026#34;,(*p).num,(*p).name,(*p).sex,(*p).score); printf(\u0026#34;\\n\u0026#34;); printf(\u0026#34;学号是：%d\\n名字是%s\\n性别是：%c\\n成绩是：%f\\n\u0026#34;,p-\u0026gt;num,p-\u0026gt;name,p-\u0026gt;sex,p-\u0026gt;score); return 0; } 29.输入字符串直到输入#为止 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include\u0026lt;stdio.h\u0026gt; #include\u0026lt;stdlib.h\u0026gt; /* C语音实现从键盘输入一些字符，逐个把他们送到磁盘上去，直到用户输入一个“#”为止。 */ int main(){ FILE *fp; char ch,filename[10]; printf(\u0026#34;请输入所用的文件名:\u0026#34;); scanf(\u0026#34;%s\u0026#34;,filename); if((fp=fopen(filename,\u0026#34;w\u0026#34;))==NULL){ printf(\u0026#34;cannot open file.\\n\u0026#34;); exit(0); } ch=getchar(); printf(\u0026#34;请输入一个准备存储到磁盘的字符串（以#结束）：\u0026#34;); ch=getchar(); while(ch!=\u0026#39;#\u0026#39;){ fputc(ch,fp); putchar(ch); ch=getchar(); } fclose(fp); putchar(10); return 0; } 30.最大公约数最小公倍数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include\u0026lt;stdio.h\u0026gt; /* C语言编程实现求两个数的最大公约数和最小公倍数 解题思路：最大公因数，也称最大公约数、最大公因子， 指两个或多个整数共有约数中最大的一个； 最小公倍数是指两个或多个整数公有的倍数叫做它们的公倍数， 其中除0以外最小的一个公倍数就叫做这几个整数的最小公倍数。 最小公倍数=两整数的乘积÷最大公约数 ， 所以怎么求最大公约数是关键。 */ int main(){ int m,n,num1,num2,temp; printf(\u0026#34;请输入两个数：\u0026#34;); scanf(\u0026#34;%d,%d\u0026#34;,\u0026amp;num1,\u0026amp;num2); m=num1; n=num2; while(num2!=0){ temp=num1%num2; num1=num2; num2=temp; } printf(\u0026#34;最大公约数是：%d\\n\u0026#34;,num1); printf(\u0026#34;最小公倍数是：%d\\n\u0026#34;,m*n/num1); return 0; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include\u0026lt;stdio.h\u0026gt; /* 求最大公约数： 其算法过程为：设两数为a,b设其中a 做被除数,b做除数， temp为余数 1、大数放a中、小数放b中； 2、求a/b的余数； 3、若temp=0则b为最大公约数； 4、如果temp!=0则把b的值给a、temp的值给b； 5、返回第二步； 求最小公倍数： 一个简单的方法直接求：a*b/最大公约数 */ int main(){ int divisor(int a,int b); int multiple(int a,int b); int a,b; printf(\u0026#34;请输入两个整数：\u0026#34;); scanf(\u0026#34;%d,%d\u0026#34;,\u0026amp;a,\u0026amp;b); printf(\u0026#34;最小公倍数:%d 最大公约数:%d\u0026#34;, multiple(a,b),divisor(a,b)); return 0; } //辗转相除法函数嵌套求两数的最大公约数 int divisor(int a,int b){ int temp; if(a\u0026lt;b){ temp=a; a=b; b=temp; } while(b!=0){ //通过循环求两数的余数，直到余数为0 temp=a%b; a=b; b=temp; } return a; } //辗转相除法函数嵌套求两数的最小公倍数 int multiple(int a,int b){ int divisor(int a,int b); int temp; temp=divisor(a,b); //再次调用自定义函数，求出最大公约数 return (a*b/temp); //返回最小公倍数到主调函数处进行输出 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include\u0026lt;stdio.h\u0026gt; /* 穷举法（也叫枚举法）的基本思想是根据题目的部分条件确定 答案的大致范围，并在此范围内对所有可能的情况逐一验证，直到 全部情况验证完毕。若某个情况验证符合题目的全部条件，则为本 问题的一个解；若全部情况验证后都不符合题目的全部条件，则本 题无解。 解题思想：从两个数中较小数开始由大到小列举约数，直到找 到公约数立即中断列举，得到的公约数便是最大公约数 。 解题步骤： 1、求最大公约数 对两个正整数a,b如果能在区间[a,0]或[b,0]内能找到一个整 数temp能同时被a和b所整除，则temp即为最大公约数。 2、求最小公倍数 对两个正整数a,b,如果若干个a之和或b之和能被b所整除或能 被a所整除，则该和数即为所求的最小公倍数。 */ int main(){ int divisor(int a,int b); int multiple(int a,int b); int a,b; printf(\u0026#34;请输入两个整数：\u0026#34;); scanf(\u0026#34;%d,%d\u0026#34;,\u0026amp;a,\u0026amp;b); printf(\u0026#34;最小公倍数:%d 最大公约数:%d\u0026#34;, multiple(a,b),divisor(a,b)); return 0; } //穷举法求两数的最大公约数 int divisor(int a,int b){ int temp; temp=(a\u0026gt;b)?a:b; while(temp\u0026gt;0){ if(a%temp==0 \u0026amp;\u0026amp; b%temp==0) break; //只要找到一个数能同时被a,b所整除，则中止循环 temp--; //如不满足if条件则变量自减，直到能被a,b所整除 } return temp; } //穷举法求两数的最小公倍数 int multiple(int a,int b){ int p,q,temp; p=(a\u0026gt;b)?a:b; q=(a\u0026gt;b)?b:a; temp=p; //最大值赋给p为变量自增作准备 while(1){ //利用循环语句来求满足条件的数值 if(p%q==0) break; //只要找到变量的和数能被a或b所整除，则中止循环 p+=temp; //如果条件不满足则变量自身相加 } return p; } 31.求圆周长 面积 表面积 体积 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #include\u0026lt;stdio.h\u0026gt; #define PI 3.14 /* C语言编程求圆周长、圆面积、圆球表面积、圆球体积、圆柱体积。 解题思路：就是简单的数学公式套用， 圆周长公式=2πr，圆面积=πr2，圆球表面积=4πr2， 圆球体积=4πR3 /3，圆柱体积=πr2h。 */ int main(){ float r,h; float perimeter; //圆周长 float area; //圆面积 float sphere_Surface_Area;//圆球表面积 float sphere_Volume;//圆球体积 float cylinder_Volume;//圆柱体积 printf(\u0026#34;输入圆半径r，圆柱高h：\u0026#34;); scanf(\u0026#34;%f,%f\u0026#34;,\u0026amp;r,\u0026amp;h); perimeter=2*PI*r; area=PI*r*r; sphere_Surface_Area=4*PI*r*r; sphere_Volume=(4*PI*r*r*r)/3; cylinder_Volume=(PI*r*r)*h; printf(\u0026#34;周长=%3.1f\\n\u0026#34;,perimeter); printf(\u0026#34;圆面积=%3.1f\\n\u0026#34;,area); printf(\u0026#34;圆球表面积=%3.1f\\n\u0026#34;,sphere_Surface_Area); printf(\u0026#34;圆球体积=%3.1f\\n\u0026#34;,sphere_Volume); printf(\u0026#34;圆柱体积=%3.1f\\n\u0026#34;,cylinder_Volume); return 0; } 32.统计字符中英文 空格 数字 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include\u0026lt;stdio.h\u0026gt; /* 输入一行字符，C语言编程分别统计出其中英文字母、空格、数字和其他字符的个数。 */ int main(){ char input_Character; int letters=0,space=0,digit=0,other=0; printf(\u0026#34;请输入一行字符：\u0026#34;); while((input_Character=getchar())!=\u0026#39;\\n\u0026#39;){ if((input_Character\u0026lt;=\u0026#39;z\u0026#39; \u0026amp;\u0026amp; input_Character\u0026gt;=\u0026#39;a\u0026#39;) || (input_Character\u0026lt;=\u0026#39;Z\u0026#39; \u0026amp;\u0026amp; input_Character\u0026gt;=\u0026#39;A\u0026#39;)) letters++; else if(input_Character==\u0026#39; \u0026#39;) space++; else if(input_Character\u0026lt;=\u0026#39;9\u0026#39; \u0026amp;\u0026amp; input_Character\u0026gt;=\u0026#39;0\u0026#39;) digit++; else other++; } printf(\u0026#34;字母：%d个\\n\u0026#34;,letters);//输出字母个数 printf(\u0026#34;空格：%d个\\n\u0026#34;,space);//输出空格个数 printf(\u0026#34;数字：%d个\\n\u0026#34;,digit);//输出数字个数 printf(\u0026#34;其他字符：%d个\\n\u0026#34;,other);//输出其他字符个数 return 0; } 33.求1！+2！+3！+\u0026hellip;20! 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include\u0026lt;stdio.h\u0026gt; /* C语言编程求1！+2！+3！+...20! */ int main(){ double sum=0,temp=1; for(int i=1;i\u0026lt;=20;i++){ temp*=i; sum+=temp; } printf(\u0026#34;结果：%22.15e\\n\u0026#34;,sum); return 0; } 34.输出水仙花数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include\u0026lt;stdio.h\u0026gt; /* C语言编程输出100-1000之间所有的“水仙花数”，所谓的“水仙花数”是指一个3位数，其各位数字立方和等于该数本身。 */ int main(){ int i,j,k,n; printf(\u0026#34;水仙花数是：\\n\u0026#34;); for(n=100;n\u0026lt;1000;n++){ i=n/100; j=n/10%10; k=n%10; if(n==(i*i*i+j*j*j+k*k*k)){ printf(\u0026#34;%d\\t\u0026#34;,n); } } return 0; } 35.求完数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include\u0026lt;stdio.h\u0026gt; /* 一个数如果恰好等于它的因子之和，这个数就称为完数， C语言编程找出1000之内的所有完数，并输出其因子。 解题思路：6的因子为1,2,3，而6=1+2+3，因此6是“完数”， 1不用判断，直接从2开始，因为1的因子只有1 */ int main(){ int i,j,s,n; printf(\u0026#34;上限：\\n\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); for(i=2;i\u0026lt;=n;i++){ s=0; for(j=1;j\u0026lt;i;j++){ if(i%j==0) s+=j;\t} if(s==i) printf(\u0026#34;%d\\t\u0026#34;,i); } return 0; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include\u0026lt;stdio.h\u0026gt; /* 一个数如果恰好等于它的因子之和，这个数就称为完数， C语言编程找出1000之内的所有完数，并输出其因子。 解题思路：6的因子为1,2,3，而6=1+2+3，因此6是“完数”， 1不用判断，直接从2开始，因为1的因子只有1 */ int main(){ int n,s,i; for(n=2;n\u0026lt;1000;n++){ s=0; for(i=1;i\u0026lt;n;i++){ if(n%i==0) s+=i; } if(s==n){ printf(\u0026#34;%d的因子为：\u0026#34;,n); for(i=1;i\u0026lt;n;i++){ if(n%i==0) printf(\u0026#34;%d \u0026#34;,i); } printf(\u0026#34;\\n\u0026#34;); } } return 0; } 36.求某个数列前20项和 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include\u0026lt;stdio.h\u0026gt; /* 有一个分数列：2/1,3/2,5/3，8/5,13/8,21/13...， C语言编程求出这个数列的前20项之和。 */ int main(){ int i; double a=2,b=1,sum=0,temp; for(i=1;i\u0026lt;=20;i++){ sum=sum+a/b; temp=a; a+=b; b=temp; } printf(\u0026#34;sum=%7.7f\\n\u0026#34;,sum); return 0; } 37.自由落地 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include\u0026lt;stdio.h\u0026gt; /* 一个球从100m高度自由落下，每次落地后反跳回原高度的一半，再落下，再反弹。 C语言编程求它在第10次落地时，共经过多少米，第10次反弹多高。 */ int main(){ double height,bounce_Height; height=100; bounce_Height=height/2; for(int i=2;i\u0026lt;=10;i++){ height=height+2*bounce_Height; bounce_Height=bounce_Height/2; } printf(\u0026#34;第10次落地时共经过%f米\\n\u0026#34;,height); printf(\u0026#34;第10次反弹%f米\\n\u0026#34;,bounce_Height); return 0; } 38.猴子吃桃 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include\u0026lt;stdio.h\u0026gt; /* 猴子吃桃问题。猴子第1天摘下若干个桃子，当即吃了一半，还不过瘾，又多吃了一个。 第2天早上又将剩下的桃子吃掉一半，又多吃了一个， 以后每天早上都吃了前一天剩下的一半零一个，到第10天早上想再吃时， 就只剩下一个桃子了。C语言编程求第1天共摘了多少个桃子。 */ int main(){ int day,day_1,day_2; day=9; day_2=1; while(day--\u0026gt;0){ day_1=(day_2+1)*2; day_2=day_1; } printf(\u0026#34;第一天共摘了%d个桃子\\n\u0026#34;,day_1); return 0; } 39. 找出3对赛手的名单 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include\u0026lt;stdio.h\u0026gt; /* 两个乒乓球队进行比赛，各出3个人。甲队为A，B，C，3人，乙对为X，Y，Z，3人， 已抽签决定比赛名单。有人向队员打听比赛的名单，A说他不和X比赛，C说他不和X，Z比赛， C语言编程程序找出3对赛手的名单。 */ int main(){ char i,j,k; for(i=\u0026#39;X\u0026#39;;i\u0026lt;=\u0026#39;Z\u0026#39;;i++){ for(j=\u0026#39;X\u0026#39;;j\u0026lt;=\u0026#39;Z\u0026#39;;j++){ if(i!=j){ for(k=\u0026#39;X\u0026#39;;k\u0026lt;=\u0026#39;Z\u0026#39;;k++){ if(i!=k \u0026amp;\u0026amp; j!=k){ if(i!=\u0026#39;X\u0026#39; \u0026amp;\u0026amp; k!=\u0026#39;X\u0026#39;\u0026amp;\u0026amp;k!=\u0026#39;Z\u0026#39;){ printf(\u0026#34;A--%c\\nB--%c\\nC--%c\\n\u0026#34;,i,j,k); } } } } } } return 0; } 40.求矩阵对角线元素和 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include\u0026lt;stdio.h\u0026gt; /* C语言求3*3的整型矩阵对角线元素之和 。 */ int main(){ int a[3][3],sum=0; int i,j; printf(\u0026#34;输入数据：\\n\u0026#34;); for(i=0;i\u0026lt;3;i++){ for(j=0;j\u0026lt;3;j++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;a[i][j]); } } sum=a[0][0]+a[1][1]+a[2][2]+a[0][2]+a[1][1]+a[2][0]; printf(\u0026#34;对角线元素之和为%d\\n\u0026#34;,sum); return 0; } **41.向数组中插入数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include\u0026lt;stdio.h\u0026gt; /* 有一个已经排好序的数组，要求C语言实现输入一个数后，按原来排序的规律将它插入数组中。 解题思路：假设数组a有n个元素，而且已按升序排列，在插入一个数时按以下方法处理： 如果插入的数num比a数组最后一个数大，则将插入的数放在a数组末尾。 如果插入的数num不比a数组最后一个数大，则将它依次和a[0]~a[n-1]比较， 直到出现a[i]\u0026gt;num为止，这时表示a[0]~a[i-1]各元素的值比num小， a[i]~a[n-1]各元素的值比num大。 */ int main(){ int a[11]={1,4,6,9,13,16,19,28,40,100}; int t1,t2,num,end,i,j; printf(\u0026#34;原来的输出：\\n\u0026#34;); for(i=0;i\u0026lt;10;i++){ //注意这里的10 printf(\u0026#34;%d \u0026#34;,a[i]); } printf(\u0026#34;\\n\u0026#34;); printf(\u0026#34;输入要插入的数：\\n\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;num); end=a[9]; //将最后一个数赋值给end if(num\u0026gt;end){ //先和最后一个数比大小 a[10]=num; }else{ //小于的话，依次比较，直到比插入的数大 for(i=0;i\u0026lt;10;i++){ if(a[i]\u0026gt;num){ t1=a[i]; a[i]=num; for(j=i+1;j\u0026lt;11;j++){ t2=a[j]; a[j]=t1; t1=t2; } break; } } } printf(\u0026#34;插入之后排序：\\n\u0026#34;); for(i=0;i\u0026lt;11;i++){ printf(\u0026#34;%d \u0026#34;,a[i]); } printf(\u0026#34;\\n\u0026#34;); return 0; } 42.魔方矩阵 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include\u0026lt;stdio.h\u0026gt; /* C语言实现输出“魔方阵”。所谓魔方阵是指它的每一行，每一列和对角线之和均相等。 解题思路：魔方阵中各数的排列规律，魔方阵的阶数应该为奇数。 将1放在第1行中间一列 从2开始直到n*n止各数依次按下：每一个数存放的行比前一个数的行数减1，列数加1. 如果上一数的行为为1，则下一个数的行数为n 当上一个数的列数为n时，下一个数的列数应为1，行数减1 按上面的规则确定的位置上已有数，或上一个数是第1行第n列时，则把下一个数放在上一个数的下面 */ int main(){ int a[20][20]={0}; int i,j,k,n; i=1; printf(\u0026#34;请输入阶数为1~15之间的奇数：\\n\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); //输入魔方阵的维度n j=n/2+1; // j是维度的一半加1 a[i][j]=1; //确定第一排的中间一个数为1 for(k=2;k\u0026lt;=n*n;k++){ //已经确定1的位置了，再循环确定2~n*n的位置 i=i-1; //挪位，竖排往上挪一位 j=j+1; //挪位，横排往右挪一位 if((i\u0026lt;=0)\u0026amp;\u0026amp;(j\u0026lt;=n)) i=n; //如果竖排挪到顶，同时横排还没有超过最右，竖排就到从最下再继续。 if((i\u0026lt;=0)\u0026amp;\u0026amp;(j\u0026gt;n)) i=i+2,j=j-1; //如果竖排挪到顶，同时横排超过最右，竖排往下挪两位，横排往左移一位。 if(j\u0026gt;n) j=1; //如果只有横排超过最右，横排挪到左边第二行。 if(a[i][j]==0) a[i][j]=k; //如果这个位置还没有赋值，那么赋值为k else i+=2,j-=1,a[i][j]=k; //已经赋值过了。那么竖排往下挪两位，横排往左移一位，再赋值为k } for(i=1;i\u0026lt;=n;i++){ //循环输出 for(j=1;j\u0026lt;=n;j++){ printf(\u0026#34;%3d \u0026#34;,a[i][j]); } printf(\u0026#34;\\n\u0026#34;); } return 0; } 43. 统计一段话中的字符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include\u0026lt;stdio.h\u0026gt; /* 有一篇文章，共有3行文字，每行有80个字符。 C语言编程实现分别统计出其中英文大写字母、小写字母、数字、空格以及其他字符的个数 在该列上最小。也可能没有鞍点。 */ int main(){ int i,j,capital=0,lower=0,number=0,space=0,other=0; char text[3][80]; for(i=0;i\u0026lt;3;i++){ printf(\u0026#34;请随意输入一行：\\n\u0026#34;); gets(text[i]); for(j=0;j\u0026lt;80\u0026amp;\u0026amp;text[i][j]!=\u0026#39;\\0\u0026#39;;j++){ if(text[i][j]\u0026gt;=\u0026#39;A\u0026#39;\u0026amp;\u0026amp;text[i][j]\u0026lt;=\u0026#39;Z\u0026#39;) capital++; else if(text[i][j]\u0026gt;=\u0026#39;a\u0026#39;\u0026amp;\u0026amp;text[i][j]\u0026lt;=\u0026#39;z\u0026#39;) lower++; else if(text[i][j]\u0026gt;=\u0026#39;0\u0026#39;\u0026amp;\u0026amp;text[i][j]\u0026lt;=\u0026#39;9\u0026#39;) number++; else if(text[i][j]==\u0026#39; \u0026#39;) space++; else other++; } } printf(\u0026#34;\\n输出结果：\\n\u0026#34;); printf(\u0026#34;大写字母 :%d\\n\u0026#34;,capital); printf(\u0026#34;小写字母 :%d\\n\u0026#34;,lower); printf(\u0026#34;数字 :%d\\n\u0026#34;,number); printf(\u0026#34;空格 :%d\\n\u0026#34;,space); printf(\u0026#34;其他字符 :%d\\n\u0026#34;,other); return 0; } **44.将密码译回原文 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include\u0026lt;stdio.h\u0026gt; /* 有一行电文，已按下面规律译成密码：A-\u0026gt;Z a-\u0026gt;z;B-\u0026gt;Y b-\u0026gt;y;即第1个字母变成第26个字母，第i个字母变成第（26-i+1）个字母，非字母字符不变。要求C语言编程将密码译回原文，并输出密码和原文。 */ int main(){ int j,n; char ch[80],tran[80]; printf(\u0026#34;输入密码：\\n\u0026#34;); gets(ch); printf(\u0026#34;\\n密码是：\\n%s\u0026#34;,ch); j=0; while(ch[j]!=\u0026#39;\\0\u0026#39;){ if((ch[j]\u0026gt;=\u0026#39;A\u0026#39;)\u0026amp;\u0026amp;(ch[j]\u0026lt;=\u0026#39;Z\u0026#39;)) tran[j]=155-ch[j]; else if((ch[j]\u0026gt;=\u0026#39;a\u0026#39;)\u0026amp;\u0026amp;(ch[j]\u0026lt;=\u0026#39;z\u0026#39;)) tran[j]=219-ch[j]; else tran[j]=ch[j]; j++; } n=j; printf(\u0026#34;\\n输出原文：\\n\u0026#34;); for(j=0;j\u0026lt;n;j++){ putchar(tran[j]); } printf(\u0026#34;\\n\u0026#34;); return 0; } 45.拼接字符串 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include\u0026lt;stdio.h\u0026gt; /* C语言编写一个程序，将两个字符串连接起来，不要用strcat函数 */ int main(){ char str1[80],str2[80]; int i=0,j=0; printf(\u0026#34;输入字符串1：\u0026#34;); scanf(\u0026#34;%s\u0026#34;,str1); printf(\u0026#34;输入字符串2：\u0026#34;); scanf(\u0026#34;%s\u0026#34;,str2); while(str1[i]!=\u0026#39;\\0\u0026#39;){ i++; } while(str2[j]!=\u0026#39;\\0\u0026#39;){ str1[i++]=str2[j++]; } str1[i]=\u0026#39;\\0\u0026#39;; printf(\u0026#34;\\n新的字符串是：%s\\n\u0026#34;,str1); return 0; } 46.比较两个字符串 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include\u0026lt;stdio.h\u0026gt; /* C语言编一个程序，将两个字符串s1和s2比较，若s1\u0026gt;s2，输出一个正数； 若s1=s2，输出0，否则输出负数要求不要用strcmp函数 */ int main(){ int i=0,result; char s1[100],s2[100]; printf(\u0026#34;输入字符1：\u0026#34;); gets(s1); printf(\u0026#34;输入字符2：\u0026#34;); gets(s2); while((s1[i]==s2[i])\u0026amp;\u0026amp;(s1[i]!=\u0026#39;\\0\u0026#39;)){ i++; } if(s1[i]!=\u0026#39;\\0\u0026#39;\u0026amp;\u0026amp;s2[i]!=\u0026#39;\\0\u0026#39;){ result=0; }else{ result=s1[i]-s2[i]; } printf(\u0026#34;\\n输出结果：%d\\n\u0026#34;,result); return 0; } 47.将元音字母复制到另一个字符串中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include\u0026lt;stdio.h\u0026gt; /* C语言写一个函数，将一个字符串中的元音字母复制到另一字符串，然后输出。 */ int main(){ void copy(char s[],char str[]); char str1[80],str2[80]; printf(\u0026#34;输入字符串：\u0026#34;); gets(str1); copy(str1,str2); printf(\u0026#34;元音字母是：%s\\n\u0026#34;,str2); return 0; } void copy(char s[],char str[]){ int i,j=0; for(i=0;s[i]!=\u0026#39;\\0\u0026#39;;i++){ if(s[i]==\u0026#39;a\u0026#39;||s[i]==\u0026#39;e\u0026#39;||s[i]==\u0026#39;i\u0026#39;||s[i]==\u0026#39;o\u0026#39;||s[i]==\u0026#39;u\u0026#39;||s[i]==\u0026#39;A\u0026#39;||s[i]==\u0026#39;E\u0026#39;||s[i]==\u0026#39;I\u0026#39;||s[i]==\u0026#39;O\u0026#39;||s[i]==\u0026#39;U\u0026#39;){ str[j++]=s[i]; }else{ str[j]=\u0026#39;\\0\u0026#39;; } } } 48.输出4个字符且每个字符间空一格 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include\u0026lt;stdio.h\u0026gt; #include\u0026lt;string.h\u0026gt; /* C语言编写一个函数，输入一个4位数字，要求输出这4个数字字符，但每两个数字间空一个空格。如输入1990，应输出“1 9 9 0”出。 */ int main(){ void insert(char s[]); char str[80]; printf(\u0026#34;输入一个4位数字：\u0026#34;); scanf(\u0026#34;%s\u0026#34;,str); insert(str); return 0; } void insert(char s[]){ for(int i=strlen(s);i\u0026gt;0;i--){ s[2*i]=s[i]; s[2*i-1]=\u0026#39; \u0026#39;; } printf(\u0026#34;输出结果：%s\\n\u0026#34;,s); } **49.递归求勒让德多项式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include\u0026lt;stdio.h\u0026gt; /* C语言编程用递归方法求n阶勒让德多项式 解题思路：勒让德多项式是描述矩形表面和口径的另外一组多项式集合，它的优点是具有正交性。 由于存在正交性条件，高阶项系数趋于零，并且增加和删除一个项对其他项没有影响。 勒让德方程的解可写成标准的幂级数形式。 当方程满足 |x| \u0026lt; 1 时，可得到有界解（即解级数收敛）。 并且当n 为非负整数，即n = 0, 1, 2,... 时，在x = ± 1 点亦有有界解。 这种情况下，随n 值变化方程的解相应变化， 构成一组由正交多项式组成的多项式序列，这组多项式称为勒让德多项式 */ int main(){ float polynomial(int,int); int temp,num; printf(\u0026#34;输入num \u0026amp; temp:\u0026#34;); scanf(\u0026#34;%d,%d\u0026#34;,\u0026amp;num,\u0026amp;temp); printf(\u0026#34;Polynomial=%6.2f\\n\u0026#34;,polynomial(num,temp)); return 0; } float polynomial(int number,int x){ if(number==0) return 1; else if(number==1) return x; else return (2*number-1)*x*polynomial((number-1),x)-(number-1)*polynomial((number-2),x)/number; } 50.将数字转为字符串 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include\u0026lt;stdio.h\u0026gt; /* C语言用递归方法将一个整数n转换成字符串。例如，输入483，应输出字符串“483”，n的位数不确定i，可以是任意位数的整数。 解题思路：如果是负数，要把它转换为正数，同时为地输出一个“-”号。convert函数只处理正数。 字符‘0’的ASCII代码是48，3+48=51，51是字符‘3’的代码，因此putchar（n%10+‘0’）输出字符‘3’。 32在ASCII代码中代表空格，以使两个字符之间空格隔开。 */ int main(){ void convert(int n); int number; printf(\u0026#34;输入一个整数：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;number); printf(\u0026#34;输出结构：\u0026#34;); if(number\u0026lt;0){ putchar(\u0026#39;-\u0026#39;); putchar(\u0026#39; \u0026#39;); number=-number; } convert(number); printf(\u0026#34;\\n\u0026#34;); return 0; } void convert(int n){ int i; if((i=n/10)!=0){ convert(i); } putchar(n%10+\u0026#39;0\u0026#39;); putchar(32); } 51.求某日是该年第几天 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include\u0026lt;stdio.h\u0026gt; /* 给出年月日，C语言编程计算该日是该年的第几天 */ int main(){ int leap(int year); int sum_day(int month,int day); int year,month,day,days; printf(\u0026#34;输入日期：\u0026#34;); scanf(\u0026#34;%d %d %d\u0026#34;,\u0026amp;year,\u0026amp;month,\u0026amp;day); printf(\u0026#34;%d-%d-%d\u0026#34;,year,month,day); days=sum_day(month,day); if(leap(year)\u0026amp;\u0026amp;month\u0026gt;=3) days+=1; printf(\u0026#34;是这一年的第%d天\\n\u0026#34;,days); return 0; } int sum_day(int month,int day){ int day_tab[13]={0,31,28,31,30,31,30,31,31,30,31,30,31}; for(int i=1;i\u0026lt;month;i++){ day+=day_tab[i]; } return day; } int leap(int year){ //判断是否为闰年 return ((year%4==0\u0026amp;\u0026amp;year%100!=0) || (year%400==0)); } 52.指针由小到大输出3个数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include\u0026lt;stdio.h\u0026gt; /* C语言输入3个整数，按由小到大的顺序输出。（要求用指针处理） */ int main(){ void swap(int *p1,int *p2); int n1,n2,n3; int *p1,*p2,*p3; printf(\u0026#34;请输入3个整数：\u0026#34;); scanf(\u0026#34;%d %d %d\u0026#34;,\u0026amp;n1,\u0026amp;n2,\u0026amp;n3); p1=\u0026amp;n1; p2=\u0026amp;n2; p3=\u0026amp;n3; if(n1\u0026gt;n2) swap(p1,p2); if(n1\u0026gt;n3) swap(p1,p3); if(n2\u0026gt;n3) swap(p2,p3); printf(\u0026#34;%d %d %d\\n\u0026#34;,n1,n2,n3); return 0; } void swap(int *p1,int *p2){ int t; t=*p1; *p1=*p2; *p2=t; } **53. 对n个字符开辟连续的存储空间 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include\u0026lt;stdio.h\u0026gt; /* 有n个整数，使前面各数顺序向后移动m个位置，最后m个数变成最前面m个数， C语言写一函数实现以上功能， 在主函数中输入n个整数和输出调整后的n个数，要求用指针。 */ int main(){ void move(int a[20],int n,int m); int number[20],n,m,i; printf(\u0026#34;共有多少个数：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); printf(\u0026#34;输入这%d个数\\n\u0026#34;,n); for(i=0;i\u0026lt;n;i++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;number[i]); } printf(\u0026#34;向后移动多少个数：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;m); move(number,n,m); for(i=0;i\u0026lt;n;i++){ printf(\u0026#34;%d \u0026#34;,number[i]); } printf(\u0026#34;\\n\u0026#34;); return 0; } void move(int a[20],int n,int m){ int *p,a_end; a_end=*(a+n-1); for(p=a+n-1;p\u0026gt;a;p--){ *p=*(p-1); } *a=a_end; m--; if(m\u0026gt;0) move(a,n,m); } 54.顺序排号 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include\u0026lt;stdio.h\u0026gt; /* 有n个人围成一圈，C语言进行顺序排号，要求用指针。 */ int main(){ int i,k,m,n; int num[50]; int *p; printf(\u0026#34;输入n=\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); p=num; for(i=0;i\u0026lt;n;i++){ *(p+i)=i+1; } i=0; k=0; m=0; while(m\u0026lt;n-1){ if(*(p+i)!=0) k++; if(k==3) *(p+i)=0,k=0,m++; i++; if(i==n) i=0; } while(*p==0) p++; printf(\u0026#34;最后一个数是:%d\\n\u0026#34;,*p); return 0; } 55.求字符串的长度 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include\u0026lt;stdio.h\u0026gt; /* C语言写一个函数，求一个字符串的长度，在main函数中输入字符串，并输出其长度，要求用指针 */ int main(){ int num_length(char *p); char str[20]; printf(\u0026#34;请输入要求长度的字符串：\u0026#34;); scanf(\u0026#34;%s\u0026#34;,str); printf(\u0026#34;字符串的长度是%d\\n\u0026#34;,num_length(str)); return 0; } int num_length(char *p){ int i=0; while(*p!=\u0026#39;\\0\u0026#39;){ i++; p++; } return i; } **56.指向指针的指针排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include\u0026lt;stdio.h\u0026gt; #include\u0026lt;string.h\u0026gt; /* C语言写一个函数，求一个字符串的长度，在main函数中输入字符串，并输出其长度，要求用指针 */ int main(){ void sort(char **p); char **p,*pstr[5],str[5][20]; for(int i=0;i\u0026lt;5;i++){ pstr[i]=str[i]; } printf(\u0026#34;输入五个字符串：\\n\u0026#34;); for(int i=0;i\u0026lt;5;i++){ scanf(\u0026#34;%s\u0026#34;,pstr[i]); } p=pstr; sort(p); printf(\u0026#34;————————————\\n\u0026#34;); printf(\u0026#34;输出排序后的结果：\\n\u0026#34;); for(int i=0;i\u0026lt;5;i++){ printf(\u0026#34;%s\\n\u0026#34;,pstr[i]); } return 0; } void sort(char **p){ int i,j; char *temp; for(i=0;i\u0026lt;5;i++){ for(j=i+1;j\u0026lt;5;j++){ if(strcmp(*(p+i),*(p+j))\u0026gt;0){ temp=*(p+i); *(p+i)=*(p+j); *(p+j)=temp; } } } } **57.指向指针的指针 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include\u0026lt;stdio.h\u0026gt; /* C语言用指向指针的指针的方法对n个整数排序并输出；要求将排序单独写成一个函数； n个整数在主函数中输入，最后在主函数中输出。 */ int main(){ void sort(int **p,int n); int i,number,data[20],**p,*pstr[20]; printf(\u0026#34;输入要排序的个数number：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;number); for(i=0;i\u0026lt;number;i++){ pstr[i]=\u0026amp;data[i]; } printf(\u0026#34;逐个输入这%d个数：\u0026#34;,number); for(i=0;i\u0026lt;number;i++){ scanf(\u0026#34;%d\u0026#34;,pstr[i]); } p=pstr; sort(p,number); printf(\u0026#34;\\n-------------------\\n\u0026#34;); printf(\u0026#34;输出结果：\\n\u0026#34;); for(i=0;i\u0026lt;number;i++){ printf(\u0026#34;%d \u0026#34;,*pstr[i]); } printf(\u0026#34;\\n\u0026#34;); return 0; } void sort(int **p,int n){ int i,j,*temp; for(i=0;i\u0026lt;n;i++){ for(j=i+1;j\u0026lt;n;j++){ if(**(p+i)\u0026gt;**(p+j)){ temp=*(p+i); *(p+i)=*(p+j); *(p+j)=temp; } } } } 58.是否可以构成三角形 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include\u0026lt;stdio.h\u0026gt; #include\u0026lt;math.h\u0026gt; /* 给定平面上任意三个点的坐标(x1,y1)、(x2,y2)、(x3,y3)，检验它们能否构成三角形。 */ int main(){ double x1,y1,x2,y2,x3,y3; float side_length1,side_length2,side_length3; printf(\u0026#34;请输入第一个坐标；\u0026#34;); scanf(\u0026#34;%lf %lf\u0026#34;,\u0026amp;x1,\u0026amp;y1); printf(\u0026#34;请输入第二个坐标；\u0026#34;); scanf(\u0026#34;%lf %lf\u0026#34;,\u0026amp;x2,\u0026amp;y2); printf(\u0026#34;请输入第三个坐标；\u0026#34;); scanf(\u0026#34;%lf %lf\u0026#34;,\u0026amp;x3,\u0026amp;y3); side_length1=sqrt(pow(x2-x1,2)+pow(y2-y1,2)); side_length2=sqrt(pow(x3-x1,2)+pow(y3-y1,2)); side_length3=sqrt(pow(x3-x2,2)+pow(y3-y2,2)); if(side_length1+side_length2\u0026gt;side_length3\u0026amp;\u0026amp;side_length2+side_length3\u0026gt;side_length1 \u0026amp;\u0026amp;side_length1+side_length3\u0026gt;side_length2) printf(\u0026#34;这三个点可以构成三角形！\\n\u0026#34;); else printf(\u0026#34;这三个点可以构成三角形！\\n\u0026#34;); return 0; } 59. 求a+aa+\u0026hellip;+aa..a的值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include\u0026lt;stdio.h\u0026gt; /* 求sum=a+aa+aaa+aaaa+aa...a的值，其中a是一个数字。例如2+22+222+2222+22222(此时共有5个数相加)，几个数相加由键盘控制。 */ int main(){ int a,number,count=1; long int sum=0,temp=0; printf(\u0026#34;请输入a 和 number：\u0026#34;); scanf(\u0026#34;%d %d\u0026#34;,\u0026amp;a,\u0026amp;number); printf(\u0026#34;a=%d,number=%d\\n\u0026#34;,a,number); while(count\u0026lt;=number){ temp+=a; sum+=temp; a*=10; ++count; } printf(\u0026#34;a+aa+...=%ld\\n\u0026#34;,sum); return 0; } 60.判断回文数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include\u0026lt;stdio.h\u0026gt; /* 求一个五位数，C语言编程判断它是不是回文数。 回文数是指个位与万位相同，十位与千位相同 */ int main(){ long individual; long ten; long thousand; long ten_Thousand; //万 long n; printf(\u0026#34;请输入要判断的数：\u0026#34;); scanf(\u0026#34;%ld\u0026#34;,\u0026amp;n); ten_Thousand=n/10000; thousand=n%10000/1000; ten=n%100/10; individual=n%10; if(individual==ten_Thousand\u0026amp;\u0026amp;ten==thousand){ printf(\u0026#34;%ld是回文数！\\n\u0026#34;,n); }else{ printf(\u0026#34;%ld不是回文数！\\n\u0026#34;,n); } return 0; } 61.static auto register变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include\u0026lt;stdio.h\u0026gt; /* static auto register变量 */ //static /* int main(){ void varfunc(); for(int i=0;i\u0026lt;3;i++){ varfunc(); } return 0; } void varfunc(){ int var=0; static int static_var=0; printf(\u0026#34;变量var值是：%d\\n\u0026#34;,var); printf(\u0026#34;静态变量static_var值是：%d\\n\u0026#34;,static_var); printf(\u0026#34;\\n\u0026#34;); var++; static_var++; } */ /* 变量var值是：0 静态变量static_var值是：0 变量var值是：0 静态变量static_var值是：1 变量var值是：0 静态变量static_var值是：2 */ //auto /* int main(){ int num=2; for(int i=0;i\u0026lt;3;i++){ printf(\u0026#34;整型变量num的值是：%d\\n\u0026#34;,num); num++; { auto int num=1; printf(\u0026#34;auto类型的num值是：%d\\n\u0026#34;,num); num++; } } return 0; } */ /* 整型变量num的值是：2 auto类型的num值是：1 整型变量num的值是：3 auto类型的num值是：1 整型变量num的值是：4 auto类型的num值是：1 */ //register /* register这个关键字请求编译器尽可能的将变量存在CPU内部寄存器中，而不是通过内存寻址访问，以提高效率。 注意是尽可能，不是绝对。因为，如果定义了很多register变量，可能会超过CPU的寄存器个数，超过容量。 */ 62.求奇偶数个数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include\u0026lt;stdio.h\u0026gt; /* C语言编程求奇偶数的个数。 奇数是指指不能被2整除的整数；偶数是能够被2所整除的整数。 */ int main(){ int i,n,m; int odd_Number=0,even_Number=0; //同上且赋初值 printf(\u0026#34;请输入要判断几个数：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); printf(\u0026#34;输入这几个数：\u0026#34;); for(i=0;i\u0026lt;n;i++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;m); if(m%2==0) even_Number++; else odd_Number++; } printf(\u0026#34;奇数：%d个\\n偶数：%d个：\\n\u0026#34;,odd_Number,even_Number); return 0; } **63.直接插入排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include\u0026lt;stdio.h\u0026gt; /* C语言实现直接插入排序 。 解题思路：直接插入排序是一种最简单的排序方法， 其基本操作是将一条记录插入到已排好的有序表中，从而得到一个新的、记录数量增1的有序表。 */ int main(){ void insort(int post[],int n); void print(int a[],int x); int a[11]; printf(\u0026#34;请输入10个数据：\\n\u0026#34;); for(int i=1;i\u0026lt;=10;i++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;a[i]); } printf(\u0026#34;原始顺序：\\n\u0026#34;); print(a,11); insort(a,10); printf(\u0026#34;\\n插入数据排序后排序：\\n\u0026#34;); print(a,11); printf(\u0026#34;\\n\u0026#34;); return 0; } void insort(int post[],int n){ int i,j; for(i=2;i\u0026lt;=n;i++){ post[0]=post[i]; j=i-1; while(post[0]\u0026lt;post[j]){ post[j+1]=post[j]; j--; } post[j+1]=post[0]; } } void print(int a[],int x){ for(int i=1;i\u0026lt;x;i++){ printf(\u0026#34;%5d\u0026#34;,a[i]); } } **64.希尔排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include\u0026lt;stdio.h\u0026gt; /* C语言实现希尔排序。 解题思路：希尔排序是插入排序的一种又称缩小增量排序， 是直接插入排序算法的一种更高效的改进版本，希尔排序是非稳定排序算法。 希尔排序是把记录按下标的一定增量分组，对每组使用直接插入排序算法排序； 随着增量逐渐减少，每组包含的关键词越来越多， 当增量减至1时，整个文件恰被分成一组，算法便终止。 */ int main(){ void shsort(int s[],int n); void print(int a[],int x); int a[11],i; printf(\u0026#34;请输入10个数：\\n\u0026#34;); for(i=1;i\u0026lt;11;i++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;a[i]); } printf(\u0026#34;初始顺序：\\n\u0026#34;); print(a,11); shsort(a,10); printf(\u0026#34;\\n排序后顺序：\\n\u0026#34;); print(a,11); printf(\u0026#34;\\n\u0026#34;); return 0; } void shsort(int s[],int n){ int i,j,d; d=n/2; /*确定固定增虽值*/ while(d\u0026gt;=1){ for(i=d+1;i\u0026lt;=n;i++){ /*数组下标从d+1开始进行直接插入排序*/ s[0]=s[i]; /*设置监视哨*/ j=i-d; /*设置监视哨*/ while((j\u0026gt;0) \u0026amp;\u0026amp; (s[0]\u0026lt;s[j])){ s[j+d]=s[j]; /*数据右移*/ j=j-d; /*向左移d个位置V*/ } s[j+d]=s[0]; /*在确定的位罝插入s[i]*/ } d=d/2; /*增里变为原来的一半*/ } } void print(int a[],int x){ for(int i=1;i\u0026lt;x;i++){ printf(\u0026#34;%5d\u0026#34;,a[i]); } } 65.穷举解百钱百鸡 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include\u0026lt;stdio.h\u0026gt; /* 穷举解百钱百鸡 古代数学家张丘建在《算经》一书中提出的数学问题： 鸡翁一值钱五，鸡母一值钱三，鸡雏三值钱一。百钱买百鸡,问 鸡翁、鸡母、鸡雏各几何? 设鸡翁 鸡母 鸡雏数量分别为x y z，则： x+y+y=100 5x+3y+(1/3)z=100 */ int main(){ int x,y,z; for(x=0;x\u0026lt;100;x++){ for(y=0;y\u0026lt;100;y++){ for(z=0;z\u0026lt;100;z++){ if((x+y+z==100) \u0026amp;\u0026amp; (5*x+3*y+(1/3)*z==100)){ printf(\u0026#34;鸡翁%d只，鸡母%d只，鸡稚%d只。\\n\u0026#34;,x,y,z); } } } } return 0; } 66.输出规则图形，如三角形、 塔形 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include\u0026lt;stdio.h\u0026gt; //三角形 int main(){ int n; scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); for(int i=0;i\u0026lt;n;i++){ for(int j=0;j\u0026lt;n-i-1;j++){ printf(\u0026#34; \u0026#34;); } for(int k=0;k\u0026lt;2*(i+1)-1;k++){ printf(\u0026#34;*\u0026#34;); } printf(\u0026#34;\\n\u0026#34;); } return 0; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include\u0026lt;stdio.h\u0026gt; //倒三角 int main(){ int n; scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); for(int i=n;i\u0026gt;0;i--){ for(int j=0;j\u0026lt;n-i;j++){ printf(\u0026#34; \u0026#34;); } for(int k=0;k\u0026lt;2*i-1;k++){ printf(\u0026#34;*\u0026#34;); } printf(\u0026#34;\\n\u0026#34;); } return 0; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include\u0026lt;stdio.h\u0026gt; //宝塔形 int main(){ int i,j; for(i=1;i\u0026lt;=4;i++){ for(j=1;j\u0026lt;=10-i;j++){ printf(\u0026#34; \u0026#34;); } for(j=1;j\u0026lt;=i;j++){ printf(\u0026#34;%d\u0026#34;,j); } for(j=i-1;j\u0026gt;=1;j--){ printf(\u0026#34;%d\u0026#34;,j); } printf(\u0026#34;\\n\u0026#34;); } return 0; } /* 1 121 12321 1234321 */ 67.查找数组中指定的元素 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include\u0026lt;stdio.h\u0026gt; #define N 10 int main(){ int search(int a[],int n,int x); int i,index,n,x; int arr[N]; printf(\u0026#34;请输入数组输的个数:\\n\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); printf(\u0026#34;请输入%d数:\\n\u0026#34;,n); for(i=0;i\u0026lt;n;i++){ scanf(\u0026#34;%d\u0026#34;,\u0026amp;arr[i]); } printf(\u0026#34;请输入需要查找的数:\\n\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;x); index=search(arr,n,x); if(index!=-1) printf(\u0026#34;第%d个数\\n\u0026#34;,index+1); else printf(\u0026#34;not found\\n\u0026#34;); return 0; } int search(int a[],int n,int x){ int index,i; index=-1; for(i=0;i\u0026lt;n;i++){ if(a[i]==x){ index=i; break; } } return index; } **68.快速排序法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #include\u0026lt;stdio.h\u0026gt; /* 快速排序算法 */ int main(){ int qusort(int s[],int start,int end); int a[11], i; //定义数组及变量为基本整型 printf(\u0026#34;请输入10个数：\\n\u0026#34;); for(i=1;i\u0026lt;=10;i++) scanf(\u0026#34;%d\u0026#34;,\u0026amp;a[i]); //从键盘中输入10个要进行排序的数 qusort(a,1,10); //调用qusort()函数进行排序 printf(\u0026#34;排序后的顺序是：\\n\u0026#34;); for(i=1;i\u0026lt;=10;i++) printf(\u0026#34;%5d\u0026#34;,a[i]); //输出排好序的数组 printf(\u0026#34;\\n\u0026#34;); return 0; } int qusort(int s[],int start,int end){ //自定义函数 qusort() int i,j; //定义变量为基本整型 i=start; //将每组首个元素赋给i j = end; //将每组末尾元素赋给j s[0]=s[start]; //设置基准值 while(i\u0026lt;j) { while(i\u0026lt;j\u0026amp;\u0026amp;s[0]\u0026lt;s[j]) j--; //位置左移 if(i\u0026lt;j) { s[i]=s[j]; //将s[j]放到s[i]的位置上 i++; //位置右移 } while(i\u0026lt;j\u0026amp;\u0026amp;s[i]\u0026lt;=s[0]) i++; //位置左移 if(i\u0026lt;j) { s[j]=s[i]; //将大于基准值的s[j]放到s[i]位置 j--; //位置左移 } } s[i]=s[0]; //将基准值放入指定位置 if (start\u0026lt;i) qusort(s,start,j-1); //对分割出的部分递归调用qusort()函数 if (i\u0026lt;end) qusort(s,j+1,end); return 0; } **69.归并排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 #include\u0026lt;stdio.h\u0026gt; /* 归并排序算法 */ int main(){ int merge(int r[],int s[],int x1,int x2,int x3); int merge_sort(int r[],int s[],int m,int n); int a[11]; int i; printf(\u0026#34;请输入10个数：\\n\u0026#34;); for(i=1;i\u0026lt;=10;i++) scanf(\u0026#34;%d\u0026#34;,\u0026amp;a[i]); //从键盘中输入10个数 merge_sort(a,a,1,10); //调用merge_sort()函数进行归并排序 printf(\u0026#34;排序后的顺序是：\\n\u0026#34;); for(i=1;i\u0026lt;=10;i++) printf(\u0026#34;%5d\u0026#34;,a[i]); //输出排序后的数据 printf(\u0026#34;\\n\u0026#34;); return 0; } int merge(int r[],int s[],int x1,int x2,int x3) //自定义实现一次归并样序的函数 { int i,j,k; i=x1; //第一部分的开始位置 j=x2+1; //第二部分的开始位置 k=x1; while((i\u0026lt;=x2)\u0026amp;\u0026amp;(j\u0026lt;=x3)){ //当i和j都在两个要合并的部分中时 if(r[i]\u0026lt;=r[j]) //筛选两部分中较小的元素放到数组s中 { s[k] = r[i]; i++; k++; } else { s[k]=r[j]; j++; k++; } while(i\u0026lt;=x2) //将x1~x2范围内未比较的数顺次加到数组r中 s[k++]=r[i++]; while(j\u0026lt;=x3) //将x2+l~x3范围内未比较的数顺次加到数组r中 s[k++]=r[j++]; } return 0; } int merge_sort(int r[],int s[],int m,int n) { int p; int t[20]; if(m==n) s[m]=r[m]; else { p=(m+n)/2; merge_sort(r,t,m,p); //递归调用merge_soit()函数将r[m]?r[p]归并成有序的t[m]?t[p] merge_sort(r,t,p+1,n); //递归一调用merge_sort()函数将r[p+l]?r[n]归并成有序的t[p+l]?t[n] merge(t,s,m,p,n); //调用函数将前两部分归并到s[m]?s[n】*/ } return 0; } **70.二分查找算法，折半查找算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include \u0026lt;stdio.h\u0026gt; int binary_search(int key,int a[],int n) //自定义函数binary_search() { int low,high,mid,count=0,count1=0; low=0; high=n-1; while(low\u0026lt;high) //査找范围不为0时执行循环体语句 { count++; //count记录査找次数 mid=(low+high)/2; //求中间位置 if(key\u0026lt;a[mid]) //key小于中间值时 high=mid-1; //确定左子表范围 else if(key\u0026gt;a[mid]) //key 大于中间值时 low=mid+1; //确定右子表范围 else if(key==a[mid]) //当key等于中间值时，证明查找成功 { printf(\u0026#34;查找成功!\\n 查找 %d 次!a[%d]=%d\u0026#34;,count,mid,key); //输出査找次数及所査找元素在数组中的位置 count1++; //count1记录查找成功次数 break; } } if(count1==0) //判断是否查找失敗 printf(\u0026#34;查找失敗!\u0026#34;); //査找失敗输出no found return 0; } int main() { int i,key,a[100],n; printf(\u0026#34;请输入数组的长度：\\n\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); //输入数组元素个数 printf(\u0026#34;请输入数组元素：\\n\u0026#34;); for(i=0;i\u0026lt;n;i++) scanf(\u0026#34;%d\u0026#34;,\u0026amp;a[i]); //输入有序数列到数组a中 printf(\u0026#34;请输入你想查找的元素：\\n\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;key); //输入要^找的关键字 binary_search(key,a,n); //调用自定义函数 printf(\u0026#34;\\n\u0026#34;); return 0; } **71.分块查找算法，索引顺序查找算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include \u0026lt;stdio.h\u0026gt; struct index //定义块的结构 { int key; //块的关键字 int start; //块的起始值 int end; //块的结束值 }index_table[4]; //定义结构体数组 int block_search(int key,int a[]) //自定义实现分块查找 { int i,j; i=1; while(i\u0026lt;=3\u0026amp;\u0026amp;key\u0026gt;index_table[i].key) //确定在哪个块中 i++; if(i\u0026gt;3) //大于分得的块数，则返回0 return 0; j=index_table[i].start; //j等于块范围的起始值 while(j\u0026lt;=index_table[i].end\u0026amp;\u0026amp;a[j]!=key) //在确定的块内进行顺序查找 j++; if(j\u0026gt;index_table[i].end) //如果大于块范围的结束值，则说明没有要査找的数，j置0 j = 0; return j; } int main() { int i,j=0,k,key,a[16]; printf(\u0026#34;请输入15个数：\\n\u0026#34;); for(i=1;i\u0026lt;16;i++) scanf(\u0026#34;%d\u0026#34;,\u0026amp;a[i]); //输入由小到大的15个数 for(i=1;i\u0026lt;=3;i++) { index_table[i].start=j+1; //确定每个块范围的起始值 j=j+1; index_table[i].end=j+4; //确定每个块范围的结束值 j=j + 4; index_table[i].key=a[j]; //确定每个块范围中元素的最大值 } printf(\u0026#34;请输入你想査找的元素：\\n\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;key); //输入要查询的数值 k=block_search(key,a); //调用函数进行杳找 if(k!=0) printf(\u0026#34;查找成功，其位置是：%d\\n\u0026#34;,k); //如果找到该数，则输出其位置 else printf(\u0026#34;查找失败!\u0026#34;); //若未找到，则输出提示信息 return 0; } 72.求自然底数e 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include \u0026lt;stdio.h\u0026gt; /* 自然底数 e=2.718281828…，e 的计算公式如下： e=1+1/1!+1/2!+1/3!+… 要求当最后一项的值小于 10-10 时结束。 */ int main(){ float e=1.0,n=1.0; int i=1; while(1/n\u0026gt;1e-10){ e+=1/n; i++; n=i*n; } printf(\u0026#34;e的值是：%f\\n\u0026#34;,e); return 0; } 73.求回文素数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include \u0026lt;stdio.h\u0026gt; /* 任意的整数，当从左向右读与从右向左读是相同的，且为素数时，称为回文素数。求 1000 以内的所有回文素数。 实例的重点是判断一个数是否是回文素数。要输出 1000 以内的所有回文素数，首先应判断这个数是否是素数；如果是，再进一步判断这个数是两位数还是三位数，若是两位数，则需判断个位数和十位数是否相同；若是三位数，则需判断个位数和百位数是否相同。若相同，则判断为回文素数，否则继续下次判断。 ① 定义一个函数 sushu，其作用是判断一个数是否是素数。 ② 对判断为素数的数，再判断其是否是两位数。 若是两位数，再判断其个位数和十位数是否相同，若相同则打印输出；若不相同，则执行④；若不是两位数，则执行③。 ③ 若是三位数，则判断其个位数和百位数是否相同。若相同，则打印输出；若不相同，则执行 ④。 ④ 循环控制变量 i 自增 1。 ⑤ 直到 i 自增至 1000 结束。 */ int main(){ int sushu(int n); int i; for(i=0;i\u0026lt;1000;i++){ if(sushu(i)==1){ if(i/100==0){ if(i/10==i%10) printf(\u0026#34;%5d\u0026#34;,i); if(i%5==0) printf(\u0026#34;\\n\u0026#34;); } else{ if(i/100==i%10) printf(\u0026#34;%5d\u0026#34;,i); if(i%5==0) printf(\u0026#34;\\n\u0026#34;); } } } return 0; } int sushu(int n){ int i; if(n\u0026lt;=1) return 0; if(n==1) return 1; for(i=2;i\u0026lt;n;i++){ if(n%i==0) return 0; else if(n!=i+1) continue; else return 1; } return 0; } 74.兔子生兔子问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include \u0026lt;stdio.h\u0026gt; /* 假设一对兔子的成熟期是一个月，即一个月可长成成兔，那么，如果每对成兔每个月都生一对小兔， 一对新生的小兔从第二个月起就开始生兔子，试问从一对兔子开始繁殖，以后每个月会有多少对兔子？ */ int main(){ int i,tu1,tu2,tu3,m; tu1=1; tu2=1; printf(\u0026#34;请输入月份数\\n\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;m); if(m==1||m==2) printf(\u0026#34;有一对兔子\u0026#34;); else if(m\u0026gt;2){ for(i=3;i\u0026lt;=m;i++){ tu3=tu1+tu2; tu1=tu2; tu2=tu3; } printf(\u0026#34;%d月的兔子数为：%d\\n\u0026#34;,m,tu3); } return 0; } **75.约瑟夫环问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include\u0026lt;stdio.h\u0026gt; /* 编号为 1，2，3，…，n 的 n 个人围坐一圈，任选一个正整数 m 作为报数上限值，从第一个人开始按顺时针方向报数， 报数到 m 时停止，报数为 m 的人出列。从出列人的顺时针方向的下一个人开始又从 1 重新报数，如此下去，直到所有人都全部出列为止。 */ #define N 100 int josef(int a[],int n,int m) { int i,j,k=0; for(i=0;i\u0026lt;n;i++) { j=1; while(j\u0026lt;m) { while(a[k]==0) k=(k+1)%n; j++; k=(k+1)%n; } while(a[k]==0) k=(k+1)%n; printf(\u0026#34;%d \u0026#34;,a[k]); a[k]=0; } return 0; } int main() { int a[100]; int i,m,n; printf(\u0026#34;input n and m：\u0026#34;); scanf(\u0026#34;%d%d\u0026#34;,\u0026amp;n,\u0026amp;m); for(i=0;i\u0026lt;n;i++) a[i]=i+1; printf(\u0026#34;\\n output：\\n\u0026#34;); josef(a,n,m); printf(\u0026#34;\\n\u0026#34;); return 0; } **76.日期处理函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;math.h\u0026gt; /* 定义一个表示日期的结构体类型，再分别定义函数完成下列功能：计算某一天是对应年的第几天， 这一年一共多少天；计算两个日期之间相隔的天数。两个日期由键盘输入。 设定结构体类型表示日期类型名为 Date，利用 typedef 将其定义为日期型类型名，有三个整型类型的成员分别表示年、月、日。 设定函数计算输入的日期是这一年的第几天。函数的形参为日期型变量，函数体中设定整型数组存放每个月的天数，二月份的天数为 28 天； 设定函数判断年份是否为闰年以决定二月份的天数。根据输入的日期月份，在数组中将相应的月份天数求和，假日曰期即为天数。 设定函数完成两个日期的比较，比较形参 d 和 s 两个日期的大小。首先比较年，同年的比较月，同月的比较日。 变量 start 保存输入的小的日期年份，end 保存输入日期大的年份，然后计算两个日期之间的天数。 程序由 6 个函数构成， yearday() 函数计算某年的天数， monthday() 函数计算某年二月份的天数， dayofyeaK() 函数计算某日期是某年的第几天， cmpdate() 函数比较两个日期的大小， interday() 函数计算两个日期之间的天数； dayofyear() 函数调用 monthday() 函数， interday() 函数调用 cmpdate() 函数、yearday() 函数、dayofyear() 函数； 主函数调用 yearday() 函数、dayofyear() 函数、interday() 函数。 */ typedef struct { int year,month,day; }Date; int yearday(int year) { int yday; if((year%4==0 \u0026amp;\u0026amp; year%100!=0)||year%400==0) yday=366; else yday=365; return yday; } int monthday(int year) { int mday; if((year%4==0 \u0026amp;\u0026amp; year%100!=0)||year%400==0) mday=29; else mday=28; return mday; } int dayofyear(Date d) { int i,total=0; int months[13]={0,31,28,31,30,31,30,31,31,30,31,30,31}; months[2]=monthday(d.year); for(i=1;i\u0026lt;d.month;i++) total=total+months[i]; total=total+d.day; return total; } int cmpdate(Date d,Date s) { int result; if(d.year==s.year) { if(d.month==s.month) { if(d.day==s.day) result=0; else result=d.day-s.day; } else result=d.month-s.month; } else result=d.year-s.year; return result; } int interday(Date d,Date s) { int result,te,ts,total; int start,end,day; int i; result=cmpdate(d,s); if(result\u0026gt;0) { start=s.year; end=d.year; te=dayofyear(d); ts=dayofyear(s); } else if(result\u0026lt;0) { start=d.year; end=s.year; ts=dayofyear(d); te=dayofyear(s); } else return 0; if(start==end) return abs(te-ts); else { total=0; for(i=start;i\u0026lt;=end;i++) { day=yearday(i); if(i==start) total=total+day-ts; else if(i==end) total=total+te; else total=total+day; } } return total; } int main() { Date d1,d2; int y,n; printf(\u0026#34;input date：\u0026#34;); scanf(\u0026#34;%d%d%d\u0026#34;,\u0026amp;d1.year,\u0026amp;d1.month,\u0026amp;d1.day); scanf(\u0026#34;%d%d%d\u0026#34;,\u0026amp;d2.year,\u0026amp;d2.month,\u0026amp;d2.day); y=yearday(d1.year); n=dayofyear(d1); printf(\u0026#34;%d days %d\\n\u0026#34;,d1.year,y); printf(\u0026#34;%d-%d-%d is the %d day.\\n\u0026#34;,d1.year,d1.month,d1.day,n); n=interday(d1,d2); printf(\u0026#34;%d-%d-%d and %d-%d-%d distance \u0026#34;,d1.year,d1.month,d1.day,d2.year,d2.month,d2.day); printf(\u0026#34;%d days\\n\u0026#34;,n); return 0; } **77.汉诺塔问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include\u0026lt;stdio.h\u0026gt; /* 汉诺塔问题是指：一块板上有三根针 A、B、C。A 针上套有 64 个大小不等的圆盘，按照大的在下、小的在上的顺序排列， 要把这 64 个圆盘从 A 针移动到 C 针上，每次只能移动一个圆盘，移动过程可以借助 B 针。但在任何时候， 任何针上的圆盘都必须保持大盘在下，小盘在上。从键盘输入需移动的圆盘个数，给出移动的过程。 算法思想 对于汉诺塔问题，当只移动一个圆盘时，直接将圆盘从 A 针移动到 C 针。若移动的圆盘为 n(n\u0026gt;1)，则分成几步走： 把 (n-1) 个圆盘从 A 针移动到 B 针（借助 C 针）；A 针上的最后一个圆盘移动到 C 针； B 针上的 (n-1) 个圆盘移动到 C 针（借助 A 针）。每做一遍，移动的圆盘少一个，逐次递减，最后当 n 为 1 时，完成整个移动过程。 因此，解决汉诺塔问题可设计一个递归函数，利用递归实现圆盘的整个移动过程，问题的解决过程是对实际操作的模拟。 */ int main() { int hanoi(int,char,char,char); int n; printf(\u0026#34;Input the number of diskes：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;n); printf(\u0026#34;\\n\u0026#34;); hanoi(n,\u0026#39;A\u0026#39;,\u0026#39;B\u0026#39;,\u0026#39;C\u0026#39;); return 0; } int hanoi(int n,char x,char y,char z) { int move(char,int,char); if(n==1) move(x,1,z); else { hanoi(n-1,x,z,y); move(x,n,z); hanoi(n-1,y,x,z); } return 0; } int move(char getone,int n,char putone) { static int k=1; printf(\u0026#34;%2d:%3d # %c---%c\\n\u0026#34;,k,n,getone,putone); if(k++%3==0) printf(\u0026#34;\\n\u0026#34;); return 0; } *78.求圆周率π 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;math.h\u0026gt; /* 题目1) 利用公式①计求π的近似值，要求累加到最后一项小于10^(-6)为止。 题目2) 根据公式②，用前100项之积计算π的值。 题目1)提供了一种解法，题目2)提供了两种解法，请看解析。 */ int main(){ float s=1; float pi=0; float i=1.0; float n=1.0; while(fabs(i)\u0026gt;=1e-6){ pi+=i; n=n+2; // 这里设计的很巧妙，每次正负号都不一样 s=-s; i=s/n; } pi=4*pi; printf(\u0026#34;pi的值为：%.6f\\n\u0026#34;,pi); return 0; } ","permalink":"https://ktzxy.top/posts/xowmgokf5a/","summary":"C语言编程的一些实例","title":"C语言编程实例"},{"content":"Kubernetes集群安全机制 概述 当我们访问K8S集群时，需要经过三个步骤完成具体操作\n认证 鉴权【授权】 准入控制 进行访问的时候，都需要经过 apiserver， apiserver做统一协调，比如门卫\n访问过程中，需要证书、token、或者用户名和密码 如果访问pod需要serviceAccount 认证 对外不暴露8080端口，只能内部访问，对外使用的端口6443\n客户端身份认证常用方式\nhttps证书认证，基于ca证书 http token认证，通过token来识别用户 http基本认证，用户名 + 密码认证 鉴权 基于RBAC进行鉴权操作\n基于角色访问控制\n准入控制 就是准入控制器的列表，如果列表有请求内容就通过，没有的话 就拒绝\nRBAC介绍 基于角色的访问控制，为某个角色设置访问内容，然后用户分配该角色后，就拥有该角色的访问权限\nk8s中有默认的几个角色\nrole：特定命名空间访问权限 ClusterRole：所有命名空间的访问权限 角色绑定\nroleBinding：角色绑定到主体 ClusterRoleBinding：集群角色绑定到主体 主体\nuser：用户 group：用户组 serviceAccount：服务账号 RBAC实现鉴权 创建命名空间 创建命名空间 我们可以首先查看已经存在的命名空间\n1 kubectl get namespace 然后我们创建一个自己的命名空间 roledemo\n1 kubectl create ns roledemo 命名空间创建Pod 为什么要创建命名空间？因为如果不创建命名空间的话，默认是在default下\n1 kubectl run nginx --image=nginx -n roledemo 创建角色 我们通过 rbac-role.yaml进行创建\ntip：这个角色只对pod 有 get、list权限\n然后通过 yaml创建我们的role\n1 2 3 4 # 创建 kubectl apply -f rbac-role.yaml # 查看 kubectl get role -n roledemo 创建角色绑定 我们还是通过 role-rolebinding.yaml 的方式，来创建我们的角色绑定\n然后创建我们的角色绑定\n1 2 3 4 # 创建角色绑定 kubectl apply -f rbac-rolebinding.yaml # 查看角色绑定 kubectl get role, rolebinding -n roledemo 使用证书识别身份 我们首先得有一个 rbac-user.sh 证书脚本\n这里包含了很多证书文件，在TSL目录下，需要复制过来\n通过下面命令执行我们的脚本\n1 ./rbac-user.sh 最后我们进行测试\n1 2 3 4 # 用get命令查看 pod 【有权限】 kubectl get pods -n roledemo # 用get命令查看svc 【没权限】 kubectl get svc -n roledmeo ","permalink":"https://ktzxy.top/posts/f3dz6yr0vg/","summary":"13 Kubernetes集群安全机制","title":"13 Kubernetes集群安全机制"},{"content":"Heml应用包管理器 helm学习文档：https://helm.sh/zh/docs/\n为什么要使用Helm？ K8S 上的应用对象，都是由特定的资源描述组成，包括 deployment、services 等。都保存各自文件中或者集中写到 一个配置文件。然后 kubectl apply -f 部署。\n如果应用只由一个或几个这样的服务组成，上面部署方式足够了。\n而对于一个复杂的应用，会有很多类似上面的资源描述文件，例如微服务架构应用，组成应用的服务可能多达十 个，几十个。如果有更新或回滚应用的需求，可能要修改和维护所涉及的大量资源文件，而这种组织和管理应用的 方式就显得力不从心了。\n且由于缺少对发布过的应用版本管理和控制，使 Kubernetes 上的应用维护和更新等面临诸多的桃战，主要面临以下问题：\n如何将这些服务作为一个整体管理 这些资源文件如何高效复用 不支持应用级别的版本管理 Helm介绍 Helm是一个Kubernetes的包管理工具，就像 Linux下的包管理器，如 yml、apt等，可以方便将之前打包好的 yaml 文件部署到 Kubernetes 上\nHeml 有两个重要的概念：\nheml：一个命令行客户端工具，主要用于 kubernetes 应用 chart 的创建、打包、发布和管理 Chart：应用描述，一系列用于描述 k8s 资源相关文件的集合 Release：基于Chart的部署实体，一个chart 被 helm 运行后将会生成一个release，将在 k8s 中创建出真实运行的资源对象 Helm v3 变化 2019 年 11 月 13 日，Helm 团队发布 helm v3 的第一个稳定版本\n该版本主要的变化如下：\n架构变化 最明显的是 Tiller 的删除\nrelease 名称可以在不同命名空间重用\n支持将Chart推送至Docker 镜像仓库中\n使用JSONSchema验证 chart values\n部署 helm 客户端 Helm客户端下载地址：Github 搜 heml\nhttps://github.com/helm/helm/releases/tag/v3.9.3\nHelm常用命令 命令 描述 create 创建一个chart并指定名称 dependency 管理chart依赖 get 下载release，可用子命令：all、hooks、manifest、notes、value history 获取release历史 install 安装一个chart list 列出release package 将chart目录打包到chart存档文件中 pull 从远程仓库中下载chart并解压到本地； helm pull stable/mysql \u0026ndash;untar repo 添加、列出、移除、更新和索引chart仓库 rollback 从之前版本回滚 search 根据关键字搜索Chart show 查看chart详细信息 template 本地呈现模板 uninstall 卸载一个release upgrade 更新一个release version 查看helm客户端版本 配置国内 chart 仓库 微软仓库 阿里云仓库 官方仓库，官方chart仓库，国内不太好使 添加存储库\n1 2 3 helm repo add stable http://xxxx.xxx helm repo add aliyun http://xxx.xxx helm repo update 查看配置的存储库\n1 2 helm repo list helm repo repo stable 一直在stable存储库中安装 charts，你可以配置其它存储库\n删除存储库\n1 helm repo remove aliyun helm基本使用 主要介绍三个命令\nchart install：安装 chart upgrade：升级 chart rollback：回滚 使用chart部署一个应用 查找chart\n1 2 helm search repo helm search repo mysql 为什么 mariadb 也在列表中？因为和mysql有关\n1 helm show stable/mysql values 部署 mysql\n1 helm install my_db stable/mysql 查看 pod 的详细信息\n1 2 kubectl get pods kubectl describe pod pod_name 需要绑定 pv\n自定义chart配置选项 上面部署的mysql并没有成功，这是因为并不是所有的charts都能够按照默认配置运行成功，可能会依赖一些环境依赖，比如 PV\n所有，我们需要自定义 chart 配置选项，安装过程中有两种方法可以传递配置数据：\n\u0026ndash;values（或 -f）：指定带有覆盖的YAML文件，这里可以多次指定，最右边的文件优先 \u0026ndash;set：在命令行上指定替代，如果两者都用，\u0026ndash;set优先级高 \u0026ndash;values使用，先将修改的变量写到一个文件中\n1 2 heml show values stable/mysql cat config.yaml 然后在 config.yaml 中，写上下面的数据\n然后执行下列命令\n1 2 helm install db -f config.yaml stable/mysql; kubectl get pods; 以上，将创建具有名称的默认MySQL用户k8s，并授权此用户访问新创建的数据库的权限，但将接收该图标的数据所有其余默认值\n命令行替换变量：\n1 helm install db --set persistence.storageClass= \u0026#34;managed-ngf-storage\u0026#34; stable/mysql 也可以将chart包下载下来，查看详情\n1 helm pull stable/mysql --untar values yaml 与 set 使用\n以蘑菇为例，如何制作 heml 包\n构建一个Heml Chart 可以通过下面名， 创建一个 heml 目录\n1 helm create mychart 最后生成的目录结构如上所示\n","permalink":"https://ktzxy.top/posts/esfzdgiav7/","summary":"53 Helm入门","title":"53 Helm入门"},{"content":"Kubernetes核心技术Service 前言 前面我们了解到 Deployment 只是保证了支撑服务的微服务Pod的数量，但是没有解决如何访问这些服务的问题。一个Pod只是一个运行服务的实例，随时可能在一个节点上停止，在另一个节点以一个新的IP启动一个新的Pod，因此不能以确定的IP和端口号提供服务。\n要稳定地提供服务需要服务发现和负载均衡能力。服务发现完成的工作，是针对客户端访问的服务，找到对应的后端服务实例。在K8S集群中，客户端需要访问的服务就是Service对象。每个Service会对应一个集群内部有效的虚拟IP，集群内部通过虚拟IP访问一个服务。\n在K8S集群中，微服务的负载均衡是由kube-proxy实现的。kube-proxy是k8s集群内部的负载均衡器。它是一个分布式代理服务器，在K8S的每个节点上都有一个；这一设计体现了它的伸缩性优势，需要访问服务的节点越多，提供负载均衡能力的kube-proxy就越多，高可用节点也随之增多。与之相比，我们平时在服务器端使用反向代理作负载均衡，还要进一步解决反向代理的高可用问题。\nService存在的意义 防止Pod失联【服务发现】 因为Pod每次创建都对应一个IP地址，而这个IP地址是短暂的，每次随着Pod的更新都会变化，假设当我们的前端页面有多个Pod时候，同时后端也多个Pod，这个时候，他们之间的相互访问，就需要通过注册中心，拿到Pod的IP地址，然后去访问对应的Pod\n定义Pod访问策略【负载均衡】 页面前端的Pod访问到后端的Pod，中间会通过Service一层，而Service在这里还能做负载均衡，负载均衡的策略有很多种实现策略，例如：\n随机 轮询 响应比 Pod和Service的关系 这里Pod 和 Service 之间还是根据 label 和 selector 建立关联的 【和Controller一样】\n我们在访问service的时候，其实也是需要有一个ip地址，这个ip肯定不是pod的ip地址，而是 虚拟IP vip\nService常用类型 Service常用类型有三种\nClusterIp：集群内部访问 NodePort：对外访问应用使用 LoadBalancer：对外访问应用使用，公有云 举例 我们可以导出一个文件 包含service的配置信息\n1 kubectl expose deployment web --port=80 --target-port=80 --dry-run -o yaml \u0026gt; service.yaml service.yaml 如下所示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: app: web name: web spec: ports: - port: 80 protocol: TCP targetPort: 80 selector: app: web status: loadBalancer: {} 如果我们没有做设置的话，默认使用的是第一种方式 ClusterIp，也就是只能在集群内部使用，我们可以添加一个type字段，用来设置我们的service类型\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 apiVersion: v1 kind: Service metadata: creationTimestamp: null labels: app: web name: web spec: ports: - port: 80 protocol: TCP targetPort: 80 selector: app: web type: NodePort status: loadBalancer: {} 修改完命令后，我们使用创建一个pod\n1 kubectl apply -f service.yaml 然后能够看到，已经成功修改为 NodePort类型了，最后剩下的一种方式就是LoadBalanced：对外访问应用使用公有云\nnode一般是在内网进行部署，而外网一般是不能访问到的，那么如何访问的呢？\n找到一台可以通过外网访问机器，安装nginx，反向代理 手动把可以访问的节点添加到nginx中 如果我们使用LoadBalancer，就会有负载均衡的控制器，类似于nginx的功能，就不需要自己添加到nginx上\n","permalink":"https://ktzxy.top/posts/agbyh9mwpz/","summary":"10 Kubernetes核心技术Service","title":"10 Kubernetes核心技术Service"},{"content":"在线作图 ProcessOn思维导图流程图-在线画思维导图流程图_在线作图实时协作\nioDraw 免费在线制作流程图,思维导图,甘特图,海报\nFlowchart Maker \u0026amp; Online Diagram Software\n工具箱 即时工具-致力打造即用即走型在线工具箱 (67tool.com)\nHeyFriday - 智能AI写作工具（星期五）\nYouTube高清视频下载 - 油管(YouTube)视频解析下载保存到手机、电脑 (iiilab.com)\n网络认证__西西软件园 (cr173.com)\n秘塔写作猫 (xiezuocat.com)\nMac毒 - Mac软件下载网站（奶酪网） (macdo.cn)\n哔哩哔哩(bilibili)视频解析下载 - 保存B站视频到手机、电脑 (snapany.com)\n爱资料工具-好用的在线工具箱 (toolnb.com)\nFree Online Developer Tools - TOOLFK\nBigjpg - AI人工智能图片无损放大 - 使用人工智能深度卷积神经网络(CNN)无损放大图片\nFree Online Developer Tools - TOOLFK (amp360.net)\n在线工具 - 你的工具箱 (tool.lu)\n首页 - 孟坤工具箱网页版 (mkblog.cn)\nDocs (feishu.cn)\n抖音视频、小红书、快手视频、B站视频无水印下载-vtool解析网\n在线编辑简历_极简PoleBrief在线编辑简历\n图片美化All effects - PhotoFunia: Free pic editor online with library of picture effects \u0026amp; photo filters\n免费logo在线制作,logo设计,logo在线生成,字体logo设计,U钙网 (uugai.com)\n艺术字体在线生成器 艺术字转换器 字体转换器 免费书法字体设计大全-找字体网 (qt86.com)\n在线海报设计\nSVG转PNG工具_在线将SVG文档批量转换成PNG图片 (fly63.com)\n视频转化GIF_动态图片制作工具 (fly63.com)\n二维码在线解码工具 (fly63.com)\n千亿像素看中国 (bigpixel.cn)\n压缩图片\n趣作图—免费抠图_在线抠图_专业抠图 (pickwant.com)\n删除照片中的物体 | Magic Eraser by Magic Studio\n在线生成ICO图标 (fly63.com)\n在线抠图软件_图片去除背景 | remove.bg – remove.bg\nBigjpg - AI人工智能图片无损放大 - 使用人工智能深度卷积神经网络(CNN)无损放大图片\n二维码美化系统_在线自定义排版二维码_草料二维码 (cli.im)\nMicrosoft数学求解器-数学问题求解器和计算器\n在线二维码网页生成器 (fly63.com)\n免费 UUID在线生成器 - 在线工具网(zxgj.cn) - 工作生活好帮手\n免费 随机数生成器 - 在线工具网(zxgj.cn) - 工作生活好帮手\n草料二维码生成器 (cli.im)\niP地址查询\u0026ndash;手机号码查询归属地 | 邮政编码查询 | iP地址归属地查询 | 身份证号码验证在线查询网 (ip138.com)\nIP查询_专业精准的IP库服务商_IPIP\n在线模拟http请求工具_GET/POST接口测试 (fly63.com)\nip地址在线计算器 (520101.com)\nascii码表_ASCII码一览表，ASCII码对照表 (fly63.com)\n免费 进制转换 - 在线工具网(zxgj.cn) - 工作生活好帮手\n在线浮点数十进制转换)\n免费 RGB颜色转换 - 在线工具网(zxgj.cn) - 工作生活好帮手\n免费 Unix时间戳转换 - 在线工具网(zxgj.cn) - 工作生活好帮手\n度量衡单位在线换算工具 (fly63.com)\nJSON在线解析及格式化验证 - JSON.cn\n在线JS代码格式化工具\n免费 SQL压缩/格式化 - 在线工具网(zxgj.cn) - 工作生活好帮手\n免费 JSON和XML在线转换 - 在线工具网(zxgj.cn) - 工作生活好帮手\nJSON转YAML,YAML转JSON (fly63.com)\n在线人民币金额大写转换工具-把数字转换成中文大写 (fly63.com)\n在线转换器 - 转换文件成为视频, 音乐, 图像, PDF - Office-Converter.com\nJSON在线解析及格式化验证 - JSON.cn\n在线接口测试\n在线图片编辑器 - 免费照片修改工具 (52fun.com)\n菜鸟工具 - 不止于工具 (jyshare.com)\ncompile c# online (rextester.com)\nGDB online Debugger | Compiler - Code, Compile, Run, Debug online C, C++ (onlinegdb.com)\nCodePen: Online Code Editor and Front End Web Developer Community\nRegulex：JavaScript Regular Expression Visualizer (jex.im)\nRegExr: Learn, Build, \u0026amp; Test RegEx\n镝数图表-简单好用的免费在线可视化图表制作工具 (dycharts.com)\n积木报表官网 - JimuReport报表,免费的企业级Web报表工具(可视化报表_低代码报表_在线大屏设计器)\nFlourish | Data Visualization \u0026amp; Storytelling\nOnline CSS3 Code Generator With a Simple Graphical Interface - EnjoyCSS\n免费 XML压缩/格式化 - 在线工具网(zxgj.cn) - 工作生活好帮手\n在线字数统计工具-统计字符字节汉字数字标点符号-计算word文章字数 (eteste.com)\nUrlify 短链接\n文本替换工具_在线字符串替换功能 (fly63.com)\nOnline Video Downloader - Download Any Video For Free - VideoFk\nPDF24 Tools: Free PDF solutions for all PDF problems\nSmallpdf.com – 您所有PDF问题的免费解决方案\niLovePDF | 为PDF爱好者提供的PDF文件在线处理工具\nPDF转Word_PDF在线转换_在线免费转换PDF文件 - HiPDF\ndocsmall - 免费在线图片压缩、GIF压缩工具、PDF压缩工具、PDF合并工具、PDF分割工具\n福昕云编辑 - 在线PDF编辑_在线办公_福昕PDF编辑器在线_在线编辑 (foxitcloud.cn)\nPDF24 Tools: 免费且易于使用的在线PDF工具\n全部Smallpdf工具，在线转换、压缩、编辑PDF文档\n在线文字识别转换 - 免费图片转文字工具OCR - 在线工具系列 (wdku.net)\n免费在线图片压缩、GIF压缩工具、PDF压缩工具、PDF合并工具、PDF分割工具\n分离人声 AI(vocalremover.org)\n万能视频图片解析下载 - SnapAny\n万能视频图片解析下载 - SnapAny\n无需上传文件的多功能图片批量处理程序)\n壁纸 极简壁纸_海量电脑桌面壁纸美图_4K超高清_最潮壁纸网站 (zzzmh.cn)\n4K壁纸_4K手机壁纸_4K高清壁纸大全_电脑壁纸_4K,5K,6K,7K,8K壁纸图片素材_彼岸图网 (netbian.com)\nWallpaper Abyss - The Best Cool HD Wallpapers And Backgrounds 素材 站长素材-分享综合设计素材免费下载的平台 (chinaz.com)\nフリー効果音素材・無料効果音 (taira-komori.jpn.org)\nMAKA设计_免费H5页面制作,电子婚礼请帖制作,海量免费设计模板,电子邀请函模板,微信营销,h5制作,微场景和模板素材设计分享平台\nCanva可画_在线设计协作平台_平面设计作图软件_视觉办公套件 - Canva中文官网\n在线编码解码工具 BASE64加密解密 (supfree.net)\n免费 md5加密 - 在线工具网(zxgj.cn) - 工作生活好帮手\n在线加密解密工具_Base64、AES、DES加密解密 (fly63.com)\nJSON Web Token - Decode (calebb.net)\nhttps://www.matools.com/code-convert-ascii\n免费 unicode编码转换 - 在线工具网(zxgj.cn) - 工作生活好帮手\n免费 UTF-8编码转换 - 在线工具网(zxgj.cn) - 工作生活好帮手\n免费 字符串编码解码 - 在线工具网(zxgj.cn) - 工作生活好帮手\nhttp://tool.chinaz.com/tools/urlencode.aspx?jdfwkey=lbixz1\nPPT模板 PPT模板 - PPT模板免费下载 - 免费PPT模板下载 - 微软officePLUS\npptbz.com\nPPT超级市场官网-免费、优质、高效、安全的PPT下载和定制 (pptsupermarket.com)\nPPT模板_PPT模版免费下载_免费PPT模板下载 -【第一PPT】 (1ppt.com)\nPPT模板免费下载_精美免费PPT模板下载 -【优品PPT】 (ypppt.com)\nHiPPTER | PPT资源导航 | PPT模板图表等设计素材免费下载\n免费PPT模板,PPT模板下载,幻灯片演示模板 - 51PPT模板网 (51pptmoban.com)\nPPT模板_PPT模板免费下载和在线预览 - 站长素材 (chinaz.com)\n小游戏 小霸王，其乐无穷 。红白机，FC在线游戏，街机游戏，街机在线，NES games，NES games online，Super Mario (yikm.net)\n动漫 Pixivel\nZzzFun动漫视频网 - (￣﹃￣)~zZZ\nkonachan.net - Konachan.com Anime Wallpapers\n电子书 书栈网 · BookStack_程序员IT互联网开源编程书籍免费阅读，助您【码】力十足！\n码农之家-计算机编程电子书下载网站 (xz577.com)\nJiumo Search 鸠摩搜索 - 文档搜索引擎 (jiumodiary.com)\n计算机免费书籍,电子书,pdf电子书,电子书籍,网络书籍,电脑书籍下载,编程书籍,编程电子书下载 - 脚本之家 (jb51.net)\n书格 (shuge.org)\n熊猫搜书_熊猫搜索_一站式读书学习导航站_聚合电子书及科研文档搜索_学习资料检索和分享_xmsoushu_xmsearch\nGitHub - imarvinle/awesome-cs-books: 🔥 经典编程书籍大全，涵盖：计算机系统与网络、系统架构、算法与数据结构、前端开发、后端开发、移动开发、数据库、测试、项目与团队、程序员职业修炼、求职面试等\n其他 WebGL Fluid Simulation (paveldogreat.github.io)\nSilk – Interactive Generative Art (创意光线绘画)\nStellarium Web Online Star Map (星系观察)\n术语在线 (termonline.cn)\n有趣网址之家 - 收藏全球最有趣的网站 (youquhome.com)\nVirtual Piano - Play Piano Online | AutoPiano\n纪妖（原名知妖） (cbaigui.com)\nMicrosoft数学求解器-数学问题求解器和计算器\n什么值得买_优惠券频道 | 电商优惠券_网购优惠券_电子优惠券 (smzdm.com)\n100,000 Stars (chromeexperiments.com)\n全历史 (allhistory.com)\n首页 - HelloGitHub\nQwerty Learner — 为键盘工作者设计的单词与肌肉记忆锻炼软件 (kaiyi.cool)\nzhongguose － 传统颜色\n学习成长 ExcelHome - 全球极具影响力的Excel门户,Office视频教程培训中心\nExcel学习网-Excel表格-Excel教程-Excel表格的基本操作 (excelcn.com)\n懒人Excel - Excel 函数公式、操作技巧、数据分析、图表模板、VBA、数据透视表教程 (lanrenexcel.com)\nExcel实战专家郑广学老师 | VBA代码助手官网 (excel880.com)\nC语言中文网：C语言程序设计门户网站(入门教程、编程软件) (biancheng.net)\nWPS学堂\n我要自学网\nPython教程 - 廖雪峰的官方网站 (liaoxuefeng.com)\n精选项目课程_IT热门课程_蓝桥云课课程 - 蓝桥云课 (lanqiao.cn)\n导航 学吧导航 | 四十万学习爱好者都在用的学霸导航网站 (xue8nav.com)\n果汁导航 - 上网从这里开始！ (guozhivip.com)\n国外网站推荐-分享互联网-外国网站大全 (egouz.com)\n虫部落快搜 - 搜索快人一步 - Google (chongbuluo.com)\n国外网站大全 - 世界各国网址大全,世界的,你我的! (world68.com)\naddog.vip | 广告人的网址导航 | 品牌/策划/营销/创意/文案\n果汁排行榜 - 各类榜单排名大全 (guozhivip.com)\n学吧导航 | 四十万学习爱好者都在用的学霸导航网站 (xue8nav.com)\n","permalink":"https://ktzxy.top/posts/o3vsaws6sc/","summary":"整理的一些在线工具，方便使用。","title":"工具合集"},{"content":"Go中的反射 反射 有时我们需要写一个函数，这个函数有能力统一处理各种值类型，而这些类型可能无法共享同一个接口，也可能布局未知，也有可能这个类型在我们设计函数时还不存在，这个时候我们就可以用到反射。\n空接口可以存储任意类型的变量，那我们如何知道这个空接口保存数据的类型是什么？ 值是什么呢？\n可以使用类型断言 可以使用反射实现，也就是在程序运行时动态的获取一个变量的类型信息和值信息。 把结构体序列化成json字符串，自定义结构体Tab标签的时候就用到了反射\n后面所说的ORM框架，底层就是用到了反射技术\nORM：对象关系映射（Object Relational Mapping，简称 ORM）是通过使用描述对象和数据库之间的映射的元数据，将面向对象语言程序中的对象自动持久化到关系数据库中。\n反射的基本介绍 反射是指在程序运行期间对程序本身进行访问和修改的能力。正常情况程序在编译时，变量被转换为内存地址，变量名不会被编译器写入到可执行部分。在运行程序时，程序无法获取自身的信息。支持反射的语言可以在程序编译期将变量的反射信息，如字段名称、类型信息、结构体信息等整合到可执行文件中，并给程序提供接口访问反射信息，这样就可以在程序运行期获取类型的反射信息，并且有能力修改它们。\nGo可以实现的功能 反射可以在程序运行期间动态的获取变量的各种信息，比如变量的类型类别 如果是结构体，通过反射还可以获取结构体本身的信息，比如结构体的字段、结构体的方法。 通过反射，可以修改变量的值，可以调用关联的方法 Go语言中的变量是分为两部分的：\n类型信息：预先定义好的元信息。 值信息：程序运行过程中可动态变化的。 在Go语言的反射机制中，任何接口值都由是一个具体类型和具体类型的值两部分组成的。\n在Go语言中反射的相关功能由内置的reflect包提供，任意接口值在反射中都可以理解为由 reflect.Type 和 reflect.Value两部分组成，并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个重要函数来获取任意对象的Value 和 Type\nreflect.TypeOf()获取任意值的类型对象 在Go 语言中，使用reflect.TypeOf（）函数可以接受任意interface}参数，可以获得任意值的类型对象（reflect.Type），程序通过类型对象可以访问任意值的类型信息。\n通过反射获取空接口的类型\n1 2 3 4 5 6 7 8 9 10 func reflectFun(x interface{}) { v := reflect.TypeOf(x) fmt.Println(v) } func main() { reflectFun(10) reflectFun(10.01) reflectFun(\u0026#34;abc\u0026#34;) reflectFun(true) } type name 和 type Kind 在反射中关于类型还划分为两种：类型（Type）和种类（Kind）。因为在Go语言中我们可以使用type关键字构造很多自定义类型，而种类（Kid）就是指底层的类型，但在反射中，当需要区分指针、结构体等大品种的类型时，就会用到种类（Kind）。举个例子，我们定义了两个指针类型和两个结构体类型，通过反射查看它们的类型和种类。\nGo 语言的反射中像数组、切片、Map、指针等类型的变量，它们的.Name（）都是返回空。\n1 2 3 4 v := reflect.TypeOf(x) fmt.Println(\u0026#34;类型 \u0026#34;, v) fmt.Println(\u0026#34;类型名称 \u0026#34;, v.Name()) fmt.Println(\u0026#34;类型种类 \u0026#34;, v.Kind()) 我们之前可以通过类型断言来实现空接口类型的数相加操作\n1 2 3 4 5 func reflectValue(x interface{}) { b,_ := x.(int) var num = 10 + b fmt.Println(num) } 到现在的话，我们就可以使用reflect.TypeOf来实现了\n1 2 3 4 5 6 7 8 func reflectValue2(x interface{}) { // 通过反射来获取变量的原始值 v := reflect.ValueOf(x) fmt.Println(v) // 获取到V的int类型 var n = v.Int() + 12 fmt.Println(n) } 同时我们还可以通过switch来完成\n1 2 3 4 5 6 7 8 9 10 11 12 // 通过反射来获取变量的原始值 v := reflect.ValueOf(x) // 获取种类 kind := v.Kind() switch kind { case reflect.Int: fmt.Println(\u0026#34;我是int类型\u0026#34;) case reflect.Float64: fmt.Println(\u0026#34;我是float64类型\u0026#34;) default: fmt.Println(\u0026#34;我是其它类型\u0026#34;) } reflect.ValueOf reflect.ValueOf() 返回的是reflect.Value类型，其中包含了原始值的值信息，reflect.Value与原始值之间可以互相转换\nreflect.value类型提供的获取原始值的方法如下\n方法 说明 interface{} 将值以interface{}类型返回，可以通过类型断言转换为指定类型 Int() int64 将值以int类型返回，所有有符号整型均可以此方式返回 Uint() uint64 将值以uint类型返回，所有无符号整型均可以以此方式返回 Float() float64 将值以双精度(float 64)类型返回，所有浮点数(float 32、float64)均可以以此方式返回 结构体反射 与结构体相关的方法 任意值通过reflect.Typeof）获得反射对象信息后，如果它的类型是结构体，可以通过反射值对象（reflect.Type）的NumField（）和Field（）方法获得结构体成员的详细信息。\nreflect.Type中与获取结构体成员相关的的方法如下表所示。\n方法 说明 Field(i int)StructField 根据索引，返回索引对应的结构体字段的信息 NumField() int 返回结构体成员字段数量 FieldByName(name string)(StructField, bool) 根据给定字符串返回字符串赌赢的结构体字段信息 FieldByIndex(index []int)StructField 多层成员访问时，根据[] int 提供的每个结构 示例代码，如下所示 我们修改结构体中的字段和类型\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 // 学生结构体 type Student4 struct { Name string `json: \u0026#34;name\u0026#34;` Age int `json: \u0026#34;age\u0026#34;` Score int `json: \u0026#34;score\u0026#34;` } func (s Student4)GetInfo()string { var str = fmt.Sprintf(\u0026#34;姓名：%v 年龄：%v 成绩：%v\u0026#34;, s.Name, s.Age, s.Score) return str } func (s *Student4)SetInfo(name string, age int, score int) { s.Name = name s.Age = age s.Score = score } func (s Student4)PrintStudent() { fmt.Println(\u0026#34;打印学生\u0026#34;) } // 打印结构体中的字段 func PrintStructField(s interface{}) { t := reflect.TypeOf(s) // 判断传递过来的是否是结构体 if t.Kind() != reflect.Struct \u0026amp;\u0026amp; t.Elem().Kind() != reflect.Struct { fmt.Println(\u0026#34;请传入结构体类型!\u0026#34;) return } // 通过类型变量里面的Field可以获取结构体的字段 field0 := t.Field(0) // 获取第0个字段 fmt.Printf(\u0026#34;%#v \\n\u0026#34;, field0) fmt.Println(\u0026#34;字段名称:\u0026#34;, field0.Name) fmt.Println(\u0026#34;字段类型:\u0026#34;, field0.Type) fmt.Println(\u0026#34;字段Tag:\u0026#34;, field0.Tag.Get(\u0026#34;json\u0026#34;)) // 通过类型变量里面的FieldByName可以获取结构体的字段中 field1, ok := t.FieldByName(\u0026#34;Age\u0026#34;) if ok { fmt.Println(\u0026#34;字段名称:\u0026#34;, field1.Name) fmt.Println(\u0026#34;字段类型:\u0026#34;, field1.Type) fmt.Println(\u0026#34;字段Tag:\u0026#34;, field1.Tag) } // 通过类型变量里面的NumField获取该结构体有几个字段 var fieldCount = t.NumField() fmt.Println(\u0026#34;结构体有：\u0026#34;, fieldCount, \u0026#34; 个属性\u0026#34;) // 获取结构体属性对应的值 v := reflect.ValueOf(s) nameValue := v.FieldByName(\u0026#34;Name\u0026#34;) fmt.Println(\u0026#34;nameValue:\u0026#34;, nameValue) } func main() { student := Student4{ \u0026#34;张三\u0026#34;, 18, 95, } PrintStructField(student) } 下列代码是获取结构体中的方法，然后调用\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 // 打印执行方法 func PrintStructFn(s interface{}) { t := reflect.TypeOf(s) // 判断传递过来的是否是结构体 if t.Kind() != reflect.Struct \u0026amp;\u0026amp; t.Elem().Kind() != reflect.Struct { fmt.Println(\u0026#34;请传入结构体类型!\u0026#34;) return } // 通过类型变量里面的Method，可以获取结构体的方法 method0 := t.Method(0) // 获取第一个方法， 这个是和ACSII相关 fmt.Println(method0.Name) // 通过类型变量获取这个结构体有多少方法 methodCount := t.NumMethod() fmt.Println(\u0026#34;拥有的方法\u0026#34;, methodCount) // 通过值变量 执行方法（注意需要使用值变量，并且要注意参数） v := reflect.ValueOf(s) // 通过值变量来获取参数 v.MethodByName(\u0026#34;PrintStudent\u0026#34;).Call(nil) // 手动传参 var params []reflect.Value params = append(params, reflect.ValueOf(\u0026#34;张三\u0026#34;)) params = append(params, reflect.ValueOf(23)) params = append(params, reflect.ValueOf(99)) // 执行setInfo方法 v.MethodByName(\u0026#34;SetInfo\u0026#34;).Call(params) // 通过值变量来获取参数 v.MethodByName(\u0026#34;PrintStudent\u0026#34;).Call(nil) } ","permalink":"https://ktzxy.top/posts/carxtv5tl8/","summary":"16 Golang中的反射","title":"16 Golang中的反射"},{"content":"单元测试 来源 单元测试：https://www.liwenzhou.com/posts/Go/16_test/\n性能测试：https://www.liwenzhou.com/posts/Go/performance_optimisation/\n前言 不写测试的开发不是好程序员。我个人非常崇尚TDD（Test Driven Development）的，然而可惜的是国内的程序员都不太关注测试这一部分。 这篇文章主要介绍下在Go语言中如何做单元测试和基准测试。\nGo test工具 Go语言中的测试依赖go test命令。编写测试代码和编写普通的Go代码过程是类似的，并不需要学习新的语法、规则或工具。\ngo test命令是一个按照一定约定和组织的测试代码的驱动程序。在包目录内，所有以_test.go为后缀名的源代码文件都是go test测试的一部分，不会被go build编译到最终的可执行文件中。\n在*_test.go文件中有三种类型的函数，单元测试函数、基准测试函数和示例函数。\n类型 格式 作用 测试函数 函数名前缀为Test 测试程序的一些逻辑行为是否正确 基准函数 函数名前缀为Benchmark 测试函数的性能 示例函数 函数名前缀为Example 为文档提供示例文档 go test命令会遍历所有的*_test.go文件中符合上述命名规则的函数，然后生成一个临时的main包用于调用相应的测试函数，然后构建并运行、报告测试结果，最后清理测试中生成的临时文件。\n测试函数的格式 每个测试函数必须导入testing包，测试函数的基本格式（签名）如下：\n1 2 3 func TestName(t *testing.T){ // ... } 测试函数的名字必须以Test开头，可选的后缀名必须以大写字母开头，举几个例子：\n1 2 3 func TestAdd(t *testing.T){ ... } func TestSum(t *testing.T){ ... } func TestLog(t *testing.T){ ... } 其中参数t用于报告测试失败和附加的日志信息。 testing.T的拥有的方法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func (c *T) Error(args ...interface{}) func (c *T) Errorf(format string, args ...interface{}) func (c *T) Fail() func (c *T) FailNow() func (c *T) Failed() bool func (c *T) Fatal(args ...interface{}) func (c *T) Fatalf(format string, args ...interface{}) func (c *T) Log(args ...interface{}) func (c *T) Logf(format string, args ...interface{}) func (c *T) Name() string func (t *T) Parallel() func (t *T) Run(name string, f func(t *T)) bool func (c *T) Skip(args ...interface{}) func (c *T) SkipNow() func (c *T) Skipf(format string, args ...interface{}) func (c *T) Skipped() bool 测试函数示例 就像细胞是构成我们身体的基本单位，一个软件程序也是由很多单元组件构成的。单元组件可以是函数、结构体、方法和最终用户可能依赖的任意东西。总之我们需要确保这些组件是能够正常运行的。单元测试是一些利用各种方法测试单元组件的程序，它会将结果与预期输出进行比较。\n接下来，我们定义一个split的包，包中定义了一个Split函数，具体实现如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // split/split.go package split import \u0026#34;strings\u0026#34; // split package with a single split function. // Split slices s into all substrings separated by sep and // returns a slice of the substrings between those separators. func Split(s, sep string) (result []string) { i := strings.Index(s, sep) for i \u0026gt; -1 { result = append(result, s[:i]) s = s[i+1:] i = strings.Index(s, sep) } result = append(result, s) return } 在当前目录下，我们创建一个split_test.go的测试文件，并定义一个测试函数如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // split/split_test.go package split import ( \u0026#34;reflect\u0026#34; \u0026#34;testing\u0026#34; ) func TestSplit(t *testing.T) { // 测试函数名必须以Test开头，必须接收一个*testing.T类型参数 got := Split(\u0026#34;a:b:c\u0026#34;, \u0026#34;:\u0026#34;) // 程序输出的结果 want := []string{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;} // 期望的结果 if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接，借助反射包中的方法比较 t.Errorf(\u0026#34;excepted:%v, got:%v\u0026#34;, want, got) // 测试失败输出错误提示 } } 此时split这个包中的文件如下：\n1 2 3 4 split $ ls -l total 16 -rw-r--r-- 1 liwenzhou staff 408 4 29 15:50 split.go -rw-r--r-- 1 liwenzhou staff 466 4 29 16:04 split_test.go 在split包路径下，执行go test命令，可以看到输出结果如下：\n1 2 3 split $ go test PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s 一个测试用例有点单薄，我们再编写一个测试使用多个字符切割字符串的例子，在split_test.go中添加如下测试函数：\n1 2 3 4 5 6 7 func TestMoreSplit(t *testing.T) { got := Split(\u0026#34;abcd\u0026#34;, \u0026#34;bc\u0026#34;) want := []string{\u0026#34;a\u0026#34;, \u0026#34;d\u0026#34;} if !reflect.DeepEqual(want, got) { t.Errorf(\u0026#34;excepted:%v, got:%v\u0026#34;, want, got) } } 再次运行go test命令，输出结果如下：\n1 2 3 4 5 6 split $ go test --- FAIL: TestMultiSplit (0.00s) split_test.go:20: excepted:[a d], got:[a cd] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 这一次，我们的测试失败了。我们可以为go test命令添加-v参数，查看测试函数名称和运行时间：\n1 2 3 4 5 6 7 8 9 split $ go test -v === RUN TestSplit --- PASS: TestSplit (0.00s) === RUN TestMoreSplit --- FAIL: TestMoreSplit (0.00s) split_test.go:21: excepted:[a d], got:[a cd] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s 这一次我们能清楚的看到是TestMoreSplit这个测试没有成功。 还可以在go test命令后添加-run参数，它对应一个正则表达式，只有函数名匹配上的测试函数才会被go test命令执行。\n1 2 3 4 5 6 7 split $ go test -v -run=\u0026#34;More\u0026#34; === RUN TestMoreSplit --- FAIL: TestMoreSplit (0.00s) split_test.go:21: excepted:[a d], got:[a cd] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 现在我们回过头来解决我们程序中的问题。很显然我们最初的split函数并没有考虑到sep为多个字符的情况，我们来修复下这个Bug：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package split import \u0026#34;strings\u0026#34; // split package with a single split function. // Split slices s into all substrings separated by sep and // returns a slice of the substrings between those separators. func Split(s, sep string) (result []string) { i := strings.Index(s, sep) for i \u0026gt; -1 { result = append(result, s[:i]) s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度 i = strings.Index(s, sep) } result = append(result, s) return } 这一次我们再来测试一下，我们的程序。注意，当我们修改了我们的代码之后不要仅仅执行那些失败的测试函数，我们应该完整的运行所有的测试，保证不会因为修改代码而引入了新的问题。\n1 2 3 4 5 6 7 split $ go test -v === RUN TestSplit --- PASS: TestSplit (0.00s) === RUN TestMoreSplit --- PASS: TestMoreSplit (0.00s) PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 这一次我们的测试都通过了。\n测试组 我们现在还想要测试一下split函数对中文字符串的支持，这个时候我们可以再编写一个TestChineseSplit测试函数，但是我们也可以使用如下更友好的一种方式来添加更多的测试用例。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func TestSplit(t *testing.T) { // 定义一个测试用例类型 type test struct { input string sep string want []string } // 定义一个存储测试用例的切片 tests := []test{ {input: \u0026#34;a:b:c\u0026#34;, sep: \u0026#34;:\u0026#34;, want: []string{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;}}, {input: \u0026#34;a:b:c\u0026#34;, sep: \u0026#34;,\u0026#34;, want: []string{\u0026#34;a:b:c\u0026#34;}}, {input: \u0026#34;abcd\u0026#34;, sep: \u0026#34;bc\u0026#34;, want: []string{\u0026#34;a\u0026#34;, \u0026#34;d\u0026#34;}}, {input: \u0026#34;沙河有沙又有河\u0026#34;, sep: \u0026#34;沙\u0026#34;, want: []string{\u0026#34;河有\u0026#34;, \u0026#34;又有河\u0026#34;}}, } // 遍历切片，逐一执行测试用例 for _, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf(\u0026#34;excepted:%v, got:%v\u0026#34;, tc.want, got) } } } 我们通过上面的代码把多个测试用例合到一起，再次执行go test命令。\n1 2 3 4 5 6 7 split $ go test -v === RUN TestSplit --- FAIL: TestSplit (0.00s) split_test.go:42: excepted:[河有 又有河], got:[ 河有 又有河] FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 我们的测试出现了问题，仔细看打印的测试失败提示信息：excepted:[河有 又有河], got:[ 河有 又有河]，你会发现[ 河有 又有河]中有个不明显的空串，这种情况下十分推荐使用%#v的格式化方式。\n我们修改下测试用例的格式化输出错误提示部分：\n1 2 3 4 5 6 7 8 9 10 func TestSplit(t *testing.T) { ... for _, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf(\u0026#34;excepted:%#v, got:%#v\u0026#34;, tc.want, got) } } } 此时运行go test命令后就能看到比较明显的提示信息了：\n1 2 3 4 5 6 7 split $ go test -v === RUN TestSplit --- FAIL: TestSplit (0.00s) split_test.go:42: excepted:[]string{\u0026#34;河有\u0026#34;, \u0026#34;又有河\u0026#34;}, got:[]string{\u0026#34;\u0026#34;, \u0026#34;河有\u0026#34;, \u0026#34;又有河\u0026#34;} FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 子测试 看起来都挺不错的，但是如果测试用例比较多的时候，我们是没办法一眼看出来具体是哪个测试用例失败了。我们可能会想到下面的解决办法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func TestSplit(t *testing.T) { type test struct { // 定义test结构体 input string sep string want []string } tests := map[string]test{ // 测试用例使用map存储 \u0026#34;simple\u0026#34;: {input: \u0026#34;a:b:c\u0026#34;, sep: \u0026#34;:\u0026#34;, want: []string{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;}}, \u0026#34;wrong sep\u0026#34;: {input: \u0026#34;a:b:c\u0026#34;, sep: \u0026#34;,\u0026#34;, want: []string{\u0026#34;a:b:c\u0026#34;}}, \u0026#34;more sep\u0026#34;: {input: \u0026#34;abcd\u0026#34;, sep: \u0026#34;bc\u0026#34;, want: []string{\u0026#34;a\u0026#34;, \u0026#34;d\u0026#34;}}, \u0026#34;leading sep\u0026#34;: {input: \u0026#34;沙河有沙又有河\u0026#34;, sep: \u0026#34;沙\u0026#34;, want: []string{\u0026#34;河有\u0026#34;, \u0026#34;又有河\u0026#34;}}, } for name, tc := range tests { got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf(\u0026#34;name:%s excepted:%#v, got:%#v\u0026#34;, name, tc.want, got) // 将测试用例的name格式化输出 } } } 上面的做法是能够解决问题的。同时Go1.7+中新增了子测试，我们可以按照如下方式使用t.Run执行子测试：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func TestSplit(t *testing.T) { type test struct { // 定义test结构体 input string sep string want []string } tests := map[string]test{ // 测试用例使用map存储 \u0026#34;simple\u0026#34;: {input: \u0026#34;a:b:c\u0026#34;, sep: \u0026#34;:\u0026#34;, want: []string{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;}}, \u0026#34;wrong sep\u0026#34;: {input: \u0026#34;a:b:c\u0026#34;, sep: \u0026#34;,\u0026#34;, want: []string{\u0026#34;a:b:c\u0026#34;}}, \u0026#34;more sep\u0026#34;: {input: \u0026#34;abcd\u0026#34;, sep: \u0026#34;bc\u0026#34;, want: []string{\u0026#34;a\u0026#34;, \u0026#34;d\u0026#34;}}, \u0026#34;leading sep\u0026#34;: {input: \u0026#34;沙河有沙又有河\u0026#34;, sep: \u0026#34;沙\u0026#34;, want: []string{\u0026#34;河有\u0026#34;, \u0026#34;又有河\u0026#34;}}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试 got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf(\u0026#34;excepted:%#v, got:%#v\u0026#34;, tc.want, got) } }) } } 此时我们再执行go test命令就能够看到更清晰的输出内容了：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 split $ go test -v === RUN TestSplit === RUN TestSplit/leading_sep === RUN TestSplit/simple === RUN TestSplit/wrong_sep === RUN TestSplit/more_sep --- FAIL: TestSplit (0.00s) --- FAIL: TestSplit/leading_sep (0.00s) split_test.go:83: excepted:[]string{\u0026#34;河有\u0026#34;, \u0026#34;又有河\u0026#34;}, got:[]string{\u0026#34;\u0026#34;, \u0026#34;河有\u0026#34;, \u0026#34;又有河\u0026#34;} --- PASS: TestSplit/simple (0.00s) --- PASS: TestSplit/wrong_sep (0.00s) --- PASS: TestSplit/more_sep (0.00s) FAIL exit status 1 FAIL github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 这个时候我们要把测试用例中的错误修改回来：\n1 2 3 4 5 6 7 8 9 10 func TestSplit(t *testing.T) { ... tests := map[string]test{ // 测试用例使用map存储 \u0026#34;simple\u0026#34;: {input: \u0026#34;a:b:c\u0026#34;, sep: \u0026#34;:\u0026#34;, want: []string{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;}}, \u0026#34;wrong sep\u0026#34;: {input: \u0026#34;a:b:c\u0026#34;, sep: \u0026#34;,\u0026#34;, want: []string{\u0026#34;a:b:c\u0026#34;}}, \u0026#34;more sep\u0026#34;: {input: \u0026#34;abcd\u0026#34;, sep: \u0026#34;bc\u0026#34;, want: []string{\u0026#34;a\u0026#34;, \u0026#34;d\u0026#34;}}, \u0026#34;leading sep\u0026#34;: {input: \u0026#34;沙河有沙又有河\u0026#34;, sep: \u0026#34;沙\u0026#34;, want: []string{\u0026#34;\u0026#34;, \u0026#34;河有\u0026#34;, \u0026#34;又有河\u0026#34;}}, } ... } 我们都知道可以通过-run=RegExp来指定运行的测试用例，还可以通过/来指定要运行的子测试用例，\n例如：go test -v -run=Split/simple 只会运行simple对应的子测试用例。\n测试覆盖率 测试覆盖率是你的代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率，也就是在测试中至少被运行一次的代码占总代码的比例。\nGo提供内置功能来检查你的代码覆盖率。我们可以使用go test -cover来查看测试覆盖率。例如：\n1 2 3 4 split $ go test -cover PASS coverage: 100.0% of statements ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s 从上面的结果可以看到我们的测试用例覆盖了100%的代码。\nGo还提供了一个额外的-coverprofile参数，用来将覆盖率相关的记录信息输出到一个文件。例如：\n1 2 3 4 split $ go test -cover -coverprofile=c.out PASS coverage: 100.0% of statements ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.005s 上面的命令会将覆盖率相关的信息输出到当前文件夹下面的c.out文件中，然后我们执行go tool cover -html=c.out，使用cover工具来处理生成的记录信息，该命令会打开本地的浏览器窗口生成一个HTML报告。上图中每个用绿色标记的语句块表示被覆盖了，而红色的表示没有被覆盖。\n基准测试 基准测试函数格式 基准测试就是在一定的工作负载之下检测程序性能的一种方法。基准测试的基本格式如下：\n1 2 3 func BenchmarkName(b *testing.B){ // ... } 基准测试以Benchmark为前缀，需要一个*testing.B类型的参数b，基准测试必须要执行b.N次，这样的测试才有对照性，b.N的值是系统根据实际情况去调整的，从而保证测试的稳定性。 testing.B拥有的方法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func (c *B) Error(args ...interface{}) func (c *B) Errorf(format string, args ...interface{}) func (c *B) Fail() func (c *B) FailNow() func (c *B) Failed() bool func (c *B) Fatal(args ...interface{}) func (c *B) Fatalf(format string, args ...interface{}) func (c *B) Log(args ...interface{}) func (c *B) Logf(format string, args ...interface{}) func (c *B) Name() string func (b *B) ReportAllocs() func (b *B) ResetTimer() func (b *B) Run(name string, f func(b *B)) bool func (b *B) RunParallel(body func(*PB)) func (b *B) SetBytes(n int64) func (b *B) SetParallelism(p int) func (c *B) Skip(args ...interface{}) func (c *B) SkipNow() func (c *B) Skipf(format string, args ...interface{}) func (c *B) Skipped() bool func (b *B) StartTimer() func (b *B) StopTimer() 基准测试示例 我们为split包中的Split函数编写基准测试如下：\n1 2 3 4 5 func BenchmarkSplit(b *testing.B) { for i := 0; i \u0026lt; b.N; i++ { Split(\u0026#34;沙河有沙又有河\u0026#34;, \u0026#34;沙\u0026#34;) } } 基准测试并不会默认执行，需要增加-bench参数，所以我们通过执行go test -bench=Split命令执行基准测试，输出结果如下：\n1 2 3 4 5 6 7 split $ go test -bench=Split goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 203 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 2.255s 其中BenchmarkSplit-8表示对Split函数进行基准测试，数字8表示GOMAXPROCS的值，这个对于并发基准测试很重要。10000000和203ns/op表示每次调用Split函数耗时203ns，这个结果是10000000次调用的平均值。\n我们还可以为基准测试添加-benchmem参数，来获得内存分配的统计数据。\n1 2 3 4 5 6 7 split $ go test -bench=Split -benchmem goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 215 ns/op 112 B/op 3 allocs/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 2.394s 其中，112 B/op表示每次操作内存分配了112字节，3 allocs/op则表示每次操作进行了3次内存分配。 我们将我们的Split函数优化如下：\n1 2 3 4 5 6 7 8 9 10 11 func Split(s, sep string) (result []string) { result = make([]string, 0, strings.Count(s, sep)+1) i := strings.Index(s, sep) for i \u0026gt; -1 { result = append(result, s[:i]) s = s[i+len(sep):] // 这里使用len(sep)获取sep的长度 i = strings.Index(s, sep) } result = append(result, s) return } 这一次我们提前使用make函数将result初始化为一个容量足够大的切片，而不再像之前一样通过调用append函数来追加。我们来看一下这个改进会带来多大的性能提升：\n1 2 3 4 5 6 7 split $ go test -bench=Split -benchmem goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 127 ns/op 48 B/op 1 allocs/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 1.423s 这个使用make函数提前分配内存的改动，减少了2/3的内存分配次数，并且减少了一半的内存分配。\n性能比较函数 上面的基准测试只能得到给定操作的绝对耗时，但是在很多性能问题是发生在两个不同操作之间的相对耗时，比如同一个函数处理1000个元素的耗时与处理1万甚至100万个元素的耗时的差别是多少？再或者对于同一个任务究竟使用哪种算法性能最佳？我们通常需要对两个不同算法的实现使用相同的输入来进行基准比较测试。\n性能比较函数通常是一个带有参数的函数，被多个不同的Benchmark函数传入不同的值来调用。举个例子如下：\n1 2 3 4 func benchmark(b *testing.B, size int){/* ... */} func Benchmark10(b *testing.B){ benchmark(b, 10) } func Benchmark100(b *testing.B){ benchmark(b, 100) } func Benchmark1000(b *testing.B){ benchmark(b, 1000) } 例如我们编写了一个计算斐波那契数列的函数如下：\n1 2 3 4 5 6 7 8 9 // fib.go // Fib 是一个计算第n个斐波那契数的函数 func Fib(n int) int { if n \u0026lt; 2 { return n } return Fib(n-1) + Fib(n-2) } 我们编写的性能比较函数如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // fib_test.go func benchmarkFib(b *testing.B, n int) { for i := 0; i \u0026lt; b.N; i++ { Fib(n) } } func BenchmarkFib1(b *testing.B) { benchmarkFib(b, 1) } func BenchmarkFib2(b *testing.B) { benchmarkFib(b, 2) } func BenchmarkFib3(b *testing.B) { benchmarkFib(b, 3) } func BenchmarkFib10(b *testing.B) { benchmarkFib(b, 10) } func BenchmarkFib20(b *testing.B) { benchmarkFib(b, 20) } func BenchmarkFib40(b *testing.B) { benchmarkFib(b, 40) } 运行基准测试：\n1 2 3 4 5 6 7 8 9 10 11 12 split $ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/fib BenchmarkFib1-8 1000000000 2.03 ns/op BenchmarkFib2-8 300000000 5.39 ns/op BenchmarkFib3-8 200000000 9.71 ns/op BenchmarkFib10-8 5000000 325 ns/op BenchmarkFib20-8 30000 42460 ns/op BenchmarkFib40-8 2 638524980 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/fib 12.944s 这里需要注意的是，默认情况下，每个基准测试至少运行1秒。如果在Benchmark函数返回时没有到1秒，则b.N的值会按1,2,5,10,20,50，…增加，并且函数再次运行。\n最终的BenchmarkFib40只运行了两次，每次运行的平均值只有不到一秒。像这种情况下我们应该可以使用-benchtime标志增加最小基准时间，以产生更准确的结果。例如：\n1 2 3 4 5 6 7 split $ go test -bench=Fib40 -benchtime=20s goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/fib BenchmarkFib40-8 50 663205114 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/fib 33.849s 这一次BenchmarkFib40函数运行了50次，结果就会更准确一些了。\n使用性能比较函数做测试的时候一个容易犯的错误就是把b.N作为输入的大小，例如以下两个例子都是错误的示范：\n1 2 3 4 5 6 7 8 9 10 11 // 错误示范1 func BenchmarkFibWrong(b *testing.B) { for n := 0; n \u0026lt; b.N; n++ { Fib(n) } } // 错误示范2 func BenchmarkFibWrong2(b *testing.B) { Fib(b.N) } 重置时间 b.ResetTimer之前的处理不会放到执行时间里，也不会输出到报告中，所以可以在之前做一些不计划作为测试报告的操作。例如：\n1 2 3 4 5 6 7 func BenchmarkSplit(b *testing.B) { time.Sleep(5 * time.Second) // 假设需要做一些耗时的无关操作 b.ResetTimer() // 重置计时器 for i := 0; i \u0026lt; b.N; i++ { Split(\u0026#34;沙河有沙又有河\u0026#34;, \u0026#34;沙\u0026#34;) } } 并行测试 func (b *B) RunParallel(body func(*PB))会以并行的方式执行给定的基准测试。\nRunParallel会创建出多个goroutine，并将b.N分配给这些goroutine执行， 其中goroutine数量的默认值为GOMAXPROCS。用户如果想要增加非CPU受限（non-CPU-bound）基准测试的并行性， 那么可以在RunParallel之前调用SetParallelism 。RunParallel通常会与-cpu标志一同使用。\n1 2 3 4 5 6 7 8 func BenchmarkSplitParallel(b *testing.B) { // b.SetParallelism(1) // 设置使用的CPU数 b.RunParallel(func(pb *testing.PB) { for pb.Next() { Split(\u0026#34;沙河有沙又有河\u0026#34;, \u0026#34;沙\u0026#34;) } }) } 执行一下基准测试：\n1 2 3 4 5 6 7 8 split $ go test -bench=. goos: darwin goarch: amd64 pkg: github.com/Q1mi/studygo/code_demo/test_demo/split BenchmarkSplit-8 10000000 131 ns/op BenchmarkSplitParallel-8 50000000 36.1 ns/op PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 3.308s 还可以通过在测试命令后添加-cpu参数如go test -bench=. -cpu 1来指定使用的CPU数量。\nSetup与TearDown 测试程序有时需要在测试之前进行额外的设置（setup）或在测试之后进行拆卸（teardown）。\nTestMain 通过在*_test.go文件中定义TestMain函数来可以在测试之前进行额外的设置（setup）或在测试之后进行拆卸（teardown）操作。\n如果测试文件包含函数:func TestMain(m *testing.M)那么生成的测试会先调用 TestMain(m)，然后再运行具体测试。TestMain运行在主goroutine中, 可以在调用 m.Run前后做任何设置（setup）和拆卸（teardown）。退出测试的时候应该使用m.Run的返回值作为参数调用os.Exit。\n一个使用TestMain来设置Setup和TearDown的示例如下：\n1 2 3 4 5 6 7 func TestMain(m *testing.M) { fmt.Println(\u0026#34;write setup code here...\u0026#34;) // 测试之前的做一些设置 // 如果 TestMain 使用了 flags，这里应该加上flag.Parse() retCode := m.Run() // 执行测试 fmt.Println(\u0026#34;write teardown code here...\u0026#34;) // 测试之后做一些拆卸工作 os.Exit(retCode) // 退出测试 } 需要注意的是：在调用TestMain时, flag.Parse并没有被调用。所以如果TestMain 依赖于command-line标志 (包括 testing 包的标记), 则应该显示的调用flag.Parse。\n子测试的Setup与Teardown 有时候我们可能需要为每个测试集设置Setup与Teardown，也有可能需要为每个子测试设置Setup与Teardown。下面我们定义两个函数工具函数如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 测试集的Setup与Teardown func setupTestCase(t *testing.T) func(t *testing.T) { t.Log(\u0026#34;如有需要在此执行:测试之前的setup\u0026#34;) return func(t *testing.T) { t.Log(\u0026#34;如有需要在此执行:测试之后的teardown\u0026#34;) } } // 子测试的Setup与Teardown func setupSubTest(t *testing.T) func(t *testing.T) { t.Log(\u0026#34;如有需要在此执行:子测试之前的setup\u0026#34;) return func(t *testing.T) { t.Log(\u0026#34;如有需要在此执行:子测试之后的teardown\u0026#34;) } } 使用方式如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func TestSplit(t *testing.T) { type test struct { // 定义test结构体 input string sep string want []string } tests := map[string]test{ // 测试用例使用map存储 \u0026#34;simple\u0026#34;: {input: \u0026#34;a:b:c\u0026#34;, sep: \u0026#34;:\u0026#34;, want: []string{\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;}}, \u0026#34;wrong sep\u0026#34;: {input: \u0026#34;a:b:c\u0026#34;, sep: \u0026#34;,\u0026#34;, want: []string{\u0026#34;a:b:c\u0026#34;}}, \u0026#34;more sep\u0026#34;: {input: \u0026#34;abcd\u0026#34;, sep: \u0026#34;bc\u0026#34;, want: []string{\u0026#34;a\u0026#34;, \u0026#34;d\u0026#34;}}, \u0026#34;leading sep\u0026#34;: {input: \u0026#34;沙河有沙又有河\u0026#34;, sep: \u0026#34;沙\u0026#34;, want: []string{\u0026#34;\u0026#34;, \u0026#34;河有\u0026#34;, \u0026#34;又有河\u0026#34;}}, } teardownTestCase := setupTestCase(t) // 测试之前执行setup操作 defer teardownTestCase(t) // 测试之后执行testdoen操作 for name, tc := range tests { t.Run(name, func(t *testing.T) { // 使用t.Run()执行子测试 teardownSubTest := setupSubTest(t) // 子测试之前执行setup操作 defer teardownSubTest(t) // 测试之后执行testdoen操作 got := Split(tc.input, tc.sep) if !reflect.DeepEqual(got, tc.want) { t.Errorf(\u0026#34;excepted:%#v, got:%#v\u0026#34;, tc.want, got) } }) } } 测试结果如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 split $ go test -v === RUN TestSplit === RUN TestSplit/simple === RUN TestSplit/wrong_sep === RUN TestSplit/more_sep === RUN TestSplit/leading_sep --- PASS: TestSplit (0.00s) split_test.go:71: 如有需要在此执行:测试之前的setup --- PASS: TestSplit/simple (0.00s) split_test.go:79: 如有需要在此执行:子测试之前的setup split_test.go:81: 如有需要在此执行:子测试之后的teardown --- PASS: TestSplit/wrong_sep (0.00s) split_test.go:79: 如有需要在此执行:子测试之前的setup split_test.go:81: 如有需要在此执行:子测试之后的teardown --- PASS: TestSplit/more_sep (0.00s) split_test.go:79: 如有需要在此执行:子测试之前的setup split_test.go:81: 如有需要在此执行:子测试之后的teardown --- PASS: TestSplit/leading_sep (0.00s) split_test.go:79: 如有需要在此执行:子测试之前的setup split_test.go:81: 如有需要在此执行:子测试之后的teardown split_test.go:73: 如有需要在此执行:测试之后的teardown === RUN ExampleSplit --- PASS: ExampleSplit (0.00s) PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 示例函数 示例函数的格式 被go test特殊对待的第三种函数就是示例函数，它们的函数名以Example为前缀。它们既没有参数也没有返回值。标准格式如下：\n1 2 3 func ExampleName() { // ... } 示例函数示例 下面的代码是我们为Split函数编写的一个示例函数：\n1 2 3 4 5 6 7 func ExampleSplit() { fmt.Println(split.Split(\u0026#34;a:b:c\u0026#34;, \u0026#34;:\u0026#34;)) fmt.Println(split.Split(\u0026#34;沙河有沙又有河\u0026#34;, \u0026#34;沙\u0026#34;)) // Output: // [a b c] // [ 河有 又有河] } 为你的代码编写示例代码有如下三个用处：\n示例函数能够作为文档直接使用，例如基于web的godoc中能把示例函数与对应的函数或包相关联。\n示例函数只要包含了// Output:也是可以通过go test运行的可执行测试。\n1 2 3 split $ go test -run Example PASS ok github.com/Q1mi/studygo/code_demo/test_demo/split 0.006s 示例函数提供了可以直接运行的示例代码，可以直接在golang.org的godoc文档服务器上使用Go Playground运行示例代码。下图为strings.ToUpper函数在Playground的示例函数效果。\n练习题 编写一个回文检测函数，并为其编写单元测试和基准测试，根据测试的结果逐步对其进行优化。（回文：一个字符串正序和逆序一样，如“Madam,I’mAdam”、“油灯少灯油”等。） ","permalink":"https://ktzxy.top/posts/6xkk74e3qy/","summary":"单元测试","title":"单元测试"},{"content":"01 工厂方法 追 MM 少不了请吃饭了，麦当劳的鸡翅和肯德基的鸡翅都是 MM 爱吃的东西，虽然口味有所不同，但不管你带 MM 去麦当劳或肯德基，只管向服务员说「来四个鸡翅」就行了。麦当劳和肯德基就是生产鸡翅的 Factory 工厂模式：客户类和工厂类分开。 消费者任何时候需要某种产品，只需向工厂请求即可。消费者无须修改就可以接纳新产品。缺点是当产品修改时，工厂类也要做相应的修改。如：如何创建及如何向客户端提供。\n02 建造者模式 MM 最爱听的就是「我爱你」这句话了，见到不同地方的 MM，要能够用她们的方言跟她说这句话哦，我有一个多种语言翻译机，上面每种语言都有一个按键，见到 MM 我只要按对应的键，它就能够用相应的语言说出「我爱你」这句话了，国外的 MM 也可以轻松搞掂，这就是我的「我爱你」builder。 建造模式：将产品的内部表象和产品的生成过程分割开来，从而使一个建造过程生成具有不同的内部表象的产品对象。建造模式使得产品内部表象可以独立的变化，客户不必知道产品内部组成的细节。建造模式可以强制实行一种分步骤进行的建造过程。\n03 抽象工厂 请 MM 去麦当劳吃汉堡，不同的 MM 有不同的口味，要每个都记住是一件烦人的事情，我一般采用 Factory Method 模式，带着 MM 到服务员那儿，说「要一个汉堡」，具体要什么样的汉堡呢，让 MM 直接跟服务员说就行了。 工厂方法模式：核心工厂类不再负责所有产品的创建，而是将具体创建的工作交给子类去做，成为一个抽象工厂角色，仅负责给出具体工厂类必须实现的接口，而不接触哪一个产品类应当被实例化这种细节。\n04 原型模式 跟 MM 用 QQ 聊天，一定要说些深情的话语了，我搜集了好多肉麻的情话，需要时只要 copy 出来放到 QQ 里面就行了，这就是我的情话 prototype 了。（100 块钱一份，你要不要） 原始模型模式：通过给出一个原型对象来指明所要创建的对象的类型，然后用复制这个原型对象的方法创建出更多同类型的对象。原始模型模式允许动态的增加或减少产品类，产品类不需要非得有任何事先确定的等级结构，原始模型模式适用于任何的等级结构。缺点是每一个类都必须配备一个克隆方法。\n05 单态模式 俺有 6 个漂亮的老婆，她们的老公都是我，我就是我们家里的老公 Sigleton，她们只要说道「老公」，都是指的同一个人，那就是我 (刚才做了个梦啦，哪有这么好的事) 单例模式：单例模式确保某一个类只有一个实例，而且自行实例化并向整个系统提供这个实例单例模式。单例模式只应在有真正的 “单一实例” 的需求时才可使用。\n06 适配器模式 在朋友聚会上碰到了一个美女 Sarah，从香港来的，可我不会说粤语，她不会说普通话，只好求助于我的朋友 kent 了，他作为我和 Sarah 之间的 Adapter，让我和 Sarah 可以相互交谈了 (也不知道他会不会耍我) 适配器（变压器）模式：把一个类的接口变换成客户端所期待的另一种接口，从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。适配类可以根据参数返还一个合适的实例给客户端。\n07 桥梁模式 早上碰到 MM，要说早上好，晚上碰到 MM，要说晚上好；碰到 MM 穿了件新衣服，要说你的衣服好漂亮哦，碰到 MM 新做的发型，要说你的头发好漂亮哦。不要问我 “早上碰到 MM 新做了个发型怎么说” 这种问题，自己用 BRIDGE 组合一下不就行了 桥梁模式：将抽象化与实现化脱耦，使得二者可以独立的变化，也就是说将他们之间的强关联变成弱关联，也就是指在一个软件系统的抽象化和实现化之间使用组合 / 聚合关系而不是继承关系，从而使两者可以独立的变化。\n08 合成模式 Mary 今天过生日。“我过生日，你要送我一件礼物。”“嗯，好吧，去商店，你自己挑。”“这件 T 恤挺漂亮，买，这条裙子好看，买，这个包也不错，买。”“喂，买了三件了呀，我只答应送一件礼物的哦。”“什么呀，T 恤加裙子加包包，正好配成一套呀，小姐，麻烦你包起来。”“……”，MM 都会用 Composite 模式了，你会了没有？ 合成模式：合成模式将对象组织到树结构中，可以用来描述整体与部分的关系。合成模式就是一个处理对象的树结构的模式。合成模式把部分与整体的关系用树结构表示出来。合成模式使得客户端把一个个单独的成分对象和由他们复合而成的合成对象同等看待。\n09 装饰模式 Mary 过完轮到 Sarly 过生日，还是不要叫她自己挑了，不然这个月伙食费肯定玩完，拿出我去年在华山顶上照的照片，在背面写上 “最好的的礼物，就是爱你的 Fita”，再到街上礼品店买了个像框（卖礼品的 MM 也很漂亮哦），再找隔壁搞美术设计的 Mike 设计了一个漂亮的盒子装起来……，我们都是 Decorator，最终都在修饰我这个人呀，怎么样，看懂了吗？ 装饰模式：装饰模式以对客户端透明的方式扩展对象的功能，是继承关系的一个替代方案，提供比继承更多的灵活性。动态给一个对象增加功能，这些功能可以再动态的撤消。增加由一些基本功能的排列组合而产生的非常大量的功能。\n10 门面模式 我有一个专业的 Nikon 相机，我就喜欢自己手动调光圈、快门，这样照出来的照片才专业，但 MM 可不懂这些，教了半天也不会。幸好相机有 Facade 设计模式，把相机调整到自动档，只要对准目标按快门就行了，一切由相机自动调整，这样 MM 也可以用这个相机给我拍张照片了。门面模式：外部与一个子系统的通信必须通过一个统一的门面对象进行。 门面模式提供一个高层次的接口，使得子系统更易于使用。每一个子系统只有一个门面类，而且此门面类只有一个实例，也就是说它是一个单例模式。但整个系统可以有多个门面类。\n11 享元模式 每天跟 MM 发短信，手指都累死了，最近买了个新手机，可以把一些常用的句子存在手机里，要用的时候，直接拿出来，在前面加上 MM 的名字就可以发送了，再不用一个字一个字敲了。共享的句子就是 Flyweight，MM 的名字就是提取出来的外部特征，根据上下文情况使用。享元模式：FLYWEIGHT 在拳击比赛中指最轻量级。 享元模式以共享的方式高效的支持大量的细粒度对象。享元模式能做到共享的关键是区分内蕴状态和外蕴状态。内蕴状态存储在享元内部，不会随环境的改变而有所不同。外蕴状态是随环境的改变而改变的。外蕴状态不能影响内蕴状态，它们是相互独立的。 将可以共享的状态和不可以共享的状态从常规类中区分开来，将不可以共享的状态从类里剔除出去。客户端不可以直接创建被共享的对象，而应当使用一个工厂对象负责创建被共享的对象。享元模式大幅度的降低内存中对象的数量。\n12 代理模式 跟 MM 在网上聊天，一开头总是 “hi, 你好”,“你从哪儿来呀？”“你多大了？”“身高多少呀？” 这些话，真烦人，写个程序做为我的 Proxy 吧，凡是接收到这些话都设置好了自己的回答，接收到其他的话时再通知我回答，怎么样，酷吧。 代理模式：代理模式给某一个对象提供一个代理对象，并由代理对象控制对源对象的引用。代理就是一个人或一个机构代表另一个人或者一个机构采取行动。某些情况下，客户不想或者不能够直接引用一个对象，代理对象可以在客户和目标对象直接起到中介的作用。 客户端分辨不出代理主题对象与真实主题对象。代理模式可以并不知道真正的被代理对象，而仅仅持有一个被代理对象的接口，这时候代理对象不能够创建被代理对象，被代理对象必须有系统的其他角色代为创建并传入。\n13 责任链模式 晚上去上英语课，为了好开溜坐到了最后一排，哇，前面坐了好几个漂亮的 MM 哎，找张纸条，写上 “Hi, 可以做我的女朋友吗？如果不愿意请向前传”，纸条就一个接一个的传上去了，糟糕，传到第一排的 MM 把纸条传给老师了，听说是个老处女呀，快跑！ 责任链模式：在责任链模式中，很多对象由每一个对象对其下家的引用而接起来形成一条链。请求在这个链上传递，直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求，系统可以在不影响客户端的情况下动态的重新组织链和分配责任。处理者有两个选择：承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象所接受。\n14 命令模式 俺有一个 MM 家里管得特别严，没法见面，只好借助于她弟弟在我们俩之间传送信息，她对我有什么指示，就写一张纸条让她弟弟带给我。这不，她弟弟又传送过来一个 COMMAND，为了感谢他，我请他吃了碗杂酱面，哪知道他说：“我同时给我姐姐三个男朋友送 COMMAND，就数你最小气，才请我吃面。” 命令模式：命令模式把一个请求或者操作封装到一个对象中。命令模式把发出命令的责任和执行命令的责任分割开，委派给不同的对象。命令模式允许请求的一方和发送的一方独立开来，使得请求的一方不必知道接收请求的一方的接口，更不必知道请求是怎么被接收，以及操作是否执行，何时被执行以及是怎么被执行的。系统支持命令的撤消。\n15 解释器模式 俺有一个《泡 MM 真经》，上面有各种泡 MM 的攻略，比如说去吃西餐的步骤、去看电影的方法等等，跟 MM 约会时，只要做一个 Interpreter，照着上面的脚本执行就可以了。 解释器模式：给定一个语言后，解释器模式可以定义出其文法的一种表示，并同时提供一个解释器。客户端可以使用这个解释器来解释这个语言中的句子。解释器模式将描述怎样在有了一个简单的文法后，使用模式设计解释这些语句。 在解释器模式里面提到的语言是指任何解释器对象能够解释的任何组合。在解释器模式中需要定义一个代表文法的命令类的等级结构，也就是一系列的组合规则。每一个命令对象都有一个解释方法，代表对命令对象的解释。命令对象的等级结构中的对象的任何排列组合都是一个语言。\n16 迭代模式 我爱上了 Mary，不顾一切的向她求婚。Mary：“想要我跟你结婚，得答应我的条件” 我：“什么条件我都答应，你说吧” Mary：“我看上了那个一克拉的钻石” 我：“我买，我买，还有吗？” Mary：“我看上了湖边的那栋别墅” 我：“我买，我买，还有吗？” Mary：“我看上那辆法拉利跑车” 我脑袋嗡的一声，坐在椅子上，一咬牙：“我买，我买，还有吗？” 迭代模式：迭代模式可以顺序访问一个聚集中的元素而不必暴露聚集的内部表象。多个对象聚在一起形成的总体称之为聚集，聚集对象是能够包容一组对象的容器对象。迭代子模式将迭代逻辑封装到一个独立的子对象中，从而与聚集本身隔开。 迭代模式简化了聚集的界面。每一个聚集对象都可以有一个或一个以上的迭代子对象，每一个迭代子的迭代状态可以是彼此独立的。迭代算法可以独立于聚集角色变化。\n17 调停者模式 四个 MM 打麻将，相互之间谁应该给谁多少钱算不清楚了，幸亏当时我在旁边，按照各自的筹码数算钱，赚了钱的从我这里拿，赔了钱的也付给我，一切就 OK 啦，俺得到了四个 MM 的电话。调停者模式：调停者模式包装了一系列对象相互作用的方式，使得这些对象不必相互明显作用。从而使他们可以松散偶合。 当某些对象之间的作用发生改变时，不会立即影响其他的一些对象之间的作用。保证这些作用可以彼此独立的变化。调停者模式将多对多的相互作用转化为一对多的相互作用。调停者模式将对象的行为和协作抽象化，把对象在小尺度的行为上与其他对象的相互作用分开处理。\n18 备忘录模式 同时跟几个 MM 聊天时，一定要记清楚刚才跟 MM 说了些什么话，不然 MM 发现了会不高兴的哦，幸亏我有个备忘录，刚才与哪个 MM 说了什么话我都拷贝一份放到备忘录里面保存，这样可以随时察看以前的记录啦。 备忘录模式：备忘录对象是一个用来存储另外一个对象内部状态的快照的对象。备忘录模式的用意是在不破坏封装的条件下，将一个对象的状态捉住，并外部化，存储起来，从而可以在将来合适的时候把这个对象还原到存储起来的状态。\n19 观察者模式 想知道咱们公司最新 MM 情报吗？加入公司的 MM 情报邮件组就行了，tom 负责搜集情报，他发现的新情报不用一个一个通知我们，直接发布给邮件组，我们作为订阅者（观察者）就可以及时收到情报啦。 观察者模式：观察者模式定义了一种一队多的依赖关系，让多个观察者对象同时监听某一个主题对象。这个主题对象在状态上发生变化时，会通知所有观察者对象，使他们能够自动更新自己。关注微信订阅号码匠笔记回复设计模式还有视频版本哦\n20 状态模式 跟 MM 交往时，一定要注意她的状态哦，在不同的状态时她的行为会有不同，比如你约她今天晚上去看电影，对你没兴趣的 MM 就会说 “有事情啦”，对你不讨厌但还没喜欢上的 MM 就会说 “好啊，不过可以带上我同事么？”，已经喜欢上你的 MM 就会说 “几点钟？看完电影再去泡吧怎么样？”，当然你看电影过程中表现良好的话，也可以把 MM 的状态从不讨厌不喜欢变成喜欢哦。 状态模式：状态模式允许一个对象在其内部状态改变的时候改变行为。这个对象看上去象是改变了它的类一样。状态模式把所研究的对象的行为包装在不同的状态对象里，每一个状态对象都属于一个抽象状态类的一个子类。 状态模式的意图是让一个对象在其内部状态改变的时候，其行为也随之改变。状态模式需要对每一个系统可能取得的状态创立一个状态类的子类。当系统的状态变化时，系统便改变所选的子类。\n21 策略模式 跟不同类型的 MM 约会，要用不同的策略，有的请电影比较好，有的则去吃小吃效果不错，有的去海边浪漫最合适，单目的都是为了得到 MM 的芳心，我的追 MM 锦囊中有好多 Strategy 哦。策略模式：策略模式针对一组算法，将每一个算法封装到具有共同接口的独立的类中，从而使得它们可以相互替换。 策略模式使得算法可以在不影响到客户端的情况下发生变化。策略模把行为和环境分开。环境类负责维持和查询行为类，各种算法在具体的策略类中提供。由于算法和环境独立开来，算法的增减，修改都不会影响到环境和客户端。\n22 模板方法模式 看过《如何说服女生上床》这部经典文章吗？女生从认识到上床的不变的步骤分为巧遇、打破僵局、展开追求、接吻、前戏、动手、爱抚、进去八大步骤 (Template method)，但每个步骤针对不同的情况，都有不一样的做法，这就要看你随机应变啦 (具体实现)； 模板方法模式：模板方法模式准备一个抽象类，将部分逻辑以具体方法以及具体构造子的形式实现，然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法，从而对剩余的逻辑有不同的实现。先制定一个顶级逻辑框架，而将逻辑的细节留给具体的子类去实现。\n23 访问者模式 情人节到了，要给每个 MM 送一束鲜花和一张卡片，可是每个 MM 送的花都要针对她个人的特点，每张卡片也要根据个人的特点来挑，我一个人哪搞得清楚，还是找花店老板和礼品店老板做一下 Visitor，让花店老板根据 MM 的特点选一束花，让礼品店老板也根据每个人特点选一张卡，这样就轻松多了； 访问者模式：访问者模式的目的是封装一些施加于某种数据结构元素之上的操作。一旦这些操作需要修改的话，接受这个操作的数据结构可以保持不变。访问者模式适用于数据结构相对未定的系统，它把数据结构和作用于结构上的操作之间的耦合解脱开，使得操作集合可以相对自由的演化。访问者模式使得增加新的操作变的很容易，就是增加一个新的访问者类。 访问者模式将有关的行为集中到一个访问者对象中，而不是分散到一个个的节点类中。当使用访问者模式时，要将尽可能多的对象浏览逻辑放在访问者类中，而不是放到它的子类中。访问者模式可以跨过几个类的等级结构访问属于不同的等级结构的成员类。\n","permalink":"https://ktzxy.top/posts/7scbrdt96x/","summary":"24工厂模式俗话解释","title":"24工厂模式俗话解释"},{"content":"脚本安装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #!/bin/bash # 安装 Docker echo \u0026#34;安装 Docker...\u0026#34; sudo apt update sudo apt install -y apt-transport-https ca-certificates curl software-properties-common curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - sudo add-apt-repository \u0026#34;deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable\u0026#34; sudo apt update sudo apt install -y docker-ce # 添加当前用户到 Docker 用户组 sudo usermod -aG docker $USER # 安装 Docker Compose echo \u0026#34;安装 Docker Compose...\u0026#34; sudo curl -L \u0026#34;https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)\u0026#34; -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose # 显示安装信息 echo \u0026#34;Docker 和 Docker Compose 安装完成。\u0026#34; docker --version docker-compose --version 手动安装 安装（以ubuntu） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 # step 1: 安装必要的一些系统工具 sudo apt-get update sudo apt-get install ca-certificates curl gnupg # step 2: 信任 Docker 的 GPG 公钥 sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg # Step 3: 写入软件源信息 echo \\ \u0026#34;deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://mirrors.aliyun.com/docker-ce/linux/ubuntu \\ \u0026#34;$(. /etc/os-release \u0026amp;\u0026amp; echo \u0026#34;$VERSION_CODENAME\u0026#34;)\u0026#34; stable\u0026#34; | \\ sudo tee /etc/apt/sources.list.d/docker.list \u0026gt; /dev/null # Step 4: 安装Docker sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # 安装指定版本的Docker-CE: # Step 1: 查找Docker-CE的版本: # apt-cache madison docker-ce # docker-ce | 17.03.1~ce-0~ubuntu-xenial | https://mirrors.aliyun.com/docker-ce/linux/ubuntu xenial/stable amd64 Packages # docker-ce | 17.03.0~ce-0~ubuntu-xenial | https://mirrors.aliyun.com/docker-ce/linux/ubuntu xenial/stable amd64 Packages # Step 2: 安装指定版本的Docker-CE: (VERSION例如上面的17.03.1~ce-0~ubuntu-xenial) # sudo apt-get -y install docker-ce=[VERSION] # 启动 Docker 并设置开机自启 sudo systemctl start docker sudo systemctl enable docker # 将当前用户加入 docker 组（避免使用 sudo） sudo usermod -aG docker $USER newgrp docker 说明：\nsudo apt update \u0026amp;\u0026amp; sudo apt upgrade -y：更新系统软件包列表和升级已安装的软件包 sudo apt install -y ...：安装Docker所需的基础依赖包 curl -fsSL ... | sudo gpg --dearmor ...：添加Docker官方GPG密钥以验证下载的包 echo \u0026quot;deb [arch=...] ...\u0026quot; | sudo tee ...：添加Docker官方APT仓库 sudo usermod -aG docker $USER：将当前用户添加到docker组，这样就无需每次都使用sudo运行docker命令 newgrp docker：激活新的用户组权限，无需重新登录 常见错误及解决方法：\n错误：\u0026ldquo;Could not get lock /var/lib/dpkg/lock-frontend\u0026rdquo; - 表示其他程序正在使用apt，等待或杀死相关进程 错误：\u0026ldquo;The following signatures couldn\u0026rsquo;t be verified because the public key is not available\u0026rdquo; - 重新添加GPG密钥 错误：\u0026ldquo;Cannot connect to the Docker daemon\u0026rdquo; - 检查Docker服务是否启动：sudo systemctl status docker 安装（以centos） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # step 1: 安装必要的一些系统工具 sudo yum install -y yum-utils # Step 2: 添加软件源信息 yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo # Step 3: 安装Docker sudo yum install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin # Step 4: 开启Docker服务 sudo service docker start # 注意： # 官方软件源默认启用了最新的软件，您可以通过编辑软件源的方式获取各个版本的软件包。例如官方并没有将测试版本的软件源置为可用，您可以通过以下方式开启。同理可以开启各种测试版本等。 # vim /etc/yum.repos.d/docker-ce.repo # 将[docker-ce-test]下方的enabled=0修改为enabled=1 # # 安装指定版本的Docker-CE: # Step 1: 查找Docker-CE的版本: # yum list docker-ce.x86_64 --showduplicates | sort -r # Loading mirror speeds from cached hostfile # Loaded plugins: branch, fastestmirror, langpacks # docker-ce.x86_64 17.03.1.ce-1.el7.centos docker-ce-stable # docker-ce.x86_64 17.03.1.ce-1.el7.centos @docker-ce-stable # docker-ce.x86_64 17.03.0.ce-1.el7.centos docker-ce-stable # Available Packages # Step2: 安装指定版本的Docker-CE: (VERSION例如上面的17.03.0.ce.1-1.el7.centos) # sudo yum -y install docker-ce-[VERSION] Docker CE镜像-Docker CE镜像下载安装-开源镜像站-阿里云\n设置开机自动启动 1 #systemctl enable docker 查看docker服务状态 1 #systemctl status docker 查看docker具体信息 1 #docker info ","permalink":"https://ktzxy.top/posts/ivzblwvqy0/","summary":"docker安装","title":"docker安装"},{"content":"Kubernetes集群YAML文件详解 概述 k8s 集群中对资源管理和资源对象编排部署都可以通过声明样式（YAML）文件来解决，也就是可以把需要对资源对象操作编辑到YAML 格式文件中，我们把这种文件叫做资源清单文件，通过kubectl 命令直接使用资源清单文件就可以实现对大量的资源对象进行编排部署了。一般在我们开发的时候，都是通过配置YAML文件来部署集群的。\nYAML文件：就是资源清单文件，用于资源编排\nYAML文件介绍 YAML概述 YAML ：仍是一种标记语言。为了强调这种语言以数据做为中心，而不是以标记语言为重点。\nYAML 是一个可读性高，用来表达数据序列的格式。\nYAML 基本语法 使用空格做为缩进 缩进的空格数目不重要，只要相同层级的元素左侧对齐即可 低版本缩进时不允许使用Tab 键，只允许使用空格 使用#标识注释，从这个字符一直到行尾，都会被解释器忽略 使用 \u0026mdash; 表示新的yaml文件开始 YAML 支持的数据结构 对象 键值对的集合，又称为映射(mapping) / 哈希（hashes） / 字典（dictionary）\n1 2 3 4 5 6 # 对象类型：对象的一组键值对，使用冒号结构表示 name: Tom age: 18 # yaml 也允许另一种写法，将所有键值对写成一个行内对象 hash: {name: Tom, age: 18} 数组 1 2 3 4 5 6 7 # 数组类型：一组连词线开头的行，构成一个数组 People - Tom - Jack # 数组也可以采用行内表示法 People: [Tom, Jack] YAML文件组成部分 主要分为了两部分，一个是控制器的定义 和 被控制的对象\n控制器的定义 被控制的对象 包含一些 镜像，版本、端口等\n属性说明 在一个YAML文件的控制器定义中，有很多属性名称\n属性名称 介绍 apiVersion API版本 kind 资源类型 metadata 资源元数据 spec 资源规格 replicas 副本数量 selector 标签选择器 template Pod模板 metadata Pod元数据 spec Pod规格 containers 容器配置 如何快速编写YAML文件 一般来说，我们很少自己手写YAML文件，因为这里面涉及到了很多内容，我们一般都会借助工具来创建\n使用kubectl create命令 这种方式一般用于资源没有部署的时候，我们可以直接创建一个YAML配置文件\n1 2 # 尝试运行,并不会真正的创建镜像 kubectl create deployment web --image=nginx -o yaml --dry-run 或者我们可以输出到一个文件中\n1 kubectl create deployment web --image=nginx -o yaml --dry-run \u0026gt; hello.yaml 然后我们就在文件中直接修改即可\n使用kubectl get命令导出yaml文件 可以首先查看一个目前已经部署的镜像\n1 kubectl get deploy 然后我们导出 nginx的配置\n1 kubectl get deploy nginx -o=yaml --export \u0026gt; nginx.yaml 然后会生成一个 nginx.yaml 的配置文件\n","permalink":"https://ktzxy.top/posts/0lv02an8ph/","summary":"7 Kubernetes集群YAML文件详解","title":"7 Kubernetes集群YAML文件详解"},{"content":"Kubernetes持久化存储 前言 之前我们有提到数据卷：emptydir ，是本地存储，pod重启，数据就不存在了，需要对数据持久化存储\n对于数据持久化存储【pod重启，数据还存在】，有两种方式\nnfs：网络存储【通过一台服务器来存储】 步骤 持久化服务器上操作 找一台新的服务器nfs服务端，安装nfs 设置挂载路径 使用命令安装nfs\n1 yum install -y nfs-utils 首先创建存放数据的目录\n1 mkdir -p /data/nfs 设置挂载路径\n1 2 3 4 # 打开文件 vim /etc/exports # 添加如下内容 /data/nfs *(rw,no_root_squash) 执行完成后，即部署完我们的持久化服务器\nNode节点上操作 然后需要在k8s集群node节点上安装nfs，这里需要在 node1 和 node2节点上安装\n1 yum install -y nfs-utils 执行完成后，会自动帮我们挂载上\n启动nfs服务端 下面我们回到nfs服务端，启动我们的nfs服务\n1 2 3 4 # 启动服务 systemctl start nfs # 或者使用以下命令进行启动 service nfs-server start K8s集群部署应用 最后我们在k8s集群上部署应用，使用nfs持久化存储\n1 2 3 4 # 创建一个pv文件 mkdir pv # 进入 cd pv 然后创建一个yaml文件 nfs-nginx.yaml\n通过这个方式，就挂载到了刚刚我们的nfs数据节点下的 /data/nfs 目录\n最后就变成了： /usr/share/nginx/html -\u0026gt; 192.168.44.134/data/nfs 内容是对应的\n我们通过这个 yaml文件，创建一个pod\n1 kubectl apply -f nfs-nginx.yaml 创建完成后，我们也可以查看日志\n1 kubectl describe pod nginx-dep1 可以看到，我们的pod已经成功创建出来了，同时下图也是出于Running状态\n下面我们就可以进行测试了，比如现在nfs服务节点上添加数据，然后在看数据是否存在 pod中\n1 2 # 进入pod中查看 kubectl exec -it nginx-dep1 bash PV和PVC 对于上述的方式，我们都知道，我们的ip 和端口是直接放在我们的容器上的，这样管理起来可能不方便\n所以这里就需要用到 pv 和 pvc的概念了，方便我们配置和管理我们的 ip 地址等元信息\nPV：持久化存储，对存储的资源进行抽象，对外提供可以调用的地方【生产者】\nPVC：用于调用，不需要关心内部实现细节【消费者】\nPV 和 PVC 使得 K8S 集群具备了存储的逻辑抽象能力。使得在配置Pod的逻辑里可以忽略对实际后台存储 技术的配置，而把这项配置的工作交给PV的配置者，即集群的管理者。存储的PV和PVC的这种关系，跟 计算的Node和Pod的关系是非常类似的；PV和Node是资源的提供者，根据集群的基础设施变化而变 化，由K8s集群管理员配置；而PVC和Pod是资源的使用者，根据业务服务的需求变化而变化，由K8s集 群的使用者即服务的管理员来配置。\n实现流程 PVC绑定PV 定义PVC 定义PV【数据卷定义，指定数据存储服务器的ip、路径、容量和匹配模式】 举例 创建一个 pvc.yaml\n第一部分是定义一个 deployment，做一个部署\n副本数：3 挂载路径 调用：是通过pvc的模式 然后定义pvc\n然后在创建一个 pv.yaml\n然后就可以创建pod了\n1 kubectl apply -f pv.yaml 然后我们就可以通过下面命令，查看我们的 pv 和 pvc之间的绑定关系\n1 kubectl get pv, pvc 到这里为止，我们就完成了我们 pv 和 pvc的绑定操作，通过之前的方式，进入pod中查看内容\n1 kubect exec -it nginx-dep1 bash 然后查看 /usr/share/nginx.html\n也同样能看到刚刚的内容，其实这种操作和之前我们的nfs是一样的，只是多了一层pvc绑定pv的操作\n","permalink":"https://ktzxy.top/posts/pijate4jl0/","summary":"16 Kubernetes持久化存储","title":"16 Kubernetes持久化存储"},{"content":"Go的函数 函数定义 函数是组织好的、可重复使用的、用于执行指定任务的代码块\nGo语言支持：函数、匿名函数和闭包\nGo语言中定义函数使用func关键字，具体格式如下：\n1 2 3 func 函数名(参数)(返回值) { 函数体 } 其中：\n函数名：由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内，函数名也不能重名 示例\n1 2 3 4 5 6 // 求两个数的和 func sumFn(x int, y int) int{ return x + y } // 调用方式 sunFn(1, 2) 获取可变的参数，可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后面加\u0026hellip; 来标识。\n注意：可变参数通常要作为函数的最后一个参数\n1 2 3 4 5 6 7 8 9 func sunFn2(x ...int) int { sum := 0 for _, num := range x { sum = sum + num } return sum } // 调用方法 sunFn2(1, 2, 3, 4, 5, 7) 方法多返回值，Go语言中函数支持多返回值，同时还支持返回值命名，函数定义时可以给返回值命名，并在函数体中直接使用这些变量，最后通过return关键字返回\n1 2 3 4 5 6 // 方法多返回值 func sunFn4(x int, y int)(sum int, sub int) { sum = x + y sub = x -y return } 函数类型和变量 定义函数类型 我们可以使用type关键字来定义一个函数类型，具体格式如下\n1 type calculation func(int, int) int 上面语句定义了一个calculation类型，它是一种函数类型，这种函数接收两个int类型的参数并且返回一个int类型的返回值。\n简单来说，凡是满足这两个条件的函数都是calculation类型的函数，例如下面的add 和 sub 是calculation类型\n1 2 3 4 5 6 7 8 9 type calc func(int, int) int // 求两个数的和 func sumFn(x int, y int) int{ return x + y } func main() { var c calc c = add } 方法作为参数 1 2 3 4 5 6 /** 传递两个参数和一个方法 */ func sunFn (a int, b int, sum func(int, int)int) int { return sum(a, b) } 或者使用switch定义方法，这里用到了匿名函数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // 返回一个方法 type calcType func(int, int)int func do(o string) calcType { switch o { case \u0026#34;+\u0026#34;: return func(i int, i2 int) int { return i + i2 } case \u0026#34;-\u0026#34;: return func(i int, i2 int) int { return i - i2 } case \u0026#34;*\u0026#34;: return func(i int, i2 int) int { return i * i2 } case \u0026#34;/\u0026#34;: return func(i int, i2 int) int { return i / i2 } default: return nil } } func main() { add := do(\u0026#34;+\u0026#34;) fmt.Println(add(1,5)) } 匿名函数 函数当然还可以作为返回值，但是在Go语言中，函数内部不能再像之前那样定义函数了，只能定义匿名函数。匿名函数就是没有函数名的函数，匿名函数的定义格式如下\n1 2 3 func (参数)(返回值) { 函数体 } 匿名函数因为没有函数名，所以没有办法像普通函数那样调用，所以匿名函数需要保存到某个变量或者作为立即执行函数：\n1 2 3 4 5 func main() { func () { fmt.Println(\u0026#34;匿名自执行函数\u0026#34;) }() } Golang中的闭包 全局变量和局部变量 全局变量的特点：\n常驻内存 污染全局 局部变量的特点\n不常驻内存 不污染全局 闭包 可以让一个变量常驻内存 可以让一个变量不污染全局 闭包可以理解成 “定义在一个函数内部的函数”。在本质上，闭包就是将函数内部 和 函数外部连接起来的桥梁。或者说是函数和其引用环境的组合体。\n闭包是指有权访问另一个函数作用域中的变量的函数 创建闭包的常见的方式就是在一个函数内部创建另一个函数，通过另一个函数访问这个函数的局部变量 注意：由于闭包里作用域返回的局部变量资源不会被立刻销毁，所以可能会占用更多的内存，过度使用闭包会导致性能下降，建议在非常有必要的时候才使用闭包。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 闭包的写法：函数里面嵌套一个函数，最后返回里面的函数就形成了闭包 func adder() func() int { var i = 10 return func() int { return i + 1 } } func main() { var fn = adder() fmt.Println(fn()) fmt.Println(fn()) fmt.Println(fn()) } 最后输出的结果\n1 2 3 11 11 11 另一个闭包的写法，让一个变量常驻内存，不污染全局\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func adder2() func(y int) int { var i = 10 return func(y int) int { i = i + y return i } } func main() { var fn2 = adder2() fmt.Println(fn2(10)) fmt.Println(fn2(10)) fmt.Println(fn2(10)) } defer语句 Go 语言中的defer 语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时，将延迟处理的语句按defer定义的逆序进行执行，也就是说，先被defer的语句最后被执行，最后被defer的语句，最先被执行。\n1 2 3 4 5 // defer函数 fmt.Println(\u0026#34;1\u0026#34;) defer fmt.Println(\u0026#34;2\u0026#34;) fmt.Println(\u0026#34;3\u0026#34;) fmt.Println(\u0026#34;4\u0026#34;) defer将会延迟执行\n1 2 3 4 1 3 4 2 如果有多个defer修饰的语句，将会逆序进行执行\n1 2 3 4 5 // defer函数 fmt.Println(\u0026#34;1\u0026#34;) defer fmt.Println(\u0026#34;2\u0026#34;) defer fmt.Println(\u0026#34;3\u0026#34;) fmt.Println(\u0026#34;4\u0026#34;) 运行结果\n1 2 3 4 1 4 3 2 如果需要用defer运行一系列的语句，那么就可以使用匿名函数\n1 2 3 4 5 6 7 8 func main() { fmt.Println(\u0026#34;开始\u0026#34;) defer func() { fmt.Println(\u0026#34;1\u0026#34;) fmt.Println(\u0026#34;2\u0026#34;) }() fmt.Println(\u0026#34;结束\u0026#34;) } 运行结果\n1 2 3 4 开始 结束 1 2 defer执行时机 在Go语言的函数中return语句在底层并不是原子操作，它分为返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后，RET指令执行前，具体如下图所示\npanic/revocer处理异常 Go语言中是没有异常机制，但是使用panic / recover模式来处理错误\npanic：可以在任何地方引发 recover：只有在defer调用函数内有效 1 2 3 4 5 6 7 8 9 10 11 12 func fn1() { fmt.Println(\u0026#34;fn1\u0026#34;) } func fn2() { panic(\u0026#34;抛出一个异常\u0026#34;) } func main() { fn1() fn2() fmt.Println(\u0026#34;结束\u0026#34;) } 上述程序会直接抛出异常，无法正常运行\n1 2 fn1 panic: 抛出一个异常 解决方法就是使用 recover进行异常的监听\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func fn1() { fmt.Println(\u0026#34;fn1\u0026#34;) } func fn2() { // 使用recover监听异常 defer func() { err := recover() if err != nil { fmt.Println(err) } }() panic(\u0026#34;抛出一个异常\u0026#34;) } func main() { fn1() fn2() fmt.Println(\u0026#34;结束\u0026#34;) } 异常运用场景 模拟一个读取文件的方法，这里可以主动发送使用panic 和 recover\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func readFile(fileName string) error { if fileName == \u0026#34;main.go\u0026#34; { return nil } else { return errors.New(\u0026#34;读取文件失败\u0026#34;) } } func myFn () { defer func() { e := recover() if e != nil { fmt.Println(\u0026#34;给管理员发送邮件\u0026#34;) } }() err := readFile(\u0026#34;XXX.go\u0026#34;) if err != nil { panic(err) } } func main() { myFn() } 内置函数 内置函数 介绍 close 主要用来关闭channel len 用来求长度，比如string、array、slice、map、channel new 用来分配内存、主要用来分配值类型，比如 int、struct ，返回的是指针 make 用来分配内存，主要用来分配引用类型，比如chan、map、slice append 用来追加元素到数组、slice中 panic\\recover 用来处理错误 ","permalink":"https://ktzxy.top/posts/b6rst3bf5j/","summary":"9 Go的函数","title":"9 Go的函数"},{"content":"Kubernetes可视化界面kubesphere 前言 Kubernetes也提供了默认的dashboard页面，但是功能不是很强大，这里就不使用了\n而是采用Kubesphere大桶全部的devops链路，通过kubesphere集成了很多套件\nhttps://kubesphere.io/zh/ ：集群要求高 https://kuboard.cn/：开源kuboard也不错，集群要求不高【轻量级】 简介 KubeSphere是一款面向云原生设计的开源项目，在目前主流容器调度平台Kubernetes之上构建的分布式多租户容器管理平台，提供简单易用的操作界面以及向导式操作方式，在降低用户使用容器调度平台学习成本的同时，极大降低开发、测试、运维的日常工作的复杂度。\n安装 前提条件 https://kubesphere.com.cn/docs/quick-start/minimal-kubesphere-on-k8s/\nKubernetes 版本必须为 “1.15.x，1.16.x，1.17.x 或 1.18.x”； 确保您的计算机满足最低硬件要求：CPU \u0026gt; 1 核，内存 \u0026gt; 2 G； 在安装之前，需要配置 Kubernetes 集群中的默认存储类； 当使用 --cluster-signing-cert-file 和 --cluster-signing-key-file 参数启动时，在 kube-apiserver 中会激活 CSR 签名功能。 请参阅 RKE 安装问题； 有关在 Kubernetes 上安装 KubeSphere 的前提条件的详细信息，请参阅前提条件。 安装helm 下面我们需要在 master 节点安装 helm\nHelm是Kubernetes的包管理器。包管理器类似于我们在 Ubuntu 中使用的 apt。Centos 中使用的 yum 或者Python 中的 pip 一样，能快速查找、下载和安装软件包。Helm由客户端组件helm和服务端组件Tiller组成，能够将一组K8S资源打包统一管理，是查找、共享和使用为Kubernetes构建的软件的最佳方式。\n安装3.0的 helm 首先我们需要去 官网下载\n第一步，下载helm安装压缩文件，上传到linux系统中 第二步，解压helm压缩文件，把解压后的helm目录复制到 usr/bin 目录中 使用命令：helm 部署KubeSphere 安装前 如果您的服务器无法访问 GitHub，则可以分别复制 kubesphere-installer.yaml 和 cluster-configuration.yaml 中的内容并将其粘贴到本地文件中。然后，您可以对本地文件使用 kubectl apply -f 来安装 KubeSphere。\n同时查看k8s集群的默认存储类\n1 kubectl get storageclass 如果没有默认存储类，那么就需要安装默认的存储类，参考博客：Kubernetes配置默认存储类\n因为我安装的是 nfs，所以在安装了 nfs 服务器启动\n1 systemctl start nfs 开始安装 如果无法正常访问github，可以提前把文件下载到本地\n1 2 3 kubectl apply -f https://github.com/kubesphere/ks-installer/releases/download/v3.0.0/kubesphere-installer.yaml kubectl apply -f https://github.com/kubesphere/ks-installer/releases/download/v3.0.0/cluster-configuration.yaml 如果下载到了本地，可以这样安装\n1 2 3 4 5 6 7 # 安装 kubectl apply -f kubesphere-installer.yaml kubectl apply -f cluster-configuration.yaml # 卸载 kubectl delete -f kubesphere-installer.yaml kubectl delete -f cluster-configuration.yaml 检查安装日志 1 kubectl logs -n kubesphere-system $(kubectl get pod -n kubesphere-system -l app=ks-install -o jsonpath=\u0026#39;{.items[0].metadata.name}\u0026#39;) -f 然后在查看pod运行状况\n1 kubectl get pod -n kubesphere-system 能够发现，我们还有两个容器正在创建\n使用 kubectl get pod --all-namespaces 查看所有 Pod 是否在 KubeSphere 的相关命名空间中正常运行。\n1 kubectl get pods --all-namespaces 能够发现所有的节点已经成功运行\n如果是，请通过以下命令检查控制台的端口：\n1 kubectl get svc/ks-console -n kubesphere-system 能够看到我们的服务确保在安全组中打开了端口 30880，并通过 NodePort（IP：30880）\n使用默认帐户和密码（admin/P@88w0rd）访问 Web 控制台。\n1 2 # 图形化页面 admin P@88w0rd http://192.168.177.130:30880/ 登录控制台后，您可以在组件中检查不同组件的状态。如果要使用相关服务，可能需要等待某些组件启动并运行。\n错误排查 错误1 kubesphere无法登录，提示 account is not active\nkubesphere 安装完成后会创建默认账户admin/P@88w0rd，待ks-controller-manager启动就绪，user controller 会将 user CRD中定义的password加密，user会被转换为active状态，至此账户才可以正常登录。\n当安装完成后遇到默认账户无法登录，看到account is not active相关错误提示时，需要检查ks-controller-manager的运行状态和日志。常见问题及解决方式如下:\n1 2 3 4 5 6 7 8 9 10 kubectl -n kubesphere-system get ValidatingWebhookConfiguration users.iam.kubesphere.io -o yaml \u0026gt;\u0026gt; users.iam.kubesphere.io.yaml kubectl -n kubesphere-system get secret ks-controller-manager-webhook-cert -o yaml \u0026gt;\u0026gt; ks-controller-manager-webhook-cert.yaml # edit ca as pr kubectl -n kubesphere-system apply -f ks-controller-manager-webhook-cert.yaml kubectl -n kubesphere-system apply -f users.iam.kubesphere.io.yaml # restart kubectl -n kubesphere-system rollout restart deploy ks-controller-manager 来源：https://kubesphere.com.cn/forum/d/2217-account-is-not-active\n","permalink":"https://ktzxy.top/posts/2hr3mnahxq/","summary":"31 Kubernetes可视化界面kubesphere","title":"31 Kubernetes可视化界面kubesphere"},{"content":"Flag包的用法 Go语言内置的flag包实现了命令行参数的解析，flag包使得开发命令行工具更为简单。\nos.Args 如果你只是简单的想要获取命令行参数，可以像下面的代码示例一样使用os.Args来获取命令行参数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; ) //os.Args demo func main() { //os.Args是一个[]string if len(os.Args) \u0026gt; 0 { for index, arg := range os.Args { fmt.Printf(\u0026#34;args[%d]=%v\\n\u0026#34;, index, arg) } } } 将上面的代码执行go build -o \u0026quot;args_demo\u0026quot;编译之后，执行：\n1 2 3 4 5 6 $ ./args_demo a b c d args[0]=./args_demo args[1]=a args[2]=b args[3]=c args[4]=d os.Args是一个存储命令行参数的字符串切片，它的第一个元素是执行文件的名称。\nflag包基本使用 本文介绍了flag包的常用函数和基本用法，更详细的内容请查看官方文档。\n导入flag包 1 import flag flag参数类型 flag包支持的命令行参数类型有bool、int、int64、uint、uint64、float float64、string、duration。\nflag参数 有效值 字符串flag 合法字符串 整数flag 1234、0664、0x1234等类型，也可以是负数。 浮点数flag 合法浮点数 bool类型flag 1, 0, t, f, T, F, true, false, TRUE, FALSE, True, False。 时间段flag 任何合法的时间段字符串。如”300ms”、”-1.5h”、”2h45m”。 合法的单位有”ns”、”us” /“µs”、”ms”、”s”、”m”、”h”。 定义命令行flag参数 有以下两种常用的定义命令行flag参数的方法。\nflag.Type() 基本格式如下：\nflag.Type(flag名, 默认值, 帮助信息)*Type 例如我们要定义姓名、年龄、婚否三个命令行参数，我们可以按如下方式定义：\n1 2 3 4 name := flag.String(\u0026#34;name\u0026#34;, \u0026#34;张三\u0026#34;, \u0026#34;姓名\u0026#34;) age := flag.Int(\u0026#34;age\u0026#34;, 18, \u0026#34;年龄\u0026#34;) married := flag.Bool(\u0026#34;married\u0026#34;, false, \u0026#34;婚否\u0026#34;) delay := flag.Duration(\u0026#34;d\u0026#34;, 0, \u0026#34;时间间隔\u0026#34;) 需要注意的是，此时name、age、married、delay均为对应类型的指针。\nflag.TypeVar() 基本格式如下： flag.TypeVar(Type指针, flag名, 默认值, 帮助信息) 例如我们要定义姓名、年龄、婚否三个命令行参数，我们可以按如下方式定义：\n1 2 3 4 5 6 7 8 var name string var age int var married bool var delay time.Duration flag.StringVar(\u0026amp;name, \u0026#34;name\u0026#34;, \u0026#34;张三\u0026#34;, \u0026#34;姓名\u0026#34;) flag.IntVar(\u0026amp;age, \u0026#34;age\u0026#34;, 18, \u0026#34;年龄\u0026#34;) flag.BoolVar(\u0026amp;married, \u0026#34;married\u0026#34;, false, \u0026#34;婚否\u0026#34;) flag.DurationVar(\u0026amp;delay, \u0026#34;d\u0026#34;, 0, \u0026#34;时间间隔\u0026#34;) flag.Parse() 通过以上两种方法定义好命令行flag参数后，需要通过调用flag.Parse()来对命令行参数进行解析。\n支持的命令行参数格式有以下几种：\n-flag xxx （使用空格，一个-符号） --flag xxx （使用空格，两个-符号） -flag=xxx （使用等号，一个-符号） --flag=xxx （使用等号，两个-符号） 其中，布尔类型的参数必须使用等号的方式指定。\nFlag解析在第一个非flag参数（单个”-“不是flag参数）之前停止，或者在终止符”–“之后停止。\nflag其他函数 1 2 3 flag.Args() ////返回命令行参数后的其他参数，以[]string类型 flag.NArg() //返回命令行参数后的其他参数个数 flag.NFlag() //返回使用的命令行参数个数 完整示例 定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func main() { //定义命令行参数方式1 var name string var age int var married bool var delay time.Duration flag.StringVar(\u0026amp;name, \u0026#34;name\u0026#34;, \u0026#34;张三\u0026#34;, \u0026#34;姓名\u0026#34;) flag.IntVar(\u0026amp;age, \u0026#34;age\u0026#34;, 18, \u0026#34;年龄\u0026#34;) flag.BoolVar(\u0026amp;married, \u0026#34;married\u0026#34;, false, \u0026#34;婚否\u0026#34;) flag.DurationVar(\u0026amp;delay, \u0026#34;d\u0026#34;, 0, \u0026#34;延迟的时间间隔\u0026#34;) //解析命令行参数 flag.Parse() fmt.Println(name, age, married, delay) //返回命令行参数后的其他参数 fmt.Println(flag.Args()) //返回命令行参数后的其他参数个数 fmt.Println(flag.NArg()) //返回使用的命令行参数个数 fmt.Println(flag.NFlag()) } 使用 命令行参数使用提示：\n1 2 3 4 5 6 7 8 9 10 $ ./flag_demo -help Usage of ./flag_demo: -age int 年龄 (default 18) -d duration 时间间隔 -married 婚否 -name string 姓名 (default \u0026#34;张三\u0026#34;) 正常使用命令行flag参数：\n1 2 3 4 5 $ ./flag_demo -name 沙河娜扎 --age 28 -married=false -d=1h30m 沙河娜扎 28 false 1h30m0s [] 0 4 使用非flag命令行参数：\n1 2 3 4 5 $ ./flag_demo a b c 张三 18 false 0s [a b c] 3 0 ","permalink":"https://ktzxy.top/posts/qjk9xomht2/","summary":"Flag包的用法","title":"Flag包的用法"},{"content":"Kubernetes配置管理 Secret Secret的主要作用就是加密数据，然后存在etcd里面，让Pod容器以挂载Volume方式进行访问\n场景：用户名 和 密码进行加密\n一般场景的是对某个字符串进行base64编码 进行加密\n1 echo -n \u0026#39;admin\u0026#39; | base64 变量形式挂载到Pod 创建secret加密数据的yaml文件 secret.yaml 然后使用下面命令创建一个pod\n1 kubectl create -f secret.yaml 通过get命令查看\n1 kubectl get pods 然后我们通过下面的命令，进入到我们的容器内部\n1 kubectl exec -it mypod bash 然后我们就可以输出我们的值，这就是以变量的形式挂载到我们的容器中\n1 2 3 4 # 输出用户 echo $SECRET_USERNAME # 输出密码 echo $SECRET_PASSWORD 最后如果我们要删除这个Pod，就可以使用这个命令\n1 kubectl delete -f secret-val.yaml 数据卷形式挂载 首先我们创建一个 secret-val.yaml 文件\n然后创建我们的 Pod\n1 2 3 4 5 6 # 根据配置创建容器 kubectl apply -f secret-val.yaml # 进入容器 kubectl exec -it mypod bash # 查看 ls /etc/foo ConfigMap ConfigMap作用是存储不加密的数据到etcd中，让Pod以变量或数据卷Volume挂载到容器中\n应用场景：配置文件\n创建配置文件 首先我们需要创建一个配置文件 redis.properties\n1 2 3 redis.port=127.0.0.1 redis.port=6379 redis.password=123456 创建ConfigMap 我们使用命令创建configmap\n1 kubectl create configmap redis-config --from-file=redis.properties 然后查看详细信息\n1 kubectl describe cm redis-config Volume数据卷形式挂载 首先我们需要创建一个 cm.yaml\n然后使用该yaml创建我们的pod\n1 2 3 4 # 创建 kubectl apply -f cm.yaml # 查看 kubectl get pods 最后我们通过命令就可以查看结果输出了\n1 kubectl logs mypod 以变量的形式挂载Pod 首先我们也有一个 myconfig.yaml文件，声明变量信息，然后以configmap创建\n然后我们就可以创建我们的配置文件\n1 2 3 4 # 创建pod kubectl apply -f myconfig.yaml # 获取 kubectl get cm 然后我们创建完该pod后，我们就需要在创建一个 config-var.yaml 来使用我们的配置信息\n最后我们查看输出\n1 kubectl logs mypod ","permalink":"https://ktzxy.top/posts/z2xptdy6ns/","summary":"12 Kubernetes配置管理","title":"12 Kubernetes配置管理"},{"content":"Go并发编程 参考 https://www.liwenzhou.com/posts/Go/14_concurrence/\n并行与并发 并发：同一时间段内执行多个任务（你在用微信和两个女朋友聊天）\n并行：同一时刻执行多个任务（你和你朋友都在用微信和女朋友聊天）\nGo语言的并发通过goroutine 实现。goroutine类似于线程，属于用户态的线程，我们可以根据需要创建成千上万个goroutine 并发工作。goroutine 是由Go语言的运行时（runtime）调度完成，而线程是由操作系统调度完成。 Go语言还提供channel 在多个goroutine间进行通信。goroutine和channel 是Go语言秉承CSP（Communicating Sequential Process）并发模式的重要实现基础。\n用户态：表示程序执行用户自己写的程序时\n内核态：表示程序执行操作系统层面的程序时\n我们学习go的并发，就是学习goroutine 和 channel\nGoroutine 在java/c++中我们要实现并发编程的时候，我们通常需要自己维护一个线程池，并且需要自己去包装一个又一个的任务，同时需要自己去调度线程执行任务并维护上下文切换，这一切通常会耗费程序员大量的心智。那么能不能有一种机制，程序员只需要定义很多个任务，让系统去帮助我们把这些任务分配到CPU上实现并发执行呢？\nGo语言中的goroutine就是一种机制，goroutine的概念类似于线程，但goroutine是由Go的运行时（runtime）调度和管理的。Go程序会智能地将goroutine中的任务合理分配给每个CPU，Go语言之所以被称为现代化的编程语言，就是因为它在语言层面已经内置了调度和上下文切换的机制。\n在Go语言编程中你不需要去自己写进程、线程、协程，你的技能包里只有一个技能- goroutine，当你需要让某个任务并发执行的时候，你只需要把这个任务包装成一个函数、开启一个goroutine去执行这个函数就可以了，就是这么简单粗暴。\n使用Goroutine Go语言中goroutine非常简单，只需要在调用函数的时候，在前面加上go关键字，就可以为一个函数创建一个goroutine\n一个goroutine必定对应一个函数，可以创建多个goroutine去执行相同的函数\n启动goroutine 启动goroutine的方式非常简单，只需要在调用的函数（普通函数和匿名函数）前面加上一个go关键字。\n举个例子如下：\n1 2 3 4 5 6 7 func hello() { fmt.Println(\u0026#34;Hello Goroutine!\u0026#34;) } func main() { hello() fmt.Println(\u0026#34;main goroutine done!\u0026#34;) } 当main()函数返回的时候该goroutine就结束了，所有在main()函数中启动的goroutine会一同结束，main函数所在的goroutine就像是权利的游戏中的夜王，其他的goroutine都是异鬼，夜王一死它转化的那些异鬼也就全部GG了\n所以我们要想办法让main函数等一等hello函数，最简单粗暴的方式就是time.Sleep了\n1 2 3 4 5 func main() { go hello() // 启动另外一个goroutine去执行hello函数 fmt.Println(\u0026#34;main goroutine done!\u0026#34;) time.Sleep(time.Second) } 启动多个goroutine 在Go语言中实现并发就是这样简单，我们还可以启动多个goroutine。让我们再来一个例子:（这里使用了sync.WaitGroup来实现goroutine的同步）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 var wg sync.WaitGroup func hello(i int) { defer wg.Done() // goroutine结束就登记-1 fmt.Println(\u0026#34;Hello Goroutine!\u0026#34;, i) } func main() { for i := 0; i \u0026lt; 10; i++ { wg.Add(1) // 启动一个goroutine就登记+1 go hello(i) } wg.Wait() // 等待所有登记的goroutine都结束 } goroutine什么时候结束？ goroutine对应的函数结束了，goroutine就结束了吗，也就是说当我们的main函数执行结束了，那么main函数对应的goroutine也结束了。\ngoroutine与线程 可增长的栈 OS线程（操作系统线程）一般都有固定的栈内存（通常为2MB）,一个goroutine的栈在其生命周期开始时只有很小的栈（典型情况下2KB），goroutine的栈不是固定的，他可以按需增大和缩小，goroutine的栈大小限制可以达到1GB，虽然极少会用到这么大。所以在Go语言中一次创建十万左右的goroutine也是可以的\nGoroutine调度 GPM是Go语言运行时（runtime）层面的实现，是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。\nG很好理解，就是个goroutine的，里面除了存放本goroutine信息外 还有与所在P的绑定等信息。 P管理着一组goroutine队列，P里面会存储当前goroutine运行的上下文环境（函数指针，堆栈地址及地址边界），P会对自己管理的goroutine队列做一些调度（比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等）当自己的队列消费完了就去全局队列里取，如果全局队列里也消费完了会去其他P的队列里抢任务。 M（machine）是Go运行时（runtime）对操作系统内核线程的虚拟， M与内核线程一般是一一映射的关系， 一个groutine最终是要放到M上执行的； 单从线程调度讲，Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的，goroutine则是由Go运行时（runtime）自己的调度器调度的，这个调度器使用一个称为m:n调度的技术（复用/调度m个goroutine到n个OS线程）。 其一大特点是goroutine的调度是在用户态下完成的， 不涉及内核态与用户态之间的频繁切换，包括内存的分配与释放，都是在用户态维护着一块大的内存池， 不直接调用系统的malloc函数（除非内存池需要改变），成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源，近似的把若干goroutine均分在物理线程上， 再加上本身goroutine的超轻量，以上种种保证了go调度方面的性能。\n","permalink":"https://ktzxy.top/posts/gkxr2wey8m/","summary":"Golang并发编程","title":"Golang并发编程"},{"content":"日志库 来源 https://www.liwenzhou.com/posts/Go/go_log/\n介绍 无论是软件开发的调试阶段还是软件上线之后的运行阶段，日志一直都是非常重要的一个环节，我们也应该养成在程序中记录日志的好习惯。\nGo语言内置的log包实现了简单的日志服务。本文介绍了标准库log的基本使用。\n使用Logger log包定义了Logger类型，该类型提供了一些格式化输出的方法。本包也提供了一个预定义的“标准”logger，可以通过调用函数Print系列(Print|Printf|Println）、Fatal系列（Fatal|Fatalf|Fatalln）、和Panic系列（Panic|Panicf|Panicln）来使用，比自行创建一个logger对象更容易使用。\n例如，我们可以像下面的代码一样直接通过log包来调用上面提到的方法，默认它们会将日志信息打印到终端界面\n1 2 3 4 5 6 7 8 9 10 11 12 13 package main import ( \u0026#34;log\u0026#34; ) func main() { log.Println(\u0026#34;这是一条很普通的日志。\u0026#34;) v := \u0026#34;很普通的\u0026#34; log.Printf(\u0026#34;这是一条%s日志。\\n\u0026#34;, v) log.Fatalln(\u0026#34;这是一条会触发fatal的日志。\u0026#34;) log.Panicln(\u0026#34;这是一条会触发panic的日志。\u0026#34;) } 编译并执行上面的代码会得到如下输出：\n1 2 3 2017/06/19 14:04:17 这是一条很普通的日志。 2017/06/19 14:04:17 这是一条很普通的日志。 2017/06/19 14:04:17 这是一条会触发fatal的日志 logger会打印每条日志信息的日期、时间，默认输出到系统的标准错误。Fatal系列函数会在写入日志信息后调用os.Exit(1)。Panic系列函数会在写入日志信息后panic。\n日志输出到文件中 我们正常的日志文件，是存储在文件中的，因此我们可以使用以下的方式，将日志存储在文件中\n1 2 3 4 5 6 7 8 9 10 11 12 13 func main() { fileObj, err := os.OpenFile(\u0026#34;./xx.log\u0026#34;, os.O_APPEND | os.O_CREATE | os.O_WRONLY, 0644) if err != nil { fmt.Printf(\u0026#34;open file failed, err : %v \\n\u0026#34;, err) return } // 设置log的输出路径 log.SetOutput(fileObj) for { log.Println(\u0026#34;这是一条测试日志\u0026#34;) time.Sleep(time.Second * 3) } } 日志库的简单实现 支持往不同的地方输出日志 日志分级别 debug Trace info warning Error Fatal：严重错误 日志要支持开关控制，比如说开发的时候什么级别都能输出，但是上线之后只有INFO级别往下才能输出 日志要有时间、行号、文件名、日志级别、日志信息 日志文件要切割 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 package main import ( \u0026#34;errors\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;path\u0026#34; \u0026#34;runtime\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;time\u0026#34; ) // 往终端写日志相关内容 type LogLevel uint16 // 定义日志级别 const( UNKNOWN LogLevel = iota // 0 DEBUG TRACE INFO WARNING ERROR FATAL ) // Logger日志结构体 type Logger struct { Level LogLevel } func parseLogLevel(s string) (LogLevel, error) { s = strings.ToLower(s) switch s { case \u0026#34;debug\u0026#34;: return DEBUG, nil case \u0026#34;trace\u0026#34;: return TRACE, nil case \u0026#34;info\u0026#34;: return INFO, nil case \u0026#34;warning\u0026#34;: return WARNING, nil case \u0026#34;error\u0026#34;: return ERROR, nil case \u0026#34;fatal\u0026#34;: return FATAL, nil default: err := errors.New(\u0026#34;无效的日志级别\u0026#34;) return UNKNOWN, err } } func getLogLevelStr(logLevel LogLevel) (string) { switch logLevel { case DEBUG: return \u0026#34;debug\u0026#34; case TRACE: return \u0026#34;trace\u0026#34; case INFO: return \u0026#34;info\u0026#34; case WARNING: return \u0026#34;warning\u0026#34; case ERROR: return \u0026#34;error\u0026#34; case FATAL: return \u0026#34;fatal\u0026#34; default: return \u0026#34;unknown\u0026#34; } } // 获取函数名、文件名、行号 // skip表示隔了几层 func getInfo(skip int)(funcName string, fileName string, lineNo int) { // pc：函数信息 // file：文件 // line：行号，也就是当前行号 pc, file, line, ok := runtime.Caller(skip) if !ok { fmt.Printf(\u0026#34;runtime.Caller() failed, err:%v \\n\u0026#34;) return } funName := runtime.FuncForPC(pc).Name() return funName, path.Base(file), line } // Logger构造方法 func NewLog(levelStr string) Logger { level, err := parseLogLevel(levelStr) if err != nil { panic(err) } // 构造了一个Logger对象 return Logger{ Level: level, } } // 判断啥级别的日志可以输出是否输出 func (l Logger) enable(logLevel LogLevel) bool { return logLevel \u0026gt;= l.Level } func printLog(lv LogLevel, msg string) { now := time.Now().Format(\u0026#34;2006-01-02 15:04:05\u0026#34;) // 拿到第二层的函数名 funcName, filePath, lineNo := getInfo(3) fmt.Printf(\u0026#34;[%s] [%s] [%s:%s:%d] %s \\n\u0026#34;, now, getLogLevelStr(lv),filePath, funcName, lineNo, msg) } func (l Logger) Debug(msg string) { if l.enable(DEBUG) { printLog(DEBUG, msg) } } func (l Logger) TRACE(msg string) { if l.enable(TRACE) { printLog(TRACE, msg) } } func (l Logger) Info(msg string) { if l.enable(INFO) { printLog(INFO, msg) } } func (l Logger) Warning(msg string) { if l.enable(WARNING) { printLog(WARNING, msg) } } func (l Logger) Error(msg string) { if l.enable(ERROR) { printLog(ERROR, msg) } } func (l Logger) Fatal(msg string) { if l.enable(FATAL) { printLog(FATAL, msg) } } func main() { log := NewLog(\u0026#34;ERROR\u0026#34;) for { log.Debug(\u0026#34;这是一条DEBUG日志\u0026#34;) log.Info(\u0026#34;这是一条INFO日志\u0026#34;) log.Warning(\u0026#34;这是一条WARNING日志\u0026#34;) log.Error(\u0026#34;这是一条ERROR日志\u0026#34;) log.Fatal(\u0026#34;这是一条FATAL日志\u0026#34;) fmt.Println(\u0026#34;----------------\u0026#34;) time.Sleep(time.Second) } } ","permalink":"https://ktzxy.top/posts/mzrnivn1rt/","summary":"日志库","title":"日志库"},{"content":"﻿\nDay-07-java异常 什么是异常\n实际工作中，遇到的情况不可能是非常完美的。比如:你写的某个模块，用户输入不一定符合你的要求、你的程序要打开某个文件，这个文件可能不存在或者文件格式不对，你要读取数据库的数据，数据可能是空的等。我们的程序再跑着，内存或硬盘可能满了。等等。\n软件程序在运行过程中，非常可能遇到刚刚提到的这些异常问题，我们叫异常，英文是:Exception，意思是例外。这些，例外情况，或者叫异常，怎么让我们写的程序做出合理的处理。而不至于程序崩溃。\n异常指程序运行中出现的不期而至的各种状况,如:文件找不到、网络连接失败、非法参数等。异常发生在程序运行期间,它影响了正常的程序执行流程。\n简单分类 要理解Java异常处理是如何工作的，你需要掌握以下三种类型的异常:\n检查性异常:最具代表的检查性异常是用户错误或问题引起的异常，这是程序员无法预见的。例如要打开一个不存在文件时，一个异常就发生了，这些异常在编译时不能被简单地忽略。\n运行时异常:运行时异常是可能被程序员避免的异常。与检查性异常相反，运行时异常可以在编译时被忽略。\n错误:错误不是异常，而是脱离程序员控制的问题。错误在代码中通常被忽略。例如，当栈溢出时，一个错误就发生了，它们在编译也检查不到的。\n层次结构 异常体系结构 Java把异常当作对象来处理，并定义一个基类java.lang.Throwable作为所有异常的超类。在Java API中已经定义了许多异常类，这些异常类分为两大类，错误Error和异常Exception.\nError Error类对象由Java虚拟机生成并抛出，大多数错误与代码编写者所执行的操作无关\nJava虚拟机运行错误(Virtual MachineError)，当JVM不再有继续执行操作所需的内存资源时，将出现OutOfMemoryError。这些异常发生时，Java虚拟机(JVM)一般会选择线程终止;\n还有发生在虚拟机试图执行应用时，如类定义错误(NoClassDefFoundError)、链接错误(LinkageError)。这些错误是不可查的，因为它们在应用程序的控制和处理能力之外，而且绝大多数是程序运行时不允许出现的状况。\nException 在Exception分支中有一个重要的子类RuntimeException(运行时异常) ArraylndexOutOfBoundsException(数组下标越界) NullPointerException(空指针异常) ArithmeticException(算术异常) MissingResourceException(丢失资源) ClassNotFoundException(找不到类）等异常，这些异常是不检查异常，程序中可以选择捕获处理，也可以不处理。\n这些异常一般是由程序逻辑错误引起的，程序应该从逻辑角度尽可能避免这类异常的发生;\nError和Exception的区别: Error通常是灾难性的致命的错误，是程序无法控制和处理的，当出现这些异常时，Java虚拟机(JVM)一般会选择终止线程; Exception通常情况下是可以被程序处理的，并且在程序中应该尽可能的去处理这些异常。\n异常处理机制 抛出异常 捕获异常\n异常处理五个关键字 try、catch、finally、throw、throws\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class demo1 { public static void main(String[] args) { int a = 1; int b = 0; //Ctrl + Alt + T try{ System.out.println(a/b); }catch (ArithmeticException e){ //catch 捕获异常 System.out.println(\u0026#34;程序出现异常，变量b不能为0\u0026#34;); }finally { //处理善后工作 System.out.println(\u0026#34;finally\u0026#34;); } //finally 可以不要finally ， 假设IO ，资源 ，关闭 } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class demo2 { public static void main(String[] args) { try { new demo2().test(1,0); } catch (ArithmeticException e) { e.printStackTrace(); } } //假设这方法中，处理不了这个异常。方法上抛出异常 public void test(int a,int b) throws ArithmeticException{ if (b==0){ //throw throws throw new ArithmeticException(); //主动抛出异常，一般在方法中使用 } } } 一：在什么情况下，try-catch后面的代码不执行?\n​\t1.throw抛出异常的情况\n​\t2.catch中没有正常的进行异常捕获\n​\t3.在try中遇到return\n二：怎么样才可以将try-catch后面的代码必须执行?\n​\t只要将必须执行的代码放入finally中，那么这个代码无论如何一定执行。\n三：return和finally执行顺序?\n​\t先执行finally最后执行return\n四：什么代码会放在finally中呢?\n关闭数据库资源，关闭IO流资源，关闭socket资源。\n五：有一句话代码很厉害，它可以让finally中代码不执行!\n​\tSystem.exit(0);//终止当前的虚拟机执行\n六：try中出现异常以后，将异常类型跟catch后面的类型依次比较，按照代码的顺序进行比对，执行第一个与异常类型匹配的catch语句\n七：一旦执行其中一条catch语句之后，后面的catch语句就会被忽略了!\n八：在安排catch语句的顺序的时候，一般会将特殊异常放在前面(并列)，一般化的异常放在后面。先写子类异常，再写父类异常。\n九：在JDK1.7以后，异常新处理方式:可以并列用|符号连接:\n十：throw和throws的区别:\n​\t(1)位置不同:\n​\tthrow:方法内部\n​\tthrows:方法的签名处，方法的声明处\n​\t(2)内容不同:\n​\tthrow+异常对象(检查异常，运行时异常)\n​\tthrows+异常的类型(可以多个类型，用，拼接)\n​\t(3)作用不同: ​\tthrow:异常出现的源头，制造异常。 ​\tthrows:在方法的声明处，告诉方法的调用者，这个方法中可能会出现我声明的这些异常。然后调用者对这个异常进行处理:要么自己处理要么再继续向外抛出异常\n十一：重载和重写的区别: 重载:在同一个类中，当方法名相同，形参列表不同的时候多个方法构成了重载 重写:在不同的类中，子类对父类提供的方法不满意的时候，要对父类的方法进行重写。\n自定义异常 使用Java内置的异常类可以描述在编程时出现的大部分异常情况。除此之外，用户还可以自定义异常。用户自定义异常类，只需继承Exception类即可。\n在程序中使用自定义异常类，大体可分为以下几个步骤: 1．创建自定义异常类。 2．在方法中通过throw关键字抛出异常对象。 3.如果在当前抛出异常的方法中处理异常，可以使用try-catch语句捕获并处理;否则在方法 的声明处通过throws关键字指明要抛出给方法调用者的异常，继续进行下一步操作。\n​\t4。在出现异常方法的调用者中捕获并处理异常。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 /自定义的异常类 public class MyException extends Exception{ //传递数字\u0026gt;10; private int detail; public MyException(int a){ this.detail = a; } //toString Alt + Insert @Override //异常的打印信息 public String toString() { return \u0026#34;MyException{\u0026#34; + \u0026#34;detail=\u0026#34; + detail + \u0026#39;}\u0026#39;; } } =============================== public class demo3 { static void test(int a) throws MyException{ System.out.println(\u0026#34;传递的参数为：\u0026#34;+a); if (a\u0026gt;10){ throw new MyException(a); } System.out.println(\u0026#34;OK\u0026#34;); } public static void main(String[] args) { try { test(11); } catch (MyException e) { System.out.println(\u0026#34;MyException is \u0026#34;+e); } } } 实际应用中的经验总结 处理运行时异常时，采用逻辑去合理规避同时辅助try-catch处理\n在多重catch块后面，可以加一个catch (Exception)来处理可能会被遗漏的异常\n对于不确定的代码，也可以加上try-catch，处理潜在的异常\n尽量去处理异常，切忌只是简单地调用printStackTrace()去打印输出\n具体如何处理异常，要根据不同的业务需求和异常类型去决定\n尽量添加finally语句块去释放占用的资源\n","permalink":"https://ktzxy.top/posts/i7xvcz8xnq/","summary":"Day 07 java异常","title":"Day 07 java异常"},{"content":"SpringCloud快速入门 1.前言 学习前提\n熟练使用SpringBoot 微服务快速开发框架 了解过Dubbo + Zookeeper 分布式基础 电脑配置内存不低于8G(个人是16G) SpringCloud五大组件\n参考CSDN博文：https://blog.csdn.net/weixin_41217541/article/details/104718834 组件 选型 备注 网关 Zuul 服务注册与发现 Eureka Consul zookeeper 服务调用 Feign 根据注解和选择机器,拼接Url地址,发起请求 简化服务调用 负载均衡 Ribbon 服务调用负载均衡，配合Feign和Euraka使用 断路器 Hystrix 隔离、熔断以及降级的一个框架 服务线程池隔离，实现不同服务的调度隔离，避免服务雪崩 Eureka:服务启动时,Eureka会将服务注册到EurekaService,并且EurakeClient还可以返回过来从EurekaService拉去注册表,从而知道服务在哪里。 Ribbon:服务间发起请求的时候,基于Ribbon服务做到负载均衡,从一个服务的对台机器中选择一台。 Feign:基于fegin的动态代理机制,根据注解和选择机器,拼接Url地址,发起请求。 Hystrix:发起的请求是通过Hystrix的线程池来走,不同的服走不同的线程池,实现了不同的服务调度隔离,避免服务雪崩的问题。 Zuul:如果前端后端移动端调用后台系统,统一走zull网关进入,有zull网关转发请求给对应的服务。 常见面试题\n什么是微服务？\n微服务之间是如何独立通讯的？\nSpringCloud 和 Dubbo有哪些区别？\nSpringBoot和SpringCloud，请你谈谈对他们的理解\n什么是服务熔断？什么是服务降级\n微服务的优缺点是分别是什么？说下你在项目开发中遇到的坑\n你所知道的微服务技术栈有哪些？请列举一二\neureka和zookeeper都可以提供服务注册与发现的功能，请说说两个的区别？\n2.微服务概述 什么是微服务？\n微服务（Microservice Architecture）是近几年流行的一种架构思想，关于它的概念很 难一言以蔽之。究竟什么是微服务呢？我们在此引用 ThoughtWorks 公司的首席科学家 Martin Fowler 于2014年提出的一段话：\n原文：https://martinfowler.com/articles/microservices.html\n汉化：https://www.cnblogs.com/liuning8023/p/4493156.html\n就目前而言，对于微服务，业界并没有一个统一的，标准的定义\n但通常而言，微服务架构是一种架构模式，或者说是一种架构风格， 它提倡将单一的应用程序划分成一组小的服务，每个服务运行在其独立的自己的进程内，服务之间互相协调，互相配置，为用户提供最终价值。服务之间采用轻量级的通信机制互相沟通，每个服务都围绕着具体的业务进行构建，并且能够被 独立的部署到生产环境中，另外，应尽量避免统一的，集中式的服务管理机制，对具体的一个服务而言，应根据业务上下文，选择合适的语言，工具对其进行构建，可以有一个非常轻量级的集中式管理来 协调这些服务，可以使用不同的语言来编写服务，也可以使用不同的数据存储；\n可能有的人觉得官方的话太过生涩，我们从技术维度来理解下：\n微服务化的核心就是将传统的一站式应用，根据业务拆分成一个一个的服务，彻底地去耦合，每一个微服务提供单个业务功能的服务，一个服务做一件事情，从技术角度看就是一种小而独立的处理过程，类似进程的概念，能够自行单独启动或销毁，拥有自己独立的数据库。\n微服务与微服务架构：\n微服务\n强调的是服务的大小，他关注的是某一个点，是具体解决某一个问题/提供落地对应服务的一个服务应用，狭义的看，可以看做是IDEA中的一个个微服务工程，或者Moudel 微服务架构\n一种新的架构形式，Martin Fowler，2014提出！\n微服务架构是一种架构模式，它提倡将单一应用程序划分成一组小的服务，服务之间互相协调，互相配合，为用户提供最终价值。每个服务运行在其独立的进程中，服务于服务间采用轻量级的通信机制互相协作，每个服务都围绕着具体的业务进行构建，并且能够被独立的部署到生产环境中，另外，应尽量避免统一的，集中式的服务管理机制，对具体的一个服务而言，应根据业务上下文，选择合适的语言，工具对其进行构建。\n微服务优缺点\n优点：\n每个服务足够内聚，足够小，代码容易理解，这样能聚焦一个指定的业务功能或业务需求； 开发简单，开发效率提高，一个服务可能就是专一的只干一件事； 微服务能够被小团队单独开发，这个小团队是2~5人的开发人员组成； 微服务是松耦合的，是有功能意义的服务，无论是在开发阶段或部署阶段都是独立的。 微服务能使用不同的语言开发。 易于和第三方集成，微服务允许容易且灵活的方式集成自动部署，通过持续集成工具，如jenkins， Hudson，bamboo 微服务易于被一个开发人员理解，修改和维护，这样小团队能够更关注自己的工作成果。无需通过合作才能体现价值。 微服务允许你利用融合最新技术。 微服务只是业务逻辑的代码，不会和HTML，CSS或其他界面混合 每个微服务都有自己的存储能力，可以有自己的数据库，也可以有统一数据库 缺点：\n开发人员要处理分布式系统的复杂性 多服务运维难度，随着服务的增加，运维的压力也在增大 系统部署依赖 服务间通信成本 数据一致性 系统集成测试 性能监控\u0026hellip;.. 微服务技术栈有哪些？\n微服务条目 落地技术 服务开发 SpringBoot,Spring,SpringMVC 服务配置与管理 Netflix公司的Archaius、阿里的Diamond等 服务注册与发现 Eureka、Consul、Zookeeper等 服务调用 Rest、RPC、gRPC 服务熔断器 Hystrix、Envoy等 负载均衡 Ribbon、Nginx等 服务接口调用（客户端调用服务的简化工具） Feign等 消息队列 Kafka、RabbitMQ、ActiveMQ等 服务配置中心管理 SpringCloudConfig、Chef等 服务路由（API网关） 服务监控 服务监控 Zabbix、Nagios、Metrics、Specatator等 全链路追踪 Zipkin、Brave、Dapper等 服务部署 Docker、OpenStack、Kubernetes等 数据流操作开发包 SpringCloud Stream(封装与Redis，Rabbit，Kafka等发 送接收消息) 事件消息总线 SpringCloud Bus Spring Cloud Alibaba 为什么选择SpringCloud作为微服务架构？\n1、选型依据\n整体解决方案和框架成熟度 社区热度 可维护性 学习曲线 2、当前各大IT公司用的微服务架构有哪些？\n阿里：dubbo+HFS 京东：JSF 新浪：Motan 当当网 DubboX \u0026hellip;\u0026hellip;. 3、各微服务框架对比\n功能点/ 服务框架 Netflix/SpringCloud Motan gRPC Thrift Dubbo/DubboX 功能定位 完整的微服务框架 RPC框架，但整合了ZK或Consul，实现集群环境的基本服务注册/发现 RPC框 架 RPC框架 服务框架 支持Rest 是，Ribbon支持多种可插拔的序列化选择 否 否 否 否 支持RPC 否 是（Hession2） 是 是 是 支持多语言 是（Rest形式）？ 否 是 是 否 负载均衡 是（服务端zuul+客户端 Ribbon），zuul-服务，动态路由，云端负载均衡 Eureka（针对中间层服务器） 是（客户端） 否 否 是（客户端） 配置服务 Netfix Archaius，Spring Cloud Config Server集中 配置 是（zookeeper提供） 否 否 否 服务调用链监控 是（zuul），zuul提供边缘服务，API网关 否 否 否 否 高可用/容错 是（服务端Hystrix+客户端Ribbon） 是（客户端） 否 否 是（客户端） 典型应用案例 Netflix Sina Google Facebook 社区活跃程度 高 一般 高 一般 2017年后重新开始维护，之前中断了5年 学习难度 中断 低 高 高 低 文档丰富程度 高 一般 一般 一般 高 其他 Spring Cloud Bus为我们 的应用程序带来了更多管理端点 支持降级 Netflix 内部在开发集成 gRPC IDL定义 实践的公司比较 多 3.SpringCloud入门 SpringCloud是什么？\nSpring官网:https://spring.io/ 地址：Spring (xy2401.com) SpringCloud, 基于SpringBoot提供了一套微服务解决方案，包括服务注册与发现，配置中心，全链路监控，服务网关，负载均衡，熔断器等组件，除了基于NetFlix的开源组件做高度抽象封装之外，还有一些选型中立的开源组件。\nSpringCloud利用SpringBoot的开发便利性，巧妙地简化了分布式系统基础设施的开发，SpringCloud为开发人员提供了快速构建分布式系统的一些工具，==包括配置管理，服务发现，断路器，路由，微代理，事件总线，全局锁，决策竞选，分布式会话等等==，他们都可以用SpringBoot的开发风格做到一键启动和部署。\nSpringBoot并没有重复造轮子，它只是将目前各家公司开发的比较成熟，经得起实际考研的服务框架组合起来，通过SpringBoot风格进行再封装，屏蔽掉了复杂的配置和实现原理，最终给开发者留出了一套简单易懂，易部署和易维护的分布式系统开发工具包。\nSpringCloud是分布式微服务架构下的一站式解决方案，是各个微服务架构落地技术的集合体，俗称微 服务全家桶。\nSpringCloud和SpringBoot关系\nSpringBoot专注于快速方便的开发单个个体微服务。 SpringCloud是关注全局的微服务协调整理治理框架，它将SpringBoot开发的一个个单体微服务整合并管理起来，为各个微服务之间提供：配置管理，服务发现，断路器，路由，微代理，事件总线，全局锁，决策竞选，分布式会话等等集成服务。 SpringBoot可以离开SpringClooud独立使用，开发项目，但是SpringCloud离不开SpringBoot，属于依赖关系。 SpringBoot专注于快速、方便的开发单个个体微服务，SpringCloud关注全局的服务治理框架。 分布式+服务治理Dubbo 目前成熟的互联网架构，应用服务化拆分 + 消息中间件。 Dubbo和SpringCloud对比\n可以看一下社区活跃度： https://github.com/dubbo https://github.com/spring-cloud 对比结果： Dubbo SpringCloud 服务注册中心 Zookeeper Spring Cloud Netfilx Eureka 服务调用方式 RPC REST API 服务监控 Dubbo-monitor Spring Boot Admin 断路器 不完善 Spring Cloud Netfilx Hystrix 服务网关 无 Spring Cloud Netfilx Zuul 分布式配置 无 Spring Cloud Config 服务跟踪 无 Spring Cloud Sleuth 消息总栈 无 Spring Cloud Bus 数据流 无 Spring Cloud Stream 批量任务 无 Spring Cloud Task ==最大区别：SpringCloud抛弃了Dubbo的RPC通信，采用的是基于HTTP的REST方式==。\n严格来说，这两种方式各有优劣。虽然从一定程度上来说，后者牺牲了服务调用的性能，但也避免了上 面提到的原生RPC带来的问题。而且REST相比RPC更为灵活，服务提供方和调用方的依赖只依靠一纸契约，不存在代码级别的强依赖，这在强调快速演化的微服务环境下，显得更加合适。\n品牌机与组装机的区别\n很明显，Spring Cloud的功能比DUBBO更加强大，涵盖面更广，而且作为Spring的拳头项目，它也能够与Spring Framework、Spring Boot、Spring Data、Spring Batch等其他Spring项目完美融合，这些对 于微服务而言是至关重要的。使用Dubbo构建的微服务架构就像组装电脑，各环节我们的选择自由度很 高，但是最终结果很有可能因为一条内存质量不行就点不亮了，总是让人不怎么放心，但是如果你是一名高手，那这些都不是问题；而Spring Cloud就像品牌机，在Spring Source的整合下，做了大量的兼容性测试，保证了机器拥有更高的稳定性，但是如果要在使用非原装组件外的东西，就需要对其基础有足够的了解。 社区支持与更新力度\n最为重要的是，DUBBO停止了5年左右的更新，虽然2017.7重启了。对于技术发展的新需求，需要由开发者自行拓展升级（比如当当网弄出了DubboX），这对于很多想要采用微服务架构的中小软件组织，显然是不太合适的，中小公司没有这么强大的技术能力去修改Dubbo源码+周边的一整套解决方案，并不是每一个公司都有阿里的大牛+真实的线上生产环境测试过。 设计模式+微服务拆分思想。 总结：\n曾风靡国内的开源 RPC 服务框架Dubbo在重启维护后，令许多用户为之雀跃，但同时，也迎来了一些质疑的声音。互联网技术发展迅速，Dubbo是否还能跟上时代？Dubbo 与 Spring Cloud相比又有何优势和差异？是否会有相关举措保证Dubbo的后续更新频率？ 人物：Dubbo重启维护开发的刘军，主要负责人之一。 刘军，阿里巴巴中间件高级研发工程师，主导了Dubbo 重启维护以后的几个发版计划，专注于高性能 RPC 框架和微服务相关领域。曾负责网易考拉RPC框架的研发及指导在内部使用，参与了服务治理平台、分布式跟踪系统、分布式一致性框架等从无到有的设计与开发过程。 ==解决的问题域不一样：Dubbo的定位是一款RPC框架，Spring Cloud的目标是微服务架构下的一站式解决方案==。 SpringCloud能干嘛？\nDistributed/versioned configuration （分布式/版本控制配置） Service registration and discovery（服务注册与发现） Routing（路由） Service-to-service calls（服务到服务的调用） Load balancing（负载均衡配置） Circuit Breakers（断路器） Distributed messaging（分布式消息管理） \u0026hellip;. SpringCloud在哪获取？\n官网：http://projects.spring.io/spring-cloud/ 它的版本号有点特别： Spring Cloud是一个由众多独立子项目组成的大型综合项目，每个子项目有不同的发行节奏，都维护着自己的发布版本号。Spring Cloud通过一个资源清单BOM（Bill of Materials）来管理每个版本的子项目清单。为避免与子项目的发布号混淆，所以没有采用版本号的方式，而是通过命名的方式。 这些版本名称的命名方式采用了伦敦地铁站的名称，同时根据字母表的顺序来对应版本时间顺序，比如：最早的Release版本：Angel，第二个Release版本：Brixton，然后是Camden、Dalston、Edgware，Finchley,目前最新的是Hoxton 版本。 自学参考：\nSpringCloud Netflix 中文文档：https://springcloud.cc/spring-cloud-netflix.html SpringCloud 中文API文档(官方文档翻译版)：https://springcloud.cc/spring-cloud-dalston.html SpringCloud中国社区：http://docs.springcloud.cn/ SpringCloud中文网：https://springcloud.cc 4.Rest微服务构建 介绍\n使用一个Dept部门模块做一个微服务通用案例。 Consumer消费者（Client）通过REST调用Provider提供者（Server）提供的服务。 回忆Spring，SpringMVC，MyBatis等以往学习的知识。。。 Maven的分包分模块架构复习 1 2 3 4 5 6 7 8 9 10 一个简单的Maven模块结构是这样的： -- app-parent：一个父项目（app-parent）聚合很多子项目（app-util，app-dao，app\u0002web...） |-- pom.xml | |-- app-core ||----pom.xml | |-- app-web ||----pom.xml ...... 一个父工程带着多个子Module子模块。\nSpringCloud父工程（Project）下初次带着3个子模块（Module）\nspringcloud-api 【封装的整体entity / 接口 / 公共配置等】 springcloud-provider-dept-8001【服务提供者】 springcloud-consumer-dept-80【服务消费者】 SpringCloud版本选择\nSpringBoot SpringCloud 关系 1.2.x Angel版本(天使) 兼容SpringBoot1.2x 1.3.x Brixton版本(布里克斯顿) 兼容SpringBoot1.3x，也兼容SpringBoot1.4x 1.4.x Camden版本(卡姆登) 兼容SpringBoot1.4x，也兼容SpringBoot1.5x 1.5.x Dalston版本(多尔斯顿) 兼容SpringBoot1.5x，不兼容SpringBoot2.0x 1.5.x Edgware版本(埃奇韦尔) 兼容SpringBoot1.5x，不兼容SpringBoot2.0x 2.0.x Finchley版本(芬奇利) 兼容SpringBoot2.0x，不兼容SpringBoot1.5x 2.1.x Greenwich版本(格林威治) 实际开发版本关系 spring-boot-starter-parent spring-cloud-dependencles 版本号 发布日期 版本号 发布日期 1.5.2.RELEASE 2017-03 Dalston.RC1 2017-x 1.5.9.RELEASE 2017-11 Edgware.RELEASE 2017-11 1.5.16.RELEASE 2018-04 Edgware.SR5 2018-10 1.5.20.RELEASE 2018-09 Edgware.SR5 2018-10 2.0.2.RELEASE 2018-05 Fomchiey.BULD-SNAPSHOT 2018-x 2.0.6.RELEASE 2018-10 Fomchiey-SR2 2018-10 2.1.4.RELEASE 2019-04 Greenwich.SR1 2019-03 使用后两个 创建父工程\n新建父工程Maven项目 springcloud-parent，切记Packageing是pom模式。 主要是定义POM文件，将后续各个子模块公用的jar包等统一提取出来，类似一个抽象父类。 pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;com.github\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;springcloud\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;modules\u0026gt; \u0026lt;/modules\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;project.build.sourceEncoding\u0026gt;UTF-8\u0026lt;/project.build.sourceEncoding\u0026gt; \u0026lt;maven.compiler.source\u0026gt;8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;junit.version\u0026gt;4.13.2\u0026lt;/junit.version\u0026gt; \u0026lt;log4j.version\u0026gt;1.2.17\u0026lt;/log4j.version\u0026gt; \u0026lt;lombok.version\u0026gt;1.18.22\u0026lt;/lombok.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;!--打包方式 pom--\u0026gt; \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-alibaba-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.2.0.RELEASE\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--springCloud的依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;Greenwich.SR1\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--SpringBoot--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.1.4.RELEASE\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--数据库--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.1.47\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;druid\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.10\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--SpringBoot 启动器--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis.spring.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.3.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--日志测试~--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;ch.qos.logback\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;logback-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${junit.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;log4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;log4j\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${log4j.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${lombok.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; \u0026lt;/project\u0026gt; 创建api公共模块\n新建springcloud-api模块 可以观察发现，在父工程中多了一个Modules。 编写springcloud-api 的 pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;artifactId\u0026gt;springcloud\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;com.github\u0026lt;/groupId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;!--当前Module的名字--\u0026gt; \u0026lt;artifactId\u0026gt;springcloud-api\u0026lt;/artifactId\u0026gt; \u0026lt;!--当前Module需要到的jar包，按自己需求添加，如果父项目已经包含了，可以不用写版本号--\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;/project\u0026gt; 创建部门数据库脚本，数据库名：springcloud 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 CREATE TABLE dept ( deptno BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT, dname VARCHAR ( 60 ), db_source VARCHAR ( 60 ) ); INSERT INTO dept ( dname, db_source ) VALUES ( \u0026#39;开发部\u0026#39;, DATABASE ()); INSERT INTO dept ( dname, db_source ) VALUES ( \u0026#39;人事部\u0026#39;, DATABASE ()); INSERT INTO dept ( dname, db_source ) VALUES ( \u0026#39;财务部\u0026#39;, DATABASE ()); INSERT INTO dept ( dname, db_source ) VALUES ( \u0026#39;市场部\u0026#39;, DATABASE ()); INSERT INTO dept ( dname, db_source ) VALUES ( \u0026#39;运维部\u0026#39;, DATABASE ()); SELECT * FROM dept; 编写实体类，注意：实体类都序列化！ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.github.pojo; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import java.io.Serializable; @NoArgsConstructor @Data @Accessors(chain = true) // 链式写法 public class Dept implements Serializable { // Dept(实体类) orm mysql-\u0026gt;Dept(表) 类表关系映射 private Long deptno; // 主键 private String dname; // 部门名称 // 来自哪个数据库，因为微服务架构可以一个服务对应一个数据库，同一个信息被存到多个不同的数据库 private String db_source; public Dept(String dname) { this.dname = dname; } /* 链式写法： Dept dept = new Dept() dept.setDeptno(11L).setDname(\u0026#34;school\u0026#34;).setDb_source(\u0026#34;DB01\u0026#34;); */ } 创建provider模块\n新建springcloud-provider-dept-8001模块 编辑pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;artifactId\u0026gt;springcloud\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;com.github\u0026lt;/groupId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;artifactId\u0026gt;springcloud-provider-dept-8001\u0026lt;/artifactId\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!--引入自定义的模块，我们就可以使用这个模块中的类了--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.github\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;springcloud-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;druid\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;ch.qos.logback\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;logback-core\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis.spring.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-jetty\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-test\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- spring-boot-devtools热部署 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-devtools\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; 编辑 application.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 server: port: 8001 # mybatis的配置 mybatis: config-location: classpath:mybatis/mybatis-config.xml type-aliases-package: com.github.pojo mapper-locations: - classpath:mybatis/mapper/**/*.xml # spring的相关配置 spring: application: name: springcloud-provider-dept datasource: type: com.alibaba.druid.pool.DruidDataSource # 数据源 driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动 url: jdbc:mysql://localhost:3306/springcloud #数据库名称 username: root password: root dbcp2: min-idle: 5 #数据库连接池的最小维持连接数 initial-size: 5 #初始化连接数 max-total: 5 #最大连接数 max-wait-millis: 200 #等待连接获取的最大超时时间 根据配置新建mybatis-config.xml文件 1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE configuration PUBLIC \u0026#34;-//mybatis.org//DTD Config 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-config.dtd\u0026#34;\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;settings\u0026gt; \u0026lt;!--开启二级缓存--\u0026gt; \u0026lt;setting name=\u0026#34;cacheEnabled\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;/settings\u0026gt; \u0026lt;/configuration\u0026gt; 编写部门的dao接口 1 2 3 4 5 6 @Mapper public interface DeptDao { public boolean addDept(Dept dept); // 添加一个部门 public Dept queryById(Long id); // 根据id查询部门 public List\u0026lt;Dept\u0026gt; queryAll(); // 查询所有部门 } 接口对应的Mapper.xml文件 mybatis\\mapper\\DeptMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;com.github.dao.DeptDao\u0026#34;\u0026gt; \u0026lt;insert id=\u0026#34;addDept\u0026#34; parameterType=\u0026#34;Dept\u0026#34;\u0026gt; insert into dept (dname,db_source) values (#{dname},DATABASE()); \u0026lt;/insert\u0026gt; \u0026lt;select id=\u0026#34;queryById\u0026#34; resultType=\u0026#34;Dept\u0026#34; parameterType=\u0026#34;Long\u0026#34;\u0026gt; select deptno,dname,db_source from dept where deptno = #{deptno}; \u0026lt;/select\u0026gt; \u0026lt;select id=\u0026#34;queryAll\u0026#34; resultType=\u0026#34;Dept\u0026#34;\u0026gt; select deptno,dname,db_source from dept; \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; 创建Service服务层接口 1 2 3 4 5 public interface DeptService { public boolean addDept(Dept dept); // 添加一个部门 public Dept queryById(Long id); // 根据id查询部门 public List\u0026lt;Dept\u0026gt; queryAll(); // 查询所有部门 } ServiceImpl实现类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Service public class DeptServiceImpl implements DeptService { // 自动注入 @Autowired private DeptDao deptDao; @Override public boolean addDept(Dept dept) { return deptDao.addDept(dept); } @Override public Dept queryById(Long id) { return deptDao.queryById(id); } @Override public List\u0026lt;Dept\u0026gt; queryAll() { return deptDao.queryAll(); } } ServiceImpl实现类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RestController @RequestMapping(\u0026#34;/dept\u0026#34;) public class DeptController { @Autowired private DeptService service; // @RequestBody // 如果参数是放在请求体中，传入后台的话，那么后台要用@RequestBody才能接收到 @PostMapping(\u0026#34;/add\u0026#34;) public boolean addDept(@RequestBody Dept dept) { return service.addDept(dept); } @GetMapping(\u0026#34;/get/{id}\u0026#34;) public Dept get(@PathVariable(\u0026#34;id\u0026#34;) Long id) { return service.queryById(id); } @GetMapping(\u0026#34;/list\u0026#34;) public List\u0026lt;Dept\u0026gt; queryAll() { return service.queryAll(); } } 编写DeptProvider的主启动类 1 2 3 4 5 6 @SpringBootApplication public class DeptProvider8001 { public static void main(String[] args) { SpringApplication.run(DeptProvider8001.class,args); } } 启动测试，注意编写细节： 创建consumer模块\n新建springcloud-consumer-dept-80模块 编辑pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;artifactId\u0026gt;springcloud\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;com.github\u0026lt;/groupId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;artifactId\u0026gt;springcloud-consumer-dept-80\u0026lt;/artifactId\u0026gt; \u0026lt;description\u0026gt;部门微服务消费者\u0026lt;/description\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.github\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;springcloud-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--热部署--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-devtools\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; application.yml配置文件 1 2 server: port: 80 新建一个ConfigBean包注入 RestTemplate！ 1 2 3 4 5 6 7 8 @Configuration public class ConfigBean { @Bean public RestTemplate getRestTemplate(){ return new RestTemplate(); } } 创建Controller包，编写DeptConsumerController类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @RestController public class DeptConsumerController { // 理解：消费者，不应该有service层 // 使用RestTemplate访问restful接口非常的简单粗暴且无脑 // （url，requestMap，ResponseBean.class） 这三个参数分别代表 // REST请求地址，请求参数，Http响应转换被转换成的对象类型 @Autowired private RestTemplate restTemplate; private static final String REST_URL_PREFIX = \u0026#34;http://localhost:8001\u0026#34;; @RequestMapping(\u0026#34;/consumer/dept/add\u0026#34;) public boolean add(Dept dept){ return restTemplate.postForObject(REST_URL_PREFIX+\u0026#34;/dept/add\u0026#34;,dept,Boolean.class); } @RequestMapping(\u0026#34;/consumer/dept/get/{id}\u0026#34;) public Dept get(@PathVariable(\u0026#34;id\u0026#34;) Long id){ return restTemplate.getForObject(REST_URL_PREFIX+\u0026#34;/dept/get/\u0026#34;+id,Dept.class); } @RequestMapping(\u0026#34;/consumer/dept/list\u0026#34;) public List\u0026lt;Dept\u0026gt; list(){ return restTemplate.getForObject(REST_URL_PREFIX+\u0026#34;/dept/list\u0026#34;,List.class); } } 了解RestTemplate：\nRestTemplate提供了多种便捷访问远程Http服务的方法，是一种简单便捷的访问restful服务模板类，是Spring提供的用于访问Rest服务的客户端模板工具集。 使用RestTemplate访问restful接口非常的简单粗暴且无脑 （url，requsetMap，ResponseBean.class）这三个参数分别代表REST请求地址，请求参数，Http响应转换被转换成的对象类型。 主启动类\n1 2 3 4 5 6 @SpringBootApplication public class DeptConsumer80 { public static void main(String[] args) { SpringApplication.run(DeptConsumer80.class,args); } } 测试访问，先启动服务方：DeptProvider8001，再启动消费方：DeptConsumer80： 5.Eureka服务注册与发现 什么是Eureka？\nEureka（服务发现框架） Netflix 在设计Eureka时，遵循的就是API原则！ CAP原则又称CAP定理，指的是在一个分布式系统中，一致性（Consistency）、可用性（Availability）、分区容错性（Partition tolerance）。CAP 原则指的是，这三个要素最多只能同时实现两点，不可能三者兼顾。 Eureka是Netflix的一个子模块，也是核心模块之一。Eureka是一个基于REST的服务，用于定位服务，以实现云端中间层服务发现和故障转移，服务注册与发现对于微服务来说是非常重要的，有了服务发现 与注册，只需要使用服务的标识符，就可以访问到服务，而不需要修改服务调用的配置文件了，功能类 似于Dubbo的注册中心，比如Zookeeper。 Eureka的基本架构：\nSpringcloud 封装了Netflix公司开发的Eureka模块来实现服务注册与发现 (对比Zookeeper)。 SpringCloud将它集成在其子项目spring-cloud-netflix中，以实现SpringCloud的服务发现功能。 Eureka采用了C-S的架构设计，EurekaServer作为服务注册功能的服务器，他是服务注册中心。 而系统中的其他微服务，使用Eureka的客户端连接到EurekaServer并维持心跳连接。这样系统的维护人员就可以通过EurekaServer来监控系统中各个微服务是否正常运行，Springcloud 的一些其他模块 (比如Zuul) 就可以通过EurekaServer来发现系统中的其他微服务，并执行相关的逻辑。 和Dubbo架构对比： Eureka包含两个组件：Eureka Server和Eureka Client。 Eureka Server提供服务注册服务，各个节点启动后，会在Eureka Server中进行注册，这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息，服务节点的信息可以在界面中直观的看到。 Eureka Client是一个java客户端，用于简化与Eureka Server的交互，客户端同时也就是一个内置的、使用轮询(round-robin)负载算法的负载均衡器。 在应用启动后，将会向Eureka Server发送心跳,默认周期为30秒，如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳，Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)。 三大角色：\nEureka Server：提供服务的注册于发现。 Service Provider：将自身服务注册到Eureka中，从而使消费方能够找到。 Service Consumer：服务消费方从Eureka中获取注册服务列表，从而找到消费服务。 服务构建\n建立springcloud-eureka-7001模块 编辑pom.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;artifactId\u0026gt;springcloud\u0026lt;/artifactId\u0026gt; \u0026lt;groupId\u0026gt;com.github\u0026lt;/groupId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;artifactId\u0026gt;springcloud-eureka-7001\u0026lt;/artifactId\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- eureka-server服务端 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-eureka-server\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--热部署--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-devtools\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; application.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 server: port: 7001 # Eureka配置 eureka: instance: hostname: localhost #eureka服务端的实例名称 client: register-with-eureka: false #是否将自己注册到Eureka服务器中，本身是服务器，无需注册 fetch-registry: false #false表示自己端就是注册中心，职责是维护服务实例，并不需要检索服务 service-url: defaultzone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个defaultzone地址 编写主启动类 1 2 3 4 5 6 7 8 9 10 11 12 13 package com.github; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer // EurekaServer服务端启动类，接受其他微服务注册进来 public class EurekaServer7001 { public static void main(String[] args){ SpringApplication.run(EurekaServer7001.class,args); } } 启动，访问测试： System Status：系统信息； DS Replicas：服务器副本； Instances currently registered with Eureka：已注册的微服务列表； General Info：一般信息； Instance Info：实例信息。 Service Provider\n将 8001 的服务入驻到 7001 的eureka中！ 修改8001服务的pom文件，增加eureka的支持！ 1 2 3 4 5 6 7 \u0026lt;!--将服务的provider注册到eureka中--\u0026gt; \u0026lt;!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-eureka\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; yaml 中配置 eureka 的支持 1 2 3 4 5 # Eureka配置：配置服务注册中心地址 eureka: client: service-url: defaultZone: http://localhost:7001/eureka/ 8001 的主启动类注解支持 1 2 3 4 5 6 7 @SpringBootApplication @EnableEurekaClient // 本服务启动之后会自动注册进Eureka中！ public class DeptProvider8001 { public static void main(String[] args) { SpringApplication.run(DeptProvider8001.class,args); } } 截止目前：服务端也有了，客户端也有了，启动7001，再启动8001，测试访问： actuator与注册微服务信息完善\n主机名称：服务名称修改 在8001的yaml中修改一下配置。 1 2 3 4 5 6 7 # Eureka配置：配置服务注册中心地址 eureka: client: service-url: defaultZone: http://localhost:7001/eureka/ instance: instance-id: springcloud-provider-dept8001 # 与client平级 重启，刷新后查看结果！ 访问信息有IP信息提示 yaml中在增加一个配置: 1 2 3 4 5 6 7 8 # Eureka配置：配置服务注册中心地址 eureka: client: service-url: defaultZone: http://localhost:7001/eureka/ instance: instance-id: springcloud-provider-dept8001 # 与client平级 prefer-ip-address: true # true表示访问路径可以显示IP地址 现在点击info，出现ERROR页面: 修改8001的pom文件，新增依赖！ 1 2 3 4 5 \u0026lt;!--actuator监控信息完善--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-actuator\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 然后回到8001的yaml配置文件中修改增加信息： 1 2 3 4 # info配置 info: app.name: subei-springcloud # 项目的名称 company.name: www.subeily.top # 公司的名称 重启项目测试：7001、8001 这里没出来的可以将spring boot版本降级即可！ 如果还没出来，可以添加如下配置： 1 2 3 4 5 management: endpoints: web: exposure: include: \u0026#34;*\u0026#34; Eureka的自我保护机制：好死不如赖活着\n之前出现的这些红色情况，没出现的，修改一个服务名，故意制造错误！ 一句话总结就是：某时刻某一个微服务不可用，eureka不会立即清理，依旧会对该微服务的信息进行保存！\n默认情况下，当eureka server在一定时间内没有收到实例的心跳，便会把该实例从注册表中删除（==默认是90秒==），但是，如果短时间内丢失大量的实例心跳，便会触发eureka server的自我保护机制，比如在开发测试时，需要频繁地重启微服务实例，但是我们很少会把eureka server一起重启（因为在开发过程中不会修改eureka注册中心），==当一分钟内收到的心跳数大量减少时，会触发该保护机制。可以在eureka管理界面看到Renews threshold和Renews(last min)，当后者（最后一分钟收到的心跳数）小于前者（心跳阈值）的时候，触发保护机制==，会出现红色的警告：EMERGENCY!EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT.RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEGING EXPIRED JUST TO BE SAFE.\n从警告中可以看到，eureka认为虽然收不到实例的心跳，但它认为实例还是健康的，eureka会保护这些实例，不会把它们从注册表中删掉。\n该保护机制的目的是避免网络连接故障，在发生网络故障时，微服务和注册中心之间无法正常通信，但服务本身是健康的，不应该注销该服务，如果eureka因网络故障而把微服务误删了，那即使网络恢复了，该微服务也不会重新注册到eureka server了，因为只有在微服务启动的时候才会发起注册请求，后面只会发送心跳和服务列表请求，这样的话，该实例虽然是运行着，但永远不会被其它服务所感知。所以，eureka server在短时间内丢失过多的客户端心跳时，会进入自我保护模式，该模式下，eureka会保护注册表中的信息，不在注销任何微服务，当网络故障恢复后，eureka会自动退出保护模式。自我保护模式可以让集群更加健壮。\n但是我们在开发测试阶段，需要频繁地重启发布，如果触发了保护机制，则旧的服务实例没有被删除，这时请求有可能跑到旧的实例中，而该实例已经关闭了，这就导致请求错误，影响开发测试。\n所以，在开发测试阶段，可以把自我保护模式关闭，只需在eureka server配置文件中加上如下配置即可：eureka.server.enable-self-preservation=false【不推荐关闭自我保护机制】\n详细可以参考：博客\n8001服务发现Discovery\n对于注册进eureka里面的微服务，可以通过服务发现来获得该服务的信息。【对外暴露服务】 修改springcloud-provider-dept-8001工程中的DeptController，并新增一个方法。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /** * DiscoveryClient 可以用来获取一些配置的信息，得到具体的微服务！ */ @Autowired private DiscoveryClient client; /** * 获取一些注册进来的微服务的信息~ * @return */ @GetMapping(\u0026#34;/discovery\u0026#34;) public Object discovery() { // 获取微服务列表的清单 List\u0026lt;String\u0026gt; services = client.getServices(); System.out.println(\u0026#34;discovery=\u0026gt;services:\u0026#34; + services); // 得到一个具体的微服务信息,通过具体的微服务id，applicaioinName； List\u0026lt;ServiceInstance\u0026gt; instances = client.getInstances(\u0026#34;SPRINGCLOUD-PROVIDER-DEPT\u0026#34;); for (ServiceInstance instance : instances) { System.out.println( instance.getHost() + \u0026#34;\\t\u0026#34; + // 主机名称 instance.getPort() + \u0026#34;\\t\u0026#34; + // 端口号 instance.getUri() + \u0026#34;\\t\u0026#34; + // uri instance.getServiceId() // 服务id ); } return this.client; } 主启动类增加一个注解： 启动Eureka服务，启动8001提供者，访问测试 http://localhost:8001/dept/discovery 后台输出： consumer访问服务：\n进入springcloud-consumer-dept-80，修改DeptConsumerController增加一个方法 1 2 3 4 @GetMapping(\u0026#34;/consumer/dept/discovery\u0026#34;) public Object discovery(){ return restTemplate.getForObject(REST_URL_PREFIX+\u0026#34;/dept/discovery\u0026#34;,Object.class); } 启动 80 项目进行测试！先启动8001服务，再启动80 Eureka：集群环境配置\n集群配置分析： 新建工程springcloud-eureka-7002、springcloud-eureka-7003； 为pom.xml添加依赖 (与springcloud-eureka-7001相同) 1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;dependencies\u0026gt; \u0026lt;!-- eureka-server服务端 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-eureka-server\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--热部署--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-devtools\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; application.yml配置(与springcloud-eureka-7001相同) 1 2 3 4 5 6 7 8 9 10 11 12 13 server: port: 7003 # Eureka配置 eureka: instance: hostname: localhost #eureka服务端的实例名称 client: register-with-eureka: false #是否将自己注册到Eureka服务器中，本身是服务器，无需注册 fetch-registry: false #false表示自己端就是注册中心，职责是维护服务实例，并不需要检索服务 service-url: defaultzone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个defaultzone地址 主启动类(与springcloud-eureka-7001相同) 1 2 3 4 5 6 7 @SpringBootApplication @EnableEurekaServer // EurekaServer服务端启动类，接受其他微服务注册进来 public class EurekaServer7003 { public static void main(String[] args){ SpringApplication.run(EurekaServer7003.class,args); } } 集群成员相互关联，修改映射配置 , windows域名映射。 配置一些自定义本机名字，找到本机hosts文件并打开： 在hosts文件最后加上，要访问的本机名称，默认是localhost 修改application.yml的配置，如图为springcloud-eureka-7001配置，springcloud-eureka-7002/springcloud-eureka-7003同样分别修改为其对应的名称即可。 在集群中使springcloud-eureka-7001关联springcloud-eureka-7002、springcloud-eureka-7003\n完整的springcloud-eureka-7001下的application.yml如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 server: port: 7001 # Eureka配置 eureka: instance: hostname: eureka7001.com #eureka服务端的实例名称 client: register-with-eureka: false #是否将自己注册到Eureka服务器中，本身是服务器，无需注册 fetch-registry: false #false表示自己端就是注册中心，职责是维护服务实例，并不需要检索服务 service-url: # 单机 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个defaultZone地址 defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ 7002 1 2 3 4 5 6 7 8 9 10 11 12 13 14 server: port: 7002 # Eureka配置 eureka: instance: hostname: eureka7002.com #eureka服务端的实例名称 client: register-with-eureka: false #是否将自己注册到Eureka服务器中，本身是服务器，无需注册 fetch-registry: false #false表示自己端就是注册中心，职责是维护服务实例，并不需要检索服务 service-url: # 单机 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个defaultZone地址 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7003.com:7003/eureka/ 7003 1 2 3 4 5 6 7 8 9 10 11 12 13 14 server: port: 7003 # Eureka配置 eureka: instance: hostname: eureka7003.com #eureka服务端的实例名称 client: register-with-eureka: false #是否将自己注册到Eureka服务器中，本身是服务器，无需注册 fetch-registry: false #false表示自己端就是注册中心，职责是维护服务实例，并不需要检索服务 service-url: # 单机 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个defaultZone地址 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/ 将8001微服务发布到1台eureka集群配置中，发现在集群中的其余注册中心也可以看到，但是平时我们保险起见，都发布！即通过springcloud-provider-dept-8001下的yml配置文件，修改Eureka配置：配置服务注册中心地址： 1 2 3 4 5 6 7 8 9 # Eureka配置：配置服务注册中心地址 eureka: client: service-url: # 注册中心地址7001-7003 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ instance: instance-id: springcloud-provider-dept-8001 # 与client平级 # prefer-ip-address: true # true表示访问路径可以显示IP地址 启动集群测试！7001、7002、7003、8001都要启动哦！ 对比Zookeeper\n回顾CAP原则 RDBMS (MySQL、Oracle、sqlServer) ===\u0026gt; ACID\nNoSQL (Redis、MongoDB) ===\u0026gt; CAP\nACID是什么？ A (Atomicity) 原子性 C (Consistency) 一致性 I (Isolation) 隔离性 D (Durability) 持久性 CAP是什么? C (Consistency) 强一致性 A (Availability) 可用性 P (Partition tolerance) 分区容错性 CAP的三进二：CA、AP、CP CAP理论的核心 一个分布式系统不可能同时很好的满足一致性，可用性和分区容错性这三个需求； 根据CAP原理，将NoSQL数据库分成了满足CA原则，满足CP原则和满足AP原则三大类； CA：单点集群，满足一致性，可用性的系统，通常可扩展性较差； CP：满足一致性，分区容错的系统，通常性能不是特别高； AP：满足可用性，分区容错的系统，通常可能对一致性要求低一些； 作为服务注册中心，Eureka比Zookeeper好在哪里？\n著名的CAP理论指出，一个分布式系统不可能同时满足C (一致性) 、A (可用性) 、P (容错性)，由于分区容错性P再分布式系统中是必须要保证的，因此我们只能再A和C之间进行权衡。\nZookeeper 保证的是 CP —\u0026gt; 满足一致性，分区容错的系统，通常性能不是特别高； Eureka 保证的是 AP —\u0026gt; 满足可用性，分区容错的系统，通常可能对一致性要求低一些； Zookeeper保证的是CP\n当向注册中心查询服务列表时，我们可以容忍注册中心返回的是几分钟以前的注册信息，但不能接收服务直接down掉不可用。也就是说，服务注册功能对可用性的要求要高于一致性。但zookeeper会出现这样一种情况，当master节点因为网络故障与其他节点失去联系时，剩余节点会重新进行leader选举。问题在于，选举leader的时间太长，30-120s，且选举期间整个zookeeper集群是不可用的，这就导致在选举期间注册服务瘫痪。在云部署的环境下，因为网络问题使得zookeeper集群失去master节点是较大概率发生的事件，虽然服务最终能够恢复，但是，漫长的选举时间导致注册长期不可用，是不可容忍的。 Eureka保证的是AP\nEureka看明白了这一点，因此在设计时就优先保证可用性。Eureka各个节点都是平等的，几个节点挂掉不会影响正常节点的工作，剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册时，如果发现连接失败，则会自动切换至其他节点，只要有一台Eureka还在，就能保住注册服务的可用性，只不过查到的信息可能不是最新的，除此之外，Eureka还有之中自我保护机制，如果在15分钟内超过85%的节点都没有正常的心跳，那么Eureka就认为客户端与注册中心出现了网络故障，此时会出现以下几种情况： Eureka不在从注册列表中移除因为长时间没收到心跳而应该过期的服务； Eureka仍然能够接受新服务的注册和查询请求，但是不会被同步到其他节点上 (即保证当前节点依然可用)； 当网络稳定时，当前实例新的注册信息会被同步到其他节点中。 因此，==Eureka可以很好的应对因网络故障导致部分节点失去联系的情况，而不会像zookeeper那样使整个注册服务瘫痪==。\n6.Ribbon：负载均衡(基于客户端) Ribbon是什么？\nSpring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具。\n简单的说，Ribbon是Netflix发布的开源项目，主要功能是提供客户端的软件负载均衡算法，将NetFlix的中间层服务连接在一起。Ribbon的客户端组件提供一系列完整的配置项如：连接超时、重试等等。简单的说，就是在配置文件中列出LoadBalancer（简称LB：负载均衡）后面所有的机器，Ribbon会自动的帮助你基于某种规则（如简单轮询，随机连接等等）去连接这些机器。我们也很容易使用Ribbon实现自定义的负载均衡算法！\nRibbon能干嘛？\nLB，即负载均衡 (LoadBalancer) ，在微服务或分布式集群中经常用的一种应用。 负载均衡简单的说就是将用户的请求平摊的分配到多个服务上，从而达到系统的HA (高用)。 常见的负载均衡软件有 Nginx、Lvs等等。 Dubbo、SpringCloud 中均给我们提供了负载均衡，SpringCloud 的负载均衡算法可以自定义。 负载均衡简单分类： 集中式LB 即在服务的提供方和消费方之间使用独立的LB设施，如Nginx(反向代理服务器)，由该设施负责把访问请求通过某种策略转发至服务的提供方！ 进程式 LB 将LB逻辑集成到消费方，消费方从服务注册中心获知有哪些地址可用，然后自己再从这些地址中选出一个合适的服务器。 Ribbon 就属于进程内LB，它只是一个类库，集成于消费方进程，消费方通过它来获取到服务提供方的地址！ Ribbon的github地址：https://github.com/NetFlix/ribbon 集成Ribbon\nspringcloud-consumer-dept-80向pom.xml中添加Ribbon和Eureka依赖\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-ribbon --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-ribbon\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.7.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-eureka --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-eureka\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.7.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 在application.yml文件中配置Eureka\n1 2 3 4 5 6 7 8 9 server: port: 80 # Eureka配置 eureka: client: register-with-eureka: false # false表示不向注册中心注册自己 service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ 主启动类加上@EnableEurekaClient注解，开启Eureka\n1 2 3 4 5 6 7 8 // Ribbon 和 Eureka 整合以后，客户端可以直接调用，不用关心IP地址和端口号 @SpringBootApplication @EnableEurekaClient // 开启Eureka 客户端 public class DeptConsumer80 { public static void main(String[] args) { SpringApplication.run(DeptConsumer80.class,args); } } 自定义Spring配置类：ConfigBean.java 配置负载均衡实现RestTemplate\n1 2 3 4 5 @Bean @LoadBalanced // Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具 public RestTemplate getRestTemplate(){ return new RestTemplate(); } 修改conroller：DeptConsumerController.java\n1 2 3 4 5 6 7 /** * 服务提供方地址前缀 * \u0026lt;p\u0026gt; * Ribbon:我们这里的地址，应该是一个变量，通过服务名来访问 */ // private static final String REST_URL_PREFIX = \u0026#34;http://localhost:8001\u0026#34;; private static final String REST_URL_PREFIX = \u0026#34;http://SPRINGCLOUD-PROVIDER-DEPT\u0026#34;; 先启动3个Eureka集群后，再启动springcloud-provider-dept-8001并注册进eureka； 启动 DeptConsumerRibbon80； 测试 http://localhost/consumer/dept/get/1 http://localhost/consumer/dept/list ==Ribbon和Eureka整合后Consumer可以直接调用服务而不用再关心地址和端口号==！ Ribbon负载均衡\n架构说明： Ribbon在工作时分成两步：\n第一步先选择EurekaServer，它优先选择在同一个区域内负载均衡较少的Server。 第二步在根据用户指定的策略，在从server去到的服务注册列表中选择一个地址。 其中Ribbon提供了多种策略，比如轮询（默认），随机和根据响应时间加权重,,,等等 测试：\n参考springcloud-provider-dept-8001，新建两份，分别为8002,8003！ 参照springcloud-provider-dept-8001，依次为另外两个Moudle添加pom.xml依赖 、resourece下的mybatis和application.yml配置，Java代码； 全部复制完毕，修改启动类名称，修改端口号名称！ 新建8002/8003数据库，各自微服务分别连接各自的数据库，复制springcloud！ 新建springcloud02 新建springcloud03 修改8002/8003各自的YML文件 端口 数据库连接 实例名也需要修改 1 2 instance: instance-id: springcloud-provider-dept-8003 # 与client平级 对外暴露的统一的服务实例名【三个服务名字必须一致！】 1 2 application: name: springcloud-provider-dept 启动3个Eureka集群配置区\n启动3个Dept微服务并都测试通过\nhttp://localhost:8001/dept/list http://localhost:8002/dept/list http://localhost:8003/dept/list 启动springcloud-consumer-dept-80\n客户端通过Ribbon完成负载均衡并访问上一步的Dept微服务\nhttp://localhost/consumer/dept/list 多刷新几次注意观察结果！ 总结：\nRibbon其实就是一个软负载均衡的客户端组件，他可以和其他所需请求的客户端结合使用，和Eureka结合只是其中的一个实例。 Ribbon核心组件IRule\nIRule：根据特定算法从服务列表中选取一个要访问的服务！ RoundRobinRule【轮询】 RandomRule【随机】 AvailabilityFilterRule【会先过滤掉由于多次访问故障而处于断路器跳闸的服务，还有并发的连接 数量超过阈值的服务，然后对剩余的服务列表按照轮询策略进行访问】 WeightedResponseTimeRule【根据平均响应时间计算所有服务的权重，响应时间越快服务权重越大，被选中的概率越高，刚启动时如果统计信息不足，则使用RoundRobinRule策略，等待统计信息足够，会切换到WeightedResponseTimeRule】 RetryRule【先按照RoundRobinRule的策略获取服务，如果获取服务失败，则在指定时间内会进行重试，获取可用的服务】 BestAvailableRule【会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务，然后选择一个并发量最小的服务】 ZoneAvoidanceRule【默认规则，复合判断server所在区域的性能和server的可用性选择服务器】 查看分析源码： IRule ILoadBalancer AbstractLoadBalancer AbstractLoadBalancerRule：这个抽象父类十分重要！核心 RoundRobinRule 分析一下方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { log.warn(\u0026#34;no load balancer\u0026#34;); return null; } else { Server server = null; int count = 0; while(true) { if (server == null \u0026amp;\u0026amp; count++ \u0026lt; 10) { List\u0026lt;Server\u0026gt; reachableServers = lb.getReachableServers(); List\u0026lt;Server\u0026gt; allServers = lb.getAllServers(); int upCount = reachableServers.size(); int serverCount = allServers.size(); if (upCount != 0 \u0026amp;\u0026amp; serverCount != 0) { int nextServerIndex = this.incrementAndGetModulo(serverCount); server = (Server)allServers.get(nextServerIndex); if (server == null) { Thread.yield(); } else { if (server.isAlive() \u0026amp;\u0026amp; server.isReadyToServe()) { return server; } server = null; } continue; } log.warn(\u0026#34;No up servers available from load balancer: \u0026#34; + lb); return null; } if (count \u0026gt;= 10) { log.warn(\u0026#34;No available alive servers after 10 tries from load balancer: \u0026#34; + lb); } return server; } } } 切换为随机策略实现试试，在ConfigBean中添加方法。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** * IRule: * RoundRobinRule 轮询策略 * RandomRule 随机策略 * AvailabilityFilteringRule ： 会先过滤掉，跳闸，访问故障的服务~，对剩下的进行轮询~ * RetryRule ： 会先按照轮询获取服务~，如果服务获取失败，则会在指定的时间内进行，重试 */ @Bean public IRule myRule() { return new RandomRule();//使用随机策略 // return new RoundRobinRule();//使用轮询策略 // return new AvailabilityFilteringRule();//使用轮询策略 // return new RetryRule();//使用轮询策略 } 重启80服务进行访问测试，查看运行结果！【注意，可能服务长时间不使用会崩】 访问测试：http://localhost/consumer/dept/list 测试：new RetryRule() 算法 RetryRule【先按照RoundRobinRule的策略获取服务，如果获取服务失败，则在指定时间内会进行重试，获取可用的服务】 在运行期间关闭掉一个服务提供者8002 消费者再次测试！发现404后继续访问测试！看结果！！！ 其余的不再挨个测试了，大家有时间可以去测试玩玩； 现在有一个新的需求，我们不需要这些默认的算法，我们需要自己重新定义，这该怎么办呢？ 自定义Ribbon：\n修改springcloud-consumer-dept-ribbon-80 主启动类添加@RibbonClient注解； 在启动该微服务的时候就能去加载我们自定义的Ribbon配置类，从而使配置类生效，例如: 1 @RibbonClient(name=\u0026#34;SPRINGCLOUD-PROVIDER-DEPT\u0026#34;,configuration=MySelfRule.class) 注意配置细节:\n官方文档明确给出了警告： 这个自定义配置类不能放在@ComponentScan所扫描的当前包以及子包下，否则我们自定义的这个配置类就会被所有的Ribbon客户端所共享，也就是说达不到特殊化定制的目的了！ 步骤：\n由于有以上配置细节原因，我们建立一个包：com.github.myrul，在这里新建一个自定义规则的Rubbion类。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration public class MySelfRule { /** * IRule: * RoundRobinRule 轮询策略 * RandomRule 随机策略 * AvailabilityFilteringRule ： 会先过滤掉，跳闸，访问故障的服务~，对剩下的进行轮询~ * RetryRule ： 会先按照轮询获取服务~，如果服务获取失败，则会在指定的时间内进行，重试 */ @Bean public IRule myRule() { return new RandomRule();//使用随机策略 // return new RoundRobinRule();//使用轮询策略 // return new AvailabilityFilteringRule();//使用轮询策略 // return new RetryRule();//使用轮询策略 } } 在主启动类上配置自定义的Ribbon。 1 2 3 4 5 6 7 8 @SpringBootApplication @EnableEurekaClient // 开启Eureka 客户端 @RibbonClient(name=\u0026#34;SPRINGCLOUD-PROVIDER-DEPT\u0026#34;,configuration= MySelfRule.class) public class DeptConsumer80 { public static void main(String[] args) { SpringApplication.run(DeptConsumer80.class,args); } } 启动所有项目，访问测试，查看编写的随机算法，现在是否生效！ 自定义规则深度解析\n问题：依旧轮询策略，但是加上新需求，每个服务器要求被调用5次，就是以前每一个机器一次，现在每个机器5次； 解析源码：RandomRule.java ， IDEA直接点击进去，复制出来，变成我们自己的类 MyRondomRule 分析阅读源码： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class RandomRule extends AbstractLoadBalancerRule { public RandomRule() { } @SuppressWarnings({\u0026#34;RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE\u0026#34;}) // ILoadBalancer选择的随机算法 public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { return null; } else { Server server = null; while(server == null) { // 查看线程是否中断了 if (Thread.interrupted()) { return null; } // Reachable： 可及；可到达；够得到 List\u0026lt;Server\u0026gt; upList = lb.getReachableServers(); // 活着的服务 List\u0026lt;Server\u0026gt; allList = lb.getAllServers(); // 获取所有的服务 int serverCount = allList.size(); if (serverCount == 0) { return null; } // 生成区间随机数！ int index = this.chooseRandomInt(serverCount); // 从活着的服务中，随机取出一个 server = (Server)upList.get(index); if (server == null) { Thread.yield(); } else { if (server.isAlive()) { return server; } server = null; Thread.yield(); } } return server; } } // 随机 protected int chooseRandomInt(int serverCount) { return ThreadLocalRandom.current().nextInt(serverCount); } public Server choose(Object key) { return this.choose(this.getLoadBalancer(), key); } public void initWithNiwsConfig(IClientConfig clientConfig) { } } 参考源码修改为我们需求要求的MyRondomRule.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 package com.github.myrule; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.AbstractLoadBalancerRule; import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.Server; import java.util.List; import java.util.concurrent.ThreadLocalRandom; public class MyRondomRule extends AbstractLoadBalancerRule { public MyRondomRule() { } // total = 0 当total数等于5以后，我们指针才能往下走 // index = 0 当前对外提供服务的服务器地址 // 如果total等于5，则index+1，将total重置为0即可！ // 问题：我们只有3台机器，所有total\u0026gt;3 则将total置为0； private int total = 0; //总共被调用的次数 private int currentIndex = 0; //当前提供服务的机器序号！ // ILoadBalancer选择的随机算法 public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { return null; } else { Server server = null; while(server == null) { // 查看线程是否中断了 if (Thread.interrupted()) { return null; } // Reachable： 可及；可到达；够得到 List\u0026lt;Server\u0026gt; upList = lb.getReachableServers(); // 活着的服务 List\u0026lt;Server\u0026gt; allList = lb.getAllServers(); // 获取所有的服务 int serverCount = allList.size(); if (serverCount == 0) { return null; } // 生成区间随机数！ // int index = this.chooseRandomInt(serverCount); // 从活着的服务中，随机取出一个 // server = (Server)upList.get(index); // ===================================== if (total\u0026lt;5){ server = upList.get(currentIndex); total++; }else { total = 0; currentIndex++; if (currentIndex\u0026gt;=upList.size()){ currentIndex = 0; } server = upList.get(currentIndex); } // ===================================== if (server == null) { Thread.yield(); } else { if (server.isAlive()) { return server; } server = null; Thread.yield(); } } return server; } } // 随机 protected int chooseRandomInt(int serverCount) { return ThreadLocalRandom.current().nextInt(serverCount); } @Override public Server choose(Object key) { return this.choose(this.getLoadBalancer(), key); } @Override public void initWithNiwsConfig(IClientConfig clientConfig) { } } 调用，在我们自定义的IRule方法中返回刚才我们写好的随机算法类。 1 2 3 4 5 6 7 8 9 @Configuration public class MySelfRule { @Bean public IRule myRule(){ // Ribbon默认是轮询，可以自定义为随机算法 return new MyRondomRule(); } } 测试实现，会连续五次之后才会切换轮询： 7.Feign：负载均衡(基于服务端) 简介\nFeign是声明式Web Service客户端，它让微服务之间的调用变得更简单，类似controller调用service。SpringCloud集成了Ribbon和Eureka，可以使用Feigin提供负载均衡的http客户端。\n只需要创建一个接口，然后添加注解即可！\nFeign，主要是社区版，大家都习惯面向接口编程。这个是很多开发人员的规范。调用微服务访问两种方法：\n微服务名字 【ribbon】 接口和注解 【feign】 Feign能干什么？\nFeign旨在使编写Java Http客户端变得更容易 前面在使用Ribbon + RestTemplate时，利用RestTemplate对Http请求的封装处理，形成了一套模板化的调用方法。但是在实际开发中，由于对服务依赖的调用可能不止一处，往往一个接口会被多处调用，所以通常都会针对每个微服务自行封装一个客户端类来包装这些依赖服务的调用。所以，Feign在此基础上做了进一步的封装，由他来帮助我们定义和实现依赖服务接口的定义，在Feign的实现下，我们只需要创建一个接口并使用注解的方式来配置它 (类似以前Dao接口上标注Mapper注解，现在是一个微服务接口上面标注一个Feign注解)，即可完成对服务提供方的接口绑定，简化了使用Spring Cloud Ribbon 时，自动封装服务调用客户端的开发量。 Feign默认集成了Ribbon\n利用Ribbon维护了MicroServiceCloud-Dept的服务列表信息，并且通过轮询实现了客户端的负载均衡，而与Ribbon不同的是，通过Feign只需要定义服务绑定接口且以声明式的方法，优雅而简单的实现了服务调用。 Feign的使用步骤：\n参考springcloud-consumer-dept-ribbon-80 新建springcloud-consumer-dept-feign-80 修改主启动类名称； 将springcloud-consumer-dept-80的内容都拷贝到feign项目中； 删除myrule文件夹； 修改主启动类的名称为 FeignDeptConsumer80； springcloud-consumer-dept-feign-80修改pom.xml，添加对Feign的支持。 1 2 3 4 5 6 \u0026lt;!--Feign的依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-feign\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 修改springcloud-api工程 pom.xml添加feign的支持 新建一个Service包 编写接口 DeptClientService，并增加新的注解@FeignClient。 1 2 3 4 5 6 7 8 9 10 11 12 @FeignClient(value = \u0026#34;SPRINGCLOUD-PROVIDER-DEPT\u0026#34;) public interface DeptClientService { @GetMapping(\u0026#34;/dept/get/{id}\u0026#34;) public Dept queryById(@PathVariable(\u0026#34;id\u0026#34;) Long id); //根据id查询部门 @GetMapping(\u0026#34;/dept/list\u0026#34;) public List\u0026lt;Dept\u0026gt; queryAll(); //查询所有部门 @PostMapping(value = \u0026#34;/dept/add\u0026#34;) public boolean addDept(Dept dept); //添加一个部门 } 记得清理一下mvn。 springcloud-consumer-dept-feign-80工程修改Controller，添加上一步新建的DeptClientService。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @RestController public class DeptConsumerController { /** * 理解：消费者，不应该有service层~ * RestTemplate .... 供我们直接调用就可以了！ 注册到Spring中 * (地址：url, 实体：Map ,Class\u0026lt;T\u0026gt; responseType) * \u0026lt;p\u0026gt; * 提供多种便捷访问远程http服务的方法，简单的Restful服务模板~ */ @Autowired private DeptClientService service; /** * 消费方添加部门信息 * @param dept * @return */ @RequestMapping(\u0026#34;/consumer/dept/add\u0026#34;) public boolean add(Dept dept){ return this.service.addDept(dept); } /** * 消费方根据id查询部门信息 * @param id * @return */ @RequestMapping(\u0026#34;/consumer/dept/get/{id}\u0026#34;) public Dept get(@PathVariable(\u0026#34;id\u0026#34;) Long id){ return this.service.queryById(id); } /** * 消费方查询部门信息列表 * @return */ @RequestMapping(\u0026#34;/consumer/dept/list\u0026#34;) public List\u0026lt;Dept\u0026gt; list(){ return this.service.queryAll(); } } microservicecloud-consumer-dept-feign工程修改主启动类，开启Feign使用！ 1 2 3 4 5 6 7 8 9 10 11 @SpringBootApplication @EnableEurekaClient // feign客户端注解,并指定要扫描的包以及配置接口DeptClientService @EnableFeignClients(basePackages = {\u0026#34;com.github.service\u0026#34;}) // 切记不要加这个注解，不然会出现404访问不到 // @ComponentScan(\u0026#34;com.github.service\u0026#34;) public class FeignDeptConsumer80 { public static void main(String[] args) { SpringApplication.run(FeignDeptConsumer80.class, args); } } 测试！ 启动eureka集群； 启动8001，8002，8003 启动feign客户端 测试： http://localhost/consumer/dept/list ==结论：Feign自带负载均衡配置项==。\n小结\nFeign通过接口的方法调用Rest服务 ( 之前是Ribbon+RestTemplate )； 该请求发送给Eureka服务器 （http://MICROSERVICECLOUD-PROVIDER-DEPT/dept/list）； 通过Feign直接找到服务接口，由于在进行服务调用的时候融合了Ribbon技术，所以也支持负载均衡作用！ feign其实不是做负载均衡的,负载均衡是ribbon的功能,feign只是集成了ribbon而已,但是负载均衡的功能还是feign内置的ribbon再做,而不是feign。feign的作用的替代RestTemplate,性能比较低，但是可以使代码可读性很强。 8.Hystrix：服务熔断 分布式系统面临的问题：\n复杂分布式体系结构中的应用程序有数十个依赖关系，每个依赖关系在某些时候将不可避免的失败！ 服务雪崩\n多个微服务之间调用的时候，假设微服务A调用微服务B和微服务C，微服务B和微服务C又调用其他的微服务，这就是所谓的 “扇出”、如果扇出的链路上某个微服务的调用响应时间过长或者不可用，对微服务A的调用就会占用越来越多的系统资源，进而引起系统崩溃，所谓的“雪崩效应”。 对于高流量的应用来说，单一的后端依赖可能会导致所有服务器上的所有资源都在几十秒内饱和。比失败更糟糕的是，这些应用程序还可能导致服务之间的延迟增加，备份队列，线程和其他系统资源紧张，导致整个系统发生更多的级联故障，这些都表示需要对故障和延迟进行隔离和管理，以达到单个依赖关系的失败而不影响整个应用程序或系统运行。 此时，我们需要弃车保帅！ 什么是Hystrix？\nHystrix是一个应用于处理分布式系统的延迟和容错的开源库，在分布式环境中，许多服务依赖项中的一些不可避免地会失败。Hystrix 是一个库，它通过添加延迟容错和容错逻辑来帮助您控制这些分布式服务之间的交互。Hystrix 通过隔离服务之间的访问点、停止它们之间的级联故障并提供回退选项来做到这一点，所有这些都可以提高系统的整体弹性。\n“断路器”本身是一种开关装置，当某个服务单元发生故障之后，通过断路器的故障监控 (类似熔断保险丝) ，向调用方返回一个服务预期的，可处理的备选响应 (FallBack) ，而不是长时间的等待或者抛出调用方法无法处理的异常，这样就可以保证了服务调用方的线程不会被长时间，不必要的占用，从而避免了故障在分布式系统中的蔓延，乃至雪崩。\nHystrix的历史\nHystrix 源于 Netflix API 团队于 2011 年开始的弹性工程工作。2012 年，Hystrix 不断发展和成熟，Netflix 内部的许多团队都采用了它。如今，Netflix 每天通过 Hystrix 执行数百亿个线程隔离和数千亿个信号量隔离调用。这极大地提高了正常运行时间和恢复能力。 Hystrix 有什么用？\nHystrix 旨在执行以下操作：\n通过第三方客户端库访问（通常通过网络）依赖关系，保护和控制延迟和故障。 停止复杂分布式系统中的级联故障。 快速失败并迅速恢复。 尽可能回退并优雅降级。 实现近乎实时的监控、警报和操作控制。 Hystrix 解决了什么问题？\n复杂分布式架构中的应用程序有几十个依赖项，每个依赖项都不可避免地会在某个时候失败。如果主机应用程序没有与这些外部故障隔离开来，它就有被它们一起关闭的风险。\n例如，对于依赖于 30 个服务且每个服务的正常运行时间为 99.99% 的应用程序，您可以期待以下内容：\n99.99 30 = 99.7% 的正常运行时间 10 亿次请求的 0.3% = 3,000,000 次故障 2 小时以上的停机时间/月，即使所有依赖项都具有出色的正常运行时间。\n现实通常更糟。\n即使所有依赖项都表现良好，如果您不对整个系统进行弹性设计，即使对数十项服务中的每项服务造成 0.01% 的停机时间的总影响也相当于可能每月停机数小时。\n当一切正常时，请求流程可能如下所示： 当许多后端系统之一变得潜在时，它可以阻止整个用户请求： 在大流量的情况下，单个后端依赖变得潜在可能会导致所有服务器上的所有资源在几秒钟内变得饱和。\n应用程序中通过网络或客户端库中可能导致网络请求的每个点都是潜在故障的根源。比故障更糟糕的是，这些应用程序还可能导致服务之间的延迟增加，这会备份队列、线程和其他系统资源，从而导致整个系统出现更多的级联故障。\n当通过第三方客户端执行网络访问时，这些问题会更加严重——一个“黑匣子”，其中实现细节被隐藏并且可以随时更改，并且每个客户端库的网络或资源配置都不同，并且通常难以监控和改变。\n更糟糕的是传递依赖，它执行潜在的昂贵或容易出错的网络调用，而没有被应用程序显式调用。\n网络连接失败或降级。服务和服务器出现故障或变慢。新的库或服务部署会改变行为或性能特征。客户端库有错误。\n所有这些都代表了需要隔离和管理的故障和延迟，以便单个故障依赖项无法关闭整个应用程序或系统。\nHystrix 的设计原则是什么？\nHystrix 通过以下方式工作：\n防止任何单个依赖项用完所有容器（例如 Tomcat）用户线程。 减轻负载并快速失败，而不是排队。 在可行的情况下提供回退，以保护用户免于失败。 使用隔离技术（例如隔板、泳道和断路器模式）来限制任何一种依赖项的影响。 通过近乎实时的指标、监控和警报优化发现时间 通过配置更改的低延迟传播和对 Hystrix 大部分方面的动态属性更改的支持来优化恢复时间，这允许您使用低延迟反馈循环进行实时操作修改。 防止整个依赖客户端执行中的故障，而不仅仅是网络流量中的故障。 Hystrix 如何实现其目标？\nHystrix 通过以下方式做到这一点： 将所有对外部系统（或“依赖项”）的调用包装在一个HystrixCommand或HystrixObservableCommand对象中，该对象通常在单独的线程中执行（这是命令模式的一个示例）。 超时调用时间超过您定义的阈值。有一个默认值，但是对于大多数依赖项，您可以通过“属性”自定义设置这些超时，以便它们略高于每个依赖项测量的 99.5 %性能。 为每个依赖项维护一个小型线程池（或信号量）；如果它已满，发往该依赖项的请求将立即被拒绝而不是排队。 测量成功、失败（客户端抛出的异常）、超时和线程拒绝。 如果服务的错误百分比超过阈值，则手动或自动触发断路器以在一段时间内停止对特定服务的所有请求。 当请求失败、被拒绝、超时或短路时执行回退逻辑。 近乎实时地监控指标和配置更改。 当您使用 Hystrix 包装每个底层依赖项时，如上图所示的架构将更改为类似于下图。每个依赖项相互隔离，限制在发生延迟时它可以饱和的资源，并包含在回退逻辑中，该逻辑决定当依赖项中发生任何类型的故障时做出什么响应： 详细了解它的工作原理和使用方法。\n==以上概念参考于官方资料：https://github.com/Netflix/Hystrix/wiki==。\n服务熔断\n什么是服务熔断?\n熔断机制是赌赢雪崩效应的一种微服务链路保护机制。\n当扇出链路的某个微服务不可用或者响应时间太长时，会进行服务的降级，进而熔断该节点微服务的调用，快速返回错误的响应信息。检测到该节点微服务调用响应正常后恢复调用链路。在SpringCloud框架里熔断机制通过Hystrix实现。Hystrix会监控微服务间调用的状况，当失败的调用到一定阀值缺省是5秒内20次调用失败，就会启动熔断机制。\n熔断机制的注解是：@HystrixCommand。\n服务熔断解决如下问题：\n当所依赖的对象不稳定时，能够起到快速失败的目的； 快速失败后，能够根据一定的算法动态试探所依赖对象是否恢复。 入门案例：\n参考springcloud-provider-dept-8001，新建springcloud-provider-dept-hystrix-8001，将之前8001的所有东西拷贝一份。 修改pom，添加Hystrix的依赖。 1 2 3 4 5 6 \u0026lt;!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-hystrix --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-hystrix\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.7.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 修改yml，修改eureka实例的id。 修改DeptController @HystrixCommand报异常后如何处理 1 2 3 // 一旦调用服务方法失败并抛出了错误信息后 // 会自动调用HystrixCommand标注好的fallbackMethod调用类中指定方法 @HystrixCommand(fallbackMethod = \u0026#34;processHystrix_Get\u0026#34;) 源码内容 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @RestController @RequestMapping(\u0026#34;/dept\u0026#34;) public class DeptController { @Autowired private DeptService service; // 一旦调用服务方法失败并抛出了错误信息后 // 会自动调用HystrixCommand标注好的fallbackMethod调用类中指定方法 /** * 如果根据id查询出现异常,则走hystrixGet这段备选代码 * @param id * @return */ @GetMapping(\u0026#34;/get/{id}\u0026#34;) @HystrixCommand(fallbackMethod = \u0026#34;hystrixGet\u0026#34;) public Dept get(@PathVariable(\u0026#34;id\u0026#34;) Long id) { Dept dept = service.queryById(id); if (dept==null){ throw new RuntimeException(\u0026#34;该id:\u0026#34;+id+\u0026#34;没有对应的的信息\u0026#34;); } return dept; } /** * 根据id查询备选方案(熔断) * @param id * @return */ public Dept hystrixGet(@PathVariable(\u0026#34;id\u0026#34;) Long id){ return new Dept().setDeptno(id) .setDname(\u0026#34;这个id=\u0026gt;\u0026#34;+id+\u0026#34;,没有对应的信息,null---@Hystrix~\u0026#34;) .setDb_source(\u0026#34;在MySQL中没有这个数据库\u0026#34;); } } 修改主启动类添加新注解 @EnableCircuitBreaker，修改主启动类的名称为 DeptProviderHystrix8001 1 2 3 4 5 6 7 8 9 10 @SpringBootApplication @EnableEurekaClient // 本服务启动之后会自动注册进Eureka中！ @EnableDiscoveryClient // 开启服务发现客户端的注解，可以用来获取一些配置的信息，得到具体的微服务 @EnableCircuitBreaker //对hystrix 熔断机制的支持 【==========new=======】 public class DeptProviderHystrix8001 { public static void main(String[] args) { SpringApplication.run(DeptProviderHystrix8001.class,args); } } 测试 启动Eureka集群; 启动主启动类 DeptProviderHystrix8001; 启动客户端 springcloud-consumer-dept-80; 访问 http://localhost/consumer/dept/get/111 使用熔断后，当访问一个不存在的id时，前台页展示数据如下: 而不使用熔断的springcloud-provider-dept–8001模块访问相同地址会出现下面状况: 服务降级\n什么是服务降级?\n服务降级是指当服务器压力剧增的情况下，根据实际业务情况及流量，对一些服务和页面有策略的不处理，或换种简单的方式处理，从而释放服务器资源以保证核心业务正常运作或高效运作。说白了，就是尽可能的把系统资源让给优先级高的服务。\n资源有限，而请求是无限的。如果在并发高峰期，不做服务降级处理，一方面肯定会影响整体服务的性能，严重的话可能会导致宕机某些重要的服务不可用。所以，一般在高峰期，为了保证核心功能服务的可用性，都要对某些服务降级处理。比如当双11活动时，把交易无关的服务统统降级，如查看蚂蚁深林，查看历史订单等等。\n服务降级主要用于什么场景呢？\n当整个微服务架构整体的负载超出了预设的上限阈值或即将到来的流量预计将会超过预设的阈值时，为了保证重要或基本的服务能正常运行，可以将一些不重要或不紧急的服务或任务进行服务的 延迟使用或暂停使用。\n降级的方式可以根据业务来，可以延迟服务，比如延迟给用户增加积分，只是放到一个缓存中，等服务平稳之后再执行；或者在粒度范围内关闭服务，比如关闭相关文章的推荐。\n由上图可得，当某一时间内服务A的访问量暴增，而B和C的访问量较少，为了缓解A服务的压力，这时候需要B和C暂时关闭一些服务功能，去承担A的部分服务，从而为A分担压力，叫做服务降级。 服务降级需要考虑的问题\n1）那些服务是核心服务，哪些服务是非核心服务 2）那些服务可以支持降级，那些服务不能支持降级，降级策略是什么 3）除服务降级之外是否存在更复杂的业务放通场景，策略是什么？ 自动降级分类\n超时降级：主要配置好超时时间和超时重试次数和机制，并使用异步机制探测回复情况。\n失败次数降级：主要是一些不稳定的api，当失败调用次数达到一定阀值自动降级，同样要使用异步机制探测回复情况。\n故障降级：比如要调用的远程服务挂掉了（网络故障、DNS故障、http服务返回错误的状态码、rpc服务抛出异常），则可以直接降级。降级后的处理方案有：默认值（比如库存服务挂了，返回默认现货）、兜底数据（比如广告挂了，返回提前准备好的一些静态页面）、缓存（之前暂存的一些缓存数据）。\n限流降级：秒杀或者抢购一些限购商品时，此时可能会因为访问量太大而导致系统崩溃，此时会使用限流来进行限制访问量，当达到限流阀值，后续请求会被降级；降级后的处理方案可以是：排队页面（将用户导流到排队页面等一会重试）、无货（直接告知用户没货了）、错误页（如活动太火爆了，稍后重试）。\n入门案例\n修改springcloud-api工程，根据已经有的DeptClientService接口新建一个实现了FallbackFactory接口的类DeptClientServiceFallbackFactory。 【注意：这个类上需要@Component注解！！！】 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Component public class DeptClientServiceFallbackFactory implements FallbackFactory { @Override public Object create(Throwable throwable) { return new DeptClientService() { @Override public Dept queryById(Long id) { return new Dept().setDeptno(id) .setDname(\u0026#34;该ID:\u0026#34; + id + \u0026#34;没有对应的信息，Consumer客户端提供的降级信息，此刻服务Provider已经关闭！\u0026#34;) .setDb_source(\u0026#34;No this database in MySQL.\u0026#34;); } @Override public List\u0026lt;Dept\u0026gt; queryAll() { return null; } @Override public boolean addDept(Dept dept) { return false; } }; } } 修改springcloud-api工程，DeptClientService接口在注解@FeignClient中添加fallbackFactory属性值。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component // 注册到spring容器中 // @FeignClient:微服务客户端注解,value:指定微服务的名字,这样就可以使Feign客户端直接找到对应的微服务 // fallbackFactory指定降级配置类 @FeignClient(value = \u0026#34;SPRINGCLOUD-PROVIDER-DEPT\u0026#34;,fallbackFactory = DeptClientServiceFallbackFactory.class) public interface DeptClientService { @GetMapping(\u0026#34;/dept/get/{id}\u0026#34;) public Dept queryById(@PathVariable(\u0026#34;id\u0026#34;) Long id); //根据id查询部门 @GetMapping(\u0026#34;/dept/list\u0026#34;) public List\u0026lt;Dept\u0026gt; queryAll(); //查询所有部门 @PostMapping(value = \u0026#34;/dept/add\u0026#34;) public boolean addDept(Dept dept); //添加一个部门 } springcloud-api工程mvn clean install； springcloud-consumer-dept-feign-80工程修改YML； 测试\n启动eureka集群；\n启动 springcloud-provider-dept-hystrix-8001；\n启动 springcloud-consumer-dept-feign-80；\n正常访问测试：http://localhost/consumer/dept/get/1；\n故意关闭微服务 springcloud-provider-dept-hystrix-8001；\n客户端自己调用提示：http://localhost/consumer/dept/get/1\n此时服务端provider已经down了，但是我们做了服务降级处理，让客户端在服务端不可用时 也会获得提示信息而不会挂起耗死服务器。 服务熔断和降级的区别\n服务熔断—\u0026gt;服务端：一般是某个服务故障或者异常引起，类似现实世界中的“保险丝”，当某个异常条件被触发，直接熔断整个服务，而不是一直等到此服务超时！ 服务降级—\u0026gt;客户端：从整体网站请求负载考虑，当某个服务熔断或者关闭之后，服务将不再被调用，此时在客户端，我们可以准备一个 FallBackFactory ，返回一个默认的值(缺省值)。会导致整体的服务下降，但是好歹能用，比直接挂掉强。 触发原因不太一样，服务熔断一般是某个服务（下游服务）故障引起，而服务降级一般是从整体负荷考虑；管理目标的层次不太一样，熔断其实是一个框架级的处理，每个微服务都需要（无层级之分），而降级一般需要对业务有层级之分（比如降级一般是从最外围服务开始）。 实现方式不太一样，服务降级具有代码侵入性(由控制器完成/或自动降级)，熔断一般称为自我熔断。 熔断，降级，限流：\n限流：限制并发的请求访问量，超过阈值则拒绝；\n降级：服务分优先级，牺牲非核心服务（不可用），保证核心服务稳定；从整体负荷考虑；\n熔断：依赖的下游服务故障触发熔断，避免引发本系统崩溃；系统自动执行和恢复\nDashboard 流监控\n除了隔离依赖服务的调用以外，Hystrix还提供了准实时的调用监控（Hystrix Dashboard），Hystrix会持续地记录所有通过Hystrix发起的请求的执行信息，并以统计报表和图形的形式展示给用户，包括每秒执行多少请求，多少成功，多少失败等等。 Netflix通过hystrix-metrics-event-stream项目实现了对以上指标的监控，SpringCloud也提供了Hystrix Dashboard的整合，对监控内容转化成可视化界面！ 新建工程springcloud-consumer-hystrix-dashboard-9001 导入pom依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 \u0026lt;dependencies\u0026gt; \u0026lt;!--Hystrix依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-hystrix\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--dashboard依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-hystrix-dashboard\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--Ribbon--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-ribbon\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--Eureka--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-eureka\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--实体类+web--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.github\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;springcloud-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--热部署--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-devtools\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 添加主启动类 1 2 3 4 5 6 7 @SpringBootApplication @EnableHystrixDashboard public class DeptConsumerDashBoardApp9001 { public static void main(String[] args) { SpringApplication.run(DeptConsumerDashBoardApp9001.class,args); } } 添加配置文件 1 2 3 4 5 6 server: port: 9001 hystrix: dashboard: proxy-stream-allow-list: localhost 给springcloud-provider-dept-hystrix-8001模块下的主启动类添加如下代码,添加监控 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @SpringBootApplication @EnableEurekaClient // 本服务启动之后会自动注册进Eureka中！ @EnableDiscoveryClient // 开启服务发现客户端的注解，可以用来获取一些配置的信息，得到具体的微服务 @EnableCircuitBreaker //对hystrix 熔断机制的支持 【==========new=======】 public class DeptProviderHystrix8001 { public static void main(String[] args) { SpringApplication.run(DeptProviderHystrix8001.class,args); } //增加一个 Servlet @Bean public ServletRegistrationBean hystrixMetricsStreamServlet(){ ServletRegistrationBean registrationBean = new ServletRegistrationBean(new HystrixMetricsStreamServlet()); //访问该页面就是监控页面 registrationBean.addUrlMappings(\u0026#34;/actuator/hystrix.stream\u0026#34;); return registrationBean; } } 测试一\n启动springcloud-consumer-hystrix-dashboard-9001监控服务\n访问：http://localhost:9001/hystrix\n启动3个Eureka集群\n启动springcloud-provider-dept-hystrix-8001\n进入监控页面，访问：\nhttp://localhost:8001/dept/get/1 http://localhost:8001/actuator/hystrix.stream 【查看1秒一动的数据流】 监控测试 多次刷新 http://localhost:8001/dept/get/1 观察监控窗口，就是那个豪猪页面； 添加监控地址： Delay: 该参数用来控制服务器上轮询监控信息的延迟时间，默认为2000毫秒，可以通过配置该属性来降低客户端的网络和CPU消耗 Title：该参数对应了头部标题HystrixStream之后的内容，默认会使用具体监控实例URL，可以通过配置该信息来展示更合适的标题。 监控结果： 如何看：\n7色：绿 \u0026gt; 蓝 \u0026gt; 青 \u0026gt; 黄 \u0026gt; 紫 \u0026gt; 红\n一圈：\n实心圆：公有两种含义，他通过颜色的变化代表了实例的健康程度。 它的健康程度从 绿色\u0026lt;黄色\u0026lt;橙色\u0026lt;红色 递减。 该实心圆除了颜色的变化之外，它的大小也会根据实例的请求流量发生变化，流量越大，该实心圆就越大，所以通过该实心圆的展示，就可以在大量的实例中快速发现故障实例和高压力实例。 一线：\n曲线:用来记录2分钟内流量的相对变化,可以通过它来观察到流量的上升和下降趋势; 整图说明：\n搞懂一个才能看懂复杂的\n9.Zull路由网关 概述\n什么是zuul?\nZull包含了对请求的路由(用来跳转的)和过滤两个最主要功能：\n其中路由功能负责将外部请求转发到具体的微服务实例上，是实现外部访问统一入口的基础，而过滤器功能则负责对请求的处理过程进行干预，是实现请求校验，服务聚合等功能的基础。Zuul和Eureka进行整合，将Zuul自身注册为Eureka服务治理下的应用，同时从Eureka中获得其他服务的消息，也即以后的访问微服务都是通过Zuul跳转后获得。\n注意：Zuul 服务最终还是会注册进 Eureka\n提供：代理 + 路由 + 过滤 三大功能！\nZuul 能干嘛？\n路由 过滤 官方文档：https://github.com/Netflix/zuul/\n入门案例\n新建springcloud-zuul模块，并导入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 \u0026lt;dependencies\u0026gt; \u0026lt;!--导入zuul依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-zuul\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--Hystrix依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-hystrix\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--dashboard依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-netflix-hystrix-dashboard\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.2.10.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--Ribbon--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-ribbon\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--Eureka--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-eureka\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--实体类+web--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.github\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;springcloud-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--热部署--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-devtools\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; application.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 server: port: 4399 spring: application: name: springcloud-zuul #Eureka的配置，服务注册到哪里 eureka: client: service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ instance: instance-id: zuul4399.com prefer-ip-address: true #改为true后默认显示的是ip地址而不再是localhost 方便团队不同人使用 # info配置 info: app.name: subei-springcloud # 项目的名称 company.name: www.subeily.top # 公司的名称 # zull 路由网关配置 zuul: # 路由相关配置 # 原来访问路由 eg:http://subeily.com:4399/springcloud-provider-dept/dept/get/2 # zull路由配置后访问路由 eg: http://subeily.com:4399/subeily/mydept/dept/get/2 routes: mydept.serviceId: springcloud-provider-dept # eureka注册中心的服务提供方路由名称 mydept.path: /mydept/** # 将eureka注册中心的服务提供方路由名称 改为自定义路由名称 ignored-services: \u0026#34;*\u0026#34; # 不能再使用这个路径访问了，*： 忽略,隐藏全部的服务名称~ prefix: /subeily # 设置公共的前缀 hosts修改\nhosts文件的位置在C:\\Windows\\System32\\drivers\\etc\\hosts 1 127.0.0.1 subeily.com 主启动类\n1 2 3 4 5 6 7 @SpringBootApplication @EnableZuulProxy // 开启Zuul public class SpringCloudZuulApp4399 { public static void main(String[] args) { SpringApplication.run(SpringCloudZuulApp4399.class,args); } } 启动：\n三个Eureka集群 服务提供者springcloud-provider-dept-8001 zuul服务 访问：http://localhost:7001/ 可以看出Zull路由网关被注册到Eureka注册中心中了！\n测试：\n不用路由：http://localhost:8001/dept/get/2\n使用路由：http://subeily.com:4399/springcloud-provider-dept/dept/get/2\n网关/微服务名字/具体的服务 上图是没有经过Zull路由网关配置时，服务接口访问的路由，可以看出直接用微服务(服务提供方)名称去访问，这样不安全，不能将微服务名称暴露！ 所以经过Zull路由网关配置后，访问封装好的url：http://subeily.com:4399/subeily/mydept/dept/get/2 微服务名称被替换并隐藏，换成了我们自定义的微服务名称mydept，同时加上了前缀subeily，这样就做到了对路由访问的加密处理。\n详情参考springcloud中文社区zuul组件:Spring Cloud Greenwich 中文文档 参考手册 中文版\n10.Spring Cloud Config 分布式配置 概述\n分布式系统面临的–配置文件问题\n微服务意味着要将单体应用中的业务拆分成一个个子服务，每个服务的粒度相对较小，因此系统中会出现大量的服务，由于每个服务都需要必要的配置信息才能运行，所以一套集中式的，动态的配置管理设施是必不可少的。spring cloud提供了configServer来解决这个问题，我们每一个微服务自己带着一个application.yml，那上百个的配置文件修改起来，令人头疼！ SpringCloud config分布式配置中心\nspring cloud config 为微服务架构中的微服务提供集中化的外部支持，配置服务器为各个不同微服务应用的所有环节提供了一个中心化的外部配置。 spring cloud config 分为服务端和客户端两部分。 服务端也称为 分布式配置中心，它是一个独立的微服务应用，用来连接配置服务器并为客户端提供获取配置信息，加密，解密信息等访问接口。 客户端则是通过指定的配置中心来管理应用资源，以及与业务相关的配置内容，并在启动的时候从配置中心获取和加载配置信息。配置服务器默认采用git来存储配置信息，这样就有助于对环境配置进行版本管理。并且可用通过git客户端工具来方便的管理和访问配置内容。 spring cloud config 分布式配置中心能干嘛？\n集中式管理配置文件 不同环境，不同配置，动态化的配置更新，分环境部署，比如 /dev /test /prod /beta /release 运行期间动态调整配置，不再需要在每个服务部署的机器上编写配置文件，服务会向配置中心统一拉取配置自己的信息 当配置发生变动时，服务不需要重启，即可感知到配置的变化，并应用新的配置 将配置信息以REST接口的形式暴露 spring cloud config 分布式配置中心与GitHub整合\n由于spring cloud config 默认使用git来存储配置文件 (也有其他方式，比如自持SVN 和本地文件)，但是最推荐的还是git，而且使用的是 http/https 访问的形式。 服务端\n前提:\n在码云上新建仓库 springcloud-config，==仓库要开源哦==！！！ 生成/添加SSH公钥 拉取到本地，编写application.yaml配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 spring: profiles: active: dev --- spring: profiles: dev application: name: springcloud-config-dev --- spring: profiles: test application: name: springcloud-config-test 将本地git仓库springcloud-config文件夹下新建的application.yml提交到码云仓库: HTTP服务具有以下格式的资源： 1 2 3 4 5 /{application}/{profile}[/{label}] /{application}-{profile}.yml /{label}/{application}-{profile}.yml /{application}-{profile}.properties /{label}/{application}-{profile}.properties 新建模块 springcloud-config-server-3344，导入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;dependencies\u0026gt; \u0026lt;!--web--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--config--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-config-server\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.1.1.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--eureka--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-eureka\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.4.6.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 编写配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 server: port: 3344 spring: application: name: springcloud-config-server # 连接码云远程仓库 cloud: config: server: git: # 注意是https的而不是ssh uri: https://gitee.com/yang365/springcloud-config.git # 通过 config-server可以连接到git，访问其中的资源以及配置~ # 不加这个配置会报Cannot execute request on any known server 这个错：连接Eureka服务端地址不对 # 或者直接注释掉eureka依赖 这里暂时用不到eureka eureka: client: register-with-eureka: false fetch-registry: false 主启动类 1 2 3 4 5 6 7 @EnableConfigServer // 开启spring cloud config server服务 @SpringBootApplication public class ConfigServer3344 { public static void main(String[] args) { SpringApplication.run(ConfigServer3344.class,args); } } 测试\n启动 ConfigServer3344 即可；\n访问 http://localhost:3344/application-dev.yaml\n访问 http://localhost:3344/application/test/master\n访问 http://localhost:3344/master/application-dev.yaml\n访问不存在的配置\n客户端\n在本地库编写一个配置文件config-client.yaml，并且上传到码云仓库。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 spring: profiles: active: dev --- server: port: 8201 #spring配置 spring: profiles: dev application: name: springcloud-config-client #Eureka的配置，服务注册到哪里 eureka: client: service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ --- server: port: 8202 #spring配置 spring: profiles: test application: name: springcloud-config-client #Eureka的配置，服务注册到哪里 eureka: client: service-url: defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ 新建 springcloud-config-client-3355 模块，导入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-config\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.1.1.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--——web--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--actuator完善监控信息--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-actuator\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; resources下创建application.yml和bootstrap.yml配置文件 其中bootstrap.yml是系统级别的配置文件，application.yml是用户级别的配置文件，系统级别更高级。因为要访问远程库的配置文件，所以一些重要的配置编写在系统级别的配置文件中。\nbootstrap.yaml 1 2 3 4 5 6 7 8 # 系统级别的配置 spring: cloud: config: name: config-client # 需要从git上读取的资源名称，不要后缀 profile: dev label: master uri: http://localhost:3344 application.yml 1 2 3 4 5 6 7 8 9 10 11 # 用户级别的配置 spring: application: name: springcloud-config-client # 不加这个配置会报Cannot execute request on any known server 这个错：连接Eureka服务端地址不对 # 或者直接注释掉eureka依赖 这里暂时用不到eureka eureka: client: register-with-eureka: false fetch-registry: false 创建controller包下的ConfigClientController.java 用于测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RestController public class ConfigClientController { @Value(\u0026#34;${spring.application.name}\u0026#34;) private String applicationName; //获取微服务名称 @Value(\u0026#34;${eureka.client.service-url.defaultZone}\u0026#34;) private String eurekaServer; //获取Eureka服务 @Value(\u0026#34;${server.port}\u0026#34;) private String port; //获取服务端的端口号 @RequestMapping(\u0026#34;/config\u0026#34;) public String getConfig(){ return \u0026#34;applicationName:\u0026#34;+applicationName + \u0026#34;eurekaServer:\u0026#34;+eurekaServer + \u0026#34;port:\u0026#34;+port; } } 编写主启动类 1 2 3 4 5 6 @SpringBootApplication public class ConfigClient3355 { public static void main(String[] args) { SpringApplication.run(ConfigClient3355.class,args); } } 测试一下，先启动3344，后启动客户端，然后访问 http://localhost:8201/config/ 在bootstrap.yaml文件中，切换一下环境dev-\u0026gt;test 1 2 3 4 5 6 7 8 # 系统级别的配置 spring: cloud: config: name: config-client # 需要从git上读取的资源名称，不要后缀 profile: test label: master uri: http://localhost:3344 重新测试，继续访问 http://localhost:8201/config 发现没用了。访问 http://localhost:8202/config 实战一下\n需求：把之前的7001、8001配置文件修改成远程库读取配置文件，实现配置与编码解耦。 本地新建config-dept.yaml和config-eureka.yaml并提交到码云仓库。 config-dept.yaml 其中为了测试dev和test唯一的不同是连接的数据库不同。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 spring: profiles: active: dev --- server: port: 8001 # mybatis的配置 mybatis: config-location: classpath:mybatis/mybatis-config.xml type-aliases-package: com.github.pojo mapper-locations: - classpath:mybatis/mapper/**/*.xml # spring的相关配置 spring: profiles: dev application: name: springcloud-config-dept datasource: type: com.alibaba.druid.pool.DruidDataSource # 数据源 driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动 url: jdbc:mysql://localhost:3306/springcloud?useSSL=false #数据库名称 username: root password: root dbcp2: min-idle: 5 #数据库连接池的最小维持连接数 initial-size: 5 #初始化连接数 max-total: 5 #最大连接数 max-wait-millis: 200 #等待连接获取的最大超时时间 # Eureka配置：配置服务注册中心地址 eureka: client: service-url: # 注册中心地址7001-7003 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ instance: instance-id: springcloud-provider-dept-8001 # 与client平级 # prefer-ip-address: true # true表示访问路径可以显示IP地址 # info配置 info: app.name: subei-springcloud # 项目的名称 company.name: www.subeily.top # 公司的名称 --- server: port: 8001 # mybatis的配置 mybatis: config-location: classpath:mybatis/mybatis-config.xml type-aliases-package: com.github.pojo mapper-locations: - classpath:mybatis/mapper/**/*.xml # spring的相关配置 spring: profiles: test application: name: springcloud-config-dept datasource: type: com.alibaba.druid.pool.DruidDataSource # 数据源 driver-class-name: org.gjt.mm.mysql.Driver # mysql驱动 url: jdbc:mysql://localhost:3306/springcloud?useSSL=false #数据库名称 username: root password: root dbcp2: min-idle: 5 #数据库连接池的最小维持连接数 initial-size: 5 #初始化连接数 max-total: 5 #最大连接数 max-wait-millis: 200 #等待连接获取的最大超时时间 # Eureka配置：配置服务注册中心地址 eureka: client: service-url: # 注册中心地址7001-7003 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ instance: instance-id: springcloud-provider-dept-8001 # 与client平级 # prefer-ip-address: true # true表示访问路径可以显示IP地址 # info配置 info: app.name: subei-springcloud # 项目的名称 company.name: www.subeily.top # 公司的名称 config-eureka.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 spring: profiles: active: dev --- server: port: 7001 #spring配置 spring: profiles: dev application: name: springcloud-config-eureka # Eureka配置 eureka: instance: hostname: eureka7001.com #eureka服务端的实例名称 client: register-with-eureka: false #是否将自己注册到Eureka服务器中，本身是服务器，无需注册 fetch-registry: false #false表示自己端就是注册中心，职责是维护服务实例，并不需要检索服务 service-url: # 单机 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个defaultZone地址 defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ --- server: port: 7001 #spring配置 spring: profiles: test application: name: springcloud-config-eureka # Eureka配置 eureka: instance: hostname: eureka7001.com #eureka服务端的实例名称 client: register-with-eureka: false #是否将自己注册到Eureka服务器中，本身是服务器，无需注册 fetch-registry: false #false表示自己端就是注册中心，职责是维护服务实例，并不需要检索服务 service-url: # 单机 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ # 设置与Eureka Server交互的地址查询服务和注册服务都需要依赖这个defaultZone地址 defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/ 新建springcloud-config-eureka-7001模块，内容和之前的springcloud-eureka-7001一样\n修改springcloud-config-eureka-7001的配置文件，换成远程库读取\n添加bootstrap.yaml系统配置文件 1 2 3 4 5 6 7 8 # 系统级别的配置 spring: cloud: config: name: config-eureka # 需要从git上读取的资源名称，不要后缀 profile: dev label: master uri: http://localhost:3344 修改application.yaml 1 2 3 4 5 6 7 8 9 10 spring: application: name: springcloud-config-eureka # 不加这个配置会报Cannot execute request on any known server 这个错：连接Eureka服务端地址不对 # 或者直接注释掉eureka依赖 这里暂时用不到eureka eureka: client: register-with-eureka: false fetch-registry: false 新建springcloud-config-provider-dept-8001模块，内容和之前的springcloud-provider-dept-8001一样\n修改springcloud-config-provider-dept-8001的配置文件，换成远程库读取\n添加bootstrap.yaml系统配置文件 1 2 3 4 5 6 7 8 # 系统级别的配置 spring: cloud: config: name: config-dept # 需要从git上读取的资源名称，不要后缀 profile: dev label: master uri: http://localhost:3344 修改application.yaml 1 2 3 4 5 6 7 8 9 10 spring: application: name: springcloud-config-provider-dept # 不加这个配置会报Cannot execute request on any known server 这个错：连接Eureka服务端地址不对 # 或者直接注释掉eureka依赖 这里暂时用不到eureka eureka: client: register-with-eureka: false fetch-registry: false ==在7001、8001的pom.xml中添加spring cloud config依赖==。\n1 2 3 4 5 6 \u0026lt;!--config--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-config\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.1.1.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 测试\n启动cofig3344服务端、客户端7001和8001 访问 http://localhost:3344/master/config-eureka-dev.yaml , 说明config服务端没问题\n访问 http://localhost:7001/ 说明远程配置读取成功\n访问 http://localhost:8001/dept/get/2 说明8001也从远程库读取配置文件成功\n修改客户端8001的配置文件，变成读取test版的配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 系统级别的配置 spring: cloud: config: name: config-dept # 需要从git上读取的资源名称，不要后缀 profile: test label: master uri: http://localhost:3344 # 不加这个配置会报Cannot execute request on any known server 这个错：连接Eureka服务端地址不对 # 或者直接注释掉eureka依赖 这里暂时用不到eureka eureka: client: register-with-eureka: false fetch-registry: false 重新启动访问 http://localhost:8001/dept/get/2 测试成功。\n总结导图 ","permalink":"https://ktzxy.top/posts/t9lwsjix2i/","summary":"SpringCloud","title":"SpringCloud"},{"content":"﻿# Day-10-IO流\nFile类 【1】对文件进行操作：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 //将文件封装为一个File类的对象： File f = new File(\u0026#34;E:\\\\test.txt\u0026#34;); File f2 = new File(\u0026#34;E:/test.txt\u0026#34;); //File.separator属性帮我们获取当前操作系统的路径拼接符号 File f3 = new File(\u0026#34;E:\u0026#34;+File.separator+\u0026#34;test.txt\u0026#34;); //建议使用这种 //常用方法： System.out.println(\u0026#34;文件是否可读\u0026#34;+f.canRead()); System.out.println(\u0026#34;文件是否可写\u0026#34;+f.canWrite()); System.out.println(\u0026#34;文件的名字\u0026#34;+f.getName()); System.out.println(\u0026#34;文件上级目录\u0026#34;+f.getParent()); System.out.println(\u0026#34;是否是一个目录\u0026#34;+f.isDirectory()); System.out.println(\u0026#34;是否是一个文件\u0026#34;+f.isFile()); System.out.println(\u0026#34;是否隐藏\u0026#34;+f.isHidden()); System.out.println(\u0026#34;文件大小\u0026#34;+f.length()); System.out.println(\u0026#34;文件是否存在\u0026#34;+f.exists()); if (f.exists()){ f.delete(); }else { f.createNewFile(); } System.out.println(f == f2); //比较两个对象的地址 System.out.println(f.equals(f2)); //比较两个对象对应的文件的路径 //跟路径相关的 System.out.println(\u0026#34;绝对路径\u0026#34;+f.getAbsolutePath()); System.out.println(\u0026#34;相对路径\u0026#34;+f.getPath()); //toString的效果永远是 相对路径 System.out.println(f.toString()); 【2】对目录进行操作：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 File f2 = new File(\u0026#34;d:\\\\a\\\\b\\\\c\u0026#34;); //创建目录： f2.mkdir(); //创建单层目录 f2.mkdirs(); //创建多层目录 //删除:如果是删除目录的话，只会删除一层，并且前提:这层目录是空的，里面没有内容，如果内容就不会被删除 f2.delete(); //查看 String[] list = f2.list(); //文件夹下目录/文件对应的名字的数组 for (String s:list){ System.out.println(s); } File[] files = f2.listFiles(); for (File file:files){ System.out.println(file.getName()+\u0026#34;,\u0026#34;+file.getAbsolutePath()); } } IO流 I/O：Input/Output的缩写，用于处理设备之间的数据的传输。\n【1】形象理解：IO流当作一根“管”：\n【2】IO流的体系结构：\n【3】利用FileReader，FileWriter完成文件复制\n逐个字符读取：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.zy.io1; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; /** * @Auther: 赵羽 * @Description: com.zy.io1 * @version: 1.0 */ public class Test1 { public static void main(String[] args) throws IOException { //创建一个File类的对象 File f = new File(\u0026#34;E:\\\\test.txt\u0026#34;); //创建一个FileReader的流的对象 FileReader fr = new FileReader(f); //读取动作 int r = fr.read(); while (r!=-1){ System.out.println((char) r); r = fr.read(); } //关闭流 fr.close(); } } 多个字符读取：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.zy.io1; import java.io.File; import java.io.FileReader; import java.io.IOException; /** * @Auther: 赵羽 * @Description: com.zy.io1 * @version: 1.0 */ public class Test2 { public static void main(String[] args) throws IOException { //创建一个File类的对象 File f = new File(\u0026#34;E:\\\\test.txt\u0026#34;); //创建一个FileReader的流的对象 FileReader fr = new FileReader(f); //读取动作 char[] ch = new char[5]; int len = fr.read(ch); while (len!=-1){ for (int i = 0; i \u0026lt; len; i++) { System.out.println(ch[i]); } len = fr.read(ch); } //关闭流 fr.close(); } } 逐个字符写入：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.zy.io1; import java.io.File; import java.io.FileWriter; import java.io.IOException; /** * @Auther: 赵羽 * @Description: com.zy.io1 * @version: 1.0 */ public class Test3 { public static void main(String[] args) throws IOException { //创建一个File类的对象 File f = new File(\u0026#34;E:\\\\demo.txt\u0026#34;); //创建一个FileReader的流的对象 FileWriter fw = new FileWriter(f); //输出动作 String str = \u0026#34;hello你好呀\u0026#34;; for (int i = 0; i \u0026lt; str.length(); i++) { fw.write(str.charAt(i)); } //关闭流： fw.close(); } } 发现: 如果目标文件不存在的话，那么会自动创建此文件。\n如果目标文件存在的话: new FileWriter(f)相当于对源文件进行覆盖操作。\nnew FileWriter(f,false)相当于对源文件进行覆盖操作。不是追加。\nnew FileWriter(f,true)对原来的文件进行追加，而不是覆盖。\n多个字符写入：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.zy.io1; import java.io.File; import java.io.FileWriter; import java.io.IOException; /** * @Auther: 赵羽 * @Description: com.zy.io1 * @version: 1.0 */ public class Test4 { public static void main(String[] args) throws IOException { //创建一个File类的对象 File f = new File(\u0026#34;E:\\\\demo.txt\u0026#34;); //创建一个FileReader的流的对象 FileWriter fw = new FileWriter(f,true); //输出动作 String str = \u0026#34;dadghge\u0026#34;; char[] chars = str.toCharArray(); fw.write(chars); //关闭流： fw.close(); } } 综合操作：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.zy.io1; import java.io.*; /** * @Auther: 赵羽 * @Description: com.zy.io1 * @version: 1.0 */ public class Test5 { public static void main(String[] args) throws IOException { //1.有一个源文件 File f1 = new File(\u0026#34;E:\\\\test.txt\u0026#34;); //2.有一个目标文件 File f2 = new File(\u0026#34;E:\\\\demo.txt\u0026#34;); //3.输入操作 FileReader fr = new FileReader(f1); //4.输出操作 FileWriter fw = new FileWriter(f2); //5.复制操作 //方式一：逐个字符复制 /* int n = fr.read(); while (n!=-1){ fw.write(n); n = fr.read(); } */ //方式二:多个字符复制 利用缓冲字符数组 char[] ch = new char[5]; int len = fr.read(ch); while (len!=-1){ fw.write(ch,0,len); len = fr.read(ch); } //6.关闭流 fw.close(); fr.close(); } } 文本文件: .txt .java .c .cpp \u0026mdash;》建议使用字符流操作 非文本文件: .jpg .mp3 .mp4 .doc .ppt \u0026mdash;》建议使用字节流操作\n异常处理：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.zy.io1; import java.io.*; /** * @Auther: 赵羽 * @Date: 2021/2/21 - 02 - 21 - 18:49 * @Description: com.zy.io1 * @version: 1.0 */ public class Test6 { public static void main(String[] args) { //1.有一个源文件 File f1 = new File(\u0026#34;E:\\\\test.txt\u0026#34;); //2.有一个目标文件 File f2 = new File(\u0026#34;E:\\\\demo.txt\u0026#34;); //3.输入操作 FileReader fr = null; try { fr = new FileReader(f1); } catch (FileNotFoundException e) { e.printStackTrace(); } //4.输出操作 FileWriter fw = null; try { fw = new FileWriter(f2); //5.复制操作 char[] ch = new char[5]; int len = fr.read(ch); while (len!=-1){ fw.write(ch,0,len); len = fr.read(ch); } } catch (IOException e) { e.printStackTrace(); }finally { //6.关闭流 try { if (fw!=null){ //防止空指针异常 fw.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (fr!=null){ //防止空指针异常 fr.close(); } } catch (IOException e) { e.printStackTrace(); } } } } 【4】FileInputStream、FileOutputStream\n利用字节流读取文本文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.zy.io2; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; /** * @Auther: 赵羽 * @Description: com.zy.io2 * @version: 1.0 */ public class Test1 { public static void main(String[] args) throws IOException { //功能：利用字节流将文件中内容读到程序中来： File f = new File(\u0026#34;E:\\\\test.txt\u0026#34;); FileInputStream fis = new FileInputStream(f); /* * 文件是utf-8进行存储的，所以英文字符 底层实际占用1个字节 * 但是中文字符 底层实际占用3个字节。 *如果文件是文本文件，那么就不要使用字节流读取了，建议使用字符流。 * * */ int n = fis.read(); while (n!=-1){ System.out.println(n); n =fis.read(); } fis.close(); } } 利用字节流读取非文本文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.zy.io2; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; /** * @Auther: 赵羽 * @Description: com.zy.io2 * @version: 1.0 */ public class Test1 { public static void main(String[] args) throws IOException { //功能：利用字节流将文件中内容读到程序中来： File f = new File(\u0026#34;E:\\\\test.jpg\u0026#34;); FileInputStream fis = new FileInputStream(f); int count =0; int n = fis.read(); while (n!=-1){ count++; System.out.println(n); n =fis.read(); } System.out.println(\u0026#34;count\u0026#34;+count) fis.close(); } } 利用字节类型的缓冲数组读取：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.zy.io2; import sun.java2d.pipe.BufferedTextPipe; import java.io.File; import java.io.FileInputStream; import java.io.IOException; /** * @Auther: 赵羽 * @Description: com.zy.io2 * @version: 1.0 */ public class Test2 { public static void main(String[] args) throws IOException { //功能：利用字节流将文件中内容读到程序中来： File f = new File(\u0026#34;E:\\\\test.jpg\u0026#34;); FileInputStream fis = new FileInputStream(f); //利用缓冲数组： byte[] b = new byte[1024*6]; int len = fis.read(b); while (len!=-1){ for (int i = 0; i \u0026lt; len; i++) { System.out.println(b[i]); } len = fis.read(b); } fis.close(); } } FileInputStream、FileOutputStream完成非文本文件的复制\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.zy.io2; import java.io.*; /** * @Auther: 赵羽 * @Description: com.zy.io2 * @version: 1.0 */ public class Test3 { public static void main(String[] args) throws IOException { //功能：完成图片的复制： File f1 = new File(\u0026#34;E:\\\\cat.jpg\u0026#34;); File f2 = new File(\u0026#34;E:\\\\cat1.jpg\u0026#34;); FileInputStream fis = new FileInputStream(f1); FileOutputStream fos = new FileOutputStream(f2); int n = fis.read(); while (n!=-1){ fos.write(n); n = fis.read(); } fos.close(); fis.close(); } } 利用缓冲数组完成\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.zy.io2; import java.io.*; /** * @Auther: 赵羽 * @Description: com.zy.io2 * @version: 1.0 */ public class Test4 { public static void main(String[] args) throws IOException { File f1 = new File(\u0026#34;E:\\\\cat.jpg\u0026#34;); File f2 = new File(\u0026#34;E:\\\\cat2.jpg\u0026#34;); FileInputStream fis = new FileInputStream(f1); FileOutputStream fos = new FileOutputStream(f2); byte[] b = new byte[1024*8]; int len = fis.read(); while (len!=-1){ fos.write(b); len = fis.read(); } fos.close(); fis.close(); } } 【5】缓冲字节流（处理流）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.zy.io2; import java.io.*; /** * @Auther: 赵羽 * @Description: com.zy.io2 * @version: 1.0 */ public class Test5 { public static void main(String[] args) throws IOException { File f1 = new File(\u0026#34;E:\\\\cat.jpg\u0026#34;); File f2 = new File(\u0026#34;E:\\\\cat3.jpg\u0026#34;); FileInputStream fis = new FileInputStream(f1); FileOutputStream fos = new FileOutputStream(f2); //功能加强，在FiLeInputStream外面套一个管:BufferedInputStream: BufferedInputStream bis = new BufferedInputStream(fis); //功能加强，在FileoutputStream外面套一个管: BufferedoutputStream:; BufferedOutputStream bos = new BufferedOutputStream(fos); byte[] b = new byte[1024 * 8]; int len = bis.read(b); while (len!=-1){ bos.write(b,0,len); len = bis.read(b); } //如果处理流包裹着节点流的话，那么其实只要关闭高级流（处理流），那么里面的字节流也会随之被关闭。 bos.close(); bis.close(); /*fos.close(); fis.close();*/ } } 【6】缓冲字符流（处理流）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.zy.io2; import java.io.*; /** * @Auther: 赵羽 * @Description: com.zy.io2 * @version: 1.0 */ public class Test6 { public static void main(String[] args) throws IOException { File f1 = new File(\u0026#34;E:\\\\test.txt\u0026#34;); File f2 = new File(\u0026#34;E:\\\\demo.txt\u0026#34;); FileReader fr = new FileReader(f1); FileWriter fw = new FileWriter(f2); BufferedReader br = new BufferedReader(fr); BufferedWriter bw = new BufferedWriter(fw); //方式一：逐个字符读取 int n = br.read(); while (n!=-1){ bw.write(n); n = br.read(); } //方式二：利用缓冲数组 char[] ch = new char[30]; int len = br.read(ch); while (len!=-1){ bw.write(ch,0,len); len = br.read(); } //方式三：读取String String s = br.readLine(); //每次读取文本文件中一行，返回字符串 while (s!=null){ bw.write(s); //在每次文件中应该再写出一个换行： bw.newLine(); //新起一行 s = br.readLine(); } bw.close(); br.close(); } } 【7】转换流\n转换流:作用:将字节流和字符流进行转换。\nlnputStreamReader :字节输入流\u0026mdash;》字符的输入流\nOutputStreamWriter :字符输出流\u0026ndash;》字节的输出流\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.zy.io2; import java.io.*; /** * @Auther: 赵羽 * @Description: com.zy.io2 * @version: 1.0 */ public class Test7 { public static void main(String[] args) throws IOException { File f = new File(\u0026#34;E:\\\\test.txt\u0026#34;); FileInputStream fis = new FileInputStream(f); //加入一个转换流，将字节流转换为字符流:（转换流属于一个处理流) //将字节转换为字符的时候，需要指定一个编码，这个编码跟文件本身的编码格式统一 //如果编码格式不统一的话，那么在控制台上展示的效果就会出现乱码 InputStreamReader isr = new InputStreamReader(fis, \u0026#34;utf-8\u0026#34;); char[] ch = new char[20]; int len = isr.read(ch); while (len!=-1){ System.out.println(new String(ch, 0, len)); len = isr.read(ch); } isr.close(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.zy.io2; import java.io.*; /** * @Auther: 赵羽 * @Description: com.zy.io2 * @version: 1.0 */ public class Test8 { public static void main(String[] args) throws IOException { File f = new File(\u0026#34;E:\\\\test.txt\u0026#34;); File f2 = new File(\u0026#34;E:\\\\demo.txt\u0026#34;); FileInputStream fis = new FileInputStream(f); InputStreamReader isr = new InputStreamReader(fis, \u0026#34;utf-8\u0026#34;); FileOutputStream fos = new FileOutputStream(f2); OutputStreamWriter ows = new OutputStreamWriter(fos,\u0026#34;gbk\u0026#34;); char[] ch = new char[20]; int len = isr.read(ch); while (len!=-1){ ows.write(ch,0,len); len = isr.read(ch); } ows.close(); isr.close(); } } 练习：键盘录入内容输出到文件中\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package com.zy.io2; import java.io.*; /** * @Auther: 赵羽 * @Description: com.zy.io2 * @version: 1.0 */ public class Test9 { public static void main(String[] args) throws IOException { //1.先准备输入方向: //键盘录入: InputStream in = System.in; //属于字节流 //字节流---》字符流 InputStreamReader isr = new InputStreamReader(in); //在isr外面再套一个缓冲流 BufferedReader br = new BufferedReader(isr); //2.再准备输出方向： //准备目标文件 File f = new File(\u0026#34;E:\\\\demo2.txt\u0026#34;); FileWriter fw = new FileWriter(f); BufferedWriter bw = new BufferedWriter(fw); //3.开始动作 String s = br.readLine(); while (!s.equals(\u0026#34;exit\u0026#34;)){ bw.write(s); bw.newLine(); //文件中换行 s = br.readLine(); } //4.关闭流 bw.close(); br.close(); } } 【8】数据流\n数据流:用来操作基本数据类型和字符串的\nDatalnputStream:将文件中存储的基本数据类型和字符串写入内存的变量中\nDataOutputStream:将内存中的基本数据类型和字符串的变量写出文件中\n利用DataOutputStream向外写出变量:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package com.zy.io3; import java.io.*; /** * @Auther: 赵羽 * @Description: com.zy.io3 * @version: 1.0 */ public class Test1 { public static void main(String[] args) throws IOException { /*File f = new File(\u0026#34;E:\\\\demo2.txt\u0026#34;); FileOutputStream fos = new FileOutputStream(f); DataOutputStream dos = new DataOutputStream(fos);*/ DataOutputStream dos = new DataOutputStream(new FileOutputStream(new File(\u0026#34;E:\\\\demo2.txt\u0026#34;))); //向外将变量写到文件中去： dos.writeUTF(\u0026#34;你好\u0026#34;); dos.writeBoolean(false); dos.writeDouble(45.2); dos.writeInt(25); dos.close(); } } 利用DataInputStream读取变量:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.zy.io3; import java.io.*; /** * @Auther: 赵羽 * @Description: com.zy.io3 * @version: 1.0 */ public class Test2 { public static void main(String[] args) throws IOException { DataInputStream dis = new DataInputStream(new FileInputStream(new File(\u0026#34;E:\\\\demo2.txt\u0026#34;))); System.out.println(dis.readUTF()); System.out.println(dis.readBoolean()); System.out.println(dis.readDouble()); System.out.println(dis.readInt()); dis.close(); } } 【9】对象流\n对象流:ObjectlnputStream,ObjectlnputStream用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源中，也能把对象从数据源中还原回来。\n序列化和反序列化: objectOutputStream类∶把内存中的Java对象转换成平台无关的二进制数据，从而允许把这种二进制数据持久地保存在磁盘上，或通过网络将这种二进制数据传输到另一个网络节点。\u0026mdash;-》序列化 用ObjectlnputStream类:当其它程序获取了这种二进制数据，就可以恢复成原来的Java对象。\u0026mdash;-》反序列化\n操作字符串类型\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.zy.io3; import java.io.*; /** * @Auther: 赵羽 * @Description: com.zy.io3 * @version: 1.0 */ public class Test3 { public static void main(String[] args) throws IOException, ClassNotFoundException { //写入 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File(\u0026#34;E:\\\\demo3.txt\u0026#34;))); oos.writeObject(\u0026#34;你好\u0026#34;); oos.close(); //读取 ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File(\u0026#34;E:\\\\demo3.txt\u0026#34;))); String s = (String)(ois.readObject()); System.out.println(s); ois.close(); } } 操作自定义类的对象：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.zy.io4; /** * @Auther: 赵羽 * @Description: com.zy.io4 * @version: 1.0 */ public class Person { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } } 测试类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.zy.io4; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; /** * @Auther: 赵羽 * @Description: com.zy.io4 * @version: 1.0 */ public class Test1 { public static void main(String[] args) throws IOException { //序列化：将内存中对象 ----》文件： //有一个对象 Person p = new Person(\u0026#34;张三\u0026#34;, 18); //有对象流 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File(\u0026#34;E:\\\\demo4.txt\u0026#34;))); //向外写： oos.writeObject(p); //关闭流 oos.close(); } } 发生异常：\n出现异常的原因: 你想要序列化的那个对象对应的类，必须要实现一个接口:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package com.zy.io4; import java.io.Serializable; /** * @Auther: 赵羽 * @Description: com.zy.io4 * @version: 1.0 */ public class Person implements Serializable { //添加接口 private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } } 结果需要反序列查看：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.zy.io4; import java.io.*; /** * @Auther: 赵羽 * @Description: com.zy.io4 * @version: 1.0 */ public class Test2 { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File(\u0026#34;E:\\\\demo4.txt\u0026#34;))); Person p = (Person)(ois.readObject()); System.out.println(p); ois.close(); } } serialVersionUID: 凡是实现Serializable接口(标识接口）的类都有一个表示序列化版本标识符的静态变量:\nprivate static final long serialVersionUID;\nserialNersionUID用来表明类的不同版本间的兼容性。简言之，其目的是以序列化对象进行版本控制，有关各版本反序加化时是否兼容。 如果类没有显示定义这个静态变量，它的值是Java运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID可能发生变化。故建议，显式声明。 简单来说，Java的序列化机制是通过在运行时判断类的serialNersionUID来验证版本一致性的。在进行反序列化时，JVM会把传来的字节流中的serialVersionUlID与本地相应实体类的serialVersionUID进行比较，如果相同就认为是一致的，可以进行反序列化，否则就会出现序列化版本不一致的异常。(lnvalidCastException)\n添加toString\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package com.zy.io4; import java.io.Serializable; /** * @Auther: 赵羽 * @Description: com.zy.io4 * @version: 1.0 */ public class Person implements Serializable { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Person() { } public Person(String name, int age) { this.name = name; this.age = age; } @Override public String toString() { return \u0026#34;Person{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#39;}\u0026#39;; } } 再次测试：\n出现异常原因：\n解决:给这个类加入一个序列号: serialVersionUID\nIDEA中配置序列化版本号：\nalt+enter自动生成序列化版本号：\n(1）被序列化的类的内部的所有属性，必须是可序列化的(基本数据类型都是可序列化的。\n(2) static,transient修饰的属性不可以被序列化。\n","permalink":"https://ktzxy.top/posts/e4dlfsd6sw/","summary":"Day 11 IO流","title":"Day 11 IO流"},{"content":"1. 数据库技术 1.1. 概述 数据库（DB）是一个以某种组织方式存储在磁盘上的数据的集合。简单理解就是用来存储数据的仓库。\n数据库 DataBase（DB）：存储数据的仓库，数据是有组织的进行存储。 数据库管理系统 DataBase Management System (DBMS)：操纵和管理数据库的大型软件。 1.2. 数据库的分类 1.3. 不同的数据存储方式 数据存储在集合（内存）中\n优点：读写速度快 缺点：不能永久存储 数据存储在文件中\n优点：可以永久存储 缺点：频繁的IO操作效率低，查询数据很不方便。 数据存储在数据库中\n优点：可以永久存储。查询速度快，查询数据很方便 缺点：要使用 SQL 语言执行增删改查操作 2. 关系型数据库 关系型数据库 - 百度百科\n2.1. 概念 关系型数据库，是指采用了关系模型来组织数据的数据库，其以行和列的形式存储数据，以便于用户理解，关系型数据库这一系列的行和列被称为表，一组表组成了数据库。用户通过查询来检索数据库中的数据，而查询是一个用于限定数据库中某些区域的执行代码。关系模型可以简单理解为二维表格模型，而一个关系型数据库就是由二维表及其之间的关系组成的一个数据组织。\n2.2. SQL 语言 SQL (Structured Query Language)：操作关系型数据库的编程语言，定义了一套操作关系型数据库统一标准\n2.3. 常见的关系型数据库管理系统 MySQL：开源免费的中小型数据库，后来 Sun 公司收购了 MySQL，而 Oracle 又收购了Sun公司。从 MySQL 6.x 版本开始推出了收费版本，但也提供了免费的社区版本。 Oracle：收费的大型数据库，Oracle 公司的产品。Oracle 收购 SUN 公司，收购 MYSQL。 DB2：IBM公司的大型数据库产品，收费的。常应用在银行系统中. SQL Server：MicroSoft 公司收费的中型的数据库。C#、.net 等语言常使用。 PostgreSQL：开源免费的功能最强大的中小型开源数据库 SyBase：已经淡出历史舞台。提供了一个非常专业数据建模的工具 PowerDesigner。 SQLite：嵌入式的微型数据库，应用在手机端。Android 内置的数据库采用的就是该数据库。 MariaDB：开源免费的中小型数据库。是 MySQL 数据库的另外一个分支、另外一个衍生产品，与 MySQL 数据库有很好的兼容性。 Java 目前应用最多的数据库是：MySQL，Oracle。不论使用以上哪一个关系型数据库，最终在操作时，都是使用 SQL 语言来进行统一操作，因为 SQL 语言，是操作关系型数据库的统一标准。例如学习 MySQL，同样可以应用到别的关系型数据库，如：Oracle、DB2、SQLServer。\n3. NoSQL 数据库 3.1. 什么是 NoSQL NoSQL(NoSQL = Not Only SQL)，意即“不仅仅是SQL”，是一项全新的数据库理念，泛指非关系型的数据库。\n关系型数据库指的是，表与表之间有主外键，这种表之间有关系的数据，我们叫做关系型数据库。\n3.2. 为什么需要 NoSQL 随着互联网 web 2.0 网站的兴起，非关系型的数据库现在成了一个极其热门的新领域，非关系数据库产品的发展非常迅速。而传统的关系数据库在应付 web 2.0 网站，特别是超大规模和高并发的 SNS 类型的 web 2.0 纯动态网站已经显得力不从心，暴露了很多难以克服的问题，例如：\nHigh performance - 对数据库高并发读写的需求：web 2.0 网站要根据用户个性化信息来实时生成动态页面和提供动态信息，所以基本上无法使用动态页面静态化技术，因此数据库并发负载非常高，往往要达到每秒上万次读写请求。关系数据库应付上万次 SQL 查询还勉强顶得住，但是应付上万次 SQL 写数据请求，硬盘 IO 就已经无法承受了。其实对于普通的 BBS 网站，往往也存在对高并发写请求的需求，例如网站的实时统计在线用户状态，记录热门帖子的点击次数，投票计数等，因此这是一个相当普遍的需求。 Huge Storage - 对海量数据的高效率存储和访问的需求：类似 Facebook，Twitter，Friendfeed 这样的 SNS 网站，每天用户产生海量的用户动态，以 Friendfeed 为例，一个月就达到了 2.5 亿条用户动态，对于关系数据库来说，在一张 2.5 亿条记录的表里面进行 SQL 查询，效率是极其低下乃至不可忍受的。再例如大型 web 网站的用户登录系统，例如腾讯，盛大，动辄数以亿计的帐号，关系数据库也很难应付。 High Scalability \u0026amp;\u0026amp; High Availability - 对数据库的高可扩展性和高可用性的需求：在基于 web 的架构当中，数据库是最难进行横向扩展的，当一个应用系统的用户量和访问量与日俱增的时候，你的数据库却没有办法像 web server 和 app server 那样简单的通过添加更多的硬件和服务节点来扩展性能和负载能力。对于很多需要提供 24 小时不间断服务的网站来说，对数据库系统进行升级和扩展是非常痛苦的事情，往往需要停机维护和数据迁移，为什么数据库不能通过不断的添加服务器节点来实现扩展呢？ NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战，尤其是大数据应用难题。\n3.3. 主流 NoSQL 产品分类 NoSQL 数据库的四大分类如下：\n键值(Key-Value)存储数据库、\n相关产品： Tokyo Cabinet/Tyrant、Redis、Voldemort、Berkeley DB 典型应用： 内容缓存，主要用于处理大量数据的高访问负载。 数据模型： 一系列键值对 优势： 快速查询 劣势： 存储的数据缺少结构化 列存储数据库\n相关产品：Cassandra, HBase, Riak 典型应用：分布式的文件系统 数据模型：以列簇式存储，将同一列数据存在一起 优势：查找速度快，可扩展性强，更容易进行分布式扩展 劣势：功能相对局限 文档型数据库\n相关产品：CouchDB、MongoDB 典型应用：Web应用（与Key-Value类似，Value是结构化的） 数据模型： 一系列键值对 优势：数据结构要求不严格 劣势： 查询性能不高，而且缺乏统一的查询语法 图形(Graph)数据库\n相关数据库：Neo4J、InfoGrid、Infinite Graph 典型应用：社交网络 数据模型：图结构 优势：利用图结构相关算法。 劣势：需要对整个图做计算才能得出结果，不容易做分布式的集群方案。 3.4. NoSQL 特点 在大数据存取上具备关系型数据库无法比拟的性能优势，例如：\n易扩展。NoSQL 数据库种类繁多，但是一个共同的特点都是去掉关系数据库的关系型特性。数据之间无关系，这样就非常容易扩展。也无形之间，在架构的层面上带来了可扩展的能力。 大数据量，高性能。NoSQL 数据库都具有非常高的读写性能，尤其在大数据量下，同样表现优秀。这得益于它的无关系性，数据库的结构简单。 灵活的数据模型。NoSQL 无需事先为要存储的数据建立字段，随时可以存储自定义的数据格式。而在关系数据库里，增删字段是一件非常麻烦的事情。如果是非常大数据量的表，增加字段简直就是一个噩梦。这点在大数据量的 Web2.0 时代尤其明显。 高可用。NoSQL 在不太影响性能的情况，就可以方便的实现高可用的架构。比如 Cassandra，HBase 模型，通过复制模型也能实现高可用。 综上所述，NoSQL 的非关系特性使其成为了后 Web2.0 时代的宠儿，助力大型 Web2.0 网站的再次起飞，是一项全新的数据库革命性运动。\n4. 关系型数据库表设计规范化 4.1. 数据库设计步骤 收集信息：与该系统有关人员进行交流、座谈，充分了解用户需求，理解数据库需要完成的任务 标识实体（Entity）：标识数据库要管理的关键对象或实体，实体一般是名词 标识每个实体的属性（Attribute） 标识实体之间的关系（Relationship） 4.2. 什么是范式(NF) 良好的表结构设计是高性能的基石，应该根据系统将要执行的业务查询来设计，这往往需要权衡各种因素。糟糕的表结构设计，直接影响到数据库的性能，并需要花费大量不必要的优化时间，往往还没有什么效果。在数据库表设计上有个很重要的设计准则，称为范式设计。\n范式来自英文 Normal Form，简称 NF，是一套用来设计数据库的规则，在关系型数据库中这些规则就称为范式。好的数据库设计对数据的存储性能和后期的程序开发，都会产生重要的影响。建立科学的，规范的数据库就需要满足一些规则来优化数据的设计和存储。\n要想设计—个好的关系，必须使关系满足一定的约束条件，此约束已经形成了规范，分成几个等级，一级比一级要求得严格。满足这些规范的数据库是简洁的、结构明晰的，同时，不会发生插入(insert)、删除(delete)和更新(update)操作异常。\n4.3. 范式分类 目前关系数据库有六种范式：第一范式（1NF）、第二范式（2NF）、第三范式（3NF）、巴斯-科德范式（BCNF）、第四范式(4NF）和第五范式（5NF，又称完美范式）。\n满足最低要求的范式是第一范式（1NF）。在第一范式的基础上进一步满足更多规范要求的称为第二范式（2NF），其余范式以次类推。一般说来，数据库只需满足第三范式(3NF）就行了。\n4.3.1. 第一范式(1NF) 第一范式是数据库设计最基本的要求。 要求数据库表的每一列都是不可再分割的原子性数据项。 第一范式每一列不可再拆分，称为原子性。 不能是集合、数组、记录等非原子数据项。即实体中的某个属性有多个值时，必须拆分为不同的属性。在符合第一范式（1NF）表中每个列的值只能是表的一个属性或一个属性的一部分。\n4.3.2. 第二范式(2NF) 要先满足第一范式前提下，表中必须有主键，其他非主键列要完全依赖于主键，而不能只依赖于一部分。简单来说，一张表只描述一件事情。\n第二范式（2NF）要求数据库表中的每个实例或记录必须可以被唯一地区分。选取一个能区分每个实体的属性或属性组，作为实体的唯一标识。例如在员工表中的身份证号码即可实现每个员工的区分，该身份证号码即为候选键，任何一个候选键都可以被选作主键。在找不到候选键时，可额外增加属性以实现区分。\n第二范式（2NF）要求实体的属性完全依赖于主关键字。所谓完全依赖是指不能存在仅依赖主关键字一部分的属性。如果存在，那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体，新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列，以存储各个实例的唯一标识。简而言之，第二范式就是在第一范式的基础上属性完全依赖于主键。\n4.3.3. 第三范式(3NF) 在满足第二范式的前提下，表中的每一列都直接依赖于主键，而不是通过其它的列来间接依赖于主键（不存在传递依赖）。\n所谓传递依赖是指如果存在“A(主键字段) -\u0026gt; B(非主键字段) -\u0026gt; C(非主键字段)”的决定关系，则C依赖A。\n第三范式（3NF）是第二范式（2NF）的一个子集，即满足第三范式（3NF）必须满足第二范式（2NF）。\n例如：存在一个部门信息表，其中每个部门有部门编号（dept_id）、部门名称、部门简介等信息。那么在员工信息表中列出部门编号后就不能再将部门名称、部门简介等与部门有关的信息再加入员工信息表中。如果不存在部门信息表，则根据第三范式（3NF）也应该构建它，否则就会有大量的数据冗余。\n简而言之，第三范式就是属性不依赖于其它非主键属性，也就是在满足 2NF 的基础上，任何非主属性不得传递依赖于主属性。\n像：a -\u0026gt; b -\u0026gt; c 属性之间含有这样的关系，是不符合第三范式的。\n4.4. 范式说明 真正的数据库范式定义上，相当难懂，比如第二范式（2NF）的定义“若某关系 R 属于第一范式，且每一个非主属性完全函数依赖于任何一个候选码，则关系 R 属于第二范式。”，这里面有着大堆专业术语的堆叠，比如“函数依赖”、“码”、“非主属性”、与“完全函数依赖”等等，而且有完备的公式定义，深入研究可参考书籍：《数据库系统概念（第5版）》\n4.5. 反范式设计 4.5.1. 反范式化设计概念 反范式化就是为了性能和读取效率得考虑，而适当得对数据库设计范式得要求进行违反，允许存在少量得冗余。即反范式化就是使用空间来换取时间。\n4.5.2. 反范式实际应用示例 4.5.2.1. 性能提升-缓存和汇总 最常见的反范式化数据的方法是复制或者缓存，在不同的表中存储相同的特定列。\n缓存衍生值也是有用的。如果需要显示每个用户发了多少消息，可以每次执行一个对用户发送消息进行count的子查询来计算并显示它，也可以在user表用户中建一个消息发送数目的专门列，每当用户发新消息时更新这个值。\n有需要时创建一张完全独立的汇总表或缓存表也是提升性能的好办法。“缓存表”来表示存储那些可以比较简单地从其他表获取（但是每次获取的速度比较慢）数据的表（例如，逻辑上冗余的数据)。而“汇总表”时,则保存的是使用GROUP BY语句聚合数据的表。\n在使用缓存表和汇总表时，有个关键点是如何维护缓存表和汇总表中的数据，常用的有两种方式，实时维护数据和定期重建，这个取决于应用程序，不过一般来说，缓存表用实时维护数据更多点，往往在一个事务中同时更新数据本表和缓存表，汇总表则用定期重建更多，使用定时任务对汇总表进行更新。\n4.5.2.2. 性能提升-计数器表 比如网站点击数、用户的朋友数、文件下载次数等。对于高并发下的处理，首先可以创建一张独立的表存储计数器，这样可使计数器表小且快，并且可以使用一些更高级的技巧。\n比如假设有一个计数器表，只有一行数据，记录网站的点击次数，网站的每次点击都会导致对计数器进行更新，问题在于，对于任何想要更新这一行的事务来说，这条记录上都有一个全局的互斥锁(mutex)。这会使得这些事务只能串行执行，会严重限制系统的并发能力。\n改进方案：可以将计数器保存在多行中，每次随机选择一行进行更新。在具体实现上，可以增加一个槽（slot)字段，然后预先在这张表增加 100 行或者更多数据，当对计数器更新时，选择一个随机的槽（slot)进行更新即可。\n这种解决思路其实就是写热点的分散，在 JDK 的 JDK1.8 中新的原子类LongAdder也是这种处理方式，而在实际的缓冲中间件Redis等的使用、架构设计中，可以采用这种写热点的分散的方式，当然架构设计中对于写热点还有削峰填谷的处理方式，这种在 MySQL 的实现中也有体现\n4.5.2.3. 反范式设计-分库分表中的查询 例如，用户购买了商品，需要将交易记录保存下来，那么如果按照买家的纬度分表，则每个买家的交易记录都被保存在同一表中，这样可以很快、很方便地査到某个买家的购买情况，但是某个商品被购买的交易数据很有可能分布在多张表中，査找起来比较麻烦。反之，按照商品维度分表，则可以很方便地査找到该商品的购买情况，但若要査找到买家的交易记录，则会比较麻烦\n常见的解决方式如下：\n在多个分片表查询后合并数据集，这种方式的效率很低 记录两份数据，一份按照买家纬度分表，一份按照商品维度分表 通过搜索引擎解决，但如果实时性要求很高，就需要实现实时搜索 在某电商交易平台下，可能有买家査询自己在某一时间段的订单，也可能有卖家査询自已在某一时间段的订单，如果使用了分库分表方案，则这两个需求是难以满足的\n因此通用的解决方案是，在交易生成时生成一份按照买家分片的数据副本和一份按照卖家分片的数据副本，查询时分别满足之前的两个需求，因此，查询的数据和交易的数据可能是分别存储的，并从不同的系统提供接口\n4.6. 数据库设计小结 三大范式只是一般设计数据库的基本理念，可以建立冗余较小、结构合理的数据库。\n如果有特殊情况，当然要特殊对待，数据库设计最重要的是看需求跟性能，需求 \u0026gt; 性能 \u0026gt; 表结构。所以不能一味的去追求范式建立数据库。\n1NF:字段不可分（原子性 字段不可再分）; 2NF:有主键，非主键字段依赖主键（唯一性 一个表只说明一个事物）; 3NF:非主键字段不能相互依赖（每列都与主键有直接关系，不存在传递依赖）; 范式化设计优点：\n范式化的更新操作通常比反范式化要快 当数据较好地范式化时，就只有很少或者没有重复数据，所以只需要修改更少的数据 范式化的表通常更小，可以更好地放在内存里，所以执行操作会更快 很少有多余的数据意味着检索列表数据时更少需要DISTINCT或者GROUP BY语句。在非范式化的结构中必须使用DISTINCT或者GROUP BY才能获得一份唯一的列表，但是如果是一张单独的表，很可能则只需要简单的查询这张表就行了 范式化设计缺点：\n通常需要关联表查询。稍微复杂一些的查询语句在符合范式的表上都可能需要至少一次关联，也许更多 可能使一些索引策略无效 反范式化设计优点：\n反范式设计可以减少表的关联 可以更好的进行索引优化 反范式化设计缺点：\n存在数据冗余及数据维护异常 对数据的修改需要更多的成本 5. 关系型数据库、表、字段等命名规则 5.1. 数据库命名规则 根据项目的实际意思来命名。\n5.2. 数据表命名规则 数据表的命名大部分都是以名词的复数形式并且都为小写； 尽量使用前缀 table_； 如果数据表的表名是由多个单词组成，则尽量用下划线连接起来；但是不要超过30个字符，一旦超过30个字符，则使用缩写来缩短表名的长度； 具备统一前缀，对相关功能的表应当使用相同前缀，如 acl_xxx，house_xxx，ppc_xxx；其中前缀通常为这个表的模块或依赖主实体对象的名字，通常来讲表名为：业务_动作_类型，或是业务_类型； 数据表必须有主键，且建议均使用 auto_increment 的 id 作为主键（与业务无关），和业务相关的要做为唯一索引； 5.3. 字段命名规则 首先命名字段尽量采用小写，并且是采用有意义的单词； 使用前缀，前缀尽量用 表的前四个字母+下划线 组成\u0026quot;； 如果字段名由多个单词组成，则使用下划线来进行连接，一旦超过30个字符，则用缩写来缩短字段名的长度； 5.4. 视图命名规则 尽量使用前缀 view_； 如果创建的视图牵扯多张数据表，则一定列出所有表名，如果长度超过30个字符时可以简化表名，中间用下划线来连接； 5.5. 主键命名规则 主键用 pk_ 开头，后面跟上该主键所在的表名； 不能超过30个字符，尽量使用小写英文单词； 6. 参考资源 Database Series（数据库·实践笔记） - 深入浅出数据库存储：数据库理论、关系型数据库、文档型数据库、键值型数据库、New SQL、搜索引擎、数据仓库与 OLAP、大数据与数据中台。 ","permalink":"https://ktzxy.top/posts/mfz69u6kow/","summary":"数据库概述","title":"数据库概述"},{"content":" 索引是提高查询性能最有效的方式之一，在表结构设计阶段就应当考虑索引的设计，索引也不是越多越好，需要结合具体的SQL、执行频率、数据分布等多个方面综合考虑。本文整理了MySQL索引优化的一些原则、经验和技巧。\nMySQL的索引实现因存储引擎的差异而略有不同，本文主要介绍InnoDB存储引擎的索引优化。\n一、查看索引信息 查看表中有哪些索引，比如主键索引，唯一索引，普通索引等，在表结构中就能看到，如下命令： show create table table_name\\G\n想要看到索引的更多信息，比如索引的基数，索引的类型等，执行如下命令： show index from table_name;\n二、判断SQL有没有使用索引 MySQL提供explain命令查看SQL的执行计划，通过执行计划能够判断SQL在执行过程中是否使用索引。比如： explain select * from table_name where name = \u0026lsquo;xxx\u0026rsquo;;\n根据explain的输出结果key字段来判断是否使用索引，以及使用了哪个索引。\n三、索引分类 （1）主键索引 primary key 主键索引不允许有空值(null)，一张表只有一个主键，MySQL InnoDB表是索引组织表，所有的数据都在主键索引的叶子节点中。\n如果用户没有定义主键，那么MySQL会选择一个非空唯一索引作为主键，如果没有非空唯一索引，那么MySQL会创建一个隐式的主键。\n（2）唯一键索引 unique key 唯一索引的值必须唯一，不允许重复，但可以为空值(null)，一张表中可以创建多个唯一索引。\n（3）普通索引 普通的B+树索引，可以有多个，允许重复值，也允许为空值(null)。\n以上三种索引还可以根据索引包含的字段数量来划分，如下： （1）单列索引，索引字段只有一个。 （2）组合索引，索引字段有多个，常用于多个条件查询，以及形成覆盖索引，避免回表。\n四、索引优化的原则 一定要有主键索引，主键索引字段尽可能的小，最好自增，比如 int auto_increment。 主键索引不建议使用md5，uuid这类无序字符串，插入数据会频繁页分裂，影响性能，并且磁盘占用过大。 适合索引的列通常是出现在where条件中的列，或者join连接中的连接字段。 建索引的列，不建议为null值。 区分度不高的字段不适合建索引，比如性别。 组合索引建议将区分度高的字段放到前面，区分度低的字段放在后面。 建议使用短索引，对于长字符串，如果其前N个字符已经有很好的区分度，可以指定前缀索引，既能优化查询，也避免了索引过大。 不要过度索引，维护索引也是有成本的，不仅占用磁盘空间，还影响写入性能。 更新频繁的字段，不建议建索引。更新操作会变更索引，影响性能。 不要创建冗余和重复的索引。 单表索引建议控制在5个以内。 组合索引字段数建议不超过5个。字段超过5个时，实际已经起不到有效过滤数据的作用了。 五、覆盖索引 覆盖索引不是索引类型，它只是使用索引的一种方式，只读取索引数据就能返回结果给用户，不需要再回表查询。\n覆盖索引优点：\n利用索引直接返回数据，不再回表查询 利用索引的有序性，避免不必要的排序 注意：使用前缀索引，将导致覆盖索引不可用。\n六、索引使用情况监控 可以通过查看MySQL的状态变量来确认索引的使用情况： show status like \u0026lsquo;Handler_read%\u0026rsquo;;\nHandler_read_key：如果索引正常工作，该值将很高。 Handler_read_rnd_next：数据文件中读取下一行的请求数，如果正在进行大量的表扫描，该值较高，说明索引利用不理想。 ","permalink":"https://ktzxy.top/posts/384cifvuwi/","summary":"MySQL性能优化 索引优化","title":"MySQL性能优化 索引优化"},{"content":"Go的切片 为什么要使用切片 切片（Slice）是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。 它非常灵活，支持自动扩容。\n切片是一个引用类型，它的内部结构包含地址、长度和容量。\n声明切片类型的基本语法如下：\n1 var name [] T 其中：\nname：表示变量名 T：表示切片中的元素类型 举例\n1 2 3 // 声明切片，把长度去除就是切片 var slice = []int{1,2,3} fmt.Println(slice) 关于nil的认识 当你声明了一个变量，但却还并没有赋值时，golang中会自动给你的变量赋值一个默认的零值。这是每种类型对应的零值。\nbool：false numbers：0 string：\u0026quot;\u0026quot; pointers：nil slices：nil maps：nil channels：nil functions：nil nil表示空，也就是数组初始化的默认值就是nil\n1 2 var slice2 [] int fmt.Println(slice2 == nil) 运行结果\n1 true 切片的遍历 切片的遍历和数组是一样的\n1 2 3 4 var slice = []int{1,2,3} for i := 0; i \u0026lt; len(slice); i++ { fmt.Print(slice[i], \u0026#34; \u0026#34;) } 基于数组定义切片 由于切片的底层就是一个数组，所以我们可以基于数组来定义切片\n1 2 3 4 5 6 7 8 9 10 // 基于数组定义切片 a := [5]int {55,56,57,58,59} // 获取数组所有值，返回的是一个切片 b := a[:] // 从数组获取指定的切片 c := a[1:4] // 获取 下标3之前的数据（不包括3） d := a[:3] // 获取下标3以后的数据（包括3） e := a[3:] 运行结果\n1 2 3 4 5 [55 56 57 58 59] [55 56 57 58 59] [56 57 58] [55 56 57] [58 59] 同理，我们不仅可以对数组进行切片，还可以切片在切片\n切片的长度和容量 切片拥有自己的长度和容量，我们可以通过使用内置的len）函数求长度，使用内置的cap（） 函数求切片的容量。\n切片的长度就是它所包含的元素个数。\n切片的容量是从它的第一个元素开始数，到其底层数组元素末尾的个数。切片s的长度和容量可通过表达式len（s）和cap（s）来获取。\n举例\n1 2 3 4 5 6 7 8 9 // 长度和容量 s := []int {2,3,5,7,11,13} fmt.Printf(\u0026#34;长度%d 容量%d\\n\u0026#34;, len(s), cap(s)) ss := s[2:] fmt.Printf(\u0026#34;长度%d 容量%d\\n\u0026#34;, len(ss), cap(ss)) sss := s[2:4] fmt.Printf(\u0026#34;长度%d 容量%d\\n\u0026#34;, len(sss), cap(sss)) 运行结果\n1 2 3 长度6 容量6 长度4 容量4 长度2 容量4 为什么最后一个容量不一样呢，因为我们知道，经过切片后sss = [5, 7] 所以切片的长度为2，但是一因为容量是从2的位置一直到末尾，所以为4\n切片的本质 切片的本质就是对底层数组的封装，它包含了三个信息\n底层数组的指针 切片的长度(len) 切片的容量(cap) 举个例子，现在有一个数组 a := [8]int {0,1,2,3,4,5,6,7}，切片 s1 := a[:5]，相应示意图如下\n切片 s2 := a[3:6]，相应示意图如下：\n使用make函数构造切片 我们上面都是基于数组来创建切片的，如果需要动态的创建一个切片，我们就需要使用内置的make函数，格式如下：\n1 make ([]T, size, cap) 其中：\nT：切片的元素类型 size：切片中元素的数量 cap：切片的容量 举例：\n1 2 3 4 5 6 7 // make()函数创建切片 fmt.Println() var slices = make([]int, 4, 8) //[0 0 0 0] fmt.Println(slices) // 长度：4, 容量8 fmt.Printf(\u0026#34;长度：%d, 容量%d\u0026#34;, len(slices), cap(slices)) 需要注意的是，golang中没办法通过下标来给切片扩容，如果需要扩容，需要用到append\n1 2 3 4 slices2 := []int{1,2,3,4} slices2 = append(slices2, 5) fmt.Println(slices2) // 输出结果 [1 2 3 4 5] 同时切片还可以将两个切片进行合并\n1 2 3 4 5 // 合并切片 slices3 := []int{6,7,8} slices2 = append(slices2, slices3...) fmt.Println(slices2) // 输出结果 [1 2 3 4 5 6 7 8] 需要注意的是，切片会有一个扩容操作，当元素存放不下的时候，会将原来的容量扩大两倍\n使用copy()函数复制切片 前面我们知道，切片就是引用数据类型\n值类型：改变变量副本的时候，不会改变变量本身 引用类型：改变变量副本值的时候，会改变变量本身的值 如果我们需要改变切片的值，同时又不想影响到原来的切片，那么就需要用到copy函数\n1 2 3 4 5 6 7 8 9 10 // 需要复制的切片 var slices4 = []int{1,2,3,4} // 使用make函数创建一个切片 var slices5 = make([]int, len(slices4), len(slices4)) // 拷贝切片的值 copy(slices5, slices4) // 修改切片 slices5[0] = 4 fmt.Println(slices4) fmt.Println(slices5) 运行结果为\n1 2 [1 2 3 4] [4 2 3 4] 删除切片中的值 Go语言中并没有删除切片元素的专用方法，我们可以利用切片本身的特性来删除元素。代码如下\n1 2 3 4 5 // 删除切片中的值 var slices6 = []int {0,1,2,3,4,5,6,7,8,9} // 删除下标为1的值 slices6 = append(slices6[:1], slices6[2:]...) fmt.Println(slices6) 运行结果\n1 [0 2 3 4 5 6 7 8 9] 切片的排序算法以及sort包 编写一个简单的冒泡排序算法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func main() { var numSlice = []int{9,8,7,6,5,4} for i := 0; i \u0026lt; len(numSlice); i++ { flag := false for j := 0; j \u0026lt; len(numSlice) - i - 1; j++ { if numSlice[j] \u0026gt; numSlice[j+1] { var temp = numSlice[j+1] numSlice[j+1] = numSlice[j] numSlice[j] = temp flag = true } } if !flag { break } } fmt.Println(numSlice) } 在来一个选择排序\n1 2 3 4 5 6 7 8 9 10 11 12 // 编写选择排序 var numSlice2 = []int{9,8,7,6,5,4} for i := 0; i \u0026lt; len(numSlice2); i++ { for j := i + 1; j \u0026lt; len(numSlice2); j++ { if numSlice2[i] \u0026gt; numSlice2[j] { var temp = numSlice2[i] numSlice2[i] = numSlice2[j] numSlice2[j] = temp } } } fmt.Println(numSlice2) 对于int、float64 和 string数组或是切片的排序，go分别提供了sort.Ints()、sort.Float64s() 和 sort.Strings()函数，默认都是从小到大进行排序\n1 2 3 var numSlice2 = []int{9,8,7,6,5,4} sort.Ints(numSlice2) fmt.Println(numSlice2) 降序排列 Golang的sort包可以使用 sort.Reverse(slic e) 来调换slice.Interface.Less，也就是比较函数，所以int、float64 和 string的逆序排序函数可以这样写\n1 2 3 4 // 逆序排列 var numSlice4 = []int{9,8,4,5,1,7} sort.Sort(sort.Reverse(sort.IntSlice(numSlice4))) fmt.Println(numSlice4) ","permalink":"https://ktzxy.top/posts/yg2c5ygxbi/","summary":"7 Go的切片","title":"7 Go的切片"},{"content":"Go中的指针 要搞明白Go语言中的指针需要先知道三个概念\n指针地址 指针类型 指针取值 Go语言中的指针操作非常简单，我们只需要记住两个符号：\u0026amp;：取地址，*：根据地址取值\n关于指针 我们知道变量是用来存储数据的，变量的本质是给存储数据的内存地址起了一个好记的别名。比如我们定义了一个变量a:=10，这个时候可以直接通过a这个变量来读取内存中保存的10这个值。在计算机底层a这个变量其实对应了一个内存地址。\n指针也是一个变量，但它是一种特殊的变量，它存储的数据不是一个普通的值，而是另一个变量的内存地址。\n指针地址和指针类型 每个变量在运行时都拥有一个地址，这个地址代表变量在内存中的位置。Go 语言中使用\u0026amp;字符放在变量前面对变量进行取地址操作。Go语言中的值类型（int、float、bool、string、array、struct）都有对应的指针类型，如：\n1 *int、，*int64、*string等 取变量指针的语法如下：\n1 ptr := \u0026amp;v 其中：\nv：代表被取地址的变量，类型为T ptr：用于接收地址的变量，ptr的类型就为*T，被称做T的指针类型。* 代表指针 举个例子：\n指针取值 在对普通变量进行\u0026amp;操作符取地址后，会获得这个变量指针，然后可以对指针使用*操作，也就是指针取值\n1 2 3 4 5 6 7 8 9 10 11 12 // 指针取值 var c = 20 // 得到c的地址，赋值给d var d = \u0026amp;c // 打印d的值，也就是c的地址 fmt.Println(d) // 取出d指针所对应的值 fmt.Println(*d) // c对应地址的值，改成30 *d = 30 // c已经变成30了 fmt.Println(c) 改变内存中的值，会直接改变原来的变量值\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 这个类似于值传递 func fn4(x int) { x = 10 } // 这个类似于引用数据类型 func fn5(x *int) { *x = 20 } func main() { x := 5 fn4(x) fmt.Println(x) fn5(\u0026amp;x) fmt.Println(x) } 我们创建了两个方法，一个是传入局部变量，一个是传入指针类型，最后运行得到的结果\n1 2 5 20 new和make函数 需要注意的是，指针必须在创建内存后才可以使用，这个和 slice 和 map是一样的\n1 2 3 4 5 6 7 8 // 引用数据类型map、slice等，必须使用make分配空间，才能够使用 var userInfo = make(map[string]string) userInfo[\u0026#34;userName\u0026#34;] = \u0026#34;zhangsan\u0026#34; fmt.Println(userInfo) var array = make([]int, 4, 4) array[0] = 1 fmt.Println(array) 对于指针变量来说\n1 2 3 4 // 指针变量初始化 var a *int *a = 100 fmt.Println(a) 执行上面的代码会引发panic，为什么呢？在Go语言中对于引用类型的变量，我们在使用的时候不仅要声明它，还要为它分配内存空间，否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间，是因为它们在声明的时候已经默认分配好了内存空间。要分配内存，就引出来今天的new和make。Go 语言中new和make是内建的两个函数，主要用来分配内存。\n这个时候，我们就需要使用new关键字来分配内存，new是一个内置的函数，它的函数签名如下：\n1 func new(Type) *Type 其中\nType表示类型，new函数只接受一个参数，这个参数是一个类型 *Type表示类型指针，new函数返回一个指向该类型内存地址的指针 实际开发中new函数不太常用，使用new函数得到的是一个类型的指针，并且该指针对应的值为该类型的零值。举个例子：\n1 2 3 4 5 6 7 // 使用new关键字创建指针 aPoint := new(int) bPoint := new(bool) fmt.Printf(\u0026#34;%T \\n\u0026#34;, aPoint) fmt.Printf(\u0026#34;%T \\n\u0026#34;, bPoint) fmt.Println(*aPoint) fmt.Println(*bPoint) 本节开始的示例代码中 var a *int 只是声明了一个指针变量a但是没有初始化，指针作为引用类型需要初始化后才会拥有内存空间，才可以给它赋值。应该按照如下方式使用内置的\nmake和new的区别 两者都是用来做内存分配的 make只能用于slice、map以及channel的初始化，返回的还是这三个引用类型的本身 而new用于类型的内存分配，并且内存赌赢的值为类型的零值，返回的是指向类型的指针 ","permalink":"https://ktzxy.top/posts/8nf1kpam98/","summary":"11 Go中的指针","title":"11 Go中的指针"},{"content":"1. Date 类 (在util包中) 通过该类可以获得当前的日期和时间。\n1.1. Date 构造方法 1 public Date() 获得当前的系统时间，直接输出对象的结果是系统默认的显示格式。 1 public Date(long date) 根据指定的毫秒值创建日期对象，返回指定毫秒值的日期对象，(从时间零点到指定时间为止) 1.2. Date常用成员方法 1 public long getTime(); 获得当前时间的毫秒值 (从时间零点到当前时间之间共多少毫秒) 毫秒的概念： 1秒 = 1000毫秒 时间零点：1970年1月1日 00:00:00 1 public void setTime(long time); 将时间设置到指定的毫秒值上 2. DateFormat 类 / SimpleDateFormat 子类 2.1. DateFormat概念 DateFormat:日期格式化类；\n是一个抽象类，不能直接创建对象。 可以使用其子类SimpleDateFormat来创建对象。重写了DateFormat所有抽象方法。 SimpleDateFormat:实际使用的日期格式化子类；\n子类SimpleDateFormat 用来格式化日期类对象，可以将日期对象格式化成字符串。 创建DateFormat的对象（多态），示例：\n无参：SimpleDateFormat sdf = new SimpleDateFormat(); 有参：SimpleDateFormat sdf = new SimpleDateFormat(\u0026quot;yyyy-MM-dd HH:mm:ss\u0026quot;); 2.2. SimpleDateFormat构造方法 1 public SimpleDateFormat() 创建使用默认日期格式的对象(创建对象输出的时间是系统默认格式) 1 public SimpleDateFormat(String pattern) 根据指定的时间模式创建日期格式对象（传入的格式是有规范的） 常用时间模式，如：yyyy-MM-dd HH:mm:ss 年-月-日 时:分:秒 格式规范列表 字母 日期或时间元素 表示 示例 G Era 标志符 Text AD y 年 Year 1996; 96 M 年中的月份 Month July; Jul; 07 w 年中的周数 Number 27 W 月份中的周数 Number 2 D 年中的天数 Number 189 d 月份中的天数 Number 10 F 月份中的星期 Number 2 E 星期中的天数 Text Tuesday; Tue a Am/pm 标记 Text PM H 一天中的小时数（0-23） Number 0 k 一天中的小时数（1-24） Number 24 K am/pm 中的小时数（0-11） Number 0 h am/pm 中的小时数（1-12） Number 12 m 小时中的分钟数 Number 30 s 分钟中的秒数 Number 55 S 毫秒数 Number 978 z 时区 General time zone Pacific Standard Time; PST; GMT-08:00 Z 时区 RFC 822 time zone -0800 2.3. SimpleDateFormat常用方法和使用步骤 2.3.1. 日期对象格式化成字符串 1 public final String format(Date date) 将Date日期对象格式化为指定格式的日期/时间字符串**(这个方法是重写父类DateFormal的方法)**，输出的结果示例：2017-10-12 12:12:12 使用步骤： 创建SimpleDateFormat对象并指定日期模式 调用format()方法传入日期对象获得字符串 日期对象格式化成字符串案例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.text.SimpleDateFormat; import java.util.Date; public class Test09 { public static void main(String[] args) { // 获取时间格式化对象 Date d = new Date(); SimpleDateFormat sdf = new SimpleDateFormat(\u0026#34;yyyy-MM-dd HH:mm:ss\u0026#34;); // 将时间格式化对象转成字符串输出 String time = sdf.format(d); // 将格式化后的时间字符串输出 System.out.println(time); } } 2.3.2. 将时间字符串转换成日期对象 1 public Date parse(String source) throws ParseException 将日期时间字符串转换成日期对象**(重写父类DateFormal的方法)**，示例：“2017-10-12” --\u0026gt; Date --\u0026gt; 运算 使用步骤： 创建SimpleDateFormat对象并指定日期模式 调用parse()方法传入时间字符串获得日期对象 注意事项：时间字符串的格式和创建格式化时指定的日期模式要一致，否则会转换失败抛出异常 时间字符串转换成日期对象示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class Test09 { public static void main(String[] args) throws ParseException { // 时间字符串 String timeStr = \u0026#34;2017-10-22 17:07:56\u0026#34;; // 时间字符串的格式和创建格式化时指定的日期模式要一致，否则会转换失败抛出异常。 SimpleDateFormat sdf = new SimpleDateFormat(\u0026#34;yyyy-MM-dd HH:mm:ss\u0026#34;); Date d = sdf.parse(timeStr); // 直接输出d，输出是系统默认的格式 ： Sun Oct 22 17:07:56 CST 2017 System.out.println(d); // 如果再次格式化成字符串再输出，可以输出原来的格式： 2017-10-22 17:07:56 System.out.println(sdf.format(sdf.parse(timeStr))); // 如果想输出其他的格式，只能新创建一个SimpleDateFormat对象，定义其他格式 // 这样输出：2017/10/22 17:07:56 SimpleDateFormat sdf2 = new SimpleDateFormat(\u0026#34;yyyy/MM/dd HH:mm:ss\u0026#34;); System.out.println(sdf2.format(sdf.parse(timeStr))); // 注意：上面format()方法里的用sdf调用parse()方法，因为要与时间字符串的格式一致！ } } 1 public void applyPattern(String pattern) 将给定模式字符串应用于此日期格式。（此方法用于SimpleDateFormal创建对象的时候没有指定格式，使用这方法可以给对象指定日期格式。） 2.4. SimpleDateFormat存在问题与解决方案 2.4.1. 存在线程安全问题 SimpleDateFormat 并不是一个线程安全的类。在多线程情况下，会出现异常\n一般使用 SimpleDateFormat 的时候会把它定义为一个静态变量，避免频繁创建它的对象实例。因为把 SimpleDateFormat 定义为静态变量，那么多线程下 SimpleDateFormat 的实例就会被多个线程共享，B线程会读取到A线程的时间，就会出现时间差异和其它各种问题。SimpleDateFormat 和它继承的 DateFormat 类也不是线程安全的。\n通常使用 SimpleDateFormat，下面是一个常见的日期工具类。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.moon.test; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public final class DateUtils { public static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat(\u0026#34;yyyy-MM-dd\u0026#34;); private DateUtils() { } public static Date parse(String target) { try { return SIMPLE_DATE_FORMAT.parse(target); } catch (ParseException e) { e.printStackTrace(); } return null; } public static String format(Date target) { return SIMPLE_DATE_FORMAT.format(target); } } 使用单线程运行程序，输出的结果正确 1 2 3 4 5 6 7 8 9 10 11 12 13 package com.moon.test; public class TestSimpleDateFormat { public static void main(String[] args) { testSimpleDateFormatInSingleThread(); // 输出的结果：Mon Jul 29 00:00:00 CST 2019 } private static void testSimpleDateFormatInSingleThread() { final String source = \u0026#34;2019-01-11\u0026#34;; System.out.println(DateUtils.parse(source)); } } 使用多线程运行程序，输出结果就出现问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.moon.test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.IntStream; public class TestSimpleDateFormat { public static void main(String[] args) { testSimpleDateFormatWithThreads(); } private static void testSimpleDateFormatWithThreads() { ExecutorService executorService = Executors.newFixedThreadPool(10); final String source = \u0026#34;2019-07-29\u0026#34;; System.out.println(\u0026#34;:: parsing date string ::\u0026#34;); IntStream.rangeClosed(0, 20) .forEach((i) -\u0026gt; executorService.submit(() -\u0026gt; System.out.println(DateUtils.parse(source)))); executorService.shutdown(); } } 输出结果\n1 2 3 4 5 6 :: parsing date string :: Thu Jul 29 00:00:00 CST 41920 Mon Jul 29 00:00:00 CST 2019 Sun Jul 29 00:00:00 CST 7201 Mon Jul 29 00:00:00 CST 2019 Mon Jul 29 00:00:00 CST 2019 出现这种情况就是因为没有考虑到线程安全，以下是 Java 文档有关 SimpleDateFormat 的描述：\n“日期格式是非同步的。建议为每个线程创建单独的日期格式化实例。如果多个线程并发访问某个格式化实例，则必须保证外部调用同步性。”\n提示：使用实例变量时，应该每次检查这个类是不是线程安全。\n2.4.2. 解决方案1：ThreadLocal 可以使用 ThreadLocal 解决。Threadlocal 的 get() 方法会给当前线程提供正确的值。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.moon.test; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public final class DateUtilsThreadLocal { public static final ThreadLocal SIMPLE_DATE_FORMAT = ThreadLocal .withInitial(() -\u0026gt; new SimpleDateFormat(\u0026#34;yyyy-MM-dd\u0026#34;)); private DateUtilsThreadLocal() { } public static Date parse(String target) { try { return ((DateFormat) SIMPLE_DATE_FORMAT.get()).parse(target); } catch (ParseException e) { e.printStackTrace(); } return null; } public static String format(Date target) { return ((DateFormat) SIMPLE_DATE_FORMAT.get()).format(target); } } 使用多线程测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.moon.test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.IntStream; public class TestSimpleDateFormat { public static void main(String[] args) { testSimpleDateFormatThreadLocalWithThreads(); } private static void testSimpleDateFormatThreadLocalWithThreads() { ExecutorService executorService = Executors.newFixedThreadPool(10); final String source = \u0026#34;2019-07-29\u0026#34;; System.out.println(\u0026#34;:: parsing date string ::\u0026#34;); IntStream.rangeClosed(0, 10) .forEach((i) -\u0026gt; executorService.submit(() -\u0026gt; System.out.println(DateUtilsThreadLocal.parse(source)))); executorService.shutdown(); } } 输出结果正确 1 2 3 4 5 6 7 8 9 10 11 12 :: parsing date string :: Mon Jul 29 00:00:00 CST 2019 Mon Jul 29 00:00:00 CST 2019 Mon Jul 29 00:00:00 CST 2019 Mon Jul 29 00:00:00 CST 2019 Mon Jul 29 00:00:00 CST 2019 Mon Jul 29 00:00:00 CST 2019 Mon Jul 29 00:00:00 CST 2019 Mon Jul 29 00:00:00 CST 2019 Mon Jul 29 00:00:00 CST 2019 Mon Jul 29 00:00:00 CST 2019 Mon Jul 29 00:00:00 CST 2019 2.4.3. 解决方案2：Java 8 线程安全的时间日期 API（推荐） Java8 引入了新的日期时间 API，SimpleDateFormat 有了更好的替代者。如果继续坚持使用 SimpleDateFormat 可以配合 ThreadLocal 一起使用。也可以使用新的 API Java 8 提供了几个线程安全的日期类，包括 DateTimeFormatter、OffsetDateTime、ZonedDateTime、LocalDateTime、LocalDate 和 LocalTime。Java 文档中这么描述： 这个类是具有不可变和线程安全的特点。\n使用新的API，DateTimeFormatter类与LocalDate类改造示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.moon.test; import java.time.LocalDate; import java.time.format.DateTimeFormatter; public class DateUtilsJava8 { public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(\u0026#34;yyyy-MM-dd\u0026#34;); private DateUtilsJava8() { } public static LocalDate parse(String target) { return LocalDate.parse(target, DATE_TIME_FORMATTER); } public static String format(LocalDate target) { return target.format(DATE_TIME_FORMATTER); } } 使用多线程测试\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.moon.test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.stream.IntStream; public class TestSimpleDateFormat { public static void main(String[] args) { testDateTimeFormatterWithThreads(); } private static void testDateTimeFormatterWithThreads() { ExecutorService executorService = Executors.newFixedThreadPool(10); final String source = \u0026#34;2019-07-29\u0026#34;; System.out.println(\u0026#34;:: parsing date string ::\u0026#34;); IntStream.rangeClosed(0, 10) .forEach((i) -\u0026gt; executorService.submit(() -\u0026gt; System.out.println(DateUtilsJava8.parse(source)))); executorService.shutdown();d } } 程序输出结果\n1 2 3 4 5 6 7 8 9 10 11 12 :: parsing date string :: 2019-07-29 2019-07-29 2019-07-29 2019-07-29 2019-07-29 2019-07-29 2019-07-29 2019-07-29 2019-07-29 2019-07-29 2019-07-29 2.4.4. 解决方案3：使用局部变量 将 SimpleDateFormat 变成了局部变量，就不会被多个线程同时访问到了，就避免了线程安全问题。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ExecutorService executorService = Executors.newFixedThreadPool(10); final String source = \u0026#34;2019-07-29 08:11:20\u0026#34;; System.out.println(\u0026#34;:: parsing date string ::\u0026#34;); IntStream.rangeClosed(0, 10) .forEach((i) -\u0026gt; { // SimpleDateFormat 声明成局部变量 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(\u0026#34;yyyy-MM-dd HH:mm:ss\u0026#34;); executorService.submit(() -\u0026gt; { try { System.out.println(simpleDateFormat.parse(source)); } catch (ParseException e) { throw new RuntimeException(e); } }); }); executorService.shutdown(); 2.4.5. 解决方案4：加同步锁 对于 SimpleDateFormat 共享变量进行加锁。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat(\u0026#34;yyyy-MM-dd HH:mm:ss\u0026#34;); public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); final String source = \u0026#34;2019-07-29 07:08:11\u0026#34;; System.out.println(\u0026#34;:: parsing date string ::\u0026#34;); IntStream.rangeClosed(0, 10) .forEach((i) -\u0026gt; { executorService.submit(() -\u0026gt; { // 加锁 synchronized (simpleDateFormat) { try { System.out.println(simpleDateFormat.parse(source)); } catch (ParseException e) { throw new RuntimeException(e); } } }); }); executorService.shutdown(); } 3. Calendar 类 3.1. Calendar 概述 Calendar是一个日历类，抽象类；不能直接创建对象\n3.2. Calendar 日历类对象创建方法 1 public static Calendar getInstance() 通过Calendar类的静态方法获取日历类对象 Calendar一般不通过子类new对象，而通过Calendar类的静态方法获得该类对象。此方法相当于new GregorianCalendar(); 创建 Calendar 对象，不使用构造方法，使用以下方法，支持语言敏感的问题，静态方法 getInstance，获取当前时间 注：返回值是Calendar对象，因为Calendar是抽象类，不能new对象，返回值的应该是它的子类对象，只是用父类Calendar来接收，这里使用多态的特性 一般一些抽象类都有提供getInstance()方法，返回自己类型的对象 3.3. Calendar 常用成员方法 1 public final Date getTime() 获得日期对象，返回一个表示此 Calendar 时间值（从历元至现在的毫秒偏移量） 1 public long getTimeInMillis() 返回此 Calendar 的时间值，以毫秒为单位。（时间零点至当前时间） 1 public int get(int field); 根据指定的日历字段值获得对应的值。 注意事项： 月份是0至11，获得的月份需要+1才是得到我们实际的月份。 DAY_OF_WEEK是从星期日开始算起，如果是星期日的话，输出是“0”； 1 public void set(int field, int value); 设置指定日历字段的值。 1 public final void set(int year, int month, int date); 设置日历字段 YEAR、MONTH 和 DAY_OF_MONTH 的值 field是字段值，Calendar类中很多字段值用是static修饰，可以直接用类名.调用。取值：Calendar.YEAR; Calendar.MONTH; Calendar.DAY_OF_MONTH(DATE); 注：get方法相当获得目前的时间，通过set方法可以相当将日历设置到我们需要的日期，再通过get的方法获取值。 如果设置的字段值超出正常的范围，系统会自动将日期向前偏移。 1 public abstract void add(int field, int amount); 将指定日历字段的值在当前的基础上偏移指定的值（整数向后偏移，负数向前偏移） 根据日历的规则，为给定的日历字段添加或减去指定的时间量。例如，要从当前日历时间减去5天，可以通过调用以下方法做到这一点：add(Calendar.DAY_OF_MONTH, -5)。 3.4. Calendar 使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import java.util.Calendar; import java.util.Date; public class Test09 { public static void main(String[] args) { // 创建日历类对象,获取当前时间 Calendar c = Calendar.getInstance(); // 直接打印效果： System.out.println(c); System.out.println(\u0026#34;===================\u0026#34;); // Calendar转成 Date对象 Date time = c.getTime(); // 打印效果：Sun Oct 22 17:58:30 CST 2017 System.out.println(time); // 分别获取年、月、日的属性值 int year = c.get(Calendar.YEAR); int month = c.get(Calendar.MONTH); int day = c.get(Calendar.DAY_OF_MONTH); // 打印效果：2017年10月22日 System.out.println(year + \u0026#34;年\u0026#34; + (month + 1) + \u0026#34;月\u0026#34; + day + \u0026#34;日\u0026#34;); // 设置年月日,打印效果:Sun Jan 22 18:08:11 CST 2017 c.set(2017, 0, 22); System.out.println(c.getTime()); // 指定日历字段的值在当前的基础上偏移,打印效果：Fri Jan 22 18:10:20 CST 2016 c.add(Calendar.YEAR, -1); System.out.println(c.getTime()); } } 4. Math 工具类 4.1. 常用方法 1 public static int sqrt(double d); 返回 d 的算术平方根值（±√￣） 1 public static double pow(double a, double b); 返回 a 的 b 次幂的值,返回 double 形式 1 public static double floor(double d); 返回小于等于 d 的最大整数，返回该整数的小数形式 1 public static double ceil(double d); 返回大于等于 d 的最小整数，返回该整数的小数形式 1 public static int abs(int d); 返回 d 的绝对值 1 public static double random() 返回带正号的 double 值，返回0至1的随机小数，包括0不包括1； 本质也是通过Random类获取随机数。 1 public static long round(double d); 对 d 进行四舍五入，返回四舍五入后的值 注：Math 是一个数字工具类，该类提供了大量与数学运算相关的方法。工具类都是static方法，可以直接用类点调用。如果遇到算术有关的内容，可以到API查找相关的方法。如：求最大，最小值，求三角余弦等\n5. System 工具类 5.1. 常用方法 1 public static long currentTimeMillis(); 获得系统当前时间毫秒值。(跟之前Date类getTime()方法和Calendar类 getTimeInMillis()方法获得的结果是一样的) 一般用来测试代码的执行时间。 1 public static void exit(int status); 退出JVM，终止程序运行。参数用作状态码；根据惯例，非 0 的状态码表示异常终止。（0 表示正常退出。-1 表示异常退出） 1 public static void gc(); 运行垃圾回收器。通知垃圾回收器进行垃圾回收，垃圾回收器可能会回收，也可以不会回收。当对象被垃圾回收器回收时，系统会自动调用方法 protected void finalize() throws Throwable 1 public static Properties getProperties(); 获得操作系统的相关的属性值，比如操作系统的名字。 1 public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); 数组复制方法。参数列表解释如下： src：源数组 srcPos：源数组的起始位置 dest：目标数组 destPos：目标数组的起始位置 length：复制元素的个数 5.2. System 类使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class Test10 { public static void main(String[] args) { // 数组复制 // 源数组 int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 }; // 调用方法打印源数组 printArray(arr); System.out.println(\u0026#34;===============\u0026#34;); // 复制的数组 int[] arrCopy = new int[arr.length]; int[] arrCopy1 = new int[arr.length]; int[] arrCopy2 = new int[3]; // 使用System工具类的方法复制数组（全部复制） System.arraycopy(arr, 0, arrCopy, 0, arr.length); // 调用方法打印数组 printArray(arrCopy); // 使用System工具类的方法复制数组（复制部分,从第2个元素复制3个，从新数组索引1开始放）; // 输出结果是[0 3 4 5 0 0 0 0 0 0] System.arraycopy(arr, 2, arrCopy1, 1, 3); printArray(arrCopy1); // 使用System工具类的方法复制数组（复制3个元素,从索引6开始复制3个，放到新数组中） // 输出结果是[7 8 9] System.arraycopy(arr, 6, arrCopy2, 0, 3); printArray(arrCopy2); } public static void printArray(int[] arr) { // 增强式for打印数组 System.out.print(\u0026#34;数组：[\u0026#34;); for (int i : arr) { System.out.print(i + \u0026#34; \u0026#34;); } System.out.println(\u0026#34;]\u0026#34;); } } 6. Date 类（SQL包的，是util包下Date的子类） 1 public class Date extends Date 位置：java.sql 包 一个包装了毫秒值的瘦包装器，JDBC 将毫秒值标识为 SQL DATE 值 6.1. 构造方法 1 public Date(long date); 使用给定毫秒时间值构造一个 Date 对象。直接输出对象是一个包装过的对象，格式：yyyy-MM-dd\n6.2. 常用方法 1 public static Date valueOf(String s); sql包下的Date类的静态方法，将 JDBC 日期“yyyy-MM-dd”格式转义形式的字符串转换成 Date 值。 eg：Date date = Date.valueOf(\u0026quot;2017-10-10\u0026quot;); 获取一个Date对象，注意，但这个Date类继承了java.util包下的Date类，所以也可以使用util包的Date父类接收 1 public String toString(); 将日期对象格式化成 yyyy-mm-dd 的日期字符串 7. Time 类 1 public class Time extends Date 位置：java.sql 包 继承java.util包下的Date类，瘦包装器， 7.1. 构造方法 1 public Time(long time) 使用毫秒时间值构造 Time 对象。输出是经过包装的时间格式。输出对象是HH:mm:ss 7.2. 常用方法 1 public String toString() 使用 JDBC 时间转义格式对时间进行格式化。 1 public static Time valueOf(String s) 将使用 JDBC 时间转义格式的字符串转换为 Time 值。s使用 \u0026ldquo;hh:mm:ss\u0026rdquo; 格式的时间 7.3. sql包 Date和Time 示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 import java.sql.Time; import java.util.Date; public class MoonZero { public static void main(String[] args) { Date d = new Date(System.currentTimeMillis()); // 输出：Thu Nov 23 19:11:03 CST 2017 （util.Date） System.out.println(d); java.sql.Date d2 = new java.sql.Date(System.currentTimeMillis()); // 输出：2017-11-23(sql.Date) System.out.println(d2); java.sql.Date d3 = java.sql.Date.valueOf(\u0026#34;2011-11-11\u0026#34;); // 输出：2011-11-11(sql.Date) System.out.println(d3); Time t = new Time(System.currentTimeMillis()); // 输出：19:16:51（sql.Time） System.out.println(t); Time t2 = Time.valueOf(\u0026#34;19:30:30\u0026#34;); // 输出：19:30:30 System.out.println(t2); } } 8. Timestamp 时间戳 1 public class Timestamp extends Date 位置：java.sql 包 一个与 java.util.Date 类有关的瘦包装器，可以生成时间戳 8.1. 构造方法 1 public Timestamp(long time); 使用毫秒时间值构造 Timestamp 对象。 8.2. 常用方法 1 public static Timestamp valueOf(String s); 将使用 JDBC 时间戳转义格式的 String 对象转换为 Timestamp 值。 1 public String toString(); 使用 JDBC 时间戳转义格式编排时间戳。yyyy-mm-dd hh:mm:ss.fffffffff，其中 ffffffffff 指示毫微秒。 9. DecimalFormat 数字格式化类 1 public class DecimalFormat extends NumberFormat 位置：java.text 包 DecimalFormat 是 NumberFormat 的一个具体子类，用于格式化十进制数字。 9.1. 构造方法 1 public DecimalFormat(String pattern); 使用给定的模式和默认语言环境的符号创建一个 DecimalFormat。 参数：pattern - 一个非本地化的模式字符串。 传入一个字符串的格式。eg: new DecimalFormat(\u0026quot;￥#,###.00\u0026quot;) 9.2. 常用方法 1 public final StringBuffer format(Object number, StringBuffer toAppendTo, FieldPosition pos) 格式化一个数，并将所得文本追加到给定的字符串缓冲区。该数可以是 Number 的任意子类。 示例如下 1 2 // 输入结果是￥1,234.01 System.out.println(new DecimalFormat(\u0026#34;￥#,###.00\u0026#34;).format(1234.011)); 10. UUID 类 java.util.UUID 所有已实现的接口：Serializable, Comparable\u0026lt;UUID\u0026gt;\n10.1. 常用方法 1 static UUID randomUUID() UUID类静态方法，获取类型 4（伪随机生成的）UUID 的静态工厂。返回是UUID对象 1 public String toString() 返回表示此 UUID 的 String 对象 10.2. UUID的生成 UUID.randomUUID().toString() 是javaJDK提供的一个自动生成主键的方法。UUID(Universally Unique Identifier)全局唯一标识符,是指在一台机器上生成的数字，它保证对在同一时空中的所有机器都是唯一的，是由一个十六位的数字组成,表现出来的形式。由以下几部分的组合：当前日期和时间(UUID的第一个部分与时间有关，如果你在生成一个UUID之后，过几秒又生成一个UUID，则第一个部分不同，其余相同)，时钟序列，全局唯一的IEEE机器识别号（如果有网卡，从网卡获得，没有网卡以其他方式获得），UUID的唯一缺陷在于生成的结果串会比较长\n11. Scanner 类 11.1. 常用方法 1 public String nextLine() 此扫描器执行当前行，并返回跳过的输入信息。 此方法返回当前行的其余部分，不包括结尾处的行分隔符。当前位置移至下一行的行首 1 public String next(String pattern) 如果下一个标记与从指定字符串构造的模式匹配，则返回下一个标记。如果匹配操作成功，则扫描器执行与该模式匹配的输入 1 public boolean hasNextInt() 如果通过使用 nextInt() 方法，此扫描器输入信息中的下一个标记可以解释为默认基数中的一个 int 值，则返回 true 1 public boolean hasNextInt(int radix) 如果通过使用 nextInt() 方法，此扫描器输入信息中的下一个标记可以解释为指定基数中的一个 int 值，则返回 true 11.2. Scanner类的next()与nextLine()区别 next(); 从左往右开始扫描内容，在没有扫描到有效内容时遇到的空格，tab键,换行符都会被自己过滤掉。 一旦读取到空格，tab键，换行符都会结束扫描。 nextLine(); 从左到右开始扫描，不会过滤任何字符，直到扫描到换行符就结束一行扫描。 示例：\n1 2 3 int age = sc.nextInt(); String str = sc.nextLine(); 如果第1句输入数字后再1个空格再回车，则下一句无法再接收接入，因为第1句在遇到空格时就结束，第2句nextLine()会读取到上一句的空格和换行符，就结束了扫描。因为无法再输入第2句 ","permalink":"https://ktzxy.top/posts/bnwz2jjy4u/","summary":"Java基础 JDK常用API","title":"Java基础 JDK常用API"},{"content":"Go操作消息队列 NSQ是目前比较流行的一个分布式的消息队列，本文主要介绍了NSQ及Go语言如何操作NSQ。\n使用消息队列的主要目的，异步、解耦、削峰\nNSQ介绍 NSQ是Go语言编写的一个开源的实时分布式内存消息队列，其性能十分优异。 NSQ的优势有以下优势：\nNSQ提倡分布式和分散的拓扑，没有单点故障，支持容错和高可用性，并提供可靠的消息交付保证 NSQ支持横向扩展，没有任何集中式代理。 NSQ易于配置和部署，并且内置了管理界面。 NSQ的应用场景 通常来说，消息队列都适用以下场景。\n异步处理 参照下图利用消息队列把业务流程中的非关键流程异步化，从而显著降低业务请求的响应时间。\n应用解耦 通过使用消息队列将不同的业务逻辑解耦，降低系统间的耦合，提高系统的健壮性。后续有其他业务要使用订单数据可直接订阅消息队列，提高系统的灵活性。\n流量削峰 类似秒杀（大秒）等场景下，某一时间可能会产生大量的请求，使用消息队列能够为后端处理请求提供一定的缓冲区，保证后端服务的稳定性。\n安装 官方下载页面根据自己的平台下载并解压即可。\nNSQ组件 nsqd nsqd是一个守护进程，它接收、排队并向客户端发送消息。\n启动nsqd，指定-broadcast-address=127.0.0.1来配置广播地址\n1 ./nsqd -broadcast-address=127.0.0.1 如果是在搭配nsqlookupd使用的模式下需要还指定nsqlookupd地址:\n1 ./nsqd -broadcast-address=127.0.0.1 -lookupd-tcp-address=127.0.0.1:4160 最后我们还需要启动图形化的界面 nsqadmin\n1 nsqadmin -lookupd-http-address=127.0.0.1:4161 然后在启动成功后，在浏览器输入 127.0.0.1:4171，即可进入图形化界面\n如果是部署了多个nsqlookupd节点的集群，那还可以指定多个-lookupd-tcp-address。\nnsqdq相关配置项如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 -auth-http-address value \u0026lt;addr\u0026gt;:\u0026lt;port\u0026gt; to query auth server (may be given multiple times) -broadcast-address string address that will be registered with lookupd (defaults to the OS hostname) (default \u0026#34;PROSNAKES.local\u0026#34;) -config string path to config file -data-path string path to store disk-backed messages -deflate enable deflate feature negotiation (client compression) (default true) -e2e-processing-latency-percentile value message processing time percentiles (as float (0, 1.0]) to track (can be specified multiple times or comma separated \u0026#39;1.0,0.99,0.95\u0026#39;, default none) -e2e-processing-latency-window-time duration calculate end to end latency quantiles for this duration of time (ie: 60s would only show quantile calculations from the past 60 seconds) (default 10m0s) -http-address string \u0026lt;addr\u0026gt;:\u0026lt;port\u0026gt; to listen on for HTTP clients (default \u0026#34;0.0.0.0:4151\u0026#34;) -http-client-connect-timeout duration timeout for HTTP connect (default 2s) -http-client-request-timeout duration timeout for HTTP request (default 5s) -https-address string \u0026lt;addr\u0026gt;:\u0026lt;port\u0026gt; to listen on for HTTPS clients (default \u0026#34;0.0.0.0:4152\u0026#34;) -log-prefix string log message prefix (default \u0026#34;[nsqd] \u0026#34;) -lookupd-tcp-address value lookupd TCP address (may be given multiple times) -max-body-size int maximum size of a single command body (default 5242880) -max-bytes-per-file int number of bytes per diskqueue file before rolling (default 104857600) -max-deflate-level int max deflate compression level a client can negotiate (\u0026gt; values == \u0026gt; nsqd CPU usage) (default 6) -max-heartbeat-interval duration maximum client configurable duration of time between client heartbeats (default 1m0s) -max-msg-size int maximum size of a single message in bytes (default 1048576) -max-msg-timeout duration maximum duration before a message will timeout (default 15m0s) -max-output-buffer-size int maximum client configurable size (in bytes) for a client output buffer (default 65536) -max-output-buffer-timeout duration maximum client configurable duration of time between flushing to a client (default 1s) -max-rdy-count int maximum RDY count for a client (default 2500) -max-req-timeout duration maximum requeuing timeout for a message (default 1h0m0s) -mem-queue-size int number of messages to keep in memory (per topic/channel) (default 10000) -msg-timeout string duration to wait before auto-requeing a message (default \u0026#34;1m0s\u0026#34;) -node-id int unique part for message IDs, (int) in range [0,1024) (default is hash of hostname) (default 616) -snappy enable snappy feature negotiation (client compression) (default true) -statsd-address string UDP \u0026lt;addr\u0026gt;:\u0026lt;port\u0026gt; of a statsd daemon for pushing stats -statsd-interval string duration between pushing to statsd (default \u0026#34;1m0s\u0026#34;) -statsd-mem-stats toggle sending memory and GC stats to statsd (default true) -statsd-prefix string prefix used for keys sent to statsd (%s for host replacement) (default \u0026#34;nsq.%s\u0026#34;) -sync-every int number of messages per diskqueue fsync (default 2500) -sync-timeout duration duration of time per diskqueue fsync (default 2s) -tcp-address string \u0026lt;addr\u0026gt;:\u0026lt;port\u0026gt; to listen on for TCP clients (default \u0026#34;0.0.0.0:4150\u0026#34;) -tls-cert string path to certificate file -tls-client-auth-policy string client certificate auth policy (\u0026#39;require\u0026#39; or \u0026#39;require-verify\u0026#39;) -tls-key string path to key file -tls-min-version value minimum SSL/TLS version acceptable (\u0026#39;ssl3.0\u0026#39;, \u0026#39;tls1.0\u0026#39;, \u0026#39;tls1.1\u0026#39;, or \u0026#39;tls1.2\u0026#39;) (default 769) -tls-required require TLS for client connections (true, false, tcp-https) -tls-root-ca-file string path to certificate authority file -verbose enable verbose logging -version print version string -worker-id do NOT use this, use --node-id nsqlookupd nsqlookupd是维护所有nsqd状态、提供服务发现的守护进程。它能为消费者查找特定topic下的nsqd提供了运行时的自动发现服务。 它不维持持久状态，也不需要与任何其他nsqlookupd实例协调以满足查询。因此根据你系统的冗余要求尽可能多地部署nsqlookupd节点。它们消耗的资源很少，可以与其他服务共存。我们的建议是为每个数据中心运行至少3个集群。\nnsqlookupd相关配置项如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -broadcast-address string address of this lookupd node, (default to the OS hostname) (default \u0026#34;PROSNAKES.local\u0026#34;) -config string path to config file -http-address string \u0026lt;addr\u0026gt;:\u0026lt;port\u0026gt; to listen on for HTTP clients (default \u0026#34;0.0.0.0:4161\u0026#34;) -inactive-producer-timeout duration duration of time a producer will remain in the active list since its last ping (default 5m0s) -log-prefix string log message prefix (default \u0026#34;[nsqlookupd] \u0026#34;) -tcp-address string \u0026lt;addr\u0026gt;:\u0026lt;port\u0026gt; to listen on for TCP clients (default \u0026#34;0.0.0.0:4160\u0026#34;) -tombstone-lifetime duration duration of time a producer will remain tombstoned if registration remains (default 45s) -verbose enable verbose logging -version print version string nsqadmin 一个实时监控集群状态、执行各种管理任务的Web管理平台。 启动nsqadmin，指定nsqlookupd地址:\n1 ./nsqadmin -lookupd-http-address=127.0.0.1:4161 我们可以使用浏览器打开http://127.0.0.1:4171/访问如下管理界面。\nnsqadmin相关的配置项如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 -allow-config-from-cidr string A CIDR from which to allow HTTP requests to the /config endpoint (default \u0026#34;127.0.0.1/8\u0026#34;) -config string path to config file -graphite-url string graphite HTTP address -http-address string \u0026lt;addr\u0026gt;:\u0026lt;port\u0026gt; to listen on for HTTP clients (default \u0026#34;0.0.0.0:4171\u0026#34;) -http-client-connect-timeout duration timeout for HTTP connect (default 2s) -http-client-request-timeout duration timeout for HTTP request (default 5s) -http-client-tls-cert string path to certificate file for the HTTP client -http-client-tls-insecure-skip-verify configure the HTTP client to skip verification of TLS certificates -http-client-tls-key string path to key file for the HTTP client -http-client-tls-root-ca-file string path to CA file for the HTTP client -log-prefix string log message prefix (default \u0026#34;[nsqadmin] \u0026#34;) -lookupd-http-address value lookupd HTTP address (may be given multiple times) -notification-http-endpoint string HTTP endpoint (fully qualified) to which POST notifications of admin actions will be sent -nsqd-http-address value nsqd HTTP address (may be given multiple times) -proxy-graphite proxy HTTP requests to graphite -statsd-counter-format string The counter stats key formatting applied by the implementation of statsd. If no formatting is desired, set this to an empty string. (default \u0026#34;stats.counters.%s.count\u0026#34;) -statsd-gauge-format string The gauge stats key formatting applied by the implementation of statsd. If no formatting is desired, set this to an empty string. (default \u0026#34;stats.gauges.%s\u0026#34;) -statsd-interval duration time interval nsqd is configured to push to statsd (must match nsqd) (default 1m0s) -statsd-prefix string prefix used for keys sent to statsd (%s for host replacement, must match nsqd) (default \u0026#34;nsq.%s\u0026#34;) -version print version string NSQ架构 NSQ工作模式 Topic和Channel 每个nsqd实例旨在一次处理多个数据流。这些数据流称为“topics”，一个topic具有1个或多个“channels”。每个channel都会收到topic所有消息的副本，实际上下游的服务是通过对应的channel来消费topic消息。\ntopic和channel不是预先配置的。topic在首次使用时创建，方法是将其发布到指定topic，或者订阅指定topic上的channel。channel是通过订阅指定的channel在第一次使用时创建的。\ntopic和channel都相互独立地缓冲数据，防止缓慢的消费者导致其他chennel的积压（同样适用于topic级别）。\nchannel可以并且通常会连接多个客户端。假设所有连接的客户端都处于准备接收消息的状态，则每条消息将被传递到随机客户端。例如：\n总而言之，消息是从topic -\u0026gt; channel（每个channel接收该topic的所有消息的副本）多播的，但是从channel -\u0026gt; consumers均匀分布（每个消费者接收该channel的一部分消息）。\nNSQ接收和发送消息流程 input Chan：就是go语言中的通道 In-Memory Chan：是内存的通道，负责将消息进行持久化 Output Chan： NSQ特性 消息默认不持久化，可以配置成持久化模式。nsq采用的方式时内存+硬盘的模式，当内存到达一定程度时就会将数据持久化到硬盘。 如果将--mem-queue-size设置为0，所有的消息将会存储到磁盘。 服务器重启时也会将当时在内存中的消息持久化。 每条消息至少传递一次。 消息不保证有序。 Go操作NSQ 官方提供了Go语言版的客户端：go-nsq，更多客户端支持请查看CLIENT LIBRARIES。\n启动 首先进入bin目录下，打开cmd，输入\n1 nsqlookupd 然后就开启了nsq服务，端口号是4160\n然后我们在启动一个cmd界面，输入下面的代码，启动nsqd，nsqd是一个守护进程，它用于接收、排队并向客户端发送消息，启动nsqd，指定 -broadcast-address=127.0.0.1 来配置广播地址\n1 nsqd -broadcast-address=127.0.0.1 如果是在搭配 nsqlookupd，使用的模式下需要还指定nsqlookupd的地址\n1 nsqd -broadcast-address=127.0.0.1 -lookupd-tcp-address=127.0.0.1:4160 启动成功的图片如下所示\n安装 1 go get -u github.com/nsqio/go-nsq 生产者 一个简单的生产者示例代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 // nsq_producer/main.go package main import ( \u0026#34;bufio\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;strings\u0026#34; \u0026#34;github.com/nsqio/go-nsq\u0026#34; ) // NSQ Producer Demo var producer *nsq.Producer // 初始化生产者 func initProducer(str string) (err error) { config := nsq.NewConfig() producer, err = nsq.NewProducer(str, config) if err != nil { fmt.Printf(\u0026#34;create producer failed, err:%v\\n\u0026#34;, err) return err } return nil } func main() { nsqAddress := \u0026#34;127.0.0.1:4150\u0026#34; err := initProducer(nsqAddress) if err != nil { fmt.Printf(\u0026#34;init producer failed, err:%v\\n\u0026#34;, err) return } reader := bufio.NewReader(os.Stdin) // 从标准输入读取 for { data, err := reader.ReadString(\u0026#39;\\n\u0026#39;) if err != nil { fmt.Printf(\u0026#34;read string from stdin failed, err:%v\\n\u0026#34;, err) continue } data = strings.TrimSpace(data) if strings.ToUpper(data) == \u0026#34;Q\u0026#34; { // 输入Q退出 break } // 向 \u0026#39;topic_demo\u0026#39; publish 数据 err = producer.Publish(\u0026#34;topic_demo\u0026#34;, []byte(data)) if err != nil { fmt.Printf(\u0026#34;publish msg to nsq failed, err:%v\\n\u0026#34;, err) continue } } } 将上面的代码编译执行，然后在终端输入两条数据123和456：\n1 2 3 4 $ ./nsq_producer 123 2018/10/22 18:41:20 INF 1 (127.0.0.1:4150) connecting to nsqd 456 使用浏览器打开http://127.0.0.1:4171/可以查看到类似下面的页面： 在下面这个页面能看到当前的topic信息：\n点击页面上的topic_demo就能进入一个展示更多详细信息的页面，在这个页面上我们可以查看和管理topic，同时能够看到目前在LWZMBP:4151 (127.0.01:4151)这个nsqd上有2条message。又因为没有消费者接入所以暂时没有创建channel。\n在/nodes这个页面我们能够很方便的查看当前接入lookupd的nsqd节点。\n这个/counter页面显示了处理的消息数量，因为我们没有接入消费者，所以处理的消息数量为0。![images/nsqadmin4.png)\n在/lookup界面支持创建topic和channel。\n消费者 一个简单的消费者示例代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 // nsq_consumer/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;os\u0026#34; \u0026#34;os/signal\u0026#34; \u0026#34;syscall\u0026#34; \u0026#34;time\u0026#34; \u0026#34;github.com/nsqio/go-nsq\u0026#34; ) // NSQ Consumer Demo // MyHandler 是一个消费者类型 type MyHandler struct { Title string } // HandleMessage 是需要实现的处理消息的方法 func (m *MyHandler) HandleMessage(msg *nsq.Message) (err error) { fmt.Printf(\u0026#34;%s recv from %v, msg:%v\\n\u0026#34;, m.Title, msg.NSQDAddress, string(msg.Body)) return } // 初始化消费者 func initConsumer(topic string, channel string, address string) (err error) { config := nsq.NewConfig() config.LookupdPollInterval = 15 * time.Second c, err := nsq.NewConsumer(topic, channel, config) if err != nil { fmt.Printf(\u0026#34;create consumer failed, err:%v\\n\u0026#34;, err) return } consumer := \u0026amp;MyHandler{ Title: \u0026#34;沙河1号\u0026#34;, } c.AddHandler(consumer) // if err := c.ConnectToNSQD(address); err != nil { // 直接连NSQD if err := c.ConnectToNSQLookupd(address); err != nil { // 通过lookupd查询 return err } return nil } func main() { err := initConsumer(\u0026#34;topic_demo\u0026#34;, \u0026#34;first\u0026#34;, \u0026#34;127.0.0.1:4161\u0026#34;) if err != nil { fmt.Printf(\u0026#34;init consumer failed, err:%v\\n\u0026#34;, err) return } c := make(chan os.Signal) // 定义一个信号的通道 signal.Notify(c, syscall.SIGINT) // 转发键盘中断信号到c \u0026lt;-c // 阻塞 } 将上面的代码保存之后编译执行，就能够获取之前我们publish的两条消息了：\n1 2 3 4 5 $ ./nsq_consumer 2018/10/22 18:49:06 INF 1 [topic_demo/first] querying nsqlookupd http://127.0.0.1:4161/lookup?topic=topic_demo 2018/10/22 18:49:06 INF 1 [topic_demo/first] (127.0.0.1:4150) connecting to nsqd 沙河1号 recv from 127.0.0.1:4150, msg:123 沙河1号 recv from 127.0.0.1:4150, msg:456 同时在nsqadmin的/counter页面查看到处理的数据数量为2。\n关于go-nsq的更多内容请阅读go-nsq的官方文档。\n","permalink":"https://ktzxy.top/posts/h7ey5c8e50/","summary":"Go操作消息队列","title":"Go操作消息队列"},{"content":" MySQL高级篇笔记 MySQL体系结构 连接层\n最上层是一些客户端和链接服务，主要完成一些类似于连接处理、授权认证、及相关的安全方案。服务器也会为安全接入的每个客户 端验证它所具有的操作权限。\n服务层\n第二层架构主要完成大多数的核心服务功能，如SQL接口，并完成缓存的查询，SQL的分析和优化，部分内置函数的执行。所有跨存 储引擎的功能也在这一层实现，如 过程、函数等。\n引擎层\n存储引擎真正的负责了MySQL中数据的存储和提取，服务器通过API和存储引擎进行通信。不同的存储引擎具有不同的功能，这样我 们可以根据自己的需要，来选取合适的存储引擎。\n存储层\n主要是将数据存储在文件系统之上，并完成与存储引擎的交互。\nshow engines查看当前数据库支持的存储引擎\n存储引擎特点 InnoDB底层文件 xxx.ibd：xxx代表的是表名，innoDB引擎的每张表都会对应这样一个表空间文件，存储该表的表结构（frm、sdi）、数据和索引。\n是否使用独立表空间可以通过innodb_file_per_table 来设置。\n在配置文件（my.cnf）中设置： innodb_file_per_table = 1 #1为开启，0为关闭\n通过show variables like '%per_table%';查询当前状态\n也可以通过set global innodb_file_per_table =OFF;临时修改，重启后失效。\nMyISAM底层文件 xxx.sdi：存储表结构信息\nxxx.MYD: 存储数据\nxxx.MYI: 存储索引\n索引 慢查询日志 1 2 #通过 show [session|global] status 命令可以提供服务器状态信息。通过如下指令，可以查看当前数据库的INSERT、UPDATE、DELETE、SELECT的访问频次： SHOW GLOBAL STATUS LIKE \u0026#39;Com_______\u0026#39;; 慢查询日志记录了所有执行时间超过参数 long_query_time 设置值并且扫描记录数不小于 min_examined_row_limit的所有的SQL语句的日志，默认未开启。long_query_time 默认为 10 秒，最小为 0， 精度可以到微秒。\n1 2 #慢查询日志 show variables like \u0026#39;slow_query_log\u0026#39;; 在MySQL的配置文件（/etc/my.cnf）中配置如下信息：\n1 2 3 4 5 #开启MYSQL慢查询日志 slow_query_log=1 #设置慢查询日志的时间为2秒,SQL语句执行时间超过2秒，就会被是为慢查询，记录慢查询日志 long_query_time=2 重启MYSQL服务生效。\n默认慢查询日志文件位置/var/lib/mysql/localhost-slow.log\n默认情况下，不会记录管理语句，也不会记录不使用索引进行查找的查询。可以使用log_slow_admin_statements和 更改此行为 log_queries_not_using_indexes，如下所述。\nprofile详情 show profiles 能够在做SQL优化时帮助我们了解时间都耗费到哪里去了。\n通过have_profiling参数，能够看到当前MySQL是否支持。 查看当前profiling是否已经开启，默认是0,表示未开启。 开始profiling\n查看SQL语句的耗时情况\n1 2 3 4 5 6 7 8 #查看每一条SQL的耗时 show profiles; #查看指定query_id的SQL语句各个阶段的耗时情况 show profile for query 2; #查看指定query_id的SQL语句各个阶段的CPU消耗情况 show profile cpu for query 2; explain执行计划 EXPLAIN 或者 DESC命令获取 MySQL 如何执行 SELECT 语句的信息，包括在 SELECT 语句执行过程中表如何连接和连接的顺序。\nEXPLAIN 执行计划各字段含义： ​\t➢ Id: select查询的序列号，表示查询中执行select子句或者是操作表的顺序(id相同，执行顺序从上到下；id不同，值越大，越先执行)。\n​\t➢ select_type: 表示 SELECT 的类型，常见的取值有 SIMPLE（简单表，即不使用表连接或者子查询）、PRIMARY（主查询，即外层的查询）、UNION（UNION 中的第二个或者后面的查询语句）、SUBQUERY（SELECT/WHERE之后包含了子查询）等\n​\t➢ type: 表示连接类型，性能由好到差的连接类型为NULL、system、const、eq_ref、ref、range、 index、all 。\n​\t➢ possible_key: 显示可能应用在这张表上的索引，一个或多个。\n​\t➢ key: 实际使用的索引，如果为NULL，则没有使用索引。\n​\t➢ key_len: 表示索引中使用的字节数， 该值为索引字段最大可能长度，并非实际使用长度，在不损失精确性的前提下， 长度越短越好 。\n​\t➢ rows: MySQL认为必须要执行查询的行数，在innodb引擎的表中，是一个估计值，可能并不总是准确的。\n​\t➢ filtered: 表示返回结果的行数占需读取行数的百分比， filtered 的值越大越好。\n索引使用 最左前缀法则 如果索引了多列（联合索引），要遵守最左前缀法则。最左前缀法则指的是查询从索引的最左列开始，并且不跳过索引中的列。 如果跳跃某一列，索引将部分失效(后面的字段索引失效)。\n范围查询 联合索引中，出现范围查询(\u0026gt;,\u0026lt;)，范围查询右侧的列索引失效\n索引列运算 不要在索引列上进行运算操作， 索引将失效。\n字符串不加引号 字符串类型字段使用时，不加引号， 索引将失效。\n模糊查询 如果仅仅是尾部模糊匹配，索引不会失效。如果是头部模糊匹配，索引失效。\nor连接的条件 用or分割开的条件， 如果or前的条件中的列有索引，而后面的列中没有索引，那么涉及的索引都不会被用到。\n数据分布影响 如果MySQL评估使用索引比全表更慢，则不使用索引。\nSQL提示 use index：建议使用指定索引\nignore index：忽略指定索引\nforce index：强制使用指定索引\n覆盖索引 尽量使用覆盖索引（查询使用了索引，并且需要返回的列，在该索引中已经全部能够找到），减少一次主键上的select 。\n前缀索引 当字段类型为字符串（varchar，text等）时，有时候需要索引很长的字符串，这会让索引变得很大，查询时，浪费大量的磁盘IO， 影 响查询效率。此时可以只将字符串的一部分前缀，建立索引，这样可以大大节约索引空间，从而提高索引效率。\n➢ 语法\n1 create index index_name on table_name(clumn(n)); ➢ 前缀长度\n可以根据索引的选择性来决定，而选择性是指不重复的索引值（基数）和数据表的记录总数的比值，索引选择性越高则查询效率越高 ， 唯一索引的选择性是1，这是最好的索引选择性，性能也是最好的\n单列索引与联合索引 单列索引：即一个索引只包含单个列。\n联合索引：即一个索引包含了多个列。\n在业务场景中，如果存在多个查询条件，考虑针对于查询字段建立索引时，建议建立联合索引，而非单列索引。\n索引设计原则 针对于数据量较大，且查询比较频繁的表建立索引。 针对于常作为查询条件（where）、排序（order by）、分组（group by）操作的字段建立索引。 尽量选择区分度高的列作为索引，尽量建立唯一索引，区分度越高，使用索引的效率越高。 如果是字符串类型的字段，字段的长度较长，可以针对于字段的特点，建立前缀索引。 尽量使用联合索引，减少单列索引，查询时，联合索引很多时候可以覆盖索引，节省存储空间，避免回表，提高查询效率。 要控制索引的数量，索引并不是多多益善，索引越多，维护索引结构的代价也就越大，会影响增删改的效率。 如果索引列不能存储NULL值，请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时，它可以更好地确定哪 个索引最有效地用于查询。 SQL优化 插入数据 insert优化 ➢ 批量插入\n➢ 手动提交事务\n➢ 主键顺序插入\n大批量插入数据 如果一次性需要插入大批量数据，使用insert语句插入性能较低，此时可以使用MySQL数据库提供的load指令进行插入。操作如下:\n1 2 3 4 5 6 #客户端连接服务端时，加上参数 --local-infile mysql --local-infile -u root -p #设置全局参数local_infile为1 ，开启从本地加载文件导入数据的开关 set global local_infile=1; #执行load指令将准备好的数据，加载到表结构中 load data local infile \u0026#39;/root/load_user_100w_sort.sql\u0026#39; into table tb_user fields terminated by \u0026#39;,\u0026#39; lines terminated by \u0026#39;\\n\u0026#39;; !\n主键顺序插入性能高于乱序插入\n主要的原因是由于底层数据每一页在物理磁盘存放是按照主键由低到高顺序存放的，如果按照主键顺序插入就类似顺序写入磁盘；如果是乱序插入，就需要调整磁盘上已写入数据顺序。\n主键优化 数据组织方式 在InnoDB存储引擎中，表数据都是根据主键顺序组织存放的，这种存储方式的表称为索引组织表(index organized table IOT)。\n页分裂\n页可以为空，也可以填充一半，也可以填充100%。每个页包含了2-N行数据(如果一行数据多大，会行溢出)，根据主键排列。\n页合并\n当删除一行记录时，实际上记录并没有被物理删除，只是记录被标记（flaged）为删除并且它的空间变得允许被其他记录声明使用。\n当页中删除的记录达到 MERGE_THRESHOLD（默认为页的50%），InnoDB会开始寻找最靠近的页（前或后）看看是否可以将两个页合并以优 化空间使用。\n主键设计原则\n➢ 满足业务需求的情况下，尽量降低主键的长度。\n➢ 插入数据时，尽量选择顺序插入，选择使用AUTO_INCREMENT自增主键。\n➢ 尽量不要使用UUID做主键或者是其他自然主键，如身份证号。\n➢ 业务操作时，避免对主键的修改。\norder by优化 ① Using filesort:\n通过表的索引或全表扫描，读取满足条件的数据行，然后在排序缓冲区 中完成排序操作，所有不是通过索引直接返回排序结果的排序都叫 排序。\n② Using index:\n通过有序索引顺序扫描直接返回有序数据，这种情况即为 ，不需要额外排序，操作效率高。\n➢ 根据排序字段建立合适的索引，多字段排序时，也遵循最左前缀法则。\n➢ 尽量使用覆盖索引。\n➢ 多字段排序 一个升序一个降序，此时需要注意联合索引在创建时的规则（ASC/DESC ）。\n➢ 如果不可避免的出现filesort，大数据量排序时，可以适当增大排序缓冲区大小sort_buffer_size(默认256k) 。\ngroup by优化 ➢ 在分组操作时，可以通过索引来提高效率。\n➢ 分组操作时，索引的使用也是满足最左前缀法则的。\nlimit优化 一个常见又非常头疼的问题就是 limit 2000000,10 ，此时需要MySQL排序前2000010 记录，仅仅返回2000000 - 2000010 的记录，其他记录丢弃，查询排序的代价非常大 。\n优化思路: 一般分页查询时，通过创建 覆盖索引 能够比较好地提高性能，可以通过覆盖索引加子查询形式进行优化。\n1 2 3 4 5 #这种比较耗时 select * from tb_user limit 10000,10 #这种相对较快 select s.* from tb_user s,(select id from tb_user order by id limit 10000,10) a where s.id=a.id; count优化 ➢ MyISAM 引擎把一个表的总行数存在了磁盘上，因此执行 count(*) 的时候会直接返回这个数，效率很高；\n➢ InnoDB 引擎就麻烦了，它执行 count() 的时候，需要把数据一行一行地从引擎里面读出来，然后累积计数。\n优化思路：自己计数。\ncount的几种用法 count优化\n➢ count() 是一个聚合函数，对于返回的结果集，一行行地判断，如果 count 函数的参数不是 NULL，累计值就加 1，否则不加，最 后返回累计值。\n➢ 用法：count（*）、count（主键）、count（字段）、count（1）\ncount的几种用法\n​\t➢ count（主键）\n​\tInnoDB 引擎会遍历整张表，把每一行的 主键id 值都取出来，返回给服务层。服务层拿到主键后，直接按行进行累加(主键不可能为null)\n​\t➢ count（字段）\n​\t没有not null 约束 : InnoDB 引擎会遍历整张表把每一行的字段值都取出来，返回给服务层，服务层判断是否为null，不为null，计数累加 。\n​\t有not null 约束：InnoDB 引擎会遍历整张表把每一行的字段值都取出来，返回给服务层，直接按行进行累加。\n​\t➢ count（1）\n​\tInnoDB 引擎遍历整张表，但不取值。服务层对于返回的每一行，放一个数字“1”进去，直接按行进行累加。\n​\t➢ count（*）\n​\tInnoDB引擎并不会把全部字段取出来，而是专门做了优化，不取值，服务层直接按行进行累加。\n按照效率排序的话，count(字段) \u0026lt; count(主键 id) \u0026lt; count(1) ≈ count(*)，所以尽量使用 count(*)。\nupdate优化 InnoDB的行锁是针对索引加的锁，不是针对记录加的锁 ,并且该索引不能失效，否则会从行锁升级为表锁 。\n这个说法应该是错误的，只能说看起来表象是从行锁升级到了表锁。具体的细节可以看这里Innodb到底是怎么加锁的 - 掘金 (juejin.cn)\n视图 介绍 ​\t视图（View）是一种虚拟存在的表。视图中的数据并不在数据库中实际存在，行和列数据来自定义视图的查询中使用的表，并且是在使用视 图时动态生成的。\n​\t通俗的讲，视图只保存了查询的SQL逻辑，不保存查询结果。所以我们在创建视图的时候，主要的工作就落在创建这条SQL查询语句上。\n创建 1 2 3 4 5 #语法 CREATE [ OR REPLACE ] VIEW 视图名称 [(列名列表)] AS SELECT语句 [WITH [CASCADED | LOCAL ] CHECK OPTION] #示例 create or replace view view_tb_user as select id,name from tb_user where id\u0026lt;10; 查询 1 2 3 4 5 #查看创建视图语句 SHOW CREATE VIEW 视图名称; #查看视图数据 SELECT * FROM 视图名称 ...; 修改 1 2 3 4 5 #方式一 CREATE [ OR REPLACE ] VIEW 视图名称 [(列名列表)] AS SELECT语句 [WITH [CASCADED | LOCAL ] CHECK OPTION] #方式二 ALTER VIEW 视图名称 [(列名列表)] AS SELECT语句 [WITH [CASCADED | LOCAL ] CHECK OPTION] 删除 1 DROP VIEW [IF EXISTS] 视图名称[,视图名称] 视图的检查选项 当使用WITH CHECK OPTION子句创建视图时，MySQL会通过视图检查正在更改的每个行，例如 插入，更新，删除，以使其符合视图的定 义。 MySQL允许基于另一个视图创建视图，它还会检查依赖视图中的规则以保持一致性。\n为了确定检查的范围，mysql提供了两个选项： CASCADED 和 LOCAL ，默认值为 CASCADED\n视图的更新 要使视图可更新，视图中的行与基础表中的行之间必须存在一对一的关系。\n如果视图包含以下任何一项，则该视图不可更新：\n聚合函数或窗口函数（SUM()、 MIN()、 MAX()、 COUNT()等） DISTINCT GROUP BY HAVING UNION 或者 UNION ALL 作用 ➢ 简单\n视图不仅可以简化用户对数据的理解，也可以简化他们的操作。那些被经常使用的查询可以被定义为视图，从而使得用户不必为以后的操 作每次指定全部的条件。\n➢ 安全\n数据库可以授权，但不能授权到数据库特定行和特定的列上。通过视图用户只能查询和修改他们所能见到的数据\n➢ 数据独立\n视图可帮助用户屏蔽真实表结构变化带来的影响。\n系统变量 查看系统变量 1 SHOW [SESSION|GLOBAL] VARIABLES; #查看所有系统变量SHOW [SESSION|GLOBAL] VARIABLES LIKE \u0026#39;...\u0026#39;; #模糊查看系统变量SHOW @@[SESSION|GLOBAL] 系统变量名; #查看指定变量的值 设置变量的值 1 SET [SESSION|GLOBAL] 系统变量名=值；SET @@[SESSION|GLOBAL] 系统变量名=值； 注意:\n​\t如果没有指定SESSION/GLOBAL，默认是SESSION，会话变量。\n​\tmysql服务重新启动之后，所设置的全局参数会失效，要想不失效，可以在 /etc/my.cnf 中配置。\n锁 全局锁 全局锁就是对整个数据库实例加锁，加锁后整个实例就处于只读状态，后续的DML的写语句，DDL语句，已经更新操作的事务提交语句都 将被阻塞。\n其典型的使用场景是做全库的逻辑备份，对所有的表进行锁定，从而获取一致性视图，保证数据的完整性。\n语法 1 2 3 flush tables with read lock; #开启全局锁 ...... #只有只读语句可以执行，所有session会话中的写的语句都会阻塞 unlock tables; #解除全局锁 备份数据库\n1 mysqldump -uroot -p123456 数据库实例名 \u0026gt;xxx.sql 特点 数据库中加全局锁，是一个比较重的操作，存在以下问题：\n如果在主库上备份，那么在备份期间都不能执行更新，业务基本上就得停摆。 如果在从库上备份，那么在备份期间从库不能执行主库同步过来的二进制日志（binlog），会导致主从延迟。 在InnoDB引擎中，我们可以在备份时加上参数 \u0026ndash;single-transaction 参数来完成不加锁的一致性数据备份。\n1 mysqldump --single-transaction -uroot -p123456 数据库实例名 \u0026gt;xxx.sql 表级锁 表级锁，每次操作锁住整张表。锁定粒度大，发生锁冲突的概率最高，并发度最低。应用在MyISAM、InnoDB、BDB等存储引擎中。\n对于表级锁，主要分为以下三类：\n表锁 元数据锁（meta data lock，MDL） 意向锁 表锁 对于表锁，分为两类： 表共享读锁（read lock） 表独占写锁（write lock） 语法： 加锁：lock tables 表名\u0026hellip; read/write。 释放锁：unlock tables / 客户端断开连接 。 元数据锁（ meta data lock， MDL）\nMDL加锁过程是系统自动控制，无需显式使用，在访问一张表的时候会自动加上。MDL锁主要作用是维护表元数据的数据一致性，在表 上有活动事务的时候，不可以对元数据进行写入操作。 为了避免DML与DDL冲突，保证读写的正确性。\n在MySQL5.5中引入了MDL，当对一张表进行增删改查的时候，加MDL读锁(共享)；当对表结构进行变更操作的时候，加MDL写锁(排他)。\n对应SQL 锁类型 说明 lock tables xxx read / write SHARED_READ_ONLY / SHARED_NO_READ_WRITE select 、select \u0026hellip; lock in share mode SHARED_READ 与SHARED_READ、SHARED_WRITE兼容，与EXCLUSIVE互斥 insert 、update、delete、select \u0026hellip; for update SHARED_WRITE 与SHARED_READ、SHARED_WRITE兼容，与EXCLUSIVE互斥 alter table \u0026hellip; EXCLUSIVE 与其他的MDL都互斥 查看元数据锁\n1 select object_type,object_schema,object_name,lock_type,lock_duration from performance_schema.metadata_locks; 意向锁\n为了避免DML在执行时，加的行锁与表锁的冲突，在InnoDB中引入了意向锁，使得表锁不用检查每行数据是否加锁，使用意向锁来减 少表锁的检查。\n意向共享锁（IS）：\n由语句 select \u0026hellip; lock in share mode添加。\n与表锁共享锁（read）兼容，与表锁排它锁（write）互斥。\n意向排他锁（IX）：\n由insert、update、delete、select \u0026hellip; for update 添加。\n与表锁共享锁（read）及排它锁（write）都互斥。意向锁之间不会互斥。\n查看意向锁及行锁的加锁情况\n1 select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_locks; 行级锁 行级锁，每次操作锁住对应的行数据。锁定粒度最小，发生锁冲突的概率最低，并发度最高。应用在InnoDB存储引擎中。\nInnoDB的数据是基于索引组织的，行锁是通过对索引上的索引项加锁来实现的，而不是对记录加的锁。对于行级锁，主要分为以下三类：\n行锁（Record Lock）：锁定单个行记录的锁，防止其他事务对此行进行update和delete。在RC、RR隔离级别下都支持。\n共享锁（S）：允许一个事务去读一行，阻止其他事务获得相同数据集的排它锁。 排他锁（X）：允许获取排他锁的事务更新数据，阻止其他事务获得相同数据集的共享锁和排他锁。 默认情况下，InnoDB在 REPEATABLE READ事务隔离级别运行，InnoDB使用 next-key 锁进行搜索和索引扫描，以防止幻读。\n针对唯一索引进行检索时，对已存在的记录进行等值匹配时，将会自动优化为行锁。 InnoDB的行锁是针对于索引加的锁，不通过索引条件检索数据，那么InnoDB将对表中的所有记录加锁，此时 就会升级为表锁。 间隙锁（Gap Lock）：锁定索引记录间隙（不含该记录），确保索引记录间隙不变，防止其他事务在这个间隙进行insert，产生幻读。在RR隔离级别下都支持。\n临键锁（Next-Key Lock）：行锁和间隙锁组合，同时锁住数据，并锁住数据前面的间隙Gap。在RR隔离级别下支持。\n默认情况下，InnoDB在 REPEATABLE READ事务隔离级别运行，InnoDB使用 next-key 锁进行搜索和索引扫描，以防止幻读。\n索引上的等值查询(唯一索引)，给不存在的记录加锁时, 优化为间隙锁 。 索引上的等值查询(普通索引)，向右遍历时最后一个值不满足查询需求时，next-key lock 退化为间隙锁。 索引上的范围查询(唯一索引)\u0026ndash;会访问到不满足条件的第一个值为止。 注意：间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存，一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。\nInnoDB引擎 逻辑存储结构 架构 MySQL5.5 版本开始，默认使用InnoDB存储引擎，它擅长事务处理，具有崩溃恢复特性，在日常开发中使用非常广泛。下面是InnoDB架构图，左侧为内存结构，右 侧为磁盘结构。\n架构-内存架构 架构-磁盘结构 示例：\n执行如下sql： 1 2 create tablespace ts_itheima add datafile \u0026#39;myitheima.ibd\u0026#39; engine=innodb; create table a(id int primary key auto_increment,name varchar(32) ) engine=innodb tablespace ts_itheima; 查看mysql数据文件目录，默认在/var/lib/mysql目录。就会有myitheima.ibd这个文件 架构-后台线程 查看innod的状态信息\n1 show engine innodb status; 事务原理 事务 是一组操作的集合，它是一个不可分割的工作单位，事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求，即这些操作 要么同时成功，要么同时失败。\n特性\n• 原子性（Atomicity）：事务是不可分割的最小操作单元，要么全部成功，要么全部失败。\n• 一致性（Consistency）：事务完成时，必须使所有的数据都保持一致状态。\n• 隔离性（Isolation）：数据库系统提供的隔离机制，保证事务在不受外部并发操作影响的独立环境下运行。\n• 持久性（Durability）：事务一旦提交或回滚，它对数据库中的数据的改变就是永久的。\n事务的原子性，一致性，持久性通过redo log、undo log来实现。\n事务的隔离性通过锁，MVCC来实现。\n重做日志，记录的是事务提交时数据页的物理修改，是用来实现事务的持久性。\n该日志文件由两部分组成：重做日志缓冲（redo log buffer）以及重做日志文件（redo log file）,前者是在内存中，后者在磁盘中。当事务 提交之后会把所有修改信息都存到该日志文件中, 用于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用。\n回滚日志，用于记录数据被修改前的信息 , 作用包含两个 : 提供回滚 和 MVCC(多版本并发控制) 。\nundo log和redo log记录物理日志不一样，它是逻辑日志。可以认为当delete一条记录时，undo log中会记录一条对应的insert记录，反之 亦然，当update一条记录时，它记录一条对应相反的update记录。当执行rollback时，就可以从undo log中的逻辑记录读取到相应的内容 并进行回滚。\nUndo log销毁：undo log在事务执行时产生，事务提交时，并不会立即删除undo log，因为这些日志可能还用于MVCC。\nUndo log存储：undo log采用段的方式进行管理和记录，存放在前面介绍的 rollback segment 回滚段中，内部包含1024个undo log segment。\nMVCC-基本概念 当前读\n读取的是记录的最新版本，读取时还要保证其他并发事务不能修改当前记录，会对读取的记录进行加锁。\n对于我们日常的操作，如： select \u0026hellip; lock in share mode(共享锁)，select \u0026hellip; for update、update、insert、delete(排他锁)都是一种当前读。\n快照读\n简单的select（不加锁）就是快照读，快照读，读取的是记录数据的可见版本，有可能是历史数据，不加锁，是非阻塞读。\nRead Committed：每次select，都生成一个快照读。 Repeatable Read：开启事务后第一个select语句才是快照读的地方。 Serializable：快照读会退化为当前读。 MVCC\n全称 Multi-Version Concurrency Control，多版本并发控制。指维护一个数据的多个版本，使得读写操作没有冲突，快照读为MySQL实现 MVCC提供了一个非阻塞读功能。MVCC的具体实现，还需要依赖于数据库记录中的三个隐式字段、undo log日志、readView。\nMVCC-实现原理 记录中的隐藏字段 可以通过idb2sdi命令通过查看idb文件(默认在/var/lib/mysql/)，就能看到表结构，其中就有隐藏字段。比如DB_TRX_ID、DB_ROLL_PTR\n1 ibd2sdi XXX.idb undo log 回滚日志，在insert、update、delete的时候产生的便于数据回滚的日志。\n当insert的时候，产生的undo log日志只在回滚时需要，在事务提交后，可被立即删除。\n而update、delete的时候，产生的undo log日志不仅在回滚时需要，在快照读时也需要，不会立即被删除。\nundo log版本链 如上图:\n事务2执行时，需要记住修改之前的数据，在回滚时使用。 事务3执行时，由于事务2已经提交，所以它需要在事务2执行结果的基础上进行操作。所以它的undo log里面的记录就是事务2执行的记录。\n事务4执行时，由于事务3已经提交，所以它需要在事务2执行结果的基础上进行操作。所以它的undo log里面的记录就是事务3执行的记录。\n不同事务或相同事务对同一条记录进行修改，会导致该记录的undolog生成一条记录版本链表，链表的头部是最新的旧记录，链表尾部是最早的旧记录。\nreadview ReadView（读视图）是 快照读 SQL执行时MVCC提取数据的依据，记录并维护系统当前活跃的事务（未提交的）id。\nReadView中包含了四个核心字段：\n字段 含义 m_ids 当前活跃的事务ID集合 min_trx_id 最小活跃事务ID max_trx_id 预分配事务ID，当前最大事务ID+1（因为事务ID是自增的） creator_trx_id ReadView创建者的事务ID 上面4个规则，只要满足1，2，4任何一个就可以访问对应的数据；不满足3就要拒绝。\n不同的隔离级别，生成ReadView的时机不同：\n➢ READ COMMITTED ：在事务中每一次执行快照读时生成ReadView。\n➢ REPEATABLE READ：仅在事务中第一次执行快照读时生成ReadView，后续复用该ReadView。\n如上图，当前隔离级别是READ COMMITTED时，事务5中的第一次查询的结果时是事务2提交的记录。\n如上图，当前隔离界别时READ COMMITTED时，事务5中的第二次查询的结果时是事务3提交的记录\n当前隔离级别是读已提交时，事务5中两次查询的ReadView是相同的，所以两次查询的结果都是事务2提交的记录。\nMySQL管理 Mysql数据库安装完成后，自带了一下四个数据库，具体作用如下：\n数据库 含义 mysql 存储MySQL服务器正常运行所需要的各种信息 （时区、主从、用户、权限等） information_schema 提供了访问数据库元数据的各种表和视图，包含数据库，表，字段类型及访问权限等 performance_schema 为MySQL服务器运行时状态提供了一个底层监控功能，主要用于手机数据库服务器性能参数 sys 包含了一系列方便DBA和开发人员利用performance_schema性能数据库进行性能调优和诊断的视图 常用工具: mysql 该mysql不是指mysql服务，而是指mysql的客户端工具。\n-e选项可以在Mysql客户端执行SQL语句，而不用连接到MySQL数据库再执行，对于一些批处理脚本，这种方式尤其方便。\nmysqladmin 可以通过通过mysqladmin --help查看所有选项\nmysqladmin 是一个执行管理操作的客户端程序。可以用它来检查服务器的配置和当前状态、创建并删除数据库等。\nmysqlbinlog 由于服务器生成的二进制日志文件以二进制格式保存，所以如果想要检查这些文本的文本格式，就会使用到mysqlbinlog 日志管理工具。\n如通过mysqlbinlog binlog.000001查看日志(在/var/lib/mysql目录)\nmysqlshow mysqlshow 客户端对象查找工具，用来很快地查找存在哪些数据库、数据库中的表、表中的列或者索引。\nmysqldump mysqldump 客户端工具用来备份数据库或在不同数据库之间进行数据迁移。备份内容包含创建表，及插入表的SQL语句。\n1 2 mysqldump -uroot -p123456 test\u0026gt;test.sql #备份数据库test到test.sql mysqldump -uroot -p123456 -T /var/lib/mysql-files test #将test数据库的表结构和数据导出到/var/lib/mysql-files目录下 mysqlimport/source mysqlimport 是客户端数据导入工具，用来导入mysqldump 加 -T 参数后导出的文本文件。\n如果需要导入sql文件,可以使用mysql中的source 指令 。\n","permalink":"https://ktzxy.top/posts/h5ywf58tp4/","summary":"MySQL高级篇笔记","title":"MySQL高级篇笔记"},{"content":"一、背景 对很多人来说，未知、不确定、不在掌控的东西，会有潜意识的逃避。当我第一次接触 Prometheus 的时候也有类似的感觉。对初学者来说， Prometheus 包含的概念太多了，门槛也太高了。\n概念：Instance、Job、Metric、Metric Name、Metric Label、Metric Value、Metric Type（Counter、Gauge、Histogram、Summary）、DataType（Instant Vector、Range Vector、Scalar、String）、Operator、Function\n马云说：“虽然阿里巴巴是全球最大的零售平台，但阿里不是零售公司，是一家数据公司”。\nPrometheus 也是一样，本质来说是一个基于数据的监控系统。\n1.1 Promethues介绍 Prometheus 是一套开源的系统监控报警框架。它启发于 Google 的 borgmon 监控系统，由工作在 SoundCloud 的 google 前员工在 2012 年创建，作为社区开源项目进行开发，并于 2015 年正式发布。2016 年，Prometheus 正式加入 Cloud Native Computing Foundation，成为受欢迎度仅次于 Kubernetes 的项目。\n作为新一代的监控框架，Prometheus 具有以下特点：\n强大的多维度数据模型： 时间序列数据通过 metric 名和键值对来区分。 所有的 metrics 都可以设置任意的多维标签。 数据模型更随意，不需要刻意设置为以点分隔的字符串。 可以对数据模型进行聚合，切割和切片操作。 支持双精度浮点类型，标签可以设为全unicode。 灵活而强大的查询语句（PromQL）：在同一个查询语句，可以对多个 metrics进行乘法、加法、连接、取分数位等操作。 易于管理： Prometheus server是一个单独的二进制文件，可直接在本地工作，不依赖于分布式存储。 高效：平均每个采样点仅占 3.5 bytes，且一个 Prometheus server 可以处理数百万的 metrics。 使用 pull模式采集时间序列数据，这样不仅有利于本机测试而且可以避免有问题的服务器推送坏的 metrics。 可以采用 push gateway 的方式把时间序列数据推送至 Prometheus server 端。 可以通过服务发现或者静态配置去获取监控的 targets。 有多种可视化图形界面。 易于伸缩。 需要指出的是，由于数据采集可能会有丢失，所以 Prometheus 不适用对采集数据要 100% 准确的情形。但如果用于记录时间序列数据，Prometheus 具有很大的查询优势，此外，Prometheus 适用于微服务的体系架构。 示例图: 1.2 Prometheus的适用场景 在选择Prometheus作为监控工具前，要明确它的适用范围，以及不适用的场景。 Prometheus在记录纯数值时间序列方面表现非常好。它既适用于以服务器为中心的监控，也适用于高动态的面向服务架构的监控。 在微服务的监控上，Prometheus对多维度数据采集及查询的支持也是特殊的优势。 Prometheus更强调可靠性，即使在故障的情况下也能查看系统的统计信息。权衡利弊，以可能丢失少量数据为代价确保整个系统的可用性。因此，它不适用于对数据准确率要求100%的系统，比如实时计费系统（涉及到钱）。 1.3 Prometheus核心组件介绍 Prometheus Server: Prometheus Server是Prometheus组件中的核心部分，负责实现对监控数据的获取，存储以及查询。 Prometheus Server可以通过静态配置管理监控目标，也可以配合使用Service Discovery的方式动态管理监控目标，并从这些监控目标中获取数据。其次Prometheus Server需要对采集到的监控数据进行存储，Prometheus Server本身就是一个时序数据库，将采集到的监控数据按照时间序列的方式存储在本地磁盘当中。最后Prometheus Server对外提供了自定义的PromQL语言，实现对数据的查询以及分析。 Prometheus Server内置的Express Browser UI，通过这个UI可以直接通过PromQL实现数据的查询以及可视化。 Prometheus Server的联邦集群能力可以使其从其他的Prometheus Server实例中获取数据，因此在大规模监控的情况下，可以通过联邦集群以及功能分区的方式对Prometheus Server进行扩展。\nExporters: Exporter将监控数据采集的端点通过HTTP服务的形式暴露给Prometheus Server，Prometheus Server通过访问该Exporter提供的Endpoint端点，即可获取到需要采集的监控数据。 一般来说可以将Exporter分为2类： 直接采集：这一类Exporter直接内置了对Prometheus监控的支持，比如cAdvisor，Kubernetes，Etcd，Gokit等，都直接内置了用于向Prometheus暴露监控数据的端点。 间接采集：间接采集，原有监控目标并不直接支持Prometheus，因此我们需要通过Prometheus提供的Client Library编写该监控目标的监控采集程序。例如： Mysql Exporter，JMX Exporter，Consul Exporter等。\nPushGateway: 在Prometheus Server中支持基于PromQL创建告警规则，如果满足PromQL定义的规则，则会产生一条告警，而告警的后续处理流程则由AlertManager进行管理。在AlertManager中我们可以与邮件，Slack等等内置的通知方式进行集成，也可以通过Webhook自定义告警处理方式。\nService Discovery: 服务发现在Prometheus中是特别重要的一个部分，基于Pull模型的抓取方式，需要在Prometheus中配置大量的抓取节点信息才可以进行数据收集。有了服务发现后，用户通过服务发现和注册的工具对成百上千的节点进行服务注册，并最终将注册中心的地址配置在Prometheus的配置文件中，大大简化了配置文件的复杂程度， 也可以更好的管理各种服务。 在众多云平台中（AWS,OpenStack），Prometheus可以 通过平台自身的API直接自动发现运行于平台上的各种服务，并抓取他们的信息Kubernetes掌握并管理着所有的容器以及服务信息，那此时Prometheus只需要与Kubernetes打交道就可以找到所有需要监控的容器以及服务对象.\nConsul（官方推荐）等服务发现注册软件 通过DNS进行服务发现 通过静态配置文件（在服务节点规模不大的情况下） 1.4 Prometheus UI Prometheus UI是Prometheus内置的一个可视化管理界面，通过Prometheus UI用户能够轻松的了解Prometheus当前的配置，监控任务运行状态等。 通过Graph面板，用户还能直接使用PromQL实时查询监控数据。访问ServerIP:9090/graph打开WEB页面，通过PromQL可以查询数据，可以进行基础的数据展示。 如下所示，查询主机负载变化情况，可以使用关键字node_load1可以查询出Prometheus采集到的主机负载的样本数据，这些样本数据按照时间先后顺序展示，形成了主机负载随时间变化的趋势图表： 二、日常监控 假设需要监控 WebServerA 每个API的请求量为例，需要监控的维度包括：服务名（job）、实例IP（instance）、API名（handler）、方法（method）、返回码(code)、请求量（value）。\npromql\n如果以SQL为例，演示常见的查询操作：\n查询 method=put 且 code=200 的请求量(红框)\nSELECT * from http_requests_total WHERE code=”200” AND method=”put” AND created_at BETWEEN 1495435700 AND 1495435710;\n查询 handler=prometheus 且 method=post 的请求量(绿框)\nSELECT * from http_requests_total WHERE handler=”prometheus” AND method=”post” AND created_at BETWEEN 1495435700 AND 1495435710;\n查询 instance=10.59.8.110 且 handler 以 query 开头 的请求量(绿框)\nSELECT * from http_requests_total WHERE handler=”query” AND instance=”10.59.8.110” AND created_at BETWEEN 1495435700 AND 1495435710;\n通过以上示例可以看出，在常用查询和统计方面，日常监控多用于根据监控的维度进行查询与时间进行组合查询。如果监控100个服务，平均每个服务部署10个实例，每个服务有20个API，4个方法，30秒收集一次数据，保留60天。那么总数据条数为：100(服务)* 10（实例）* 20（API）* 4（方法）* 86400（1天秒数）* 60(天) / 30（秒）= 138.24 亿条数据，写入、存储、查询如此量级的数据是不可能在Mysql类的关系数据库上完成的。因此 Prometheus 使用 TSDB 作为 存储引擎\n三、存储引擎 TSDB 作为 Prometheus 的存储引擎完美契合了监控数据的应用场景\n存储的数据量级十分庞大 大部分时间都是写入操作 写入操作几乎是顺序添加，大多数时候数据到达后都以时间排序 写操作很少写入很久之前的数据，也很少更新数据。大多数情况在数据被采集到数秒或者数分钟后就会被写入数据库 删除操作一般为区块删除，选定开始的历史时间并指定后续的区块。很少单独删除某个时间或者分开的随机时间的数据 基本数据大，一般超过内存大小。一般选取的只是其一小部分且没有规律，缓存几乎不起任何作用 读操作是十分典型的升序或者降序的顺序读 高并发的读操作十分常见 那么 TSDB 是怎么实现以上功能的呢？\n1 2 3 4 5 6 7 \u0026#34;labels\u0026#34;: [{ \u0026#34;latency\u0026#34;: \u0026#34;500\u0026#34; }] \u0026#34;samples\u0026#34;:[{ \u0026#34;timestamp\u0026#34;: 1473305798, \u0026#34;value\u0026#34;: 0.9 }] 原始数据分为两部分 label, samples。前者记录监控的维度（标签:标签值），指标名称和标签的可选键值对唯一确定一条时间序列（使用 series_id 代表）；后者包含包含了时间戳（timestamp）和指标值（value）。\n1 2 3 4 5 6 7 8 series ^ │. . . . . . . . . . . . server{latency=\u0026#34;500\u0026#34;} │. . . . . . . . . . . . server{latency=\u0026#34;300\u0026#34;} │. . . . . . . . . . . server{} │. . . . . . . . . . . . v \u0026lt;-------- time ----------\u0026gt; TSDB 使用 timeseries:doc:: 为 key 存储 value。为了加速常见查询查询操作：label 和 时间范围结合。TSDB 额外构建了三种索引：Series, Label Index 和 Time Index。\n以标签 latency 为例：\nSeries\n存储两部分数据。一部分是按照字典序的排列的所有标签键值对序列（series）；另外一部分是时间线到数据文件的索引，按照时间窗口切割存储数据块记录的具体位置信息，因此在查询时可以快速跳过大量非查询窗口的记录数据\nLabel Index\n每对 label 为会以 index:label: 为 key，存储该标签所有值的列表，并通过引用指向 Series 该值的起始位置。\nTime Index\n数据会以 index:timeseries:: 为 key，指向对应时间段的数据文件\n四、数据计算 强大的存储引擎为数据计算提供了完美的助力，使得 Prometheus 与其他监控服务完全不同。Prometheus 可以查询出不同的数据序列，然后再加上基础的运算符，以及强大的函数，就可以执行 metric series 的矩阵运算（见下图）。\ntime series matrix\n如此，Promtheus体系的能力不弱于监控界的“数据仓库”+“计算平台”。因此，在大数据的开始在业界得到应用，就能明白，这就是监控未来的方向。\n五、一次计算，处处查询 当然，如此强大的计算能力，消耗的资源也是挺恐怖的。因此，查询预计算结果通常比每次需要原始表达式都要快得多，尤其是在仪表盘和告警规则的适用场景中，仪表盘每次刷新都需要重复查询相同的表达式，告警规则每次运算也是如此。因此，Prometheus提供了 Recoding rules，可以预先计算经常需要或者计算量大的表达式，并将其结果保存为一组新的时间序列， 达到 一次计算，多次查询 的目的\n六、其他配置 6.1 prometheus动态加载配置 Prometheus数据源的配置主要分为静态配置和动态发现, 常用的为以下几类:\nstatic_configs: 静态服务发现 file_sd_configs: 文件服务发现 dns_sd_configs: DNS 服务发现 kubernetes_sd_configs: Kubernetes 服务发现 consul_sd_configs:Consul 服务发现（推荐使用） file_sd_configs的方式提供简单的接口，可以实现在单独的配置文件中配置拉取对象，并监视这些文件的变化并自动加载变化。基于这个机制，我们可以自行开发程序，监控监控对象的变化自动生成配置文件，实现监控对象的自动发现。\n在prometheus文件夹目录下创建targets.json文件 配置如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [ { \u0026#34;targets\u0026#34;: [ \u0026#34;192.168.214.129:9100\u0026#34;], \u0026#34;labels\u0026#34;: { \u0026#34;instance\u0026#34;: \u0026#34;node\u0026#34;, \u0026#34;job\u0026#34;: \u0026#34;server-129\u0026#34; } }, { \u0026#34;targets\u0026#34;: [ \u0026#34;192.168.214.134:9100\u0026#34;], \u0026#34;labels\u0026#34;: { \u0026#34;instance\u0026#34;: \u0026#34;node\u0026#34;, \u0026#34;job\u0026#34;: \u0026#34;server-134\u0026#34; } } ] 然后在prometheus目录下新增如下配置:\n1 2 3 4 - job_name: \u0026#39;file_sd\u0026#39; file_sd_configs: - files: - targets.json ","permalink":"https://ktzxy.top/posts/mhlal77mdf/","summary":"Prometheus概述","title":"Prometheus概述"},{"content":"﻿\u0026gt; ### 本套实验来自《数据库原理及应用 微课视频版》李唯唯主编，个人学习，仅供参考。\n1、创建数据库 使用SQL Server Management Studio管理平台创建教材示例数据库supermarketDB——校园超市数据库。已创建路径D:\\salesystem。命名数据文件的逻辑名为supermarketDB_data，物理文件名为supermarketDB_data.mdf，存放在上述已建立路径下，初始大小为10MB，最大200MB，自动增长增量为5MB。命名数据库逻辑文件的逻辑名称为supermarketDB_log，物理文件名为supermarketDB_data.ldf，存放在上述已建立路径下，初始大小为8MB，最大100MB，自动增长增量为4MB。\n2、使用SQL语句完成数据库的创建和维护： （1）创建一个只设置名称的数据库，数据库名为dbtest；\n1 create database dbtest; （2）创建一个包含数据文件和日志文件的数据库sjkDB。已创建路径D:\\teaching。命名数据文件的逻辑名为sjkDB_data，物理文件名为sjkDB_data.mdf，存放在上述已建立路径下，初始大小为6MB，最大60MB，自动增长增量为2MB。命名数据库逻辑文件的逻辑名称为sjkDB_log，物理文件名为sjkDB_data.ldf，存放在上述已建立路径下，初始大小为3MB，最大30MB，自动增长增量为1MB。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 create database sjkDB on ( name=sjkDB_data, filename=\u0026#39;D:\\teaching\\sjkDB_data.mdf\u0026#39;, size=6, maxsize=60, filegrowth=2 ) log on ( name=sjkDB_log, filename=\u0026#39;D:\\teaching\\sjkDB_log.ldf\u0026#39;, size=3, maxsize=30, filegrowth=1 ) （3）修改数据库sjkDB种数据文件的初始大小，将其初始大小改为9MB，最大为120MB。\n1 2 3 4 5 6 7 alter database sjkDB modify file ( name=sjkDB_data, size=9, maxsize=120 ) （4）为数据库sjkDB添加新的日志文件，逻辑名称为sjkDBlog1，存储路径为D:\\teaching，物理路径名为sjkDBlog1.ldf，初始大小3MB，增量1MB，最大20MB。\n1 2 3 4 5 6 7 8 9 alter database sjkDB add log file ( name=sjkDBlog1, filename=\u0026#39;D:\\teaching\\sjkDBlog1.ldf\u0026#39;, size=3, maxsize=20, filegrowth=1 ) （5）将数据库dbtest更名为test_1。\n1 alter database dbtest modify name=test_1 （6）删除数据库test_1。\n1 drop database test_1 3、修改配置 使用SQL语句创建数据库students，数据文件初始大小6MB，增量1MB，最大100MB；日志文件初始大小3MB，增量10%，最大80MB，存放在E盘。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 create database students on ( name=students_data, filename=\u0026#39;E:\\students_data.mdf\u0026#39;, size=6, maxsize=100, filegrowth=1 ) log on ( name=students_log, filename=\u0026#39;E:\\students_log.ldf\u0026#39;, size=3, maxsize=80, filegrowth=10% ) ","permalink":"https://ktzxy.top/posts/cl0twqj501/","summary":"实验1 数据库的创建与管理","title":"实验1 数据库的创建与管理"},{"content":"1. 面向对象概念 Java 语言提供类、接口和继承等面向对象的特性，为了简单起见，只支持类之间的单继承，但支持接口之间的多继承，并支持类与接口之间的实现机制（关键字为 implements）。Java 语言全面支持动态绑定，而 C++语言只对虚函数使用动态绑定。总之，Java 语言是一个纯的面向对象程序设计语言。\n1.1. 面向对象的特征 面向对象有以下四大特征：\n抽象：是将一类对象的共同特征总结出来构造类的过程，包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为，并不关注这些行为的细节是什么。 继承：是从已有类得到继承信息创建新类的过程。提供继承信息的类被称为父类(超类、基类)，得到继承信息的类被称为子类(派生类)。继承让变化中的软件系统有了一定的延续性，同时继承也是封装程序中可变因素的重要手段。 封装：通常认为封装是把数据和操作数据的方法绑定起来，对数据的访问只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。在类中编写的方法就是对实现细节的一种封装；编写一个类就是对数据和数据操作的封装。可以说，封装就是隐藏一切可隐藏的东西，只向外界提供最简单的编程接口。 多态：是指允许不同子类型的对象对同一消息作出不同的响应。简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分为编译时的多态性和运行时的多态性。方法重载（overload）实现的是编译时的多态性（也称为前绑定），而方法重写（override）实现的是运行时的多态性（也称为后绑定）。运行时的多态是面向对象最精髓的东西，要实现多态需要做两件事： 方法重写（子类继承父类并重写父类中已有的或抽象的方法）。 对象造型（用父类型引用引用子类型对象，这样同样的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为）。 1.2. 面向对象五大基本原则 单一职责原则 SRP(Single Responsibility Principle)：类的功能要单一 开放封闭原则 OCP(Open－Close Principle)：一个模块对于拓展是开放的，对于修改是封闭的 里式替换原则 LSP(the Liskov Substitution Principle LSP)：子类可以替换父类出现在父类能够出现的任何地方 依赖倒置原则 DIP(the Dependency Inversion Principle DIP)：高层次的模块不应该依赖于低层次的模块，他们都应该依赖于抽象。抽象不应该依赖于具体实现，具体实现应该依赖于抽象 接口分离原则 ISP(the Interface Segregation Principle ISP)：设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好 2. Java 对象和类 2.1. 概念 类：是一个模板，它描述一类对象的行为和状态。 对象：是类的一个实例，有状态和行为。 2.2. 类的定义 使用 class 关键字来定义类。语法如下：\n1 2 public class 类的名称 { } 2.2.1. 类的属性 一个类可以包含以下类型变量：\n局部变量：在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中，方法结束后，变量就会自动销毁。 成员变量：成员变量是定义在类中，方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。 类变量：类变量也声明在类中，方法体之外，但必须声明为 static 类型。 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Dog { String breed; int size; static String color; int age; void eat(String food) { } void run() { } void sleep(){ } } Notes: 通过对象直接访问成员变量，会存在数据安全问题。通常会将成员变量使用 private 关键字修饰，让成员变量只能在本类中被访问。如果其他类需要访问 private 修饰的成员变量，则要提供相应的getXxx()和setXxx()用于获取和设置成员变量的值，方法用 public 修饰\n2.2.2. 构造方法 每个类都有构造方法。如果没有显式地为类定义构造方法，Java 编译器将会为该类提供一个默认无参构造方法。\n在创建一个对象的时候，至少要调用一个构造方法。构造方法的名称必须与类同名，一个类可以有多个构造方法。\n1 2 3 4 5 6 7 8 9 public class Puppy{ // 默认无参构造方法 public Puppy(){ } // 这个构造器仅有一个参数：name public Puppy(String name){ } } 构造方法的特性：\n名字与类名相同 没有返回值，也不能使用 void 声明构造函数 创建类的对象时自动调用 Notes:\n构造方法会在创建对象的时候自动调用 在创建的时候，构造方法只会被调用一次。而类的其他方法，可以通过对象来反复调用多次 如果在类中手动定义了构造方法后，则不会再为该类提供默认的无参构造方法。 2.3. 对象 使用一个类，其实就是使用该类的成员(成员变量和成员方法)。而要想使用一个类的成员，就必须首先拥有该类的对象。\n2.3.1. 创建对象 对象是根据类创建的。在Java中，使用关键字 new 来创建一个新的对象。创建对象需要以下三步：\n声明：声明一个对象，包括对象名称和对象类型。 实例化：使用关键字 new 来创建一个对象。 初始化：使用 new 创建对象时，会调用构造方法初始化对象。 1 类名 对象名 = new 类名(); 2.3.2. 访问实例变量和方法 通过已创建的对象来访问成员变量和成员方法，如下所示：\n1 2 3 4 5 6 /* 实例化对象 */ Object referenceVariable = new Constructor(); /* 访问类中的变量 */ referenceVariable.variableName; /* 访问类中的方法 */ referenceVariable.methodName(); 注：示例的变量与方法均为 public 修饰\n2.3.3. Java 中使用变量，遵循“就近原则” 在使用变量时需要遵循的原则为：就近原则。顺序如下：\n局部 -\u0026gt; 本类成员 -\u0026gt; 父类成员 -\u0026gt; Object 有就使用，没有就报错。\n2.4. 匿名对象 2.4.1. 匿名对象定义语法格式 正常有名字的对象：\n1 Person p = new Person(); 匿名对象：\n1 new Person(); 当方法的返回值是对象的话，可以继续调用方法\n1 2 3 4 5 6 7 8 9 10 11 12 public class Person { // ...省略 public Person show() { System.out.println(this.name + \u0026#34;xxx\u0026#34;); return this; } // show 之后的返回值是对象，所以可以继续调用方法method public static void main(String[] args) { int age = new Person().show().getAge(); } } 2.4.2. 匿名对象的特点 没有任何引用变量指向。 在调用完方法后就会成垃圾，会在垃圾回收器(gc)空闲的时候将对象进行回收。 2.4.3. 匿名对象和有名对象调用方法的区别 有名对象：在创建对象的代码量多些，后续可以使用变量名多次调用方法。 匿名对象：在创建对象的代码量少些，但创建对象后只能调用一次方法。 2.4.4. 使用场景 当对象只调用一个成员方法一次的时候，后面不再使用这个对象，可以使用匿名对象(减少代码量) 作为方法的实参参数 2.5. 成员变量和局部变量的区别 在类中的位置不同 成员变量：类中，方法外 局部变量：方法中或者方法声明上(形式参数) 作用域不同 成员变量：针对整个类有效 局部变量：只在某个范围内有效。(一般指的就是方法块内) 在内存中存储的位置不同 成员变量：存储在堆内存中 局部变量：存储在栈内存中 生命周期不同 成员变量：随着对象的创建而存在，随着对象的消失而消失 局部变量：在方法被调用，或者语句被执行的时候存在；当方法调用完，或者语句结束后，就自动释放 初始化值的问题 成员变量：有默认值(int/byte/short/lnng 默认是 0；char 默认是 \\u0000；double/float 默认是 0.0；boolean 默认是 false) 局部变量：没有默认值。必须先定义，赋值，才能使用 2.6. 类实例化的顺序（重点） Java 类的实例化的顺序是：静态变量 -\u0026gt; 静态代码块 -\u0026gt; 成员变量（全局变量） -\u0026gt; 初始化代码块 -\u0026gt; 构造函数。测试示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class InitSequenceBean { private static String staticStr = initStaticMember(); private String str = initOrdinaryMember(); static { System.out.println(\u0026#34;静态代码块执行了....\u0026#34;); } { System.out.println(\u0026#34;初始化代码块执行了....\u0026#34;); } public InitSequenceBean() { System.out.println(\u0026#34;无参构造函数执行了....\u0026#34;); } private static String initStaticMember() { System.out.println(\u0026#34;静态成员变量初始化....\u0026#34;); return \u0026#34;123\u0026#34;; } private String initOrdinaryMember() { System.out.println(\u0026#34;普通成员变量初始化....\u0026#34;); return \u0026#34;abc\u0026#34;; } } 测试代码与结果\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testInitializationSequence() { InitSequenceBean bean = new InitSequenceBean(); System.out.println(bean); /* * 测试结果： * 静态成员变量初始化.... * 静态代码块执行了.... * 普通成员变量初始化.... * 初始化代码块执行了.... * 无参构造函数执行了.... * com.moon.java.basic.InitSequenceBean@ba8a1dc */ } 3. Java 方法 3.1. 概念 Java方法是语句的集合，它们在一起执行一个功能。\n方法是解决一类问题的步骤的有序组合 方法包含于类或对象中 方法在程序中被创建，在其他地方被引用 1 2 3 4 5 6 7 8 9 10 11 public class Dog { void eat(String food) { } void run() { } void sleep(){ } } 一个类可以拥有多个方法，在上面的例子中：eat()、run()、sleep() 都是 Dog 类的方法。\n3.2. 方法的定义语法 一般情况下，定义一个方法包含以下语法：\n1 2 3 4 5 6 修饰符 返回值类型 方法名(参数类型 参数名1, 参数类型 参数名2, ...){ ... 方法体 ... return 返回值; } 方法包含一个方法头和一个方法体。下面是一个方法的所有部分：\n修饰符：修饰符，这是可选的，告诉编译器如何调用该方法。定义了该方法的访问类型。 返回值类型：方法可能会有返回值，returnValueType 是用于限定方法返回值的数据类型。有些方法执行所需的操作，但没有返回值。在这种情况下，returnValueType 是关键字 void。 方法名：是方法的实际名称。方法名和参数表共同构成方法签名。 参数类型与参数名：参数像是一个占位符。当方法被调用时，传递值给参数。这个值被称为实参或变量。参数列表是指方法的参数类型、顺序和参数的个数。参数是可选的，方法可以不包含任何参数。 方法体：方法体包含具体的语句，定义该方法的功能。 return（可选）：结束方法，若有返回值，则返回给调用者。 3.3. 形参 \u0026amp; 实参 3.3.1. 概念 参数在程序语言中分为：\n实参（实际参数、argument）：用于传递给函数/方法的参数，可以是常量、变量、表达式、函数等。无论实参是何种类型，在调用函数时，它们都必须具有确定的值， 以便把这些值传送给方法的形参。 形参（形式参数、parameter）：由于它不是实际存在的变量，所以又称虚拟变量。用于定义函数/方法时使用的参数列表，用于接收调用该函数时传入的实参，将实参赋值给形参。因而，调用函数/方法时必须注意实参的个数、类型应与形参一一对应，并且实参必须要有确定的值。 1 2 3 4 5 6 7 String hello = \u0026#34;Hello!\u0026#34;; // hello 为实参 sayHello(hello); // str 为形参 void sayHello(String str) { System.out.println(str); } 3.3.2. 形参与实参使用总结 定义位置与使用范围：形参出现在函数定义中，在整个函数体内都可以使用，离开该函数则不能使用；实参出现在主调函数中，进入被调函数后，实参变量也不能使用。 两者的功能是数据传送：函数调用时，主调函数把实参的值传送给被调函数的形参从而实现主调函数向被调函数的数据传送。 形参变量只有在被调用时才分配内存单元，在调用结束时， 即刻释放所分配的内存单元。因此，形参只有在函数内部有效。函数调用结束返回主调函数后则不能再使用该形参变量。 在进行函数调用时，实参都必须具有确定的值，以便把这些值传送给形参。 实参和形参在数量上、类型上、顺序上应严格一致，否则会发生“类型不匹配”的错误。 函数调用中发生的数据传送是单向的。只能把实参的值传送给形参，而不能把形参的值反向地传送给实参。因此在函数调用过程中，形参的值发生改变，而实参中的值不会变化。 当形参和实参不是引用类型时，在该函数运行时，形参和实参是不同的变量，它们在内存中位于不同的位置，形参将实参的内容复制一份，在该函数运行结束的时候形参被释放，而实参内容不会改变；而如果函数的参数是引用类型变量，在调用该函数的过程中，传给函数的是实参的地址（也是复制的），在函数体内部使用的也是实参的地址，即使用的就是实参本身。所以在函数体内部可以改变实参对象中的值，但不能改变其引用地址！！ 3.4. 值传递 \u0026amp; 引用传递 程序设计语言将实参传递给方法（或函数）的方式分为两种：\n值传递：指的是在方法调用时，方法接收的是实参值的拷贝，会创建副本，传递后就互不相关了。 引用传递：指的是在方法调用时，方法接收的直接是实参所引用的对象在堆中的地址，不会创建副本，即传递前和传递后都指向同一个引用（也就是同一个内存空间）。对形参的修改将影响到实参。 很多程序设计语言（比如 C++、 Pascal）提供了两种参数传递的方式，值得注意的是，在 Java 中只有值传递。\n3.5. Java 只有通过值传递参数 调用一个方法时必须按照参数列表指定的顺序提供参数。\nNotes: 程序设计语言中有关将参数传递给方法（或函数）的一些专业术语。**按值调用(call by value)表示方法接收的是调用者提供的值，而按引用调用（call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值，而不能修改传递值调用所对应的变量值。**它用来描述各种程序设计语言（不只是Java)中方法参数传递方式。\nJava 程序设计语言总是采用按值调用。即：方法得到的是所有参数值的一个拷贝，方法不能修改传递给它的任何参数变量的内容。\n案例证明：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Person { private String name; // 省略构造函数、Getter\u0026amp;Setter方法 } public static void main(String[] args) { Person xiaoZhang = new Person(\u0026#34;小张\u0026#34;); Person xiaoLi = new Person(\u0026#34;小李\u0026#34;); swap(xiaoZhang, xiaoLi); System.out.println(\u0026#34;xiaoZhang:\u0026#34; + xiaoZhang.getName()); System.out.println(\u0026#34;xiaoLi:\u0026#34; + xiaoLi.getName()); } public static void swap(Person person1, Person person2) { Person temp = person1; person1 = person2; person2 = temp; System.out.println(\u0026#34;person1:\u0026#34; + person1.getName()); System.out.println(\u0026#34;person2:\u0026#34; + person2.getName()); } 输出结果：\n1 2 3 4 person1:小李 person2:小张 xiaoZhang:小张 xiaoLi:小李 解析：两个引用类型的形参互换并没有影响实参啊！因为 swap 方法的参数 person1 和 person2 只是拷贝的实参 xiaoZhang 和 xiaoLi 的地址。因此，person1 和 person2 的互换只是拷贝的两个地址的互换罢了，并不会影响到实参 xiaoZhang 和 xiaoLi。\n3.6. 方法重载(Overload) 方法重载(overloading) 是在一个类里面，定义方法名字相同，而参数不同、返回类型可以相同也可以不同的相关方法（与返回值类型无关）。每个重载的方法（或者构造函数）都必须有一个独一无二的参数类型列表。最常见就是构造器的重载。\n重载(Overload)是一个类中多态性的一种表现。方法重载的规则如下：\n被重载的方法必须改变参数列表(参数个数、类型、顺序，其中一项不相同即可) 被重载的方法可以改变返回类型 被重载的方法可以改变访问修饰符 被重载的方法可以声明新的或更广的检查异常 方法能够在同一个类中或者在一个子类中被重载 Notes: 不能以返回值类型、访问权限、抛出的异常（数量）等不同作为重载函数的区分标准\n3.7. 方法的特点总结 在调用方法的时候，Java 虚拟机会通过参数列表的不同来区分同名的方法。方法有如下特点：\n方法不调用，不执行。 编写方法的时候不能嵌套定义，但是能嵌套调用。 写方法不能提高程序的效率。 方法能提高代码的复用性。 方法的参数如果是基本数据类型：形式参数的改变不影响实际参数。 如果参数是引用数据类型：形式参数的改变（只是引用对象内的属性值）直接影响实际参数 3.8. 扩展：native（本地）方法 native 方法表示该方法要用另外一种依赖平台的编程语言实现的。\n例如，FileOutputStream 类需要与硬件打交道，底层的实现用的是操作系统相关的 API 实现。若在 windows 系统则需要用 c 语言实现的，所以，查看 jdk 的源代码，可以发现 FileOutputStream 的 open 方法的定义如下：\n1 2 3 4 5 private void open(String name, boolean append) throws FileNotFoundException { open0(name, append); } private native void open0(String name, boolean append) throws FileNotFoundException; 使用 Java 是无法直接调用别人写的 C 语言函数，需要按照 Java 的要求写一个 C 语言的函数，再用这个 C 语言函数去调用别人的 C 语言函数。由于是按 Java 的要求来写的，所以这个 C 语言函数就可以与 Java 对接上，Java 那边的对接方式就是定义出与这个 C 函数相对应的方法，Java 中对应的方法不需要写具体的代码，但需要在前面声明 native。\nTips: native 方法不能是抽象的，不能与 abstract 混用\n4. 组合和继承 Java中类与类之间常见的关系\n组合关系 继承关系 代理模式关系 4.1. 组合 一个类型A中的成员变量的数据类型是类型B时，此时A和B就是组合关系。\n即A类中的成员变量的数据类型是B类。例如：Student 类中 String 类型的属性就是组合关系。\n4.2. 继承 4.2.1. 概念 继承就是子类继承父类的特征和行为，使得子类对象（实例）具有父类的实例域和方法，或子类从父类继承方法，使得子类具有父类相同的行为。\n继承是面向对象四大特征(封装、继承、多态、抽象)之一，面向对象重点。是类与类之间关系的一种。从类与类之间设计角度看，子类必须是父类的一种才可能使用继承\nTips: 继承是实现多态的前提。\n4.2.2. 继承的好处 解决代码复用的常用方式 提高了代码的扩展性 为多态提供前提条件 4.2.3. 继承的语法格式 (extends 关键字) 使用 extends 关键字来实现继承，而且所有的类都是继承于 java.lang.Object，当一个类没有继承相关的关键字时，则默认继承 Object（这个类在 java.lang 包中，不需要 import 导入）祖先类。\n1 2 public class 子类名 extends 父类名{ } 4.3. 继承的类型 Java 只支持单继承，不支持多继承(即 extends 后面不能跟多个父类)。但支持多层继承，例如：\n1 2 3 Foo extends Bar; // 这样 Foo 就是间接继承了父类 SuperBar Bar extends SuperBar; 4.4. 继承的特点 子类拥有(除构造方法以外)父类的所有成员(成员变量和成员方法) 子类能够直接访问父类非 private 修饰的成员 子类可以在父类的基础上添加特有属性 子类可以对父类的方法进行功能扩展(方法重写) 子类可以在父类的基础上添加特有的方法 构造方法不能被继承，但是子类可以通过 super 关键字间接调用父类的构造方法。实际中一般成员变量都用 private 修饰，父类的成员变量要提供相应的 getXxx/setXxx 方法让子类调用来访问 4.5. 继承的注意事项 每个类都直接或者间接继承 Object 父类。当一个类没有明显继承其他类时，都隐藏一个 extends Object 的父类 Object 类是所有类的父类（超类） 子类并不是父类的一个子集。实际上，一个子类通常比它的父类包含更多的信息和方法。一般情况下，最好能为每个类提供一个无参构造方法，以便于对该类进行扩展，同时避免错误。 4.6. 方法重写(Override) 4.6.1. 概念与语法规则 重写是子类对父类的允许访问的方法的实现逻辑进行重新编写，即跟父类的方法声明完全一样，只是方法体核心逻辑不一样！方法重写有如下要求：\n需要存在父子关系 方法名相同 参数列表相同(类型、个数、顺序均一致) 返回值类型相同，或者是原方法返回的子类类型 Notes: 与方法重载不一样地方是：方法名、参数列表、返回值全部一致。（返回值可以是子类）\n4.6.2. 方法重写的注意事项 子类重写父类的方法后，通过子类对象调用的是重写后的方法。 子类在重写父类方法时，访问权限修饰符要大于或等于(\u0026gt;=)父类的方法访问权限修饰符。(private \u0026lt; 默认 \u0026lt; protected \u0026lt; public)。一般重写方法都修饰符一致即可。 如果父类的方法用 private 修饰，则不能被子类重写，即使子类有声明一样的方法，也不属于重写，属于定义了一个同名的新方法 当需要扩展父类方法(父类方法功能不能满足需求时)，就可以使用方法重写。最常用的用法是，在重写的方法中用 super.xxx()，这样既保留父类的功能，双达到增强功能的效果。 1 2 3 4 5 @Override public void xxx(){ super.xxx(); .... } 重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如：父类的一个方法申明了一个检查异常 IOException，但是在重写这个方法的时候不能抛出 Exception 异常，因为 Exception 是 IOException 的父类，抛出 IOException 异常或者 IOException 的子类异常。 被 final 关键字修饰的方法不能被重写。 构造方法不能被重写。 如果某个类与另一个类不存在继承关系，则不能重写对方的方法。 子类与父类所在包的位置，会影响可重写的方法范围也有区别： 子类和父类在同一个包中，那么子类可以重写父类中声明为非 private 和非 final 的所有方法。 子类和父类不在同一个包中，那么子类只能够重写父类的声明为 public 和 protected 修饰的非 final 方法。 声明为 static 的方法不能被重写，但是能够被再次声明（相当于方法重载）。 4.6.3. @Override 注解 @Override\t注解用来修饰方法，表示该方法是重写父类的方法。如果修饰的方法在父类中没有找到，则编译失败。\n1 2 3 4 5 6 7 8 9 10 11 12 class Animal{ public void move(){ System.out.println(\u0026#34;动物可以移动\u0026#34;); } } class Dog extends Animal{ @Override public void move(){ System.out.println(\u0026#34;狗可以跑和走\u0026#34;); } } 4.6.4. 重写与重载的区别 区别点 重载方法 重写方法 参数列表 不能相同 必须一致 返回类型 可以不相同 必须一致 异常 可以不相同 可以减少或删除，一定不能抛出新的或者更广的异常 访问权限 可以不相同 一定不能做更严格的限制（可以降低限制） 5. 内部类 5.1. 概述 将一个类定义在另一个类中或另一个类的方法中的类，该类就称为内部类。内部类是一个相对概念。有以下特点：\n内部类可以直接访问任何外部类的成员，包括 private 修饰的。 外部类编译后会出现两个 class 文件。内部类生成的 class 文件的命名：外部类名$内部类名.class 内部类分为成员内部类与局部内部类。定义时是一个正常定义类的过程，同样包含各种修饰符、继承与实现关系等。 在日常的企业级开发中，很少会使用到内部类来实现业务逻辑，一般用匿名内部类或成员内部类 5.2. 内部类4种类型 静态内部类 成员内部类 局部内部类 匿名内部类 5.3. 静态内部类 5.3.1. 定义 定义在类内部的静态类，称为静态内部类。\n1 2 3 4 5 6 7 8 9 10 public class Out { private static int a; private int b; public static class Inner { public void print() { System.out.println(a); } } } 例如：Java 集合类 HashMap 内部就有一个静态内部类 Entry。Entry 是 HashMap 存放元素的抽象，HashMap 内部维护 Entry 数组用了存放元素，但是 Entry 对使用者是透明的。像这种和外部类关系密切的，且不依赖外部类实例的，都可以使用静态内部类。\n5.3.2. 使用与特点 静态内部类只能访问外部类所有的静态变量和静态方法，即使是 private 的也一样。如果要访问外部类的成员变量与方法，则必须要new一个外部类的对象，通过对象去访问成员变量与方法。 静态内部类和一般类一致，可以定义静态变量、方法，构造方法等。 其它类使用静态内部类需要使用 外部类.静态内部类 方式创建一个静态内部类对象，如下所示： 1 2 Out.Inner inner = new Out.Inner(); inner.print(); 5.4. 成员内部类 5.4.1. 成员内部类定义 定义在类内部的非静态类叫作成员内部类，定义在成员位置的，与成员变量同级。定义格式：\n1 2 3 4 5 6 7 8 public class Outer{ private String a; class Inner{ // 其他代码 // 成员变量 // 成员方法 } } 5.4.2. 访问方式 访问方式1：间接访问。在外部为中提供一个方法，在该方法中创建内部类的对象。 访问方式2：直接访问。外部类.内部类 变量名 = new 外部类().new 内部类(); 1 Outer.Inner x = new Outer().new Inner(); 5.4.3. 使用注意事项 成员内部类可以访问外部类所有的变量和方法，包括静态和非静态，私有和公有。 成员内部类不能定义静态方法和静态变量（final 修饰的除外），因为成员内部类是非静态的，而在 Java 的非静态代码块中不能定义静态方法和静态变量。 当内部类和外部类出现同名的成员时，默认访问的是内部类的成员，内部类如果要访问外部类的成员，则需要使用以下格式： 1 2 外部类.this.成员变量; 外部类.this.成员方法(参数列表); 外部类要访问成员内部类的属性或者调用方法，必须要创建一个内部类的对象，使用该对象访问属性或者调用方法。 其他的类要访问普通内部类的属性或者调用普通内部类的方法，必须要在外部类中创建一个成员内部类类型的对象作为属性，外同类可以通过该属性访问到成员内部类的对象，再通过成员内部类的对象来调用方法或者访问内部类的属性 5.4.4. 使用场景 当一个类只被一个类使用时，就可以该类定义为某一个类的内部类。 在描述事物A时，发现事物A中还包含了另一类事物B，而且事物B要使用到事物A的一些成员数据，此时就可以将事物 B定义为事物A的内部类。 5.4.5. 使用案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public static void main(String[] args) { // 创建汽车对象 Car c = new Car(true); Car.Engine e = c.new Engine(); // 将创建出来的发动机对象调用work方法 e.work(); System.out.println(\u0026#34;================\u0026#34;); // 另一种创建内部类对象的方法 Car.Engine e1 = new Car(false).new Engine(); e1.work(); } // 新建一个发动机类 class Car { private boolean status; public Car(boolean status) { super(); this.status = status; } public Car() { super(); } // 新建一个发动机内部类 class Engine { // 直接使用car类中的成员变量 public void work() { if (status) {\t// 内部类可以直接访问任何外部类的成员，包括 private 修饰的。 System.out.println(\u0026#34;发动机就飞速旋转。\u0026#34;); } else { System.out.println(\u0026#34;发动机停止工作。\u0026#34;); } } } } 5.5. 局部内部类 定义在外部类的某一个成员方法中的类，叫作局部内部类。定义格式：\n1 2 3 4 5 6 7 public class Outer{ public void method{ class Inner{ //其他代码 } } } 定义在实例方法中的局部类可以访问外部类的所有变量和方法；定义在静态方法中的局部类只能访问外部类的静态变量和方法\n访问格式：只能在成员方法内部创建该内部类的对象，调用相关方法。\n使用场景：如果一个类只在某个方法中使用，则可以考虑使用局部类。\n注意事项：\n局部内部类不能使用权限修饰符。 局部内部类中如果要访问方法中的局部变量， JDK1.8 之前该局部变量需要使用 final 修饰。 JDK1.8 之后该局部变量可以不用 final 修饰，但也不能修改。 5.6. 匿名内部类 5.6.1. 概述与前提 匿名内部类是指，通过继承一个父类或者实现一个接口的方式，直接定义并创建实例对象的类。\n匿名内部类只继承一个父类或者实现一个接口，同时它也是没有 class 关键字，这是因为匿名内部类是直接使用 new 关键字生成一个对象的引用。可以理解为将定义子类与创建子类对象两个步骤由一个格式一次完成，两个步骤是连在一起的、即时的。\nTips: 匿名内部类是局部内部类的一种。\n5.6.2. 匿名内部类使用格式 匿名内部类如果不定义变量引用，则也是匿名对象。格式如下：\n1 2 3 new (父)类或接口(){ // 重写需要重写的方法 }; 5.6.3. 匿名内部类使用说明 过程： 临时定义一个类型的子类 定义后即刻创建刚刚定义的这个类的对象 目的： 匿名内部类是创建某个类型子类对象的快捷方式。 我们为了临时定义一个类的子类，并创建这个子类的对象而使用匿名内部类。 常见问题： 匿名内部类无法定义构造方法，因为没有类名； 匿名内部类可以定义特有的方法和变量；但是无法去访问。所以一般不会这样添加。 特点： 匿名内部类编译后也会出现两个 class 文件。匿名内部类生成的 class 文件的命名：外部类名$1.class, 如果第2次创建的话，就是外部类名$2.class，如此类推。例：Test2_02$1.class 创建出来的匿名内部类对象可以直接使用一次。可以用于直接赋值，也可以将地址值赋给父类/接口类型的对象。这样可以多次使用。匿名内部类主要是省了不用创建子类.java。 匿名内部类必须继承一个抽象类或者实现一个接口 匿名内部类不能定义任何静态成员和静态方法 当所在的方法的形参需要被匿名内部类使用时，必须声明为 final。因为生命周期不一致，局部变量直接存储在栈中，当方法执行结束后，非final的局部变量就被销毁。而局部内部类对局部变量的引用依然存在，如果局部内部类要调用局部变量时，就会出错。声明 final 可以确保局部内部类使用的变量与外层的局部变量区分开，解决生命周期不一致的问题。 匿名内部类不能是抽象的，它必须要实现继承的类或者实现的接口的所有抽象方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public static void main(String[] args) { // 定义匿名内部类，调用参加运动会的方法 Sport s = new Sport() { @Override public void run() {} }; enter(s); System.out.println(\u0026#34;===========================\u0026#34;); // 匿名内部类，其实就是在一个类中直接创建一个匿名的子类对象，可以直接使用。 // 如果需要多次使用的话，要用一个父类对象或接口对象接收。 enter(new Sport() { @Override public void run() {} }); } // 定义一个方法，以接口作为参数，输入参加是否参加运会 public static void enter(Sport s) { System.out.println(\u0026#34;参加运动会，奔跑吧。\u0026#34;); } 5.7. 内部类的优点 一个内部类对象可以访问创建它的外部类对象的内容，包括私有数据！ 内部类不为同一包的其他类所见，具有很好的封装性； 内部类有效实现了“多重继承”，优化 java 单继承的缺陷。 匿名内部类可以很方便的定义回调。 5.8. 内部类的应用场景 一些多算法场合 解决一些非面向对象的语句块 适当使用内部类，使得代码更加灵活和富有扩展性 当某个类除了它的外部类，不再被其他的类使用时 6. 抽象类与抽象方法 6.1. 抽象方法的概念 使用 abstract 修饰，没有方法体的方法就是抽象方法。抽象方法没有定义，方法名后面直接跟一个分号，而不是花括号。\n抽象方法定义格式：\n1 权限修饰符(public) abstract 返回值类型 方法名(参数列表); Notes: 该方法的具体实现由它的子类确定\n6.2. 抽象类的概念 被 abstract 修饰的类就是抽象类，具有抽象方法的类就是必须是一个抽象类。抽象类除了不能实例化对象之外，类的其它功能依然存在，成员变量、成员方法和构造方法的访问方式和普通类一样。\n抽象类的作用是：用来描述一种类型应该具备的基本特征和行为(功能/方法)，实现这些功能就由子类通过方法重写来完成。\n抽象类的定义格式：\n1 2 3 权限修饰符(public) abstract class 类名{ // ... } Tips:\n子类继承抽象类的话，就必须要重写抽象类中的抽象方法 什么情况下要定义抽象类和抽象方法？当某种功能(方法)无法确定的时候，子类都需要重写该方法的时候，就将方法定义成抽象方法。 6.3. 抽象类的特点 抽象类和抽象方法必须使用 abstract 修饰。 抽象类不能直接创建对象（初学者很容易犯的错）。如果被实例化，就会报错，编译无法通过。只能通过子类继承抽象类，进行创建子类对象，调用重写方法等操作。 不能直接创建的原因是，如果可以创建对象后，就是说对象可以调用抽象类中的抽象方法，但抽象方法没有什么作用，这样调用就没有意义。为了避免这个不必要的操作，就规定抽象类不能直接创建对象。\n抽象类中是可以不定义抽象方法(但一般不会这样操作，在适配器模式会使用到)，但是有抽象方法的类必定是抽象类。 构造方法，类方法（用 static 修饰的方法）不能声明为抽象方法。因为抽象方法需要被子类实现的，静态方法无法被重写。 子类继承了抽象类时必须重写抽象类中的所有抽象方法，否则该子类也要定义为抽象类。 抽象类中可以有构造方法，意义是让子类可以能过 super 调用父类的构造方法给父类的成员变量赋值 一个抽象类不作为父类出现，没有任何意义。 抽象类中可以定义普通方法(非抽象方法)。 抽象关键字 abstract 不能和 private 关键字一起使用。因为一般继承了抽象类的子类，要求要重写全部的抽象方法，如果用 private 关键字修饰了，就不能重写该方法，就相互矛盾。 抽象类可以继承普通类。 抽象类可以定义静态的 main 方法。 Tips: 只要记住抽象类与普通类的唯一区别就是不能创建实例对象和允许有 abstract 修饰的方法。要理解抽象类的本质和作用，可以思考自己就是 Java 语言的设计者，你是否会提供这样的支持，如果不提供的话，有什么理由吗？如果你没有道理不提供，那答案就是肯定的了。\n7. this / super 关键字 7.1. this 关键字的作用 Java 中使用变量，遵循“就近原则”：局部 -\u0026gt; 本类成员 -\u0026gt; 父类成员 -\u0026gt; Object 有就使用，没有就报错。\nthis 关键字可以用来解决局部变量和成员变量重名的问题。如：\n代表本类当前对象的引用（谁调用，this 就代表谁） 调用本类的其他构造方法 访问本类的其他成员（成员变量和成员方法） 使用时注意问题：不能在静态方法中使用 this 关键字。\n7.2. this / super 的使用 this 关键字用于访问本类的成员变量、成员方法、构造方法等；而当父类和子类出现同名的成员变量与成员方法时，则需要通过super关键来访问父类的成员变量与成员方法。\n7.2.1. this / super 访问成员变量 this.成员变量：先在本类中查找，本类找不到，去父类找，父类直到到找不到则报错 super.成员变量名：直接去父类中查找指定的成员变量，如果父类中没有，再往上找，找到 Object 没有，则报错。 7.2.2. this / super 访问成员方法 this.成员方法(参数列表)：先调用本类的，本类找不到，则调用父类的，父类直到到找不到则报错。 super.成员方法(参数列表)：直接去父类中查找指定的成员方法，如果父类中没有，再往上找，找到 Object 没有，则报错。 7.2.3. this / super 访问构造方法 this 访问本类构造方法(注意：没有\u0026quot;.\u0026quot;)：\nthis()：访问本类无参数构造方法。 this(参数列表)：访问本类有参数构造方法。 super 访问父类的构造方法(注意：没有\u0026quot;.\u0026quot;)\nsuper()：调用父类无参构造方法 super(参数列表)：调用父类有参构造方法 this/super 调用构造方法的注意事项：\n必须是在构造方法中使用，并且在第一行有效语句。不能在非构造方法中通过 this()/super() 调用本类/父类构造方法。 this 只会在本类找对应构造方法，不会去父类查找，因为父类的构造方法是不能被继承的。 this() 和 super() 不能同时出现在构造方法中。 7.3. this 与 super 的区别 super：它引用当前对象的直接父类中的成员（用来访问直接父类中被隐藏的父类中成员数据或函数，基类与派生类中有相同成员定义时如：super.变量名、super.成员函数据名（实参） this：它代表当前对象名（在程序中易产生二义性之处，应使用 this 来指明当前对象；如果函数的形参与类中的成员数据同名，这时需用 this 来指明成员变量名） super() 和 this() 的用法类似，均需放在构造方法内第一行。区别是，super() 在子类中调用父类的构造方法，this() 在本类内调用本类的其它构造方法。 super() 和 this() 不能同时出现在一个构造函数里面，因为 this 必然会调用其它的构造函数，其它的构造函数必然也会有 super 语句的存在，所以在同一个构造函数里面有相同的语句，就失去了语句的意义，编译器也不会通过。 this() 和 super() 都指的是对象，所以，均不可以在 static 环境中使用。包括：static 变量、static 方法、static 语句块。 尽管可以用 this 调用一个构造器，但却不能调用两个。从本质上讲，this 是一个指向本对象的指针，然而 super 是一个 Java 关键字。\n7.4. 构造方法调用注意事项 当通过子类创建对象时，默认会先调用父类的无参数构造方法。子类的每一个构造方法中，如果方法体没有显示指定父类构造方法，都默认首行隐藏 super(); 语句来调用父类的无参构造方法。 如果需要调用父类的有参构造方法，就在创建子类对象时的有参构造方法体首行中写上 super(xxx, xxx, ...); 语句。当通过手动调用父类构造方法时，就不会默认再调用父类无参数构造方法; 通过 super 调用父类的构造方法时，该语句必须是第一行有效语句，必须在构造方法中使用 不能在子类的非构造方法中通过 super 调用父类的构造方法 总结：this 用于调用本类的构造方法，super 用于调用父类的构造方法\nTips:\n创建子类对象，不会创建父类对象，默认调用父类的构造方法只是为了给父类的成员变量赋值。 Java 程序在执行子类的构造方法之前，如果该构造方法中没有显示使用 super() 来调用父类特定的构造方法，则会默认调用父类中“无参构造方法”。如果父类中只定义了有参数的构造方法，而在子类的构造方法中又没有用 super() 来调用父类中特定的构造方法，则编译时将发生错误，因为 Java 程序在父类中找不到无参的构造方法可供执行。因此建议父类里显式加上无参构造方法，从而避免编译错误。 8. 接口 8.1. 接口的概述 接口（英文：Interface），在 JAVA 编程语言中是一个抽象类型，是抽象方法的集合。可以理解为，接口是用来描述功能集合，只描述功能所具备的方法，具体如何实现这些功能由实现类通过方法重写完成。\n接口并不是类，编写接口的方式和类很相似，但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。接口通常以 interface 关键字来声明。一个类通过实现接口的方式，从而来继承接口的抽象方法。除非实现接口的类是抽象类，否则该类要定义接口中的所有方法。实现类可以理解成接口的“子类”。\n接口无法被实例化，但是可以被实现。一个实现接口的类，必须实现接口内所描述的所有方法，否则就必须声明为抽象类。另外，在 Java 中，接口类型可用来声明一个变量，他们可以成为一个空指针，或是被绑定在一个以此接口实现的对象。\nTips: 接口也是一种引用数据类型，比抽象类更加抽象的“类”，但实质不是类。但在多态中使用中可以当成“类”。\n8.2. 接口的声明语法 使用 interface 关键字来定义接口，接口名与类名的命名规则一致。接口的声明语法格式如下：\n1 2 3 4 5 6 7 8 9 [权限修饰符] interface 接口名 [extends 其他的接口名] { // 声明变量 String FOO = \u0026#34;foo\u0026#34;; // 默认是 public static final 修饰 // 抽象方法，必须是 public 类型 public void method1(); public int method2(方法参数...); String method3(方法参数...); // 省略权限修饰符与 abstract 抽象关键字，默认是 public abstract 修饰 .... } 接口定义的语法注意点：\n接口是隐式抽象的，当声明一个接口的时候，不必使用 abstract 关键字。 接口中的定义的方法全部都是抽象方法（JDk 1.8 以后，接口可以定义普通默认方法），无具体的逻辑实现。可以省略 abstract 关键字，因为在接口中默认是抽象方法。 接口可以定义成员变量，默认是 public static final 修饰的变量 接口的所有方法默认是 public abstract 修饰的方法，可以省略不写，但权限类型必须是 public。 示例：\n1 2 3 4 5 6 7 8 9 10 public interface DemoInterface { String FOO = \u0026#34;foo\u0026#34;; void foo(); default void bar(){ // do something... }; } 8.3. 接口的实现语法 当类实现接口的时候，必须实现接口中所有的方法。否则，该类必须声明为抽象的类。类可以同时实现多个接口。\n类使用 implements 关键字实现接口。在类声明中，implements 关键字放在 class 声明后面。语法格式如下：\n1 2 3 public class 实现类名称 implements 接口名称[, 接口2名称, 接口3名称..., ...] { // 实现接口所有抽象方法 } 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class MammalInt implements Animal { @Override public void eat() { System.out.println(\u0026#34;Mammal eats\u0026#34;); } @Override public void travel() { System.out.println(\u0026#34;Mammal travels\u0026#34;); } @Override public int noOfLegs() { return 0; } public static void main(String args[]) { MammalInt m = new MammalInt(); m.eat(); m.travel(); } } 8.3.1. 重写接口中声明的方法注意规则 类在实现接口的方法时，不能抛出强制性异常，只能在接口中，或者继承接口的抽象类中抛出该强制性异常。 类在重写方法时要保持一致的方法名，并且应该保持相同或者相兼容的返回值类型。否则该方法不是实现接口的抽象方法，而属于类自身定义的方法。 如果实现接口的类是抽象类，那么可以不实现该接口的抽象方法。 8.3.2. 实现接口注意规则 一个类可以同时实现多个接口。 一个类只能继承一个类，但是能实现多个接口。 一个接口能继承另一个接口，这和类之间的继承比较相似。 8.4. 接口特性 接口中每一个方法均为隐式的抽象方法，接口中的方法会被隐式的指定为 public abstract（其他修饰符都会报错），可以省略不写。 接口中可以含有成员变量，但是接口中的变量会被隐式的指定为 public static final 修饰（变量只能是 public，用 private 修饰会报编译错误），可以省略不写。 接口中的方法是不能在接口中实现，只能由实现接口的类来实现接口中的方法。但 Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。 8.4.1. JDK 1.8 版本的新特性 JDK 1.8 以后，接口里可以有静态方法和方法体了。 JDK 1.8 以后，接口允许包含具体实现的方法，该方法称为\u0026quot;默认方法\u0026quot;，默认方法使用 default 关键字修饰。 1 2 3 public default void xxxx() { // do something... } 8.4.2. JDK 1.9 版本的新特性 JDK 1.9 以后，允许将方法定义为 private，使得某些复用的代码不会把方法暴露出去。 8.5. 接口的继承 一个接口能继承其他接口，和类之间的继承方式比较相似。接口的继承也是使用 extends 关键字，子接口会继承父接口的所有方法。\n在 Java 中，类的是不允许多继承，但接口是可以多继承。在接口的多继承中 extends 关键字只需要使用一次，在其后跟着继承所有接口，使用“,”(英文逗号)分隔。接口的继承声明语法格式如下：\n1 public interface 接口名称 extends 接口1名称[, 接口2名称, 接口3名称..., ...] 示例：\n1 public interface Hockey extends Sports, Event {} 8.6. 标记接口 标记接口是一种特殊的接口。该接口没有任何方法和属性，它仅仅用于表明它的类属于一个特定的类型，让其容易识别与分类，供特殊场景使用。\n标记接口作用简单形象的说，就是给某个对象打个标（盖个戳），使对象拥有某个或某些特权。例如：java.awt.event 包中的 MouseListener 接口继承的 java.util.EventListener 接口定义如下：\n1 2 3 4 5 6 7 8 package java.util; /** * A tagging interface that all event listener interfaces must extend. * @since JDK1.1 */ public interface EventListener { } 标记接口主要用于以下目的：\n建立一个公共的父接口。正如 EventListener 接口，这是由几十个其他接口扩展的 Java API，可以使用一个标记接口来建立一组接口的父接口。例如：当一个接口继承了 EventListener 接口，Java 虚拟机(JVM)就知道该接口将要被用于一个事件的代理方案。 向一个类添加数据类型。这种情况是标记接口最初的目的，实现标记接口的类不需要定义任何接口方法(因为标记接口根本就没有方法)，但是该类通过多态性变成一个接口类型。 8.7. 总结 类实现接口时必须实现该接口中所有抽象方法，包括父接口的，如果没有全部实现，则该类应该声明成抽象类。 类在继承一个类的同时还可以实现多个接口。 抽象父类中的抽象方法可以和接口中的抽象方法同名，子类在实现父类和接口方法时，只需要重写一个即可。 接口中没有普通的成员变量，只能定义常量，一旦赋值就不能改变。通过 接口名.成员变量名称 直接调用该成员变量。 接口中的变量和方法均有默认修饰符。 成员变量：public static final 成员方法：public abstract 接口可以承继接口，而且可以多继承；实现类也要将所有父接口中的所有抽象方法进行重写。 8.7.1. 接口与类的比较 接口与类的相似点：\n一个接口可以有多个方法。 接口文件保存在 .java 结尾的文件中，文件名使用接口名。 接口的字节码文件保存在 .class 结尾的文件中。 接口相应的字节码文件必须在与包名称相匹配的目录结构中。 接口与类的区别：\n接口不能用于实例化对象。 接口没有构造方法。 接口中所有的方法必须是抽象方法，Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。 接口不能包含成员变量，除了 static 和 final 变量。 接口不是被类继承了，而是被类实现 接口支持多继承。 8.7.2. 接口与抽象类的对比 从设计层面来说，抽象类是对类的抽象，是一种模板设计；接口是行为的抽象，是一种行为的规范。\n两者的相同点：\n接口和抽象类都不能创建对象 接口和抽象类都包含抽象方法，其子类都必须覆写这些抽象方法 两者的不相同点：\n声明的方式 抽象类使用 abstract 关键字声明 接口使用 interface 关键字声明 子类继承/实现方式 子类使用 extends 关键字来继承抽象类。如果子类不是抽象类的话，它需要提供抽象类中所有声明的方法的实现。 子类使用 implements 关键字来实现接口，并且需要提供接口中所有声明的方法的实现。 构造方法 抽象类可以有构造方法。 接口不能有构造方法。 方法访问修饰符 抽象类中的方法可以是任意访问修饰符，但抽象方法不能定义为 private。 接口方法默认修饰符是 public abstract，并且不允许定义为 private 或者 protected。 多继承 一个类最多只能继承一个抽象类 一个类可以实现多个接口 成员变量、静态变量 抽象类可以定义成员变量与静态变量（包括常量），并且抽象类中的成员变量可以是任意访问类型。 接口不可以定义成员变量，但是可以定义常量。即接口的字段（变量）默认都是 public static final 修饰的类型。 普通方法 抽象类可以定义普通方法。即可以有方法体，就是能实现方法的具体功能 接口在 JDK1.8 之前只能有抽象方法；JDK1.8 之后可以定义默认方法（使用 default 关键字修饰的非抽象方法）。如果有普通方法，普通方法也是可以由现实类重写。 静态代码块和静态方法 抽象类中可以包含静态代码块和静态方法 接口在 JDK1.8 以前不能含有静态代码块以及静态方法(用 static 修饰的方法)，在 JDK1.8 之后可以定义静态方法。 抽象类与接口之间的继承性 抽象类可以实现多个接口。如果抽象类实现接口，则可以把接口中方法映射到抽象类中作为抽象方法而不必实现，而在抽象类的子类中实现接口中方法 接口不可以继承抽象类(但可以继承多个接口) 8.7.3. 接口和抽象类如何选择 明确该方法是否是某种数据类型的共性内容。\n如果是共性内容，该方法就应该放到该种类型的父类中。然后再考虑该方法父类知不知道如何实现，如果父类不知道如何实现并且要求子类必须重写的，则将该方法定义为抽象方法，该父类就必须定义为抽象类。即当“我是你的一种时”使用继承(抽象类)，父类是通过不断的抽取共性内容而得出来的。 如果不是共性内容，该方法就应该定义到接口中，然后需要该功能的类实现接口重写方法即可。当“我应该像你一样具备某种功能时”使用接口，接口是功能的集合，只描述功能具备的方法，由实现类通过方法重写来完成，这个功能不是抽象类都有的功能的时候。 接口和抽象类各有优缺点，在接口和抽象类的选择上，必须遵守这样一个原则：\n行为模型应该总是通过接口而不是抽象类定义，所以通常是优先选用接口，尽量少用抽象类。 选择抽象类的时候通常是如下情况：需要定义子类的行为，又要为子类提供通用的功能。 9. 多态 9.1. 多态的概述 同一种事物表现出来的多种形态。是面向对象的三大特征之一。\n所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量具体调用的方法实现在编程时并不确定，而是在程序运行期间才确定，即一个引用变量到底会指向哪个类的实例对象，该引用变量发出的方法调用到底是哪个类中实现的方法，必须在由程序运行期间才能决定。\n在 Java 中有两种形式可以实现多态：继承（多个子类对同一方法的重写）和接口（实现接口并覆盖接口中同一方法）。\n9.1.1. 多态的前提 必须要子父类关系(继承)或者类实现接口的关系 要有方法重写 要有父类引用指向子类对象。如：Parent p = new Child(); 9.1.2. 多态的优缺点 多态的好处(优点)\n提高了代码的灵活性和可维护性：通过多态，可以将代码编写成通用的、松耦合的形式，提高代码的可维护性。 提高了代码的扩展性：通过添加新的子类，可以扩展系统的功能。 提高了代码的复用性 消除类型之间的耦合关系 多态的弊端(缺点)\n多态情况下，不能访问子类特有的成员变量和成员方法。即用父类和接口去作为对象类型时，创建出来的对象不能调用子类特有的方法，因为子类的方法不存在父类和接口中 9.2. Java 中实现多态的机制 实现多态的机制就是，父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。而程序调用的方法在运行期才动态绑定，就是引用变量所指向的具体实例对象的方法，也就是内存里正在运行的那个对象的方法，而不是引用变量的类型中定义的方法。\n基础语法定义：\n1 2 父类类型 对象名 = new 子类类名(); 接口类型 对象名 = new 接口实现类类名(); 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class Person{ method1; } public class Student extends Person{ @Override method1; } // 创建学生对象 // Student stu = new Student(); Person stu = new Student();\t// 多态：父类引用指向子类对象 // 另外一种方法 (接口可以理解为Student的父类) public interface Play{ public void playGame(); } public class Student implements Play{ @Override public void playGame(){ .... } } // 创建学生对象 Play p = new Student(); 9.3. 多态的转型 多态的转型分为向上转型与向下转型两种。\n向上转型：父类引用指向子类对象的过程就是向上转型，由编译器自动完成。\n1 父类名 变量名 = new 子类类名(); 向下转型：把向上转型的父类引用强制转换成子类引用。\n1 子类类型名 变量名 = (子类类型名)父类引用变量名; 9.4. 多态的使用场景 作为形式参数：多态作为方法形式参数，可以接收更多数据类型的对象。 作为方法返回值类型：多态作为方法返回值，可以返回更多数据类型的对象。 9.5. 多态的注意事项 多态情况下，当子父类出现同名的成员变量时，通过父类引用访问成员变量时，访问的是父类的成员变；当子父类出现同名的成员方法时，通过父类引用调用成员方法时，调用的是子类重写后的方法。\n成员变量属于静态绑定：在程序运行之前就确定了访问的成员变量是哪个对象的。 成员方法属于动态绑定：在程序运行过程中才去确定调用哪个对象的，如果子类重写了，则调用子类的，否则调用父类的。 小结：\n如果访问的是成员变量，编译看左边，运行也看左边。 如果调用的是成员方法，编译看左边，运行看右边。 9.6. 多态的实现原理 要了解多态的实现原理，需要先了解两个概念：动态绑定和虚拟方法调用。\n9.6.1. 动态绑定 动态绑定（Dynamic Binding）：指的是在编译时，Java 编译器只能知道变量的声明类型，而无法确定其实际的对象类型。而在运行时，Java 虚拟机（JVM）会通过动态绑定来解析实际对象的类型。这意味着，编译器会推迟方法的绑定（即方法的具体调用）到运行时。正是这种动态绑定机制，使得多态成为可能。\n9.6.2. 虚拟方法调用 虚拟方法调用（Virtual Method Invocation）：在 Java 中，所有的非私有、非静态和非 final 方法都是被隐式地指定为虚拟方法。虚拟方法调用是在运行时根据实际对象的类型来确定要调用的方法的机制。当通过父类类型的引用变量调用被子类重写的方法时，虚拟机会根据实际对象的类型来确定要调用的方法版本，而不是根据引用变量的声明类型。\n9.6.3. 实现流程综述 多态的实现原理主要是依靠“动态绑定”和“虚拟方法调用”，它的实现流程如下：\n创建父类类型的引用变量，并将其赋值为子类对象。 在运行时，通过动态绑定确定引用变量所指向的实际对象的类型。 根据实际对象的类型，调用相应的方法版本。 10. JavaBean 10.1. 概念 Java 定义一种特殊的类名为『JavaBean』，该需要满足以下三个要求：\n类必须是 public 修饰 类必须有 public 的无参构造方法 必须为每一个成员变量提供对应的 setter \u0026amp; getter 方法 10.2. 字段与属性 在 Java 中经常提起的『字段』，其实就是成员变量，字段名就是成员变量名\n一般情况下，属性名就是字段名，成员变量名。 另一种情况，属性名是通过 setter \u0026amp; getter 方法得到的。 例如：setName -\u0026gt; 去掉set单词 -\u0026gt; Name -\u0026gt; 首字母小写 -\u0026gt; name，此时 name 才是对应类的属性（字段）\n1 2 3 4 5 6 7 8 9 private String description; public void setDesc(String description){ this.description = description; } public String getDesc{ return this.description; } 值得注意的是，根据 JavaBean 的定义，此时属性名是 desc，而非 description\n11. Object 类 11.1. 概述 Object 是类层次结构中的根类，所有的类都直接或者间接的继承自该类。\n如果一个方法的形式参数是Object，那么这里我们就可以传递它的任意的子类对象。\n11.2. 核心方法 11.2.1. equals 1 public boolean equals(Object obj) Object 类 equals 方法用来比较两个对象是否相同。默认通过 == 比较两个对象的引用变量是否指向同一个内存地址，以此判断两个对象是否相同。如果需要判断对象属性的内容是否相同，则通过子类来重写 equals 方法。如下：\n1 2 3 4 5 6 7 8 9 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age \u0026amp;\u0026amp; height == person.height \u0026amp;\u0026amp; Objects.equals(name, person.name); } 11.2.2. toString 1 2 3 public String toString() { return getClass().getName() + \u0026#34;@\u0026#34; + Integer.toHexString(hashCode()); } 父类 Object 类的 toString 方法默认返回值是 包名.类名@对象的哈希码（十六进制） 的字符串。此方法的调用时机：\n直接调用：手动通过对象调用，通过对象名.toString(); 间接调用：当打印输出该对象时系统自动调用 toString() 方法。 通过重写 toString 方法即可自定义输出的内容：\n1 2 3 4 5 6 7 8 @Override public String toString() { return new StringJoiner(\u0026#34;, \u0026#34;, Person.class.getSimpleName() + \u0026#34;[\u0026#34;, \u0026#34;]\u0026#34;) .add(\u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#34;\u0026#39;\u0026#34;) .add(\u0026#34;age=\u0026#34; + age) .add(\u0026#34;height=\u0026#34; + height) .toString(); } 11.2.3. hashCode 1 public native int hashCode(); 将与对象相关的信息映射成一个哈希值，默认的实现 hashCode 值是根据对象的内存地址换算出来。也可以通过重写方法，自定义hashCode 生成规则：\n1 2 3 4 @Override public int hashCode() { return Objects.hash(name, age, height); } Tips: 重写了 equals 方法一般都要重写 hashCode 方法！\n11.2.4. clone 1 protected native Object clone() throws CloneNotSupportedException; Java 赋值是复制对象引用，如果想要得到一个对象的副本，使用赋值操作是无法达到目的的。此 clone() 方法实现了对象中各个属性的复制，但需要注意此方法的可见范围是 protected。\n实体类使用克隆的前提是：\n类必须实现 Cloneable 接口，这是一种约定，Cloneable 是一个标记接口，自身没有方法。在调用 clone 方法时，会判断是否实现 Cloneable 接口，无实现则抛出 CloneNotSupportedException 异常。 覆盖 clone() 方法，将方法的权限修饰符提升为 public。 11.2.4.1. 浅拷贝 浅拷贝：拷贝对象和原始对象的引用类型，新旧对象引用同一个对象内存地址。\n如下例中，Person 对象里面有个 Student 对象，调用 clone 方法之后，克隆对象和原对象的 Student 引用的是同一个对象，这种即是浅拷贝。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Person implements Cloneable { private String name; private int age; private int height; private Student student; @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(); Student student = new Student(\u0026#34;Moon\u0026#34;, 16, 100); person.student = student; Person clonePerson = (Person) person.clone(); student.setName(\u0026#34;斩月\u0026#34;); // 修改原对象的属性值 System.out.println(clonePerson.student.getName()); // 输出结果是：斩月，即浅拷贝后的对象与原对象是指向同一个对象 } // ...省略getter/setter } 11.2.4.2. 深拷贝 深拷贝：拷贝对象和原始对象的引用类型，新旧对象引用不同的对象，修改新对象不会影响改到原对象。\n如下例中，在 clone 函数中不仅调用了 super.clone，而且调用 Student 对象的 clone 方法（Student 类也要实现 Cloneable 接口并重写 clone 方法），从而实现了深拷贝。最终于打印的结果是『Moon』，可以看到拷贝Student对象的值不会受到原对象的影响。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class Student implements Cloneable { volatile private String name; private int age; private int socre; public Student(String name, int age, int socre) { this.name = name; this.age = age; this.socre = socre; } @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } // ...省略getter/setter } public class Person implements Cloneable { private String name; private int age; private int height; private Student student; @Override public Object clone() throws CloneNotSupportedException { Person p = null; p = (Person) super.clone(); p.student = (Student) student.clone(); // 拷贝Student对象 return p; } public static void main(String[] args) throws CloneNotSupportedException { Person person = new Person(); Student student = new Student(\u0026#34;Moon\u0026#34;, 16, 100); person.student = student; Person clonePerson = (Person) person.clone(); student.setName(\u0026#34;斩月\u0026#34;); // 修改原对象的属性值 System.out.println(clonePerson.student.getName()); // 输出结果是：Moon，即深拷贝后的对象与原对象不是同一个对象 } // ...省略getter/setter } 11.2.5. getClass 1 public final native Class\u0026lt;?\u0026gt; getClass(); 方法返回此 Object 实例运行时的字节码对象，常用于 java 反射机制。\n11.2.6. wait / notify 1 2 3 public final void wait() throws InterruptedException public final native void wait(long timeout) throws InterruptedException; public final void wait(long timeout, int nanos) throws InterruptedException 当前线程调用对象的 wait() 方法之后，当前线程会释放对象锁，进入等待状态。等待其他线程调用此对象的 notify()/notifyAll() 唤醒或者等待超时时间 wait(long timeout) 自动唤醒。线程必须获取 Object 对象锁之后才能通过对象调用 wait()。 1 2 public final native void notify(); public final native void notifyAll(); notify() 唤醒在此对象上等待的单个线程，选择是任意性的。notifyAll() 唤醒在此对象上等待的所有线程。 11.2.7. finalize 1 protected void finalize() throws Throwable { } 当垃圾回收器确定不存在对该对象的引用时，由对象的垃圾回收器调用此方法。Java 中允许子类重写 finalize() 方法并在垃圾收集器将对象从内存中清除出去之前做必要的配置系统资源或执行其他清理工作。\n11.3. 相关扩展 11.3.1. 两个对象的 hashCode 相同，则 equals 是否也一定为 true？ equals 与 hashcode 的关系：\n如果两个对象调用 equals 比较返回 true，那么它们的 hashCode 值一定要相同； 如果两个对象的 hashCode 相同，它们并不一定相同。 hashcode 方法主要是用来提升对象比较的效率，先进行 hashcode() 的比较，如果不相同，那就不必在进行 equals 的比较，这样就大大减少了 equals 比较的次数，当比较对象的数量很大的时候能提升效率。\n之所以重写 equals() 时也要重写 hashcode() ，是为了保证 equals() 方法返回 true 的情况下 hashcode 值也要一致；如果重写了 equals() 没有重写 hashcode()，就会出现两个对象相等但 hashcode 不相等的情况。这样，当用其中的一个对象作为键保存到 HashMap、HashTable 或 HashSet 中，再以另一个对象作为键值去查找他们的时候，则会查找不到。\n11.3.2. 重写 equals 不重写 hashcode 会有什么效果及原理 重写 equals 不重写 hashcode，可能会导致这个类在使用哈希数据结构存储时出现问题。因为哈希数据结构需要根据 hashCode 值来进行数据的查找和定位，这会导致即使两个对象的属性值相等，但它们的 hashCode 值也可能不相等。\n由于 equals 方法一般和 hashCode 方法是配合使用的，即当两个对象使用 equals 方法比较返回 true 时，它们的 hashCode 方法应该返回相同的值。因此，如果在重写 equals 方法时不重写 hashCode 方法，那么可能会导致这两个方法的行为不一致，从而破坏哈希数据结构的性质。\n为了解决这个问题，通常需要同时重写 equals 方法和 hashCode 方法。在重写 hashCode 方法时，需要根据对象的属性值来计算出一个 hashCode 值，通常可以使用 Java 提供的 Objects.hash(Object... values) 方法来实现。同时，在重写 hashCode 方法时，需要保证对于 equals 方法返回 true 的两个对象，它们的 hashCode 方法返回的值相等，从而保证哈希数据结构的正确性。\n12. String 类 12.1. 简述 1 2 3 4 5 6 7 8 public final class String implements java.io.Serializable, Comparable\u0026lt;String\u0026gt;, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 } String 字符串类，由多个字符组成的一串数据，字符串其本质是一个字符数组。有以下特点：\n不能被继承与修改：String 类是 final 关键字修饰，此类不能被继承。并且该类的所有成员变量也都是 final 修饰。 \u0026ldquo;abc\u0026quot;是 String 类的一个实例，或者成为 String 类的一个对象，也可以看成是一个字符串对象(相当于char data[] = {'a', 'b', 'c'};) 字符串是常量，一旦被赋值，就不能被改变 线程安全。同一个字符串实例可以被多个线程共享，因为字符串不可变，本身就是线程安全的。 支持hash映射和缓存。因为String的hash值经常会使用到，比如作为 Map 的键，不可变的特性使得 hash 值也不会变，不需要重新计算。 字符串常量池优化。String 对象创建之后，会缓存到字符串常量池中，下次需要创建同样的对象时，可以直接返回缓存的引用。 Notes: 字符串是一种比较特殊的引用数据类型，直接输出字符串对象输出的是该对象中的数据。\n12.2. 常用方法 12.2.1. 构造方法 把字符串数据封装成字符串对象，或者是简写成 String s = \u0026quot;xxx\u0026quot;; 可直接创建对象。注意：只有 String 类型才能直接赋值创建对象\n1 2 public String() public String(String original) 通过字符数组创建的构造方法：\n1 2 3 4 5 6 // 把字符数组的数据封装成字符串对象 public String(char value[]) // 把字符数组中的一部分数据封装成字符串对象 public String(char value[], int offset, int count) String(char[] value, boolean share) 其他的构造方法：\n1 2 3 4 5 6 7 8 9 10 11 12 public String(int[] codePoints, int offset, int count) public String(byte bytes[], int offset, int length, String charsetName) throws UnsupportedEncodingException public String(byte bytes[], int offset, int length, Charset charset) public String(byte bytes[], String charsetName) throws UnsupportedEncodingException public String(byte bytes[], Charset charset) public String(byte bytes[], int offset, int length) public String(byte bytes[]) public String(StringBuffer buffer) public String(StringBuilder builder) 已过时的构造方法：\n1 2 3 4 5 @Deprecated public String(byte ascii[], int hibyte, int offset, int count) @Deprecated public String(byte ascii[], int hibyte) 12.2.2. 字符串判断方法 1 public boolean equals(Object anObject) 比较字符串的内容是否相同 1 public boolean equalsIgnoreCase(String anotherString) 比较字符串的内容是否相同（忽略大小写） 1 2 public boolean startsWith(String prefix) public boolean startsWith(String prefix, int toffset) 判断字符串对象是否以指定的 prefix 开头 1 public boolean endsWith(String suffix) 判断字符串对象是否以指定的 str 结尾 1 public boolean contains(CharSequence s) 如果此列表中包含指定的元素，则返回 true。更确切地讲，当且仅当此列表包含至少一个满足 (o==null ? e==null : o.equals(e)) 的元素 e 时，则返回 true。 1 public int compareTo(String anotherString) 按字典顺序，当前字符串对象与参数 anotherString 指定的字符串比较大小。如果当前字符串与 anotherString 相同，该方法返回值0；如果当前字符串对象大于 anotherString，该方法返回正值；如果小于 anotherString，该方法返回负值。例如： 1 2 3 4 String str = \u0026#34;abcde\u0026#34;; str.compareTo(\u0026#34;boy\u0026#34;); // 返回负整数 str.compareTo(\u0026#34;aba\u0026#34;); // 返回正整数 str.compareTo(\u0026#34;abcde\u0026#34;); // 返回0 1 public int compareToIgnoreCase(String str) 按字典顺序比较两个字符串，不考虑大小写。此方法返回一个整数，其符号与使用规范化的字符串调用 compareTo 所得符号相同，规范化字符串的大小写差异已通过对每个字符调用 Character.toLowerCase(Character.toUpperCase(character)) 消除。返回值是根据指定 str 大于、等于还是小于当前 String 对象（不考虑大小写），分别返回一个负整数、0 或一个正整数。 1 public boolean isEmpty() 当字符串对象的 length() 为 0 时返回 true。 12.2.3. 获取字符串信息的方法 1 public int length() 获取字符串的长度，就是字符个数 1 public char charAt(int index) 获取指定索引处的字符，首个字符的索引值是0 1 public int indexOf(String str) 获取str在字符串对象中第一次出现的索引。(如果参数值是不存在的字符，输出-1) 1 public int indexOf(String str, int fromIndex) 从当前字符串的 fromIndex 位置外开始检索字符串 str，并返回首次出现 str 的位置。如果没有检索到字符串str，该方法返回的值是-1。 1 public int lastIndexOf(String str) 从当前字符串的头开始检索到字符串str，并返回最后出现str的位置。如果没有检索到字符串str，该方法返回的值是-1。 12.2.4. 字符串操作方法 1 2 3 4 // 从 beginIndex 索引处开始截取字符串，默认到结尾。 public String substring(int beginIndex) // 从 beginIndex 索引处开始，到 endIndex 索引结束截取字符串(包含beginIndex，不包含endIndex) public String substring(int beginIndex, int endIndex) 截取当前字符串对象。（注：并非直接截取当前对象） 1 public String trim() 去除字符串两端空格 1 2 public String[] split(String regex) public String[] split(String regex, int limit) 按照指定符号分割字符串，regex 可以为正则表达式。注意：不能根据“+”和“/”进行切割，要特殊处理。 1 public String replace(char oldChar, char newChar) 返回一个新的字符串，它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。 1 public String replace(CharSequence target, CharSequence replacement) 使用指定的字面值替换序列替换此字符串所有匹配字面值目标序列的子字符串，该替换从字符串的开头朝末尾执行。例如，用 \u0026ldquo;b\u0026rdquo; 替换字符串 \u0026ldquo;aaa\u0026rdquo; 中的 \u0026ldquo;aa\u0026rdquo; 将生成 \u0026ldquo;ba\u0026rdquo; 而不是 \u0026ldquo;ab\u0026rdquo;。 1 public String replaceAll(String regex, String replacement) 使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。 1 public String replaceFirst(String regex, String replacement) 使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。 12.2.5. 字符串转换方法 1 public char[] toCharArray() 将当前字符串对象转换为字符数组 1 2 public String toLowerCase() public String toLowerCase(Locale locale) 将当前字符串对象内容转换为小写字符串 1 2 public String toUpperCase() public String toUpperCase(Locale locale) 将当前字符串对象内容转换为大写字符串 1 2 3 4 5 6 public byte[] getBytes() public byte[] getBytes(Charset charset) public byte[] getBytes(String charsetName) throws UnsupportedEncodingException @Deprecated public void getBytes(int srcBegin, int srcEnd, byte dst[], int dstBegin) 返回当前字符串的 byte 类型数组 12.2.6. 其他类型转换成字符串对象方法 String 类的静态方法 valueOf，将 Object 类的对象、基础数据类型、字符数组等转换字符串表示形式（对象）返回。此方法有重载，参数可以是 boolean、char、char[]、long、int、double、float 等\n1 2 3 4 5 6 7 8 9 10 11 public static String valueOf(Object obj) public static String valueOf(char c) public static String valueOf(char data[]) public static String valueOf(char data[], int offset, int count) public static String valueOf(boolean b) public static String valueOf(int i) public static String valueOf(long l) public static String valueOf(float f) public static String valueOf(double d) 12.2.7. format 方法专题（java字符串格式化） JAVA字符串格式化-String.format()的使用\nString 类的 format() 方法用于创建格式化的字符串以及连接多个字符串对象，显示不同转换符实现不同数据类型到字符串的转换\n1 format(String format, Object... args) 新字符串使用本地语言环境，制定字符串格式和参数生成格式化的新字符串。 1 format(Locale locale, String format, Object... args) 使用指定的语言环境，制定字符串格式和参数生成格式化的字符串。 12.2.8. 其他方法 1 public native String intern(); intern() 是个 Native 方法，其作用是首先从常量池中查找是否存在该常量值的字符串，若不存在则先在常量池中创建，否则直接返回常量池已经存在的字符串的引用。比如\n1 2 3 String s1 = \u0026#34;aa\u0026#34;; String s2 = s1.intern(); System.out.print(s1 == s2); // true 上述代码因为\u0026quot;aa\u0026quot;会在编译阶段确定下来，并放置字符串常量池中，因此最终 s1 和 s2 引用的是同一个字符串常量对象。\n12.3. 字符串的遍历 方式1：length() 配合 charAt()\n1 2 3 4 String str = \u0026#34;MooNkirA\u0026#34;; for (int i = 0; i \u0026lt; str.length(); i++) { System.out.println(str.charAt(i)); } 方式2：把字符串转换为字符数组，然后遍历数组\n1 2 3 4 5 String str = \u0026#34;MooNkirA\u0026#34;; char[] chars = str.toCharArray(); for (char c : chars) { System.out.println(c); } 12.4. 构造方法与直接赋值创建字符串的区别 通过构造方法创建字符串对象是在堆内存。 直接赋值方式创建对象是在方法区的常量池。 Notes: 字符串的内容是存储在方法区的常量池中，是为了方便字符串的重复使用。\n这两种方式创建的字符串对象地址值是不同，但是里面保存的内容是相同的，所以不能用 == 来判断字符串(String)是否相等。== 比较运算符可用于以下情况：\n用于基本数据类型：比较的是基本数据类型的值是否相同 用于引用数据类型：比较的是引用数据类型的地址值是否相同。(不同类型是不能比较，会直接报错) 12.5. StringBuilder 12.5.1. 概述 StringBuilder 可以理解为是一个可变的字符串，字符串缓冲区类。\n1 2 public final class StringBuilder extends AbstractStringBuilder implements Serializable, CharSequence String 和 StringBuilder 的区别：\nString 的内容是固定的 StringBuilder 的内容是可变的 Notes: String 与 StringBuilder 不是同一类型对象，不能直接进行比较。\n12.5.2. 常用方法 1 2 3 4 5 6 7 public StringBuilder() public StringBuilder(int capacity) public StringBuilder(String str) public StringBuilder(CharSequence seq) StringBuilder 的构造方法 1 public int capacity() 返回当前容量 (理论值) 1 public int length() 返回长度(已经存储的字符个数，实际值) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public AbstractStringBuilder append(Object obj) public AbstractStringBuilder append(String str) public AbstractStringBuilder append(StringBuffer sb) AbstractStringBuilder append(AbstractStringBuilder asb) public AbstractStringBuilder append(CharSequence s) public AbstractStringBuilder append(CharSequence s, int start, int end) public AbstractStringBuilder append(char[] str) public AbstractStringBuilder append(char str[], int offset, int len) public AbstractStringBuilder append(boolean b) public AbstractStringBuilder append(char c) public AbstractStringBuilder append(int i) public AbstractStringBuilder append(long l) public AbstractStringBuilder append(float f) public AbstractStringBuilder append(double d) 添加数据的系列方法，并返回自身对象。因为添加方法均返回对象本身，因此可以使用链式编程：sb.append(\u0026quot;hello\u0026quot;).append(\u0026quot;world\u0026quot;).append(true).append(100); 1 public AbstractStringBuilder reverse() 字符串反转 1 public AbstractStringBuilder delete(int start, int end) 移除此序列的子字符串中的字符。该子字符串从指定的 start 处开始，一直到索引 end - 1 处的字符（即包头不包尾）。如果不存在这种字符，则一直到序列尾部。如果 start 等于 end，则不发生任何更改。 1 public AbstractStringBuilder deleteCharAt(int index) 移除此序列指定位置上的 char。此序列将缩短一个 char 1 public char charAt(int index) 返回此序列中指定索引处的 char 值。index 参数必须大于等于 0，且小于此序列的长度。 12.5.3. StringBuilder 与 String 相互转换方法 StringBuilder -\u0026gt; String：通过 Object 类的 toString() 方法或者 String 类的构造方法，均可实现将 StringBuilder 对象转成 String\n1 2 3 4 5 6 7 StringBuilder sb = new StringBuilder(); sb.append(\u0026#34;MooN\u0026#34;).append(\u0026#34;kirA\u0026#34;); // 通过 toString 方法转成 String 对象 String sb2String = sb.toString(); // 通过 String 类的构造方法创建 String sb2Str = new String(sb); String -\u0026gt; StringBuilder：直接通过构造方法即可以实现把 String 转成 StringBuilder\n1 StringBuilder sb = new StringBuilder(\u0026#34;MooNkirA\u0026#34;); 12.6. StringBuffer (待整理) TODO: 待整理\n13. 类与对象综合知识 13.1. 实体类各种命名含义 13.1.1. VO、PO、DO、DTO、BO、QO、DAO、POJO 的概念 DO（ Data Object）领域对象：与数据库表结构一一对应，通过DAO层向上传输数据源对象。 PO（persistant object）持久对象：在 o/r 映射的时候出现的概念，如果没有 o/r 映射，没有这个概念存在了。通常对应数据模型 ( 数据库 ), 本身还有部分业务逻辑的处理。可以看成是与数据库中的表相映射的 java 对象。最简单的 PO 就是对应数据库中某个表中的一条记录，多个记录可以用 PO 的集合。 PO 中应该不包含任何对数据库的操作 DTO（ Data Transfer Object）数据传输对象：Service或Manager向外传输的对象。这个概念来源于J2EE的设计模式，原来的目的是为了EJB的分布式应用提供粗粒度的数据实体，以减少分布式调用的次数，从而提高分布式调用的性能和降低网络负载，但在这里，泛指用于展示层与服务层之间的数据传输对象。 BO（ Business Object）业务对象：由Service层输出的封装业务逻辑的对象。 AO（ Application Object）应用对象：在Web层与Service层之间抽象的复用对象模型，极为贴近展示层，复用度不高。 VO（ View Object）显示层对象：通常是Web向模板渲染引擎层传输的对象。 POJO（ Plain Ordinary Java Object）：POJO专指只有setter/getter/toString的简单类，包括DO/DTO/BO/VO等。 Query：数据查询对象，各层接收上层的查询请求。注意超过2个参数的查询封装，禁止使用Map类来传输。 13.1.2. JDO Java Data Object 简称 JDO，是 Java 对象持久化的新的规范，也是一个用于存取某种数据仓库中的对象的标准化 API。JDO 提供了透明的对象存储，因此对开发人员来说，存储数据对象完全不需要额外的代码（如 JDBC API 的使用）。这些繁琐的例行工作已经转移到 JDO 产品提供商身上，使开发人员解脱出来，从而集中时间和精力在业务逻辑上。另外，JDO 很灵活，因为它可以在任何数据底层上运行。\nJDO 与 JDBC 比较：\nJDBC 只是面向关系数据库（RDBMS） JDO 更通用，提供到任何数据底层的存储功能，比如关系数据库、文件、XML以及对象数据库（ODBMS）等等，使得应用可移植性更强。 ","permalink":"https://ktzxy.top/posts/jo957qiezb/","summary":"Java基础 对象与类","title":"Java基础 对象与类"},{"content":"Kubernetes集群资源监控 概述 监控指标 一个好的系统，主要监控以下内容\n集群监控 节点资源利用率 节点数 运行Pods Pod监控 容器指标 应用程序【程序占用多少CPU、内存】 监控平台 使用普罗米修斯【prometheus】 + Grafana 搭建监控平台\nprometheus【定时搜索被监控服务的状态】\n开源的 监控、报警、数据库 以HTTP协议周期性抓取被监控组件状态 不需要复杂的集成过程，使用http接口接入即可 Grafana\n开源的数据分析和可视化工具 支持多种数据源 部署prometheus 首先需要部署一个守护进程\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 --- apiVersion: apps/v1 kind: DaemonSet metadata: name: node-exporter namespace: kube-system labels: k8s-app: node-exporter spec: selector: matchLabels: k8s-app: node-exporter template: metadata: labels: k8s-app: node-exporter spec: containers: - image: prom/node-exporter name: node-exporter ports: - containerPort: 9100 protocol: TCP name: http --- apiVersion: v1 kind: Service metadata: labels: k8s-app: node-exporter name: node-exporter namespace: kube-system spec: ports: - name: http port: 9100 nodePort: 31672 protocol: TCP type: NodePort selector: k8s-app: node-exporter 然后执行下面命令\n1 kubectl create -f node-exporter.yaml 执行完，发现会报错\n这是因为版本不一致的问题，因为发布的正式版本，而这个属于测试版本\n所以我们找到第一行，然后把内容修改为如下所示\n1 2 3 4 # 修改前 apiVersion: extensions/v1beta1 # 修改后 【正式版本发布后，测试版本不能使用】 apiVersion: apps/v1 创建完成后的效果\n然后通过yaml的方式部署prometheus\nconfigmap：定义一个configmap：存储一些配置文件【不加密】 prometheus.deploy.yaml：部署一个deployment【包括端口号，资源限制】 prometheus.svc.yaml：对外暴露的端口 rbac-setup.yaml：分配一些角色的权限 下面我们进入目录下，首先部署 rbac-setup.yaml\n1 kubectl create -f rbac-setup.yaml 然后分别部署\n1 2 3 4 5 6 # 部署configmap kubectl create -f configmap.yaml # 部署deployment kubectl create -f prometheus.deploy.yml # 部署svc kubectl create -f prometheus.svc.yml 部署完成后，我们使用下面命令查看\n1 kubectl get pods -n kube-system 在我们部署完成后，即可看到 prometheus 的 pod了，然后通过下面命令，能够看到对应的端口\n1 kubectl get svc -n kube-system 通过这个，我们可以看到 prometheus 对外暴露的端口为 30003，访问页面即可对应的图形化界面\n1 http://192.168.177.130:30003 在上面我们部署完prometheus后，我们还需要来部署grafana\n1 kubectl create -f grafana-deploy.yaml 然后执行完后，发现下面的问题\n1 error: unable to recognize \u0026#34;grafana-deploy.yaml\u0026#34;: no matches for kind \u0026#34;Deployment\u0026#34; in version \u0026#34;extensions/v1beta1\u0026#34; 我们需要修改如下内容\n1 2 3 4 5 6 7 8 9 10 # 修改 apiVersion: apps/v1 # 添加selector spec: replicas: 1 selector: matchLabels: app: grafana component: core 修改完成后，我们继续执行上述代码\n1 2 3 4 5 6 # 创建deployment kubectl create -f grafana-deploy.yaml # 创建svc kubectl create -f grafana-svc.yaml # 创建 ing kubectl create -f grafana-ing.yaml 我们能看到，我们的grafana正在\n配置数据源 下面我们需要开始打开 Grafana，然后配置数据源，导入数据显示模板\n1 kubectl get svc -n kube-system 我们可以通过 ip + 30431 访问我们的 grafana 图形化页面\n然后输入账号和密码：admin admin\n进入后，我们就需要配置 prometheus 的数据源\n和 对应的IP【这里IP是我们的ClusterIP】\n设置显示数据的模板 选择Dashboard，导入我们的模板\n然后输入 315 号模板\n然后选择 prometheus数据源 mydb，导入即可\n导入后的效果如下所示\n","permalink":"https://ktzxy.top/posts/ufhhxz8ynm/","summary":"17 Kubernetes集群资源监控","title":"17 Kubernetes集群资源监控"},{"content":"﻿\nDay-04-java方法详解 何谓方法？ ​\tSystem.out.println(),那么它是什么呢?\n​\t调用系统类里的标准输出对象out中的方法println\n​\tJava方法是语句的集合，它们在一起执行一个功能。\n​\t方法是解决一类问题的步骤的有序组合\n​\t方法包含于类或对象中，方法和方法是并列的关系，所以我们定义的方法不能写到main方法中\n​\t方法在程序中被创建，在其他地方被引用\n​\t设计方法的原则:方法的本意是功能块，就是实现某个功能的语句块的集合。我们设计方法的时候，最好保持方法的原子性，就是一个方法只完成1个功能，这样利于我们后期的扩展。\n方法的定义 Java的方法类似争其它语言的函数,是一段用来完成特定功能的代码片段，一股情况卜，定义一个方法包含以下语法:\n方法包含一个方法头和一个方法体。 下面是一个方法的所有部分:\n修饰符:修饰符，这是可选的，告诉编译器如何调用该方法。定义了该方法的访问类型。\n**返回值类型∶**方法可能会返回值。returnValueType 是方法返回值的数据类型。有些方法执行所需的操作，但没有返回值。在这种情况下，returnValueType是关键字void。\n方法名: 是方法的实际名称。方法名和参数表共同构成方法签名。\n参数类型: 参数像是一个占位符。当方法被调用时，传递值给参数。这个值被称为实参或变量。参数列表是指方法的参数类型、顺序和参数的个数。参数是可选的，方法可以不包含任何参数。\n​\t形式参数:在方法被调用时用于接收外界输入的数据。\n​\t实参:调用方法时实际传给方法的数据。 方法体: 方法体包含具体的语句，定义该方法的功能。\n1 2 3 4 5 6 7 8 9 10 //main 方法 public static void main(String[] args) { //实际参数：实际调用传递给它的参数 int sum = add(1,2); System.out.println(sum); } //形式参数：用来定义作用的 public static int add(int a,int b){ return a+b; } 总结方法定义的格式：\n1）修饰符：暂时使用public static\n2）方法返回值类型：方法的返回值对应的数据类型\n数据类型：可以是基本数据类型（byte，short，int，long，float，double，char，boolean）也可以是引用数据类型\n3）方法名：见名之意，首字母小写，其余遵循驼峰命名\n4）形参列表：方法定义的时候需要的形式参数：int a，int b 相当于告诉方法的调用者：需要传入几个参数，需要传入的参数的类型\n​\t实际参数：方法调用的时候传入的具体的参数\n5）方法体：具体的业务逻辑代码\n6）return方法返回值：\n​\t方法如果有返回值的话：return +方法返回值，将返回值返回到方法的调用处\n​\t方法没有返回值的话：return可以省略不写，并且方法的返回值类型为：void\n方法调用 调用方法:对象名.方法名(实参列表)\nJava支持两种调用方法的方式，根据方法是否返回值来选择。\n当方法返回一个值的时候，方法调用通常被当做一个值。例如:\n方法的作用：提高代码的复用性\n1 int larger = max(10，20); 如果方法返回值是void，方法调用一定是一条语句。\n1 System.out.println( \u0026#34;HelloWorld!\u0026#34;); 方法的重载 重载就是在一个类中，有相同的函数名称，但形参不同的函数。\n方法的重载的规则: 方法名称必须相同。\n​\t参数列表必须不同（个数不同、或类型不同、参数排列顺序不同等)。\n​\t方法的返回类型可以相同也可以不相同。 ​\t仅仅返回类型不同不足以成为方法的重载。 实现理论: ​\t方法名称相同时，编译器会根据调用方法的参数个数、参数类型等去逐个匹配, 以选择对应的方法，如果匹配失败，则编译器报错。\n命令行传参 有时候你希望运行一个程序时候再传递给它消息。这要靠传递命令行参数给main()函数实现。\n1 2 3 4 5 6 7 public class CommandLine { public static void main(String args[]){ for(int i=0; i\u0026lt;args.length; i++){ system.out.println( \u0026#34;args[\u0026#34; + i + \u0026#34;]: \u0026#34; + args[i]); } } } 可变参数 JDK 1.5开始，Java支持传递同类型的可变参数给一个方法。\n在方法声明中，在指定参数类型后加一个省略号(.….)。\n一个方法中只能指定一个可变参数，它必须是方法的最后一个参数。任何普通的参数必须在它之前声明。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void main(String[] args) { //调用可变参数的方法 printMax(1,2,3,5,46,45); printMax(new double[]{1,2,3}); } public static void printMax( double... numbers){ if ( numbers.length == 0) { System.out.println( \u0026#34;No argument passed\u0026#34; ); return; } double result = numbers[0]; //排序! for (int i = 1; i \u0026lt;numbers.length; i++){ if ( numbers[i] \u0026gt;result) { result = numbers[i]; } } System.out.println( \u0026#34;The max value is \u0026#34; + result); } 递归 A方法调用B方法，我们很容易理解!\n递归就是: A方法调用A方法!就是自己调用自己\n利用递归可以用简单的程序来解决一些复杂的问题。它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求\n解，递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算，大大地减少了程序的代码量。递归的能力在于用有限的语句来\n定义对象的无限集合。\n递归结构包括两个部分:\n​\t递归头:什么时候不调用自身方法。如果没有头，将陷入死循环。\n​\t递归体:什么时候需要调用自身方法。\n1 2 3 4 5 6 7 8 9 10 11 //5! 5的阶乘 public static void main(String[] args) { System.out.println(f(5)); } public static int f(int n){ if (n==1){ return 1; }else { return n*f(n-1); } } 面试题分析： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class demo4 { public static void main(String[] args) { int a = 10; int b = 20; System.out.println(\u0026#34;输出交换前的两个数：\u0026#34;+a+\u0026#34;---\u0026#34;+b); changeNum(a,b); System.out.println(\u0026#34;输出交换后的两个数：\u0026#34;+a+\u0026#34;---\u0026#34;+b); } public static void changeNum(int num1,int num2){ int t; t = num1; num1 = num2; num2 = t; } } ","permalink":"https://ktzxy.top/posts/q6trfkl2e6/","summary":"Day 04 java方法详解","title":"Day 04 java方法详解"},{"content":"mysql 完整备份和恢复 一、MySQL 完整备份操作\n1、直接打包数据库文件夹\n创建数据库 auth：\n1 2 MariaDB [(none)]\u0026gt; create database auth; Query OK, 1 row affected (0.00 sec) 进入数据库：\n1 2 MariaDB [(none)]\u0026gt; use auth Database changed 创建数据表：\n1 2 MariaDB [auth]\u0026gt; create table user(name char(10)not null,ID int(48)); Query OK, 0 rows affected (0.01 sec) 插入数据信息：\n1 2 MariaDB [auth]\u0026gt; insert into user values(\u0026#39;crushlinux\u0026#39;,\u0026#39;123\u0026#39;); Query OK, 1 row affected (0.01 sec) 查看数据信息：\n1 2 3 4 5 6 7 MariaDB [auth]\u0026gt; select * from user; +------------+------+ | name | ID | +------------+------+ | crushlinux | 123 | +------------+------+ 1 row in set (0.00 sec) 对它进行备份\n先退出 MySQL 停库\n1 [root@localhost ~]# systemctl stop mariadb 直接对它进行打包压缩（新引入一个小命令）\n1 2 3 4 5 6 [root@localhost ~]# rpm -q xz xz-5.1.2-9alpha.el7.x86_64 [root@localhost ~]# mkdir backup // 创建一个文件，把压缩包放进去 [root@localhost ~]# tar Jcf backup/mysql_all-$(date +%F).tar.xz /var/lib/mysql/ tar: 从成员名中删除开头的 “/” 模拟数据丢失：\n1 [root@localhost ~]# rm -rf /var/lib/mysql/auth/ 起服务：\n1 [root@localhost ~]# systemctl start mariadb 恢复数据：\n1 2 3 4 5 6 7 8 9 10 [root@localhost ~]# mkdir restore // 虽已创建一个文件 [root@localhost ~]# tar xf backup/mysql_all-2019-10-13.tar.xz -C restore/ 将那个压缩包解压到这个文件里 [root@localhost ~]# cd restore/ // 切换到这个文件里查看 [root@localhost restore]# ls var [root@localhost restore]# cd var/lib/mysql/ // 继续查看 [root@localhost mysql]# ls aria_log.00000001 auth ibdata1 ib_logfile1 performance_schema aria_log_control crushlinux ib_logfile0 mysql test [root@localhost mysql]# mv auth/ /var/lib/mysql/ // 发现有 auth，将它移动到 / var/lib/mysql / 下 登录 MySQL 查看：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 MariaDB [(none)]\u0026gt; show databases; +--------------------+ | Database | +--------------------+ | information_schema | | auth | | crushlinux | | mysql | | performance_schema | | test | +--------------------+ 6 rows in set (0.01 sec) MariaDB [auth]\u0026gt; select * from user; +------------+------+ | name | ID | +------------+------+ | crushlinux | 123 | +------------+------+ 1 row in set (0.00 sec) 2、使用专用备份工具 mysqldump\nMySQL 自带的备份工具 mysqldump，可以很方便的对 MySQL 进行备份。通过该命令工具可以将数据库、数据表或全部的库导出为 SQL 脚本，便于该命令在不同版本的 MySQL 务器上使用。例如， 当需要升级 MySQL 服务器时，可以先使用 mysqldump 命令将原有库信息到导出，然后直接在升级后的 MySQL 服务器中导入即可。\n（1）对单个库进行完全备份\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [root@localhost ~]# mysqldump -uroot -p123 --databases auth \u0026gt; backup/auth-$(date +%Y%m%d).sql [root@localhost ~]# ls backup/ auth-20191013.sql mysql_all-2019-10-13.tar.xz [root@localhost ~]# grep -Ev \u0026#34;^/|^$|^-\u0026#34; backup/auth-20191013.sql CREATE DATABASE /*!32312 IF NOT EXISTS*/ `auth` /*!40100 DEFAULT CHARACTER SET latin1 */; USE `auth`; DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `name` char(10) NOT NULL, `ID` int(48) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; LOCK TABLES `user` WRITE; INSERT INTO `user` VALUES (\u0026#39;crushlinux\u0026#39;,123); UNLOCK TABLES; （2）对多个库进行完全备份：\n1 2 3 4 [root@localhost ~]# mysqldump -uroot -p123 --databases auth mysql\u0026gt; backup/auth+mysql-$(date +%Y%m%d).sql [root@localhost ~]# ls backup/ auth-20191013.sql auth+mysql-20191013.sql mysql_all-2019-10-13.tar.xz （3）对所有库进行完全备份：\n1 2 3 4 5 [root@localhost ~]# mysqldump -uroot -p123 --events --opt --all-databases \u0026gt; backup/mysql_all.$(date +%Y%m%d).sql [root@localhost ~]# ls backup/ auth-20191013.sql mysql_all.20191013.sql auth+mysql-20191013.sql mysql_all-2019-10-13.tar.xz （4）对表进行完全备份：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@localhost ~]# mysqldump -uroot -p123 auth user \u0026gt; backup/auth_user-$(date +%Y%m%d).sql [root@localhost ~]# ls backup/ auth-20191013.sql auth_user-20191013.sql mysql_all-2019-10-13.tar.xz auth+mysql-20191013.sql mysql_all.20191013.sql [root@localhost ~]# grep -Ev \u0026#34;^/|^$|^-\u0026#34; backup/auth_user-20191013.sql DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `name` char(10) NOT NULL, `ID` int(48) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; LOCK TABLES `user` WRITE; INSERT INTO `user` VALUES (\u0026#39;crushlinux\u0026#39;,123); UNLOCK TABLES; （5）对表结构的备份\n1 2 3 4 5 6 7 8 [root@localhost ~]# **mysqldump -uroot -p123 -d auth user \u0026gt; backup/desc_auth_user-$(date +%Y%m%d).sql** [root@localhost ~]# grep -Ev \u0026#34;^/|^$|^-\u0026#34; backup/desc_auth_user-20191013.sql DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `name` char(10) NOT NULL, `ID` int(48) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 二、恢复数据\n1、使用 source 命令\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [root@localhost ~]# mysql -uroot -p123 Welcome to the MariaDB monitor. Commands end with ; or \\g. Your MariaDB connection id is 8 Server version: 5.5.41-MariaDB MariaDB Server Copyright (c) 2000, 2014, Oracle, MariaDB Corporation Ab and others. Type \u0026#39;help;\u0026#39; or \u0026#39;\\h\u0026#39; for help. Type \u0026#39;\\c\u0026#39; to clear the current input statement. MariaDB [(none)]\u0026gt; drop database auth; Query OK, 1 row affected (0.02 sec) MariaDB [(none)]\u0026gt; show databases; +--------------------+ | Database | +--------------------+ | information_schema | | crushlinux | | mysql | | performance_schema | | test | +--------------------+ 5 rows in set (0.00 sec) **先确定 auth 的备份的路径：**backup/auth-20191013.sql\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 MariaDB [(none)]\u0026gt; source backup/auth-20191013.sql MariaDB [auth]\u0026gt; show databases; +--------------------+ | Database | +--------------------+ | information_schema | | auth | | crushlinux | | mysql | | performance_schema | | test | +--------------------+ 6 rows in set (0.00 sec) 2、用 mysql 命令\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 MariaDB [auth]\u0026gt; drop database auth; Query OK, 1 row affected (0.01 sec) [root@localhost ~]# mysql -uroot -p123 \u0026lt; backup/auth-20191013.sql MariaDB [(none)]\u0026gt; show databases; +--------------------+ | Database | +--------------------+ | information_schema | | auth | | crushlinux | | mysql | | performance_schema | | test | +--------------------+ 6 rows in set (0.00 sec) -e 后面可以加执行的语句：\n1 2 3 4 5 6 [root@localhost ~]# mysql -uroot -p123 -e \u0026#39;select * from auth.user;\u0026#39; +------------+------+ | name | ID | +------------+------+ | crushlinux | 123 | +------------+------+ 三、MySQL 备份思路\n1、定期实施备份，指定备份计划或策略，并严格遵守.\n2、除了进行完全备份，开启 MySQL 服务器的 binlog_日志功能是很重要的（完全备份加上日志，可以对 MySQL 进行最大化还原）。\n3、使用统一和易理解的备份名称，推荐使用库名或者表名加上时间的命名规则，如 mysql_user-20181214.sql，不要使用 backup1 或者 abc 之类没有意义的名字。\n4、定期抽查备份的可靠性，做还原测试或者检查文件大小等方式。\n5、通过异地或者跨机房等方式来存放备份数据，防止源数据和备份文件一起损坏。\n一、MySQL 完整备份操作\n1、直接打包数据库文件夹\n创建数据库 auth：\n1 2 MariaDB [(none)]\u0026gt; create database auth; Query OK, 1 row affected (0.00 sec) 进入数据库：\n1 2 MariaDB [(none)]\u0026gt; use auth Database changed 创建数据表：\n1 2 MariaDB [auth]\u0026gt; create table user(name char(10)not null,ID int(48)); Query OK, 0 rows affected (0.01 sec) 插入数据信息：\n1 2 MariaDB [auth]\u0026gt; insert into user values(\u0026#39;crushlinux\u0026#39;,\u0026#39;123\u0026#39;); Query OK, 1 row affected (0.01 sec) 查看数据信息：\n1 2 3 4 5 6 7 MariaDB [auth]\u0026gt; select * from user; +------------+------+ | name | ID | +------------+------+ | crushlinux | 123 | +------------+------+ 1 row in set (0.00 sec) 对它进行备份\n先退出 MySQL 停库\n1 [root@localhost ~]# systemctl stop mariadb 直接对它进行打包压缩（新引入一个小命令）\n1 2 3 4 5 6 [root@localhost ~]# rpm -q xz xz-5.1.2-9alpha.el7.x86_64 [root@localhost ~]# mkdir backup // 创建一个文件，把压缩包放进去 [root@localhost ~]# tar Jcf backup/mysql_all-$(date +%F).tar.xz /var/lib/mysql/ tar: 从成员名中删除开头的 “/” 模拟数据丢失：\n1 [root@localhost ~]# rm -rf /var/lib/mysql/auth/ 起服务：\n1 [root@localhost ~]# systemctl start mariadb 恢复数据：\n1 2 3 4 5 6 7 8 9 10 [root@localhost ~]# mkdir restore // 虽已创建一个文件 [root@localhost ~]# tar xf backup/mysql_all-2019-10-13.tar.xz -C restore/ 将那个压缩包解压到这个文件里 [root@localhost ~]# cd restore/ // 切换到这个文件里查看 [root@localhost restore]# ls var [root@localhost restore]# cd var/lib/mysql/ // 继续查看 [root@localhost mysql]# ls aria_log.00000001 auth ibdata1 ib_logfile1 performance_schema aria_log_control crushlinux ib_logfile0 mysql test [root@localhost mysql]# mv auth/ /var/lib/mysql/ // 发现有 auth，将它移动到 / var/lib/mysql / 下 登录 MySQL 查看：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 MariaDB [(none)]\u0026gt; show databases; +--------------------+ | Database | +--------------------+ | information_schema | | auth | | crushlinux | | mysql | | performance_schema | | test | +--------------------+ 6 rows in set (0.01 sec) MariaDB [auth]\u0026gt; select * from user; +------------+------+ | name | ID | +------------+------+ | crushlinux | 123 | +------------+------+ 1 row in set (0.00 sec) 2、使用专用备份工具 mysqldump\nMySQL 自带的备份工具 mysqldump，可以很方便的对 MySQL 进行备份。通过该命令工具可以将数据库、数据表或全部的库导出为 SQL 脚本，便于该命令在不同版本的 MySQL 务器上使用。例如， 当需要升级 MySQL 服务器时，可以先使用 mysqldump 命令将原有库信息到导出，然后直接在升级后的 MySQL 服务器中导入即可。\n（1）对单个库进行完全备份\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [root@localhost ~]# mysqldump -uroot -p123 --databases auth \u0026gt; backup/auth-$(date +%Y%m%d).sql [root@localhost ~]# ls backup/ auth-20191013.sql mysql_all-2019-10-13.tar.xz [root@localhost ~]# grep -Ev \u0026#34;^/|^$|^-\u0026#34; backup/auth-20191013.sql CREATE DATABASE /*!32312 IF NOT EXISTS*/ `auth` /*!40100 DEFAULT CHARACTER SET latin1 */; USE `auth`; DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `name` char(10) NOT NULL, `ID` int(48) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; LOCK TABLES `user` WRITE; INSERT INTO `user` VALUES (\u0026#39;crushlinux\u0026#39;,123); UNLOCK TABLES; （2）对多个库进行完全备份：\n1 2 3 4 [root@localhost ~]# mysqldump -uroot -p123 --databases auth mysql\u0026gt; backup/auth+mysql-$(date +%Y%m%d).sql [root@localhost ~]# ls backup/ auth-20191013.sql auth+mysql-20191013.sql mysql_all-2019-10-13.tar.xz （3）对所有库进行完全备份：\n1 2 3 4 5 [root@localhost ~]# mysqldump -uroot -p123 --events --opt --all-databases \u0026gt; backup/mysql_all.$(date +%Y%m%d).sql [root@localhost ~]# ls backup/ auth-20191013.sql mysql_all.20191013.sql auth+mysql-20191013.sql mysql_all-2019-10-13.tar.xz （4）对表进行完全备份：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@localhost ~]# mysqldump -uroot -p123 auth user \u0026gt; backup/auth_user-$(date +%Y%m%d).sql [root@localhost ~]# ls backup/ auth-20191013.sql auth_user-20191013.sql mysql_all-2019-10-13.tar.xz auth+mysql-20191013.sql mysql_all.20191013.sql [root@localhost ~]# grep -Ev \u0026#34;^/|^$|^-\u0026#34; backup/auth_user-20191013.sql DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `name` char(10) NOT NULL, `ID` int(48) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; LOCK TABLES `user` WRITE; INSERT INTO `user` VALUES (\u0026#39;crushlinux\u0026#39;,123); UNLOCK TABLES; （5）对表结构的备份\n1 2 3 4 5 6 7 8 [root@localhost ~]# **mysqldump -uroot -p123 -d auth user \u0026gt; backup/desc_auth_user-$(date +%Y%m%d).sql** [root@localhost ~]# grep -Ev \u0026#34;^/|^$|^-\u0026#34; backup/desc_auth_user-20191013.sql DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `name` char(10) NOT NULL, `ID` int(48) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1; 二、恢复数据\n1、使用 source 命令\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 [root@localhost ~]# mysql -uroot -p123 Welcome to the MariaDB monitor. Commands end with ; or \\g. Your MariaDB connection id is 8 Server version: 5.5.41-MariaDB MariaDB Server Copyright (c) 2000, 2014, Oracle, MariaDB Corporation Ab and others. Type \u0026#39;help;\u0026#39; or \u0026#39;\\h\u0026#39; for help. Type \u0026#39;\\c\u0026#39; to clear the current input statement. MariaDB [(none)]\u0026gt; drop database auth; Query OK, 1 row affected (0.02 sec) MariaDB [(none)]\u0026gt; show databases; +--------------------+ | Database | +--------------------+ | information_schema | | crushlinux | | mysql | | performance_schema | | test | +--------------------+ 5 rows in set (0.00 sec) **先确定 auth 的备份的路径：**backup/auth-20191013.sql\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 MariaDB [(none)]\u0026gt; source backup/auth-20191013.sql MariaDB [auth]\u0026gt; show databases; +--------------------+ | Database | +--------------------+ | information_schema | | auth | | crushlinux | | mysql | | performance_schema | | test | +--------------------+ 6 rows in set (0.00 sec) 2、用 mysql 命令\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 MariaDB [auth]\u0026gt; drop database auth; Query OK, 1 row affected (0.01 sec) [root@localhost ~]# mysql -uroot -p123 \u0026lt; backup/auth-20191013.sql MariaDB [(none)]\u0026gt; show databases; +--------------------+ | Database | +--------------------+ | information_schema | | auth | | crushlinux | | mysql | | performance_schema | | test | +--------------------+ 6 rows in set (0.00 sec) -e 后面可以加执行的语句：\n1 2 3 4 5 6 [root@localhost ~]# mysql -uroot -p123 -e \u0026#39;select * from auth.user;\u0026#39; +------------+------+ | name | ID | +------------+------+ | crushlinux | 123 | +------------+------+ 三、MySQL 备份思路\n1、定期实施备份，指定备份计划或策略，并严格遵守.\n2、除了进行完全备份，开启 MySQL 服务器的 binlog_日志功能是很重要的（完全备份加上日志，可以对 MySQL 进行最大化还原）。\n3、使用统一和易理解的备份名称，推荐使用库名或者表名加上时间的命名规则，如 mysql_user-20181214.sql，不要使用 backup1 或者 abc 之类没有意义的名字。\n4、定期抽查备份的可靠性，做还原测试或者检查文件大小等方式。\n5、通过异地或者跨机房等方式来存放备份数据，防止源数据和备份文件一起损坏。\n","permalink":"https://ktzxy.top/posts/c3e1d6q1oq/","summary":"mysql完整备份和恢复","title":"mysql完整备份和恢复"},{"content":"使用find命令查找大文件 find命令是 Linux 系统管理员工具库中最强大的工具之一。它允许你根据不同的标准（包括文件大小）搜索文件和目录。 例如，如果在当前工作目录中要搜索大小超过 100MB 的文件，请使用以下命令：\n1 sudo find . -xdev -type f -size +100M . 代表当前目录。如要搜索其它目录替换.为要搜索目录的路径。\n输出将显示的文件列表，不会包含其它信息。\n1 2 3 4 5 6 /var/lib/libvirt/images/centos-7-desktop_default.img /var/lib/libvirt/images/bionic64_default.img /var/lib/libvirt/images/winqcow2 /var/lib/libvirt/images/debian-9_default.img /var/lib/libvirt/images/ubuntu-18-04-desktop_default.img /var/lib/libvirt/images/centos-7_default.img find命令还可以与其他命令结合使用，例如ls或sort对这些文件执行操作。\n在下面的示例中，我们传递find命令的输出到ls ，ls将打印已找到的每个文件的大小，然后将将输出传递给sort命令，以根据文件大小的第 5 列对其进行排序。\n1 find . -xdev -type f -size +100M -print | xargs ls -lh | sort -k5,5 -h -r 输出像这样：\n1 2 3 4 5 6 -rw------- 1 root root 40967M Jan 5 14:12 /var/lib/libvirt/images/winqcow2 -rw------- 1 root root 3725M Jan 7 22:12 /var/lib/libvirt/images/debian-9_default.img -rw------- 1 root root 1524M Dec 30 07:46 /var/lib/libvirt/images/centos-7-desktop_default.img -rw------- 1 root root 999M Jan 5 14:43 /var/lib/libvirt/images/ubuntu-18-04-desktop_default.img -rw------- 1 root root 562M Dec 31 07:38 /var/lib/libvirt/images/centos-7_default.img -rw------- 1 root root 378M Jan 7 22:26 /var/lib/libvirt/images/bionic64_default.img 如果输出包含大量信息，你可以使用该head命令仅打印前 10 行：\n1 find . -xdev -type f -size +100M -print | xargs ls -lh | sort -k5,5 -h -r | head 分解命令：find . -xdev -type f -size +100M -print\n仅搜索当前工作目录（.）中的 文件 (-type f)，大于 100MB（-size +100M），不要查找其他文件系统上的目录（-xdev）并在标准输出上打印完整文件名，然后是新的一行（-print） 。 xargs ls -lh- find命令的输出通过管道xargs执行，ls -lh命令将以长列表可读格式打印输出。 sort -k5,5 -h -r- 基于第 5 列（-k5,5）对行进行排序，以可读格式（-h）的值并反转结果（-r）。 head ：仅打印管道输出的前 10 行。 find命令带有许多强大的选项。例如，你可以搜索超过多少天的大文件，具有特定扩展名的大文件或属于特定用户的大文件。\n使用du命令查找大文件和目录 du命令用于估计文件空间使用情况，对于查找占用大量磁盘空间的目录和文件特别有用。\n以下命令将打印最大的文件和目录：\n1 du -ahx . | sort -rh | head -5 第一列包含文件大小，第二列包含文件名：\n1 2 3 4 5 55G . 24G ./.vagrant.d/boxes 24G ./.vagrant.d 13G ./Projects 2G ./.minikube 命令说明：\ndu -ahx .：估算当前工作目录（.）中的磁盘空间使用情况，包括文件和目录（a），以比较接近人的常见可读格式打印大小（h）并跳过不同文件系统上的目录（x）。 sort -rh：通过可读格式（-h）的值并反转结果（-r）来对输出行进行排序。 head -5 ：仅打印管道输出的前 5 行。 Linux上查找最大文件的3种方法_无忧杂货铺的博客-CSDN博客_\n","permalink":"https://ktzxy.top/posts/ot3cq8goln/","summary":"Linux中查找大文件","title":"Linux中查找大文件"},{"content":"Day-09-集合 集合 1.Java 集合框架概述 1.集合框架与数组的对比及概述 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /** * 一、集合的框架 * * 1.集合、数组都是对多个数据进行存储操作的结构，简称Java容器。 * 说明；此时的存储，主要是指能存层面的存储，不涉及到持久化的存储（.txt,.jpg,.avi,数据库中） * * 2.1数组在存储多个数据封面的特点： * 》一旦初始化以后，它的长度就确定了。 * 》数组一旦定义好，它的数据类型也就确定了。我们就只能操作指定类型的数据了。 * 比如：String[] arr;int[] str; * 2.2数组在存储多个数据方面的特点： * 》一旦初始化以后，其长度就不可修改。 * 》数组中提供的方法非常有限，对于添加、删除、插入数据等操作，非常不便，同时效率不高。 * 》获取数组中实际元素的个数的需求，数组没有现成的属性或方法可用 * 》数组存储数据的特点：有序、可重复。对于无序、不可重复的需求，不能满足。 * * * @create 2020-05-11 16:23 */ 集合的使用场景 2.集合框架涉及到的API Java 集合可分为Collection 和Map 两种体系\nCollection接口：单列数据，定义了存取一组对象的方法的集合 List：元素有序、可重复的集合 Set：元素无序、不可重复的集合 Map接口：双列数据，保存具有映射关系“key-value对”的集合 Collection接口继承树\nMap接口继承树 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /** * * 二、集合框架 * \u0026amp;---Collection接口：单列集合，用来存储一个一个的对象 * \u0026amp;---List接口：存储有序的、可重复的数据。 --\u0026gt;“动态”数组 * \u0026amp;---ArrayList、LinkedList、Vector * * \u0026amp;---Set接口：存储无序的、不可重复的数据 --\u0026gt;高中讲的“集合” * \u0026amp;---HashSet、LinkedHashSet、TreeSet * * \u0026amp;---Map接口：双列集合，用来存储一对(key - value)一对的数据 --\u0026gt;高中函数：y = f(x) * \u0026amp;---HashMap、LinkedHashMap、TreeMap、Hashtable、Properties * * * @create 2020-05-11 16:23 */ 2.Collection接口方法 Collection 接口是List、Set 和Queue 接口的父接口，该接口里定义的方法既可用于操作Set 集合，也可用于操作List 和Queue 集合。 JDK不提供此接口的任何直接实现，而是提供更具体的子接口(如：Set和List)实现。 在Java5 之前，Java 集合会丢失容器中所有对象的数据类型，把所有对象都当成Object 类型处理；从JDK 5.0 增加了泛型以后，Java 集合可以记住容器中对象的数据类型。 Collection接口中的常用方法1 添加 add(Objectobj) addAll(Collectioncoll) 获取有效元素的个数 intsize() 清空集合 voidclear() 是否是空集合 boolean isEmpty() 是否包含某个元素 booleancontains(Objectobj)：是通过元素的equals方法来判断是否是同一个对象 booleancontainsAll(Collectionc)：也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较。 删除 boolean remove(Object obj) ：通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素 boolean removeAll(Collection coll)：取当前集合的差集 取两个集合的交集 boolean retainAll(Collection c)：把交集的结果存在当前集合中，不影响c 集合是否相等 boolean equals(Object obj) 转成对象数组 Object[] toArray() 获取集合对象的哈希值 hashCode() 遍历 iterator()：返回迭代器对象，用于集合遍历 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 import org.junit.Test; import java.util.ArrayList; import java.util.Collection; import java.util.Date; /** * * 三、Collection接口中的方法的使用 * * * * * @create 2020-05-11 16:23 */ public class CollectionTest { @Test public void test1(){ Collection coll = new ArrayList(); //add(Object e):将元素e添加到集合coll中 coll.add(\u0026#34;AA\u0026#34;); coll.add(\u0026#34;BB\u0026#34;); coll.add(123); //自动装箱 coll.add(new Date()); //size():获取添加的元素的个数 System.out.println(coll.size()); //4 //addAll(Collection coll1):将coll1集合中的元素添加到当前的集合中 Collection coll1 = new ArrayList(); coll1.add(456); coll1.add(\u0026#34;CC\u0026#34;); coll.addAll(coll1); System.out.println(coll.size()); //6 System.out.println(coll); //clear():清空集合元素 coll.clear(); //isEmpty():判断当前集合是否为空 System.out.println(coll.isEmpty()); } } Collection接口中的常用方法2 Person类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import java.util.Objects; /** * * @create 2020-05-12 10:11 */ public class Person { private String name; private int age; public Person() { super(); } public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return \u0026#34;Person{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#39;}\u0026#39;; } @Override public boolean equals(Object o) { System.out.println(\u0026#34;Person equals()....\u0026#34;); if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age \u0026amp;\u0026amp; Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } } 测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; /** * Collection接口中声明的方法的测试 * * 结论： * 向Collection接口的实现类的对象中添加数据obj时，要求obj所在类要重写equals(). * * * @create 2020-05-12 10:06 */ public class CollectinoTest { @Test public void test(){ Collection coll = new ArrayList(); coll.add(123); coll.add(456); // Person p = new Person(\u0026#34;Jerry\u0026#34;,20); // coll.add(p); coll.add(new Person(\u0026#34;Jerry\u0026#34;,20)); coll.add(new String(\u0026#34;Tom\u0026#34;)); coll.add(false); //1.contains(Object obj):判断当前集合中是否包含obj //我们在判断时会调用obj对象所在类的equals()。 boolean contains = coll.contains(123); System.out.println(contains); System.out.println(coll.contains(new String(\u0026#34;Tam\u0026#34;))); // System.out.println(coll.contains(p));//true System.out.println(coll.contains(new Person(\u0026#34;Jerry\u0026#34;,20)));//false --\u0026gt;true //2.containsAll(Collection coll1):判断形参coll1中的所有元素是否都存在于当前集合中。 Collection coll1 = Arrays.asList(123,4567); System.out.println(coll.containsAll(coll1)); } } Collection接口中的常用方法3 Person类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import java.util.Objects; /** * * @create 2020-05-12 10:11 */ public class Person { private String name; private int age; public Person() { super(); } public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return \u0026#34;Person{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#39;}\u0026#39;; } @Override public boolean equals(Object o) { System.out.println(\u0026#34;Person equals()....\u0026#34;); if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age \u0026amp;\u0026amp; Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } } 测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; /** * Collection接口中声明的方法的测试 * * 结论： * 向Collection接口的实现类的对象中添加数据obj时，要求obj所在类要重写equals(). * * * @create 2020-05-12 10:06 */ public class CollectinoTest { @Test public void test2(){ //3.remove(Object obj):从当前集合中移除obj元素。 Collection coll = new ArrayList(); coll.add(123); coll.add(456); coll.add(new Person(\u0026#34;Jerry\u0026#34;,20)); coll.add(new String(\u0026#34;Tom\u0026#34;)); coll.add(false); coll.remove(1234); System.out.println(coll); coll.remove(new Person(\u0026#34;Jerry\u0026#34;,20)); System.out.println(coll); //4. removeAll(Collection coll1):差集：从当前集合中移除coll1中所有的元素。 Collection coll1 = Arrays.asList(123,456); coll.removeAll(coll1); System.out.println(coll); } @Test public void test3(){ Collection coll = new ArrayList(); coll.add(123); coll.add(456); coll.add(new Person(\u0026#34;Jerry\u0026#34;,20)); coll.add(new String(\u0026#34;Tom\u0026#34;)); coll.add(false); //5.retainAll(Collection coll1):交集：获取当前集合和coll1集合的交集，并返回给当前集合 // Collection coll1 = Arrays.asList(123,456,789); // coll.retainAll(coll1); // System.out.println(coll); //6.equals(Object obj):要想返回true，需要当前集合和形参集合的元素都相同。 Collection coll1 = new ArrayList(); coll1.add(456); coll1.add(123); coll1.add(new Person(\u0026#34;Jerry\u0026#34;,20)); coll1.add(new String(\u0026#34;Tom\u0026#34;)); coll1.add(false); System.out.println(coll.equals(coll1)); } } Collection接口中的常用方法4 Person类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import java.util.Objects; /** * * @create 2020-05-12 10:11 */ public class Person { private String name; private int age; public Person() { super(); } public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return \u0026#34;Person{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#39;}\u0026#39;; } @Override public boolean equals(Object o) { System.out.println(\u0026#34;Person equals()....\u0026#34;); if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age \u0026amp;\u0026amp; Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } } 测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; /** * Collection接口中声明的方法的测试 * * 结论： * 向Collection接口的实现类的对象中添加数据obj时，要求obj所在类要重写equals(). * * * @create 2020-05-12 10:06 */ public class CollectinoTest { @Test public void test4(){ Collection coll = new ArrayList(); coll.add(123); coll.add(456); coll.add(new Person(\u0026#34;Jerry\u0026#34;,20)); coll.add(new String(\u0026#34;Tom\u0026#34;)); coll.add(false); //7.hashCode():返回当前对象的哈希值 System.out.println(coll.hashCode()); //8.集合 ---\u0026gt;数组：toArray() Object[] arr = coll.toArray(); for(int i = 0;i \u0026lt; arr.length;i++){ System.out.println(arr[i]); } //拓展：数组 ---\u0026gt;集合:调用Arrays类的静态方法asList() List\u0026lt;String\u0026gt; list = Arrays.asList(new String[]{\u0026#34;AA\u0026#34;, \u0026#34;BB\u0026#34;, \u0026#34;CC\u0026#34;}); System.out.println(list); List arr1 = Arrays.asList(123, 456); System.out.println(arr1);//[123, 456] List arr2 = Arrays.asList(new int[]{123, 456}); System.out.println(arr2.size());//1 List arr3 = Arrays.asList(new Integer[]{123, 456}); System.out.println(arr3.size());//2 //9.iterator():返回Iterator接口的实例，用于遍历集合元素。放在IteratorTest.java中测试 } } 3.Iterator迭代器接口 Iterator对象称为迭代器(设计模式的一种)，主要用于遍历Collection 集合中的元素。 GOF给迭代器模式的定义为：提供一种方法访问一个容器(container)对象中各个元素，而又不需暴露该对象的内部细节。迭代器模式，就是为容器而生。类似于“公交车上的售票员”、“火车上的乘务员”、“空姐”。 Collection接口继承了java.lang.Iterable接口，该接口有一个iterator()方法，那么所有实现了Collection接口的集合类都有一个iterator()方法，用以返回一个实现了Iterator接口的对象。 Iterator 仅用于遍历集合，Iterator本身并不提供承装对象的能力。如果需要创建Iterator 对象，则必须有一个被迭代的集合。 集合对象每次调用iterator()方法都得到一个全新的迭代器对象，默认游标都在集合的第一个元素之前。 使用Iterator遍历Collection 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import org.junit.Test; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; /** * 集合元素的遍历操作，使用迭代器Iterator接口 * 内部的方法：hasNext()和 next() * * * @create 2020-05-12 12:22 */ public class IteratorTest { @Test public void test(){ Collection coll = new ArrayList(); coll.add(123); coll.add(456); coll.add(new Person(\u0026#34;Jerry\u0026#34;,20)); coll.add(new String(\u0026#34;Tom\u0026#34;)); coll.add(false); Iterator iterator = coll.iterator(); //方式一： // System.out.println(iterator.next()); // System.out.println(iterator.next()); // System.out.println(iterator.next()); // System.out.println(iterator.next()); // System.out.println(iterator.next()); // //报异常：NoSuchElementException // //因为：在调用it.next()方法之前必须要调用it.hasNext()进行检测。若不调用，且下一条记录无效，直接调用it.next()会抛出NoSuchElementException异常。 // System.out.println(iterator.next()); //方式二：不推荐 // for(int i = 0;i \u0026lt; coll.size();i++){ // System.out.println(iterator.next()); // } //方式三：推荐 while(iterator.hasNext()){ System.out.println(iterator.next()); } } } 迭代器Iterator的执行原理 Iterator遍历集合的两种错误写法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import org.junit.Test; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; /** * 集合元素的遍历操作，使用迭代器Iterator接口 * 1.内部的方法：hasNext()和 next() * 2.集合对象每次调用iterator()方法都得到一个全新的迭代器对象，默认游标都在集合的第一个元素之前。 * * * @create 2020-05-12 12:22 */ public class IteratorTest { @Test public void test2(){ Collection coll = new ArrayList(); coll.add(123); coll.add(456); coll.add(new Person(\u0026#34;Jerry\u0026#34;,20)); coll.add(new String(\u0026#34;Tom\u0026#34;)); coll.add(false); //错误方式一： // Iterator iterator = coll.iterator(); // while(iterator.next() != null){ // System.out.println(iterator.next()); // } //错误方式二： //集合对象每次调用iterator()方法都得到一个全新的迭代器对象，默认游标都在集合的第一个元素之前。 while(coll.iterator().hasNext()){ System.out.println(coll.iterator().next()); } } } Iterator迭代器remove()的使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 import org.junit.Test; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; /** * 集合元素的遍历操作，使用迭代器Iterator接口 * 1.内部的方法：hasNext()和 next() * 2.集合对象每次调用iterator()方法都得到一个全新的迭代器对象，默认游标都在集合的第一个元素之前。 * 3.内部定义了remove(),可以在遍历的时候，删除集合中的元素。此方法不同于集合直接调用remove() * * * @create 2020-05-12 12:22 */ public class IteratorTest { //测试Iterator中的remove()方法 @Test public void test3(){ Collection coll = new ArrayList(); coll.add(123); coll.add(456); coll.add(new Person(\u0026#34;Jerry\u0026#34;,20)); coll.add(new String(\u0026#34;Tom\u0026#34;)); coll.add(false); //删除集合中”Tom” //如果还未调用next()或在上一次调用 next 方法之后已经调用了 remove 方法， // 再调用remove都会报IllegalStateException。 Iterator iterator = coll.iterator(); while(iterator.hasNext()){ // iterator.remove(); Object obj = iterator.next(); if(\u0026#34;Tom\u0026#34;.equals(obj)){ iterator.remove(); // iterator.remove(); } } //遍历集合 iterator = coll.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } } } 注意： Iterator可以删除集合的元素，但是是遍历过程中通过迭代器对象的remove方法，不是集合对象的remove方法。 如果还未调用next()或在上一次调用next方法之后已经调用了remove方法，再调用remove都会报IllegalStateException。 新特性foreach循环遍历集合或数组 Java 5.0 提供了foreach循环迭代访问Collection和数组。 遍历操作不需获取Collection或数组的长度，无需使用索引访问元素。 遍历集合的底层调用Iterator完成操作。 foreach还可以用来遍历数组。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import org.junit.Test; import java.util.ArrayList; import java.util.Collection; /** * jdk 5.0 新增了foreach循环，用于遍历集合、数组 * * * @create 2020-05-12 16:08 */ public class ForTest { @Test public void test(){ Collection coll = new ArrayList(); coll.add(123); coll.add(456); coll.add(new Person(\u0026#34;Jerry\u0026#34;,20)); coll.add(new String(\u0026#34;Tom\u0026#34;)); coll.add(false); //for(集合元素的类型 局部变量 : 集合对象),内部仍然调用了迭代器。 for(Object obj : coll){ System.out.println(obj); } } @Test public void test2(){ int[] arr = new int[]{1,2,3,4,5,6}; //for(数组元素的类型 局部变量 : 数组对象) for(int i : arr){ System.out.println(i); } } //练习题 @Test public void test3(){ String[] arr = new String[]{\u0026#34;SS\u0026#34;,\u0026#34;KK\u0026#34;,\u0026#34;RR\u0026#34;}; // //方式一：普通for赋值 // for(int i = 0;i \u0026lt; arr.length;i++){ // arr[i] = \u0026#34;HH\u0026#34;; // } //方式二：增强for循环 for(String s : arr){ s = \u0026#34;HH\u0026#34;; } for(int i = 0;i \u0026lt; arr.length;i++){ System.out.println(arr[i]); } } } 4.Collection子接口之一：List接口 鉴于Java中数组用来存储数据的局限性，我们通常使用List替代数组 List集合类中元素有序、且可重复，集合中的每个元素都有其对应的顺序索引。 List容器中的元素都对应一个整数型的序号记载其在容器中的位置，可以根据序号存取容器中的元素。 JDK API中List接口的实现类常用的有：ArrayList、LinkedList和Vector。 List接口常用实现类的对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /** * 1. List接口框架 * * |----Collection接口：单列集合，用来存储一个一个的对象 * |----List接口：存储有序的、可重复的数据。 --\u0026gt;“动态”数组,替换原有的数组 * |----ArrayList：作为List接口的主要实现类；线程不安全的，效率高；底层使用Object[] elementData存储 * |----LinkedList：对于频繁的插入、删除操作，使用此类效率比ArrayList高；底层使用双向链表存储 * |----Vector：作为List接口的古老实现类；线程安全的，效率低；底层使用Object[] elementData存储 * * * 面试题：比较ArrayList、LinkedList、Vector三者的异同？ * 同：三个类都是实现了List接口，存储数据的特点相同：存储有序的、可重复的数据 * 不同：见上 * * * @create 2020-05-12 20:54 */ ArrayList的源码分析 ArrayList是List 接口的典型实现类、主要实现类 本质上，ArrayList是对象引用的一个”变长”数组 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /** * 2.ArrayList的源码分析： * 2.1 jdk 7情况下 * ArrayList list = new ArrayList();//底层创建了长度是10的Object[]数组elementData * list.add(123);//elementData[0] = new Integer(123); * ... * list.add(11);//如果此次的添加导致底层elementData数组容量不够，则扩容。 * 默认情况下，扩容为原来的容量的1.5倍，同时需要将原有数组中的数据复制到新的数组中。 * * 结论：建议开发中使用带参的构造器：ArrayList list = new ArrayList(int capacity) * * 2.2 jdk 8中ArrayList的变化： * ArrayList list = new ArrayList();//底层Object[] elementData初始化为{}.并没有创建长度为10的数组 * * list.add(123);//第一次调用add()时，底层才创建了长度10的数组，并将数据123添加到elementData[0] * ... * 后续的添加和扩容操作与jdk 7 无异。 * 2.3 小结：jdk7中的ArrayList的对象的创建类似于单例的饿汉式，而jdk8中的ArrayList的对象 * 的创建类似于单例的懒汉式，延迟了数组的创建，节省内存。 * */ LinkedList的源码分析 对于频繁的插入或删除元素的操作，建议使用LinkedList类，效率较高 LinkedList：双向链表，内部没有声明数组，而是定义了Node类型的first和last，用于记录首末元素。同时，定义内部类Node，作为LinkedList中保存数据的基本结构。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /** * 3.LinkedList的源码分析： * LinkedList list = new LinkedList(); 内部声明了Node类型的first和last属性，默认值为null * list.add(123);//将123封装到Node中，创建了Node对象。 * * 其中，Node定义为：体现了LinkedList的双向链表的说法 * private static class Node\u0026lt;E\u0026gt; { * E item; * Node\u0026lt;E\u0026gt; next; * Node\u0026lt;E\u0026gt; prev; * * Node(Node\u0026lt;E\u0026gt; prev, E element, Node\u0026lt;E\u0026gt; next) { * this.item = element; * this.next = next; //next变量记录下一个元素的位置 * this.prev = prev; //prev变量记录前一个元素的位置 * } * } */ Vector的源码分析 Vector 是一个古老的集合，JDK1.0就有了。大多数操作与ArrayList相同，区别之处在于Vector是线程安全的。 在各种list中，最好把ArrayList作为缺省选择。当插入、删除频繁时，使用LinkedList；Vector总是比ArrayList慢，所以尽量避免使用。 1 2 3 4 /** * 4.Vector的源码分析：jdk7和jdk8中通过Vector()构造器创建对象时，底层都创建了长度为10的数组。 * 在扩容方面，默认扩容为原来的数组长度的2倍。 */ List接口中的常用方法测试 List除了从Collection集合继承的方法外，List 集合里添加了一些根据索引来操作集合元素的方法。 void add(intindex, Object ele):在index位置插入ele元素 boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来 Object get(int index):获取指定index位置的元素 int indexOf(Object obj):返回obj在集合中首次出现的位置 int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置 Object remove(int index):移除指定index位置的元素，并返回此元素 Object set(int index, Object ele):设置指定index位置的元素为ele List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; /** * * 5.List接口的常用方法 * * * * @create 2020-05-12 20:54 */ public class ListTest { /** * * void add(int index, Object ele):在index位置插入ele元素 * boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来 * Object get(int index):获取指定index位置的元素 * int indexOf(Object obj):返回obj在集合中首次出现的位置 * int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置 * Object remove(int index):移除指定index位置的元素，并返回此元素 * Object set(int index, Object ele):设置指定index位置的元素为ele * List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合 * * 总结：常用方法 * 增：add(Object obj) * 删：remove(int index) / remove(Object obj) * 改：set(int index, Object ele) * 查：get(int index) * 插：add(int index, Object ele) * 长度：size() * 遍历：① Iterator迭代器方式 * ② 增强for循环 * ③ 普通的循环 * */ @Test public void test3(){ ArrayList list = new ArrayList(); list.add(123); list.add(456); list.add(\u0026#34;AA\u0026#34;); //方式一：Iterator迭代器方式 Iterator iterator = list.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } System.out.println(\u0026#34;***************\u0026#34;); //方式二：增强for循环 for(Object obj : list){ System.out.println(obj); } System.out.println(\u0026#34;***************\u0026#34;); //方式三：普通for循环 for(int i = 0;i \u0026lt; list.size();i++){ System.out.println(list.get(i)); } } @Test public void tets2(){ ArrayList list = new ArrayList(); list.add(123); list.add(456); list.add(\u0026#34;AA\u0026#34;); list.add(new Person(\u0026#34;Tom\u0026#34;,12)); list.add(456); //int indexOf(Object obj):返回obj在集合中首次出现的位置。如果不存在，返回-1. int index = list.indexOf(4567); System.out.println(index); //int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置。如果不存在，返回-1. System.out.println(list.lastIndexOf(456)); //Object remove(int index):移除指定index位置的元素，并返回此元素 Object obj = list.remove(0); System.out.println(obj); System.out.println(list); //Object set(int index, Object ele):设置指定index位置的元素为ele list.set(1,\u0026#34;CC\u0026#34;); System.out.println(list); //List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的左闭右开区间的子集合 List subList = list.subList(2, 4); System.out.println(subList); System.out.println(list); } @Test public void test(){ ArrayList list = new ArrayList(); list.add(123); list.add(456); list.add(\u0026#34;AA\u0026#34;); list.add(new Person(\u0026#34;Tom\u0026#34;,12)); list.add(456); System.out.println(list); //void add(int index, Object ele):在index位置插入ele元素 list.add(1,\u0026#34;BB\u0026#34;); System.out.println(list); //boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来 List list1 = Arrays.asList(1, 2, 3); list.addAll(list1); // list.add(list1); System.out.println(list.size());//9 //Object get(int index):获取指定index位置的元素 System.out.println(list.get(2)); } } List的一个面试题 面试题1 请问ArrayList/LinkedList/Vector的异同？谈谈你的理解？ArrayList底层是什么？扩容机制？Vector和ArrayList的最大区别?\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /** * 请问ArrayList/LinkedList/Vector的异同？谈谈你的理解？ * ArrayList底层是什么？扩容机制？Vector和ArrayList的最大区别? * * ArrayList和LinkedList的异同二者都线程不安全，相对线程安全的Vector，执行效率高。 * 此外，ArrayList是实现了基于动态数组的数据结构，LinkedList基于链表的数据结构。 * 对于随机访问get和set，ArrayList觉得优于LinkedList，因为LinkedList要移动指针。 * 对于新增和删除操作add(特指插入)和remove，LinkedList比较占优势，因为ArrayList要移动数据。 * * ArrayList和Vector的区别Vector和ArrayList几乎是完全相同的, * 唯一的区别在于Vector是同步类(synchronized)，属于强同步类。 * 因此开销就比ArrayList要大，访问要慢。正常情况下, * 大多数的Java程序员使用ArrayList而不是Vector, * 因为同步完全可以由程序员自己来控制。Vector每次扩容请求其大小的2倍空间， * 而ArrayList是1.5倍。Vector还有一个子类Stack。 */ 面试题2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import org.junit.Test; import java.util.ArrayList; import java.util.List; /** * * @create 2020-05-12 23:07 */ public class ListEver { /** * 区分List中remove(int index)和remove(Object obj) */ @Test public void testListRemove() { List list = new ArrayList(); list.add(1); list.add(2); list.add(3); updateList(list); System.out.println(list);// } private void updateList(List list) { // list.remove(2); list.remove(new Integer(2)); } } 5.Collection子接口之二：Set接口 Set接口是Collection的子接口，set接口没有提供额外的方法 Set 集合不允许包含相同的元素，如果试把两个相同的元素加入同一个Set 集合中，则添加操作失败。 Set 判断两个对象是否相同不是使用== 运算符，而是根据equals() 方法 Set接口实现类的对比 1 2 3 4 5 6 7 8 9 10 11 12 /** * 1.Set接口的框架： * |----Collection接口：单列集合，用来存储一个一个的对象 * |----Set接口：存储无序的、不可重复的数据 --\u0026gt;高中讲的“集合” * |----HashSet：作为Set接口的主要实现类；线程不安全的；可以存储null值 * |----LinkedHashSet：作为HashSet的子类；遍历其内部数据时，可以按照添加的顺序遍历 * 对于频繁的遍历操作，LinkedHashSet效率高于HashSet. * |----TreeSet：可以按照添加对象的指定属性，进行排序。 * * * @create 2020-05-13 8:24 */ Set的无序性与不可重复性的理解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import org.junit.Test; import java.util.HashSet; import java.util.Iterator; import java.util.Set; /** * * 1.Set接口中没有定义额外的方法，使用的都是Collection中声明过的方法。 * * * * @create 2020-05-13 8:24 */ public class SetTest { /** * 一、Set:存储无序的、不可重复的数据 * 1.无序性：不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加，而是根据数据的哈希值决定的。 * * 2.不可重复性：保证添加的元素按照equals()判断时，不能返回true.即：相同的元素只能添加一个。 * * 二、添加元素的过程：以HashSet为例： * * */ @Test public void test(){ Set set = new HashSet(); set.add(123); set.add(456); set.add(\u0026#34;fgd\u0026#34;); set.add(\u0026#34;book\u0026#34;); set.add(new User(\u0026#34;Tom\u0026#34;,12)); set.add(new User(\u0026#34;Tom\u0026#34;,12)); set.add(129); Iterator iterator = set.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } } } User类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 /** * * * @create 2020-05-13 8:47 */ public class User{ private String name; private int age; public User() { } public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return \u0026#34;User{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#39;}\u0026#39;; } @Override public boolean equals(Object o) { System.out.println(\u0026#34;User equals()....\u0026#34;); if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; if (age != user.age) return false; return name != null ? name.equals(user.name) : user.name == null; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; } } HashSet中元素的添加过程 HashSet是Set 接口的典型实现，大多数时候使用Set 集合时都使用这个实现类。 HashSet按Hash 算法来存储集合中的元素，因此具有很好的存取、查找、删除性能。 HashSet具有以下特点：不能保证元素的排列顺序 HashSet不是线程安全的 集合元素可以是null HashSet 集合判断两个元素相等的标准：两个对象通过hashCode() 方法比较相等，并且两个对象的equals() 方法返回值也相等。 对于存放在Set容器中的对象，对应的类一定要重写equals()和hashCode(Object obj)方法，以实现对象相等规则。即：“相等的对象必须具有相等的散列码”。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /** * 一、Set:存储无序的、不可重复的数据 * 1.无序性：不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加，而是根据数据的哈希值决定的。 * * 2.不可重复性：保证添加的元素按照equals()判断时，不能返回true.即：相同的元素只能添加一个。 * * 二、添加元素的过程：以HashSet为例： * 我们向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法，计算元素a的哈希值， * 此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置（即为：索引位置），判断 * 数组此位置上是否已经有元素： * 如果此位置上没有其他元素，则元素a添加成功。 ---\u0026gt;情况1 * 如果此位置上有其他元素b(或以链表形式存在的多个元素），则比较元素a与元素b的hash值： * 如果hash值不相同，则元素a添加成功。---\u0026gt;情况2 * 如果hash值相同，进而需要调用元素a所在类的equals()方法： * equals()返回true,元素a添加失败 * equals()返回false,则元素a添加成功。---\u0026gt;情况2 * * 对于添加成功的情况2和情况3而言：元素a 与已经存在指定索引位置上数据以链表的方式存储。 * jdk 7 :元素a放到数组中，指向原来的元素。 * jdk 8 :原来的元素在数组中，指向元素a * 总结：七上八下 * * HashSet底层：数组+链表的结构。 * */ 底层也是数组，初始容量为16，当如果使用率超过0.75，（16*0.75=12）就会扩大容量为原来的2倍。（16扩容为32，依次为64,128\u0026hellip;.等） 关于hashCode()和equals()的重写 重写hashCode() 方法的基本原则 在程序运行时，同一个对象多次调用hashCode() 方法应该返回相同的值。 当两个对象的equals() 方法比较返回true 时，这两个对象的hashCode() 方法的返回值也应相等。 对象中用作equals() 方法比较的Field，都应该用来计算hashCode 值。 重写equals() 方法的基本原则 以自定义的Customer类为例，何时需要重写equals()？\n当一个类有自己特有的“逻辑相等”概念,当改写equals()的时候，总是要改写hashCode()，根据一个类的equals方法（改写后），两个截然不同的实例有可能在逻辑上是相等的，但是，根据Object.hashCode()方法，它们仅仅是两个对象。 因此，违反了“相等的对象必须具有相等的散列码”。 结论：复写equals方法的时候一般都需要同时复写hashCode方法。通常参与计算hashCode的对象的属性也应该参与到equals()中进行计算。 Eclipse/IDEA工具里hashCode()的重写 以Eclipse/IDEA为例，在自定义类中可以调用工具自动重写equals和hashCode。问题：为什么用Eclipse/IDEA复写hashCode方法，有31这个数字？\n选择系数的时候要选择尽量大的系数。因为如果计算出来的hash地址越大，所谓的“冲突”就越少，查找起来效率也会提高。（减少冲突） 并且31只占用5bits,相乘造成数据溢出的概率较小。 31可以由i*31== (i\u0026laquo;5)-1来表示,现在很多虚拟机里面都有做相关优化。（提高算法效率） 31是一个素数，素数作用就是如果我用一个数字来乘以这个素数，那么最终出来的结果只能被素数本身和被乘数还有1来整除！(减少冲突) 1 2 3 4 5 /** * 2.要求：向Set(主要指：HashSet、LinkedHashSet)中添加的数据，其所在的类一定要重写hashCode()和equals() * 要求：重写的hashCode()和equals()尽可能保持一致性：相等的对象必须具有相等的散列码 * 重写两个方法的小技巧：对象中用作 equals() 方法比较的 Field，都应该用来计算 hashCode 值。 */ LinkedHashSet的使用 LinkedHashSet是HashSet的子类 LinkedHashSet根据元素的hashCode值来决定元素的存储位置，但它同时使用双向链表维护元素的次序，这使得元素看起来是以插入顺序保存的。 LinkedHashSet插入性能略低于HashSet，但在迭代访问Set 里的全部元素时有很好的性能。 LinkedHashSet不允许集合元素重复。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import org.junit.Test; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Set; public class SetTest { /** * LinkedHashSet的使用 * LinkedHashSet作为HashSet的子类，在添加数据的同时，每个数据还维护了两个引用，记录此数据前一个 * 数据和后一个数据。 * 优点：对于频繁的遍历操作，LinkedHashSet效率高于HashSet */ @Test public void test2(){ Set set = new LinkedHashSet(); set.add(456); set.add(123); set.add(123); set.add(\u0026#34;AA\u0026#34;); set.add(\u0026#34;CC\u0026#34;); set.add(new User(\u0026#34;Tom\u0026#34;,12)); set.add(new User(\u0026#34;Tom\u0026#34;,12)); set.add(129); Iterator iterator = set.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } } } User类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 /** * * * @create 2020-05-13 8:47 */ public class User{ private String name; private int age; public User() { } public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return \u0026#34;User{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#39;}\u0026#39;; } @Override public boolean equals(Object o) { System.out.println(\u0026#34;User equals()....\u0026#34;); if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; if (age != user.age) return false; return name != null ? name.equals(user.name) : user.name == null; } @Override public int hashCode() { //return name.hashCode() + age; int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; } } TreeSet的自然排序 TreeSet是SortedSet接口的实现类，TreeSet可以确保集合元素处于排序状态。\nTreeSet底层使用红黑树结构存储数据\n新增的方法如下：(了解)\nComparator comparator() Object first() Object last() Object lower(Object e) Object higher(Object e) SortedSet subSet(fromElement, toElement) SortedSet headSet(toElement) SortedSet tailSet(fromElement) TreeSet两种排序方法：自然排序和定制排序。默认情况下，TreeSet采用自然排序。\nTreeSet和后面要讲的TreeMap采用红黑树的存储结构\n特点：有序，查询速度比List快\n自然排序：TreeSet会调用集合元素的compareTo(Object obj) 方法来比较元素之间的大小关系，然后将集合元素按升序(默认情况)排列。\n如果试图把一个对象添加到TreeSet时，则该对象的类必须实现Comparable 接口。\n实现Comparable 的类必须实现compareTo(Object obj) 方法，两个对象即通过compareTo(Object obj) 方法的返回值来比较大小。 Comparable 的典型实现：\nBigDecimal、BigInteger 以及所有的数值型对应的包装类：按它们对应的数值大小进行比较 Character：按字符的unicode值来进行比较 Boolean：true 对应的包装类实例大于false 对应的包装类实例 String：按字符串中字符的unicode 值进行比较 Date、Time：后边的时间、日期比前面的时间、日期大 向TreeSet中添加元素时，只有第一个元素无须比较compareTo()方法，后面添加的所有元素都会调用compareTo()方法进行比较。\n因为只有相同类的两个实例才会比较大小，所以向TreeSet中添加的应该是同一个类的对象。\n对于TreeSet集合而言，它判断两个对象是否相等的唯一标准是：两个对象通过compareTo(Object obj) 方法比较返回值。\n当需要把一个对象放入TreeSet中，重写该对象对应的equals() 方法时，应保证该方法与compareTo(Object obj) 方法有一致的结果：如果两个对象通过equals() 方法比较返回true，则通过compareTo(Object obj) 方法比较应返回0。否则，让人难以理解。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import org.junit.Test; import java.util.Iterator; import java.util.TreeSet; /** * 1.向TreeSet中添加的数据，要求是相同类的对象。 * 2.两种排序方式：自然排序（实现Comparable接口） 和 定制排序（Comparator） * 3.自然排序中，比较两个对象是否相同的标准为：compareTo()返回0.不再是equals(). * 4.定制排序中，比较两个对象是否相同的标准为：compare()返回0.不再是equals(). * * * @create 2020-05-13 9:41 */ public class TreeSetTest { @Test public void test() { TreeSet set = new TreeSet(); //失败：不能添加不同类的对象 // set.add(123); // set.add(456); // set.add(\u0026#34;AA\u0026#34;); // set.add(new User(\u0026#34;Tom\u0026#34;,12)); //举例一： // set.add(34); // set.add(-34); // set.add(43); // set.add(11); // set.add(8); //举例二： set.add(new User(\u0026#34;Tom\u0026#34;,12)); set.add(new User(\u0026#34;Jerry\u0026#34;,32)); set.add(new User(\u0026#34;Jim\u0026#34;,2)); set.add(new User(\u0026#34;Mike\u0026#34;,65)); set.add(new User(\u0026#34;Jack\u0026#34;,33)); set.add(new User(\u0026#34;Jack\u0026#34;,56)); Iterator iterator = set.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } } } User类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 /** * * * @create 2020-05-13 8:47 */ public class User implements Comparable{ private String name; private int age; public User() { } public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return \u0026#34;User{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#39;}\u0026#39;; } @Override public boolean equals(Object o) { System.out.println(\u0026#34;User equals()....\u0026#34;); if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; if (age != user.age) return false; return name != null ? name.equals(user.name) : user.name == null; } @Override public int hashCode() { //return name.hashCode() + age; int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; } //按照姓名从大到小排列,年龄从小到大排列 @Override public int compareTo(Object o) { if (o instanceof User) { User user = (User) o; // return this.name.compareTo(user.name); //按照姓名从小到大排列 // return -this.name.compareTo(user.name); //按照姓名从大到小排列 int compare = -this.name.compareTo(user.name); //按照姓名从大到小排列 if(compare != 0){ //年龄从小到大排列 return compare; }else{ return Integer.compare(this.age,user.age); } } else { throw new RuntimeException(\u0026#34;输入的类型不匹配\u0026#34;); } } } TreeSet的定制排序 TreeSet的自然排序要求元素所属的类实现Comparable接口，如果元素所属的类没有实现Comparable接口，或不希望按照升序(默认情况)的方式排列元素或希望按照其它属性大小进行排序，则考虑使用定制排序。定制排序，通过Comparator接口来实现。需要重写compare(T o1,T o2)方法。 利用int compare(T o1,T o2)方法，比较o1和o2的大小：如果方法返回正整数，则表示o1大于o2；如果返回0，表示相等；返回负整数，表示o1小于o2。 要实现定制排序，需要将实现Comparator接口的实例作为形参传递给TreeSet的构造器。 此时，仍然只能向TreeSet中添加类型相同的对象。否则发生ClassCastException异常。 使用定制排序判断两个元素相等的标准是：通过Comparator比较两个元素返回了0。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import org.junit.Test; import java.util.Comparator; import java.util.Iterator; import java.util.TreeSet; /** * 1.向TreeSet中添加的数据，要求是相同类的对象。 * 2.两种排序方式：自然排序（实现Comparable接口） 和 定制排序（Comparator） * 3.自然排序中，比较两个对象是否相同的标准为：compareTo()返回0.不再是equals(). * 4.定制排序中，比较两个对象是否相同的标准为：compare()返回0.不再是equals(). * * * @create 2020-05-13 9:41 */ public class TreeSetTest { @Test public void tets2(){ Comparator com = new Comparator() { //按照年龄从小到大排列 @Override public int compare(Object o1, Object o2) { if(o1 instanceof User \u0026amp;\u0026amp; o2 instanceof User){ User u1 = (User)o1; User u2 = (User)o2; return Integer.compare(u1.getAge(),u2.getAge()); }else{ throw new RuntimeException(\u0026#34;输入的数据类型不匹配\u0026#34;); } } }; TreeSet set = new TreeSet(com); set.add(new User(\u0026#34;Tom\u0026#34;,12)); set.add(new User(\u0026#34;Jerry\u0026#34;,32)); set.add(new User(\u0026#34;Jim\u0026#34;,2)); set.add(new User(\u0026#34;Mike\u0026#34;,65)); set.add(new User(\u0026#34;Mary\u0026#34;,33)); set.add(new User(\u0026#34;Jack\u0026#34;,33)); set.add(new User(\u0026#34;Jack\u0026#34;,56)); Iterator iterator = set.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } } } User类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 /** * * * @create 2020-05-13 8:47 */ public class User implements Comparable{ private String name; private int age; public User() { } public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return \u0026#34;User{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#39;}\u0026#39;; } @Override public boolean equals(Object o) { System.out.println(\u0026#34;User equals()....\u0026#34;); if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; if (age != user.age) return false; return name != null ? name.equals(user.name) : user.name == null; } @Override public int hashCode() { //return name.hashCode() + age; int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; } //按照姓名从大到小排列,年龄从小到大排列 @Override public int compareTo(Object o) { if (o instanceof User) { User user = (User) o; // return this.name.compareTo(user.name); //按照姓名从小到大排列 // return -this.name.compareTo(user.name); //按照姓名从大到小排列 int compare = -this.name.compareTo(user.name); //按照姓名从大到小排列 if(compare != 0){ //年龄从小到大排列 return compare; }else{ return Integer.compare(this.age,user.age); } } else { throw new RuntimeException(\u0026#34;输入的类型不匹配\u0026#34;); } } } TreeSet的课后练习 MyDate类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 /** * MyDate类包含: * private成员变量year,month,day；并为每一个属性定义getter, setter 方法； * * * @create 2020-05-13 15:21 */ public class MyDate implements Comparable{ private int year; private int month; private int day; public int getYear() { return year; } public void setYear(int year) { this.year = year; } public int getMonth() { return month; } public void setMonth(int month) { this.month = month; } public int getDay() { return day; } public void setDay(int day) { this.day = day; } public MyDate() { } public MyDate(int year, int month, int day) { this.year = year; this.month = month; this.day = day; } @Override public String toString() { return \u0026#34;MyDate{\u0026#34; + \u0026#34;year=\u0026#34; + year + \u0026#34;, month=\u0026#34; + month + \u0026#34;, day=\u0026#34; + day + \u0026#39;}\u0026#39;; } @Override public int compareTo(Object o) { if(o instanceof MyDate){ MyDate m = (MyDate)o; //比较年 int minusYear = this.getYear() - m.getYear(); if(minusYear != 0){ return minusYear; } //比较月 int minusMonth = this.getMonth() - m.getMonth(); if(minusMonth != 0){ return minusMonth; } //比较日 return this.getDay() - m.getDay(); } throw new RuntimeException(\u0026#34;传入的数据类型不一致！\u0026#34;); } } Employee类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 /** * 定义一个Employee类。 * 该类包含：private成员变量name,age,birthday， * 其中birthday 为MyDate 类的对象； * 并为每一个属性定义getter, setter 方法； * 并重写toString 方法输出name, age, birthday * * * @create 2020-05-13 15:24 */ public class Employee implements Comparable{ private String name; private int age; private MyDate birthday; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public MyDate getBirthday() { return birthday; } public void setBirthday(MyDate birthday) { this.birthday = birthday; } public Employee() { } public Employee(String name, int age, MyDate birthday) { this.name = name; this.age = age; this.birthday = birthday; } @Override public String toString() { return \u0026#34;Employee{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#34;, birthday=\u0026#34; + birthday + \u0026#39;}\u0026#39;; } //按name排序 @Override public int compareTo(Object o){ if(o instanceof Employee){ Employee e = (Employee)o; return this.name.compareTo(e.name); } // return 0; throw new RuntimeException(\u0026#34;传入的数据类型不一致\u0026#34;); } } 测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 import org.junit.Test; import java.util.Comparator; import java.util.Iterator; import java.util.TreeSet; /** * 创建该类的5 个对象，并把这些对象放入TreeSet 集合中 * （下一章：TreeSet 需使用泛型来定义）分别按以下两种方式 * 对集合中的元素进行排序，并遍历输出： * * 1). 使Employee 实现Comparable 接口，并按name 排序 * 2). 创建TreeSet 时传入Comparator对象，按生日日期的先后排序。 * * * @create 2020-05-13 15:30 */ public class EmployeeTest { //问题二：按生日日期的先后排序 @Test public void test2(){ TreeSet set = new TreeSet(new Comparator() { @Override public int compare(Object o1, Object o2) { if(o1 instanceof Employee \u0026amp;\u0026amp; o2 instanceof Employee){ Employee e1 = (Employee)o1; Employee e2 = (Employee)o2; MyDate b1 = e1.getBirthday(); MyDate b2 = e2.getBirthday(); //方式一： // //比较年 // int minusYear = b1.getYear() - b2.getYear(); // if(minusYear != 0){ // return minusYear; // } // // //比较月 // int minusMonth = b1.getMonth() - b2.getMonth(); // if(minusMonth != 0){ // return minusMonth; // } // // //比较日 // return b1.getDay() - b2.getDay(); //方式二： return b1.compareTo(b2); } // return 0; throw new RuntimeException(\u0026#34;传入的数据类型不一致！\u0026#34;); } }); Employee e1 = new Employee(\u0026#34;wangxianzhi\u0026#34;,41,new MyDate(334,5,4)); Employee e2 = new Employee(\u0026#34;simaqian\u0026#34;,43,new MyDate(-145,7,12)); Employee e3 = new Employee(\u0026#34;yanzhenqin\u0026#34;,44,new MyDate(709,5,9)); Employee e4 = new Employee(\u0026#34;zhangqian\u0026#34;,51,new MyDate(-179,8,12)); Employee e5 = new Employee(\u0026#34;quyuan\u0026#34;,21,new MyDate(-340,12,4)); set.add(e1); set.add(e2); set.add(e3); set.add(e4); set.add(e5); Iterator iterator = set.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } } //问题一：使用自然排序 @Test public void test(){ TreeSet set = new TreeSet(); Employee e1 = new Employee(\u0026#34;wangxianzhi\u0026#34;,41,new MyDate(334,5,4)); Employee e2 = new Employee(\u0026#34;simaqian\u0026#34;,43,new MyDate(-145,7,12)); Employee e3 = new Employee(\u0026#34;yanzhenqin\u0026#34;,44,new MyDate(709,5,9)); Employee e4 = new Employee(\u0026#34;zhangqian\u0026#34;,51,new MyDate(-179,8,12)); Employee e5 = new Employee(\u0026#34;quyuan\u0026#34;,21,new MyDate(-340,12,4)); set.add(e1); set.add(e2); set.add(e3); set.add(e4); set.add(e5); Iterator iterator = set.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); } } } Set课后两道面试题 练习：在List内去除重复数字值，要求尽量简单 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import org.junit.Test; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; /** * * @create 2020-05-13 14:43 */ public class CollectionTest { //练习：在List内去除重复数字值，要求尽量简单 public static List duplicateList(List list) { HashSet set = new HashSet(); set.addAll(list); return new ArrayList(set); } @Test public void test2(){ List list = new ArrayList(); list.add(new Integer(1)); list.add(new Integer(2)); list.add(new Integer(2)); list.add(new Integer(4)); list.add(new Integer(4)); List list2 = duplicateList(list); for (Object integer : list2) { System.out.println(integer); } } } 面试题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import org.junit.Test; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; /** * * @create 2020-05-13 14:43 */ public class CollectionTest { @Test public void test3(){ HashSet set = new HashSet(); Person p1 = new Person(1001,\u0026#34;AA\u0026#34;); Person p2 = new Person(1002,\u0026#34;BB\u0026#34;); set.add(p1); set.add(p2); System.out.println(set); p1.name = \u0026#34;CC\u0026#34;; set.remove(p1); System.out.println(set); set.add(new Person(1001,\u0026#34;CC\u0026#34;)); System.out.println(set); set.add(new Person(1001,\u0026#34;AA\u0026#34;)); System.out.println(set); } } Person类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 /** * * @create 2020-05-13 16:13 */ public class Person { int id; String name; public Person(int id, String name) { this.id = id; this.name = name; } public Person() { } @Override public String toString() { return \u0026#34;Person{\u0026#34; + \u0026#34;id=\u0026#34; + id + \u0026#34;, name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; if (id != person.id) return false; return name != null ? name.equals(person.name) : person.name == null; } @Override public int hashCode() { int result = id; result = 31 * result + (name != null ? name.hashCode() : 0); return result; } } 6.Map接口 Map接口及其多个实现类的对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import org.junit.Test; import java.util.HashMap; import java.util.Map; /** * 一、Map的实现类的结构： * |----Map:双列数据，存储key-value对的数据 ---类似于高中的函数：y = f(x) * |----HashMap:作为Map的主要实现类；线程不安全的，效率高；存储null的key和value * |----LinkedHashMap:保证在遍历map元素时，可以按照添加的顺序实现遍历。 * 原因：在原有的HashMap底层结构基础上，添加了一对指针，指向前一个和后一个元素。 * 对于频繁的遍历操作，此类执行效率高于HashMap。 * |----TreeMap:保证按照添加的key-value对进行排序，实现排序遍历。此时考虑key的自然排序或定制排序 * 底层使用红黑树 * |----Hashtable:作为古老的实现类；线程安全的，效率低；不能存储null的key和value * |----Properties:常用来处理配置文件。key和value都是String类型 * * * HashMap的底层：数组+链表 （jdk7及之前） * 数组+链表+红黑树 （jdk 8） * * 面试题： * 1. HashMap的底层实现原理？ * 2. HashMap 和 Hashtable的异同？ * 3. CurrentHashMap 与 Hashtable的异同？（暂时不讲） * * * @create 2020-05-13 16:37 */ public class MapTest { @Test public void test(){ Map map = new HashMap(); // map = new Hashtable(); map.put(null,123); } } Map中存储的key-value的特点 Map与Collection并列存在。用于保存具有映射关系的数据:key-value Map 中的key 和value 都可以是任何引用类型的数据 Map 中的key 用Set来存放，不允许重复，即同一个Map 对象所对应的类，须重写hashCode()和equals()方法 常用String类作为Map的“键” key 和value 之间存在单向一对一关系，即通过指定的key 总能找到唯一的、确定的value Map接口的常用实现类：HashMap、TreeMap、LinkedHashMap和Properties。其中，HashMap是Map 接口使用频率最高的实现类 1 2 3 4 5 6 7 8 /** * 二、Map结构的理解： * Map中的key:无序的、不可重复的，使用Set存储所有的key ---\u0026gt; key所在的类要重写equals()和hashCode() （以HashMap为例） * Map中的value:无序的、可重复的，使用Collection存储所有的value ---\u0026gt;value所在的类要重写equals() * 一个键值对：key-value构成了一个Entry对象。 * Map中的entry:无序的、不可重复的，使用Set存储所有的entry * */ Map实现类之一：HashMap HashMap是Map 接口使用频率最高的实现类。 允许使用null键和null值，与HashSet一样，不保证映射的顺序。 所有的key构成的集合是Set:无序的、不可重复的。所以，key所在的类要重写：equals()和hashCode() 所有的value构成的集合是Collection:无序的、可以重复的。所以，value所在的类要重写：equals() 一个key-value构成一个entry 所有的entry构成的集合是Set:无序的、不可重复的 HashMap 判断两个key 相等的标准是：两个key 通过equals() 方法返回true，hashCode值也相等。 HashMap判断两个value相等的标准是：两个value 通过equals() 方法返回true。 HashMap的底层实现原理 JDK 7及以前版本：HashMap是数组+链表结构(即为链地址法)\nJDK 8版本发布以后：HashMap是数组+链表+红黑树实现。\nHashMap源码中的重要常量 1 2 3 4 5 6 7 /* * DEFAULT_INITIAL_CAPACITY : HashMap的默认容量，16 * DEFAULT_LOAD_FACTOR：HashMap的默认加载因子：0.75 * threshold：扩容的临界值，=容量*填充因子：16 * 0.75 =\u0026gt; 12 * TREEIFY_THRESHOLD：Bucket中链表长度大于该默认值，转化为红黑树:8 * MIN_TREEIFY_CAPACITY：桶中的Node被树化时最小的hash表容量:64 */ HashMap在JDK7中的底层实现原理 HashMap的内部存储结构其实是数组和链表的结合。当实例化一个HashMap时，系统会创建一个长度为Capacity的Entry数组，这个长度在哈希表中被称为容量(Capacity)，在这个数组中可以存放元素的位置我们称之为“桶”(bucket)，每个bucket都有自己的索引，系统可以根据索引快速的查找bucket中的元素。 每个bucket中存储一个元素，即一个Entry对象，但每一个Entry对象可以带一个引用变量，用于指向下一个元素，因此，在一个桶中，就有可能生成一个Entry链。而且新添加的元素作为链表的head。 添加元素的过程： 向HashMap中添加entry1(key，value)，需要首先计算entry1中key的哈希值(根据key所在类的hashCode()计算得到)，此哈希值经过处理以后，得到在底层Entry[]数组中要存储的位置i。如果位置i上没有元素，则entry1直接添加成功。如果位置i上已经存在entry2(或还有链表存在的entry3，entry4)，则需要通过循环的方法，依次比较entry1中key和其他的entry。如果彼此hash值不同，则直接添加成功。如果hash值不同，继续比较二者是否equals。如果返回值为true，则使用entry1的value去替换equals为true的entry的value。如果遍历一遍以后，发现所有的equals返回都为false,则entry1仍可添加成功。entry1指向原有的entry元素。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 /* * 三、HashMap的底层实现原理？以jdk7为例说明： * HashMap map = new HashMap(): * 在实例化以后，底层创建了长度是16的一维数组Entry[] table。 * ...可能已经执行过多次put... * map.put(key1,value1): * 首先，调用key1所在类的hashCode()计算key1哈希值，此哈希值经过某种算法计算以后，得到在Entry数组中的存放位置。 * 如果此位置上的数据为空，此时的key1-value1添加成功。 ----情况1 * 如果此位置上的数据不为空，(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据 * 的哈希值： * 如果key1的哈希值与已经存在的数据的哈希值都不相同，此时key1-value1添加成功。----情况2 * 如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同，继续比较：调用key1所在类的equals(key2)方法，比较： * 如果equals()返回false:此时key1-value1添加成功。----情况3 * 如果equals()返回true:使用value1替换value2。 * * 补充：关于情况2和情况3：此时key1-value1和原来的数据以链表的方式存储。 * * 在不断的添加过程中，会涉及到扩容问题，当超出临界值(且要存放的位置非空)时，扩容。默认的扩容方式：扩容为原来容量的2倍，并将原有的数据复制过来。 * */ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /** * HashMap的扩容 * 当HashMap中的元素越来越多的时候，hash冲突的几率也就越来越高， * 因为数组的长度是固定的。所以为了提高查询的效率， * 就要对HashMap的数组进行扩容，而在HashMap数组扩容之后， * 最消耗性能的点就出现了：原数组中的数据必须重新计算其在新数组中的位置， * 并放进去，这就是resize。 * * 那么HashMap什么时候进行扩容呢？ * 当HashMap中的元素个数超过数组大小(数组总大小length, * 不是数组中个数size)*loadFactor时，就 会 进 行 数 组 扩 容， * loadFactor的默认值(DEFAULT_LOAD_FACTOR)为0.75，这是一个折中的取值。 * 也就是说，默认情况下，数组大小(DEFAULT_INITIAL_CAPACITY)为16， * 那么当HashMap中元素个数超过16*0.75=12（这个值就是代码中的threshold值， * 也叫做临界值）的时候，就把数组的大小扩展为2*16=32，即扩大一倍， * 然后重新计算每个元素在数组中的位置，而这是一个非常消耗性能的操作， * 所以如果我们已经预知HashMap中元素的个数， * 那么预设元素的个数能够有效的提高HashMap的性能。 */ HashMap在JDK8中的底层实现原理 HashMap的内部存储结构其实是数组+链表+树的结合。当实例化一个HashMap时，会初始化initialCapacity和loadFactor，在put第一对映射关系时，系统会创建一个长度为initialCapacity的Node数组，这个长度在哈希表中被称为容量(Capacity)，在这个数组中可以存放元素的位置我们称之为“桶”(bucket)，每个bucket都有自己的索引，系统可以根据索引快速的查找bucket中的元素。\n每个bucket中存储一个元素，即一个Node对象，但每一个Node对象可以带一个引用变量next，用于指向下一个元素，因此，在一个桶中，就有可能生成一个Node链。也可能是一个一个TreeNode对象，每一个TreeNode对象可以有两个叶子结点left和right，因此，在一个桶中，就有可能生成一个TreeNode树。而新添加的元素作为链表的last，或树的叶子结点。\n那么HashMap什么时候进行扩容和树形化呢？\n当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor时，就会进行数组扩容，loadFactor的默认值(DEFAULT_LOAD_FACTOR)为0.75，这是一个折中的取值。也就是说，默认情况下，数组大小(DEFAULT_INITIAL_CAPACITY)为16，那么当HashMap中元素个数超过160.75=12（这个值就是代码中的threshold值，也叫做临界值）的时候，就把数组的大小扩展为2*16=32，即扩大一倍，然后重新计算每个元素在数组中的位置，而这是一个非常消耗性能的操作，所以如果我们已经预知HashMap中元素的个数，那么预设元素的个数能够有效的提高HashMap的性能。\n当HashMap中的其中一个链的对象个数如果达到了8个，此时如果capacity没有达到64，那么HashMap会先扩容解决，如果已经达到了64，那么这个链会变成树，结点类型由Node变成TreeNode类型。当然，如果当映射关系被移除后，下次resize方法时判断树的结点个数低于6个，也会把树再转为链表。\n关于映射关系的key是否可以修改？answer：不要修改\n映射关系存储到HashMap中会存储key的hash值，这样就不用在每次查找时重新计算每一个Entry或Node（TreeNode）的hash值了，因此如果已经put到Map中的映射关系，再修改key的属性，而这个属性又参与hashcode值的计算，那么会导致匹配不上。\n1 2 3 4 5 6 7 8 9 /* 总结： * jdk8 相较于jdk7在底层实现方面的不同： * 1.new HashMap():底层没有创建一个长度为16的数组 * 2.jdk 8底层的数组是：Node[],而非Entry[] * 3.首次调用put()方法时，底层创建长度为16的数组 * 4.jdk7底层结构只有：数组+链表。jdk8中底层结构：数组+链表+红黑树。 * 4.1形成链表时，七上八下（jdk7:新的元素指向旧的元素。jdk8：旧的元素指向新的元素） * 4.2当数组的某一个索引位置上的元素以链表形式存在的数据个数 \u0026gt; 8 且当前数组的长度 \u0026gt; 64时，此时此索引位置上的所数据改为使用红黑树存储。 */ LinkedHashMap的底层实现原理（了解！！！） LinkedHashMap是HashMap的子类\n在HashMap存储结构的基础上，使用了一对双向链表来记录添加元素的顺序\n与LinkedHashSet类似，LinkedHashMap可以维护Map 的迭代顺序：迭代顺序与Key-Value 对的插入顺序一致\nHashMap中的内部类：Node\nLinkedHashMap中的内部类：Entry 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /* * 四、LinkedHashMap的底层实现原理（了解） * 源码中： * static class Entry\u0026lt;K,V\u0026gt; extends HashMap.Node\u0026lt;K,V\u0026gt; { * Entry\u0026lt;K,V\u0026gt; before, after;//能够记录添加的元素的先后顺序 * Entry(int hash, K key, V value, Node\u0026lt;K,V\u0026gt; next) { * super(hash, key, value, next); * } * } */ import org.junit.Test; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; public class MapTest { @Test public void test2(){ Map map = new HashMap(); map = new LinkedHashMap(); map.put(123,\u0026#34;AA\u0026#34;); map.put(345,\u0026#34;BB\u0026#34;); map.put(12,\u0026#34;CC\u0026#34;); System.out.println(map); } } Map中的常用方法1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 import org.junit.Test; import java.util.*; /** * 五、Map中定义的方法： * 添加、删除、修改操作： * Object put(Object key,Object value)：将指定key-value添加到(或修改)当前map对象中 * void putAll(Map m):将m中的所有key-value对存放到当前map中 * Object remove(Object key)：移除指定key的key-value对，并返回value * void clear()：清空当前map中的所有数据 * 元素查询的操作： * Object get(Object key)：获取指定key对应的value * boolean containsKey(Object key)：是否包含指定的key * boolean containsValue(Object value)：是否包含指定的value * int size()：返回map中key-value对的个数 * boolean isEmpty()：判断当前map是否为空 * boolean equals(Object obj)：判断当前map和参数对象obj是否相等 * 元视图操作的方法： * Set keySet()：返回所有key构成的Set集合 * Collection values()：返回所有value构成的Collection集合 * Set entrySet()：返回所有key-value对构成的Set集合 * * * * @create 2020-05-13 16:37 */ public class MapTest { /** * 元素查询的操作： * Object get(Object key)：获取指定key对应的value * boolean containsKey(Object key)：是否包含指定的key * boolean containsValue(Object value)：是否包含指定的value * int size()：返回map中key-value对的个数 * boolean isEmpty()：判断当前map是否为空 * boolean equals(Object obj)：判断当前map和参数对象obj是否相等 */ @Test public void test4(){ Map map = new HashMap(); map.put(\u0026#34;AA\u0026#34;,123); map.put(45,123); map.put(\u0026#34;BB\u0026#34;,56); // Object get(Object key) System.out.println(map.get(45)); //containsKey(Object key) boolean isExist = map.containsKey(\u0026#34;BB\u0026#34;); System.out.println(isExist); isExist = map.containsValue(123); System.out.println(isExist); map.clear(); System.out.println(map.isEmpty()); } /** * 添加、删除、修改操作： * Object put(Object key,Object value)：将指定key-value添加到(或修改)当前map对象中 * void putAll(Map m):将m中的所有key-value对存放到当前map中 * Object remove(Object key)：移除指定key的key-value对，并返回value * void clear()：清空当前map中的所有数据 */ @Test public void test3(){ Map map = new HashMap(); //添加 map.put(\u0026#34;AA\u0026#34;,123); map.put(45,123); map.put(\u0026#34;BB\u0026#34;,56); //修改 map.put(\u0026#34;AA\u0026#34;,87); System.out.println(map); Map map1 = new HashMap(); map1.put(\u0026#34;CC\u0026#34;,123); map1.put(\u0026#34;DD\u0026#34;,456); map.putAll(map1); System.out.println(map); //remove(Object key) Object value = map.remove(\u0026#34;CC\u0026#34;); System.out.println(value); System.out.println(map); //clear() map.clear();//与map = null操作不同 System.out.println(map.size()); System.out.println(map); } } Map中的常用方法2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 import org.junit.Test; import java.util.*; /** * 五、Map中定义的方法： * 添加、删除、修改操作： * Object put(Object key,Object value)：将指定key-value添加到(或修改)当前map对象中 * void putAll(Map m):将m中的所有key-value对存放到当前map中 * Object remove(Object key)：移除指定key的key-value对，并返回value * void clear()：清空当前map中的所有数据 * 元素查询的操作： * Object get(Object key)：获取指定key对应的value * boolean containsKey(Object key)：是否包含指定的key * boolean containsValue(Object value)：是否包含指定的value * int size()：返回map中key-value对的个数 * boolean isEmpty()：判断当前map是否为空 * boolean equals(Object obj)：判断当前map和参数对象obj是否相等 * 元视图操作的方法： * Set keySet()：返回所有key构成的Set集合 * Collection values()：返回所有value构成的Collection集合 * Set entrySet()：返回所有key-value对构成的Set集合 * * 总结：常用方法： * 添加：put(Object key,Object value) * 删除：remove(Object key) * 修改：put(Object key,Object value) * 查询：get(Object key) * 长度：size() * 遍历：keySet() / values() / entrySet() * * 面试题： * 1. HashMap的底层实现原理？ * 2. HashMap 和 Hashtable的异同？ * 1.HashMap与Hashtable都实现了Map接口。由于HashMap的非线程安全性，效率上可能高于Hashtable。Hashtable的方法是Synchronize的，而HashMap不是，在多个线程访问Hashtable时，不需要自己为它的方法实现同步，而HashMap 就必须为之提供外同步。 * 2.HashMap允许将null作为一个entry的key或者value，而Hashtable不允许。 * 3.HashMap把Hashtable的contains方法去掉了，改成containsvalue和containsKey。因为contains方法容易让人引起误解。 * 4.Hashtable继承自Dictionary类，而HashMap是Java1.2引进的Map interface的一个实现。 * 5.Hashtable和HashMap采用的hash/rehash算法都大概一样，所以性能不会有很大的差异。 * * 3. CurrentHashMap 与 Hashtable的异同？（暂时不讲） * * * @create 2020-05-13 16:37 */ public class MapTest { /** * 元视图操作的方法： * Set keySet()：返回所有key构成的Set集合 * Collection values()：返回所有value构成的Collection集合 * Set entrySet()：返回所有key-value对构成的Set集合 */ @Test public void test5(){ Map map = new HashMap(); map.put(\u0026#34;AA\u0026#34;,123); map.put(45,1234); map.put(\u0026#34;BB\u0026#34;,56); //遍历所有的key集：keySet() Set set = map.keySet(); Iterator iterator = set.iterator(); while(iterator.hasNext()){ System.out.println(iterator.next()); } System.out.println(\u0026#34;*****************\u0026#34;); //遍历所有的values集：values() Collection values = map.values(); for(Object obj : values){ System.out.println(obj); } System.out.println(\u0026#34;***************\u0026#34;); //遍历所有的key-values //方式一： Set entrySet = map.entrySet(); Iterator iterator1 = entrySet.iterator(); while (iterator1.hasNext()){ Object obj = iterator1.next(); //entrySet集合中的元素都是entry Map.Entry entry = (Map.Entry) obj; System.out.println(entry.getKey() + \u0026#34;----\u0026gt;\u0026#34; + entry.getValue()); } System.out.println(\u0026#34;/////////////////\u0026#34;); //方式二： Set keySet = map.keySet(); Iterator iterator2 = keySet.iterator(); while(iterator2.hasNext()){ Object key = iterator2.next(); Object value = map.get(key); System.out.println(key + \u0026#34;=====\u0026#34; + value); } } } TreeMap两种添加方式的使用 TreeMap存储Key-Value 对时，需要根据key-value 对进行排序。TreeMap可以保证所有的Key-Value 对处于有序状态。\nTreeSet底层使用红黑树结构存储数据\nTreeMap的Key 的排序：\n自然排序：TreeMap的所有的Key 必须实现Comparable 接口，而且所有的Key 应该是同一个类的对象，否则将会抛出ClasssCastException 定制排序：创建TreeMap时，传入一个Comparator 对象，该对象负责对TreeMap中的所有key 进行排序。此时不需要Map 的Key 实现Comparable 接口 TreeMap判断两个key相等的标准：两个key通过compareTo()方法或者compare()方法返回0。\nUser类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 /** * * @create 2020-05-13 19:24 */ public class User implements Comparable{ private String name; private int age; public User() { } public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return \u0026#34;User{\u0026#34; + \u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#39;}\u0026#39;; } @Override public boolean equals(Object o) { System.out.println(\u0026#34;User equals()....\u0026#34;); if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; if (age != user.age) return false; return name != null ? name.equals(user.name) : user.name == null; } @Override public int hashCode() { //return name.hashCode() + age; int result = name != null ? name.hashCode() : 0; result = 31 * result + age; return result; } //按照姓名从大到小排列,年龄从小到大排列 @Override public int compareTo(Object o) { if(o instanceof User){ User user = (User)o; // return -this.name.compareTo(user.name); int compare = -this.name.compareTo(user.name); if(compare != 0){ return compare; }else{ return Integer.compare(this.age,user.age); } }else{ throw new RuntimeException(\u0026#34;输入的类型不匹配\u0026#34;); } } } 测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 import org.junit.Test; import java.util.*; /** * * * @create 2020-05-13 19:23 */ public class TreeMapTest { /** * 向TreeMap中添加key-value，要求key必须是由同一个类创建的对象 * 因为要按照key进行排序：自然排序 、定制排序 */ //自然排序 @Test public void test(){ TreeMap map = new TreeMap(); User u1 = new User(\u0026#34;Tom\u0026#34;,23); User u2 = new User(\u0026#34;Jerry\u0026#34;,32); User u3 = new User(\u0026#34;Jack\u0026#34;,20); User u4 = new User(\u0026#34;Rose\u0026#34;,18); map.put(u1,98); map.put(u2,89); map.put(u3,76); map.put(u4,100); Set entrySet = map.entrySet(); Iterator iterator1 = entrySet.iterator(); while (iterator1.hasNext()){ Object obj = iterator1.next(); Map.Entry entry = (Map.Entry) obj; System.out.println(entry.getKey() + \u0026#34;----\u0026gt;\u0026#34; + entry.getValue()); } } //定制排序 @Test public void test2(){ TreeMap map = new TreeMap(new Comparator() { @Override public int compare(Object o1, Object o2) { if(o1 instanceof User \u0026amp;\u0026amp; o2 instanceof User){ User u1 = (User)o1; User u2 = (User)o2; return Integer.compare(u1.getAge(),u2.getAge()); } throw new RuntimeException(\u0026#34;输入的类型不匹配！\u0026#34;); } }); User u1 = new User(\u0026#34;Tom\u0026#34;,23); User u2 = new User(\u0026#34;Jerry\u0026#34;,32); User u3 = new User(\u0026#34;Jack\u0026#34;,20); User u4 = new User(\u0026#34;Rose\u0026#34;,18); map.put(u1,98); map.put(u2,89); map.put(u3,76); map.put(u4,100); Set entrySet = map.entrySet(); Iterator iterator1 = entrySet.iterator(); while (iterator1.hasNext()){ Object obj = iterator1.next(); Map.Entry entry = (Map.Entry) obj; System.out.println(entry.getKey() + \u0026#34;----\u0026gt;\u0026#34; + entry.getValue()); } } } Hashtable Hashtable是个古老的Map 实现类，JDK1.0就提供了。不同于HashMap，Hashtable是线程安全的。 Hashtable实现原理和HashMap相同，功能相同。底层都使用哈希表结构，查询速度快，很多情况下可以互用。 与HashMap不同，Hashtable不允许使用null 作为key 和value 与HashMap一样，Hashtable也不能保证其中Key-Value 对的顺序 Hashtable判断两个key相等、两个value相等的标准，与HashMap一致。 Properties处理属性文件 Properties 类是Hashtable的子类，该对象用于处理属性文件 由于属性文件里的key、value 都是字符串类型，所以Properties 里的key 和value 都是字符串类型 存取数据时，建议使用setProperty(String key,Stringvalue)方法和getProperty(String key)方法 新建jdbc.properties文件 编写源代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import java.io.FileInputStream; import java.io.IOException; import java.util.Properties; /** * * @create 2020-05-13 19:45 */ public class PropertiesTest { //Properties:常用来处理配置文件。key和value都是String类型 public static void main(String[] args){ //快捷键：ALT+Shift+Z FileInputStream fis = null; try { Properties pros = new Properties(); fis = new FileInputStream(\u0026#34;jdbc.properties\u0026#34;); pros.load(fis); //加载流对应文件 String name = pros.getProperty(\u0026#34;name\u0026#34;); String password = pros.getProperty(\u0026#34;password\u0026#34;); System.out.println(\u0026#34;name = \u0026#34; + name + \u0026#34;,password = \u0026#34; + password); } catch (IOException e) { e.printStackTrace(); } finally { if(fis != null){ try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } } } 如果jdbc.properties文件中写入为中文；\n防止jdbc.properties出现中文乱码，可根据如下解决：\n2.新建jdbc.properties\n7.Collections工具类 操作数组的工具类：Arrays\nCollections 是一个操作Set、List和Map 等集合的工具类\nCollections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作，还提供了对集合对象设置不可变、对集合对象实现同步控制等方法\n排序操作：（均为static方法）\nreverse(List)：反转List 中元素的顺序 shuffle(List)：对List集合元素进行随机排序 sort(List)：根据元素的自然顺序对指定List 集合元素按升序排序 sort(List，Comparator)：根据指定的Comparator 产生的顺序对List 集合元素进行排序 swap(List，int，int)：将指定list 集合中的i处元素和j 处元素进行交换 Collections工具类常用方法的测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 import org.junit.Test; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** * Collections:操作Collection、Map的工具类 * * 面试题：Collection 和 Collections的区别？ * Collection是集合类的上级接口，继承于他的接口主要有Set 和List. * Collections是针对集合类的一个帮助类，他提供一系列静态方法实现对各种集合的搜索、排序、线程安全化等操作. * * * @create 2020-05-13 19:59 */ public class CollectionTest { /** * reverse(List)：反转 List 中元素的顺序 * shuffle(List)：对 List 集合元素进行随机排序 * sort(List)：根据元素的自然顺序对指定 List 集合元素按升序排序 * sort(List，Comparator)：根据指定的 Comparator 产生的顺序对 List 集合元素进行排序 * swap(List，int， int)：将指定 list 集合中的 i 处元素和 j 处元素进行交换 * * Object max(Collection)：根据元素的自然顺序，返回给定集合中的最大元素 * Object max(Collection，Comparator)：根据 Comparator 指定的顺序，返回给定集合中的最大元素 * Object min(Collection) * Object min(Collection，Comparator) * int frequency(Collection，Object)：返回指定集合中指定元素的出现次数 * void copy(List dest,List src)：将src中的内容复制到dest中 * boolean replaceAll(List list，Object oldVal，Object newVal)：使用新值替换 List 对象的所有旧值 * */ @Test public void test(){ List list = new ArrayList(); list.add(123); list.add(43); list.add(765); list.add(765); list.add(765); list.add(-97); list.add(0); System.out.println(list); // Collections.reverse(list); // Collections.shuffle(list); // Collections.sort(list); // Collections.swap(list,1,2); int frequency = Collections.frequency(list, 123); System.out.println(list); System.out.println(frequency); } @Test public void test2(){ List list = new ArrayList(); list.add(123); list.add(43); list.add(765); list.add(-97); list.add(0); //报异常：IndexOutOfBoundsException(\u0026#34;Source does not fit in dest\u0026#34;) // List dest = new ArrayList(); // Collections.copy(dest,list); //正确的： List dest = Arrays.asList(new Object[list.size()]); System.out.println(dest.size());//list.size(); Collections.copy(dest,list); System.out.println(dest); /** * Collections 类中提供了多个 synchronizedXxx() 方法， * 该方法可使将指定集合包装成线程同步的集合，从而可以解决 * 多线程并发访问集合时的线程安全问题 */ //返回的list1即为线程安全的List List list1 = Collections.synchronizedList(list); } } 补充：Enumeration(了解！！！) Enumeration 接口是Iterator迭代器的“古老版本” 1 2 3 4 Enumeration stringEnum = new StringTokenizer(\u0026#34;a-b*c-d-e-g\u0026#34;, \u0026#34;-\u0026#34;); while(stringEnum.hasMoreElements()){ Object obj= stringEnum.nextElement();System.out.println(obj); } ","permalink":"https://ktzxy.top/posts/ezb970x3zv/","summary":"Day 09 集合","title":"Day 09 集合"},{"content":"[TOC]\nprometheus自定义监控内容 Prometheus Metrics 设计的最佳实践和应用实例，看这篇够了！ 一、io.micrometer的使用 在SpringBoot2.X中，spring-boot-starter-actuator引入了io.micrometer，对1.X中的metrics进行了重构，主要特点是支持tag/label，配合支持tag/label的监控系统，使得我们可以更加方便地对metrics进行多维度的统计查询及监控。io.micrometer目前支持Counter、Gauge、Timer、Summary等多种不同类型的度量方式(不知道有没有遗漏)，下面逐个简单分析一下它们的作用和使用方式。 需要在SpringBoot项目下引入下面的依赖：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.micrometer\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;micrometer-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${micrometer.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 目前最新的micrometer.version为1.0.5。注意一点的是：io.micrometer支持Tag(标签)的概念，Tag是其metrics是否能够有多维度的支持的基础，Tag必须成对出现，也就是必须配置也就是偶数个Tag，有点类似于K-V的关系。\n1.1 Counter Counter(计数器)简单理解就是一种只增不减的计数器。它通常用于记录服务的请求数量、完成的任务数量、错误的发生数量等等。举个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; /** * @author throwable * @version v1.0 * @description * @since 2018/7/19 23:10 */ public class CounterSample { public static void main(String[] args) throws Exception { //tag必须成对出现，也就是偶数个 Counter counter = Counter.builder(\u0026#34;counter\u0026#34;) .tag(\u0026#34;counter\u0026#34;, \u0026#34;counter\u0026#34;) .description(\u0026#34;counter\u0026#34;) .register(new SimpleMeterRegistry()); counter.increment(); counter.increment(2D); System.out.println(counter.count()); System.out.println(counter.measure()); //全局静态方法 Metrics.addRegistry(new SimpleMeterRegistry()); counter = Metrics.counter(\u0026#34;counter\u0026#34;, \u0026#34;counter\u0026#34;, \u0026#34;counter\u0026#34;); counter.increment(10086D); counter.increment(10087D); System.out.println(counter.count()); System.out.println(counter.measure()); } } 输出：\n1 2 3 4 3.0 [Measurement{statistic=\u0026#39;COUNT\u0026#39;, value=3.0}] 20173.0 [Measurement{statistic=\u0026#39;COUNT\u0026#39;, value=20173.0}] Counter的Measurement的statistic(可以理解为度量的统计角度)只有COUNT，也就是它只具备计数(它只有增量的方法，因此只增不减)，这一点从它的接口定义可知：\n1 2 3 4 5 6 7 8 9 10 11 12 public interface Counter extends Meter { default void increment() { increment(1.0); } void increment(double amount); double count(); //忽略其他方法或者成员 } Counter还有一个衍生类型FunctionCounter，它是基于函数式接口ToDoubleFunction进行计数统计的，用法差不多。\n1.2 Gauge Gauge(仪表)是一个表示单个数值的度量，它可以表示任意地上下移动的数值测量。Gauge通常用于变动的测量值，如当前的内存使用情况，同时也可以测量上下移动的\u0026quot;计数\u0026quot;，比如队列中的消息数量。举个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import java.util.concurrent.atomic.AtomicInteger; /** * @author throwable * @version v1.0 * @description * @since 2018/7/19 23:30 */ public class GaugeSample { public static void main(String[] args) throws Exception { AtomicInteger atomicInteger = new AtomicInteger(); Gauge gauge = Gauge.builder(\u0026#34;gauge\u0026#34;, atomicInteger, AtomicInteger::get) .tag(\u0026#34;gauge\u0026#34;, \u0026#34;gauge\u0026#34;) .description(\u0026#34;gauge\u0026#34;) .register(new SimpleMeterRegistry()); atomicInteger.addAndGet(5); System.out.println(gauge.value()); System.out.println(gauge.measure()); atomicInteger.decrementAndGet(); System.out.println(gauge.value()); System.out.println(gauge.measure()); //全局静态方法，返回值竟然是依赖值，有点奇怪，暂时不选用 Metrics.addRegistry(new SimpleMeterRegistry()); AtomicInteger other = Metrics.gauge(\u0026#34;gauge\u0026#34;, atomicInteger, AtomicInteger::get); } } 输出结果:\n1 2 3 4 5.0 [Measurement{statistic=\u0026#39;VALUE\u0026#39;, value=5.0}] 4.0 [Measurement{statistic=\u0026#39;VALUE\u0026#39;, value=4.0}] Gauge关注的度量统计角度是VALUE(值)，它的构建方法中依赖于函数式接口ToDoubleFunction的实例(如例子中的实例方法引用AtomicInteger::get)和一个依赖于ToDoubleFunction改变自身值的实例(如例子中的AtomicInteger实例)，它的接口方法如下：\n1 2 3 4 5 6 public interface Gauge extends Meter { double value(); //忽略其他方法或者成员 } 1.3 Timer Timer(计时器)同时测量一个特定的代码逻辑块的调用(执行)速度和它的时间分布。简单来说，就是在调用结束的时间点记录整个调用块执行的总时间，适用于测量短时间执行的事件的耗时分布，例如消息队列消息的消费速率。举个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import io.micrometer.core.instrument.Timer; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import java.util.concurrent.TimeUnit; /** * @author throwable * @version v1.0 * @description * @since 2018/7/19 23:44 */ public class TimerSample { public static void main(String[] args) throws Exception{ Timer timer = Timer.builder(\u0026#34;timer\u0026#34;) .tag(\u0026#34;timer\u0026#34;,\u0026#34;timer\u0026#34;) .description(\u0026#34;timer\u0026#34;) .register(new SimpleMeterRegistry()); timer.record(()-\u0026gt;{ try { TimeUnit.SECONDS.sleep(2); }catch (InterruptedException e){ //ignore } }); System.out.println(timer.count()); System.out.println(timer.measure()); System.out.println(timer.totalTime(TimeUnit.SECONDS)); System.out.println(timer.mean(TimeUnit.SECONDS)); System.out.println(timer.max(TimeUnit.SECONDS)); } } 输出结果：\n1 2 3 4 5 1 [Measurement{statistic=\u0026#39;COUNT\u0026#39;, value=1.0}, Measurement{statistic=\u0026#39;TOTAL_TIME\u0026#39;, value=2.000603975}, Measurement{statistic=\u0026#39;MAX\u0026#39;, value=2.000603975}] 2.000603975 2.000603975 2.000603975 Timer的度量统计角度主要包括记录执行的最大时间、总时间、平均时间、执行完成的总任务数，它提供多种的统计方法变体：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public interface Timer extends Meter, HistogramSupport { void record(long amount, TimeUnit unit); default void record(Duration duration) { record(duration.toNanos(), TimeUnit.NANOSECONDS); } \u0026lt;T\u0026gt; T record(Supplier\u0026lt;T\u0026gt; f); \u0026lt;T\u0026gt; T recordCallable(Callable\u0026lt;T\u0026gt; f) throws Exception; void record(Runnable f); default Runnable wrap(Runnable f) { return () -\u0026gt; record(f); } default \u0026lt;T\u0026gt; Callable\u0026lt;T\u0026gt; wrap(Callable\u0026lt;T\u0026gt; f) { return () -\u0026gt; recordCallable(f); } //忽略其他方法或者成员 } 这些record或者包装方法可以根据需要选择合适的使用，另外，一些度量属性(如下限和上限)或者单位可以自行配置，具体属性的相关内容可以查看DistributionStatisticConfig类，这里不详细展开。\n另外，Timer有一个衍生类LongTaskTimer，主要是用来记录正在执行但是尚未完成的任务数，用法差不多。\n1.4 Summary Summary(摘要)用于跟踪事件的分布。它类似于一个计时器，但更一般的情况是，它的大小并不一定是一段时间的测量值。在micrometer中，对应的类是DistributionSummary，它的用法有点像Timer，但是记录的值是需要直接指定，而不是通过测量一个任务的执行时间。举个例子：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import io.micrometer.core.instrument.DistributionSummary; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; /** * @author throwable * @version v1.0 * @description * @since 2018/7/19 23:55 */ public class SummarySample { public static void main(String[] args) throws Exception { DistributionSummary summary = DistributionSummary.builder(\u0026#34;summary\u0026#34;) .tag(\u0026#34;summary\u0026#34;, \u0026#34;summary\u0026#34;) .description(\u0026#34;summary\u0026#34;) .register(new SimpleMeterRegistry()); summary.record(2D); summary.record(3D); summary.record(4D); System.out.println(summary.measure()); System.out.println(summary.count()); System.out.println(summary.max()); System.out.println(summary.mean()); System.out.println(summary.totalAmount()); } } 输出结果：\n1 2 3 4 5 [Measurement{statistic=\u0026#39;COUNT\u0026#39;, value=3.0}, Measurement{statistic=\u0026#39;TOTAL\u0026#39;, value=9.0}, Measurement{statistic=\u0026#39;MAX\u0026#39;, value=4.0}] 3 4.0 3.0 9.0 Summary的度量统计角度主要包括记录过的数据中的最大值、总数值、平均值和总次数。另外，一些度量属性(如下限和上限)或者单位可以自行配置，具体属性的相关内容可以查看DistributionStatisticConfig类。\n二、扩展 SpringCloud体系的监控，扩展一个功能，记录一下每个有效的请求的执行时间。添加下面几个类或者方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 //注解 @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MethodMetric { String name() default \u0026#34;\u0026#34;; String description() default \u0026#34;\u0026#34;; String[] tags() default {}; } //切面类 @Aspect @Component public class HttpMethodCostAspect { @Autowired private MeterRegistry meterRegistry; @Pointcut(\u0026#34;@annotation(club.throwable.sample.aspect.MethodMetric)\u0026#34;) public void pointcut() { } @Around(value = \u0026#34;pointcut()\u0026#34;) public Object process(ProceedingJoinPoint joinPoint) throws Throwable { Method targetMethod = ((MethodSignature) joinPoint.getSignature()).getMethod(); //这里是为了拿到实现类的注解 Method currentMethod = ClassUtils.getUserClass(joinPoint.getTarget().getClass()) .getDeclaredMethod(targetMethod.getName(), targetMethod.getParameterTypes()); if (currentMethod.isAnnotationPresent(MethodMetric.class)) { MethodMetric methodMetric = currentMethod.getAnnotation(MethodMetric.class); return processMetric(joinPoint, currentMethod, methodMetric); } else { return joinPoint.proceed(); } } private Object processMetric(ProceedingJoinPoint joinPoint, Method currentMethod, MethodMetric methodMetric) throws Throwable { String name = methodMetric.name(); if (!StringUtils.hasText(name)) { name = currentMethod.getName(); } String desc = methodMetric.description(); if (!StringUtils.hasText(desc)) { desc = name; } String[] tags = methodMetric.tags(); if (tags.length == 0) { tags = new String[2]; tags[0] = name; tags[1] = name; } Timer timer = Timer.builder(name).tags(tags) .description(desc) .register(meterRegistry); return timer.record(() -\u0026gt; { try { return joinPoint.proceed(); } catch (Throwable throwable) { throw new IllegalStateException(throwable); } }); } } //启动类里面添加方法 @SpringBootApplication @EnableEurekaClient @RestController public class SampleApplication { public static void main(String[] args) { SpringApplication.run(SampleApplication.class, args); } @MethodMetric @GetMapping(value = \u0026#34;/hello\u0026#34;) public String hello(@RequestParam(name = \u0026#34;name\u0026#34;, required = false, defaultValue = \u0026#34;doge\u0026#34;) String name) { return String.format(\u0026#34;%s say hello!\u0026#34;, name); } } 配置好Grafana的面板，重启项目，多次调用/hello接口。\n","permalink":"https://ktzxy.top/posts/akg0bgab7j/","summary":"Prometheus自定义监控内容","title":"Prometheus自定义监控内容"},{"content":"﻿# Day-0-IDEA简单学习\n1.设置主题 2.编辑区的字体变大或者变小：（ctrl+鼠标滚轮） 3.鼠标悬浮在代码上有提示： 4.自动导包和优化多余的包： 手动导包：快捷键：alt+enter 自动导包和优化多余的包：\n5.显示行号 ， 方法和方法间的分隔符： 6.忽略大小写，进行提示： 7.修改代码中注释的字体颜色： 8.修改类头的文档注释信息：注意：对新建的类才有效 /**\n@Auther: XXX @Date: ${DATE} - ${MONTH} - ${DAY} - ${TIME} @Description: ${PACKAGE_NAME} @version: 1.0 */ 8.自动编译： 9.常用快捷键 【1】创建内容：alt+insert\n【2】main方法：psvm\n【3】输出语句：sout\n【4】复制行：ctrl+d\n【5】删除行：ctrl+y\n【6】代码向上/下移动：Ctrl + Shift + Up / Down\n【7】搜索类： ctrl+n\n【8】生成代码 ：alt + Insert（如构造函数等，getter,setter,hashCode,equals,toString）\n【9】百能快捷键 : alt + Enter （导包，生成变量等）\n【10】单行注释或多行注释 ： Ctrl + / 或 Ctrl + Shift + /\n【11】重命名 shift+f6\n【12】for循环 直接 ：fori 回车即可\n【13】代码块包围：try-catch,if,while等 ctrl+alt+t\n【14】显示代码结构 : alt + 7\n【15】显示导航栏： alt +1\n【16】撤回：ctrl+z\n【17】缩进：tab 取消缩进： shift+tab\n10.常用的代码模板 【1】模板1： main方法：\n​\tmain 或者 psvm\n【2】模板2：输出语句：\n​\tsout 或者 .sout\n一些变型：\n​\tsoutp:打印方法的形参 ​\tsoutm:打印方法的名字 ​\tsoutv:打印变量\n【3】模板3： 循环\n​\t普通for循环： fori（正向） 或者 .fori （正向） . forr(逆向) ​\t增强for循环： iter 或者 .for ​\t（可以用于数组的遍历，集合的遍历）\n【4】模板4： 条件判断\n​\tifn 或者 .null ：判断是否为null （if null） ​\tinn 或者 .nn ：判断不等于null (if not null)\n【5】模板5： 属性修饰符：\n​\tprsf : private static final ​\tpsf :public static final\n11.常用断点调试快捷键 调试在开发中大量应用： 【1】Debug的优化设置：更加节省内存空间： 设置Debug连接方式，默认是Socket。 Shared memory是Windows 特有的一个属性，一般在Windows系统下建议使用此设置， 内存占用相对较少。\n【2】常用断点调试快捷键：\n一步一步的向下运行代码，不会走入任何方法中。 一步一步的向下运行代码，不会走入系统类库的方法中，但是会走入自定义的方法中。 一步一步的向下运行代码，会走入系统类库的方法中，也会走入自定义的方法中。 跳出方法 结束程序 进入到下一个断点，如果没有下一个断点了，就直接运行到程序结束。\n在当前次取消未执行的断点。\n","permalink":"https://ktzxy.top/posts/xsjar30pqe/","summary":"IDEA简单学习","title":"IDEA简单学习"},{"content":"[TOC]\n1、优化思路 设计数据库时：数据库表、字段的设计，存储引擎。 利用好MySQL自身提供的功能，如索引等。 横向扩展：MySQL集群、负载均衡、读写分离。 SQL语句的优化（收效较低） 2、字段设计 2.1 原则：定长和非定长数据类型的选择 decimal不会损失精度，存储空间会随数据的增大而增大。double占用固定空间，较大数的存储会损失精度。非定长的还有varchar、text。\n金额 对数据精度要求较高，小数的运算和存储存在精度问题（不能将所有小数转换成二进制）。 定点数decimal price decimal(8,3)有2位小数的定点数，定点数支持很大的数（甚至是超过int，bigint存储范围的数） 字符串存储 定长char，非定长varchar、text（上限65535,其中varchar还会消耗1-3字节记录长度，而text使用额外空间记录长度） 小单位大数额避免出现小数。 2.2 原则：尽可能使用小的数据类型和指定短的长度 尽可能使用not null 非null字段的处理要比null字段处理的高效。切不需要判断是否为null。 null在mysql中，存储需要额外空间，运算也需要特殊的运算符。如select null = null和select null \u0026lt;\u0026gt; null，有相同结果，只能通过is null 和 is not null来判断字段是否为null。 mysql中每条记录都需要额外的存储空间，表示每个字段是否为null。因此通常使用特殊的数据进行占位，比如int not null default 0，string not null default \u0026lsquo;\u0026rsquo;。 2.3 单表字段不宜过多 \u0026lt;30为合格， \u0026lt;20为佳 3、关联表的设计 外键foreign key只能实现一对一或一对多的映射。\n一对多：使用外键 多对多：单独新建一张表将多对多拆分成两个一对多。 一对一：使用相同的主键或者增加一个外键字段。 ","permalink":"https://ktzxy.top/posts/58dzvhw0tu/","summary":"MySql优化","title":"MySql优化"},{"content":"1. RabbitMQ 简介 RabbitMQ 官方地址：http://www.rabbitmq.com/\nMQ 全称为 Message Queue，即消息队列， RabbitMQ 是由 erlang 语言开发，基于 AMQP（Advanced Message Queue 高级消息队列协议）协议实现的消息队列，它是一种应用程序之间的通信方法，实现服务之间的高度解耦。消息队列在分布式系统开发中应用非常广泛，市场上还有其他的消息队列框架，如：ActiveMQ，ZeroMQ，Kafka，MetaMQ，RocketMQ、Redis。\n1.1. RabbitMQ 的特点 遵循 AMQP 标准协议开发的 MQ 服务 使得简单，功能强大。在分布式系统下具备异步，削峰，负载均衡等一系列高级功能。 社区活跃，文档完善。 高并发性能好，这主要得益于 Erlang 语言。 Spring Boot 默认已集成 RabbitMQ。 拥有持久化的机制，进程消息，队列中的信息也可以保存下来。 实现消费者和生产者之间的解耦。 对于高并发场景下，利用消息队列可以使得同步访问变为串行访问达到一定量的限流，利于数据库的操作。 1.2. RabbitMQ 的基本结构 组成部分说明如下\nMessage：由消息头和消息体组成。消息体是不透明的，而消息头则由一系列的可选属性组成，这些属性包括 routing-key、priority、delivery-mode（是否持久性存储）等。 Broker：消息队列服务进程，此进程包括两个部分：Exchange 和 Queue。 Exchange：消息队列交换机，接收消息并按一定的规则将消息路由转发到一个或多个队列(Queue)，对消息进行过滤。default exchange 是默认的直连交换机，名字为空字符串，每个新建队列都会自动绑定到默认交换机上，绑定的路由键名称与队列名称相同。 Queue：存储消息的队列，消息到达队列并转发给指定的消费方。队列的特性是先进先出。一个消息可分发到一个或多个队列。 Binding：将 Exchange 和 Queue 进行关联，让 Exchange 就知道将消息路由到哪个 Queue 中。 Virtual host：每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器，拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础，必须在连接时指定，RabbitMQ 默认的 vhost 是 /。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时，可以划分出多个 vhost，每个用户在自己的 vhost 创建 exchange 和 queue。 Producer：消息生产者，即生产方客户端，生产方客户端将消息发送到 MQ。 Consumer：消息消费者，即消费方客户端，接收 MQ 转发的消息。 Channel：消息通道，在客户端的每个连接里，可建立多个 channel，每个 channel 代表一个会话任务由 Exchange、Queue、RoutingKey 三个要素才能决定一个从 Exchange 到 Queue 的唯一的线路。 1.3. 消息发布与接收流程 发送消息\n生产者和 Broker 建立 TCP 连接 生产者和 Broker 建立通道 生产者通过通道消息发送给 Broker，由 Exchange 将消息进行转发 Exchange 将消息转发到指定的 Queue（队列） 接收消息\n消费者和 Broker 建立 TCP 连接 消费者和 Broker 建立通道 消费者监听指定的 Queue（队列） 当有消息到达 Queue 时 Broker 默认将消息推送给消费者 消费者接收到消息 2. RabbitMQ 快速入门 2.1. window 版安装 2.1.1. 说明 RabbitMQ 由 Erlang 语言开发，Erlang 语言用于并发及分布式系统的开发，在电信领域应用广泛，OTP（Open Telecom Platform）作为 Erlang 语言的一部分，包含了很多基于 Erlang 开发的中间件及工具库，安装 RabbitMQ 需要安装 Erlang/OTP，并保持版本匹配，如下图：\n本次测试使用 Erlang/OTP 22.0 版本和 RabbitMQ 3.7.15 版本。\n2.1.2. Erlang 下载与安装 erlang 下载地址：http://www.erlang.org/downloads\n下载安装包后，以管理员方式运行安装。安装的过程中可能会出现依赖 Windows 组件的提示，根据提示下载安装即可，都是自动执行的，如下：\nerlang 安装完成后，需要配置 erlang 的环境变量，否则 RabbitMQ 将无法找到安装的 Erlang\n1 ERLANG_HOME=D:\\development\\erl10.4 在 path 变量中添加\n1 %ERLANG_HOME%\\bin; 2.1.3. RabbitMQ 下载与安装 RabbitMQ 的下载地址：http://www.rabbitmq.com/download.html\n安装包下载完成后，以管理员方式运行 RabbitMQ 安装文件。\n官方安装说明文档：https://www.rabbitmq.com/install-windows.html\n2.1.4. 启动 安装成功后会自动创建 RabbitMQ 服务并且启动，默认对外服务端口是 5672\n从开始菜单启动 RabbitMQ 如果没有开始菜单则进入安装目录下 sbin 目录手动启动 安装并运行服务 1 2 3 rabbitmq-service.bat install # 安装服务 rabbitmq-service.bat stop # 停止服务 rabbitmq-service.bat start # 启动服务 2.1.5. 安装管理插件 RabbitMQ 也提供有 web 控制台服务，但是此功能是一个插件，需要先启用才可以使用。安装 rabbitMQ 的管理插件，方便在浏览器端管理 RabbitMQ。以管理员身份运行 cmd 命令行，执行以下命令：\n1 2 ./rabbitmq-plugins.bat list # 查看当前所有插件的运行状态 ./rabbitmq-plugins.bat enable rabbitmq_management # 启动rabbitmq_management插件 安装管理插件启动成功后，打开浏览器访问：http://localhost:15672\n初始化用户名和密码均为：guest，成功登录后进入管理后台界面，如下：\n2.1.6. 注意事项 安装 erlang 和 rabbitMQ 都以管理员身份运行。 当卸载重新安装时会出现 RabbitMQ 服务注册失败，此时需要进入注册表清理 erlang，搜索 RabbitMQ、ErlSrv，将对应的项全部删除。 2.2. Linux 版安装 2.2.1. 使用 Docker 安装部署 RabbitMQ docker search rabbitmq:management：查询RabbitMQ的镜像 docker pull rabbitmq:management：拉取RabbitMQ镜像，注意：如果docker pull rabbitmq 后面不带management，启动rabbitmq后是无法打开管理界面的，所以我们要下载带management插件的rabbitmq. 创建rabbitmq容器 1 2 # 创建rabbitmq容器 docker run -id --name moon_rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p 25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management 映射的端口说明：4369 (epmd)；25672 (Erlang distribution)；5672, 5671 (AMQP 0-9-1 without and with TLS)应用访问端口号；15671，15672 (if management plugin is enabled)控制台端口号；61613, 61614 (if STOMP is enabled)；1883, 8883 (if MQTT is enabled)；\n开放端口防火墙 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 对外开放8080端口 firewall-cmd --zone=public --add-port=8080/tcp --permanent # 注：–zone：作用域 # –add-port=8080/tcp：添加端口，格式为：端口/通讯协议 # –permanent：永久生效，没有此参数重启后失效 # 重启防火墙 firewall-cmd --reload # 查看已经开放的端口 firewall-cmd --list-ports # 停止防火墙 systemctl stop firewalld.service # 启动防火墙 systemctl start firewalld.service # 禁止防火墙开机启动 systemctl disable firewalld.service 2.2.2. 传统方式安装部署 RabbitMQ（待整理） TODO: 待整理\n2.3. 测试使用 按照官方教程文档，测试 hello world\n2.3.1. 搭建环境 2.3.1.1. Java client 生产者和消费者都属于客户端，rabbitMQ 的 java 客户端参考：https://github.com/rabbitmq/rabbitmq-java-client/\n先用 rabbitMQ 官方提供的 java client 测试，目的是对 RabbitMQ 的交互过程有个清晰的认识。\n2.3.1.2. 创建maven工程 创建生产者工程和消费者工程，分别加入 RabbitMQ java client 的依赖。\ntest-rabbitmq-producer：生产者工程 test-rabbitmq-consumer：消费者工程 1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.rabbitmq\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;amqp-client\u0026lt;/artifactId\u0026gt; \u0026lt;!-- 此版本与spring boot 1.5.9版本匹配 --\u0026gt; \u0026lt;version\u0026gt;4.0.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-logging\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 2.3.2. 生产者 在生产者工程下的 test 中创建测试类如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 /** * RabbitMQ的入门程序 */ public class Producer01 { /* 定义队列的名称 */ private static final String QUEUE = \u0026#34;helloworld\u0026#34;; public static void main(String[] args) { Connection connection = null; Channel channel = null; try { // 通过连接工厂创建新的连接和mq建立连接 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost(\u0026#34;192.168.12.132\u0026#34;); // 设置RabbitMQ服务主机地址 connectionFactory.setPort(5672); // 设置端口号 connectionFactory.setUsername(\u0026#34;guest\u0026#34;); //设置用户名与密码 connectionFactory.setPassword(\u0026#34;guest\u0026#34;); /* 设置虚拟机。rabbitmq默认虚拟机名称为“/”，一个mq服务可以设置多个虚拟机，每个虚拟机就相当于一个独立的mq */ connectionFactory.setVirtualHost(\u0026#34;/\u0026#34;); // 创建与RabbitMQ服务的TCP连接 connection = connectionFactory.newConnection(); // 创建与Exchange的会话通道，生产者和mq服务所有通信都在channel通道中完成，每个连接可以创建多个通道，每个通道代表一个会话任务 channel = connection.createChannel(); /* * 声明队列，如果队列在RabbitMQ中没有则将自动创建 * Queue.DeclareOk queueDeclare(String queue, boolean durable, * boolean exclusive, boolean autoDelete, * Map\u0026lt;String, Object\u0026gt; arguments) throws IOException; * 参数明细 * 1、queue 队列名称 * 2、durable 是否持久化，如果持久化，mq重启后队列还在 * 3、exclusive 是否独占连接，队列只允许在该连接中访问，如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建 * 4、autoDelete 自动删除，队列不再使用时是否自动删除此队列，如果将此参数和exclusive参数设置为true就可以实现临时队列（队列不用了就自动删除） * 5、arguments 参数，可以设置一个队列的扩展参数，比如：可设置存活时间 */ channel.queueDeclare(QUEUE, true, false, false, null); /* * 消息发布的方法 * void basicPublish(String exchange, String routingKey, boolean mandatory, BasicProperties props, byte[] body) throws IOException; * 参数明细 * 1、exchange，交换机名称，如果不指定将使用mq的默认交换机Default Exchange（设置为\u0026#34;\u0026#34;） * 2、routingKey，消息路由key，交换机根据路由key来将消息转发到指定的队列，如果使用默认交换机，routingKey设置为队列的名称 * 3、props，消息包含的属性 * 4、body，消息内容，字节数组 * 注：这里没有指定交换机，消息将发送给默认交换机，每个队列也会绑定那个默认的交换机，但是不能显示绑定或解除绑定 * 默认的交换机，routingKey等于队列名称 */ String message = \u0026#34;Hello! MooNkirA!\u0026#34;; channel.basicPublish(\u0026#34;\u0026#34;, QUEUE, null, message.getBytes()); System.out.println(\u0026#34;send to mq \u0026#34; + message); } catch (Exception ex) { ex.printStackTrace(); } finally { // 关闭资源，先关闭通道，再关闭连接 if (channel != null) { try { channel.close(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } if (connection != null) { try { connection.close(); } catch (IOException e) { e.printStackTrace(); } } } } } 2.3.3. 消费者 在消费者工程下的 test 中创建测试类如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 /** * 入门程序消费者 */ public class Consumer01 { /* 定义队列的名称 */ private static final String QUEUE = \u0026#34;helloworld\u0026#34;; public static void main(String[] args) throws IOException, TimeoutException { // 通过连接工厂创建新的连接和mq建立连接 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost(\u0026#34;192.168.12.132\u0026#34;); // 设置RabbitMQ服务主机地址 connectionFactory.setPort(5672); // 设置端口号 connectionFactory.setUsername(\u0026#34;guest\u0026#34;); //设置用户名与密码 connectionFactory.setPassword(\u0026#34;guest\u0026#34;); /* 设置虚拟机。rabbitmq默认虚拟机名称为“/”，一个mq服务可以设置多个虚拟机，每个虚拟机就相当于一个独立的mq */ connectionFactory.setVirtualHost(\u0026#34;/\u0026#34;); // 创建与RabbitMQ服务的TCP连接 Connection connection = connectionFactory.newConnection(); // 创建与Exchange的会话通道，生产者和mq服务所有通信都在channel通道中完成，每个连接可以创建多个通道，每个通道代表一个会话任务 Channel channel = connection.createChannel(); /* * 监听队列，声明队列，如果队列在RabbitMQ中没有则将自动创建 * Queue.DeclareOk queueDeclare(String queue, boolean durable, * boolean exclusive, boolean autoDelete, * Map\u0026lt;String, Object\u0026gt; arguments) throws IOException; * 参数明细 * 1、queue 队列名称 * 2、durable 是否持久化，如果持久化，mq重启后队列还在 * 3、exclusive 是否独占连接，队列只允许在该连接中访问，如果connection连接关闭队列则自动删除,如果将此参数设置true可用于临时队列的创建 * 4、autoDelete 自动删除，队列不再使用时是否自动删除此队列，如果将此参数和exclusive参数设置为true就可以实现临时队列（队列不用了就自动删除） * 5、arguments 参数，可以设置一个队列的扩展参数，比如：可设置存活时间 */ channel.queueDeclare(QUEUE, true, false, false, null); /* 实现消费方法（重写） */ DefaultConsumer consumer = new DefaultConsumer(channel) { /** * 当消费者接收到消息后，此方法将被调用 * * @param consumerTag 消费者标签，用来标识消费者的，在监听队列时设置channel.basicConsume * @param envelope 信封，消息包的内容，可从中获取消息id，消息routingkey，交换机，消息和重传标志(收到消息失败后是否需要重新发送) * @param properties 消息属性 * @param body 消息内容 */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { // 获取交换机 String exchange = envelope.getExchange(); // 路由key String routingKey = envelope.getRoutingKey(); // 消息id，mq在channel中用来标识消息的id，可用于确认消息已接收 long deliveryTag = envelope.getDeliveryTag(); System.out.println(\u0026#34;交换机exchange：\u0026#34; + exchange + \u0026#34;；路由routingKey：\u0026#34; + routingKey + \u0026#34;；消息id：\u0026#34; + deliveryTag); // 消息内容 String message = new String(body, StandardCharsets.UTF_8); System.out.println(\u0026#34;receive message:\u0026#34; + message); } }; /* * 监听队列 * String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException; * 参数明细 * 1、queue 队列名称 * 2、autoAck 是否自动回复，当消费者接收到消息后要告诉mq消息已接收，如果将此参数设置为tru表示会自动回复mq，如果设置为false要通过编程实现回复 * 3、callback，消费方法，当消费者接收到消息要执行的方法 */ channel.basicConsume(QUEUE, true, consumer); } } 2.3.4. 总结 发送端操作流程\n创建连接 创建通道 声明队列 发送消息 接收端操作流程\n创建连接 创建通道 声明队列 监听队列 接收消息 ack回复 3. 工作模式 RabbitMQ 有以下几种工作模式：\nWork queues Publish/Subscribe Routing Topics Header RPC 3.1. Exchange 交换机的类型 根据 RabbitMQ 不同类型的工作模式会选择不同类型的 Exchange，在分发消息时会选择不同的分发策略，目前共四种类型：direct、fanout、topic、headers。\n其中 headers 模式根据消息的 headers 进行路由，此外 headers 交换器和 direct 交换器完全一致，但性能差很多。\n类型名称 类型描述 相应的工作模式 fanout 把所有发送到该Exchange的消息路由到所有与它绑定的Queue中 publish/subscribe direct 发送到Routing Key与Binding Key完全匹配的的Queue中 Routing topic 模糊匹配 Topics headers Exchange不依赖于routing key与binding key的匹配规则来路由消息，而是根据发送的消息内容中的header属性进行匹配 headers 3.1.1. fanout 所有发到 fanout 类型交换机的消息都会路由到所有与该交换机绑定的队列。fanout 类型转发消息是最快的。\n3.1.2. direct direct 交换机会将消息路由到 binding key 和 routing key 完全匹配的队列中。它是完全匹配、单播的模式。\n3.1.3. topic topic 交换机使用 routing key 和 binding key 进行模糊匹配，匹配成功则将消息发送到相应的队列。routing key 和 binding key 都是句号 . 作为分隔的字符串，binding key 中可以存在两种特殊字符 * 与 #，用于做模糊匹配，其中 * 用于匹配一个单词，# 用于匹配多个单词。\n3.1.4. headers headers 交换机是根据发送的消息内容中的 headers 属性进行路由的。在绑定 Queue 与 Exchange 时指定一组键值对；当消息发送到 Exchange 时，RabbitMQ 会取到该消息的 headers（也是一个键值对的形式），对比其中的键值对是否完全匹配 Queue 与 Exchange 绑定时指定的键值对；如果完全匹配则消息会路由到该 Queue，否则不会路由到该 Queue。\n3.2. Work queues 工作队列(多个消费者监听同一个队列) Work queues 与入门程序相比，只是多了一个消费端，两个消费端共同消费同一个队列中的消息。\nWork queues 工作队列应用场景：对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。\n测试流程：\n使用入门程序，启动多个消费者 生产者发送多个消息 测试结果：\n一条消息只会被一个消费者接收； rabbitmq 采用轮询的方式将消息是平均发送给消费者的； 消费者在处理完某条消息后，才会收到下一条消息 3.3. Publish/Subscribe 发布订阅工作模式 发布订阅模式：\n每个消费者监听自己的队列。 生产者将消息发给 broker，由交换机将消息转发到绑定此交换机的每个队列，每个绑定交换机的队列都将接收到消息 3.3.1. 案例代码 用户通知，当用户充值成功或转账完成系统通知用户，通知方式有短信、邮件多种方法 。\n3.3.1.1. 生产者 声明 Exchange_fanout_inform 交换机。 声明两个队列并且绑定到此交换机，绑定时不需要指定 routingkey 发送消息时不需要指定 routingkey 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 /** * RabbitMQ - Publish/subscribe 工作模式 */ public class Producer02_publish { /* 定义队列的名称 */ private static final String QUEUE_INFORM_EMAIL = \u0026#34;queue_inform_email\u0026#34;; private static final String QUEUE_INFORM_SMS = \u0026#34;queue_inform_sms\u0026#34;; /* 定义交换机名称 */ private static final String EXCHANGE_FANOUT_INFORM = \u0026#34;exchange_fanout_inform\u0026#34;; public static void main(String[] args) { Connection connection = null; Channel channel = null; try { // 通过连接工厂创建新的连接和mq建立连接 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost(\u0026#34;192.168.12.132\u0026#34;); connectionFactory.setPort(5672); connectionFactory.setUsername(\u0026#34;guest\u0026#34;); connectionFactory.setPassword(\u0026#34;guest\u0026#34;); connectionFactory.setVirtualHost(\u0026#34;/\u0026#34;); // 创建与RabbitMQ服务的TCP连接 connection = connectionFactory.newConnection(); // 创建与Exchange的会话通道 channel = connection.createChannel(); // 声明两个队列 channel.queueDeclare(QUEUE_INFORM_EMAIL, true, false, false, null); channel.queueDeclare(QUEUE_INFORM_SMS, true, false, false, null); /* * 声明一个交换机 * Exchange.DeclareOk exchangeDeclare(String exchange, BuiltinExchangeType type) throws IOException; * 参数明细 * 1、交换机的名称 * 2、交换机的类型 * fanout：对应的rabbitmq的工作模式是 publish/subscribe * direct：对应的Routing工作模式 * topic：对应的Topics工作模式 * headers：对应的headers工作模式 */ channel.exchangeDeclare(EXCHANGE_FANOUT_INFORM, BuiltinExchangeType.FANOUT); /* * 交换机和队列绑定 * Queue.BindOk queueBind(String queue, String exchange, String routingKey) throws IOException; * 参数明细 * 1、queue 队列名称 * 2、exchange 交换机名称 * 3、routingKey 路由key，作用是交换机根据路由key的值将消息转发到指定的队列中，在发布订阅模式中调协为空字符串 */ channel.queueBind(QUEUE_INFORM_EMAIL, EXCHANGE_FANOUT_INFORM, \u0026#34;\u0026#34;); channel.queueBind(QUEUE_INFORM_SMS, EXCHANGE_FANOUT_INFORM, \u0026#34;\u0026#34;); // 发送消息 for (int i = 0; i \u0026lt; 5; i++) { String message = \u0026#34;send inform message to user! NO.\u0026#34; + i; channel.basicPublish(EXCHANGE_FANOUT_INFORM, \u0026#34;\u0026#34;, null, message.getBytes()); System.out.println(\u0026#34;send to mq \u0026#34; + message); } } catch (Exception ex) { ex.printStackTrace(); } finally { // 关闭资源，先关闭通道，再关闭连接 if (channel != null) { try { channel.close(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } if (connection != null) { try { connection.close(); } catch (IOException e) { e.printStackTrace(); } } } } } 3.3.1.2. 邮件发送消费者 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 /** * RabbitMQ消费者（消费邮件队列） - Publish/subscribe 工作模式 */ public class Consumer02_subscribe_email { /* 定义队列的名称 */ private static final String QUEUE_INFORM_EMAIL = \u0026#34;queue_inform_email\u0026#34;; /* 定义交换机的名称 */ private static final String EXCHANGE_FANOUT_INFORM = \u0026#34;exchange_fanout_inform\u0026#34;; public static void main(String[] args) throws IOException, TimeoutException { // 通过连接工厂创建新的连接和mq建立连接 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost(\u0026#34;192.168.12.132\u0026#34;); connectionFactory.setPort(5672); connectionFactory.setUsername(\u0026#34;guest\u0026#34;); connectionFactory.setPassword(\u0026#34;guest\u0026#34;); connectionFactory.setVirtualHost(\u0026#34;/\u0026#34;); // 创建与RabbitMQ服务的TCP连接 Connection connection = connectionFactory.newConnection(); // 创建与Exchange的会话通道 Channel channel = connection.createChannel(); // 声明监听的队列，如果队列在RabbitMQ中没有则将自动创建 channel.queueDeclare(QUEUE_INFORM_EMAIL, true, false, false, null); // 声明一个交换机 channel.exchangeDeclare(EXCHANGE_FANOUT_INFORM, BuiltinExchangeType.FANOUT); // 进行交换机和队列绑定 channel.queueBind(QUEUE_INFORM_EMAIL, EXCHANGE_FANOUT_INFORM, \u0026#34;\u0026#34;); /* 实现消费方法（重写） */ DefaultConsumer consumer = new DefaultConsumer(channel) { /** * 当消费者接收到消息后，此方法将被调用 * * @param consumerTag 消费者标签，用来标识消费者的，在监听队列时设置channel.basicConsume * @param envelope 信封，消息包的内容，可从中获取消息id，消息routingkey，交换机，消息和重传标志(收到消息失败后是否需要重新发送) * @param properties 消息属性 * @param body 消息内容 */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { // 获取交换机 String exchange = envelope.getExchange(); // 路由key String routingKey = envelope.getRoutingKey(); // 消息id，mq在channel中用来标识消息的id，可用于确认消息已接收 long deliveryTag = envelope.getDeliveryTag(); System.out.println(\u0026#34;交换机exchange：\u0026#34; + exchange + \u0026#34;；路由routingKey：\u0026#34; + routingKey + \u0026#34;；消息id：\u0026#34; + deliveryTag); // 消息内容 String message = new String(body, StandardCharsets.UTF_8); System.out.println(\u0026#34;receive message:\u0026#34; + message); } }; // 监听队列 channel.basicConsume(QUEUE_INFORM_EMAIL, true, consumer); } } 3.3.1.3. 短信发送消费者 参考上边的邮件发送消费者代码编写。只需要将邮件队列的名称换成短信队列即可\n1 2 3 4 5 6 7 8 9 10 11 /* 定义队列的名称 */ private static final String QUEUE_INFORM_EMAIL = \u0026#34;queue_inform_email\u0026#34;; ... // 声明监听的队列，如果队列在RabbitMQ中没有则将自动创建 channel.queueDeclare(QUEUE_INFORM_EMAIL, true, false, false, null); ... // 进行交换机和队列绑定 channel.queueBind(QUEUE_INFORM_SMS, EXCHANGE_FANOUT_INFORM, \u0026#34;\u0026#34;); ... // 监听队列 channel.basicConsume(QUEUE_INFORM_SMS, true, consumer); 3.3.2. 测试 打开RabbitMQ的管理界面，观察交换机绑定情况：\n使用生产者发送若干条消息，每条消息都转发到各个队列，每个消费者都接收到了消息\n3.3.3. 总结 publish/subscribe与work queues有什么区别 区别 work queues不用定义交换机，而publish/subscribe需要定义交换机 publish/subscribe的生产方是面向交换机发送消息，work queues的生产方是面向队列发送消息(底层使用默认交换机) publish/subscribe需要设置队列和交换机的绑定，work queues不需要设置，实质上work queues会将队列绑定到默认的交换机 相同点 两者实现的发布/订阅的效果是一样的，多个消费端监听同一个队列不会重复消费消息 实质工作用什么 publish/subscribe还是work queues 建议使用 publish/subscribe，发布订阅模式比工作队列模式更强大，并且发布订阅模式可以指定自己专用的交换机 3.4. Routing 路由工作模式 路由模式：\n每个消费者监听自己的队列，并且设置 routingkey 生产者将消息发给交换机，由交换机根据 routingkey 来转发消息到指定的队列 3.4.1. RabbitMQ 消息实现路由的原理 消息路由必须有三部分：交换器、路由、绑定。生产者把消息发布到交换器上；绑定决定了消息如何从路由器路由到特定的队列；消息最终到达队列，并被消费者接收。\n消息发布到交换器时，消息将拥有一个路由键（routing key），在消息创建时设定。 通过队列路由键，可以把队列绑定到交换器上。 消息到达交换器后，RabbitMQ 会将消息的路由键与队列的路由键进行匹配（针对不同的交换器有不同的路由规则）。如果能够匹配到队列，则消息会投递到相应队列中。 3.4.2. 案例代码 3.4.2.1. 生产者 声明 exchange_routing_inform 交换机。 声明两个队列并且绑定到此交换机，绑定时需要指定 routingkey 发送消息时需要指定 routingkey 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 /** * RabbitMQ生产者 - routing 工作模式 */ public class Producer03_routing { /* 定义队列的名称 */ private static final String QUEUE_INFORM_EMAIL = \u0026#34;queue_inform_email\u0026#34;; private static final String QUEUE_INFORM_SMS = \u0026#34;queue_inform_sms\u0026#34;; /* 定义交换机名称 */ private static final String EXCHANGE_ROUTING_INFORM = \u0026#34;exchange_routing_inform\u0026#34;; /* 定义路由名称 */ private static final String ROUTINGKEY_EMAIL = \u0026#34;inform_email\u0026#34;; private static final String ROUTINGKEY_SMS = \u0026#34;inform_sms\u0026#34;; public static void main(String[] args) { Connection connection = null; Channel channel = null; try { // 通过连接工厂创建新的连接和mq建立连接 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost(\u0026#34;192.168.12.132\u0026#34;); connectionFactory.setPort(5672); connectionFactory.setUsername(\u0026#34;guest\u0026#34;); connectionFactory.setPassword(\u0026#34;guest\u0026#34;); connectionFactory.setVirtualHost(\u0026#34;/\u0026#34;); // 创建与RabbitMQ服务的TCP连接 connection = connectionFactory.newConnection(); // 创建与Exchange的会话通道 channel = connection.createChannel(); // 声明两个队列 channel.queueDeclare(QUEUE_INFORM_EMAIL, true, false, false, null); channel.queueDeclare(QUEUE_INFORM_SMS, true, false, false, null); /* * 声明一个交换机 * Exchange.DeclareOk exchangeDeclare(String exchange, BuiltinExchangeType type) throws IOException; * 参数明细 * 1、交换机的名称 * 2、交换机的类型 * fanout：对应的rabbitmq的工作模式是 publish/subscribe * direct：对应的Routing\t工作模式 * topic：对应的Topics工作模式 * headers：对应的headers工作模式 */ channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT); /* * 交换机和队列绑定 * Queue.BindOk queueBind(String queue, String exchange, String routingKey) throws IOException; * 参数明细 * 1、queue 队列名称 * 2、exchange 交换机名称 * 3、routingKey 路由key，作用是交换机根据路由key的值将消息转发到指定的队列中，在发布订阅模式中调协为空字符串 */ channel.queueBind(QUEUE_INFORM_EMAIL, EXCHANGE_ROUTING_INFORM, ROUTINGKEY_EMAIL); channel.queueBind(QUEUE_INFORM_EMAIL, EXCHANGE_ROUTING_INFORM, \u0026#34;inform\u0026#34;); channel.queueBind(QUEUE_INFORM_SMS, EXCHANGE_ROUTING_INFORM, ROUTINGKEY_SMS); channel.queueBind(QUEUE_INFORM_SMS, EXCHANGE_ROUTING_INFORM, \u0026#34;inform\u0026#34;); // 测试1：发送消息(指定routingKey为inform_email) /*for (int i = 0; i \u0026lt; 5; i++) { String message = \u0026#34;send email inform message to user! NO.\u0026#34; + i; channel.basicPublish(EXCHANGE_ROUTING_INFORM, ROUTINGKEY_EMAIL, null, message.getBytes()); System.out.println(\u0026#34;send to mq \u0026#34; + message); }*/ // 测试2：发送消息(指定routingKey为inform_sms) /*for (int i = 0; i \u0026lt; 5; i++) { String message = \u0026#34;send sms inform message to user! NO.\u0026#34; + i; channel.basicPublish(EXCHANGE_ROUTING_INFORM, ROUTINGKEY_SMS, null, message.getBytes()); System.out.println(\u0026#34;send to mq \u0026#34; + message); }*/ // 测试3：发送消息(指定routingKey为inform) for (int i = 0; i \u0026lt; 5; i++) { String message = \u0026#34;send inform message to user! NO.\u0026#34; + i; channel.basicPublish(EXCHANGE_ROUTING_INFORM, \u0026#34;inform\u0026#34;, null, message.getBytes()); System.out.println(\u0026#34;send to mq \u0026#34; + message); } } catch (Exception ex) { ex.printStackTrace(); } finally { // 关闭资源，先关闭通道，再关闭连接 if (channel != null) { try { channel.close(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } if (connection != null) { try { connection.close(); } catch (IOException e) { e.printStackTrace(); } } } } } 3.4.2.2. 邮件发送消费者 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 /** * RabbitMQ消费者（消费邮件队列） - Routing 工作模式 */ public class Consumer03_routing_email { /* 定义队列的名称 */ private static final String QUEUE_INFORM_EMAIL = \u0026#34;queue_inform_email\u0026#34;; /* 定义交换机的名称 */ private static final String EXCHANGE_ROUTING_INFORM = \u0026#34;exchange_routing_inform\u0026#34;; /* 定义路由的名称 */ private static final String ROUTINGKEY_EMAIL = \u0026#34;inform_email\u0026#34;; public static void main(String[] args) throws IOException, TimeoutException { // 通过连接工厂创建新的连接和mq建立连接 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost(\u0026#34;192.168.12.132\u0026#34;); connectionFactory.setPort(5672); connectionFactory.setUsername(\u0026#34;guest\u0026#34;); connectionFactory.setPassword(\u0026#34;guest\u0026#34;); connectionFactory.setVirtualHost(\u0026#34;/\u0026#34;); // 创建与RabbitMQ服务的TCP连接 Connection connection = connectionFactory.newConnection(); // 创建与Exchange的会话通道 Channel channel = connection.createChannel(); // 声明监听的队列，如果队列在RabbitMQ中没有则将自动创建 channel.queueDeclare(QUEUE_INFORM_EMAIL, true, false, false, null); // 声明一个交换机（路由模式） channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT); // 进行交换机和队列绑定 channel.queueBind(QUEUE_INFORM_EMAIL, EXCHANGE_ROUTING_INFORM, ROUTINGKEY_EMAIL); /* 实现消费方法（重写） */ DefaultConsumer consumer = new DefaultConsumer(channel) { /** * 当消费者接收到消息后，此方法将被调用 * * @param consumerTag 消费者标签，用来标识消费者的，在监听队列时设置channel.basicConsume * @param envelope 信封，消息包的内容，可从中获取消息id，消息routingkey，交换机，消息和重传标志(收到消息失败后是否需要重新发送) * @param properties 消息属性 * @param body 消息内容 */ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { // 获取交换机 String exchange = envelope.getExchange(); // 路由key String routingKey = envelope.getRoutingKey(); // 消息id，mq在channel中用来标识消息的id，可用于确认消息已接收 long deliveryTag = envelope.getDeliveryTag(); System.out.println(\u0026#34;交换机exchange：\u0026#34; + exchange + \u0026#34;；路由routingKey：\u0026#34; + routingKey + \u0026#34;；消息id：\u0026#34; + deliveryTag); // 消息内容 String message = new String(body, StandardCharsets.UTF_8); System.out.println(\u0026#34;receive message:\u0026#34; + message); } }; // 监听队列 channel.basicConsume(QUEUE_INFORM_EMAIL, true, consumer); } } 3.4.2.3. 短信发送消费者 参考邮件发送消费者的代码流程，编写短信通知的代码，修改队列名称与路由名称即可\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /* 定义队列的名称 */ private static final String QUEUE_INFORM_SMS = \u0026#34;queue_inform_sms\u0026#34;; /* 定义交换机的名称 */ private static final String EXCHANGE_ROUTING_INFORM = \u0026#34;exchange_routing_inform\u0026#34;; /* 定义路由的名称 */ private static final String ROUTINGKEY_SMS = \u0026#34;inform_sms\u0026#34;; ... // 声明监听的队列，如果队列在RabbitMQ中没有则将自动创建 channel.queueDeclare(QUEUE_INFORM_SMS, true, false, false, null); ... // 进行交换机和队列绑定 channel.queueBind(QUEUE_INFORM_SMS, EXCHANGE_ROUTING_INFORM, ROUTINGKEY_SMS); ... // 监听队列 channel.basicConsume(QUEUE_INFORM_SMS, true, consumer); 3.4.3. 测试 打开 RabbitMQ 的管理界面，观察交换机绑定情况\n使用生产者发送若干条消息，交换机根据routingkey转发消息到指定的队列\n3.4.4. 总结 Routing 模式和Publish/subscibe 的区别：Routing 模式要求队列在绑定交换机时要指定 routingkey，消息会转发到符合 routingkey 的队列\n3.5. Topics 主题工作模式 主题模式：\n一个交换机可以绑定多个队列，每个队列可以设置一个或多个带统配符的 routingKey 生产者将消息发给交换机，交换机根据 routingKey 的值来匹配队列，匹配时采用统配符方式，匹配成功的将消息转发到指定的队列 每个消费者监听自己的队列，并且设置带统配符的 routingkey。 生产者将消息发给 broker，由交换机根据 routingkey 来转发消息到指定的队列。 3.5.1. 案例代码 案例：根据用户的通知设置去通知用户，设置接收 Email 的用户只接收 Email，设置接收 sms 的用户只接收 sms，设置两种通知类型都接收的则两种通知都有效。\n3.5.1.1. 生产者 声明交换机，指定topic类型\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /* * 声明一个交换机 * 1、交换机的名称 * 2、交换机的类型 * fanout：对应的rabbitmq的工作模式是 publish/subscribe * direct：对应的Routing\t工作模式 * topic：对应的Topics工作模式 * headers：对应的headers工作模式 */ channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC); /* Email通知 */ channel.basicPublish(EXCHANGE_TOPICS_INFORM, \u0026#34;inform.email\u0026#34;, null, message.getBytes()); /* SMS通知 */ channel.basicPublish(EXCHANGE_TOPICS_INFORM, \u0026#34;inform.sms\u0026#34;, null, message.getBytes()); /* 两种类型都通知 */ channel.basicPublish(EXCHANGE_TOPICS_INFORM, \u0026#34;inform.sms.email\u0026#34;, null, message.getBytes()); 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 /** * RabbitMQ生产者 - topics 工作模式 */ public class Producer04_topics { /* 定义队列的名称 */ private static final String QUEUE_INFORM_EMAIL = \u0026#34;queue_inform_email\u0026#34;; private static final String QUEUE_INFORM_SMS = \u0026#34;queue_inform_sms\u0026#34;; /* 定义交换机名称 */ private static final String EXCHANGE_TOPICS_INFORM = \u0026#34;exchange_topics_inform\u0026#34;; /* 定义路由名称(带通配符) */ private static final String ROUTINGKEY_EMAIL = \u0026#34;inform.#.email.#\u0026#34;; private static final String ROUTINGKEY_SMS = \u0026#34;inform.#.sms.#\u0026#34;; public static void main(String[] args) { Connection connection = null; Channel channel = null; try { // 通过连接工厂创建新的连接和mq建立连接 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost(\u0026#34;192.168.12.132\u0026#34;); connectionFactory.setPort(5672); connectionFactory.setUsername(\u0026#34;guest\u0026#34;); connectionFactory.setPassword(\u0026#34;guest\u0026#34;); connectionFactory.setVirtualHost(\u0026#34;/\u0026#34;); // 创建与RabbitMQ服务的TCP连接 connection = connectionFactory.newConnection(); // 创建与Exchange的会话通道 channel = connection.createChannel(); // 声明两个队列 channel.queueDeclare(QUEUE_INFORM_EMAIL, true, false, false, null); channel.queueDeclare(QUEUE_INFORM_SMS, true, false, false, null); /* * 声明一个交换机 * Exchange.DeclareOk exchangeDeclare(String exchange, BuiltinExchangeType type) throws IOException; * 参数明细 * 1、交换机的名称 * 2、交换机的类型 * fanout：对应的rabbitmq的工作模式是 publish/subscribe * direct：对应的Routing\t工作模式 * topic：对应的Topics工作模式 * headers：对应的headers工作模式 */ channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC); /* * 交换机和队列绑定 * Queue.BindOk queueBind(String queue, String exchange, String routingKey) throws IOException; * 参数明细 * 1、queue 队列名称 * 2、exchange 交换机名称 * 3、routingKey 路由key，作用是交换机根据路由key的值将消息转发到指定的队列中，在发布订阅模式中调协为空字符串 */ channel.queueBind(QUEUE_INFORM_EMAIL, EXCHANGE_TOPICS_INFORM, ROUTINGKEY_EMAIL); channel.queueBind(QUEUE_INFORM_SMS, EXCHANGE_TOPICS_INFORM, ROUTINGKEY_SMS); // 测试1：发送消息(指定routingKey为inform_email) for (int i = 0; i \u0026lt; 5; i++) { String message = \u0026#34;send email inform message to user! NO.\u0026#34; + i; channel.basicPublish(EXCHANGE_TOPICS_INFORM, \u0026#34;inform.email\u0026#34;, null, message.getBytes()); System.out.println(\u0026#34;send to mq \u0026#34; + message); } // 测试2：发送消息(指定routingKey为inform_sms) for (int i = 0; i \u0026lt; 5; i++) { String message = \u0026#34;send sms inform message to user! NO.\u0026#34; + i; channel.basicPublish(EXCHANGE_TOPICS_INFORM, \u0026#34;inform.sms\u0026#34;, null, message.getBytes()); System.out.println(\u0026#34;send to mq \u0026#34; + message); } // 测试3：发送消息(指定routingKey为inform) for (int i = 0; i \u0026lt; 5; i++) { String message = \u0026#34;send inform message to user! NO.\u0026#34; + i; channel.basicPublish(EXCHANGE_TOPICS_INFORM, \u0026#34;inform.sms.email\u0026#34;, null, message.getBytes()); System.out.println(\u0026#34;send to mq \u0026#34; + message); } } catch (Exception ex) { ex.printStackTrace(); } finally { // 关闭资源，先关闭通道，再关闭连接 if (channel != null) { try { channel.close(); } catch (IOException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } } if (connection != null) { try { connection.close(); } catch (IOException e) { e.printStackTrace(); } } } } } 3.5.1.2. 消费端 队列绑定交换机指定通配符，统配符规则如下：\n中间以 . 分隔。 符号#可以匹配多个词，符号*可以匹配一个词语。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 /** * RabbitMQ消费者（消费邮件队列） - Topics 工作模式 */ public class Consumer04_topics_email { /* 定义队列的名称 */ private static final String QUEUE_INFORM_EMAIL = \u0026#34;queue_inform_email\u0026#34;; /* 定义交换机的名称 */ private static final String EXCHANGE_TOPICS_INFORM = \u0026#34;exchange_topics_inform\u0026#34;; /* 定义路由的名称 */ private static final String ROUTINGKEY_EMAIL = \u0026#34;inform.#.email.#\u0026#34;; public static void main(String[] args) throws IOException, TimeoutException { ... // 声明监听的队列，如果队列在RabbitMQ中没有则将自动创建 channel.queueDeclare(QUEUE_INFORM_EMAIL, true, false, false, null); // 声明一个交换机（路由模式） channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC); // 进行交换机和队列绑定 channel.queueBind(QUEUE_INFORM_EMAIL, EXCHANGE_TOPICS_INFORM, ROUTINGKEY_EMAIL); ... // 监听队列 channel.basicConsume(QUEUE_INFORM_EMAIL, true, consumer); } } /** * RabbitMQ消费者（消费短信队列） - Topics 工作模式 */ public class Consumer04_topics_sms { /* 定义队列的名称 */ private static final String QUEUE_INFORM_SMS = \u0026#34;queue_inform_sms\u0026#34;; /* 定义交换机的名称 */ private static final String EXCHANGE_TOPICS_INFORM = \u0026#34;exchange_topics_inform\u0026#34;; /* 定义路由的名称 */ private static final String ROUTINGKEY_SMS = \u0026#34;inform.#.sms.#\u0026#34;; public static void main(String[] args) throws IOException, TimeoutException { ... // 声明监听的队列，如果队列在RabbitMQ中没有则将自动创建 channel.queueDeclare(QUEUE_INFORM_SMS, true, false, false, null); // 声明一个交换机（路由模式） channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC); // 进行交换机和队列绑定 channel.queueBind(QUEUE_INFORM_SMS, EXCHANGE_TOPICS_INFORM, ROUTINGKEY_SMS); ... // 监听队列 channel.basicConsume(QUEUE_INFORM_SMS, true, consumer); } } 3.5.2. 测试 使用生产者发送若干条消息，交换机根据routingkey统配符匹配并转发消息到指定的队列\n3.5.3. 总结 本案例的需求使用Routing工作模式能否实现？\n使用Routing模式也可以实现本案例，共设置三个 routingkey，分别是email、sms、all，email队列绑定email和all，sms队列绑定sms和all，这样就可以实现上边案例的功能，实现过程比topics复杂。 Topic模式更多加强大，它可以实现Routing、publish/subscirbe模式的功能 3.5.4. Topics 与 Routing 的区别 Topics 与 Routing 的基本原理相同，即：生产者将消息发给交换机，交换机根据 routingKey 将消息转发给与 routingKey 匹配的队列 不同之处是：routingKey 的匹配方式，Routing 模式是相等匹配，topics 模式是统配符匹配 符号#：匹配一个或者多个词，比如 inform.# 可以匹配 inform.sms、inform.email、inform.email.sms\n符号*：只能匹配一个词，比如 inform.* 可以匹配 inform.sms、inform.email\n3.6. Header 工作模式 header模式与routing不同的地方在于，header模式取消routingkey，使用header中的 key/value（键值对）匹配队列。\n3.6.1. 案例代码 案例需求：根据用户的通知设置去通知用户，设置接收Email的用户只接收Email，设置接收sms的用户只接收sms，设置两种通知类型都接收的则两种通知都有效。\n3.6.1.1. 生产者 队列与交换机绑定的代码与之前不同，如下 1 2 3 4 5 6 Map\u0026lt;String, Object\u0026gt; headers_email = new Hashtable\u0026lt;String, Object\u0026gt;(); headers_email.put(\u0026#34;inform_type\u0026#34;, \u0026#34;email\u0026#34;); Map\u0026lt;String, Object\u0026gt; headers_sms = new Hashtable\u0026lt;String, Object\u0026gt;(); headers_sms.put(\u0026#34;inform_type\u0026#34;, \u0026#34;sms\u0026#34;); channel.queueBind(QUEUE_INFORM_EMAIL, EXCHANGE_HEADERS_INFORM, \u0026#34;\u0026#34;, headers_email); channel.queueBind(QUEUE_INFORM_SMS, EXCHANGE_HEADERS_INFORM, \u0026#34;\u0026#34;, headers_sms); 通知 1 2 3 4 5 6 7 8 String message = \u0026#34;email inform to user\u0026#34; + i; Map\u0026lt;String, Object\u0026gt; headers = new Hashtable\u0026lt;String, Object\u0026gt;(); headers.put(\u0026#34;inform_type\u0026#34;, \u0026#34;email\u0026#34;); // 匹配email通知消费者绑定的header // headers.put(\u0026#34;inform_type\u0026#34;, \u0026#34;sms\u0026#34;); // 匹配sms通知消费者绑定的header AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties.Builder(); properties.headers(headers); // Email通知 channel.basicPublish(EXCHANGE_HEADERS_INFORM, \u0026#34;\u0026#34;, properties.build(), message.getBytes()); 3.6.1.2. 发送邮件消费者 1 2 3 4 5 6 7 channel.exchangeDeclare(EXCHANGE_HEADERS_INFORM, BuiltinExchangeType.HEADERS); Map\u0026lt;String, Object\u0026gt; headers_email = new Hashtable\u0026lt;String, Object\u0026gt;(); headers_email.put(\u0026#34;inform_email\u0026#34;, \u0026#34;email\u0026#34;); // 交换机和队列绑定 channel.queueBind(QUEUE_INFORM_EMAIL, EXCHANGE_HEADERS_INFORM, \u0026#34;\u0026#34;, headers_email); // 指定消费队列 channel.basicConsume(QUEUE_INFORM_EMAIL, true, consumer); 3.6.2. 测试 3.7. RPC 工作模式 RPC即客户端远程调用服务端的方法，使用MQ可以实现RPC的异步调用，基于Direct交换机实现，流程如下：\n客户端即是生产者就是消费者，向 RPC 请求队列发送 RPC 调用消息，同时监听 RPC 响应队列。 服务端监听 RPC 请求队列的消息，收到消息后执行服务端的方法，得到方法返回的结果 服务端将 RPC 方法的结果发送到 RPC 响应队列 客户端（RPC 调用方）监听 RPC 响应队列，接收到 RPC 调用结果。 4. Spring 整合 RibbitMQ 4.1. 搭建SpringBoot环境 基于Spring-Rabbit去操作RabbitMQ，参考：https://github.com/spring-projects/spring-amqp 使用 spring-boot-starter-amqp 会自动添加 spring-rabbit 依赖。修改工程的pom.xml文件： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-amqp\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;fastjson\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-logging\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 4.2. 配置 4.2.1. 配置application.yml 配置连接 rabbitmq 的参数\n1 2 3 4 5 6 7 8 9 10 11 server: port: 44000 spring: application: name: test-rabbitmq-producer rabbitmq: host: 192.168.12.132 port: 5672 username: guest password: guest virtualHost: / 4.2.2. 定义RabbitConfig类，配置Exchange、Queue、及绑定交换机 本例配置Topic交换机\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 package com.xuecheng.test.rabbitmq.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Exchange; import org.springframework.amqp.core.ExchangeBuilder; import org.springframework.amqp.core.Queue; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * RabbitMQ配置类 */ @Configuration public class RabbitmqConfig { /* 定义队列的名称 */ private static final String QUEUE_INFORM_EMAIL = \u0026#34;queue_inform_email\u0026#34;; private static final String QUEUE_INFORM_SMS = \u0026#34;queue_inform_sms\u0026#34;; /* 定义交换机名称 */ private static final String EXCHANGE_TOPICS_INFORM = \u0026#34;exchange_topics_inform\u0026#34;; /* 定义路由名称(带通配符) */ private static final String ROUTINGKEY_EMAIL = \u0026#34;inform.#.email.#\u0026#34;; private static final String ROUTINGKEY_SMS = \u0026#34;inform.#.sms.#\u0026#34;; /** * 声明配置交换机 * ExchangeBuilder提供了fanout、direct、topic、header交换机类型的配置 * * @return the exchange */ @Bean(EXCHANGE_TOPICS_INFORM) public Exchange EXCHANGE_TOPICS_INFORM() { // 设置durable为true，表示持久化，消息队列重启后交换机仍然存在 return ExchangeBuilder.topicExchange(EXCHANGE_TOPICS_INFORM).durable(true).build(); } /** * 声明QUEUE_INFORM_EMAIL队列 * * @return Queue */ @Bean(QUEUE_INFORM_EMAIL) public Queue QUEUE_INFORM_EMAIL() { return new Queue(QUEUE_INFORM_EMAIL); } /** * 声明QUEUE_INFORM_SMS队列 * * @return Queue */ @Bean(QUEUE_INFORM_SMS) public Queue QUEUE_INFORM_SMS() { return new Queue(QUEUE_INFORM_SMS); } /** * ROUTINGKEY_EMAIL队列绑定交换机，指定routingKey * * @param queue the queue * @param exchange the exchange * @return the binding */ @Bean public Binding BINDING_QUEUE_INFORM_EMAIL(@Qualifier(QUEUE_INFORM_EMAIL) Queue queue, @Qualifier(EXCHANGE_TOPICS_INFORM) Exchange exchange) { return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_EMAIL).noargs(); } /** * ROUTINGKEY_SMS队列绑定交换机，指定routingKey * * @param queue the queue * @param exchange the exchange * @return the binding */ @Bean public Binding BINDING_ROUTINGKEY_SMS(@Qualifier(QUEUE_INFORM_SMS) Queue queue, @Qualifier(EXCHANGE_TOPICS_INFORM) Exchange exchange) { return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_SMS).noargs(); } } 4.3. 生产端 使用RarbbitTemplate发送消息\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /** * RabbitMQ生产者 与 Spring 整合 */ @SpringBootTest @RunWith(SpringRunner.class) public class Producer05_topics_springboot { /* 注入RabbitMQ操作对象 */ @Autowired private RabbitTemplate rabbitTemplate; /** * 使用rabbitTemplate发送消息 */ @Test public void testSendEmail() { for (int i = 0; i \u0026lt; 5; i++) { String message = \u0026#34;send email inform message to user! NO.\u0026#34; + i; /* * 发送消息 * public void convertAndSend(String exchange, String routingKey, final Object object) * 参数说明 * exchange：交换机名称 * routingKey：路由key * object：消息体内容 */ rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE_TOPICS_INFORM, \u0026#34;inform.email\u0026#34;, message); System.out.println(\u0026#34;send to mq \u0026#34; + message); } } } 4.4. 消费端 创建消费端工程，添加依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-amqp\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-logging\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 使用@RabbitListener注解监听队列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /** * RabbitMQ消费者 与 Spring 整合 */ @Component public class ReceiveHandler { /** * 监听email队列 * * @param msg * @param message * @param channel */ @RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_EMAIL}) public void receive_email(String msg, Message message, Channel channel) { System.out.println(\u0026#34;receive message is:\u0026#34; + msg); } /** * 监听sms队列 * * @param msg * @param message * @param channel */ @RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_SMS}) public void receive_sms(String msg, Message message, Channel channel) { System.out.println(\u0026#34;receive message is:\u0026#34; + msg); } } 4.5. 测试 5. 消息丢失 5.1. 消息丢失场景分析 消息丢失有以下场景：\n生产者生产消息到 RabbitMQ Server 的消息丢失 RabbitMQ Server 存储的消息丢失 RabbitMQ Server 到消费者的消息丢失 5.2. 针对生产者消息丢失的解决方案 5.2.1. 场景概述 生产者发送消息到队列，可能会出现网络波动问题，从而无法确保发送的消息成功的到达 server。\n5.2.2. 方案1：开启 RabbitMQ 事务 选择开启 RabbitMQ 提供的事务功能。生产者发送数据之前执行 channel.txSelect 开启 RabbitMQ 事务，然后发送消息，此时发送端会进入阻塞状态，并等待 RabbitMQ 的回应。如果消息没有成功被 RabbitMQ 接收到，那么生产者会收到异常报错，此时就可以回滚事务 channel.txRollback，然后重试发送消息；如果收到了消息，那么可以提交事务 channel.txCommit，之后才能继续发送下一条消息。方案实现示例（伪代码）如下：\n1 2 3 4 5 6 7 8 9 10 11 // 开启事务 channel.txSelect(); try { // 这里发送消息 } catch (Exception e) { channel.txRollback(); // 这里再次重发这条消息 } // 提交事务 channel.txCommit(); 这种方案的缺点就是性能较差。因为 RabbitMQ 事务机制是同步的，提交一个事务之后会阻塞，采用这种方式基本上吞吐量会下来，太耗性能。\n5.2.3. 方案2：生产者确认(confirm)机制 5.2.3.1. 概述 开启生产者确认机制。将信道设置成 confirm 模式（发送方确认模式），所有在信道上发布的消息都会生成一个唯一的 ID。只要消息成功被投递到目的队列后，或者消息被写入磁盘后（可持久化的消息），RabbitMQ 就会发送一个 ack 给生产者（包含消息唯一 ID，即使消息没有 Queue 接收，也会发送 ack）；如果消息没有成功发送到交换机或者因 RabbitMQ 发生内部错误从而导致消息丢失，也会发送一条 nack（notacknowledged，未确认）消息，提示发送失败。当确认消息返回给生产者应用程序后，生产者的回调方法就会被触发来处理确认消息，做相应的业务逻辑。\nNotes: 事务机制和 cnofirm 机制最大的不同在于，事务机制是同步的，提交一个事务之后会线程阻塞；发送方确认模式是异步的，生产者应用程序在等待确认的同时，可以继续发送消息。\n5.2.3.2. 具体实现步骤 在 Springboot 是通过 publisher-confirms 参数来设置 confirm 模式： 1 2 3 4 5 spring: rabbitmq: # RabbitMQ 相关配置 host: 192.168.12.132 port: 5672 publisher-confirms: true # 开启 confirm 确认机制 在生产端会提供一个回调方法，当服务端确认了一条或者多条消息后，生产者会回调这个方法，根据具体的结果对消息进行后续处理，比如重新发送、记录日志等。 1 2 3 4 5 6 7 8 9 // 消息是否成功发送到Exchange final RabbitTemplate.ConfirmCallback confirmCallback = (CorrelationData correlationData, boolean ack, String cause) -\u0026gt; { log.info(\u0026#34;correlationData: \u0026#34; + correlationData); log.info(\u0026#34;ack: \u0026#34; + ack); if (!ack) { log.info(\u0026#34;异常处理....\u0026#34;); } }; rabbitTemplate.setConfirmCallback(confirmCallback); 5.3. 路由不可达消息 生产者确认机制只确保消息正确到达交换机，对于从交换机路由到 Queue 失败的消息，也会被丢弃掉，导致消息丢失。对于不可路由的消息，有两种处理方式：Return 消息机制和备份交换机。\n5.3.1. Return 在核心配置文件中，将 mandatory 选项设置为 true，开启监听到路由不可达的消息。Return 消息机制提供了回调函数 ReturnCallback，当消息从交换机路由到 Queue 失败才会回调这个方法。\n1 2 3 4 spring: rabbitmq: # RabbitMQ 相关配置 # 触发 ReturnCallback 必须设置 mandatory=true, 否则 Exchange 没有找到 Queue 就会丢弃掉消息, 也不会触发 ReturnCallback template.mandatory: true 通过 ReturnCallback 回调函数监听路由不可达消息。\n1 2 3 4 5 6 final RabbitTemplate.ReturnCallback returnCallback = (Message message, int replyCode, String replyText, String exchange, String routingKey) -\u0026gt; { // do something... log.info(\u0026#34;return exchange: \u0026#34; + exchange + \u0026#34;, routingKey: \u0026#34; + routingKey + \u0026#34;, replyCode: \u0026#34; + replyCode + \u0026#34;, replyText: \u0026#34; + replyText); }; rabbitTemplate.setReturnCallback(returnCallback); 当消息从交换机路由到 Queue 失败时，会返回 return exchange: , routingKey: MAIL, replyCode: 312, replyText: NO_ROUTE。\n5.3.2. 备份交换机 备份交换机 alternate-exchange 是一个普通的 exchange，当发送消息到对应的 exchange 时，没有匹配到 queue，就会自动转移到备份交换机对应的 queue，这样消息就不会丢失。\n5.4. 针对 RabbitMQ Server 消息丢失的解决方案 5.4.1. 场景概述 如果 RabbitMQ 服务异常导致重启，将会导致消息丢失。会出现以下几种情况：\n保证 RabbitMQ 不丢失消息，那么就需要开启 RabbitMQ 的持久化机制，即把消息持久化到硬盘，这样即使 RabbitMQ 挂掉在重启后仍然可以从硬盘读取消息。 RabbitMQ 单点故障，这种情况不会造成消息丢失，但会涉及到 RabbitMQ 的3种安装方式：单机模式、普通集群模式、镜像集群模式。要保证 RabbitMQ 的高可用就要配合 HAPROXY 做镜像集群模式。 硬盘坏掉怎么保证消息不丢失？ 5.4.2. 消息持久化 RabbitMQ 的消息默认存放在内存中，如果不特别声明设置，消息不会持久化保存到硬盘，如果节点重启或者意外 crash 掉，消息就会丢失。RabbitMQ 提供了持久化的机制，将内存中的消息持久化到硬盘上，即使重启 RabbitMQ，消息也不会丢失。消息持久化需要满足以下条件(缺一不可)：\n消息设置持久化。发布消息前，设置投递模式 delivery mode 为2，表示消息需要持久化。 Queue 设置持久化。 交换机设置持久化。 在开启消息持久化后，RabbitMQ 根据不同情况做以下处理：\n当生产者发布一条消息到交换机上时，RabbitMQ 会先把消息写入持久化日志，然后才向消息生产者发送响应。 一旦从队列中消费了一条消息并且做了确认，RabbitMQ 会在持久化日志中移除这条消息。 在消费消息前，如果 RabbitMQ 重启的话，服务器会自动重建交换机和队列（以及绑定），加载持久化日志中的消息到相应的队列或者交换机上，保证消息不会丢失。 持久化的缺点：降低了服务器的吞吐量，因为使用的是磁盘而非内存存储，从而降低了吞吐量。可尽量使用 ssd 硬盘来缓解吞吐量的问题。\n5.4.3. 镜像队列 回顾 RabbitMQ 三种部署模式，详见后面章节\n当 MQ 发生故障时，会导致服务不可用。引入 RabbitMQ 的镜像队列机制，将 queue 镜像到集群中其他的节点之上。如果集群中的一个节点失效了，能自动地切换到镜像中的另一个节点以保证服务的可用性。\n通常每一个镜像队列都包含一个 master 和多个 slave，分别对应于不同的节点。发送到镜像队列的所有消息总是被直接发送到 master 和所有的 slave 之上。除了 publish 外所有动作都只会向 master 发送，然后由 master 将命令执行的结果广播给 slave，从镜像队列中的消费操作实际上是在 master 上执行的。\n5.4.4. 消息补偿机制 可能有这么一种情况：在持久化的消息，保存到硬盘过程中，当前队列节点挂了，存储节点硬盘又坏了，消息丢了，怎么办？处理方案如下：\n生产端首先将业务数据以及消息数据入库，需要在同一个事务中，消息数据入库失败，则整体回滚。 根据消息表中消息状态，失败则进行消息补偿措施，重新发送消息处理。 5.5. 针对消费者消息丢失的解决方案 5.5.1. 场景概述 消费者收到消息后，还没来得及处理或者处理失败，MQ 服务就宕机了，从而导致消息丢失。\n5.5.2. 消费者手动消息确认 5.5.2.1. 概述 RabbitMQ 的 消费者默认采用自动 ack，一旦消费者收到消息后会通知 MQ Server 这条消息已经处理好了，MQ 就会移除这条消息。但有可能消费者收到消息还没来得及处理，MQ 服务就宕机了，从而导致消息丢失。\n解决方案：消费者设置为手动确认消息。消费者处理完逻辑之后再给 broker 回复 ack，表示消息已经成功消费，可以从 broker 中删除。当消息者消费失败的时候，给 broker 回复 nack，根据配置决定重新入队还是从 broker 移除，或者进入死信队列。只要没收到消费者的 acknowledgment，broker 就会一直保存着这条消息，但不会重新入队，也不会分配给其他消费者。\n注意：在消费者确认机制中没有使用超时机制，RabbitMQ 仅通过 Consumer 的连接中断来确认是否需要重新发送消息。也就是说，只要连接不中断，RabbitMQ 给了 Consumer 足够长的时间来处理消息，保证数据的最终一致性。\n5.5.2.2. 具体实现步骤 配置消费者设置手动消息确认 ack： 1 2 3 4 5 spring: rabbitmq: # RabbitMQ 相关配置 listener: simple: acknowledge-mode: manual # 设置消费端手动消息确认 ack 或者\n1 2 # 设置消费端手动消息确认 ack spring.rabbitmq.listener.simple.acknowledge-mode=manual 消息处理完，手动确认： 1 2 3 4 5 6 7 8 9 10 11 12 @RabbitListener(queues = RabbitMqConfig.MAIL_QUEUE) public void onMessage(Message message, Channel channel) throws IOException { try { Thread.sleep(5000); // 模拟业务逻辑耗时 } catch (InterruptedException e) { e.printStackTrace(); } long deliveryTag = message.getMessageProperties().getDeliveryTag(); // 手工 ack；第二个参数是 multiple，设置为 true，表示 deliveryTag 序列号之前（包括自身）的消息都已经收到，设为 false 则表示收到一条消息 channel.basicAck(deliveryTag, true); System.out.println(\u0026#34;mail listener receive: \u0026#34; + new String(message.getBody())); } 当消息消费失败时，消费端给 broker 回复 nack，如果 consumer 设置了 requeue 为 false，则 nack 后 broker 会删除消息或者进入死信队列，否则消息会重新入队。\n5.6. 总结 如果需要保证消息在整条链路中不丢失，那就需要生产端、MQ 服务与消费端共同去保障。\n生产端：对生产的消息进行状态标记，开启 confirm 机制，依据 mq 的响应来更新消息状态，使用定时任务重新投递超时的消息，多次投递失败进行报警。 MQ 服务：开启持久化，并在落盘后再进行 ack。如果是镜像部署模式，需要在同步到多个副本之后再进行 ack。 消费端：开启手动 ack 模式，在业务处理完成后再进行 ack，并且需要保证幂等。 Notes: 通过以上的处理，理论上不存在消息丢失的情况，但是系统的吞吐量以及性能有所下降。\n6. 重复消费 消息重复的情况有以下两种：\n生产时消息重复。生产者发送消息给 MQ，在 MQ 确认的时候出现了网络波动，生产者没有收到确认，这时候生产者就会重新发送这条消息，导致 MQ 会接收到重复消息。 消费时消息重复。消费者消费成功后，给 MQ 确认的时候出现了网络波动，MQ 没有接收到确认，为了保证消息不丢失，MQ 就会继续给消费者投递之前的消息。这时候消费者就接收到了两条一样的消息。 重复消息是由于网络原因造成的，无法避免。\n解决方案：发送消息时让每个消息携带一个全局的唯一ID，在消费消息时先判断消息是否已经被消费过，保证消息消费逻辑的幂等性。具体消费过程为：\n消费者获取到消息后先根据 id 去查询 redis/db 是否存在该消息。 如果不存在，则正常消费，消费完毕后写入 redis/db。 如果存在，则证明消息被消费过，直接丢弃。 7. 消费端限流 当 RabbitMQ 服务器积压大量消息时，队列里的消息会大量涌入消费端，可能导致消费端服务器奔溃。这种情况下需要对消费端限流。\nSpring RabbitMQ 提供参数 prefetch 可以设置单个请求处理的消息个数。如果消费者同时处理的消息到达最大值的时候，则该消费者会阻塞，不会消费新的消息，直到有消息 ack 才会消费新的消息。\n开启消费端限流： properties 配置：\n1 2 # 在单个请求中处理的消息个数，unack 的最大数量 spring.rabbitmq.listener.simple.prefetch=2 yaml 配置：\n1 2 3 4 5 spring: rabbitmq: # RabbitMQ 相关配置 listener: simple: prefetch: 2 # 在单个请求中处理的消息个数，unack 的最大数量 原生 RabbitMQ 还提供 prefetchSize 和 global 两个参数。但 Spring RabbitMQ 没有这两个参数。 1 2 3 4 // 单条消息大小限制，0代表不限制 // global：限制限流功能是channel级别的还是consumer级别。 // 当设置为 false，consumer 级别，限流功能生效，设置为 true 没有了限流功能，因为 channel 级别尚未实现。 void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException; 8. (待整理学习)死信队列 死信队列，是消费失败的消息存放的队列。消息消费失败的原因主要有如下：\n消费者使用 basic.reject 或 basic.nack 声明消费失败（消息被拒绝）并且消息没有重新入队（消息的 requeue 参数设置为 false） 消息超时未消费 达到最大队列长度，最早的消息可能成为死信 8.1. 死信交换机 如果该队列配置了 dead-letter-exchange 属性，指定了一个交换机，那么队列中的死信就会投递到这个交换机中，而这个交换机称为死信交换机（Dead Letter Exchange，简称DLX）。参考代码如下：\n死信队列流程简图：\n8.2. TTL TTL，即 Time-To-Live。如果一个队列中的消息 TTL 结束仍未消费，则会变为死信，ttl 超时分为两种情况：\n消息所在的队列设置了存活时间 消息本身设置了存活时间 参考代码：\n流程简图：\n8.3. 延迟队列 延迟队列，是指进入队列的消息会被延迟消费的队列。延迟队列 = 死信交换机 + TTL（生存时间）。\n延迟队列的使用场景场景：超时订单、限时优惠、定时发布等。\n8.3.1. 延迟队列插件 需要在 RabbitMQ 中安装 DelayExchange 插件。在 RabbitMQ 官方的插件社区获取，地址为：https://www.rabbitmq.com/community-plugins.html\nDelayExchange 插件的本质还是官方的三种交换机，只是添加了延迟功能。因此使用时只需要声明一个交换机，交换机的类型可以是任意类型，然后设定 delayed 属性为 true 即可。\n9. （待整理研究）消息堆积 当生产者发送消息的速度超过了消费者处理消息的速度，就会导致队列中的消息堆积，直到队列存储消息达到上限。之后发送的消息就会成为死信，也可能会被丢弃，这就是消息堆积问题。\n9.1. 解决消息堆积的思路 增加更多消费者，提高消费速度。 在消费者内开启线程池，使用多线程加快消息处理速度。 扩大队列容积，提高堆积上限。（采用惰性队列） 9.2. 惰性队列 在声明队列的时候可以设置 x-queue-mode 属性为 lazy，即为惰性队列。\n惰性队列的特征如下：\n接收到消息后直接存入磁盘而非内存。消息数量上限更高。 消费者要消费消息时才会从磁盘中读取并加载到内存。 支持数百万条的消息存储。 性能比较稳定，但基于磁盘存储，受限于磁盘IO，时效性会降低。 10. RabbitMQ 部署的模式 RabbitMQ 有三种模式：单机模式、普通集群模式、镜像集群模式。RabbitMQ 是基于主从（非分布式）模式实现高可用性的。\n10.1. 单机模式 一般用于本地开发测试使用，单实例，无法保证高可用。节点挂了，消息就不能用了。业务可能瘫痪，只能等待。\n10.2. 普通集群模式 在多台机器上启动多个 RabbitMQ 实例，每个机器部署一个实例，可以一台机器部署多个实例。普通集群，或者叫标准集群（classic cluster），具备下列特征：\n此模式下创建的队列(queue)，只会存放到一个 RabbitMQ 实例上，但是集群中其他各个实例都同步 queue 的元数据。元数据可以认为是 queue 的一些配置信息，包括：交换机、队列元信息。但不包含队列中的消息。通过元数据，可以找到 queue 所在实例。 当消费消息的时候，实际上如果连接到了另外一个实例，此时那个实例会从 queue 所在实例上拉取数据过来。这方案主要是提高吞吐量的，即让集群中多个节点来服务某个 queue 的读写操作。 队列所在节点宕机，队列中的消息就会丢失。 10.3. 镜像集群模式 10.3.1. 概述 镜像集群模式才是 RabbitMQ 真正的高可用模式，本质是主从模式。跟普通集群模式不一样的是，在镜像集群模式下创建的 queue，无论元数据还是 queue 里的消息都会存在于多个实例上。即每个 RabbitMQ 节点都有这个 queue 的一个完整镜像，包含 queue 的全部数据。然后每次写消息到 queue 的时候，都会自动把消息同步到多个实例的 queue 上。\n主要特征如下：\n创建队列的节点被称为该队列的主节点，备份到的其它节点叫做该队列的镜像节点。 一个队列的主节点可能是另一个队列的镜像节点。 所有操作都是主节点完成，然后同步给镜像节点。 主宕机后，镜像节点会替代成新的主节点。 镜像集群模式的优缺点：\n优点：任何一个机器宕机了，其它机器（节点）还包含了这个 queue 的完整数据，别的 consumer 都可以到其它节点上去消费数据。 缺点：性能开销大，消息需要同步到所有机器上，导致网络带宽压力和消耗加重，系统的吞吐量会有所下降。 RabbitMQ 有很好的管理控制台，假如在后台新增一个镜像集群模式的策略，策略是可以设置数据同步到所有节点，也可以设置同步到指定数量的节点。之后创建 queue 的时候，都会应用这个策略，就会自动将数据同步到其他的节点上去了。\n10.3.2. HA 镜像模式队列的不同策略 HA策略模式：\n同步至所有的 同步最多N个机器 只同步至符合指定名称的nodes 命令处理 HA 策略的语法：\n1 rabbitmqctl set_policy [-p Vhost] Name Pattern Definition [Priority] 为每个以 rock.wechat 开头的队列设置所有节点的镜像，并且设置为自动同步模式 1 2 rabbitmqctl set_policy ha-all \u0026#34;^rock.wechat\u0026#34; \u0026#39;{\u0026#34;ha-mode\u0026#34;:\u0026#34;all\u0026#34;,\u0026#34;ha-sync-mode\u0026#34;:\u0026#34;automatic\u0026#34;}\u0026#39; rabbitmqctl set_policy -p rock ha-all \u0026#34;^rock.wechat\u0026#34; \u0026#39;{\u0026#34;ha-mode\u0026#34;:\u0026#34;all\u0026#34;,\u0026#34;ha-sync-mode\u0026#34;:\u0026#34;automatic\u0026#34;}\u0026#39; 为每个以 rock.wechat. 开头的队列设置两个节点的镜像，并且设置为自动同步模式 1 2 rabbitmqctl set_policy -p rock ha-exacly \u0026#34;^rock.wechat\u0026#34; \\ \u0026#39;{\u0026#34;ha-mode\u0026#34;:\u0026#34;exactly\u0026#34;,\u0026#34;ha-params\u0026#34;:2,\u0026#34;ha-sync-mode\u0026#34;:\u0026#34;automatic\u0026#34;}\u0026#39; 为每个以 node. 开头的队列分配指定的节点做镜像 1 2 rabbitmqctl set_policy ha-nodes \u0026#34;^nodes\\.\u0026#34; \\ \u0026#39;{\u0026#34;ha-mode\u0026#34;:\u0026#34;nodes\u0026#34;,\u0026#34;ha-params\u0026#34;:[\u0026#34;rabbit@nodeA\u0026#34;, \u0026#34;rabbit@nodeB\u0026#34;]}\u0026#39; 10.3.3. 仲裁队列 仲裁队列，是 3.8 版本以后才有的新功能，用来替代镜像队列。具备下列特征：\n与镜像队列一样，都是主从模式，支持主从数据同步 使用非常简单，没有复杂的配置 主从同步基于 Raft 协议，强一致 1 2 3 4 5 6 7 @Bean public Queue quorumQueue() { return QueueBuilder .durable(\u0026#34;quorum.queue\u0026#34;) // 持久化 .quorum() // 仲裁队列 .build(); } 10.4. 部署总结 10.4.1. RabbitMQ 集群搭建注意事项 各节点之间使用 –link 连接，此属性不能忽略。 各节点使用的 erlang cookie 值必须相同，此值相当于“秘钥”的功能，用于各节点的认证。 整个集群中必须包含一个磁盘节点。 10.4.2. 集群中每个节点是其他节点的完整拷贝吗？ 集群中每个节点不是其他节点的完整拷贝，原因有以下两个：\n存储空间的考虑：如果每个节点都拥有所有队列的完全拷贝，这样新增节点不但没有新增存储空间，反而增加了更多的冗余数据； 性能的考虑：如果每条消息都需要完整拷贝到每一个集群节点，那新增节点并没有提升处理消息的能力，最多是保持和单节点相同的性能甚至是更糟。 10.4.3. RabbitMQ 集群中唯一一个磁盘节点崩溃后会发生的情况 如果唯一的磁盘节点崩溃了，集群是可以保持运行的，但不能更改任何东西。即不能进行以下操作：\n不能创建队列 不能创建交换器 不能创建绑定 不能添加用户 不能更改权限 不能添加和删除集群节点 10.4.4. 集群节点停止顺序要求 RabbitMQ 对集群的停止的顺序的要求是：应该先关闭内存节点，最后再关闭磁盘节点。如果顺序恰好相反的话，可能会造成消息的丢失。\n","permalink":"https://ktzxy.top/posts/3y56c204y4/","summary":"RabbitMQ","title":"RabbitMQ"},{"content":"使用二进制方式搭建K8S集群 注意 【暂时没有使用二进制方式搭建K8S集群，因此本章节内容不完整\u0026hellip;】\n准备工作 在开始之前，部署Kubernetes集群机器需要满足以下几个条件\n一台或多台机器，操作系统CentOS 7.x 硬件配置：2GB ，2个CPU，硬盘30GB 集群中所有机器之间网络互通 可以访问外网，需要拉取镜像，如果服务器不能上网，需要提前下载镜像导入节点 禁止swap分区 步骤 创建多台虚拟机，安装Linux系统 操作系统的初始化 为etcd 和 apiserver 自签证书 部署etcd集群 部署master组件【安装docker、kube-apiserver、kube-controller-manager、kube-scheduler、etcd】 部署node组件【安装kubelet、kube-proxy、docker、etcd】 部署集群网络 准备虚拟机 首先我们准备了两台虚拟机，来进行安装测试\n主机名 ip k8s_2_master 192.168.177.140 k8s_2_node 192.168.177.141 操作系统的初始化 然后我们需要进行一些系列的初始化操作\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 # 关闭防火墙 systemctl stop firewalld systemctl disable firewalld # 关闭selinux # 永久关闭 sed -i \u0026#39;s/enforcing/disabled/\u0026#39; /etc/selinux/config # 临时关闭 setenforce 0 # 关闭swap # 临时 swapoff -a # 永久关闭 sed -ri \u0026#39;s/.*swap.*/#\u0026amp;/\u0026#39; /etc/fstab # 根据规划设置主机名【master节点上操作】 hostnamectl set-hostname k8s_2_master # 根据规划设置主机名【node1节点操作】 hostnamectl set-hostname k8s_2_node1 # 在master添加hosts cat \u0026gt;\u0026gt; /etc/hosts \u0026lt;\u0026lt; EOF 192.168.177.140 k8s_2_master 192.168.177.141 k8s_2_node1 EOF # 将桥接的IPv4流量传递到iptables的链 cat \u0026gt; /etc/sysctl.d/k8s.conf \u0026lt;\u0026lt; EOF net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 EOF # 生效 sysctl --system # 时间同步 yum install ntpdate -y ntpdate time.windows.com 部署Etcd集群 Etcd是一个分布式键值存储系统，Kubernetes使用Etcd进行数据存储，所以先准备一个Etcd数据库，为了解决Etcd单点故障，应采用集群方式部署，这里使用3台组建集群，可容忍一台机器故障，当然也可以使用5台组件集群，可以容忍2台机器故障\n自签证书 提到证书，我们想到的就是下面这个情况\n这个https证书，其实就是服务器颁发给网站的，代表这是一个安全可信任的网站。\n而在我们K8S集群的内部，其实也是有证书的，如果不带证书，那么访问就会受限\n同时在集群内部 和 外部的访问，我们也需要签发证书\n如果我们使用二进制的方式，那么就需要自己手动签发证书。\n自签证书：我们可以想象成在一家公司上班，然后会颁发一个门禁卡，同时一般门禁卡有两种，一个是内部员工的门禁卡，和外部访客门禁卡。这两种门禁卡的权限可能不同，员工的门禁卡可以进入公司的任何地方，而访客的门禁卡是受限的，这个门禁卡其实就是自签证书\n准备cfssl证书生成工具 cfssl是一个开源的证书管理工具，使用json文件生成证书，相比openssl 更方便使用。找任意一台服务器操作，这里用Master节点。\n1 2 3 4 5 6 7 wget https://pkg.cfssl.org/R1.2/cfssl_linux-amd64 wget https://pkg.cfssl.org/R1.2/cfssljson_linux-amd64 wget https://pkg.cfssl.org/R1.2/cfssl-certinfo_linux-amd64 chmod +x cfssl_linux-amd64 cfssljson_linux-amd64 cfssl-certinfo_linux-amd64 mv cfssl_linux-amd64 /usr/local/bin/cfssl mv cfssljson_linux-amd64 /usr/local/bin/cfssljson mv cfssl-certinfo_linux-amd64 /usr/bin/cfssl-certinfo ","permalink":"https://ktzxy.top/posts/c3rcqrdjur/","summary":"4 使用二进制方式搭建K8S集群","title":"4 使用二进制方式搭建K8S集群"},{"content":"Go操作MySQL数据库 来源 http://moguit.cn/#/info?blogUid=3d1cc9ce434aeaf2187692eb0feea294\nhttps://www.liwenzhou.com/posts/Go/go_mysql/\n前言 常见的数据库有\nSqlLite MySQL SQLServer postgreSQL Oracle MySQL主流的关系型数据库，类似的还有postgreSQL\n关系型数据库：用表来存储一类的数据\n表结构设计的三大范式：《漫画数据库》\nMySQL知识点 SQL语句 DDL：操作数据库的\nDML：表的增删改查\nDCL：用户及权限\n存储引擎 MySQL支持插件式的存储引擎\n常见的存储引擎：MyISAM和InnoDB\nMyISAM： 查询快 只支持表锁 不支持事务 InnoDB 整体速度快 支持表锁和行锁 事务 把多个SQL操作当成是一个整体\n事务的特点 ACID就是事务的特性\n原子性：事务要么成功要么失败，没有中间操作 一致性：数据库的完整性没有被破坏 隔离性：事务之间是相互隔离的 持久性：事务操作完成后，是持久化到数据库的，不会再次改变 索引 索引的原理是：B树和B+树\n索引的类型和索引的命中\n其它内容 分库分表\nSQL注入\nSQL慢查询优化\nMySQL主从\nMySQL读写分离\nGo操作数据库 Go语言中的database/sql包提供了保证SQL或类SQL数据库的泛用接口，并不提供具体的数据库驱动。使用database/sql包时必须注入（至少）一个数据库驱动。\n我们常用的数据库基本上都有完整的第三方实现。例如：MySQL驱动\ndatabase/sql 原生支持连接池，是并发安全的\n这个标准库没有具体的实现，只是列出一些需要第三方库实现的具体内容\n下载依赖 首先我们需要使用go mod命令初始化项目\n1 go mod init GoAdvanceCode 执行完成后，会在项目的根目录下生成一个go.mod的文件，以后我们添加的依赖，就会在这里显示出来\n然后下载数据库依赖\n1 go get -u github.com/go-sql-driver/mysql go get包的路径就是下载第三方的依赖，将第三方的依赖默认保存在 $GOPATH/src\n使用MySQL驱动 导入刚刚引入的包\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 package main import ( \u0026#34;database/sql\u0026#34; \u0026#34;fmt\u0026#34; _\u0026#34;github.com/go-sql-driver/mysql\u0026#34; ) // 定义一个全局的DB，是一个连接池对象 var db *sql.DB func initDB()(err error) { // 连接数据库 dsn := \u0026#34;root:root@tcp(127.0.0.1:3306)/mogu_demo\u0026#34; // 连接MySQL数据库（注意不能使用 := ） db, err = sql.Open(\u0026#34;mysql\u0026#34;, dsn) if err != nil { fmt.Printf(\u0026#34;open %s failed, err: %v \\n\u0026#34;, dsn, err) return } // 尝试连接数据库 err = db.Ping() if err != nil { fmt.Printf(\u0026#34;open %s failed, err: %v, \\n\u0026#34;, dsn, err) return } fmt.Println(\u0026#34;连接数据库成功\u0026#34;) return } type user struct { id string name string age int } // 查询操作 func query() { sqlStr := \u0026#34;select id, name, age from user where id \u0026gt; ?\u0026#34; rows, err := db.Query(sqlStr, 0) if err != nil { fmt.Println() fmt.Printf(\u0026#34;query failed, err:%v\\n\u0026#34;, err) return } // 非常重要：关闭rows释放持有的数据库链接 defer rows.Close() // 循环读取结果集中的数据 for rows.Next() { var u user err := rows.Scan(\u0026amp;u.id, \u0026amp;u.name, \u0026amp;u.age) if err != nil { fmt.Printf(\u0026#34;scan failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;id:%d name:%s age:%d\\n\u0026#34;, u.id, u.name, u.age) } } // Go连接MySQL func main() { err := initDB() if err != nil { fmt.Println(\u0026#34;数据库初始化失败\u0026#34;) } // 查询单条记录 query() } 其中sql.DB是表示连接的数据库对象（结构体实例），它保存了连接数据库相关的所有信息。它内部维护着一个具有零到多个底层连接的连接池，它可以安全地被多个goroutine同时使用。\nSetMaxOpenConns 1 func (db *DB) SetMaxOpenConns(n int) SetMaxOpenConns设置与数据库建立连接的最大数目。 如果n大于0且小于最大闲置连接数，会将最大闲置连接数减小到匹配最大开启连接数的限制。 如果n\u0026lt;=0，不会限制最大开启连接数，默认为0（无限制）。\n需要注意的是，我们再查询完成后，需要使用Scan进行连接的释放\n1 2 3 4 5 6 // 调用Scan才会释放我们的连接 err := rows.Scan(\u0026amp;u.id, \u0026amp;u.name, \u0026amp;u.age) if err != nil { fmt.Printf(\u0026#34;scan failed, err:%v\\n\u0026#34;, err) return } SetMaxIdleConns 1 func (db *DB) SetMaxIdleConns(n int) SetMaxIdleConns设置连接池中的最大闲置连接数。 如果n大于最大开启连接数，则新的最大闲置连接数会减小到匹配最大开启连接数的限制。 如果n\u0026lt;=0，不会保留闲置连接。\nCRUD 建库建表 我们先在MySQL中创建一个名为sql_test的数据库\n1 CREATE DATABASE sql_test; 进入该数据库:\n1 use sql_test; 执行以下命令创建一张用于测试的数据表：\n1 2 3 4 5 6 CREATE TABLE `user` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `name` VARCHAR(20) DEFAULT \u0026#39;\u0026#39;, `age` INT(11) DEFAULT \u0026#39;0\u0026#39;, PRIMARY KEY(`id`) )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; 查询 为了方便查询，我们事先定义好一个结构体来存储user表的数据。\n1 2 3 4 5 type user struct { id int age int name string } 单行查询 单行查询db.QueryRow()执行一次查询，并期望返回最多一行结果（即Row）。QueryRow总是返回非nil的值，直到返回值的Scan方法被调用时，才会返回被延迟的错误。（如：未找到结果）\n1 func (db *DB) QueryRow(query string, args ...interface{}) *Row 具体示例代码：\n1 2 3 4 5 6 7 8 9 10 11 12 // 查询单条数据示例 func queryRowDemo() { sqlStr := \u0026#34;select id, name, age from user where id=?\u0026#34; var u user // 非常重要：确保QueryRow之后调用Scan方法，否则持有的数据库链接不会被释放 err := db.QueryRow(sqlStr, 1).Scan(\u0026amp;u.id, \u0026amp;u.name, \u0026amp;u.age) if err != nil { fmt.Printf(\u0026#34;scan failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;id:%d name:%s age:%d\\n\u0026#34;, u.id, u.name, u.age) } 多行查询 多行查询db.Query()执行一次查询，返回多行结果（即Rows），一般用于执行select命令。参数args表示query中的占位参数。\n1 func (db *DB) Query(query string, args ...interface{}) (*Rows, error) 具体示例代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 查询多条数据示例 func queryMultiRowDemo() { sqlStr := \u0026#34;select id, name, age from user where id \u0026gt; ?\u0026#34; rows, err := db.Query(sqlStr, 0) if err != nil { fmt.Printf(\u0026#34;query failed, err:%v\\n\u0026#34;, err) return } // 非常重要：关闭rows释放持有的数据库链接 defer rows.Close() // 循环读取结果集中的数据 for rows.Next() { var u user err := rows.Scan(\u0026amp;u.id, \u0026amp;u.name, \u0026amp;u.age) if err != nil { fmt.Printf(\u0026#34;scan failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;id:%d name:%s age:%d\\n\u0026#34;, u.id, u.name, u.age) } } 插入数据 插入、更新和删除操作都使用Exec方法。\n1 func (db *DB) Exec(query string, args ...interface{}) (Result, error) Exec执行一次命令（包括查询、删除、更新、插入等），返回的Result是对已执行的SQL命令的总结。参数args表示query中的占位参数。\n具体插入数据示例代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 插入数据 func insertRowDemo() { sqlStr := \u0026#34;insert into user(name, age) values (?,?)\u0026#34; ret, err := db.Exec(sqlStr, \u0026#34;王五\u0026#34;, 38) if err != nil { fmt.Printf(\u0026#34;insert failed, err:%v\\n\u0026#34;, err) return } theID, err := ret.LastInsertId() // 新插入数据的id if err != nil { fmt.Printf(\u0026#34;get lastinsert ID failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;insert success, the id is %d.\\n\u0026#34;, theID) } 更新数据 具体更新数据示例代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 更新数据 func updateRowDemo() { sqlStr := \u0026#34;update user set age=? where id = ?\u0026#34; ret, err := db.Exec(sqlStr, 39, 3) if err != nil { fmt.Printf(\u0026#34;update failed, err:%v\\n\u0026#34;, err) return } n, err := ret.RowsAffected() // 操作影响的行数 if err != nil { fmt.Printf(\u0026#34;get RowsAffected failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;update success, affected rows:%d\\n\u0026#34;, n) } 删除数据 具体删除数据的示例代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 删除数据 func deleteRowDemo() { sqlStr := \u0026#34;delete from user where id = ?\u0026#34; ret, err := db.Exec(sqlStr, 3) if err != nil { fmt.Printf(\u0026#34;delete failed, err:%v\\n\u0026#34;, err) return } n, err := ret.RowsAffected() // 操作影响的行数 if err != nil { fmt.Printf(\u0026#34;get RowsAffected failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;delete success, affected rows:%d\\n\u0026#34;, n) } MySQL预处理 什么是预处理？ 普通SQL语句执行过程：\n客户端对SQL语句进行占位符替换得到完整的SQL语句。 客户端发送完整SQL语句到MySQL服务端 MySQL服务端执行完整的SQL语句并将结果返回给客户端。 预处理执行过程：\n把SQL语句分成两部分，命令部分与数据部分。 先把命令部分发送给MySQL服务端，MySQL服务端进行SQL预处理。 然后把数据部分发送给MySQL服务端，MySQL服务端对SQL语句进行占位符替换。 MySQL服务端执行完整的SQL语句并将结果返回给客户端。 为什么要预处理？ 优化MySQL服务器重复执行SQL的方法，可以提升服务器性能，提前让服务器编译，一次编译多次执行，节省后续编译的成本。 避免SQL注入问题。 Go实现MySQL预处理 database/sql中使用下面的Prepare方法来实现预处理操作。\n1 func (db *DB) Prepare(query string) (*Stmt, error) Prepare方法会先将sql语句发送给MySQL服务端，返回一个准备好的状态用于之后的查询和命令。返回值可以同时执行多个查询和命令。\n查询操作的预处理示例代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // 预处理查询示例 func prepareQueryDemo() { sqlStr := \u0026#34;select id, name, age from user where id \u0026gt; ?\u0026#34; stmt, err := db.Prepare(sqlStr) if err != nil { fmt.Printf(\u0026#34;prepare failed, err:%v\\n\u0026#34;, err) return } defer stmt.Close() rows, err := stmt.Query(0) if err != nil { fmt.Printf(\u0026#34;query failed, err:%v\\n\u0026#34;, err) return } defer rows.Close() // 循环读取结果集中的数据 for rows.Next() { var u user err := rows.Scan(\u0026amp;u.id, \u0026amp;u.name, \u0026amp;u.age) if err != nil { fmt.Printf(\u0026#34;scan failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;id:%d name:%s age:%d\\n\u0026#34;, u.id, u.name, u.age) } } 插入、更新和删除操作的预处理十分类似，这里以插入操作的预处理为例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 // 预处理插入示例 func prepareInsertDemo() { sqlStr := \u0026#34;insert into user(name, age) values (?,?)\u0026#34; stmt, err := db.Prepare(sqlStr) if err != nil { fmt.Printf(\u0026#34;prepare failed, err:%v\\n\u0026#34;, err) return } defer stmt.Close() _, err = stmt.Exec(\u0026#34;小王子\u0026#34;, 18) if err != nil { fmt.Printf(\u0026#34;insert failed, err:%v\\n\u0026#34;, err) return } _, err = stmt.Exec(\u0026#34;沙河娜扎\u0026#34;, 18) if err != nil { fmt.Printf(\u0026#34;insert failed, err:%v\\n\u0026#34;, err) return } fmt.Println(\u0026#34;insert success.\u0026#34;) } SQL注入问题 我们任何时候都不应该自己拼接SQL语句！\n这里我们演示一个自行拼接SQL语句的示例，编写一个根据name字段查询user表的函数如下：\n1 2 3 4 5 6 7 8 9 10 11 12 // sql注入示例 func sqlInjectDemo(name string) { sqlStr := fmt.Sprintf(\u0026#34;select id, name, age from user where name=\u0026#39;%s\u0026#39;\u0026#34;, name) fmt.Printf(\u0026#34;SQL:%s\\n\u0026#34;, sqlStr) var u user err := db.QueryRow(sqlStr).Scan(\u0026amp;u.id, \u0026amp;u.name, \u0026amp;u.age) if err != nil { fmt.Printf(\u0026#34;exec failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;user:%#v\\n\u0026#34;, u) } 此时以下输入字符串都可以引发SQL注入问题：\n1 2 3 sqlInjectDemo(\u0026#34;xxx\u0026#39; or 1=1#\u0026#34;) sqlInjectDemo(\u0026#34;xxx\u0026#39; union select * from user #\u0026#34;) sqlInjectDemo(\u0026#34;xxx\u0026#39; and (select count(*) from user) \u0026lt;10 #\u0026#34;) **补充：**不同的数据库中，SQL语句使用的占位符语法不尽相同。\n数据库 占位符语法 MySQL ? PostgreSQL $1, $2等 SQLite ? 和$1 Oracle :name Go实现MySQL事务 什么是事务？ 事务：一个最小的不可再分的工作单元；通常一个事务对应一个完整的业务(例如银行账户转账业务，该业务就是一个最小的工作单元)，同时这个完整的业务需要执行多次的DML(insert、update、delete)语句共同联合完成。A转账给B，这里面就需要执行两次update操作。\n在MySQL中只有使用了Innodb数据库引擎的数据库或表才支持事务。事务处理可以用来维护数据库的完整性，保证成批的SQL语句要么全部执行，要么全部不执行。\n事务的ACID 通常事务必须满足4个条件（ACID）：原子性（Atomicity，或称不可分割性）、一致性（Consistency）、隔离性（Isolation，又称独立性）、持久性（Durability）。\n条件 解释 原子性 一个事务（transaction）中的所有操作，要么全部完成，要么全部不完成，不会结束在中间某个环节。事务在执行过程中发生错误，会被回滚（Rollback）到事务开始前的状态，就像这个事务从来没有执行过一样。 一致性 在事务开始之前和事务结束以后，数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则，这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。 隔离性 数据库允许多个并发事务同时对其数据进行读写和修改的能力，隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别，包括读未提交（Read uncommitted）、读提交（read committed）、可重复读（repeatable read）和串行化（Serializable）。 持久性 事务处理结束后，对数据的修改就是永久的，即便系统故障也不会丢失。 事务相关方法 Go语言中使用以下三个方法实现MySQL中的事务操作。 开始事务\n1 func (db *DB) Begin() (*Tx, error) 提交事务\n1 func (tx *Tx) Commit() error 回滚事务\n1 func (tx *Tx) Rollback() error 事务示例 下面的代码演示了一个简单的事务操作，该事物操作能够确保两次更新操作要么同时成功要么同时失败，不会存在中间状态。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 // 事务操作示例 func transactionDemo() { tx, err := db.Begin() // 开启事务 if err != nil { if tx != nil { tx.Rollback() // 回滚 } fmt.Printf(\u0026#34;begin trans failed, err:%v\\n\u0026#34;, err) return } sqlStr1 := \u0026#34;Update user set age=30 where id=?\u0026#34; ret1, err := tx.Exec(sqlStr1, 2) if err != nil { tx.Rollback() // 回滚 fmt.Printf(\u0026#34;exec sql1 failed, err:%v\\n\u0026#34;, err) return } affRow1, err := ret1.RowsAffected() if err != nil { tx.Rollback() // 回滚 fmt.Printf(\u0026#34;exec ret1.RowsAffected() failed, err:%v\\n\u0026#34;, err) return } sqlStr2 := \u0026#34;Update user set age=40 where id=?\u0026#34; ret2, err := tx.Exec(sqlStr2, 3) if err != nil { tx.Rollback() // 回滚 fmt.Printf(\u0026#34;exec sql2 failed, err:%v\\n\u0026#34;, err) return } affRow2, err := ret2.RowsAffected() if err != nil { tx.Rollback() // 回滚 fmt.Printf(\u0026#34;exec ret1.RowsAffected() failed, err:%v\\n\u0026#34;, err) return } fmt.Println(affRow1, affRow2) if affRow1 == 1 \u0026amp;\u0026amp; affRow2 == 1 { fmt.Println(\u0026#34;事务提交啦...\u0026#34;) tx.Commit() // 提交事务 } else { tx.Rollback() fmt.Println(\u0026#34;事务回滚啦...\u0026#34;) } fmt.Println(\u0026#34;exec trans success!\u0026#34;) } 更强大、更好用的sqlx库\n练习题 结合net/http和database/sql实现一个使用MySQL存储用户信息的注册及登陆的简易web程序。 ","permalink":"https://ktzxy.top/posts/yh7qgocdt2/","summary":"Go操作数据库","title":"Go操作数据库"},{"content":"在计算机世界里“数据结构+算法=程序”，因此算法在程序开发中起着至关重要的作用。\n目录\n[TOC]\n1. 初识算法 1.1. 什么是算法？ 算法的定义：在数学和计算机科学领域，算法是一系列有限的严谨指令，通常用于解决一类特定问题或执行计算。\nIn mathematics and computer science, an algorithm (/ˈælɡərɪðəm/) is a finite sequence of rigorous instructions, typically used to solve a class of specific problems or to perform a computation.\n\u0026ndash; 参考文献：\u0026ldquo;Definition of ALGORITHM\u0026rdquo;. Merriam-Webster Online Dictionary. Archived from the original on February 14, 2020. Retrieved November 14, 2019.\nIntroduction to Algorithm（中文译作《算法导论》）\n不正式的说，算法就是任何定义优良的计算过程：接收一些值作为输入，在有限的时间内，产生一些值作为输出。\nInformally, an algorithm is any well-defined computational procedure that takes some value, or set of values, as input and produces some value, or set of values, as output in a finite amount of time.\n1.2. 算法分类 1.2.1. 排序算法 一些常用的排序算法。\n基数排序 冒泡排序 选择排序 归并排序 堆排序 快速排序 直接插入排序 希尔排序 拓扑排序 注：以下代码案例，存放在pyg-test工程中sort-test模块中\n1.2.2. 逻辑处理算法 递归算法 2. 算法复杂度分析（整理中） 算法复杂度分析是用于判断代码的执行性能好坏。包含以下两个内容：\n时间复杂度 空间复杂度 Tips: 通常情况下说复杂度，都是指时间复杂度\n2.1. 时间复杂度 2.1.1. 常见复杂度表示形式 速记口诀：常对幂指阶。越在上面的性能就越高，越往下性能就越低。下图是一些比较常见时间复杂度的时间与数据规模的趋势：\n2.2. 空间复杂度 空间复杂度全称是渐进空间复杂度，表示算法占用的额外存储空间与数据规模之间的增长关系。\n3. 递归 3.1. 递归概念 递归，指在当前方法内调用自己的这种现象\n3.2. 递归分类 直接递归：方法A调用方法A。 间接递归：A 方法调用 B 方法，B 方法调用 C 方法，C 方法调用 A 方法。（间接递归实际开发中比较少用。） 3.3. 递归注意事项 递归一定要有出口；要有结束递归的条件。 递归次数不能太多。 构造方法中不能使用递归。 递归算法：方法自身调用方法自身，必须有方法出口(可以结束方法的条件)，递归次数不宜过多，会有 stackoverflow (栈内存溢出错误)。\n3.4. 递归扩展知识 递归是程序控制的另一种形式，实质上就是不用循环控制的重复\n递归会产生相当大的系统开销，它耗费了太多时间并占用了太多的内存。有些本质上有递归特性的问题用其他方法很难解决。如果迭代能解决的方案，就使用迭代。迭代通常比递归效率更高\n递归方法是一个直接或间接调用自己的方法。要终止一个递归方法，必须有一个或多个基础情况（程序出口）。\n4. 基数排序 4.1. 要点 基数排序与本系列前面讲解的七种排序方法都不同，它不需要比较关键字的大小。\n它是根据关键字中各位的值，通过对排序的N个元素进行若干趟“分配”与“收集”来实现排序的。\n不妨通过一个具体的实例来展示一下，基数排序是如何进行的。 设有一个初始序列为: R {50, 123, 543, 187, 49, 30, 0, 2, 11, 100}。\n我们知道，任何一个阿拉伯数，它的各个位数上的基数都是以09来表示的。所以我们不妨把09视为10个桶。\n我们先根据序列的个位数的数字来进行分类，将其分到指定的桶中。例如：R[0] = 50，个位数上是0，将这个数存入编号为0的桶中。\n分类后，我们在从各个桶中，将这些数按照从编号0到编号9的顺序依次将所有数取出来。这时，得到的序列就是个位数上呈递增趋势的序列。\n按照个位数排序： {50, 30, 0, 100, 11, 2, 123, 543, 187, 49}。接下来，可以对十位数、百位数也按照这种方法进行排序，最后就能得到排序完成的序列。\n4.2. LSD法实现（完整参考代码） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 package com.moon.demo.sort; /** * 基数排序算法 */ public class RadixSort { // 获取x这个数的d位数上的数字 // 比如获取123的1位数，结果返回3 public int getDigit(int x, int d) { int a[] = {1, 1, 10, 100}; // 本实例中的最大数是百位数，所以只要到100就可以了 return ((x / a[d]) % 10); } public void radixSort(int[] list, int begin, int end, int digit) { final int radix = 10; // 基数 int i = 0, j = 0; int[] count = new int[radix]; // 存放各个桶的数据统计个数 int[] bucket = new int[end - begin + 1]; // 按照从低位到高位的顺序执行排序过程 for (int d = 1; d \u0026lt;= digit; d++) { // 置空各个桶的数据统计 for (i = 0; i \u0026lt; radix; i++) { count[i] = 0; } // 统计各个桶将要装入的数据个数 for (i = begin; i \u0026lt;= end; i++) { j = getDigit(list[i], d); count[j]++; } // count[i]表示第i个桶的右边界索引 for (i = 1; i \u0026lt; radix; i++) { count[i] = count[i] + count[i - 1]; } // 将数据依次装入桶中 // 这里要从右向左扫描，保证排序稳定性 for (i = end; i \u0026gt;= begin; i--) { j = getDigit(list[i], d); // 求出关键码的第k位的数字， 例如：576的第3位是5 bucket[count[j] - 1] = list[i]; // 放入对应的桶中，count[j]-1是第j个桶的右边界索引 count[j]--; // 对应桶的装入数据索引减一 } // 将已分配好的桶中数据再倒出来，此时已是对应当前位数有序的表 for (i = begin, j = 0; i \u0026lt;= end; i++, j++) { list[i] = bucket[j]; } } } public int[] sort(int[] list) { radixSort(list, 0, list.length - 1, 3); return list; } // 打印完整序列 public void printAll(int[] list) { for (int value : list) { System.out.print(value + \u0026#34;\\t\u0026#34;); } System.out.println(); } public static void main(String[] args) { int[] array = {50, 123, 543, 187, 49, 30, 0, 2, 11, 100}; RadixSort radix = new RadixSort(); System.out.print(\u0026#34;排序前:\\t\\t\u0026#34;); radix.printAll(array); radix.sort(array); System.out.print(\u0026#34;排序后:\\t\\t\u0026#34;); radix.printAll(array); } } 运行结果\n排序前: 50 123 543 187 49 30 0 2 11 100\n排序后: 0 2 11 30 49 50 100 123 187 543\n4.3. 算法分析 基数排序的性能\n时间复杂度\n通过上文可知，假设在基数排序中，r为基数，d为位数。则基数排序的时间复杂度为O(d(n+r))。我们可以看出，基数排序的效率和初始序列是否有序没有关联。\n空间复杂度\n在基数排序过程中，对于任何位数上的基数进行“装桶”操作时，都需要n+r个临时空间。\n算法稳定性\n在基数排序过程中，每次都是将当前位数上相同数值的元素统一“装桶”，并不需要交换位置。所以基数排序是稳定的算法。\n5. 冒泡排序 5.1. 冒泡排序介绍 冒泡排序(Bubble Sort)，又被称为气泡排序或泡沫排序。\n它是一种较简单的排序算法。它会遍历若干次要排序的数列，每次遍历时，它都会从前往后依次的比较相邻两个数的大小；如果前者比后者大，则交换它们的位置。这样，一次遍历之后，最大的元素就在数列的末尾！ 采用相同的方法再次遍历时，第二大的元素就被排列在最大元素之前。重复此操作，直到整个数列都有序为止！\n原理：比较两个相邻的元素，将值大的元素交换至右端\n5.2. 自己写的冒泡排序demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import java.util.Arrays; public class MoonZero { public static void main(String[] args) { int[] arr = { 6, 3, 8, 9, 2, 7 }; System.out.println(Arrays.toString(arr)); // 冒泡排序 升序排列,如果降序就将if的判断改在(arr[j]\u0026lt;arr[j+1]) for (int i = 0; i \u0026lt; arr.length - 1; i++) { for (int j = 0; j + 1 \u0026lt; arr.length - i; j++) { if (arr[j] \u0026gt; arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } System.out.println(Arrays.toString(arr)); } } 5.3. 冒泡排序图文说明 5.3.1. 冒泡排序实现一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static void bubbleSort1(int[] a, int n) { int i, j; for (i = n - 1; i \u0026gt; 0; i--) { // 将a[0...i]中最大的数据放在末尾 for (j = 0; j \u0026lt; i; j++) { if (a[j] \u0026gt; a[j + 1]) { // 交换a[j]和a[j+1] int tmp = a[j]; a[j] = a[j + 1]; a[j + 1] = tmp; } } } } 下面以数列{20,40,30,10,60,50}为例，演示它的冒泡排序过程(如下图)。\n先分析第1趟排序\n当i=5,j=0时，a[0]\u0026lt;a[1]。此时，不做任何处理！ 当i=5,j=1时，a[1]\u0026gt;a[2]。此时，交换a[1]和a[2]的值；交换之后，a[1]=30，a[2]=40。 当i=5,j=2时，a[2]\u0026gt;a[3]。此时，交换a[2]和a[3]的值；交换之后，a[2]=10，a[3]=40。 当i=5,j=3时，a[3]\u0026lt;a[4]。此时，不做任何处理！ 当i=5,j=4时，a[4]\u0026gt;a[5]。此时，交换a[4]和a[5]的值；交换之后，a[4]=50，a[3]=60。 于是，第1趟排序完之后，数列{20,40,30,10,60,50}变成了{20,30,10,40,50,60}。此时，数列末尾的值最大。\n根据这种方法：\n第2趟排序完之后，数列中a[5\u0026hellip;6]是有序的。 第3趟排序完之后，数列中a[4\u0026hellip;6]是有序的。 第4趟排序完之后，数列中a[3\u0026hellip;6]是有序的。 第5趟排序完之后，数列中a[1\u0026hellip;6]是有序的。 第5趟排序之后，整个数列也就是有序的了。\n5.3.2. 冒泡排序实现二 观察上面冒泡排序的流程图，第3趟排序之后，数据已经是有序的了；第4趟和第5趟并没有进行数据交换。\n下面我们对冒泡排序进行优化，使它效率更高一些：添加一个标记，如果一趟遍历中发生了交换，则标记为true，否则为false。如果某一趟没有发生交换，说明排序已经完成！\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public static void bubbleSort2(int[] a, int n) { int i, j; // 标记 int flag; for (i = n - 1; i \u0026gt; 0; i--) { // 初始化标记为0 flag = 0; // 将a[0...i]中最大的数据放在末尾 for (j = 0; j \u0026lt; i; j++) { if (a[j] \u0026gt; a[j + 1]) { // 交换a[j]和a[j+1] int tmp = a[j]; a[j] = a[j + 1]; a[j + 1] = tmp; // 若发生交换，则设标记为1 flag = 1; } } if (flag == 0) { // 若没发生交换，则说明数列已有序。 break; } } } 5.4. 冒泡排序的时间复杂度和稳定性 冒泡排序时间复杂度\n冒泡排序的时间复杂度是O(N2)。\n假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N)，需要遍历多少次呢？N-1次！因此，冒泡排序的时间复杂度是O(N2)。\n冒泡排序稳定性\n冒泡排序是稳定的算法，它满足稳定算法的定义。\n算法稳定性 \u0026ndash; 假设在数列中存在a[i]=a[j]，若在排序之前，a[i]在a[j]前面；并且排序之后，a[i]仍然在a[j]前面。则这个排序算法是稳定的！\n5.5. 冒泡排序实现 5.5.1. 冒泡排序Java实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 package com.moon.demo.sort; /** * 冒泡排序算法 */ public class BubbleSort { /* * 冒泡排序 * * 参数说明： * a -- 待排序的数组 * n -- 数组的长度 */ public static void bubbleSort1(int[] a, int n) { int i, j; for (i = n - 1; i \u0026gt; 0; i--) { // 将a[0...i]中最大的数据放在末尾 for (j = 0; j \u0026lt; i; j++) { if (a[j] \u0026gt; a[j + 1]) { // 交换a[j]和a[j+1] int tmp = a[j]; a[j] = a[j + 1]; a[j + 1] = tmp; } } } } /* * 冒泡排序(改进版) * * 参数说明： * a -- 待排序的数组 * n -- 数组的长度 */ public static void bubbleSort2(int[] a, int n) { int i, j; // 标记 int flag; for (i = n - 1; i \u0026gt; 0; i--) { // 初始化标记为0 flag = 0; // 将a[0...i]中最大的数据放在末尾 for (j = 0; j \u0026lt; i; j++) { if (a[j] \u0026gt; a[j + 1]) { // 交换a[j]和a[j+1] int tmp = a[j]; a[j] = a[j + 1]; a[j + 1] = tmp; // 若发生交换，则设标记为1 flag = 1; } } if (flag == 0) { // 若没发生交换，则说明数列已有序。 break; } } } public static void main(String[] args) { int i; int[] a = {20, 40, 30, 10, 60, 50}; System.out.printf(\u0026#34;before sort:\u0026#34;); for (i = 0; i \u0026lt; a.length; i++) { System.out.printf(\u0026#34;%d \u0026#34;, a[i]); } System.out.printf(\u0026#34;\\n\u0026#34;); bubbleSort1(a, a.length); //bubbleSort2(a, a.length); System.out.printf(\u0026#34;after sort:\u0026#34;); for (i = 0; i \u0026lt; a.length; i++) { System.out.printf(\u0026#34;%d \u0026#34;, a[i]); } System.out.printf(\u0026#34;\\n\u0026#34;); } } 5.5.2. 冒泡排序C实现（了解） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 /** * 冒泡排序：C 语言 */ #include \u0026lt;stdio.h\u0026gt; // 数组长度 #define LENGTH(array) ( (sizeof(array)) / (sizeof(array[0])) ) // 交互数值 #define swap(a,b) (a^=b,b^=a,a^=b) /* * 冒泡排序 * * 参数说明： * a -- 待排序的数组 * n -- 数组的长度 */ void bubble_sort1(int a[], int n) { int i,j; for (i=n-1; i\u0026gt;0; i--) { // 将a[0...i]中最大的数据放在末尾 for (j=0; j\u0026lt;i; j++) { if (a[j] \u0026gt; a[j+1]) swap(a[j], a[j+1]); } } } /* * 冒泡排序(改进版) * * 参数说明： * a -- 待排序的数组 * n -- 数组的长度 */ void bubble_sort2(int a[], int n) { int i,j; int flag; // 标记 for (i=n-1; i\u0026gt;0; i--) { flag = 0; // 初始化标记为0 // 将a[0...i]中最大的数据放在末尾 for (j=0; j\u0026lt;i; j++) { if (a[j] \u0026gt; a[j+1]) { swap(a[j], a[j+1]); flag = 1; // 若发生交换，则设标记为1 } } if (flag==0) break; // 若没发生交换，则说明数列已有序。 } } void main() { int i; int a[] = {20,40,30,10,60,50}; int ilen = LENGTH(a); printf(\u0026#34;before sort:\u0026#34;); for (i=0; i\u0026lt;ilen; i++) printf(\u0026#34;%d \u0026#34;, a[i]); printf(\u0026#34;\\n\u0026#34;); bubble_sort1(a, ilen); //bubble_sort2(a, ilen); printf(\u0026#34;after sort:\u0026#34;); for (i=0; i\u0026lt;ilen; i++) printf(\u0026#34;%d \u0026#34;, a[i]); printf(\u0026#34;\\n\u0026#34;); } 5.5.3. 冒泡排序C++实现（了解） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 ** * 冒泡排序：C++ */ #include \u0026lt;iostream\u0026gt; using namespace std; /* * 冒泡排序 * * 参数说明： * a -- 待排序的数组 * n -- 数组的长度 */ void bubbleSort1(int* a, int n) { int i,j,tmp; for (i=n-1; i\u0026gt;0; i--) { // 将a[0...i]中最大的数据放在末尾 for (j=0; j\u0026lt;i; j++) { if (a[j] \u0026gt; a[j+1]) { // 交换a[j]和a[j+1] tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; } } } } /* * 冒泡排序(改进版) * * 参数说明： * a -- 待排序的数组 * n -- 数组的长度 */ void bubbleSort2(int* a, int n) { int i,j,tmp; int flag; // 标记 for (i=n-1; i\u0026gt;0; i--) { flag = 0; // 初始化标记为0 // 将a[0...i]中最大的数据放在末尾 for (j=0; j\u0026lt;i; j++) { if (a[j] \u0026gt; a[j+1]) { // 交换a[j]和a[j+1] tmp = a[j]; a[j] = a[j+1]; a[j+1] = tmp; flag = 1; // 若发生交换，则设标记为1 } } if (flag==0) break; // 若没发生交换，则说明数列已有序。 } } int main() { int i; int a[] = {20,40,30,10,60,50}; int ilen = (sizeof(a)) / (sizeof(a[0])); cout \u0026lt;\u0026lt; \u0026#34;before sort:\u0026#34;; for (i=0; i\u0026lt;ilen; i++) cout \u0026lt;\u0026lt; a[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; endl; bubbleSort1(a, ilen); //bubbleSort2(a, ilen); cout \u0026lt;\u0026lt; \u0026#34;after sort:\u0026#34;; for (i=0; i\u0026lt;ilen; i++) cout \u0026lt;\u0026lt; a[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; endl; return 0; } 上面3种实现的原理和输出结果都是一样的。下面是它们的输出结果：\n1 2 before sort:20 40 30 10 60 50 after sort:10 20 30 40 50 60 6. 选择排序 6.1. 选择排序介绍 择排序(Selection sort)是一种简单直观的排序算法。\n它的基本思想是：首先在未排序的数列中找到最小(or最大)元素，然后将其存放到数列的起始位置；接着，再从剩余未排序的元素中继续寻找最小(or最大)元素，然后放到已排序序列的末尾。以此类推，直到所有元素均排序完毕。\n原理：首先在未排序序列中找到最小（大）元素，存放到排序序列的起始位置，然后，再从剩余未排序元素中继续寻找最小（大）元素，然后放到已排序序列的末尾。以此类推，直到所有元素均排序完毕。\n6.2. 自己写的选择排序demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.util.Arrays; public class MoonZero { public static void main(String[] args) { int[] arr = { 6, 3, 8, 9, 2, 7 }; System.out.println(Arrays.toString(arr)); // 选择排序 for (int i = 0; i \u0026lt; arr.length - 1; i++) { for (int j = i+1; j \u0026lt; arr.length; j++) { // 选择排序如果判断“\u0026gt;”就是升序，“\u0026lt;”就是降序 if (arr[i] \u0026gt; arr[j]) { int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } } System.out.println(Arrays.toString(arr)); } } 6.3. 选择排序图文说明 下面以数列{20,40,30,10,60,50}为例，演示它的选择排序过程(如下图)。\n6.3.1. 排序流程 第1趟：i=0。找出a[1\u0026hellip;5]中的最小值a[3]=10，然后将a[0]和a[3]互换。 数列变化：20,40,30,10,60,50 \u0026ndash; \u0026gt; 10,40,30,20,60,50 第2趟：i=1。找出a[2\u0026hellip;5]中的最小值a[3]=20，然后将a[1]和a[3]互换。 数列变化：10,40,30,20,60,50 \u0026ndash; \u0026gt; 10,20,30,40,60,50 第3趟：i=2。找出a[3\u0026hellip;5]中的最小值，由于该最小值大于a[2]，该趟不做任何处理。 第4趟：i=3。找出a[4\u0026hellip;5]中的最小值，由于该最小值大于a[3]，该趟不做任何处理。 第5趟：i=4。交换a[4]和a[5]的数据。 数列变化：10,20,30,40,60,50 \u0026ndash; \u0026gt; 10,20,30,40,50,60 6.3.2. 选择排序的时间复杂度和稳定性 选择排序时间复杂度\n选择排序的时间复杂度是O(N2)。\n假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N)，需要遍历多少次呢？N-1！因此，选择排序的时间复杂度是O(N2)。\n选择排序稳定性\n选择排序是稳定的算法，它满足稳定算法的定义。\n算法稳定性 \u0026ndash; 假设在数列中存在a[i]=a[j]，若在排序之前，a[i]在a[j]前面；并且排序之后，a[i]仍然在a[j]前面。则这个排序算法是稳定的！\n6.4. 选择排序实现 6.4.1. 选择排序Java实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.moon.demo.sort; /** * 选择排序算法 */ public class SelectSort { /* * 选择排序 * * 参数说明： * a -- 待排序的数组 * n -- 数组的长度 */ public static void selectSort(int[] a, int n) { int i; // 有序区的末尾位置 int j; // 无序区的起始位置 int min; // 无序区中最小元素位置 for (i = 0; i \u0026lt; n; i++) { min = i; // 找出\u0026#34;a[i+1] ... a[n]\u0026#34;之间的最小元素，并赋值给min。 for (j = i + 1; j \u0026lt; n; j++) { if (a[j] \u0026lt; a[min]) min = j; } // 若min!=i，则交换 a[i] 和 a[min]。 // 交换之后，保证了a[0] ... a[i] 之间的元素是有序的。 if (min != i) { int tmp = a[i]; a[i] = a[min]; a[min] = tmp; } } } public static void main(String[] args) { int i; int[] a = {20, 40, 30, 10, 60, 50}; System.out.printf(\u0026#34;before sort:\u0026#34;); for (i = 0; i \u0026lt; a.length; i++) { System.out.printf(\u0026#34;%d \u0026#34;, a[i]); } System.out.printf(\u0026#34;\\n\u0026#34;); selectSort(a, a.length); System.out.printf(\u0026#34;after sort:\u0026#34;); for (i = 0; i \u0026lt; a.length; i++) { System.out.printf(\u0026#34;%d \u0026#34;, a[i]); } System.out.printf(\u0026#34;\\n\u0026#34;); } } 6.4.2. 选择排序C实现（了解） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 /** * 选择排序：C 语言 */ #include \u0026lt;stdio.h\u0026gt; // 数组长度 #define LENGTH(array) ( (sizeof(array)) / (sizeof(array[0])) ) #define swap(a,b) (a^=b,b^=a,a^=b) /* * 选择排序 * * 参数说明： * a -- 待排序的数组 * n -- 数组的长度 */ void select_sort(int a[], int n) { int i; // 有序区的末尾位置 int j; // 无序区的起始位置 int min; // 无序区中最小元素位置 for(i=0; i\u0026lt;n; i++) { min=i; // 找出\u0026#34;a[i+1] ... a[n]\u0026#34;之间的最小元素，并赋值给min。 for(j=i+1; j\u0026lt;n; j++) { if(a[j] \u0026lt; a[min]) min=j; } // 若min!=i，则交换 a[i] 和 a[min]。 // 交换之后，保证了a[0] ... a[i] 之间的元素是有序的。 if(min != i) swap(a[i], a[min]); } } void main() { int i; int a[] = {20,40,30,10,60,50}; int ilen = LENGTH(a); printf(\u0026#34;before sort:\u0026#34;); for (i=0; i\u0026lt;ilen; i++) printf(\u0026#34;%d \u0026#34;, a[i]); printf(\u0026#34;\\n\u0026#34;); select_sort(a, ilen); printf(\u0026#34;after sort:\u0026#34;); for (i=0; i\u0026lt;ilen; i++) printf(\u0026#34;%d \u0026#34;, a[i]); printf(\u0026#34;\\n\u0026#34;); } 6.4.3. 选择排序C++实现（了解） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 /** * 选择排序：C++ */ #include \u0026lt;iostream\u0026gt; using namespace std; /* * 选择排序 * * 参数说明： * a -- 待排序的数组 * n -- 数组的长度 */ void selectSort(int* a, int n) { int i; // 有序区的末尾位置 int j; // 无序区的起始位置 int min; // 无序区中最小元素位置 for(i=0; i\u0026lt;n; i++) { min=i; // 找出\u0026#34;a[i+1] ... a[n]\u0026#34;之间的最小元素，并赋值给min。 for(j=i+1; j\u0026lt;n; j++) { if(a[j] \u0026lt; a[min]) min=j; } // 若min!=i，则交换 a[i] 和 a[min]。 // 交换之后，保证了a[0] ... a[i] 之间的元素是有序的。 if(min != i) { int tmp = a[i]; a[i] = a[min]; a[min] = tmp; } } } int main() { int i; int a[] = {20,40,30,10,60,50}; int ilen = (sizeof(a)) / (sizeof(a[0])); cout \u0026lt;\u0026lt; \u0026#34;before sort:\u0026#34;; for (i=0; i\u0026lt;ilen; i++) cout \u0026lt;\u0026lt; a[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; endl; selectSort(a, ilen); cout \u0026lt;\u0026lt; \u0026#34;after sort:\u0026#34;; for (i=0; i\u0026lt;ilen; i++) cout \u0026lt;\u0026lt; a[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; endl; return 0; } 上面3种实现的原理和输出结果都是一样的。下面是它们的输出结果：\n1 2 before sort:20 40 30 10 60 50 after sort:10 20 30 40 50 60 7. 归并排序 7.1. 基本思想 归并排序（MERGE-SORT）是利用归并的思想实现的排序方法，该算法采用经典的分治（divide-and-conquer）策略（分治法将问题分(divide)成一些小的问题然后递归求解，而治(conquer)的阶段则将分的阶段得到的各答案\u0026quot;修补\u0026quot;在一起，即分而治之)。\n分而治之\n可以看到这种结构很像一棵完全二叉树，本文的归并排序我们采用递归去实现（也可采用迭代的方式去实现）。分阶段可以理解为就是递归拆分子序列的过程，递归深度为log2n。\n合并相邻有序子序列\n再来看看治阶段，我们需要将两个已经有序的子序列合并成一个有序序列，比如上图中的最后一次合并，要将[4,5,7,8]和[1,2,3,6]两个已经有序的子序列，合并为最终序列[1,2,3,4,5,6,7,8]，来看下实现步骤。\n7.2. 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package com.moon.demo.sort; import java.util.Arrays; /** * 归并排序算法 */ public class MergeSort { public static void main(String[] args) { int[] arr = {9, 8, 7, 6, 5, 4, 3, 2, 1}; sort(arr); System.out.println(Arrays.toString(arr)); } public static void sort(int[] arr) { int[] temp = new int[arr.length]; //在排序前，先建好一个长度等于原数组长度的临时数组， //避免递归中频繁开辟空间 sort(arr, 0, arr.length - 1, temp); } private static void sort(int[] arr, int left, int right, int[] temp) { if (left \u0026lt; right) { int mid = (left + right) / 2; sort(arr, left, mid, temp); //左边归并排序，使得左子序列有序 sort(arr, mid + 1, right, temp); //右边归并排序，使得右子序列有序 merge(arr, left, mid, right, temp); //将两个有序子数组合并操作 } } private static void merge(int[] arr, int left, int mid, int right, int[] temp) { int i = left; //左序列指针 int j = mid + 1; //右序列指针 int t = 0; //临时数组指针 while (i \u0026lt;= mid \u0026amp;\u0026amp; j \u0026lt;= right) { if (arr[i] \u0026lt;= arr[j]) { temp[t++] = arr[i++]; } else { temp[t++] = arr[j++]; } } while (i \u0026lt;= mid) { //将左边剩余元素填充进temp中 temp[t++] = arr[i++]; } while (j \u0026lt;= right) { //将右序列剩余元素填充进temp中 temp[t++] = arr[j++]; } t = 0; //将temp中的元素全部拷贝到原数组中 while (left \u0026lt;= right) { arr[left++] = temp[t++]; } } } 执行结果：[1, 2, 3, 4, 5, 6, 7, 8, 9]\n最后\n归并排序是稳定排序，它也是一种十分高效的排序，能利用完全二叉树特性的排序一般性能都不会太差。java中Arrays.sort()采用了一种名为TimSort的排序算法，就是归并排序的优化版本。从上文的图中可看出，每次合并操作的平均时间复杂度为O(n)，而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且，归并排序的最好，最坏，平均时间复杂度均为O(nlogn)。\n8. 堆排序 8.1. 堆排序介绍 堆排序是利用堆这种数据结构而设计的一种排序算法，堆排序是一种选择排序，它的最坏，最好，平均时间复杂度均为O(nlogn)，它也是不稳定排序。首先简单了解下堆结构。\n堆\n堆是具有以下性质的完全二叉树：每个结点的值都大于或等于其左右孩子结点的值，称为大顶堆；或者每个结点的值都小于或等于其左右孩子结点的值，称为小顶堆。如下图：\n同时，我们对堆中的结点按层进行编号，将这种逻辑结构映射到数组中就是下面这个样子：\n该数组从逻辑上讲就是一个堆结构，我们用简单的公式来描述一下堆的定义就是：\n大顶堆：arr[i] \u0026gt;= arr[2i+1] \u0026amp;\u0026amp; arr[i] \u0026gt;= arr[2i+2]\n小顶堆：arr[i] \u0026lt;= arr[2i+1] \u0026amp;\u0026amp; arr[i] \u0026lt;= arr[2i+2]\n8.2. 堆排序基本思想及步骤 堆排序的基本思想是：将待排序序列构造成一个大顶堆，此时，整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换，此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆，这样会得到n个元素的次小值。如此反复执行，便能得到一个有序序列了\n步骤一：构造初始堆。将给定无序序列构造成一个大顶堆（一般升序采用大顶堆，降序采用小顶堆)。\n假设给定无序序列结构如下 此时我们从最后一个非叶子结点开始（叶结点自然不用调整，第一个非叶子结点 arr.length/2-1=5/2-1=1，也就是下面的6结点），从左至右，从下至上进行调整。 找到第二个非叶节点4，由于[4,9,8]中9元素最大，4和9交换。 这时，交换导致了子根[4,5,6]结构混乱，继续调整，[4,5,6]中6最大，交换4和6。\n此时，我们就将一个无需序列构造成了一个大顶堆。\n步骤二：将堆顶元素与末尾元素进行交换，使末尾元素最大。然后继续调整堆，再将堆顶元素与末尾元素交换，得到第二大元素。如此反复进行交换、重建、交换。\na. 将堆顶元素9和末尾元素4进行交换\nb. 重新调整结构，使其继续满足堆定义\nc. 再将堆顶元素8与末尾元素5进行交换，得到第二大元素8\n后续过程，继续进行调整，交换，如此反复进行，最终使得整个序列有序\n再简单总结下堆排序的基本思路：\n将无需序列构建成一个堆，根据升序降序需求选择大顶堆或小顶堆; 将堆顶元素与末尾元素交换，将最大元素\u0026quot;沉\u0026quot;到数组末端; 重新调整结构，使其满足堆定义，然后继续交换堆顶元素与当前末尾元素，反复执行调整+交换步骤，直到整个序列有序。 8.3. 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 package com.moon.demo.sort; import java.util.Arrays; /** * 堆排序算法 */ public class HeapSort { public static void main(String[] args) { int[] arr = {9, 8, 7, 6, 5, 4, 3, 2, 1}; sort(arr); System.out.println(Arrays.toString(arr)); } public static void sort(int[] arr) { // 1.构建大顶堆 for (int i = arr.length / 2 - 1; i \u0026gt;= 0; i--) { // 从第一个非叶子结点从下至上，从右至左调整结构 adjustHeap(arr, i, arr.length); } // 2.调整堆结构+交换堆顶元素与末尾元素 for (int j = arr.length - 1; j \u0026gt; 0; j--) { // 将堆顶元素与末尾元素进行交换 swap(arr, 0, j); // 重新对堆进行调整 adjustHeap(arr, 0, j); } } /** * 调整大顶堆（仅是调整过程，建立在大顶堆已构建的基础上） * * @param arr * @param i * @param length */ public static void adjustHeap(int[] arr, int i, int length) { int temp = arr[i];//先取出当前元素i for (int k = i * 2 + 1; k \u0026lt; length; k = k * 2 + 1) { // 从i结点的左子结点开始，也就是2i+1处开始 if (k + 1 \u0026lt; length \u0026amp;\u0026amp; arr[k] \u0026lt; arr[k + 1]) { // 如果左子结点小于右子结点，k指向右子结点 k++; } if (arr[k] \u0026gt; temp) { // 如果子节点大于父节点，将子节点值赋给父节点（不用进行交换） arr[i] = arr[k]; i = k; } else { break; } } // 将temp值放到最终的位置 arr[i] = temp; } /** * 交换元素 * * @param arr * @param a * @param b */ public static void swap(int[] arr, int a, int b) { int temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; } } 结果：[1, 2, 3, 4, 5, 6, 7, 8, 9]\n最后\n堆排序是一种选择排序，整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n)，在交换并重建堆的过程中，需交换n-1次，而重建堆的过程中，根据完全二叉树的性质，[log2(n-1),log2(n-2)\u0026hellip;1]逐步递减，近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。\n9. 快速排序 9.1. 快速排序介绍 9.1.1. 基础原理 **快速排序(Quick Sort)基本原理是采用分治思想：**选择一个基准数，通过一趟排序将要排序的数据分割成独立的两部分；其中一部分的所有数据都比另外一部分的所有数据都要小。然后，再按此方法对这两部分数据分别进行快速排序，整个排序过程可以递归进行，以此达到整个数据变成有序序列。\n9.1.2. 快速排序流程 从数列中挑出一个基准值。 将所有比基准值小的摆放在基准前面，所有比基准值大的摆在基准的后面(相同的数可以到任一边)；在这个分区退出之后，该基准就处于数列的中间位置。 递归地把\u0026quot;基准值前面的子数列\u0026quot;和\u0026quot;基准值后面的子数列\u0026quot;进行排序。 9.1.3. 快速排序图文说明 下面以数列a={30,40,60,10,20,50}为例，演示它的快速排序过程(如下图)。\n上图只是给出了第1趟快速排序的流程。在第1趟中，设置x=a[i]，即x=30。\n从\u0026quot;右 -\u0026gt; 左\u0026quot;查找小于x的数：找到满足条件的数a[j]=20，此时j=4；然后将a[j]赋值a[i]，此时i=0；接着从左往右遍历。 从\u0026quot;左 -\u0026gt; 右\u0026quot;查找大于x的数：找到满足条件的数a[i]=40，此时i=1；然后将a[i]赋值a[j]，此时j=4；接着从右往左遍历。 从\u0026quot;右 -\u0026gt; 左\u0026quot;查找小于x的数：找到满足条件的数a[j]=10，此时j=3；然后将a[j]赋值a[i]，此时i=1；接着从左往右遍历。 从\u0026quot;左 -\u0026gt; 右\u0026quot;查找大于x的数：找到满足条件的数a[i]=60，此时i=2；然后将a[i]赋值a[j]，此时j=3；接着从右往左遍历。 从\u0026quot;右 -\u0026gt; 左\u0026quot;查找小于x的数：没有找到满足条件的数。当i\u0026gt;=j时，停止查找；然后将x赋值给a[i]。此趟遍历结束！ 按照同样的方法，对子数列进行递归遍历。最后得到有序数组！\n9.1.4. 快速排序的时间复杂度和稳定性 快速排序稳定性\n快速排序是不稳定的算法，它不满足稳定算法的定义。\n算法稳定性：假设在数列中存在a[i]=a[j]，若在排序之前，a[i]在a[j]前面；并且排序之后，a[i]仍然在a[j]前面。则这个排序算法是稳定的！\n快速排序时间复杂度\n快速排序的时间复杂度在最坏情况下是O(N2)，平均的时间复杂度是O(N*lgN)。\n这句话很好理解：假设被排序的数列中有N个数。遍历一次的时间复杂度是O(N)，需要遍历多少次呢？至少lg(N+1)次，最多N次。\n为什么最少是lg(N+1)次？快速排序是采用的分治法进行遍历的，我们将它看作一棵二叉树，它需要遍历的次数就是二叉树的深度，而根据完全二叉树的定义，它的深度至少是lg(N+1)。因此，快速排序的遍历次数最少是lg(N+1)次。 为什么最多是N次？这个应该非常简单，还是将快速排序看作一棵二叉树，它的深度最大是N。因此，快读排序的遍历次数最多是N次。 9.2. 快速排序实现 9.2.1. 快速排序Java实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 package com.moon.demo.sort; /** * 快速排序算法 */ public class QuickSort { /* * 快速排序 * * 参数说明： * a -- 待排序的数组 * l -- 数组的左边界(例如，从起始位置开始排序，则l=0) * r -- 数组的右边界(例如，排序截至到数组末尾，则r=a.length-1) */ public static void quickSort(int[] a, int l, int r) { if (l \u0026lt; r) { int i, j, x; i = l; j = r; x = a[i]; while (i \u0026lt; j) { while (i \u0026lt; j \u0026amp;\u0026amp; a[j] \u0026gt; x) { // 从右向左找第一个小于x的数 j--; } if (i \u0026lt; j) { a[i++] = a[j]; } while (i \u0026lt; j \u0026amp;\u0026amp; a[i] \u0026lt; x) { // 从左向右找第一个大于x的数 i++; } if (i \u0026lt; j) { a[j--] = a[i]; } } a[i] = x; /* 递归调用 */ quickSort(a, l, i - 1); /* 递归调用 */ quickSort(a, i + 1, r); } } public static void main(String[] args) { int i; int a[] = {30, 40, 60, 10, 20, 50}; System.out.printf(\u0026#34;before sort:\u0026#34;); for (i = 0; i \u0026lt; a.length; i++) { System.out.printf(\u0026#34;%d \u0026#34;, a[i]); } System.out.printf(\u0026#34;\\n\u0026#34;); quickSort(a, 0, a.length - 1); System.out.printf(\u0026#34;after sort:\u0026#34;); for (i = 0; i \u0026lt; a.length; i++) { System.out.printf(\u0026#34;%d \u0026#34;, a[i]); } System.out.printf(\u0026#34;\\n\u0026#34;); } } 9.2.2. 快速排序C实现（了解） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 /** * 快速排序：C 语言 */ #include \u0026lt;stdio.h\u0026gt; // 数组长度 #define LENGTH(array) ( (sizeof(array)) / (sizeof(array[0])) ) /* * 快速排序 * * 参数说明： * a -- 待排序的数组 * l -- 数组的左边界(例如，从起始位置开始排序，则l=0) * r -- 数组的右边界(例如，排序截至到数组末尾，则r=a.length-1) */ void quick_sort(int a[], int l, int r) { if (l \u0026lt; r) { int i,j,x; i = l; j = r; x = a[i]; while (i \u0026lt; j) { while(i \u0026lt; j \u0026amp;\u0026amp; a[j] \u0026gt; x) j--; // 从右向左找第一个小于x的数 if(i \u0026lt; j) a[i++] = a[j]; while(i \u0026lt; j \u0026amp;\u0026amp; a[i] \u0026lt; x) i++; // 从左向右找第一个大于x的数 if(i \u0026lt; j) a[j--] = a[i]; } a[i] = x; quick_sort(a, l, i-1); /* 递归调用 */ quick_sort(a, i+1, r); /* 递归调用 */ } } void main() { int i; int a[] = {30,40,60,10,20,50}; int ilen = LENGTH(a); printf(\u0026#34;before sort:\u0026#34;); for (i=0; i\u0026lt;ilen; i++) printf(\u0026#34;%d \u0026#34;, a[i]); printf(\u0026#34;\\n\u0026#34;); quick_sort(a, 0, ilen-1); printf(\u0026#34;after sort:\u0026#34;); for (i=0; i\u0026lt;ilen; i++) printf(\u0026#34;%d \u0026#34;, a[i]); printf(\u0026#34;\\n\u0026#34;); } 9.2.3. 快速排序C++实现（了解） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 /** * 快速排序：C++ */ #include \u0026lt;iostream\u0026gt; using namespace std; /* * 快速排序 * * 参数说明： * a -- 待排序的数组 * l -- 数组的左边界(例如，从起始位置开始排序，则l=0) * r -- 数组的右边界(例如，排序截至到数组末尾，则r=a.length-1) */ void quickSort(int* a, int l, int r) { if (l \u0026lt; r) { int i,j,x; i = l; j = r; x = a[i]; while (i \u0026lt; j) { while(i \u0026lt; j \u0026amp;\u0026amp; a[j] \u0026gt; x) j--; // 从右向左找第一个小于x的数 if(i \u0026lt; j) a[i++] = a[j]; while(i \u0026lt; j \u0026amp;\u0026amp; a[i] \u0026lt; x) i++; // 从左向右找第一个大于x的数 if(i \u0026lt; j) a[j--] = a[i]; } a[i] = x; quickSort(a, l, i-1); /* 递归调用 */ quickSort(a, i+1, r); /* 递归调用 */ } } int main() { int i; int a[] = {30,40,60,10,20,50}; int ilen = (sizeof(a)) / (sizeof(a[0])); cout \u0026lt;\u0026lt; \u0026#34;before sort:\u0026#34;; for (i=0; i\u0026lt;ilen; i++) cout \u0026lt;\u0026lt; a[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; endl; quickSort(a, 0, ilen-1); cout \u0026lt;\u0026lt; \u0026#34;after sort:\u0026#34;; for (i=0; i\u0026lt;ilen; i++) cout \u0026lt;\u0026lt; a[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; endl; return 0; } 上面3种语言的实现原理和输出结果都是一样的。下面是它们的输出结果：\n1 2 before sort:30 40 60 10 20 50 after sort:10 20 30 40 50 60 10. 直接插入排序 10.1. 直接插入排序介绍 直接插入排序(Straight Insertion Sort)的基本思想是：把n个待排序的元素看成为一个有序表和一个无序表。开始时有序表中只包含1个元素，无序表中包含有n-1个元素，排序过程中每次从无序表中取出第一个元素，将它插入到有序表中的适当位置，使之成为新的有序表，重复n-1次可完成排序过程。\n10.1.1. 直接插入排序图文说明 下面选取直接插入排序的一个中间过程对其进行说明。假设{20,30,40,10,60,50}中的前3个数已经排列过，是有序的了；接下来对10进行排列。示意图如下：\n图中将数列分为有序区和无序区。我们需要做的工作只有两个：(1)取出无序区中的第1个数，并找出它在有序区对应的位置。(2)将无序区的数据插入到有序区；若有必要的话，则对有序区中的相关数据进行移位。\n10.1.2. 直接插入排序的时间复杂度和稳定性 直接插入排序时间复杂度\n直接插入排序的时间复杂度是O(N2)。\n假设被排序的数列中有N个数。遍历一趟的时间复杂度是O(N)，需要遍历多少次呢？N-1！因此，直接插入排序的时间复杂度是O(N*N)。\n直接插入排序稳定性\n直接插入排序是稳定的算法，它满足稳定算法的定义。\n算法稳定性 \u0026ndash; 假设在数列中存在a[i]=a[j]，若在排序之前，a[i]在a[j]前面；并且排序之后，a[i]仍然在a[j]前面。则这个排序算法是稳定的！\n10.2. 直接插入排序实现 10.2.1. 直接插入排序Java实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 package com.moon.demo.sort; /** * 直接插入排序算法 */ public class InsertSort { /* * 直接插入排序 * * 参数说明： * a -- 待排序的数组 * n -- 数组的长度 */ public static void insertSort(int[] a, int n) { int i, j, k; for (i = 1; i \u0026lt; n; i++) { // 为a[i]在前面的a[0...i-1]有序区间中找一个合适的位置 for (j = i - 1; j \u0026gt;= 0; j--) if (a[j] \u0026lt; a[i]) break; // 如找到了一个合适的位置 if (j != i - 1) { //将比a[i]大的数据向后移 int temp = a[i]; for (k = i - 1; k \u0026gt; j; k--) a[k + 1] = a[k]; // 将a[i]放到正确位置上 a[k + 1] = temp; } } } public static void main(String[] args) { int i; int[] a = {20, 40, 30, 10, 60, 50}; System.out.printf(\u0026#34;before sort:\u0026#34;); for (i = 0; i \u0026lt; a.length; i++) { System.out.printf(\u0026#34;%d \u0026#34;, a[i]); } System.out.printf(\u0026#34;\\n\u0026#34;); insertSort(a, a.length); System.out.printf(\u0026#34;after sort:\u0026#34;); for (i = 0; i \u0026lt; a.length; i++) { System.out.printf(\u0026#34;%d \u0026#34;, a[i]); } System.out.printf(\u0026#34;\\n\u0026#34;); } } 10.2.2. 直接插入排序C实现（了解） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 /** * 直接插入排序：C 语言 */ #include \u0026lt;stdio.h\u0026gt; // 数组长度 #define LENGTH(array) ( (sizeof(array)) / (sizeof(array[0])) ) /* * 直接插入排序 * * 参数说明： * a -- 待排序的数组 * n -- 数组的长度 */ void insert_sort(int a[], int n) { int i, j, k; for (i = 1; i \u0026lt; n; i++) { //为a[i]在前面的a[0...i-1]有序区间中找一个合适的位置 for (j = i - 1; j \u0026gt;= 0; j--) if (a[j] \u0026lt; a[i]) break; //如找到了一个合适的位置 if (j != i - 1) { //将比a[i]大的数据向后移 int temp = a[i]; for (k = i - 1; k \u0026gt; j; k--) a[k + 1] = a[k]; //将a[i]放到正确位置上 a[k + 1] = temp; } } } void main() { int i; int a[] = {20,40,30,10,60,50}; int ilen = LENGTH(a); printf(\u0026#34;before sort:\u0026#34;); for (i=0; i\u0026lt;ilen; i++) printf(\u0026#34;%d \u0026#34;, a[i]); printf(\u0026#34;\\n\u0026#34;); insert_sort(a, ilen); printf(\u0026#34;after sort:\u0026#34;); for (i=0; i\u0026lt;ilen; i++) printf(\u0026#34;%d \u0026#34;, a[i]); printf(\u0026#34;\\n\u0026#34;); } 10.2.3. 直接插入排序C++实现（了解） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 /** * 直接插入排序：C++ */ #include \u0026lt;iostream\u0026gt; using namespace std; /* * 直接插入排序 * * 参数说明： * a -- 待排序的数组 * n -- 数组的长度 */ void insertSort(int* a, int n) { int i, j, k; for (i = 1; i \u0026lt; n; i++) { //为a[i]在前面的a[0...i-1]有序区间中找一个合适的位置 for (j = i - 1; j \u0026gt;= 0; j--) if (a[j] \u0026lt; a[i]) break; //如找到了一个合适的位置 if (j != i - 1) { //将比a[i]大的数据向后移 int temp = a[i]; for (k = i - 1; k \u0026gt; j; k--) a[k + 1] = a[k]; //将a[i]放到正确位置上 a[k + 1] = temp; } } } int main() { int i; int a[] = {20,40,30,10,60,50}; int ilen = (sizeof(a)) / (sizeof(a[0])); cout \u0026lt;\u0026lt; \u0026#34;before sort:\u0026#34;; for (i=0; i\u0026lt;ilen; i++) cout \u0026lt;\u0026lt; a[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; endl; insertSort(a, ilen); cout \u0026lt;\u0026lt; \u0026#34;after sort:\u0026#34;; for (i=0; i\u0026lt;ilen; i++) cout \u0026lt;\u0026lt; a[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; endl; return 0; } 上面3种实现的原理和输出结果都是一样的。下面是它们的输出结果：\n1 2 before sort:20 40 30 10 60 50 after sort:10 20 30 40 50 60 11. 希尔排序 11.1. 希尔排序介绍 希尔(Shell)排序又称为缩小增量排序，它是一种插入排序。它是直接插入排序算法的一种威力加强版。该方法因DL．Shell于1959年提出而得名\n希尔排序的基本思想\n把记录按步长 gap 分组，对每组记录采用直接插入排序方法进行排序。\n随着步长逐渐减小，所分成的组包含的记录越来越多，当步长的值减小到 1 时，整个数据合成为一组，构成一组有序记录，则完成排序。\n我们来通过演示图，更深入的理解一下这个过程。\n在上面这幅图中：\n初始时，有一个大小为 10 的无序序列。\n在第一趟排序中，我们不妨设 gap1 = N / 2 = 5，即相隔距离为 5 的元素组成一组，可以分为 5 组。接下来，按照直接插入排序的方法对每个组进行排序。\n在第二趟排序中，我们把上次的 gap 缩小一半，即 gap2 = gap1 / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组，可以分为 2 组。按照直接插入排序的方法对每个组进行排序。\n在第三趟排序中，再次把 gap 缩小一半，即gap3 = gap2 / 2 = 1。 这样相隔距离为 1 的元素组成一组，即只有一组。按照直接插入排序的方法对每个组进行排序。此时，排序已经结束。\n需要注意一下的是，图中有两个相等数值的元素 5 和 5 。我们可以清楚的看到，在排序过程中，两个元素位置交换了。\n所以，希尔排序是不稳定的算法。\n11.2. 算法分析 11.2.1. 希尔排序的算法性能 11.2.2. 时间复杂度 步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。\n算法最开始以一定的步长进行排序。然后会继续以一定步长进行排序，最终算法以步长为1进行排序。当步长为1时，算法变为插入排序，这就保证了数据一定会被排序。\nDonald Shell 最初建议步长选择为N/2并且对步长取半直到步长达到1。虽然这样取可以比O(N2)类的算法（插入排序）更好，但这样仍然有减少平均时间和最差时间的余地。\n可能希尔排序最重要的地方在于当用较小步长排序后，以前用的较大步长仍然是有序的。比如，如果一个数列以步长5进行了排序然后再以步长3进行排序，那么该数列不仅是以步长3有序，而且是以步长5有序。如果不是这样，那么算法在迭代过程中会打乱以前的顺序，那就\n不会以如此短的时间完成排序了。\n已知的最好步长序列是由Sedgewick提出的(1, 5, 19, 41, 109,\u0026hellip;)，该序列的项来自这两个算式。\n这项研究也表明“比较在希尔排序中是最主要的操作，而不是交换。”用这样步长序列的希尔排序比插入排序和堆排序都要快，甚至在小数组中比快速排序还快，但是在涉及大量数据时希尔排序还是比快速排序慢。\n11.2.3. 算法稳定性 由上文的希尔排序算法演示图即可知，希尔排序中相等数据可能会交换位置，所以希尔排序是不稳定的算法。\n11.2.4. 直接插入排序和希尔排序的比较 直接插入排序是稳定的；而希尔排序是不稳定的。 直接插入排序更适合于原始记录基本有序的集合。 希尔排序的比较次数和移动次数都要比直接插入排序少，当N越大时，效果越明显。 在希尔排序中，增量序列gap的取法必须满足：最后一个步长必须是1。 直接插入排序也适用于链式存储结构；希尔排序不适用于链式结构。 11.3. 代码实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package com.moon.demo.sort; /** * 希尔排序算法 */ public class ShellSort { public void shellSort(int[] list) { int gap = list.length / 2; while (1 \u0026lt;= gap) { // 把距离为 gap 的元素编为一个组，扫描所有组 for (int i = gap; i \u0026lt; list.length; i++) { int j = 0; int temp = list[i]; // 对距离为 gap 的元素组进行排序 for (j = i - gap; j \u0026gt;= 0 \u0026amp;\u0026amp; temp \u0026lt; list[j]; j = j - gap) { list[j + gap] = list[j]; } list[j + gap] = temp; } System.out.format(\u0026#34;gap = %d:\\t\u0026#34;, gap); printAll(list); // 减小增量 gap = gap / 2; } } // 打印完整序列 public void printAll(int[] list) { for (int value : list) { System.out.print(value + \u0026#34;\\t\u0026#34;); } System.out.println(); } public static void main(String[] args) { int[] array = {9, 1, 2, 5, 7, 4, 8, 6, 3, 5}; // 调用希尔排序方法 ShellSort shell = new ShellSort(); System.out.print(\u0026#34;排序前:\\t\\t\u0026#34;); shell.printAll(array); shell.shellSort(array); System.out.print(\u0026#34;排序后:\\t\\t\u0026#34;); shell.printAll(array); } } 运行结果：\n1 2 3 4 5 排序前: 9 1 2 5 7 4 8 6 3 5 gap = 5: 4 1 2 3 5 9 8 6 5 7 gap = 2: 2 1 4 3 5 6 5 7 8 9 gap = 1: 1 2 3 4 5 5 6 7 8 9 排序后: 1 2 3 4 5 5 6 7 8 9 12. 拓扑排序 12.1. 拓扑排序介绍 拓扑排序(Topological Order)是指，将一个有向无环图(Directed Acyclic Graph简称DAG)进行排序进而得到一个有序的线性序列。\n这样说，可能理解起来比较抽象。下面通过简单的例子进行说明！\n例如，一个项目包括A、B、C、D四个子部分来完成，并且A依赖于B和D，C依赖于D。现在要制定一个计划，写出A、B、C、D的执行顺序。这时，就可以利用到拓扑排序，它就是用来确定事物发生的顺序的。\n在拓扑排序中，如果存在一条从顶点A到顶点B的路径，那么在排序结果中B出现在A的后面。\n12.2. 拓扑排序的算法图解 12.2.1. 拓扑排序算法的基本步骤 构造一个队列Q(queue) 和 拓扑排序的结果队列T(topological)； 把所有没有依赖顶点的节点放入Q； 当Q还有顶点的时候，执行下面步骤： 从Q中取出一个顶点n(将n从Q中删掉)，并放入T(将n加入到结果集中)； 对n每一个邻接点m(n是起点，m是终点)； 去掉边\u0026lt;n,m\u0026gt;； 如果m没有依赖顶点，则把m放入Q； 注：顶点A没有依赖顶点，是指不存在以A为终点的边。\n以上图为例，来对拓扑排序进行演示。\n第1步：将B和C加入到排序结果中。\n顶点B和顶点C都是没有依赖顶点，因此将C和C加入到结果集T中。假设ABCDEFG按顺序存储，因此先访问B，再访问C。访问B之后，去掉边\u0026lt;B,A\u0026gt;和\u0026lt;B,D\u0026gt;，并将A和D加入到队列Q中。同样的，去掉边\u0026lt;C,F\u0026gt;和\u0026lt;C,G\u0026gt;，并将F和G加入到Q中。\n将B加入到排序结果中，然后去掉边\u0026lt;B,A\u0026gt;和\u0026lt;B,D\u0026gt;；此时，由于A和D没有依赖顶点，因此并将A和D加入到队列Q中。 将C加入到排序结果中，然后去掉边\u0026lt;C,F\u0026gt;和\u0026lt;C,G\u0026gt;；此时，由于F有依赖顶点D，G有依赖顶点A，因此不对F和G进行处理。 第2步：将A,D依次加入到排序结果中。\n第1步访问之后，A,D都是没有依赖顶点的，根据存储顺序，先访问A，然后访问D。访问之后，删除顶点A和顶点D的出边。\n第3步：将E,F,G依次加入到排序结果中。\n因此访问顺序是：B -\u0026gt; C -\u0026gt; A -\u0026gt; D -\u0026gt; E -\u0026gt; F -\u0026gt; G\n12.3. 拓扑排序的代码实现 拓扑排序是对有向无向图的排序。下面以邻接表实现的有向图来对拓扑排序进行说明。\n12.3.1. 基本定义 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.moon.demo.sort; /** * 拓扑排序基本定义邻接表对应的结构体 */ public class ListDG { // 邻接表中表对应的链表的顶点 private class ENode { // 该边所指向的顶点的位置 int ivex; // 指向下一条弧的指针 ENode nextEdge; } // 邻接表中表的顶点 private class VNode { // 顶点信息 char data; // 指向第一条依附该顶点的弧 ENode firstEdge; } // 顶点集合 private List\u0026lt;VNode\u0026gt; mVexs; ...... } ListDG是邻接表对应的结构体。mVexs则是保存顶点信息的一维数组。 VNode是邻接表顶点对应的结构体。data是顶点所包含的数据，而firstEdge是该顶点所包含链表的表头指针。 ENode是邻接表顶点所包含的链表的节点对应的结构体。ivex是该节点所对应的顶点在vexs中的索引，而nextEdge是指向下一个节点的。 12.3.2. 拓扑排序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 package com.moon.demo.sort; import java.util.LinkedList; import java.util.List; import java.util.Queue; /** * 拓扑排序算法 */ public class ListDG { // 邻接表中表对应的链表的顶点 private class ENode { // 该边所指向的顶点的位置 int ivex; // 指向下一条弧的指针 ENode nextEdge; } // 邻接表中表的顶点 private class VNode { // 顶点信息 char data; // 指向第一条依附该顶点的弧 ENode firstEdge; } // 顶点集合 private List\u0026lt;VNode\u0026gt; mVexs; /* * 拓扑排序 * * 返回值： * -1 -- 失败(由于内存不足等原因导致) * 0 -- 成功排序，并输入结果 * 1 -- 失败(该有向图是有环的) */ public int topologicalSort() { int index = 0; int num = mVexs.size(); // 入度数组 int[] ins; // 拓扑排序结果数组，记录每个节点的排序后的序号。 char[] tops; // 辅组队列 Queue\u0026lt;Integer\u0026gt; queue; ins = new int[num]; tops = new char[num]; queue = new LinkedList\u0026lt;Integer\u0026gt;(); // 统计每个顶点的入度数 for (int i = 0; i \u0026lt; num; i++) { ENode node = mVexs.get(i).firstEdge; while (node != null) { ins[node.ivex]++; node = node.nextEdge; } } // 将所有入度为0的顶点入队列 for (int i = 0; i \u0026lt; num; i++) { if (ins[i] == 0) { // 入队列 queue.offer(i); } } while (!queue.isEmpty()) { // 队列非空 int j = queue.poll().intValue(); // 出队列。j是顶点的序号 tops[index++] = mVexs.get(j).data; // 将该顶点添加到tops中，tops是排序结果 ENode node = mVexs.get(j).firstEdge; // 获取以该顶点为起点的出边队列 // 将与\u0026#34;node\u0026#34;关联的节点的入度减1； // 若减1之后，该节点的入度为0；则将该节点添加到队列中。 while (node != null) { // 将节点(序号为node.ivex)的入度减1。 ins[node.ivex]--; // 若节点的入度为0，则将其\u0026#34;入队列\u0026#34; if (ins[node.ivex] == 0) { // 入队列 queue.offer(node.ivex); } node = node.nextEdge; } } if (index != num) { System.out.printf(\u0026#34;Graph has a cycle\\n\u0026#34;); return 1; } // 打印拓扑排序结果 System.out.printf(\u0026#34;== TopSort: \u0026#34;); for (int i = 0; i \u0026lt; num; i++) { System.out.printf(\u0026#34;%c \u0026#34;, tops[i]); } System.out.printf(\u0026#34;\\n\u0026#34;); return 0; } } 说明：\nqueue的作用就是用来存储没有依赖顶点的顶点。它与前面所说的Q相对应。 tops的作用就是用来存储排序结果。它与前面所说的T相对应。 13. 题外拓展：面试时算法题的解答思路 面试中纯粹考算法的问题一般是让很多程序员朋友痛恨的，这里分享下我对于解答算法题的一些思路和技巧。\n一般关于算法的文章，都是从经典算法讲起，一种一种算法介绍，见得算法多了，自然就有了感悟，但如此学习花费的时间和精力却是过于巨大，也不适合在博客里面交流。这一篇文，却是专门讲快捷思路的，很多人面对算法题的时候几乎是脑子里一片空白，这一篇文章讲的就是从题目下手，把毫无思路的题目打开一个缺口的几种常见技巧。\n13.1. （一）由简至繁 事实上，很多问题确实是很难在第一时间内得到正确的思路的，这时候可以尝试一种由简至繁的思路。首先把问题规模缩小到非常容易解答的地步。\n[题目]有足够量的2分、5分、1分硬币，请问凑齐1元钱有多少种方法？\n此题乍看上去，只会觉得完全无法入手，但是按照由简至繁的思路，我们可以先考虑极端简单的情况，假如把问题规模缩小成:有足够量的1分硬币，请问凑齐1分钱有多少种方法？毫无疑问，答案是1。\n得到这一答案之后，我们可以略微扩大问题的规模： 有足够量的1分硬币，凑齐2分钱有多少种方法？凑齐n分钱有多少种方法？答案仍然是1\n接下来，我们可以从另一个角度来扩大问题，有足够量的1分硬币和2分硬币，凑齐n分钱有多少种方法？这时我们手里已经有了有足够量的1分硬币，凑齐任意多钱都只有1种方法，那么只用1分钱凑齐n-2分钱，有1种方法，只用1分钱凑齐n-4分钱，有1种方法，只用1分钱凑齐n-6分钱，有1种方法\u0026hellip;\u0026hellip;\n而凑齐这些n-2、n-4、n-6这些钱数，各自补上2分钱，会产生一种新的凑齐n分钱的方法，这些方法的总数+1，就是用1分硬币和2分硬币，凑齐n分钱的方法数了。\n在面试时，立刻采用这种思路是一种非常有益的尝试，解决小规模问题可以让你更加熟悉问题，并且慢慢发现问题的特性，最重要的是给你的面试官正面的信号——立即动手分析问题比皱眉冥思苦想看起来好得多。\n对于此题而言，我们可以很快发现问题的规模有两个维度：用a1-ak种硬币和凑齐n分钱，所以我们可以记做P(k,n)。当我们发现递归公式 P(k,n) = P(k-1,n - ak) + P(k-1,n - 2*ak) + P(k-1,n - 3*ak) ... ... 时，这个问题已经是迎刃而解了\n通常由简至繁的思路，用来解决动态规划问题是非常有效的，当积累了一定量简单问题的解的时候，往往通向更高一层问题的答案已经摆在眼前了。\n13.2. （二）一分为二 另一种思路，就是把问题一刀斩下，把问题分为两半，变成两个与原来问题同构的问题，能把问题一分为2，就能再一分为4，就能再一分为8，直到分成我们容易解决的问题。当尝试这种思路时，其实只需要考虑两个问题：1.一分为二以后，问题是否被简化了？ 2.根据一分为二的两个问题的解，能否方便地得出整个问题的解？\n[题目]将一个数组排序。\n这个经典算法肯定所有人都熟悉的不能再熟悉了，不过若是从头开始思考这个问题，倒也不是所有人都能想出几种经典的排序算法之一的，这里仅仅是用来做例子说明一分为二的思路的应用。\n最简单的一分为二，就是将数组分成两半，分别排序。对于两个有序数组，我们有办法将它合并成一个有序数组，所以这个一分为二的思路是可行的，同样对于已经分成两半的数组，我们还可以将这个数组分作两半，直到我们分好的数组仅有1个元素，1个元素的数组天然就是有序的。不难看出，按这种思路我们得出的是经典数组排序算法中的“归并排序”。\n还有另一种一分为二的思路，考虑到自然将数组分成两半合并起来比较复杂，我们可以考虑将数组按照大于和小于某个元素分成两半，这样只要分别解决就可以直接连接成一个有序数组了，同样这个问题也是能够再次一分为二。按照这个思路，则可以得出经典数组排序算法中的“快速排序”。\n13.3. （三）化虚为实 这种思路针对的是浮点数有关的特殊问题，因为无论是穷举还是二分，对于浮点数相关的计算问题（尤其是计算几何）都难以启效，所以化虚为实，指的是把有点\u0026quot;虚\u0026quot;的浮点数，用整数来替代。具体做法是，把题目中给出的一些浮点数（不限于浮点数，我们不关心其具体大小的整数也可以）排序，然后用浮点数的序号代替本身来思考问题，等到具体计算时再替换回来。\n[题目]已知n个边水平竖直的矩形（用四元组[x1,y1,x2,y2]表示），求它们的总共覆盖面积。\n因为坐标可能出现浮点数，所以此题看起来十分繁复（可以实践上面由简至繁和一分为二的思路都基本无效），略一思考，矩形的覆盖关系其实只跟矩形坐标的大小有关，所以我们尝试思考将矩形的所有x值排序，然后用序号代替具体竖直，y值亦然，于是我们得到所有矩形其实处于一个2nx2n的区块当中，这样我们用最简单的穷举办法，可以计算出每一个1x1的格子是否被覆盖住了。至此，只要我们计算面积的时候，把格子的真实长宽换算回来，就已经得到题目的答案了。\n以上三种思路，是我平时遇到算法问题的快速思考方向，并非万灵药方，若是不能生效，就要静下心来慢慢思考观察了，考虑到面试的时候基本不会遇到高难度算法题，这几种技巧的命中率应该不会太低，共享给大家，希望有所帮助。\n","permalink":"https://ktzxy.top/posts/c6e0hapw9o/","summary":"Java扩展 算法","title":"Java扩展 算法"},{"content":"使用Rancher2.0搭建Kubernetes集群 中文文档：https://docs.rancher.cn/docs/rancher2\n安装Rancher2.0 使用下面命令，我们快速的安装\n1 2 3 4 # 启动 rancher【没有的话会从后台拉取】 docker run -d -p 80:80 -p 443:443 rancher/rancher:v2.0.0 # 查看 docker ps -a 我们可以来查看我们的日志\n1 docker logs eloquent_curie 同时，我们可以直接访问我们新建的Rancher集群\n1 https://192.168.177.150/ 第一次登录，需要我们填写密码，我们自己的密码后，点击下一步，完成后即可进入到我们的控制台\n导入K8S集群 在我们安装好Rancher2.0后，我们就可以导入我们的K8S集群进行管理了\n首先我们点击 Add Cluster ，然后选择 IMPORT 导入我们的集群\n然后会有Add Cluster页面，下面我们通过命令来添加\n我们首先选择上面这条，在我们的master节点上执行，将我们的集群被Rancher接管\n1 kubectl apply -f https://192.168.177.130/v3/import/6pqf9w75fmx4pt94tpbpklxd2t5qkq2fm9v6dgl6w8z6rc8727bpdk.yaml 如果执行命令有问题的话，我们可以提前把脚本下载下来，然后拷贝到里面的 rancher.yaml\n1 kubectl apply -f rancher.yaml 在执行上述命令，可能会出现这个问题，我们只需要把里面的 extensions/v1beta1 修改成 apps/v1 即可\n修改完成后，再次执行即可\n我们通过下面命令，查看我们创建的pods\n1 kubectl get pods -n cattle-system 执行完上述操作后，我们到Rancher的UI界面，点击Done，即可看到我们的集群被成功导入\n","permalink":"https://ktzxy.top/posts/x5fzlhrrun/","summary":"50 使用Rancher搭建Kubernetes集群","title":"50 使用Rancher搭建Kubernetes集群"},{"content":"shell脚本+定时任务\u0026quot;的方式来实现自动备份mysql数据库。\n环境 备份路径：/data/mysqlbak/\n备份脚本：/data/mysqlbak/mysqlbak.sh\n备份时间：每天23：59备份\n备份要求：比如备份的数据只保留1周\nmysqlbak.sh脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #!/bin/bash #数据库IP dbserver=\u0026#39;127.0.0.1\u0026#39; #数据库用户名 dbuser=\u0026#39;root\u0026#39; #数据密码 dbpasswd=\u0026#39;********\u0026#39; #数据库,如有多个库用空格分开 dbname=\u0026#39;back01\u0026#39; #备份时间 backtime=`date +%Y%m%d` #备份输出日志路径 logpath=\u0026#39;/data/mysqlbak/\u0026#39; echo \u0026#34;################## ${backtime} #############################\u0026#34; echo \u0026#34;开始备份\u0026#34; #日志记录头部 echo \u0026#34;\u0026#34; \u0026gt;\u0026gt; ${logpath}/mysqlback.log echo \u0026#34;-------------------------------------------------\u0026#34; \u0026gt;\u0026gt; ${logpath}/mysqlback.log echo \u0026#34;备份时间为${backtime},备份数据库表 ${dbname} 开始\u0026#34; \u0026gt;\u0026gt; ${logpath}/mysqlback.log #正式备份数据库 for table in $dbname; do source=`mysqldump -h ${dbserver} -u ${dbuser} -p${dbpasswd} ${table} \u0026gt; ${logpath}/${backtime}.sql` 2\u0026gt;\u0026gt; ${logpath}/mysqlback.log; #备份成功以下操作 if [ \u0026#34;$?\u0026#34; == 0 ];then cd $datapath #为节约硬盘空间，将数据库压缩 tar zcf ${table}${backtime}.tar.gz ${backtime}.sql \u0026gt; /dev/null #删除原始文件，只留压缩后文件 rm -f ${datapath}/${backtime}.sql #删除七天前备份，也就是只保存7天内的备份 find $datapath -name \u0026#34;*.tar.gz\u0026#34; -type f -mtime +7 -exec rm -rf {} \\; \u0026gt; /dev/null 2\u0026gt;\u0026amp;1 echo \u0026#34;数据库表 ${dbname} 备份成功!!\u0026#34; \u0026gt;\u0026gt; ${logpath}/mysqlback.log else #备份失败则进行以下操作 echo \u0026#34;数据库表 ${dbname} 备份失败!!\u0026#34; \u0026gt;\u0026gt; ${logpath}/mysqlback.log fi done echo \u0026#34;完成备份\u0026#34; echo \u0026#34;################## ${backtime} #############################\u0026#34; 为脚本加上执行权限：\n1 #chmod +x /data/mysqlbak/mysqlbak.sh 配置定时任务执行脚本\n1 2 3 #crontab -e 59 23 * * * /data/mysqlbak/mysqlbak.sh 参数说明：\n格式为 ：分 时 日 月 周 命令\n59 23 * * * :每天23：59分自动执行脚本\nM: 分钟（0-59）。每分钟用*或者 */1表示\nH：小时（0-23）。（0表示0点）\nD：天（1-31）。\nm: 月（1-12）。\nd: 一星期内的天（0~6，0为星期天）。\n提示：最好你先执行一下脚本能不能跑通，然后在写到crontab中，等执行完了，进入/data/mysqlbak/目录查看一下有没有备份文件，如果有，则表示脚本执行成功，记得不要搞错了备份的用户和密码。\n","permalink":"https://ktzxy.top/posts/bhu6l5osgd/","summary":"生产环境下实现每天自动备份mysql数据库","title":"生产环境下实现每天自动备份mysql数据库"},{"content":"﻿### 1、使用SQL Server Management Studio管理平台，为supermarket数据库添加如教材P53页图3-4~图3-8所示的示例数据。\n前面实验2已经加过，不再执行\n2、完成教材上的例3-28~例3-69的操作。 查询全体学生姓名、学号、专业 1 select SName,SNO,Major from Student 查询全体学生的详细信息 1 select * from Student; 查询全体学生的学号、姓名、年龄 1 select SNO,SName,YEAR(GETDATE())-BirthYear Age from Student 查询购买了商品的学生学号 1 select distinct SNO from SaleBill 查询管理信息系统专业学生名单 1 select * from Student where Major = \u0026#39;MIS\u0026#39; 查询年龄不大于20的学生名单 1 select * from Student where YEAR(GETDATE())-BirthYear !\u0026gt; 20; 查询现存货量为3~10的商品信息 1 select * from Goods where Number between 3 and 10 查询2017年生产的商品信息 1 select * from Goods where ProductTime between \u0026#39;2017-1-1\u0026#39; and \u0026#39;2017-12-31\u0026#39; 查询姓名在“李明”和“闵红”之间的学生信息 1 select * from Student where SName between \u0026#39;李明\u0026#39; and \u0026#39;闵红\u0026#39; 查询商品编号分别为GN0001、GN0002的销售信息 1 select * from SaleBill where GoodsNO in (\u0026#39;GN0001\u0026#39;,\u0026#39;GN0002\u0026#39;) 查询不是MIS专业的学生信息 1 select * from Student where Major != \u0026#39;MIS\u0026#39; 查询商品名称中包含“咖啡”的商品信息 1 select * from Goods where GoodsName like \u0026#39;%咖啡%\u0026#39; 查询学生姓名第二个字为“民”的学生信息 1 select * from Student where SName like \u0026#39;_民%\u0026#39; 查询商品编号最后一位不是1、4、7的商品信息 1 2 3 select * from Goods where GoodsNO not like \u0026#39;%[147]\u0026#39; select * from Goods where GoodsNO like \u0026#39;%[^147]\u0026#39; 查询AC专业的学生和MIS专业男生的信息 1 select * from Student where Major = \u0026#39;AC\u0026#39; or Major = \u0026#39;MIS\u0026#39; and Ssex = \u0026#39;男\u0026#39; 查询学生信息，按出生年升序排列 1 select * from Student order by BirthYear 查询商品名包含“咖啡”的商品的商品编号、商品名、现货存量和生产时间。按现货存量升序、生产日期降序排列。 1 2 select GoodsNO,GoodsName,Number,ProductTime from Goods where GoodsName like \u0026#39;%咖啡%\u0026#39; order by Number,ProductTime desc 查询商品表的商品编号、商品名称、现货存量、生产日期、保质期剩余天数，按保质期剩余天数升序排列 1 2 3 4 select GoodsNO,GoodsName,Number,ProductTime, QGPeriod * 30 - DATEDIFF(DAY,ProductTime,GETDATE()) days_remaining from Goods order by days_remaining 查询商品个数 1 select COUNT(*) 商品个数 from Goods 查询售出商品种类 1 select COUNT(distinct GoodsNO) 商品种类 from SaleBill 统计销售表中最多、最少和平均销售量 1 select MAX(Number) 最大销售量,MIN(Number) 最小销售量,AVG(Number) 平均销售量 from SaleBill 统计每个学生购买的商品种类 1 select SNO,COUNT(*) 商品种类 from SaleBill group by SNO 统计每个学生购买的商品种类，列出购买3种或3种以上商品学生的学号，购买商品种类 1 select SNO,COUNT(*) 商品种类 from SaleBill group by SNO having COUNT(*)\u0026gt;=3 统计学生表中每年出生的男、女生人数，按出生年降序、人数升序排列。 1 2 3 select Student.BirthYear,Student.Ssex,COUNT(*) from Student group by BirthYear,Ssex order by BirthYear desc,count(*) 查询学生购物情况 1 select * from Student S join SaleBill SA on S.SNO = SA.SNO 查询MIS专业学生的购物情况 1 2 3 select * from Student S join SaleBill SA on S.SNO = SA.SNO where Major = \u0026#39;MIS\u0026#39; 查询“CS”学校各学生的消费金额 1 2 3 4 select college,SName,sum(SA.Number * SalePrice) 消费金额 from Student S join SaleBill SA on S.SNO = SA.SNO join Goods G on SA.GoodsNO = G.GoodsNO where college = \u0026#39;CS\u0026#39; group by college,SName 查询与商品“麦氏威尔冰咖啡”同一类别的商品的商品编号、商品名 1 2 3 4 select G2.GoodsNO,G2.GoodsName from Goods G join Goods G2 on G.CategoryNO = G2.CategoryNO where G.GoodsName = \u0026#39;麦氏威尔冰咖啡\u0026#39; and G2.GoodsName != \u0026#39;麦氏威尔冰咖啡\u0026#39; 查询没人购买的商品，列出商品名与现货存量 1 2 3 select GoodsName,G.Number 现货存量 from Goods G left join SaleBill GA on GA.GoodsNO = G.GoodsNO where GA.SNO is null 查询与商品“麦氏威尔冰咖啡”同一类别的商品的商品编号、商品名 1 2 3 select GoodsName from Goods where CategoryNO = (select CategoryNO from Goods where GoodsName = \u0026#39;麦氏威尔冰咖啡\u0026#39;) and GoodsName != \u0026#39;麦氏威尔冰咖啡\u0026#39;; 查询进价大于平均进价的商品名称和进价 1 2 select GoodsName,InPrice from Goods where InPrice \u0026gt; (select avg(InPrice) from Goods) 查询购买了“东菀市南城久润食品贸易部”经销的商品的学生学号和姓名。 1 2 3 4 5 select SNO,SName from Student where SNO in(select distinct SNO from SaleBill where GoodsNO in(select GoodsNO from Goods where SupplierNO = (select SupplierNO from Supplier where SupplierName = \u0026#39;东菀市南城久润食品贸易部\u0026#39;))) 查询超过同类商品平均进价的商品信息 1 2 select * from Goods where InPrice \u0026gt; (select avg(InPrice) from Goods G where CategoryNO = Goods.CategoryNO) 查询购买了商品的学生信息 1 2 select * from Student where exists (select * from SaleBill where SNO = Student.SNO) 查询至少购买了学生S02购买的全部商品的学生学号 1 2 3 4 select distinct SNO from SaleBill S1 where S1.SNO != \u0026#39;S02\u0026#39; and not exists (select * from SaleBill S2 where S2.SNO = \u0026#39;S02\u0026#39; and not exists (select * from SaleBill S3 where S3.SNO = S1.SNO and S3.GoodsNO = S2.GoodsNO)) 查询MIS专业或出生年晚于1999年的学生信息 1 2 3 select * from Student where Major = \u0026#39;MIS\u0026#39; union select * from Student where BirthYear \u0026gt; 1999 查询MIS专业，出生年晚于1991年的学生信息 1 2 3 select * from Student where Major = \u0026#39;MIS\u0026#39; intersect select * from Student where BirthYear \u0026gt; 1991 查询至少购买了学生S02购买的全部商品的学生学号 1 2 3 4 5 select distinct SNO from SaleBill where SNO!=\u0026#39;S02\u0026#39; and not exists (select GoodsNO from SaleBill where SNO = \u0026#39;S02\u0026#39; except select GoodsNO from SaleBill S where S.SNO = SaleBill.SNO) 查询各类别商品的商品种类名和平均售价 1 2 3 4 select C.CategoryName,AVG_CA.AVGSALEPRICE from Category C join (select CategoryNO,avg(SalePrice) from Goods group by CategoryNO) as AVG_CA(CategoryNO,AVGSALEPRICE) on C.CategoryNO = AVG_CA.CategoryNO 查询购买了GN0002商品的学生信息 1 2 select * from Student S join (select SNO,GoodsNO from SaleBill where GoodsNO = \u0026#39;GN0002\u0026#39;) SA_SNO on S.SNO = SA_SNO.SNO 查询销售额前三的商品与销售额 1 2 3 4 5 select top 3 G.GoodsNO,sum(SA.Number * G.SalePrice) GOODSUM from Goods G join SaleBill SA on SA.GoodsNO = SA.GoodsNO group by G.GoodsNO order by GOODSUM desc 查询年龄最大的3名学生的信息 1 2 select top 3 with ties * from Student order by BirthYear 3、在数据库supermarket上完成下列操作。 （1）查询商品种类信息。\n1 select CategoryNO,CategoryName,Description from Category （2）查询IT专业所有学生信息。\n1 select * from Student where Major = \u0026#39;IT\u0026#39; （3）查询MIS专业年龄小于20岁的学生信息。并为MIS列取别名为“信息管理系统”。\n1 2 select SNO,SName,BirthYear,Ssex,College,Major 信息管理系统,WeiXin from Student where YEAR(GETDATE()) - BirthYear \u0026lt;22 and Major = \u0026#39;MIS\u0026#39; （4）查询利润率大于30%的商品编号与商品名。\n1 2 select GoodsNO,GoodsName,ROUND((SalePrice-InPrice)/InPrice,2) 利润率 from Goods where (SalePrice-InPrice)/InPrice \u0026gt; 0.3 （5）查询广州佛山供应的商品信息。\n1 2 select * from Goods G join Supplier S on G.SupplierNO = S.SupplierNO where S.Address = \u0026#39;广州佛山\u0026#39; （6）查询购买了商品种类为咖啡的MIS专业的学生信息。\n1 2 3 4 5 6 7 8 select * from Student where SNO in( select SNO from SaleBill where GoodsNO in( select GoodsNO from Goods G join Category C on G.CategoryNO = C.CategoryNO and CategoryName = \u0026#39;咖啡\u0026#39; ) ) and Major = \u0026#39;MIS\u0026#39; （7）查询购买了商品种类为咖啡的各专业的学生人数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 select Major,count(Major) 人数 from Student where SNO in( select SNO from SaleBill where GoodsNO in( select GoodsNO from Goods G join Category C on G.CategoryNO = C.CategoryNO and CategoryName = \u0026#39;咖啡\u0026#39; ) ) group by Major go --方法二 select Major, count(*) 人数 from ( select distinct Student.* from Salebill,Student,Goods,Category where SaleBill.SNO=Student.SNO and SaleBill.GoodsNO=Goods.GoodsNO and Goods.CategoryNO=Category.CategoryNO and CategoryName=\u0026#39;咖啡\u0026#39; ) S group by Major go --方法三 select Major,count(distinct(Student.SNO)) 人数 from Salebill,Student,Goods,Category where SaleBill.SNO=Student.SNO and SaleBill.GoodsNO=Goods.GoodsNO and Goods.CategoryNO=Category.CategoryNO and CategoryName=\u0026#39;咖啡\u0026#39; group by Major go （8）查询购买各商品种类的各专业的学生人数。\n1 2 3 4 5 6 7 8 9 10 11 select CategoryName,Major, count(*) 人数 from ( select distinct Student.*,CategoryName from Salebill,Student,Goods,Category where SaleBill.SNO=Student.SNO and SaleBill.GoodsNO=Goods.GoodsNO and Goods.CategoryNO=Category.CategoryNO ) S group by CategoryName,Major order by CategoryName go （9）查询从未购买过商品的学生信息。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 select * from Student where SNO not in( select distinct SNO from SaleBill where GoodsNO in( select GoodsNO from Goods G join Category C on G.CategoryNO = C.CategoryNO ) ) go select * from student except select distinct student.* from SaleBill,Student where SaleBill.SNO=Student.SNO go （10）查询与商品编号GN0005相同产地的商品编号、商品名。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 select GoodsNO,GoodsName from Goods where SupplierNO in ( select SupplierNO from Supplier where Address =( select Address from Supplier S join Goods G on S.SupplierNO = G.SupplierNO where GoodsNO = \u0026#39;GN0005\u0026#39; ) ) go select GoodsNo, GoodsName from Goods,Supplier where Goods.SupplierNO=Supplier.SupplierNO and Goods.GoodsNO!=\u0026#39;GN0005\u0026#39; and address=( select address from goods,supplier where goods.GoodsNO=\u0026#39;GN0005\u0026#39; and Goods.SupplierNO=Supplier.SupplierNO ) go （11） 使用派生表查询各供应商的存货量。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 select SupplierName 供应商名称,S2.SUM_Number 存货量 from Supplier S join ( select SupplierNO,SUM(Number) SUM_Number from Goods group by SupplierNO) S2 on S.SupplierNO = S2.SupplierNO go select supplierName 供应商,存货量 from supplier, ( select Supplier.supplierNO,sum(goods.number) 存货量 from goods,supplier where goods.SupplierNO=supplier.SupplierNO group by supplier.SupplierNO) N where supplier.SupplierNO=N.SupplierNO go （12） 查询售价大于该种类商品售价均值的商品号、商品名。\n1 2 3 4 5 6 7 8 9 10 11 12 13 select GoodsNO,GoodsName from Goods G join ( select CategoryNO,ROUND(avg(SalePrice),2) avg_salePrice from Goods group by CategoryNO) G2 on G.CategoryNO = G2.CategoryNO and SalePrice \u0026gt; avg_salePrice go select GoodsNO, GoodsName from Goods G where SalePrice\u0026gt;(select avg(SalePrice) from goods where CategoryNO=G.CategoryNO) go （13）分别用子查询与连接查询查询购买了商品编号为“GN0003”和“GN0007”的学生学号与姓名。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 select SNO,SName from Student where SNO in ( select SNO from SaleBill where GoodsNO in(\u0026#39;GN0003\u0026#39;,\u0026#39;GN0007\u0026#39;) group by SNO having count(GoodsNO) = 2) go select S.SNO,SName from Student S right join ( select SNO from SaleBill where GoodsNO in(\u0026#39;GN0003\u0026#39;,\u0026#39;GN0007\u0026#39;) group by SNO having count(GoodsNO) = 2 ) g on S.SNO = g.SNO order by S.SNO select S.SNO,S.SName from SaleBill SB, Student S where SB.GoodsNO=\u0026#39;GN0003\u0026#39; and Exists (select * from SaleBill where SaleBill.GoodsNO=\u0026#39;GN0007\u0026#39; and SB.SNO=SaleBill.SNO) and SB.SNO=S.SNO go （14）查询各校销售额。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 select College,sum(sum_Number_salePrice) 销售额 from Student S join( select SA.SNO,sum(SA.Number * G.SalePrice) sum_Number_salePrice from SaleBill SA join Goods G on SA.GoodsNO = G.GoodsNO group by SNO) g on S.SNO = g.SNO group by College go select College, Sum(SaleBill.Number*Goods.SalePrice) from SaleBill,Goods,Student where SaleBill.GoodsNO=Goods.GoodsNO and SaleBill.SNO=Student.SNO group by College go （15）查询购买额前三的校名、专业名。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 select top 3 S.College,S.Major,sum(sum_Number_salePrice) amount from Student S join ( select SA.SNO,sum(SA.Number * G.SalePrice) sum_Number_salePrice from SaleBill SA join Goods G on SA.GoodsNO = G.GoodsNO group by SNO) g on S.SNO = g.SNO group by College,Major order by amount desc go select top 3 college,major,sum(saleprice*salebill.number) amount from salebill,student,goods where salebill.sno=student.sno and salebill.goodsno=goods.goodsno group by college,major order by amount desc go （16）使用集合查询方式查询生产日期早于2018-1-1 或库存量小于30的商品信息。\n1 2 3 4 5 6 7 8 9 select * from Goods where ProductTime \u0026lt;\u0026#39;2018-1-1\u0026#39; union select * from Goods where Number \u0026lt;30 go select * from goods where datediff(day,\u0026#39;2018-1-1\u0026#39;,producttime)\u0026lt;0 union select * from goods where number\u0026lt;30 go ","permalink":"https://ktzxy.top/posts/9v82ah4b0i/","summary":"实验4 SQL的数据查询功能","title":"实验4 SQL的数据查询功能"},{"content":" MySQL在处理join查询时，遍历驱动表的记录，把驱动表的记录传递给被驱动表，然后根据join连接条件进行匹配。优化器通常会将更小的表作为驱动表，通过在驱动表上做额外的where条件过滤（Condition Filtering），能够将驱动表限制在一个更小的范围，以便优化器能够做出更优的执行计划。\n1. 什么是条件过滤(Condition Filtering) 如果没有使用条件过滤，join查询的驱动表预估扫描的记录数与索引条件相关，比如一个二级索引 idx_name(name)，name=\u0026lsquo;abc\u0026rsquo; 的记录数有100个，那么执行计划中的预估扫描记录数就是100左右。如果此时where条件中关于驱动表有另外一个条件限制，比如age=20，满足name=\u0026lsquo;abc\u0026rsquo;且age=20的记录数为10，通过条件过滤后，实际参与到join运算的驱动表记录数只有10条左右。\n条件过滤有一些限制：\n条件只能是常量 条件过滤中的where条件不在索引条件中 2. 条件过滤在explain中的表现 在explain的输出中，rows字段表示所选择的索引访问方式预估的扫描记录数，filtered字段反映了条件过滤，filtered值是一个百分比，最大值是100，表示没有进行任何过滤，该值越小，说明过滤效果越好。\n如果一个SQL的执行计划，rows为100，filtered为10%，那么最终预估的扫描记录数为 100*10%=10。\n3. 条件过滤案例 有两张表做join查询，employee 为雇员表，department为部门表，查询SQL如下：\n1 2 3 4 SELECT * FROM employee JOIN department ON employee.dept_no = department.dept_no WHERE employee.first_name = \u0026#39;John\u0026#39; AND employee.hire_date BETWEEN \u0026#39;2018-01-01\u0026#39; AND \u0026#39;2018-06-01\u0026#39;; employee表记录总数：1024 department表记录总数：12 两张表在dept_no字段上都有索引。 employee表在first_name上有索引。 满足 employee.first_name = \u0026lsquo;John\u0026rsquo; 的记录数：8 满足 employee.hire_date BETWEEN \u0026lsquo;2018-01-01\u0026rsquo; AND \u0026lsquo;2018-06-01\u0026rsquo; 的记录数：150 满足 employee.first_name = \u0026lsquo;John\u0026rsquo; AND employee.hire_date BETWEEN \u0026lsquo;2018-01-01\u0026rsquo; AND \u0026lsquo;2018-06-01\u0026rsquo; 记录数：1 （1）如果没有使用条件过滤，explain执行计划如下：\n1 2 3 4 5 6 +----+------------+--------+------------------+---------+---------+------+----------+ | id | table | type | possible_keys | key | ref | rows | filtered | +----+------------+--------+------------------+---------+---------+------+----------+ | 1 | employee | ref | name,h_date,dept | name | const | 8 | 100.00 | | 1 | department | eq_ref | PRIMARY | PRIMARY | dept_no | 1 | 100.00 | +----+------------+--------+------------------+---------+---------+------+----------+ （2）使用了条件过滤，explain执行计划如下：\n1 2 3 4 5 6 +----+------------+--------+------------------+---------+---------+------+----------+ | id | table | type | possible_keys | key | ref | rows | filtered | +----+------------+--------+------------------+---------+---------+------+----------+ | 1 | employee | ref | name,h_date,dept | name | const | 8 | 16.31 | | 1 | department | eq_ref | PRIMARY | PRIMARY | dept_no | 1 | 100.00 | +----+------------+--------+------------------+---------+---------+------+----------+ 很明显，表employee上的filtered 由 100 变为了 16.31，8 × 16.31% = 1.3，过滤效果非常好。\n4. 条件过滤开关 MySQL提供了参数来控制是否打开条件过滤，默认是打开的。\nSET optimizer_switch = \u0026lsquo;condition_fanout_filter=on\u0026rsquo;;\n打开条件过滤有时并不总是能提高性能，优化器可能会高估条件过滤的影响，个别场景下使用条件过滤反而会导致性能下降。在排查类似性能问题时，可参考以下思路：\njoin连接的字段是否有索引，如果没有索引，则应当先加上索引，以便优化器能够掌握字段值的分布情况，更准确的预估行数。 表的join顺序是否合适，通过改变表的join顺序，让更小的表作为驱动表。可以考虑使用STRAIGHT_JOIN，强制优化器使用指定的表join顺序。 如果不使用条件过滤，性能会更好，那么可以关闭会话级条件过滤功能。 SET optimizer_switch = \u0026lsquo;condition_fanout_filter=off\u0026rsquo;; ","permalink":"https://ktzxy.top/posts/huqum0gouq/","summary":"MySQL性能优化 条件过滤(Condition Filtering)","title":"MySQL性能优化 条件过滤(Condition Filtering)"},{"content":"Prometheus和Grafana介绍 系统监控 gopsutil：做系统监控信息的采集，写入influxDb，使用grafana作展示\nprometheus：采集性能指标数据，使用grafana作展示\nPrometheus 普罗米修斯：专用于服务监控，主动去拉取数据\nJobs/Exporters：任务，监控项 Serveice Discovery：服务发现 Short-lived jobs：短期存活的任务 下载普罗米修斯 去Github的官网下载\n下载完成后解压即可\n双击 prometheus.exe启动，然后访问下面的地址\n1 http://localhost:9090/graph 进入到prometheus的图形化页面\n使用 就能够看到我们的图形化信息了\n通过上图我们发现，使用 prometheus的图形化界面好像不太美观，所以就引出了下面的 grafana\ngrafana grafana是采用go语言编写的，非常美观的图形化展示，我们找到官网下载，选择window环境\n解压后的目录，如下所示\n我们进入bin目录，找到 grafana-server.exe 然后启动 【首次启动比较慢，需要建立数据库】，启动成功后，访问下面的地址\n1 http://127.0.0.1:3000 即可进入到grafana的图形化页面了\n然后输入admin admin 登录即可\n然后选择普罗米修斯\n然后输入url保存\n然后导入我们的普罗米修斯的仪表盘\n然后到Home目录下，选择 new board\n选择刚刚的import 的 仪表信息\n这样就生成了我们的仪表信息了\n或者可以选择另外一个样式\n结语 如果我们要监控其它的一些服务，比如redis、mysql、Memcache等等，需要自己到官网下载对应的包\nhttps://prometheus.io/download/\n","permalink":"https://ktzxy.top/posts/07rt7r21om/","summary":"Prometheus和Grafana介绍","title":"Prometheus和Grafana介绍"},{"content":"etcd介绍 前言 etcd：目前比较火的开源库，docker和k8s都是使用的它\n目标：使用etcd优化日志收集项目\n介绍 etcd是使用Go语言开发的一个开源的、高可用的分布式key-value存储系统，可以用于配置共享和服务的注册和发现，类似项目有Zookeeper和consul\netcd具有以下特点\n完全复制：集群中的每个节点都可以使用完整的存档 高可用性：Etcd可用于避免硬件的单点故障或网络问题（选择出另外的leader） 一致性：每次读取都会返回跨多主机的最新写入 简单：包括一个定义良好、面向用户的API（gRPC） 安全：实现了带有可选的客户端证书身份验证的自动化TLS 快速：每秒10000次写入的基准速度 可靠：使用 Raft 算法实现了强一致性，高可用的服务存储目录 Raft协议：选举、日志复制机制、异常处理（脑裂）、Zookeeper的zad协议的区别 etcd应用场景 服务发现 服务发现要解决的也是分布式系统中最常见的问题之一，即在同一个分布式集群中的进程或服务，要如何才能找到对方并建立连接。本质上来说，服务发现就是想要了解集群中是否有进程在监听udp或tcp端口，并且通过名字就可以查找和连接。\n配置中心 将一些配置信息放到etcd上进行集中管理。这类场景的使用方式通常是这样的：应用启动的时候，主动从etcd获取一次配置信息，同时，在etcd节点上注册一个Watcher并等待，以后每次配置有更新的时候，etcd都会实时通知订阅者，以此达到获取最新配置信息的目的。\n分布式锁 因为etcd使用Raft算法保持了数据的强一致性，某次操作存储到集群中的值必然是全局一致性的，所以很容易实现分布式锁。锁服务有两种使用方式，一种是保持独占，二是控制时序。\n保持独占即所有获取锁的用户最终只有一个可以得到。etcd为此提供了一套实现分布式锁原子操作CAS（CompareAndSwap）的API。通过设置preExist值，可以保证在多个节点同时去创建某个目录时，只有一个成功，而创建成功的用户就可以认为是获得了锁。 控制时序，即所有想要获得锁的用户都会被安排执行，但是获得锁的顺序也是全局唯一的，同时决定了执行顺序。etcd为此也提供了一套API（自动创建有序键），对一个目录建值时指定为POST动作，这样etcd会自动在目录下生成一个当前最大的键值。此时这些键的值就是客户端的时序，而这些键中的存储的值可以代表客户端的编号。 上图就是三个同时来竞争锁，最后只有一个获取到了锁\n为什么使用etcd而不用Zookeeper？ Zookeeper存在的问题 etcd 实现的这些功能，ZooKeeper都能实现。那么为什么要用etcd 而非直接使用ZooKeeper呢？相较之下，ZooKeeper有如下缺点：\n复杂：ZooKeeper的部署维护复杂，管理员需要掌握一系列的知识和技能；而Paxos 强一致性算法也是素来以复杂难懂而闻名于世；另外，ZooKeeper的使用也比较复杂，需要安装客户端，官方只提供了Java和C两种语言的接口。 Java编写：这里不是对Java有偏见，而是Java本身就偏向于重型应用，它会引入大量的依赖。而运维人员则普遍希望保持强一致、高可用的机器集群尽可能简单，维护起来也不易出错。 发展缓慢：Apache 基金会项目特有的\u0026quot;Apache Way”在开源界饱受争议，其中一大原因就是由于基金会庞大的结构以及松散的管理导致项目发展缓慢。 etcd的优势 而etcd作为一个后起之秀，其优点也很明显\n简单：使用Go语言编写部署简单；使用HTTP作为接口使用简单；使用Raft 算法保证强一致性让用户易于理解。\n数据持久化：etcd 默认数据一更新就进行持久化。\n安全：etcd 支持SSL客户端安全认证。\n最后，etcd作为一个年轻的项目，真正告诉迭代和开发中，这既是一个优点，也是一个缺点。优点是它的未来具有无限的可能性，缺点是无法得到大项目长时间使用的检验。然而，目前CoreOS、Kubernetes 和CloudFoundry等知名项目均在生产环境中使用了etcd，所以总的来说，etcd值得你去尝试。\netcd架构 从etcd的架构图中我们可以看到，etcd主要分为四个部分\nHTTP Server：用于处理用户发送的API请求以及其他etcd节点的同步与心跳信息请求 Store：用于处理etcd支持的各类功能的事务，包括数据索引、节点状态变更、监控与反馈、事件处理与执行等等，是etcd对用户提供的大多数API功能的具体实现 Raft：Raft强一致性算法的具体实现，是etcd的核心 WAL：Write Ahead Log（预写式日志），是etcd的数据存储方式。除了在内存中存有所有数据的状态以及节点的索引以外，etcd就通过WAL进行持久化存储。WAL中，所有的数据提交前都会实现记录日志。Snapshot是为了防止数据过多而进行的状态快照；Entry表示存储的具体日志内容。 etcd集群 etcd作为一个高可用键值存储系统，天生就是为集群化而设计的，由于Raft算法在做决策时需要多数节点投票，所以etcd一般部署集群推荐奇数个节点，推荐的数量为3、5或者7个节点构成一个集群。\n搭建一个3节点集群示例 在每个etcd节点指定集群成员，为了区分不同的集群最好同时配置一个独一无二的token。\n下面是提前定义好的集群信息，其中n1、n2和n3表示3个不同的etcd节点。\n在n1这台机器上执行以下命令来启动etcd：\n在n2这台机器上执行以下命令启动etcd：\n在n3这台机器上执行以下命令启动etcd：\netcd官网提供了一个公网访问的etcd存储地址，你可以通过如下命令得到etcd服务的目录，并把它作为-discovery参数使用\netcd部署 下载 找到对应的 Github官网，到相应的releases，找到windows平台的压缩包进行下载\n解压完成后的目录\n启动 双击etcd.exe就是启动了etcd。其它平台解压之后在bin目录下找etcd可执行文件。\n默认会在2379端口监听客户端通信，在2380端口监听节点间的通信。\netcdctl.ext可以理解为一个客户端或者本机etcd的控制端\n连接 默认的etcdctrl使用的是v2版本的命令，我们需要设置环境变量ETCDCTL_API=3来使用v3版本的API，而默认的也就是环境变量为ETCDCTL_API=2是使用v2版本的API\n修改环境变量指定使用API的版本\n1 SET_ETCDCTL_API=3 简单使用 put：设置 1 .\\etcdctl.exe --endpoints=http://127.0.0.1:2379 put baodelu \u0026#34;dsb\u0026#34; 显示设置成功~\nget：获取 1 .\\etcdctl.exe --endpoints=http://127.0.0.1:2379 get baodelu del：删除 1 .\\etcdctl.exe --endpoints=http://127.0.0.1:2379 del baodelu Go操作etcd 安装依赖 这里使用官方的 etcd/clientv3包来连接etcd并进行相关操作\n1 go get go.etcd.io/etcd/clientv3 put和get操作 put命令用来设置键值对数据，get命令用来根据key获取值\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;go.etcd.io/etcd/clientv3\u0026#34; \u0026#34;time\u0026#34; ) func main() { cli, err := clientv3.New(clientv3.Config { Endpoints: []string{\u0026#34;127.0.0.1:2379\u0026#34;}, // etcd的节点，可以传入多个 DialTimeout: 5*time.Second, // 连接超时时间 }) if err != nil { fmt.Printf(\u0026#34;connect to etcd failed, err: %v \\n\u0026#34;, err) return } fmt.Println(\u0026#34;connect to etcd success\u0026#34;) // 延迟关闭 defer cli.Close() // put操作 设置1秒超时 ctx, cancel := context.WithTimeout(context.Background(), time.Second) _, err = cli.Put(ctx, \u0026#34;moxi\u0026#34;, \u0026#34;lalala\u0026#34;) cancel() if err != nil { fmt.Printf(\u0026#34;put to etcd failed, err:%v \\n\u0026#34;, err) return } // get操作，设置1秒超时 ctx, cancel = context.WithTimeout(context.Background(), time.Second) resp, err := cli.Get(ctx, \u0026#34;q1mi\u0026#34;) cancel() if err != nil { fmt.Printf(\u0026#34;get from etcd failed, err:%v \\n\u0026#34;, err) return } fmt.Println(resp) } 错误实例 在我们运行代码的时候，突然出错了，undefined: resolver.BuildOption\n经过排查，是因为 google.golang.org/grpc 1.26后的版本不支持clientv3的\n所以我们只能将其改成1.26版本的就可以了，具体操作需要在go.mod上加上以下代码\n1 replace google.golang.org/grpc =\u0026gt; google.golang.org/grpc v1.26.0 watch 使用watch可以做服务的热更新\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;go.etcd.io/etcd/clientv3\u0026#34; \u0026#34;time\u0026#34; ) // etcd 的watch操作 func main() { cli, err := clientv3.New(clientv3.Config { Endpoints: []string{\u0026#34;127.0.0.1:2379\u0026#34;}, // etcd的节点，可以传入多个 DialTimeout: 5*time.Second, // 连接超时时间 }) if err != nil { fmt.Printf(\u0026#34;connect to etcd failed, err: %v \\n\u0026#34;, err) return } fmt.Println(\u0026#34;connect to etcd success\u0026#34;) defer cli.Close() // watch // 派一个哨兵，一直监视着 moxi 这个key的变化（新增，修改，删除），返回一个只读的chan ch := cli.Watch(context.Background(), \u0026#34;moxi\u0026#34;) // 从通道中尝试获取值（监视的信息） for wresp := range ch{ for _, evt := range wresp.Events{ fmt.Printf(\u0026#34;Type:%v key:%v value:%v \\n\u0026#34;, evt.Type, evt.Kv.Key, evt.Kv.Value) } } } 然后我们往etcd中插入数据的时候\n我们的代码就会监听到数据的变化\n使用etcd优化日志项目 logagent根据etcd的配置创建多个tailtask 见代码部分 18_LogAgent\netcd底层如何实现watch给客户端发通知（websocket）\nlogagent根据IP拉取配置 ","permalink":"https://ktzxy.top/posts/ygfkrsmmz3/","summary":"etcd介绍","title":"etcd介绍"},{"content":" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 git init # 初始化本地git仓库（创建新仓库） git config --global user.name \u0026#34;xxx\u0026#34; # 配置用户名 git config --global user.email \u0026#34;xxx@xxx.com\u0026#34; # 配置邮件 git config --global color.ui true # git status等命令自动着色 git config --global color.status auto git config --global color.diff auto git config --global color.branch auto git config --global color.interactive auto git config --global --unset http.proxy # remove proxy configuration on git git clone git+ssh://git@192.168.53.168/VT.git # clone远程仓库 git status # 查看当前版本状态（是否修改） git add xyz # 添加xyz文件至index git add . # 增加当前子目录下所有更改过的文件至index git commit -m \u0026#39;xxx\u0026#39; # 提交 git commit --amend -m \u0026#39;xxx\u0026#39; # 合并上一次提交（用于反复修改） git commit -am \u0026#39;xxx\u0026#39; # 将add和commit合为一步 git rm xxx # 删除index中的文件 git rm -r * # 递归删除 git log # 显示提交日志 git log -1 # 显示1行日志 -n为n行 git log -5 git log --stat # 显示提交日志及相关变动文件 git log -p -m git show dfb02e6e4f2f7b573337763e5c0013802e392818 # 显示某个提交的详细内容 git show dfb02 # 可只用commitid的前几位 git show HEAD # 显示HEAD提交日志 git show HEAD^ # 显示HEAD的父（上一个版本）的提交日志 ^^为上两个版本 ^5为上5个版本 git tag # 显示已存在的tag git tag -a v2.0 -m \u0026#39;xxx\u0026#39; # 增加v2.0的tag git show v2.0 # 显示v2.0的日志及详细内容 git log v2.0 # 显示v2.0的日志 git diff # 显示所有未添加至index的变更 git diff --cached # 显示所有已添加index但还未commit的变更 git diff HEAD^ # 比较与上一个版本的差异 git diff HEAD -- ./lib # 比较与HEAD版本lib目录的差异 git diff origin/master..master # 比较远程分支master上有本地分支master上没有的 git diff origin/master..master --stat # 只显示差异的文件，不显示具体内容 git remote add origin git+ssh://git@192.168.53.168/VT.git # 增加远程定义（用于push/pull/fetch） git branch # 显示本地分支 git branch --contains 50089 # 显示包含提交50089的分支 git branch -a # 显示所有分支 git branch -r # 显示所有原创分支 git branch --merged # 显示所有已合并到当前分支的分支 git branch --no-merged # 显示所有未合并到当前分支的分支 git branch -m master master_copy # 本地分支改名 git checkout -b master_copy # 从当前分支创建新分支master_copy并检出 git checkout -b master master_copy # 上面的完整版 git checkout features/performance # 检出已存在的features/performance分支 git checkout --track hotfixes/BJVEP933 # 检出远程分支hotfixes/BJVEP933并创建本地跟踪分支 git checkout v2.0 # 检出版本v2.0 git checkout -b devel origin/develop # 从远程分支develop创建新本地分支devel并检出 git checkout -- README # 检出head版本的README文件（可用于修改错误回退） git merge origin/master # 合并远程master分支至当前分支 git cherry-pick ff44785404a8e # 合并提交ff44785404a8e的修改 git push origin master # 将当前分支push到远程master分支 git push origin :hotfixes/BJVEP933 # 删除远程仓库的hotfixes/BJVEP933分支 git push --tags # 把所有tag推送到远程仓库 git fetch # 获取所有远程分支（不更新本地分支，另需merge） git fetch --prune # 获取所有原创分支并清除服务器上已删掉的分支 git pull origin master # 获取远程分支master并merge到当前分支 git mv README README2 # 重命名文件README为README2 git reset --hard HEAD # 将当前版本重置为HEAD（通常用于merge失败回退） git rebase git branch -d hotfixes/BJVEP933 # 删除分支hotfixes/BJVEP933（本分支修改已合并到其他分支） git branch -D hotfixes/BJVEP933 # 强制删除分支hotfixes/BJVEP933 git ls-files # 列出git index包含的文件 git show-branch # 图示当前分支历史 git show-branch --all # 图示所有分支历史 git whatchanged # 显示提交历史对应的文件修改 git revert dfb02e6e4f2f7b573337763e5c0013802e392818 # 撤销提交dfb02e6e4f2f7b573337763e5c0013802e392818 git ls-tree HEAD # 内部命令：显示某个git对象 git rev-parse v2.0 # 内部命令：显示某个ref对于的SHA1 HASH git reflog # 显示所有提交，包括孤立节点 git show HEAD@{5} git show master@{yesterday} # 显示master分支昨天的状态 git log --pretty=format:\u0026#39;%h %s\u0026#39; --graph # 图示提交日志 git show HEAD~3 git show -s --pretty=raw 2be7fcb476 git stash # 暂存当前修改，将所有至为HEAD状态 git stash list # 查看所有暂存 git stash show -p stash@{0} # 参考第一次暂存 git stash apply stash@{0} # 应用第一次暂存 git grep \u0026#34;delete from\u0026#34; # 文件中搜索文本“delete from” git grep -e \u0026#39;#define\u0026#39; --and -e SORT_DIRENT git gc git fsck git remote rename \u0026lt;oldname\u0026gt; \u0026lt;newname\u0026gt; # 重命名远程仓库名字 ","permalink":"https://ktzxy.top/posts/zvp6wa3pp2/","summary":"git常用命令","title":"git常用命令"},{"content":"﻿\nDay-02-java基础语法 快捷键操作\npsvm \u0026ndash;\u0026gt;快速生成public static void main(String[] args) {}\nsout \u0026ndash;\u0026gt;快速生成System.out.println();\n可能会遇到的问题\n每个单词的大小不能出现问题，==Java是大小写敏感的==； 尽量使用英文； 文件名和类名必须保证一致，并且首字母大写； 符号使用的了中文。 Java运行机制\n编译型 解释型 注释： Java中的注释有三种：\n1 2 3 4 5 6 7 8 9 10 注释： 单行注释： //我是单行注释 多行注释 /*我是多行注释*/ 文档注释 /** *@description HelloWrold *@Author 作者 */ 标识符： 关键字\nJava所有的组成部分都需要名字。类名、变量名以及方法名都被称为标识符。\n标识符注意点\n所有的标识符都应该以**字母(A-Z或者a-z),美元符（$)、数字或者下划线(_)**开始 首字符之后可以是字母（A-Z或者a-z),美元符（$)、下划线(_)或数字的任何字符组合\n不能使用关键字作为变量名或方法名。\n标识符是大小写敏感的\n合法标识符举例: age、$salary._value、__1_value\n非法标识符举例:123abc、-salary、#abc\n遵照驼峰命名：\n类名：首字母大写，其余遵循驼峰命名\n方法名，变量名：首字母小写，其余遵循驼峰命名\n包名：全部小写，不遵循驼峰命名\n关键字 abstract boolean break byte case catch char const class continue default do double else extends final finally float for goto if implements import instanceof int interface long native new package private protected public return short static strictfp super switch this throw throws transient try void volatile while synchronized 数据类型 Java的数据类型分为两大类 基本类型(primitive type)\n​\t引用类型(reference type)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //八大基本数据类型 //整数 int num1 = 10; byte num2 = 20; short num3 = 30; long num4 = 40L; //long类型要在数字后面加上L //浮点数 float num5 = 50.1F; //float类型要在数字后面加上F，浮点数默认是double类型的。 double num6 = 6.15234; double num7 = 314E-2; //科学计数法 //字符，java中无论：字母，数字，符号，中文都是字符类型的常量，都占用2个字节 char name = \u0026#39;A\u0026#39;; //字符串,String 不是关键字，而是类 String names = \u0026#34;张三\u0026#34;; //布尔值 boolean flag = true; 什么是字节\n位（bit）：是计算机内部数据储存的最小单位，11001100是一个八位二进制数。 字节（byte）：是计算机中数据处理的基本单位，习惯上用大写B来表示，1B（byte字节）=8bit（位）。 字符：是指计算机中使用的字母、数字、字和符号。 1bit表示1位 1Byte表示一个字节1B=8b 1024B=1KB 1024KB=1M 1024M=1G 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 //整数拓展： 进制 二进制0b 十进制 八进制0 十六进制0x int i = 10; int i2 = 010; //八进制0 int i3 = 0x10; //十六进制 0~9 A~F 16 //浮点数拓展 //float 有限 离散 舍入误差 大约 接近但不等于 //最好不要使用浮点数进行比较，因为他们字节不相同 float f = 0.1f; //0.1 double d = 1.0/10; //0.1 System.out.println(f==d); //false System.out.println(f); System.out.println(d); //字符拓展 char c1 = \u0026#39;a\u0026#39;; char c2 = \u0026#39;中\u0026#39;; System.out.println(c1); System.out.println((int)c1); //强制转换 97 System.out.println(c2); System.out.println((int)c2); //强制转换 20013 //所有的字符本质还是数字 //转义字符 // \\t 制表符 // \\n 换行 // \\b 退格 // \\r 将光标到本行开头：回车 // \\\u0026#34; 双引号 // \\\u0026#39; 单引号 // \\\\ 反斜杠 String a1 = new String(\u0026#34;hello world\u0026#34;); String b1 = new String(\u0026#34;hello world\u0026#34;); System.out.println(a1==b1); //false String s1 = \u0026#34;hello world\u0026#34;; String s2 = \u0026#34;hello world\u0026#34;; System.out.println(s1==s2); //true //布尔值扩展 boolean flag = true; if (flag==true){} //新手 if (flag){}; //老手 ============================================= public static void main(String[] args){ //定义整数类型的变量 int num1 = 12; //默认情况下赋值就是十进制的情况 int num1 = 012; //前面加上0，这个值就是八进制的 int num1 = 0x12; //前面加上0x或者0X，这个值就是十六进制的 int num1 = 0b12; //前面加上0b或者0B，这个值就是二进制的 } 类型转换 由于java是强类型语言，所以要进行有些运算的时候，需要用到类型转换\n低\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026gt;高\nbyte,short,char——\u0026gt;int——\u0026gt;long——\u0026gt;float——\u0026gt;double\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 int i = 128; byte b = (byte)i; //内存溢出 //强制转换 （类型）变量名 高----\u0026gt;低 System.out.println(i); System.out.println(b); //自动转换 低----\u0026gt;高 /* 注意点： 1.不能对布尔值进行转换 2.不能把对象类型转换为不相干的类型 3.在把高容量转换到低容量的时候，强制转换 4.转换的时候可能存在内存溢出，或者精度问题 * */ System.out.println((int)23.7); System.out.println((int)-45.7f); //操作比较大的数的时候，注意溢出问题 //JDK新特性，数字之间可以用下划线分割 int money = 10_0000_0000; int year = 20; int total = money*year; //-1474836480 long total1 = money*year; //默认是int，转换之前就存在问题 long total2 = money*(long)year; //先把一个数转换为long //在同一个表达式中，有多个数据类型的时候，应该如和处理： //多种数据类型参与运算的时候，整数类型，浮点类型，字符类型都可以参与运算，维度布尔类型不可以参与运算。 //当一个表达式中有多种数据类型的时候，要找出当前表达式中级别最高的那个类型，然后其余的类型都转换为当前表达式中级别最高的类型进行计算。 //\tdouble a = 12+1294L+8.5F+3.81+\u0026#39;a\u0026#39;; = 12.0+1294.0+8.5+3.81+97.0 //在进行运算的时候： 左=右 ：直接赋值 左\u0026lt;右 ：强转 左\u0026gt;右 ： 直接自动转换 //一下情况属于特殊情形：对于byte，short，char类型来说，只要在他们的表述范围内，赋值的时候不需要进行 //强转了直接复制即可。 byte b = 12； byte b2 = (byte)280; 变量 Java是一种强类型语言，每个变量都必须声明其类型。 Java变量是程序中最基本的存储单元，其要素包括变量名，变量类型和作用域。 注意事项: 每个变量都有类型，类型可以是基本类型，也可以是引用类型。 变量名必须是合法的标识符。 变量声明是一条完整的语句，因此每一个声明都必须以分号结束。\n​\t变量不可以重复定义\n变量作用域：\n​\t类变量\n​\t实例变量\n​\t局部变量\n作用范围就是离变量最近的{}\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 //类变量 static static double salary = 2500; //属性：变量 //实例变量：从属于对象：如果不自行初始化，这个类型的默认值 0 0.0 //布尔值：默认是false //除了基本类型，其余的默认值都是null String name; int age; //main方法 public static void main(String[] args) { //局部变量，必须声明和初始化值 int i = 10; System.out.println(i); //变量类型 变量名字=new Demo2(); Demo2 demo2= new Demo2(); System.out.println(demo2.age); System.out.println(demo2.name); //类变量 static System.out.println(salary); } //其他方法 public void add(){ System.out.println(); } 常量： 常量(Constant):初始化(initialize)后不能再改变值!不会变动的值。 所谓常量可以理解成一种特殊的变量，它的值被设定后，在程序运行过程中不允许被改变。\n常量名一般使用大写字符。\n1 2 3 4 5 //修饰符，不存在先后顺序 等同于final static double PI=3.14; static final double PI=3.14; public static void main(String[] args) { System.out.println(PI); } 变量的命名规范 所有变量、方法、类名:见名知意 类成员变量:首字母小写和驼峰原则: monthSalary除了第一个单词以外，后面的单词首字母大写 lastname lastName 局部变量}首字母小写和驼峰原则 常量:大写字母和下划线:MAvALUE .V类名:首字母大写和驼峰原则: Man, 99M4a\u0026quot;146.56KBs\n方法名:首字母小写和驼峰原则: run(), runRun()\n运算符 1 2 3 4 5 6 7 8 9 Java语言支持如下运算符: 二元运算符（+，-，*，/， %） 一元运算符（++，--） - 算术运算符:+，-，*，/， %，++（自增），--（自减） - 赋值运算符= - 关系运算符:\u0026gt;，\u0026lt;，\u0026gt;=，\u0026lt;=，==，!= ,instanceof - 逻辑运算符: \u0026amp;，|，^，\u0026amp;\u0026amp;，||，! - 位运算符:\u0026amp;，|，^，~，\u0026gt;\u0026gt;，\u0026lt;\u0026lt;，\u0026gt;\u0026gt;\u0026gt;(了解! ! ! ) - 条件运算符?: - 扩展赋值运算符:+=，-=，*=，/= 快捷键操作 Ctrl + D :复制当前行到下一行\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 // ++ -- 自增 自减 一元运算符 //++单独使用的时候，无论放在前还是后，都是加1操作 a++; ++a; //将++参与到运算中，看++在前还是在后，如果++在前，先加1，后运算，如果++在后，先运算，后加1 /* int a = 5; System.out.println(a++); //5 System.out.println(\u0026#34;此时的a为：\u0026#34;+a); //6 System.out.println(++a); //7 System.out.println(\u0026#34;此时的a为：\u0026#34;+a); //7 System.out.println(a--); //7 System.out.println(\u0026#34;此时的a为：\u0026#34;+a); //6 System.out.println(a++ + ++a); //14 System.out.println(\u0026#34;此时的a为：\u0026#34;+a); //8 System.out.println(--a); //7 System.out.println(\u0026#34;此时的a为：\u0026#34;+a); //7 System.out.println(a--); //7 System.out.println(\u0026#34;此时的a为：\u0026#34;+a); //6 System.out.println(--a + a--); //10 System.out.println(\u0026#34;此时的a为：\u0026#34;+a); //4 */ int a = 3; int b = a++; //执行完这行代码后，先给b赋值，再自增 //a++ 意味着 a = a+1 System.out.println(a); //4 //a = a + 1 int c = ++a; //执行完这行代码，先自增，再给b赋值 System.out.println(a); //5 System.out.println(b); //3 System.out.println(c); //5 //幂运算 2^3 2*2*2 = 8 double pow = Math.pow(2,3); System.out.println(pow); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 //逻辑运算符 //逻辑与：\u0026amp; 规律：只要有一个操作数是false，那么结果一定是false //短路与：\u0026amp;\u0026amp; 规律：效率高一些，只要第一个表达式是false，那么第二个表达式就不用计算了，结果一定是false //逻辑或：| 规律：只要有一个操作数是true，那么结果一定是true //短路或：|| 规律：效率高一些，只要第一个表达式是true，那么第二个表达式就不用计算了，结果一定是true //逻辑非：！ 规律：相反结果 //逻辑异或：^ 规律：两个操作数相同，结果为false，不相同，结果为true // 与（and） 或（or） 非（取反） boolean a = true; boolean b = false; System.out.println(\u0026#34;a \u0026amp;\u0026amp; b:\u0026#34;+(a\u0026amp;\u0026amp;b)); // false 逻辑与运算：俩个变量都为真，结果才为true System.out.println(\u0026#34;a || b:\u0026#34;+(a||b)); // true 逻辑或运算：俩个变量有一个为真，则结果才为true System.out.println(\u0026#34;! (a \u0026amp;\u0026amp; b):\u0026#34;+!(a\u0026amp;\u0026amp;b)); // true 如果是真，则变为假，如果是假则变为真 //短路运算 int c = 5; boolean d = (c\u0026lt;4)\u0026amp;\u0026amp;(c++\u0026lt;4); System.out.println(d); //false System.out.println(c); //5 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* 位运算符 A = 0011 1100 B = 0000 1101 A\u0026amp;B = 0000 1100 A|B = 0011 1101 A^B = 0011 0001 ~B = 1111 0010 2*8 = 16 2*2*2*2 \u0026lt;\u0026lt; 左移 \u0026gt;\u0026gt; 右移 0000 0000 0 0000 0001 1 0000 0010 2 0000 0011 3 0000 0100 4 0000 1000 8 0001 0000 16 */ System.out.println(2\u0026lt;\u0026lt;3); //16 有符号左移\n有符号右移\n无符号右移\n\u0026amp;与\n6\u0026amp;3=2\n|或\n6|3=7\n^异或\n6^3=3\n~反\n~6=-7\n1 2 3 4 5 6 7 8 9 拓展赋值运算符 int a = 10; int b = 20; a+=b; //a = a + b a-=b; //a = a - b System.out.println(a); //字符串连接符 + ，String System.out.println(\u0026#34;\u0026#34;+a+b); //1020 拼接 System.out.println(a+b+\u0026#34;\u0026#34;); //30 1 2 3 a+=b 和 a=a+b区别: (1)a+=b 可读性稍差 编译效率高 底层自动进行类型转换 (2)a=a+b 可读性好 编译效率低 手动进行类型转换 1 2 3 4 5 6 条件运算符 //x ? y : z //如果x==true，则结果为y，否则结果为z int score = 80; String type = score \u0026gt;60 ? \u0026#34;不及格\u0026#34;:\u0026#34;及格\u0026#34;; System.out.println(type); //不及格 运算符的优先级\n1 2 3 4 5 6 7 8 9 赋值\u0026lt;三目\u0026lt;逻辑\u0026lt;关系\u0026lt;算术\u0026lt;单目 案例： 5\u0026lt;6|\u0026#39;A\u0026#39;\u0026gt;\u0026#39;a\u0026#39;\u0026amp;\u0026amp;12*6\u0026lt;=45+23\u0026amp;\u0026amp;!true =5\u0026lt;6|\u0026#39;A\u0026#39;\u0026gt;\u0026#39;a\u0026#39;\u0026amp;\u0026amp;12*6\u0026lt;=45+23\u0026amp;\u0026amp;false =5\u0026lt;6|\u0026#39;A\u0026#39;\u0026gt;\u0026#39;a\u0026#39;\u0026amp;\u0026amp;72\u0026lt;=68\u0026amp;\u0026amp;false =true|false\u0026amp;\u0026amp;false\u0026amp;\u0026amp;false =true\u0026amp;\u0026amp;false\u0026amp;\u0026amp;false =false\u0026amp;\u0026amp;false =false 包机制 为了更好的组织类，java提供了包机制，用于区别类名的命名空间。\n一般利用公司域名倒置作为包名；\n为了能够使用某一个包的成员，我们需要在Java程序中明确导入该包，使用“import”语句可以达到此功能。\n1 2 3 4 5 6 7 import 包名.类名 import 包名.* //改包下的所有类 在JDK中，不同功能的类都放在不同的包中，其中Java的核心类主要放在java包及其子包下，Java扩展的大部分类都放在javax包及其子包下。 java.util:包含Java中大量工具类、集合类等，例如Arrays、List、Set等java.net:包含Java网络编程相关的类和接口。 java.io:包含了Java输入、输出有关的类和接口。 java.awt:包含用于构建图形界面(GUI）的相关类和接口。 除了上面提到的常用包，JDK中还有很多其他的包，比如数据库编程的java.sql包、编写GUI的javax.swing包等，JDK中所有包中的类构成了Java类库。 JavaDoc javadoc命令是用来生成自己API文档的API 参数信息：\n@ author作者名 @ version版本号 @ since指明需要最早使用的jdk版本 @ paran参数名 @ return返回值情况 @ throws异常抛出情况 练习题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import java.util.Scanner; public class Demo { public static void main(String[] args) { //求圆的周长和面积 //一个变量被final修饰，这个变量就变成了一个常量，这个常量的值就不可变了 final double pi = 3.14; Scanner sc = new Scanner(System.in); System.out.print(\u0026#34;请录入一个半径：\u0026#34;); int r = sc.nextInt(); //圆的周长 double c = 2*pi*r; System.out.println(\u0026#34;圆的周长\u0026#34;+c); //圆的面积 double s = pi*r*r; System.out.println(\u0026#34;圆的面积\u0026#34;+s); } } 练习题二 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void main(String[] args) { //实现：任意给出一个四位数，求出每位上的数字并输出 Scanner sc = new Scanner(System.in); System.out.print(\u0026#34;请输入一个四位数：\u0026#34;); int num = sc.nextInt(); int num1 = num%10; int num2 = num/10%10; int num3 = num/100%10; int num4 = num/1000; System.out.println(\u0026#34;个位上的数字为\u0026#34;+num1); System.out.println(\u0026#34;十位上的数字为\u0026#34;+num2); System.out.println(\u0026#34;百位上的数字为\u0026#34;+num3); System.out.println(\u0026#34;千位上的数字为\u0026#34;+num4); } ","permalink":"https://ktzxy.top/posts/3qcgapmddw/","summary":"Day 02 java基础语法","title":"Day 02 java基础语法"},{"content":"Redis6 [TOC]\nNoSQL 数据库 NoSQL数据库概述 NoSQL ( NoSQL = Not Only SQL )，意即 “不仅仅是SQL”，泛指非关系型的数据库。\nNoSQL 不依赖业务逻辑方式存储，而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。\n不遵循SQL标准。 不支持ACID。 远超于SQL的性能。 NoSQL适用场景 对数据高并发的读写 海量数据的读写 对数据高可扩展性的 NoSQL不适用场景 需要事务支持 基于sql的结构化查询存储，处理复杂的关系,需要即席查询。 （用不着sql的和用了sql也不行的情况，请考虑用NoSql）\nNoSQL 产品概述 Memcache\nRedis\nMongoDB\nRedis6 概述安装 Redis是一个开源的key-value存储系统。 和Memcached类似，它支持存储的value类型相对更多，包括string(字符串)、list(链表)、set(集合)、zset(sorted set \u0026ndash;有序集合)和hash（哈希类型）。 这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作，而且这些操作都是原子性的。 在此基础上，Redis支持各种不同方式的排序。 与memcached一样，为了保证效率，数据都是缓存在内存中。 区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件。 并且在此基础上实现了master-slave(主从)同步。 Redis6 安装 笔记使用的是 CentOS7 版本的 Linux，Redis 6.2.3。\n官网下载 redis，官网只提供 Linux 版本的压缩包，学习 redis 前需要会基本的 Linux 操作。\n使用 Xfpt 传输压缩包到 Linux 系统中\n安装 C 语言编译环境\ngcc --version 查看 gcc 版本，有信息输出就代表有 C 语言编译环境 按顺序输入下面的指令安装 gcc yum install centos-release-scl scl-utils-build yum install -y devtoolset-8-toolchain scl enable devtoolset-8 bash 如果安装过程中有提示都输入 y gcc --version 查看 gcc 版本，检查是否安装成功 tar -zxvf redis-6.2.3.tar.gz 解压\ncd redis-6.2.3 进入目录\n在 redis-6.2.3 目录下再次执行 make 命令（只是编译好）\n如果没有准备好C语言编译环境，make 会报错— Jemalloc/jemalloc.h：没有那个文件 解决方案：运行 make distclean 清除编译文件 在 redis-6.2.3 目录下再次执行 make 命令（只是编译好） make install 安装\ncd /usr/local/bin，如果这个目录中有文件就表示安装成功。Redis 默认安装在这个目录\n1 2 3 4 5 6 7 # /usr/local/bin中文件的作用 redis-benchmark:性能测试工具，可以在自己本身运行，看看自己本身性能如何 redis-check-aof：修复有问题的AOF文件，rdb和aof后面讲 redis-check-dump：修复有问题的dump.rdb文件 redis-sentinel：Redis集群使用 redis-server：Redis服务器启动命令 redis-cli：客户端，操作入口 Redis6 启动 前台启动 ( 不推荐 ) 执行 /usr/local/bin 目录下的 redis-server 文件即可。/usr/local/bin/redis-server 命令启动。\n前台启动，命令行窗口不能关闭，否则服务器停止\n后台启动 ( 推荐 ) cd /opt/redis/redis-6.2.3/ 进入目录 cp redis.conf /etc/redis.conf 拷贝一份 redis.conf 到其他目录 cd /etc 进入拷贝的 redis.conf 的存放目录 vim redis.conf 编辑文件 /daemonize 搜索，将 daemonize no 的值改为 daemonize yes cd /usr/local/bin 进入目录 redis-server /etc/redis.conf 启动 Redis /usr/local/bin/redis-cli 访问 Redis，进入 Redis 终端 再输入 ping 如果显示 PONG 表示正常 单实例关闭：redis-cli shutdown 多实例关闭，指定端口关闭：redis-cli -p 6379 shutdown 也可以进入终端后 shutdown 进行关闭 密码设置 config set requirepass 密码 学习阶段可以不设置\nauth 密码 认证后才能操作\nRedis介绍相关知识 Redis 默认16个数据库，类似数组下标从0开始，初始默认使用 0 号库 使用命令 select dbid 来切换数据库。如: select 8 统一密码管理，所有库同样密码。 dbsize 查看当前数据库的 key 的数量 flushdb 清空当前库 flushall 通杀全部库 Redis 是单线程 + 多路IO复用技术\n多路复用是指使用一个线程来检查多个文件描述符（Socket）的就绪状态，比如调用select和poll函数，传入多个文件描述符，如果有一个文件描述符就绪，则返回，否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行，也可以启动线程执行（比如使用线程池）\n串行 vs 多线程+锁（memcached） vs 单线程+多路IO复用(Redis)\n( 与Memcache三点不同: 支持多数据类型，支持持久化，单线程+多路IO复用 )\n常用五大数据类型 http://www.redis.cn/commands.html 获得 redis 常见数据类型操作命令\nPS：第一个 Key 不是 Redis 的数据类型\nRedis 键 ( key ) redis-server /etc/redis.conf 启动 Redis，/usr/local/bin/redis-cli 进入到 Redis 终端。演示 Redis 针对 Key 的基本命令。\nkeys *：查看当前库所有 key ( 匹配：keys * 1 )\nexists key：判断某个 key 是否存在。( 0：不存在，1：存在)\ntype key：查看你的 key 是什么类型\n返回值 描述 none 不存在 string 字符串 list 列表 set 集合 zset 有序集 hash 哈希集 del key：删除指定的 key 数据，立刻删除\nunlink key：删除指定的 key 数据，根据 value 选择非阻塞删除。仅将 keys 从 keyspace 元数据中删除，真正的删除会在后续异步操作。\nexpire key 10：为给定的 key 设置过期时间，单位秒\nttl key：查看还有多少秒过期，-1表示永不过期，-2表示已过期\nselect index：命令切换数据库\ndbsize：查看当前数据库的key的数量\nflushdb：清空当前库\nflushall：通杀全部库\nRedis字符串(String) 简介 String 是 Redis 最基本的类型，你可以理解成与 Memcached 一模一样的类型，一个key对应一个value。\nString 类型是二进制安全的。意味着 Redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。\nString 类型是 Redis 最基本的数据类型，一个 Redis 中字符串value最多可以是512M\n常用命令 set \u0026lt;key\u0026gt; \u0026lt;value\u0026gt;：添加键值对\n使用 set 命令时最多可以携带三个参数。分别是中括号里面的参数，每个中括号只能选择一个参数\n1 2 3 4 5 6 7 EX: key的超时秒数 PX: key的超时毫秒数，与EX互斥 NX: 当数据库中key不存在时，可以将key-value添加数据库 XX: 当数据库中key存在时，可以将key-value添加数据库，与NX参数互斥 GET: 添加到数据库后自动运行一次get命令 get \u0026lt;key\u0026gt;：查询对应键值\nappend \u0026lt;key\u0026gt; \u0026lt;value\u0026gt;：将给定的 \u0026lt;value\u0026gt; 追加到原值的末尾\nstrlen \u0026lt;key\u0026gt;：获得值的长度\nsetnx \u0026lt;key\u0026gt; \u0026lt;value\u0026gt;：只有在 key 不存在时 设置 key 的值\nincr \u0026lt;key\u0026gt;：将 key 中储存的数字值增1。只能对数字值操作，如果为空，新增值为1\ndecr \u0026lt;key\u0026gt;：将 key 中储存的数字值减1。只能对数字值操作，如果为空，新增值为-1\nincrby / decrby \u0026lt;key\u0026gt; \u0026lt;步长\u0026gt;：将 key 中储存的数字值增减。自定义步长\nmset \u0026lt;key1\u0026gt; \u0026lt;value1\u0026gt; \u0026lt;key2\u0026gt; \u0026lt;value2\u0026gt; ..... ：同时设置一个或多个 key-value对\nmget \u0026lt;key1 \u0026gt;\u0026lt;key2\u0026gt; \u0026lt;key3\u0026gt; .....：同时获取一个或多个 value\nmsetnx \u0026lt;key1\u0026gt; \u0026lt;value1\u0026gt; \u0026lt;key2\u0026gt; \u0026lt;value2\u0026gt; ..... ：同时设置一个或多个 key-value 对，当且仅当所有给定 key 都不存在。原子性，有一个失败则都失败\ngetrange \u0026lt;key\u0026gt; \u0026lt;起始位置\u0026gt; \u0026lt;结束位置\u0026gt;：获得值的范围，取值后进行字符串截取，类似 java 中的 substring，前包，后包\nsetrange \u0026lt;key\u0026gt; \u0026lt;起始位置\u0026gt; \u0026lt;value\u0026gt;：用 \u0026lt;value\u0026gt; 覆写 \u0026lt;key\u0026gt; 所储存的字符串值，从 \u0026lt;起始位置\u0026gt; 开始 ( 索引从0开始 )。\nsetex \u0026lt;key\u0026gt; \u0026lt;过期时间\u0026gt; \u0026lt;value\u0026gt;：设置键值的同时，设置过期时间，单位秒。\ngetset \u0026lt;key\u0026gt; \u0026lt;value\u0026gt;：以新换旧，设置了新值同时获得旧值。\n数据结构 String 的数据结构为简单动态字符串 ( Simple Dynamic String,缩写SDS )。是可以修改的字符串，内部结构实现上类似于 Java 的 ArrayList，采用预分配冗余空间的方式来减少内存的频繁分配 ( 扩容 )。\n如图中所示，内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度len。当字符串长度小于1M时，扩容都是加倍现有的空间，如果超过1M，扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。\nRedis 列表 ( List ) 简介 单键多值\nRedis 列表是简单的字符串列表，按照插入顺序排序。你可以添加一个元素到列表的头部（左边）或者尾部（右边）。元素是可以重复的。\n它的底层实际是个双向链表，对两端的操作性能很高，通过索引下标的操作中间的节点性能会较差。\n常用命令 lpush / rpush \u0026lt;key\u0026gt; \u0026lt;value1\u0026gt; \u0026lt;value2\u0026gt; \u0026lt;value3\u0026gt; .... ：从左\t2/ 右边插入一个或多个值。\nlpop/rpop \u0026lt;key\u0026gt;：从左边 / 右边吐出一个值。值在键在，值光键亡。\nrpoplpush \u0026lt;key1\u0026gt; \u0026lt;key2\u0026gt;：从 \u0026lt;key1\u0026gt; 列表右边吐出一个值，插到 \u0026lt;key2\u0026gt; 列表左边。\nlrange \u0026lt;key\u0026gt; \u0026lt;start\u0026gt; \u0026lt;stop\u0026gt;：按照索引下标获得元素 ( 从左到右 )。\n1 lrange mylist 0 -1: 0左边第一个，-1右边第一个 (0,-1表示获取所有) lindex \u0026lt;key\u0026gt; \u0026lt;index\u0026gt;：按照索引下标获得元素 ( 从左到右 )\nllen \u0026lt;key\u0026gt;：获得列表长度\nlinsert \u0026lt;key\u0026gt; before / after \u0026lt;value\u0026gt; \u0026lt;newvalue\u0026gt;：在 \u0026lt;value\u0026gt; 的 ( 前 / 后 ) 插入 \u0026lt;newvalue\u0026gt; 插入值\nlrem \u0026lt;key\u0026gt; \u0026lt;n\u0026gt; \u0026lt;value\u0026gt;：从左边删除 n 个 value ( 从左到右 )\nlset \u0026lt;key\u0026gt; \u0026lt;index\u0026gt; \u0026lt;value\u0026gt;：将列表 key 下标为 index 的值替换成 value\n数据结构 List 的数据结构为快速链表 quickList。\n首先在列表元素较少的情况下会使用一块连续的内存存储，这个结构是 ziplist，也即是压缩列表。\n它将所有的元素紧挨着一起存储，分配的是一块连续的内存。\n当数据量比较多的时候才会改成 quicklist。\n因为普通的链表需要的附加指针空间太大，会比较浪费空间。比如这个列表里存的只是int类型的数据，结构上还需要两个额外的指针 prev 和next。\nRedis 将链表和 ziplist 结合起来组成了 quicklist。也就是将多个 ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能，又不会出现太大的空间冗余。\nRedis 集合 ( Set ) 简介 Redis set对外提供的功能与list类似是一个列表的功能，特殊之处在于set是可以**自动排重**的，当你需要存储一个列表数据，又不希望出现重复数据时，set是一个很好的选择，并且set提供了判断某个成员是否在一个set集合内的重要接口，这个也是list所不能提供的。\nRedis的Set是string类型的无序集合。它底层其实是一个value为null的hash表，所以添加，删除，查找的**复杂度都是O(1)**。\n一个算法，随着数据的增加，执行时间的长短，如果是O(1)，数据增加，查找数据的时间不变\n常用命令 sadd \u0026lt;key\u0026gt; \u0026lt;value1\u0026gt; \u0026lt;value2\u0026gt; ..... ：将一个或多个 member 元素加入到集合 key 中，已经存在的 member 元素将被忽略\nsmembers \u0026lt;key\u0026gt;：取出该集合的所有值，不会从集合中删除\nsismember \u0026lt;key\u0026gt; \u0026lt;value\u0026gt;：判断集合 \u0026lt;key\u0026gt; 是否为含有该 \u0026lt;value\u0026gt; 值；有1、没有0\nscard \u0026lt;key\u0026gt;：返回该集合的元素个数。\nsrem \u0026lt;key\u0026gt; \u0026lt;value1\u0026gt; \u0026lt;value2\u0026gt; ....：删除集合中的某个元素。\nspop \u0026lt;key\u0026gt;：随机从该集合中吐出一个值。\nsrandmember \u0026lt;key\u0026gt; \u0026lt;n\u0026gt;：随机从该集合中取出n个值。不会从集合中删除 。\nsmove \u0026lt;source\u0026gt; \u0026lt;destination\u0026gt; \u0026lt;value\u0026gt;：将 \u0026lt;source\u0026gt; 集合中的 \u0026lt;value\u0026gt; 移动到 \u0026lt;destination\u0026gt; 集合中\nsinter \u0026lt;key1\u0026gt; \u0026lt;key2\u0026gt;：返回两个集合的交集元素。\nsunion \u0026lt;key1\u0026gt; \u0026lt;key2\u0026gt;：返回两个集合的并集元素。\nsdiff \u0026lt;key1\u0026gt; \u0026lt;key2\u0026gt;：返回两个集合的差集元素 ( key1中的，不包含key2中的 )\n数据结构 Set 数据结构是 dict 字典，字典是用哈希表实现的。\nJava 中 HashSet 的内部实现使用的是 HashMap，只不过所有的 value 都指向同一个对象。Redis 的 set 结构也是一样，它的内部也使用 hash 结构，所有的 value 都指向同一个内部值。\nRedis 哈希 ( Hash ) 简介 Redis hash 是一个键值对集合。\nRedis hash 是一个 string 类型的 field 和 value 的映射表，hash 特别适合用于存储对象。\n类似 Java 里面的 Map\u0026lt;String,Object\u0026gt;\n用户 ID 为查找的 key，存储的 value 用户对象包含姓名，年龄，生日等信息，如果用普通的 key/value 结构来存储。\n主要有以下2种存储方式：\n每次修改用户的某个属性需要，先反序列化改好后再序列化回去。开销较大。 用户ID数据冗余 通过key(用户ID) + field(属性标签)就可以操作对应属性数据了，既不需要重复存储数据，也不会带来序列化和并发修改控制的问题 常用命令 hset \u0026lt;key \u0026gt;\u0026lt;field\u0026gt; \u0026lt;value\u0026gt;：给 \u0026lt;key\u0026gt; 集合中的 \u0026lt;field\u0026gt; 键赋值 \u0026lt;value\u0026gt;，也可以批量设置\nhmset \u0026lt;key\u0026gt; \u0026lt;field1\u0026gt; \u0026lt;value1\u0026gt; \u0026lt;field2\u0026gt; \u0026lt;value2\u0026gt;... ：批量设置hash的值\nhsetnx \u0026lt;key\u0026gt; \u0026lt;field\u0026gt; \u0026lt;value\u0026gt;：将哈希表 key 中的域 field 的值设置为 value ，当且仅当域. field 不存在是生效\nhget \u0026lt;key\u0026gt; \u0026lt;field\u0026gt;：从 \u0026lt;key\u0026gt; 集合 \u0026lt;field\u0026gt; 取出 value，不会删除 field\nhdel \u0026lt;key\u0026gt; \u0026lt;field\u0026gt;：从 \u0026lt;key\u0026gt; 集合中删除 \u0026lt;field\u0026gt;\nhexists\u0026lt;key1\u0026gt; \u0026lt;field\u0026gt;：查看哈希表 key 中，给定域 field 是否存在。( 0：不存在，1：存在 )\nhkeys \u0026lt;key\u0026gt;：列出该hash集合的所有field\nhvals \u0026lt;key\u0026gt;：列出该hash集合的所有value\nhgetall key：返回hash表中的所有的字段和值\nhincrby \u0026lt;key\u0026gt; \u0026lt;field\u0026gt; \u0026lt;increment\u0026gt;：为哈希表 \u0026lt;key\u0026gt; 中的域 \u0026lt;field\u0026gt; 的值加上 \u0026lt;increment\u0026gt; ( \u0026lt;increment\u0026gt; 可以是负数)\n数据结构 Hash 类型对应的数据结构是两种：ziplist（压缩列表），hashtable（哈希表）。当 field-value 长度较短且个数较少时，使用 ziplist，否则使用hashtable。\nRedis 有序集合 Zset ( sorted set ) 简介 Redis有序集合zset与普通集合set非常相似，是一个没有重复元素的字符串集合。\n不同之处是有序集合的每个成员都关联了一个**评分 ( score ) **,这个评分（score）被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的，但是评分可以是重复了 。\n因为元素是有序的, 所以你也可以很快的根据评分（score）或者次序（position）来获取一个范围的元素。\n访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。\n常用命令 zadd \u0026lt;key\u0026gt; \u0026lt;score1\u0026gt; \u0026lt;value1\u0026gt; \u0026lt;score2\u0026gt; \u0026lt;value2\u0026gt;…：将一个或多个 member 元素及其 score 值加入到有序集 key 当中。\nzrange \u0026lt;key\u0026gt; \u0026lt;start\u0026gt; \u0026lt;stop\u0026gt; [WITHSCORES]：返回有序集 key 中，下标在 \u0026lt;start\u0026gt; \u0026lt;stop\u0026gt; 之间的元素；带 WITHSCORES，可以让分数一起和值返回到结果集。\nzrangebyscore \u0026lt;key\u0026gt; \u0026lt;min\u0026gt; \u0026lt;max\u0026gt; [withscores] [limit offset count]：返回有序集 key 中，所有 score 值介于 min 和 max 之间 ( 包括等于 min 或 max ) 的成员。有序集成员按 score 值递增 ( 从小到大 ) 次序排列。\nzrevrangebyscore \u0026lt;key\u0026gt; \u0026lt;min\u0026gt; \u0026lt;max\u0026gt; [withscores] [limit offset count]：同上，改为从大到小排列。\nzincrby \u0026lt;key\u0026gt; \u0026lt;increment\u0026gt; \u0026lt;value\u0026gt;：为元素的score加上增量\nzrem \u0026lt;key\u0026gt; \u0026lt;value\u0026gt;：删除该集合下，指定值的元素\nzcount \u0026lt;key\u0026gt; \u0026lt;min\u0026gt; \u0026lt;max\u0026gt;：统计该集合，分数区间内的元素个数\nzrank \u0026lt;key\u0026gt; \u0026lt;value\u0026gt;：返回该值在集合中的排名，从0开始。\n数据结构 SortedSet ( zset ) 是 Redis 提供的一个非常特别的数据结构，一方面它等价于 Java 的数据结构 Map\u0026lt;String, Double\u0026gt;，可以给每一个元素 value 赋予一个权重 score，另一方面它又类似于 TreeSet，内部的元素会按照权重 score 进行排序，可以得到每个元素的名次，还可以通过 score 的范围来获取元素的列表。\nzset 底层使用了两个数据结构\nhash，hash的作用就是关联元素 value 和权重 score，保障元素 value 的唯一性，可以通过元素 value 找到相应的 score 值。 跳跃表，跳跃表的目的在于给元素 value 排序，根据 score 的范围获取元素列表。 Redis 的发布和订阅 什么是发布和订阅 Redis 发布订阅 (pub/sub) 是一种消息通信模式：发送者 (pub) 发送消息，订阅者 (sub) 接收消息。\nRedis 客户端可以订阅任意数量的频道。\nRedis的发布和订阅 1、客户端可以订阅频道如下图\n2、当给这个频道发布消息后，消息就会发送给订阅的客户端\n发布订阅命令行实现 1、 打开一个客户端订阅 channel1\nSUBSCRIBE channel1\n2、打开另一个客户端，给 channel1 发布消息 hello\npublish channel1 hello\n返回的1是订阅者数量\n3、打开第一个客户端可以看到发送的消息\n注：发布的消息没有持久化，如果在订阅的客户端收不到 hello，只能收到订阅后发布的消息\nRedis 新数据类型 Bitmaps 简介 现代计算机用二进制（位） 作为信息的基础单位， 1个字节等于8位， 例如“abc”字符串是由3个字节组成， 但实际在计算机存储时将其用二进制表示， “abc”分别对应的ASCII码分别是97、 98、 99， 对应的二进制分别是01100001、 01100010和01100011，如下图\n合理地使用操作位能够有效地提高内存使用率和开发效率。\nRedis提供了Bitmaps这个“数据类型”可以实现对位的操作：\nBitmaps本身不是一种数据类型， 实际上它就是字符串（key-value）， 但是它可以对字符串的位进行操作。 Bitmaps单独提供了一套命令， 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组， 数组的每个单元只能存储0和1， 数组的下标在 Bitmaps 中叫做偏移量。 命令 setbit 格式：setbit \u0026lt;key\u0026gt; \u0026lt;offset\u0026gt; \u0026lt;value\u0026gt;：设置 Bitmaps 中某个偏移量的值 ( 0或1 )。offset：偏移量从0开始\n应用场景：\n公司员工今日是否签到存放到 Bitmaps 中， 将签到的员工记做1， 没有签到的员工记做0， 用偏移量作为员工的id。\n设置键的第 offset 个位的值 ( 从0算起 )， 假设现在有20个员工，userid=1、6、11、15、19 的员工进行了签到，那么当前 Bitmaps 初始化结果如图\nusers:20200524 代表 2020-05-24 这天的员工签到的 Bitmaps\n**注：**很多应用的用户id以一个指定数字（例如10000） 开头， 直接将用户id和Bitmaps的偏移量对应势必会造成一定的浪费， 通常的做法是每次做setbit操作时将用户id减去这个指定数字。\n在第一次初始化Bitmaps时， 假如偏移量非常大， 那么整个初始化过程执行会比较慢， 可能会造成Redis的阻塞。\ngetbit 格式：getbit\u0026lt;key\u0026gt; \u0026lt;offset\u0026gt;：获取Bitmaps中某个偏移量的值。获取键的第offset位的值 ( 从0开始算 )\n应用场景：\n获取 id=8 的员工是否在 2020-05-24 这天是否签到， 返回0说明没有签到\n**注：**因为100根本不存在，所以也是返回0\nbitcount 统计字符串被设置为1的bit数。一般情况下，给定的整个字符串都会被进行计数，通过指定额外的 start 或 end 参数，可以让计数只在特定的位上进行。start 和 end 参数的设置，都可以使用负数值：比如 -1 表示最后一个位，而 -2 表示倒数第二个位，start、end 是指bit组的字节的下标数，二者皆包含。\n格式：bitcount \u0026lt;key\u0026gt; [start end]：统计字符串从start字节到end字节比特值为1的数量\n应用场景：\n统计 2020-05-24 这天签到员工的数量\nstart 和 end 代表起始和结束字节数， 下面操作计算用户id在第1个字节到第3个字节之间的独立访问用户数， 对应的用户id是11， 15， 19。\n举例： K1 [ 01000001 01000000 00000000 00100001 ]\nbitcount K1 1 2：统计下标1、2字节组中bit=1的个数，即 01000000 00000000。结果：1\nbitcount K1 1 3：统计下标1、2字节组中bit=1的个数，即01000000 00000000 00100001。结果：3\nbitcount K1 0 -2：统计下标0到下标倒数第2，字节组中bit=1的个数，即01000001 01000000 00000000。结果：3\n**注意：**redis 的 setbit 设置或清除的是bit位置，而bitcount计算的是byte位置。( 1byte = 8bit )\nbitop 格式：bitop and(or/not/xor) \u0026lt;destkey\u0026gt; [key…]\nbitop是一个复合操作， 它可以做多个Bitmaps的 and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在destkey中。\n举例：user:20200524：10001001 00000010( 1 5 8 15 )、user:20200525：00101000 01000010 ( 3 5 10 15 ) bitop and destkey users:20200524 users:20200525：00001000 00000010 ( 5 15 )\nbitop or destkey users:20200524 users:20200525：10100001 00000010 ( 1 3 8 10 )\nbitop not destkey users:20200524：01110110 11111101。not(非) 只能接收一个 key，将0设为1，1设为0\nbitop xor destkey users:20200524 users:20200525：10100001 01000000\n1 2 3 4 5 # 异或规则 真 + 假 = 真 假 + 真 = 真 假 + 假 = 假 真 + 真 = 假 Bitmaps 与set 对比 假设网站有1亿用户， 每天独立访问的用户有5千万， 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表\nset和Bitmaps存储一天活跃用户对比 数据类型 每个用户id占用空间 需要存储的用户量 全部内存量 集合类型 64位 50000000 64位*50000000 = 400MB Bitmaps 1位 100000000 1位*100000000 = 12.5MB 很明显， 这种情况下使用Bitmaps能节省很多的内存空间， 尤其是随着时间推移节省的内存还是非常可观的\nset和Bitmaps存储独立用户空间对比 数据类型 一天 一个月 一年 集合类型 400MB 12GB 144GB Bitmaps 12.5MB 375MB 4.5GB 但Bitmaps并不是万金油， 假如该网站每天的独立访问用户很少， 例如只有10万（大量的僵尸用户） ， 那么两者的对比如下表所示， 很显然， 这时候使用Bitmaps就不太合适了， 因为基本上大部分位都是0。\nset和Bitmaps存储一天活跃用户对比（独立用户比较少） 数据类型 每个userid占用空间 需要存储的用户量 全部内存量 集合类型 64位 100000 64位*100000 = 800KB Bitmaps 1位 100000000 1位*100000000 = 12.5MB HyperLogLog 简介 在工作当中，我们经常会遇到与统计相关的功能需求，比如统计网站PV（PageView页面访问量）,可以使用Redis的incr、incrby轻松实现。\n但像UV（UniqueVisitor，独立访客）、独立IP数、搜索记录数等需要去重和计数的问题如何解决？这种求集合中不重复元素个数的问题称为基数问题。\n解决基数问题有很多种方案：\n（1）数据存储在MySQL表中，使用distinct count计算不重复个数\n（2）使用Redis提供的hash、set、bitmaps等数据结构来处理\n以上的方案结果精确，但随着数据不断增加，导致占用空间越来越大，对于非常大的数据集是不切实际的。\n能否能够降低一定的精度来平衡存储空间？Redis推出了HyperLogLog\nRedis HyperLogLog 是用来做基数统计的算法，HyperLogLog 的优点是，在输入元素的数量或者体积非常非常大时，计算基数所需的空间总是固定的、并且是很小的。\n在 Redis 里面，每个 HyperLogLog 键只需要花费 12 KB 内存，就可以计算接近 2^64 个不同元素的基数。这和计算基数时，元素越多耗费内存就越多的集合形成鲜明对比。\n但是，因为 HyperLogLog 只会根据输入元素来计算基数，而不会储存输入元素本身，所以 HyperLogLog 不能像集合那样，返回输入的各个元素。\n什么是基数?\n比如数据集 {1, 3, 5, 7, 5, 7, 8}， 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}，基数 ( 基数集元素的个数 ) 为5。 基数估计：就是在误差可接受的范围内，快速计算基数。\n命令 pfadd 格式：pfadd \u0026lt;key\u0026gt; \u0026lt; element\u0026gt; [element ...]：添加指定元素到 HyperLogLog 中，可以是多个。\n如果执行命令后HLL估计的近似基数发生变化，则返回1，否则返回0。\npfcount 格式：pfcount \u0026lt;key\u0026gt; [key ...]：计算HLL的近似基数，可以计算多个HLL。\n比如用HLL存储每天的UV，计算一周的UV可以使用7天的UV合并计算即可\npfmerge 格式：pfmerge \u0026lt;destkey\u0026gt; \u0026lt;sourcekey\u0026gt; [sourcekey ...]：将一个或多个HLL合并后的结果存储在另一个HLL中。\n比如每月活跃用户可以使用每天的活跃用户来合并计算可得\nGeospatial 简介 Redis 3.2 中增加了对GEO类型的支持。GEO，Geographic，地理信息的缩写。该类型，就是元素的2维坐标，在地图上就是经纬度。redis基于该类型，提供了经纬度设置，查询，范围查询，距离查询，经纬度Hash等常见操作。\n命令 geoadd 格式：geoadd \u0026lt;key\u0026gt; \u0026lt;longitude\u0026gt; \u0026lt;latitude\u0026gt; \u0026lt;member\u0026gt; [longitude latitude member...]：添加地理位置（经度，纬度，名称）\n示例\ngeoadd china:city 121.47 31.23 shanghai\ngeoadd china:city 106.50 29.53 chongqing 114.05 22.52 shenzhen 116.38 39.90 beijing\n两极无法直接添加，一般会下载城市数据，直接通过 Java 程序一次性导入。\n有效的经度从 -180 度到 180 度。有效的纬度从 -85.05112878 度到 85.05112878 度。\n当坐标位置超出指定范围时，该命令将会返回一个错误。\n已经添加的数据，是无法再次往里面添加的。\ngeopos 格式：geopos \u0026lt;key\u0026gt; \u0026lt;member\u0026gt; [member...]：获得指定地区的坐标值\ngeodist 格式：geodist \u0026lt;key\u0026gt; \u0026lt;member1\u0026gt; \u0026lt;member2\u0026gt; [m|km|ft|mi ] 获取两个位置之间的直线距离\n单位：\nm 表示单位为米[默认值]。\nkm 表示单位为千米。\nmi 表示单位为英里。\nft 表示单位为英尺。\n如果用户没有显式地指定单位参数， 那么 GEODIST 默认使用米作为单位\ngeoradius 格式：georadius \u0026lt;key\u0026gt; \u0026lt; longitude\u0026gt; \u0026lt;latitude\u0026gt; \u0026lt;radius\u0026gt; \u0026lt;m|km|ft|mi\u0026gt; 以给定的经纬度为中心，找出某一半径内的元素\n经度 纬度 距离 单位\nRedis Jedis 测试 创建一个基本的 Maven 工程。\nMaven 1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;redis.clients\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jedis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.2.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Linux 打开6379端口 1 2 3 4 # 打开6379端口 firewall-cmd --permanent --add-port=6379/tcp # 重新载入使之生效 firewall-cmd --reload 创建测试程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package org.hong.jedis; import redis.clients.jedis.Jedis; public class JedisDemo1 { public static void main(String[] args) { // 1.创建Jedis对象 Jedis jedis = new Jedis(\u0026#34;192.168.200.130\u0026#34;, 6379); // 2.测试 String ping = jedis.ping(); System.out.println(ping); // 3.关闭连接 jedis.close(); } } 控制台打野\n1 2 ## 控制台输出 PONG 代表连接成功 PONG 测试相关数据 Jedis 方法与 Redis 命令几乎一样，根据 Redis 命令可以找到对应的 Jedis 方法。\n搭建 Test 环境 Maven 1 2 3 4 5 6 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.13\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package org.hong.jedis; import org.junit.Before; import org.junit.Test; import redis.clients.jedis.Jedis; import java.util.Set; public class JedisDemo1 { private Jedis jedis; /** * 在单元测试方法之前执行 */ @Before public void testBefore(){ jedis = new Jedis(\u0026#34;192.168.200.130\u0026#34;, 6379); } /** * 在单元测试方法之后执行 */ @After public void testAfter(){ jedis.close(); } } Key 1 2 3 4 5 6 7 8 9 10 11 @Test public void testKey(){ // keys * Set\u0026lt;String\u0026gt; keys = jedis.keys(\u0026#34;*\u0026#34;); keys.forEach(System.out :: println); // del key1 Long key1 = jedis.del(\u0026#34;key1\u0026#34;); // 返回删除的数量 System.out.println(key1); } String 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void testString(){ // 添加 String set = jedis.set(\u0026#34;name\u0026#34;, \u0026#34;hong\u0026#34;); System.out.println(set); // 获取 String name = jedis.get(\u0026#34;name\u0026#34;); System.out.println(name); // 设置多个值 String mset = jedis.mset(\u0026#34;k1\u0026#34;, \u0026#34;v1\u0026#34;, \u0026#34;k2\u0026#34;, \u0026#34;v2\u0026#34;); System.out.println(mset); // 获取多个值 List\u0026lt;String\u0026gt; mget = jedis.mget(\u0026#34;k1\u0026#34;, \u0026#34;k2\u0026#34;); mget.forEach(System.out :: println); } 控制台打印\n1 2 3 4 5 6 7 # 添加成功返回OK OK # 获取指定key的value值 hong OK v1 v2 List 1 2 3 4 5 6 7 8 9 10 @Test public void testList(){ // 添加列表 Long rpush = jedis.rpush(\u0026#34;key1\u0026#34;, \u0026#34;tom\u0026#34;, \u0026#34;jerry\u0026#34;, \u0026#34;hong\u0026#34;); System.out.println(rpush); // 获取列表 List\u0026lt;String\u0026gt; key1 = jedis.lrange(\u0026#34;key1\u0026#34;, 0, -1); key1.forEach(System.out :: println); } 控制台打印\n1 2 3 4 5 6 # 添加数量 3 # 返回的列表元素 tom jerry hong Set 1 2 3 4 5 6 7 8 9 10 @Test public void testSet(){ // 添加 Long sadd = jedis.sadd(\u0026#34;program\u0026#34;, \u0026#34;java\u0026#34;, \u0026#34;c++\u0026#34;, \u0026#34;mysql\u0026#34;, \u0026#34;java\u0026#34;); System.out.println(sadd); // 获取 Set\u0026lt;String\u0026gt; program = jedis.smembers(\u0026#34;program\u0026#34;); program.forEach(System.out :: println); } 控制台打印\n1 2 3 4 5 6 # 添加的数量 3 # 获取到的Set中的元素 java c++ mysql Hash 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void testHash(){ // 添加单个 Long hset = jedis.hset(\u0026#34;tom\u0026#34;, \u0026#34;age\u0026#34;, \u0026#34;18\u0026#34;); System.out.println(hset); // 批量添加 HashMap\u0026lt;String, String\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;sex\u0026#34;, \u0026#34;男\u0026#34;); map.put(\u0026#34;birth\u0026#34;, \u0026#34;2021-5-25\u0026#34;); Long hset1 = jedis.hset(\u0026#34;tom\u0026#34;, map); System.out.println(hset1); // 获取单个field String age = jedis.hget(\u0026#34;tom\u0026#34;, \u0026#34;age\u0026#34;); System.out.println(age); // 获取全部field Map\u0026lt;String, String\u0026gt; tom = jedis.hgetAll(\u0026#34;tom\u0026#34;); tom.forEach((k, v) -\u0026gt; System.out.println(k + \u0026#34;=\u0026#34; + v)); } 控制台打印\n1 2 3 4 5 6 7 8 9 # 添加数量 1 2 # 获取单个filed 18 # 获取全部field birth=2021-5-25 age=18 sex=男 Zset 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void testZset(){ // 添加单个 Long zadd = jedis.zadd(\u0026#34;China\u0026#34;, 200, \u0026#34;上海\u0026#34;); System.out.println(zadd); // 批量添加 HashMap\u0026lt;String, Double\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;北京\u0026#34;, 100D); map.put(\u0026#34;长沙\u0026#34;, 300D); map.put(\u0026#34;长沙\u0026#34;, 400D); Long china = jedis.zadd(\u0026#34;China\u0026#34;, map); System.out.println(china); // 获取 Set\u0026lt;Tuple\u0026gt; zrangeWithScores = jedis.zrangeWithScores(\u0026#34;China\u0026#34;, 0, -1); zrangeWithScores.forEach(System.out :: println); } 控制台打印\n1 2 3 4 5 6 7 # 添加数量 1 2 # Zset中的元素 [北京,100.0] [上海,200.0] [长沙,400.0] Redis Jedis 示例 完成一个手机验证码功能 1 2 3 4 要求: 1.输入手机号，点击发送后随机生成6位数字码，2分钟有效 2.输入验证码，点击验证，返回成功或失败 3.每个手机号每天只能获取3次验证码 简单分析 1 2 3 4 5 6 7 8 要求1: 1.使用Java的Random类生产验证码 2.存入Redis中并设置过期时间为120秒 要求2: 1.从Redis中取出验证码, 于用户输入继续判断 要求3: 1.incr每次获取验证码之后+1 2.大于等于3的时候不能获取验证码 Java 代码模拟 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 package org.hong.jedis; import redis.clients.jedis.Jedis; import java.util.Random; import java.util.Scanner; public class PhoneCode { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println(\u0026#34;请出入手机号\u0026#34;); String phone = scanner.next(); while (true) { System.out.println(\u0026#34;1:输入验证码; 2:获取验证码; 3:退出系统\u0026#34;); int actionCode = scanner.nextInt(); if (actionCode == 1) { System.out.println(\u0026#34;请输入验证码\u0026#34;); String code = scanner.next(); checkCode(phone, code); } else if(actionCode == 2) { verifyCode(phone); } else if(actionCode == 3) { break; } } } /** * 获取6位数字的验证码 * * @return */ public static String getCode() { String code = \u0026#34;\u0026#34;; Random random = new Random(); for (int i = 0; i \u0026lt; 6; i++) { int number = random.nextInt(10); code += number; } System.out.println(\u0026#34;验证码:\u0026#34; + code); return code; } /** * 获取验证码逻辑 * 每个手机每天只能发送三次, 验证码发到Redis中, 设置过期时间120秒 * * @param phone */ public static void verifyCode(String phone) { // 连接Redis Jedis jedis = new Jedis(\u0026#34;192.168.200.130\u0026#34;, 6379); // 拼接Key // 手机发送次数Key String countKey = \u0026#34;VerifyCode\u0026#34; + phone + \u0026#34;:count\u0026#34;; // 验证码Key String codeKey = \u0026#34;VerifyCode\u0026#34; + phone + \u0026#34;:code\u0026#34;; // 每个手机每天只能发送3次 String count = jedis.get(countKey); if (count == null) { // 如果是null, 代表是第一次发送 // 设置发送次数为1, 同时设置过期时间为1天(当然也可以设置当前时间到第二天的秒数, 更加准确) jedis.setex(countKey, 24 * 60 * 60, \u0026#34;1\u0026#34;); } else if (Integer.parseInt(count) \u0026lt;= 2) { jedis.incr(countKey); } else { System.out.println(\u0026#34;今天发送次数已经超过三次了\u0026#34;); jedis.close(); return; } // 将验证码放到Redis中 jedis.setex(codeKey, 120, getCode()); // 关闭连接 jedis.close(); } /** * 校验验证码 * * @param phone * @param code * @return */ public static void checkCode(String phone, String code) { Jedis jedis = new Jedis(\u0026#34;192.168.200.130\u0026#34;, 6379); // 验证码Key String codeKey = \u0026#34;VerifyCode\u0026#34; + phone + \u0026#34;:code\u0026#34;; // 获取Redis中存放的验证码 String redisCode = jedis.get(codeKey); // 判断 if (code.equals(redisCode)) { System.out.println(\u0026#34;验证成功\u0026#34;); } else { System.out.println(\u0026#34;验证失败\u0026#34;); } jedis.close(); } } SpringBoot 整合 Redis 创建一个基本的 SpringBoot 项目\nMaven 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- redis --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-data-redis\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- spring2.X集成redis所需common-pool2--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.commons\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;commons-pool2\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; application.properties 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #Redis服务器地址 spring.redis.host=192.168.200.130 #Redis服务器连接端口 spring.redis.port=6379 #Redis数据库索引（默认为0） spring.redis.database= 0 #连接超时时间（毫秒） spring.redis.timeout=1800000 #连接池最大连接数（使用负值表示没有限制） spring.redis.lettuce.pool.max-active=20 #最大阻塞等待时间(负数表示没限制) spring.redis.lettuce.pool.max-wait=-1 #连接池中的最大空闲连接 spring.redis.lettuce.pool.max-idle=5 #连接池中的最小空闲连接 spring.redis.lettuce.pool.min-idle=0 RedisConfig 配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 package org.hong.redis.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; @EnableCaching @Configuration public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate\u0026lt;String, Object\u0026gt; redisTemplate(RedisConnectionFactory factory) { RedisTemplate\u0026lt;String, Object\u0026gt; template = new RedisTemplate\u0026lt;\u0026gt;(); RedisSerializer\u0026lt;String\u0026gt; redisSerializer = new StringRedisSerializer(); 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); template.setConnectionFactory(factory); //key序列化方式 template.setKeySerializer(redisSerializer); //value序列化 template.setValueSerializer(jackson2JsonRedisSerializer); //value hashmap序列化 template.setHashValueSerializer(jackson2JsonRedisSerializer); return template; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisSerializer\u0026lt;String\u0026gt; redisSerializer = new StringRedisSerializer(); 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); // 配置序列化（解决乱码的问题）,过期时间600秒 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(600)) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); return cacheManager; } } 测试用例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package org.hong.redis; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; @SpringBootTest class RedisSpringbootApplicationTests { @Autowired private RedisTemplate redisTemplate; @Test void contextLoads() { Object name = redisTemplate.opsForValue().get(\u0026#34;name\u0026#34;); System.out.println(name); } } Redis 事务 锁机制 Redis 的事务定义 Redis事务是一个单独的隔离操作：事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中，不会被其他客户端发送来的命令请求所打断。\nRedis事务的主要作用就是**串联多个命令**防止别的命令插队。\nMulti、Exec、discard 从输入 Multi 命令开始，输入的命令都会依次进入命令队列中，但不会执行，直到**输入 Exec 后，Redis 会将之前的命令队列中的命令依次执行。**\n组队的过程中可以通过 discard 来放弃组队。\n组队成功，提交成功\n组队阶段报错，提交失败\n组队成功，提交有成功有失败情况\n事务的错误处理 组队中某个命令出现了报告错误，执行时整个的所有队列都会被取消。\n如果执行阶段某个命令报出了错误，则只有报错的命令不会被执行，而其他的命令都会执行，不会回滚。\n事务冲突的问题 例子 一个请求想给金额减8000\n一个请求想给金额减5000\n一个请求想给金额减1000\n悲观锁 悲观锁(Pessimistic Lock), 顾名思义，就是很悲观，每次去拿数据的时候都认为别人会修改，所以每次在拿数据的时候都会上锁，这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制，比如**行锁，表锁等，读锁，写锁**等，都是在做操作之前先上锁。\n乐观锁 乐观锁(Optimistic Lock), 顾名思义，就是很乐观，每次去拿数据的时候都认为别人不会修改，所以不会上锁，但是在更新的时候会判断一下在此期间别人有没有去更新这个数据，可以使用版本号等机制。乐观锁适用于多读的应用类型，这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。\nWATCH key [key \u0026hellip;] 在执行multi之前，先执行watch key1 [key2],可以监视一个(或多个) key ，如果在**事务执行之前这个(或这些) key 被其他命令所改动，那么事务将被打断。**\nunwatch 取消 WATCH 命令对所有 key 的监视。\n如果在执行 WATCH 命令之后，EXEC 命令或DISCARD 命令先被执行了的话，那么就不需要再执行UNWATCH 了。\nhttp://doc.redisfans.com/transaction/exec.html\nRedis 事务三特性 单独的隔离操作 事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中，不会被其他客户端发送来的命令请求所打断。 没有隔离级别的概念 队列中的命令没有提交之前都不会实际被执行，因为事务提交前任何指令都不会被实际执行 不保证原子性 事务中如果有一条命令执行失败，其后的命令仍然会被执行，没有回滚 Redis 事务 秒杀案例 秒杀案例数据存储\n代码实现 创建一个简单的 SpringBoot 项目，添加 web 场景启动器。\nMaven 1 2 3 4 5 6 \u0026lt;!-- 添加jedis依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;redis.clients\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jedis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.6.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 package org.hong.seckill.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import redis.clients.jedis.Jedis; import java.util.Random; /** * 秒杀Controller * 本次案例不分层 */ @RestController public class SeckillController { @GetMapping(\u0026#34;/seckill\u0026#34;) public Boolean seckill(@RequestParam(\u0026#34;productId\u0026#34;) String productId){ String userId = getUserId(); return doSeckill(productId, userId); } /** * 随机获得用户ID * @return */ public String getUserId(){ Random random = new Random(); String userId = \u0026#34;\u0026#34;; for (int i = 0; i \u0026lt; 4; i++) { int number = random.nextInt(10); userId += number; } return userId; } /** * 秒杀方法, 暂时不考虑高并发场景 * @param productId * @param userId * @return */ public boolean doSeckill(String productId, String userId){ Jedis jedis = new Jedis(\u0026#34;192.168.200.130\u0026#34;, 6379); // 拼接Key // 商品库存Key String countKey = \u0026#34;seckill\u0026#34; + productId + \u0026#34;:count\u0026#34;; // 秒杀成功用户Key String usersKey = \u0026#34;seckill\u0026#34; + productId + \u0026#34;:users\u0026#34;; // 1.获取当前商品的秒杀库存, 如果为null, 表示秒杀还未开始 String count = jedis.get(countKey); if(count == null){ System.out.println(\u0026#34;秒杀还未开始\u0026#34;); jedis.close(); return false; } // 2.判断商品库存, 小于1代表秒杀结束 if(Integer.parseInt(count) \u0026lt; 1){ System.out.println(\u0026#34;秒杀已结束\u0026#34;); jedis.close(); return false; } // 3.判断用户是否重复秒杀 if(jedis.sismember(usersKey, userId)){ System.out.println(\u0026#34;不能重复秒杀\u0026#34;); jedis.close(); return false; } // 4.秒杀过程 // 4.1 库存减1 jedis.decr(countKey); // 4.2 将秒杀成功用户添加到列表中 jedis.sadd(usersKey, userId); System.out.println(\u0026#34;秒杀成功\u0026#34;); jedis.close(); return true; } } index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;!-- 导入jquery --\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; src=\u0026#34;jquery.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; function seckill() { $.get(\u0026#34;/seckill\u0026#34;, {productId: \u0026#34;0105\u0026#34;}, function (data) { console.log(data) }, \u0026#34;json\u0026#34;) } \u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;HUAWEI P30 1元 限时秒杀\u0026lt;/h1\u0026gt; \u0026lt;button onclick=\u0026#34;seckill()\u0026#34;\u0026gt;秒杀\u0026lt;/button\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 秒杀并发模拟 使用工具ab模拟测试。CentOS6 默认安装、CentOS7需要手动安装 yum install httpd-tools\nab -n 2000 -c 200 http://192.168.200.1:8080/seckill?productId=0105 进行并发测试\n1 2 -n: 后面写发送次数 -c: 后面写并发量 超卖 利用乐观锁淘汰用户，解决超卖问题 增加乐观锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 package org.hong.seckill.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; import java.util.List; import java.util.Random; /** * 秒杀Controller * 本次案例不分层 */ @RestController public class SeckillController { @GetMapping(\u0026#34;/seckill\u0026#34;) public Boolean seckill(@RequestParam(\u0026#34;productId\u0026#34;) String productId){ String userId = getUserId(); return doSeckill(productId, userId); } /** * 随机获得用户ID * @return */ public String getUserId(){ Random random = new Random(); String userId = \u0026#34;\u0026#34;; for (int i = 0; i \u0026lt; 4; i++) { int number = random.nextInt(10); userId += number; } return userId; } /** * 秒杀方法 * @param productId * @param userId * @return */ public boolean doSeckill(String productId, String userId){ Jedis jedis = new Jedis(\u0026#34;192.168.200.130\u0026#34;, 6379); // 拼接Key // 商品库存Key String countKey = \u0026#34;seckill\u0026#34; + productId + \u0026#34;:count\u0026#34;; // 秒杀成功用户Key String usersKey = \u0026#34;seckill\u0026#34; + productId + \u0026#34;:users\u0026#34;; // ########################################### // 监听商品库存, 增加乐观锁 jedis.watch(countKey); // 1.获取当前商品的秒杀库存, 如果未null, 表示秒杀还未开始 String count = jedis.get(countKey); if(count == null){ System.out.println(\u0026#34;秒杀还未开始\u0026#34;); jedis.close(); return false; } // 2.判断商品库存, 小于1代表秒杀结束 if(Integer.parseInt(count) \u0026lt; 1){ System.out.println(\u0026#34;秒杀已结束\u0026#34;); jedis.close(); return false; } // 3.判断用户是否重复秒杀 if(jedis.sismember(usersKey, userId)){ System.out.println(\u0026#34;不能重复秒杀\u0026#34;); jedis.close(); return false; } // ########################################### // 4.秒杀过程 // 4.1 库存减1 // 增加事务 Transaction multi = jedis.multi(); // 加入队列 multi.decr(countKey); // 4.2 将秒杀成功用户添加到列表中 multi.sadd(usersKey, userId); // 执行 List\u0026lt;Object\u0026gt; exec = multi.exec(); if(exec == null || exec.size() != 2){ System.out.println(\u0026#34;秒杀失败\u0026#34;); jedis.close(); return false; } System.out.println(\u0026#34;秒杀成功\u0026#34;); jedis.close(); return true; } } 增加商品库存再次测试 set seckill0105:count 500：设置库存数量为500\nab -n 2000 -c 200 http://192.168.200.1:8080/seckill?productId=0105：测试并发环境\n测试结果\n使用 ab 工具发送 2000 个请求是可以卖完 500 个库存的，可是查看库存时还剩下 475 个。这是因为乐观锁造成的。\n当 200 个请求同时秒杀商品时，如果一个用户秒杀了一件库存，因为乐观锁的存在会修改版本号，其他 199 个请求在进行修改的时候就会因为版本号不一致而全部导致秒杀失败。\n解决库存遗留问题 Lua 是一个小巧的脚本语言，Lua脚本可以很容易的被C/C++ 代码调用，也可以反过来调用C/C++的函数，Lua并没有提供强大的库，一个完整的Lua解释器不过200k，所以Lua不适合作为开发独立应用程序的语言，而是作为嵌入式脚本语言。\n很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言，以此来实现可配置性、可扩展性。\n这其中包括魔兽争霸地图、魔兽世界、博德之门、愤怒的小鸟等众多游戏插件或外挂。\nhttps://www.w3cschool.cn/lua/\nLUA脚本在Redis中的优势 将复杂的或者多步的redis操作，写为一个脚本，一次提交给redis执行，减少反复连接redis的次数。提升性能。\nLUA脚本是类似redis事务，有一定的原子性，当lua脚本在执行的时候，不会有其他脚本和命令同时执行，这种语义类似于 MULTI/EXEC。从别的客户端的视角来看，一个lua脚本要么不可见，要么已经执行完。不会被其他命令插队，可以完成一些redis事务性的操作。\n但是注意redis的lua脚本功能，只有在Redis 2.6以上的版本才可以使用。\n利用lua脚本淘汰用户，解决超卖问题。\nredis 2.6版本以后，通过lua脚本**解决争抢问题**，实际上是 redis 利用其单线程的特性，用任务队列的方式解决多任务并发问题。\nLUA 脚本 效果：将所有操作 Redis 的指令写到 Lua 脚本中，Lua 脚本具有原子性，因此每次都只会有一个用户运行 Lua 脚本完成秒杀。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 local userid=KEYS[1]; local prodid=KEYS[2]; local qtkey=\u0026#34;seckill\u0026#34;..prodid..\u0026#34;:count\u0026#34;; local usersKey=\u0026#34;seckill\u0026#34;..prodid.\u0026#34;:users\u0026#39;; local userExists=redis.call(\u0026#34;sismember\u0026#34;,usersKey,userid); if tonumber(userExists)==1 then return 2; end local num= redis.call(\u0026#34;get\u0026#34; ,qtkey); if tonumber(num)\u0026lt;=0 then return 0; else redis.call(\u0026#34;decr\u0026#34;,qtkey); redis.call(\u0026#34;sadd\u0026#34;,usersKey,userid); end return 1; 代码实现 Controller 1 2 3 4 5 6 7 8 @RestController public class SeckillController { @GetMapping(\u0026#34;/seckill\u0026#34;) public boolean seckill(@RequestParam(\u0026#34;productId\u0026#34;) String productId) throws IOException { String userId = getUserId(); return SecKillRedisByScript.doSeckill(productId, userId); } } SecKillRedisByScript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package org.hong.seckill.script; import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Set; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.hong.seckill.util.JedisPoolUtil; import org.slf4j.LoggerFactory; import ch.qos.logback.core.joran.conditional.ElseAction; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.ShardedJedisPool; import redis.clients.jedis.Transaction; public class SecKillRedisByScript { private static final org.slf4j.Logger logger =LoggerFactory.getLogger(SecKillRedisByScript.class) ; static String secKillScript =\u0026#34;local userid=KEYS[1];\\r\\n\u0026#34; + \u0026#34;local prodid=KEYS[2];\\r\\n\u0026#34; + \u0026#34;local qtkey=\u0026#39;seckill\u0026#39;..prodid..\\\u0026#34;:count\\\u0026#34;;\\r\\n\u0026#34; + \u0026#34;local usersKey=\u0026#39;seckill\u0026#39;..prodid..\\\u0026#34;:users\\\u0026#34;;\\r\\n\u0026#34; + \u0026#34;local userExists=redis.call(\\\u0026#34;sismember\\\u0026#34;,usersKey,userid);\\r\\n\u0026#34; + \u0026#34;if tonumber(userExists)==1 then \\r\\n\u0026#34; + \u0026#34; return 2;\\r\\n\u0026#34; + \u0026#34;end\\r\\n\u0026#34; + \u0026#34;local num= redis.call(\\\u0026#34;get\\\u0026#34; ,qtkey);\\r\\n\u0026#34; + \u0026#34;if tonumber(num)\u0026lt;=0 then \\r\\n\u0026#34; + \u0026#34; return 0;\\r\\n\u0026#34; + \u0026#34;else \\r\\n\u0026#34; + \u0026#34; redis.call(\\\u0026#34;decr\\\u0026#34;,qtkey);\\r\\n\u0026#34; + \u0026#34; redis.call(\\\u0026#34;sadd\\\u0026#34;,usersKey,userid);\\r\\n\u0026#34; + \u0026#34;end\\r\\n\u0026#34; + \u0026#34;return 1\u0026#34; ; public static boolean doSeckill(String prodid, String uid) throws IOException { JedisPool jedispool = JedisPoolUtil.getJedisPoolInstance(); Jedis jedis=jedispool.getResource(); //String sha1= .secKillScript; String sha1= jedis.scriptLoad(secKillScript); Object result= jedis.evalsha(sha1, 2, uid,prodid); String reString=String.valueOf(result); if (\u0026#34;0\u0026#34;.equals( reString ) ) { System.err.println(\u0026#34;已抢空！！\u0026#34;); }else if(\u0026#34;1\u0026#34;.equals( reString ) ) { System.out.println(\u0026#34;抢购成功！！！！\u0026#34;); }else if(\u0026#34;2\u0026#34;.equals( reString ) ) { System.err.println(\u0026#34;该用户已抢过！！\u0026#34;); }else{ System.err.println(\u0026#34;抢购异常！！\u0026#34;); } jedis.close(); return true; } } JedisPoolUtil 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package org.hong.seckill.util; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class JedisPoolUtil { private static volatile JedisPool jedisPool = null; private JedisPoolUtil() { } public static JedisPool getJedisPoolInstance() { if (null == jedisPool) { synchronized (JedisPoolUtil.class) { if (null == jedisPool) { JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(200); poolConfig.setMaxIdle(32); poolConfig.setMaxWaitMillis(100*1000); poolConfig.setBlockWhenExhausted(true); poolConfig.setTestOnBorrow(true); // ping PONG jedisPool = new JedisPool(poolConfig, \u0026#34;192.168.200.130\u0026#34;, 6379, 60000 ); } } } return jedisPool; } public static void release(JedisPool jedisPool, Jedis jedis) { if (null != jedis) { jedisPool.returnResource(jedis); } } } 执行结果 库存商品没有出现问题。\nRedis 持久化之 RDB（Redis DataBase） RDB 概述 在指定的时间间隔内将内存中的数据集快照写入磁盘， 也就是行话讲的Snapshot快照，它恢复时是将快照文件直接读到内存里\n备份是如何执行的 Redis会单独创建（fork）一个子进程来进行持久化，会先将数据写入到 一个临时文件中，待持久化过程都结束了，再用这个临时文件替换上次持久化好的文件。 整个过程中，主进程是不进行任何IO操作的，这就确保了极高的性能 如果需要进行大规模数据的恢复，且对于数据恢复的完整性不是非常敏感，那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失 ( 持久化过程中 Redis 出现问题退出会造成数据丢失 )\ndump.rdb文件 在redis.conf中配置文件名称，默认为dump.rdb\n配置位置 rdb文件的保存路径，也可以修改。默认为 Redis 启动时命令行所在的目录下创建 dump.rdb 文件\nRDB快照触发时机 保存策略 自动触发时机 1 2 3 4 默认是被注释掉的, 即不会自动触发 save 3600 1: 1个小时之内一个key发生变化 save 300 100: 5分钟之内100个key发生变化 save 60 10000: 1分钟之内10000个key发生变化 命令 save VS bgsave 手动触发 save：save 是只管保存，其它不管，全部阻塞 ( 保存的时候无法处理请求 )。手动保存。不建议。\nbgsave：Redis会在后台异步进行快照操作， 快照同时还可以响应客户端请求。\n可以通过 lastsave 命令获取最后一次成功执行快照的时间\nflushall 命令 执行flushall命令，也会产生dump.rdb文件，但里面是空的，无意义\nsave 命令 格式：save 秒钟 写操作次数\nRDB是整个内存的压缩过的Snapshot，RDB的数据结构，可以配置复合的快照触发条件，\n默认是1分钟内改了1万次，或5分钟内改了10次，或15分钟内改了1次。\n禁用\n不设置save指令，或者给save传入空字符串\nstop-writes-on-bgsave-error 当Redis无法写入磁盘的话，直接关掉Redis的写操作。推荐yes.\nrdbcompression 压缩文件 对于存储到磁盘中的快照，可以设置是否进行压缩存储。如果是的话，redis会采用LZF算法进行压缩。\n如果你不想消耗CPU来进行压缩的话，可以设置为关闭此功能。推荐yes.\nrdbchecksum 检查完整性 在存储快照后，还可以让redis使用CRC64算法来进行数据校验，\n但是这样做会增加大约10%的性能消耗，如果希望获取到最大的性能提升，可以关闭此功能。推荐yes.\nrdb 的备份 先通过 config get dir 查询rdb文件的目录\n将 *.rdb 的文件拷贝到别的地方\nrdb的恢复\n关闭Redis 先把备份的文件拷贝到工作目录下 cp dump.rdb dump.rdb.bak 启动Redis, 备份数据会直接加载，记得先修改文件名为 dump.rdb 优势 适合大规模的数据恢复 对数据完整性和一致性要求不高更适合使用 节省磁盘空间 恢复速度快 劣势 Fork的时候，内存中的数据被克隆了一份，大致2倍的膨胀性需要考虑 虽然Redis在fork时使用了**写时拷贝技术**，但是如果数据庞大时还是比较消耗性能。 在备份周期在一定间隔时间做一次备份，所以如果Redis意外down掉的话，就会丢失最后一次快照后的所有修改。 Redis 持久化之 AOF（Append Only File） AOF 概述 以日志的形式来记录每个写操作（增量保存），将Redis执行过的所有写指令记录下来 ( 读操作不记录 )， 只许追加文件但不可以改写文件，redis启动之初会读取该文件重新构建数据，换言之，redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作\nAOF持久化流程 客户端的请求写命令会被append追加到AOF缓冲区内； AOF缓冲区根据AOF持久化策略 [always,everysec,no] 将操作sync同步到磁盘的AOF文件中； AOF文件大小超过重写策略或手动重写时，会对AOF文件rewrite重写，压缩AOF文件容量； Redis服务重启时，会重新load加载AOF文件中的写操作达到数据恢复的目的； AOF默认不开启 可以在redis.conf中配置文件名称，默认为 appendonly.aof\nAOF文件的保存路径，同RDB的路径一致。\nAOF和RDB同时开启，redis听谁的？ AOF和RDB同时开启，系统默认取AOF的数据（数据不会存在丢失）\nAOF 启动/修复/恢复 AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样，都是拷贝备份文件，需要恢复时再拷贝到Redis工作目录下，启动系统即加载。\n正常恢复\n修改默认的 appendonly no，改为 yes 将有数据的 aof 文件复制一份保存到对应目录 (查看目录：config get dir ) 恢复：重启 redis 然后重新加载 异常恢复\n修改默认的 appendonly no，改为 yes 如遇到**AOF文件损坏**，通过 /usr/local/bin/redis-check-aof--fix appendonly.aof 进行恢复 备份 aof 文件 恢复：重启 redis，然后重新加载 AOF 同步频率设置 appendfsync always 始终同步，每次 Redis 的写入都会立刻记入日志；性能较差但数据完整性比较好 appendfsync everysec 每秒同步，每秒记入日志一次，如果宕机，本秒的数据可能丢失。 appendfsync no Redis 不主动进行同步，把同步时机交给操作系统。 Rewrite 压缩 概述 AOF采用文件追加方式，文件会越来越大为避免出现此种情况，新增了重写机制, 当AOF文件的大小超过所设定的阈值时，Redis就会启动AOF文件的内容压缩， 只保留可以恢复数据的最小指令集。可以使用命令 bgrewriteaof 指示Redis开始追加唯一的文件重写过程。\n1 2 3 4 5 set k1 v1 set k2 v2 set k3 v3 一共三条指令, 经过Redis的重写机制会变成如下指令 mset k1 v1 k2 v2 k3 v3 重写原理 AOF文件持续增长而过大时，会fork出一条新进程来将文件重写 ( 也是先写临时文件最后再rename )。\n触发机制 Redis 会记录上次重写时的AOF大小，默认配置是当AOF文件大小是上次 rewrite 后大小的一倍且文件大于64M时触发\n重写虽然可以节约大量磁盘空间，减少恢复时间。但是每次重写还是有一定的负担的，因此设定Redis要满足一定条件才会进行重写。 auto-aof-rewrite-percentage：设置重写的基准值，文件达到100%时开始重写（文件是原来重写后文件的2倍时触发）\nauto-aof-rewrite-min-size：设置重写的基准值，最小文件64MB。达到这个值开始重写。\n系统载入时或者上次重写完毕时，Redis会记录此时AOF大小，设为base_size，如果 Redis的AOF当前大小 \u0026gt;= base_size + base_size * 100% (默认) AND 当前大小 \u0026gt;= 64mb(默认) 的情况下，Redis 会对 AOF 进行重写。\n1 2 工作原理: Redis记住上次重写时AOF日志的大小（或者重启后没有写操作的话，那就直接用此时的AOF文件）， 基准尺寸和当前尺寸做比较。如果当前尺寸超过指定比例，就会触发重写操作。 优势 备份机制更稳健，丢失数据概率更低。 可读的日志文本，通过操作AOF稳健，可以处理误操作。 劣势 比起RDB占用更多的磁盘空间。 恢复备份速度要慢。 每次读写都同步的话，有一定的性能压力。 存在个别Bug，造成恢复不能。 推荐 官方推荐两个都启用。\n如果对数据不敏感，可以选单独用RDB。\n不建议单独用 AOF，因为可能会出现Bug。\n如果只是做纯内存缓存，可以都不用。\n1 2 3 4 5 6 因为RDB文件只用作后备用途，建议只在Slave上持久化RDB文件，而且只要15分钟备份一次就够了，只保留save 900 1这条规则。 如果使用AOF，好处是在最恶劣情况下也只会丢失不超过两秒数据，启动脚本较简单只load自己的AOF文件就可以了。 代价,一是带来了持续的IO，二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。 只要硬盘许可，应该尽量减少AOF rewrite的频率，AOF重写的基础大小默认值64M太小了，可以设到5G以上。 默认超过原大小100%大小时重写可以改到适当的数值。 Redis 主从复制 简介 主机数据更新后根据配置和策略， 自动同步到备机的 master/slaver机制，Master以写为主，Slave以读为主\n读写分离，性能扩展 容灾快速恢复 环境搭建 创建 /myredis 文件夹存放主从环境需要的文件\nmkdir /myredis cd /myredis 复制 redis.conf 配置文件到 /myredis 文件夹中\ncp /etc/redis.conf /myredis/redis.conf 配置一主两从，创建三个配置文件 redis6379.conf、redis6380.conf、redis6381.conf\n创建配置文件 vim redis6379.conf\n写入内容。其他两个配置文件内容类似，只需要把所有的 6379 改为对应的端口号就行\n1 2 3 4 5 6 7 # 引入公共部分 include /myredis/redis.conf pidfile /var/run/redis_6379.pid # 修改端口号 port 6379 # 修改RDB文件名称 dbfilename dump6379.rdb 启动服务 Redis\n1 2 3 redis-server redis6379.conf redis-server redis6380.conf redis-server redis6381.conf 查看三个 Redis 的运行情况\n使用 Xshell 打开3个连接，使用 redis-cli -p 端口号 连接指定端口的 Redis\n进入终端后运行 info replication 打印主从复制的相关信息\n配置从机\n在从机上执行 slaveof 主机ip 主机端口号，将当前 Redis 加入到指定的主机之下\n在次运行 info replication 命令\n测试\n主机中 set k1 v1，然后在从机中 get k1，如果从机能取到值代表搭建成功\nmaterauth password ( 主机配置密码情况下从机配置文件添加 )\n常用三招 一主二从 主机可以读写，从机只能读不能写 从机 shutdown 或 GG 后再次启动从机，从机的 role 将会变为 master，我们需要再次运行 slaveof 主机ip 主机端口号 指令 从机 Slave 初始化后，从机会主动将 Master 上的所有数据都复制一份 主机 shutdown 或 GG 后，从机的 role 不会变为 master，而是等待主机直到主机上线。 薪火相传 上一个Slave可以是下一个slave的Master，Slave同样可以接收其他 slaves的连接和同步请求，那么该slave作为了链条中下一个的master, 可以有效减轻master的写压力,去中心化降低风险。\n中途变更转向：会清除之前的数据，重新建立拷贝最新的 风险是一旦某个slave宕机，后面的slave都没法备份 主机挂了，从机还是从机，无法写数据了 反客为主 当一个 master 宕机后，后面的slave可以立刻升为master，其后面的slave不用做任何修改。\n用 slaveof no one 将从机变为主机。\n复制原理 Slave启动成功连接到master后会发送一个sync命令 Master接到命令启动后台的存盘进程，同时收集所有接收到的用于修改数据集命令， 在后台进程执行完毕之后，master将传送整个数据文件到slave,以完成一次完全同步 全量复制：slave服务在接收到数据库文件数据后，将其存盘并加载到内存中。 增量复制：Master继续将新的所有收集到的修改命令依次传给slave,完成同步 但是只要是重新连接master,一次完全同步 ( 全量复制 ) 将被自动执行 复制延时 由于所有的写操作都是先在Master上操作，然后同步更新到Slave上，所以从Master同步到Slave机器有一定的延迟，当系统很繁忙的时候，延迟问题会更加严重，Slave机器数量的增加也会使这个问题更加严重。\n哨兵模式 概述 反客为主的自动版，能够后台监控主机是否故障，如果故障了根据投票数自动将从库转换为主库\n配置哨兵 搭建 一主多从 环境 在 /myredis 文件夹下创建 sentinel.conf 文件，名字不能错 内容：sentinel monitor mymaster 127.0.0.1 6379 1 其中 mymaster 为监控对象起的服务器名称， 1 为至少有多少个哨兵同意迁移的数量。 启动哨兵 redis-sentinel /myredis/sentinel.conf\n测试主机宕机 主机宕机后，哨兵会从主机中选择一个作为主机。( 哨兵检测主机宕机需要时间 )\n故障恢复 优先级在redis.conf中默认： Redis6：replica-priority 100，值越小优先级越高 Redis6之前：slave-priority 100，值越小优先级越高 salve 单词有奴隶的意思，可能是觉得这个单词不友好就换了。 偏移量是指获得原主机数据最全的 每个redis实例启动后都会随机生成一个40位的runid Java 获取主机连接 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private static JedisSentinelPool jedisSentinelPool=null; public static Jedis getJedisFromSentinel(){ if(jedisSentinelPool==null){ Set\u0026lt;String\u0026gt; sentinelSet=new HashSet\u0026lt;\u0026gt;(); sentinelSet.add(\u0026#34;192.168.200.130:26379\u0026#34;); JedisPoolConfig jedisPoolConfig =new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(10); //最大可用连接数 jedisPoolConfig.setMaxIdle(5); //最大闲置连接数 jedisPoolConfig.setMinIdle(5); //最小闲置连接数 jedisPoolConfig.setBlockWhenExhausted(true); //连接耗尽是否等待 jedisPoolConfig.setMaxWaitMillis(2000); //等待时间 jedisPoolConfig.setTestOnBorrow(true); //取连接的时候进行一下测试 ping pong // 通过哨兵来获取主机连接 jedisSentinelPool=new JedisSentinelPool(\u0026#34;mymaster\u0026#34;,sentinelSet,jedisPoolConfig); return jedisSentinelPool.getResource(); }else{ return jedisSentinelPool.getResource(); } } Redis 集群 概述 Redis 集群实现了对Redis的水平扩容，即启动N个redis节点，将整个数据库分布存储在这N个节点中，每个节点存储总数据的1/N。\nRedis 集群通过分区（partition）来提供一定程度的可用性（availability）： 即使集群中有一部分节点失效或者无法进行通讯， 集群也可以继续处理命令请求。\n之前通过代理主机来解决，但是 redis3.0 中提供了解决方案。就是**无中心化集群配置**。即使连接的不是主机，集群会自动切换主机存储。主机写，从机读。无中心化主从集群。无论从哪台主机写的数据，其他主机上都能读到数据。\n环境搭建 创建6个 Redis 实例，6379,6380,6381,6382,6383,6384\n编辑 redis*.conf 文件内容\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 引入公共部分 include /myredis/redis.conf pidfile /var/run/redis_6379.pid # 修改端口号 port 6379 # 修改RDB文件名称 dbfilename dump6379.rdb #打开集群模式 cluster-enabled yes #设定节点配置文件名 cluster-config-file nodes-6379.conf #设定节点失联时间，超过该时间（毫秒），集群自动进自动进行主从切换。 cluster-node-timeout 15000 使用查找替换修改另外5个文件 :%s/6379/6380\n启动6个 Redis 服务\n将六个节点合成一个集群。组合之前，请确保所有redis实例启动后，nodes-xxxx.conf 文件都生成正常。\ncd /opt/redis/redis-6.2.3/src\nredis-cli --cluster create --cluster-replicas 1 192.168.200.130:6379 192.168.200.130:6380 192.168.200.130:6381 192.168.200.130:6382 192.168.200.130:6383 192.168.200.130:6384\n此处不要用 127.0.0.1， 请用真实IP地址\n--replicas 1：采用最简单的方式配置集群，一台主机，一台从机，正好三组。\n-c 采用集群策略连接，写入数据会自动切换相对应的写主机。redis-cli -c -p 端口号\n1 2 # [12706]就是Redis计算的key的插槽值, 根据对应的插槽值重定向到对应端口的Redis主节点进行写入操作 Redirected to slot [12706] located at 192.200.130:6381 通过 cluster nodes 命令查看集群信息\nredis cluster 如何分配这六个节点\n一个集群至少要有**三个主节点**。 选项 --cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。 分配原则尽量保证每个主数据库运行在不同的IP地址，每个从库和主库不在一个IP地址上。 插槽 节点合成集群的时候，最后输出了这么一段内容，All 16384 slots voverd.，表示当前集群有 16384 个插槽。\n概述 一个 Redis 集群包含 16384 个插槽（hash slot）， 数据库中的每个键都属于这 16384 个插槽的其中一个，\n集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽， 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和 。\n集群中的每个节点负责处理一部分插槽。 举个例子， 如果一个集群可以有主节点， 其中：\n节点 A 负责处理 0 号至 5460 号插槽。\n节点 B 负责处理 5461 号至 10922 号插槽。\n节点 C 负责处理 10923 号至 16383 号插槽。\n使用 cluster nodes 命令也可以到看到每个主节点负责处理的插槽范围\n集群中录入多个值 在 redis-cli 每次录入、查询键值，redis 都会计算出该 key 应该送往的插槽，如果不是该客户端对应服务器的插槽，redis 会报错，并告知应前往的 redis 实例地址和端口。\nredis-cli 客户端提供了 –c 参数实现自动重定向。\n如 redis-cli -c –p 6379 登入后，再录入、查询键值对可以自动重定向。\n不在一个 slot 下的键值，是不能使用 mget,mset 等多键操作。\n可以通过{}来定义组的概念，从而使key中{}内相同内容的键值对放到一个slot中去。\n1 2 3 4 # 使用mset可以给所有的key设置一个相同的组名, 再进行添加 # Redis会使用这个组名来计算插槽值并放入对应的插槽中 # 也就是说一个插槽中会存放多个数据 mset key1{组名} value1 key2{组名} value2 添加数据时使用组，那么数据对应的 key 也会发生变化\n插槽命令 cluster keyslot \u0026lt;key\u0026gt;：查看指定 key 的插槽值\ncluster countkeysinslot \u0026lt;slot\u0026gt;：查询指定 slot ( 插槽 ) 里面的数据数量\ncluster getkeysinslot \u0026lt;slot\u0026gt; \u0026lt;count\u0026gt;：查询指定 slot ( 插槽 ) 里面的 count 个数据\n故障恢复 如果主节点下线，从节点将自动升为主节点\n主节点恢复后，主节点将变为从节点\n如果某一段插槽的主从都挂掉，而 cluster-require-full-coverage 为 yes ，那么 ，整个集群都挂掉\n如果某一段插槽的主从都挂掉，而 cluster-require-full-coverage 为 no ，那么，该插槽数据全都不能使用，也无法存储。其他节点依旧能够使用\nredis.conf 中的参数 cluster-require-full-coverage\n集群的 Jedis 开发 首先打开 Redis 集群所有节点的端口号\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package org.hong.jedis; import org.junit.Test; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.JedisCluster; public class RedisClusterDemo { @Test public void testCluster() { HostAndPort hostAndPort = new HostAndPort(\u0026#34;192.168.200.130\u0026#34;, 6379); JedisCluster jedisCluster = new JedisCluster(hostAndPort); jedisCluster.set(\u0026#34;k1\u0026#34;, \u0026#34;v1\u0026#34;); System.out.println(jedisCluster.get(\u0026#34;k1\u0026#34;)); jedisCluster.close(); } } SpringBoot 集成 Redis 集群 application.properties 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #Redis数据库索引（默认为0） spring.redis.database=0 #连接超时时间（毫秒） spring.redis.timeout=1800000 #连接池最大连接数（使用负值表示没有限制） spring.redis.lettuce.pool.max-active=20 #最大阻塞等待时间(负数表示没限制) spring.redis.lettuce.pool.max-wait=-1 #连接池中的最大空闲连接 spring.redis.lettuce.pool.max-idle=5 #连接池中的最小空闲连接 spring.redis.lettuce.pool.min-idle=0 #Redis节点 spring.redis.cluster.nodes=192.168.200.130:6379,192.168.200.130:6380,192.168.200.130:6381,192.168.200.130:6382,192.168.200.130:6383,192.168.200.130:6384 Redis 应用问题解决 缓存穿透 概述 key 对应的数据在数据源并不存在，每次针对此 key 的请求从缓存获取不到，请求都会到数据源，从而可能压垮数据源。比如用一个不存在的用户 id 获取用户信息，不论缓存还是数据库都没有，若黑客利用此漏洞进行攻击可能压垮数据库。\n解决方案 一个一定不存在缓存及查询不到的数据，由于缓存是不命中时被动写的，并且出于容错考虑，如果从存储层查不到数据则不写入缓存，这将导致这个不存在的数据每次请求都要到存储层去查询，失去了缓存的意义。\n**对空值缓存：**如果一个查询返回的数据为空（不管是数据是否不存在），我们仍然把这个空结果（null）进行缓存，设置空结果的过期时间会很短，最长不超过五分钟 设置可访问的名单（白名单）： 使用bitmaps类型定义一个可以访问的名单，名单id作为bitmaps的偏移量，每次访问和bitmap里面的id进行比较，如果访问id不在bitmaps里面，进行拦截，不允许访问。 采用布隆过滤器： (布隆过滤器（Bloom Filter）是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数（哈希函数）。 布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法，缺点是有一定的误识别率和删除困难。) 将所有可能存在的数据哈希到一个足够大的bitmaps中，一个一定不存在的数据会被这个bitmaps拦截掉，从而避免了对底层存储系统的查询压力。 **进行实时监控：**当发现Redis的命中率开始急速降低，需要排查访问对象和访问的数据，和运维人员配合，可以设置黑名单限制服务 缓存击穿 概述 key 对应的数据存在，但在 redis 中过期，此时若有大量并发请求过来，这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存，这个时候大并发的请求可能会瞬间把后端 DB 压垮。\n解决方案 **使用互斥锁(mutex key)：**业界比较常用的做法，是使用 mutex。\n就是在缓存失效的时候（判断拿出来的值为空），不是立即去 load db， 而是先使用缓存工具的某些带成功操作返回值的操作 ( 比如 Redis 的 SETNX 或者 Memcache 的 ADD ) 去 set 一个 mutex key 当操作返回成功时，再进行load db的操作并回设缓存；并回设缓存,最后删除 mutex key 当操作返回失败，证明有线程在load db，当前线程睡眠一段时间再重试整个get缓存的方法。 缓存雪崩 概述 当缓存服务器重启或者大量缓存集中在某一个时间段失效，这样在失效的时候，也会给后端系统 ( 比如DB ) 带来很大压力。\n解决方案 **构建多级缓存架构：**nginx缓存 + redis缓存 +其他缓存（ehcache等） **使用锁或队列：**用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写，从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况 **设置过期标志更新缓存：**记录缓存数据是否过期（设置提前量），如果过期会触发通知另外的线程在后台去更新实际 key 的缓存。 **将缓存失效时间分散开：**比如我们可以在原有的失效时间基础上增加一个随机值，比如1-5分钟随机，这样每一个缓存的过期时间的重复率就会降低，就很难引发集体失效的事件。这个挺不错的 分布式锁 概述 随着业务发展的需要，原单体单机部署的系统被演化成分布式集群系统后，由于分布式系统多线程、多进程并且分布在不同机器上，这将使原单机部署情况下的并发控制锁策略失效，单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问，这就是分布式锁要解决的问题！\n分布式锁主流的实现方案：\n基于数据库实现分布式锁\n基于缓存（Redis等）\n基于Zookeeper\n每一种分布式锁解决方案都有各自的优缺点：\n性能：redis最高\n可靠性：zookeeper最高\n这里，我们就基于redis实现分布式锁。\n解决方案 多个客户端同时获取锁（setnx）\n获取成功，执行业务逻辑，执行完成释放锁（del）\n其他客户端等待重试\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @GetMapping(\u0026#34;testLock\u0026#34;) public void testLock(){ //1获取锁，setnx Boolean lock = redisTemplate.opsForValue().setIfAbsent(\u0026#34;lock\u0026#34;, \u0026#34;111\u0026#34;); //2获取锁成功、查询num的值 if(lock){ Object value = redisTemplate.opsForValue().get(\u0026#34;num\u0026#34;); //2.1判断num为空return if(StringUtils.isEmpty(value)){ redisTemplate.delete(\u0026#34;lock\u0026#34;); return; } //2.2有值就转成成int int num = Integer.parseInt(value+\u0026#34;\u0026#34;); //2.3把redis的num加1 redisTemplate.opsForValue().set(\u0026#34;num\u0026#34;, ++num); //2.4释放锁，del redisTemplate.delete(\u0026#34;lock\u0026#34;); }else{ //3获取锁失败、每隔0.1秒再获取 try { Thread.sleep(100); testLock(); } catch (InterruptedException e) { e.printStackTrace(); } } } 使用 ab 工具进行压力测试 ab -n 1000 -c 100 http://192.168.140.1:8080/test/testLock。\n压力测试 结果 基本实现。\n问题 ​\tsetnx 刚好获取到锁，业务逻辑出现异常，导致锁无法释放\n解决 ​\t设置过期时间，自动释放锁。\n设置锁的过期时间 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @GetMapping(\u0026#34;testLock\u0026#34;) public void testLock(){ //1获取锁，setnx Boolean lock = redisTemplate.opsForValue().setIfAbsent(\u0026#34;lock\u0026#34;, \u0026#34;111\u0026#34;, 3, TimeUnit.SECONDS); // 设置3秒后过期, TimeUnit.SECONDS: 单位 //2获取锁成功、查询num的值 if(lock){ Object value = redisTemplate.opsForValue().get(\u0026#34;num\u0026#34;); //2.1判断num为空return if(StringUtils.isEmpty(value)){ redisTemplate.delete(\u0026#34;lock\u0026#34;); return; } //2.2有值就转成成int int num = Integer.parseInt(value+\u0026#34;\u0026#34;); //2.3把redis的num加1 redisTemplate.opsForValue().set(\u0026#34;num\u0026#34;, ++num); //2.4释放锁，del redisTemplate.delete(\u0026#34;lock\u0026#34;); }else{ //3获取锁失败、每隔0.1秒再获取 try { Thread.sleep(100); testLock(); } catch (InterruptedException e) { e.printStackTrace(); } } } 压力测试肯定也没有问题。自行测试\n问题 ​\t可能会释放其他服务器的锁。\n场景 如果业务逻辑的执行时间是7s。执行流程如下\nindex1业务逻辑没执行完，3秒后锁被自动释放。\nindex2获取到锁，执行业务逻辑，3秒后锁被自动释放。\nindex3获取到锁，执行业务逻辑\nindex1业务逻辑执行完成，开始调用del释放锁，这时释放的是index3的锁，导致index3的业务只执行1s就被别人释放。最终等于没锁的情况。\n解决 ​\tsetnx 获取锁时，设置一个指定的唯一值（例如：uuid）；释放前获取这个值，判断是否自己的锁\nUUID 防误删 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @GetMapping(\u0026#34;testLock\u0026#34;) public void testLock(){ String uuid = UUID.randomUUID().toString(); //1获取锁，setne Boolean lock = redisTemplate.opsForValue().setIfAbsent(\u0026#34;lock\u0026#34;, uuid, 3, TimeUnit.SECONDS); //2获取锁成功、查询num的值 if(lock){ Object value = redisTemplate.opsForValue().get(\u0026#34;num\u0026#34;); //2.1判断num为空return if(StringUtils.isEmpty(value)){ redisTemplate.delete(\u0026#34;lock\u0026#34;); return; } //2.2有值就转成int int num = Integer.parseInt(value+\u0026#34;\u0026#34;); //2.3把redis的num加1 redisTemplate.opsForValue().set(\u0026#34;num\u0026#34;, ++num); //2.4释放锁，del //判断当前Redis中锁的UUID是否是自己的, 如果是自己的锁才进行释放 if(uuid.equals(redisTemplate.opsForValue().get(\u0026#34;lock\u0026#34;))){ redisTemplate.delete(\u0026#34;lock\u0026#34;); } }else{ //3获取锁失败、每隔0.1秒再获取 try { Thread.sleep(100); testLock(); } catch (InterruptedException e) { e.printStackTrace(); } } } 问题 ​\t查询锁和删除锁操作缺乏原子性。\n场景 假设锁的过期时间为 10 秒 index1 执行业务代码使用了 9.5 秒，然后查询锁是否是自己的 指令发送到 Redis 使用了 0.3 秒，Redis 查询数据，此时花了 9.8 秒，锁还没过期，查询到的依然是 index1 的锁 Redis 将数据返回给我们花了 0.5 秒，此时一共花了 10.3 秒，锁过期，但是之前查询到的数据已经返回给了 index1 index1 执行删除前被打断，index2 获取到了 cpu 资源并获得了新的锁 index2 线程执行过程中被打断还没释放 lock，index1 线程获取到了 cpu 资源 由于 index1 被打断之前就已经获取到了 Redis 返回的数据，可以通过判断，并执行删锁操作，导致 index2 的锁被删除 LUA 脚本保证删除的原子性 效果：在查询锁和删除锁的时候无法被打断，保证在删除锁的时候时不会有新的锁被创建，不会造成误删\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @GetMapping(\u0026#34;testLockLua\u0026#34;) public void testLockLua() { //1 声明一个uuid ,将做为一个value 放入我们的key所对应的值中 String uuid = UUID.randomUUID().toString(); //2 定义一个锁：lua 脚本可以使用同一把锁，来实现删除！ String skuId = \u0026#34;25\u0026#34;; // 访问skuId 为25号的商品 100008348542 String lockKey = \u0026#34;lock:\u0026#34; + skuId; // 锁住的是每个商品的数据 // 3 获取锁 Boolean lock = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 3, TimeUnit.SECONDS); // 如果true if (lock) { // 不管业务是否执行成功, 都必须删除锁 try{ // 执行的业务逻辑开始 // 获取缓存中的num 数据 Object value = redisTemplate.opsForValue().get(\u0026#34;num\u0026#34;); // 如果是空直接返回 if (StringUtils.isEmpty(value)) { return; } int num = Integer.parseInt(value + \u0026#34;\u0026#34;); // 使num 每次+1 放入缓存 redisTemplate.opsForValue().set(\u0026#34;num\u0026#34;, String.valueOf(++num)); }finally{ /* 使用lua脚本来释放锁 */ // 定义lua 脚本 String script = \u0026#34;if redis.call(\u0026#39;get\u0026#39;, KEYS[1]) == ARGV[1] then return redis.call(\u0026#39;del\u0026#39;, KEYS[1]) else return 0 end\u0026#34;; // 使用redis执行lua执行 DefaultRedisScript\u0026lt;Long\u0026gt; redisScript = new DefaultRedisScript\u0026lt;\u0026gt;(); // 简写: DefaultRedisScript\u0026lt;Long\u0026gt; redisScript = new DefaultRedisScript\u0026lt;\u0026gt;(script, Long.class); // 设置执行的脚本 redisScript.setScriptText(script); // 设置脚本执行后的返回值类型 redisScript.setResultType(Long.class); // 第一个是script 脚本 ，第二个需要判断的key，第三个是key所对应的值。 Long result = redisTemplate.execute(redisScript, Arrays.asList(lockKey), uuid); } } else { // 其他线程等待 try { // 睡眠 Thread.sleep(1000); // 睡醒了之后，调用方法。 testLockLua(); } catch (InterruptedException e) { e.printStackTrace(); } } } Redisson 分布式锁 简介 Redisson在基于NIO的Netty框架上，充分的利用了Redis键值数据库提供的一系列优势，在Java实用工具包中常用接口的基础上，为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力，大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务，更进一步简化了分布式环境中程序相互之间的协作。\n入门案例 新建一个 SpringBoot 工程 pom.xml 1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.redisson\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;redisson\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.13.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; application.yaml 1 2 server: port: 11000 RedissonConfig 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package org.hong.gulimall.product.config; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.io.IOException; /** * redisson作为分布式锁, 分布式对象等功能 * 1、导入依赖 * \u0026lt;dependency\u0026gt; * \u0026lt;groupId\u0026gt;org.redisson\u0026lt;/groupId\u0026gt; * \u0026lt;artifactId\u0026gt;redisson\u0026lt;/artifactId\u0026gt; * \u0026lt;version\u0026gt;3.13.0\u0026lt;/version\u0026gt; * \u0026lt;/dependency\u0026gt; * 2、编写配置 */ @Configuration public class RedissonConfig { @Bean(destroyMethod=\u0026#34;shutdown\u0026#34;) public RedissonClient redisson() throws IOException { Config config = new Config(); config.useSingleServer() // 启用单节点模式 .setAddress(\u0026#34;redis://192.168.200.130:6379\u0026#34;); // 设置主机地址, 记得加上前缀 redis:// 或者 rediss:// return Redisson.create(config); // 创建实例 } } RedissonController 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package org.hong.gulimall.product.web; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class RedissonController { @Autowired private RedissonClient redisson; @RequestMapping(\u0026#34;/hello\u0026#34;) public String hello(){ /* * 1.获取锁, 并指定锁名称 * 2.存入Redis后会以锁名称作为key, 存入Hash类型的数据 * 2.1、其中field为(UUID:线程ID), 防止误删锁 * 2.2、value暂时不知道什么含义 * 3.需要注意的是getLock方法不会真正的加锁, 只是定义了锁的信息 */ RLock lock = redisson.getLock(\u0026#34;hello-lock\u0026#34;); /* * 调用lock()方法进行真正的加锁 * 1.将锁存入Redis中 * 2.默认过期时间为30s, 防止死锁 * 3.阻塞式等待 * 4.如果业务运行时间超长, 会自动给锁续上新的30秒 */ lock.lock(); try{ // 模拟超长业务 Thread.sleep(40000); }catch (Exception e){ }finally{ // 保险起见解锁代码放到finally块中, 保证解锁 lock.unlock(); } return \u0026#34;hello\u0026#34;; } @RequestMapping(\u0026#34;/hello2\u0026#34;) public String hello2(){ /* * 1.获取锁, 并指定锁名称 * 2.存入Redis后会以锁名称作为key, 存入Hash类型的数据 * 2.1、其中field为(UUID:线程ID), 防止误删锁 * 2.2、value暂时不知道什么含义 * 3.需要注意的是getLock方法不会真正的加锁, 只是定义了锁的信息 */ RLock lock = redisson.getLock(\u0026#34;hello-lock\u0026#34;); /* * 调用lock方法进行真正的加锁 * 1.将锁存入Redis中 * 2.指定过期时间为10, 单位为SECONDS(秒), 防止死锁 * 3.阻塞式等待 * 4.指定时长的lock是不会为锁续期的 */ lock.lock(10, TimeUnit.SECONDS); try{ // 模拟超长业务 Thread.sleep(40000); }catch (Exception e){ }finally{ // 保险起见解锁代码放到finally块中, 保证解锁 lock.unlock(); } return \u0026#34;hello\u0026#34;; } } Redis 查看 hello-lock 锁的存储信息 Redis 查看 hello-lock 锁的过期时间 Redisson 默认会给锁设置 30 秒的过期时间，但是我们模拟40秒的超长业务，30秒的锁很明显是不够用的，当程序很长一段时间没有释放锁，Redisson 就会自动给锁续期，大概在锁的过期时间还有20秒的时候就会重新将锁的过期时间设置为30秒。\n如果在程序运行期间，服务器断电，会不会造成死锁呢 ( 即锁一直无法被释放 )？ 由于锁的续期是 Redisson 进行的，当我们的服务宕机后，Redisson 自然也无法运行，也就无法给锁续期，锁到时间就会自动释放，不会造成死锁。\nlock() 空参方法 锁的过期时间：30秒\n是否会自动续期：true\nlock() 带参方法 锁的过期时间：指定的过期时间\n是否会自动续期：false\n读写锁 保证一定能督导最新数据，修改期间，写锁是一个排他锁 ( 互斥锁 )。读锁是一个共享锁。\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Autowired private StringRedisTemplate redisTemplate; @RequestMapping(\u0026#34;/read\u0026#34;) public String read (){ RReadWriteLock readWriteLock = redisson.getReadWriteLock(\u0026#34;rw-lock\u0026#34;); String value = \u0026#34;\u0026#34;; RLock rLock = readWriteLock.readLock(); rLock.lock(); System.out.println(\u0026#34;读锁加锁成功\u0026#34; + Thread.currentThread().getId() + \u0026#34;\\t\u0026#34; + new Date()); try { value = redisTemplate.opsForValue().get(\u0026#34;rw-key\u0026#34;); } finally { rLock.unlock(); System.out.println(\u0026#34;读锁解锁成功\u0026#34; + Thread.currentThread().getId() + \u0026#34;\\t\u0026#34; + new Date()); } return value; } @RequestMapping(\u0026#34;/writh\u0026#34;) public String writh (){ RReadWriteLock readWriteLock = redisson.getReadWriteLock(\u0026#34;rw-lock\u0026#34;); String uuid = UUID.randomUUID().toString(); RLock rLock = readWriteLock.writeLock(); rLock.lock(); System.out.println(\u0026#34;写锁加锁成功\u0026#34; + Thread.currentThread().getId() + \u0026#34;\\t\u0026#34; + new Date()); try { redisTemplate.opsForValue().set(\u0026#34;rw-key\u0026#34;, uuid); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } finally { rLock.unlock(); System.out.println(\u0026#34;写锁解锁成功\u0026#34; + Thread.currentThread().getId() + \u0026#34;\\t\u0026#34; + new Date()); } return uuid; } 读写锁在 Redis 中的结构 1 2 mode: 锁的类型 48fcddc7-....: 锁的唯一标识 测试 先写后读 在读锁未释放的时候，读取操作必须等待读锁释放才能继续进行。\n控制台打印\n1 2 3 4 写锁加锁成功118\tWed Aug 04 15:44:14 CST 2021 写锁解锁成功118\tWed Aug 04 15:44:24 CST 2021 读锁加锁成功119\tWed Aug 04 15:44:24 CST 2021 读锁解锁成功119\tWed Aug 04 15:44:24 CST 2021 多次写 拿到写锁的线程进行运行，其他线程等待写锁释放后获取到写锁才能运行，阻塞式等待。\n1 2 3 4 写锁加锁成功120\tWed Aug 04 15:45:12 CST 2021 写锁解锁成功120\tWed Aug 04 15:45:22 CST 2021 写锁加锁成功121\tWed Aug 04 15:45:22 CST 2021 写锁解锁成功121\tWed Aug 04 15:45:32 CST 2021 先读后写 在读的方法中添加 sleep\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RequestMapping(\u0026#34;/read\u0026#34;) public String read (){ RReadWriteLock readWriteLock = redisson.getReadWriteLock(\u0026#34;rw-lock\u0026#34;); String value = \u0026#34;\u0026#34;; RLock rLock = readWriteLock.readLock(); rLock.lock(); System.out.println(\u0026#34;读锁加锁成功\u0026#34; + Thread.currentThread().getId() + \u0026#34;\\t\u0026#34; + new Date()); try { value = redisTemplate.opsForValue().get(\u0026#34;rw-key\u0026#34;); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } finally { rLock.unlock(); System.out.println(\u0026#34;读锁解锁成功\u0026#34; + Thread.currentThread().getId() + \u0026#34;\\t\u0026#34; + new Date()); } return value; } 读锁在未解锁前无法获取到写锁，保证读锁线程获取到的数据还没有被更改\n1 2 3 4 读锁加锁成功233\tWed Aug 04 15:48:07 CST 2021 读锁解锁成功233\tWed Aug 04 15:48:17 CST 2021 写锁加锁成功234\tWed Aug 04 15:48:17 CST 2021 写锁解锁成功234\tWed Aug 04 15:48:27 CST 2021 多次读 读锁是被共享的，多次读等于没有锁\n1 2 3 读锁加锁成功113\tWed Aug 04 16:20:48 CST 2021 读锁加锁成功114\tWed Aug 04 16:20:48 CST 2021 读锁解锁成功112\tWed Aug 04 16:20:49 CST 2021 信号量 在生活中有这样的问题，当你开车进入车库时，发现没车位怎么办，只有等待别人开走留下空车位，当然如果有空车位，我们就可直接停进去，此时车位数就会减少，Semaphore信号量就是实现这种现象的一个功能。\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @GetMapping(\u0026#34;/park\u0026#34;) public String park() throws InterruptedException { //获得信号量 RSemaphore park = redisson.getSemaphore(\u0026#34;park\u0026#34;); //占用车位, 阻塞式等待, 获取不到就一直卡在这里 park.acquire(); return \u0026#34;获得一个车位...\u0026#34;; } @GetMapping(\u0026#34;/leave\u0026#34;) public String leave() { //获得信号量 RSemaphore park = redisson.getSemaphore(\u0026#34;park\u0026#34;); //释放一个车位 park.release(); return \u0026#34;释放一个车位...\u0026#34;; } 测试 先在 Redis 中创建 key 为 park 的数据 set park 3\n浏览器发起 http://localhost:8080/park 请求，此时 park 会减少一，直到 park 为0时，请求就会一直进行，直到发起 http://localhost:8080/leave请求使得 park 数增加才会终止\n信号量在 Redis 中的结构 key-value 键值对\ntryAcquire() 方法 尝试获取信号量，如果获取成功返回 true，反之 false\n1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping(\u0026#34;/park\u0026#34;) public String park() throws InterruptedException { //获得信号量 RSemaphore park = redisson.getSemaphore(\u0026#34;park\u0026#34;); //占用车位 boolean result = park.tryAcquire(); if(result){ return \u0026#34;获得一个车位...\u0026#34;; }else{ return \u0026#34;车位已满\u0026#34;; } } 闭锁 在要完成某些运算时，只有其它线程的运算全部运行完毕，当前运算才继续下去。\n场景：学校放假关门，只有所有班级的人都走玩了才能锁门。\n代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @GetMapping(\u0026#34;/lockDoor\u0026#34;) public String loclDoor() { RCountDownLatch door = redisson.getCountDownLatch(\u0026#34;door\u0026#34;); door.trySetCount(5); // 设置起始值 try { door.await(); //等待闭锁都完成 } catch (InterruptedException e) { e.printStackTrace(); } return \u0026#34;关门了\u0026#34;; } @GetMapping(\u0026#34;/gogogo/{id}\u0026#34;) public String gogogo(@PathVariable(\u0026#34;id\u0026#34;) Integer id) { RCountDownLatch door = redisson.getCountDownLatch(\u0026#34;door\u0026#34;); door.countDown(); // 计数器减一 return id + \u0026#34;班的人都走了\u0026#34;; } 测试 先访问 http://localhost:8080/lockDoor，这个请求会一直执行，只有当我们访问了 5 次 http://localhost:8080/gogogo/1，lockDoor 请求才能运行完毕\n闭锁在 Redis 中的结构 key-value 键值对\n","permalink":"https://ktzxy.top/posts/0utglweo6k/","summary":"Redis6","title":"Redis6"},{"content":"1. 正则表达式概述和作用 正则表达式是专门解决字符串规则匹配的工具。正则表达式也是一个字符串，用来定义匹配规则。参照帮助文档，在 Pattern 类中有简单的规则定义，可以结合字符串类的方法使用。\n1.1. String类中与正则表达式相关的方法 1 2 3 4 5 6 7 8 9 public boolean matches(String regex); // 使用String类来进行正则表达式规则判断。 // 概述：一个用来定义规则的字符串； // 作用：用来校验某个字符串是否符合指定的规则。 public String[] split(String regex); public String replaceAll(String regex, String replacement); // 这两个方法传入的字符也适用正则表达式。如涉及到“通配符”，“转义字符”等，(. \\d,\\t …)需要在前面加上“\\”。即(\\\\. , \\\\d, \\\\t …) 注：上述相关方法，参数regex是正则表达式的字符串，方法则按正则表达式的规则去执行\n1.2. 正则对象 Pattern 和匹配器 Matcher 范例： 1 2 3 Pattern p = Pattern.compile(\u0026#34;a*b\u0026#34;); Matcher m = p.matcher(\u0026#34;aaaaab\u0026#34;); boolean b = m.matches(); 使用步骤： 先将正则表达式编译成正则对象。使用的是Pattern类一个静态的方法。compile(regex); 让正则对象和要操作的字符串相关联，通过matcher方法完成，并返回匹配器对象。 通过匹配器对象的方法将正则模式作用到字符串上对字符串进行针对性的功能操作 1.3. 总结 正则判断可以直接使用String字符串的matches方法，或者使用正则对象 java的正则表达式都是字符串，而javaScript可以直接 var reg = /正则表达式/匹配模式; 创建一个正则表达式对象 2. 正则表达式语法规则 2.1. 语法 构造 匹配 字符 要完全匹配 x 匹配字符x \\ 转义字符 \\t tab键，匹配制表符 \\n 换行，匹配换行 \\r 回车，匹配回车 字符类：中括号中可以放n个字符，但只能匹配一类字符中的其中一个\n构造 匹配 [abc] 匹配abc中任意一个字符 [^abc] **（取反）**匹配除abc之外的任意一个字符 [a-zA-Z] 匹配所有的字母中的任意一个**（必须是从小到大）** [0-9] 匹配所有数字字符的任意一个 [a-zA-Z_0-9] 匹配所有的数字和字母和下划线的任意一个 预定义字符类：\n构造 匹配 . 是通配符，除了\\r和\\n以外的所有的任意一个字符 \\\\d 匹配所有数字字符的任意一个，相当于[0-9]；(\\\\D就是取反) \\\\w 匹配所有的数字和字母和下划线的任意一个,相当于[a-zA-Z_0-9]；(\\\\w就是取反) \\\\s 匹配空格 [ \\t\\n\\x0B\\f\\r] 数量词：\n构造 匹配 X? x出现0或1次，?修饰最近的字符 X* x出现0或n次， x可以出现任意次 X+ x出现1或n次， x可以出现1次以上 X{n} x刚好出现n次 X{n,} x至少出现n次，包含n次 X{n,m} x出现n至m次，包含n和m次 其他：\n构造 匹配 (X) X，作为捕获组 2.2. 常用正则例子 汉字：[\\u4e00-\\u9fa5] 手机：0?(13|14|15|17|18)[0-9]{9} 邮箱：\\w[-\\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\\.)+[A-Za-z]{2,14} IP：^((2[0-4]\\d|25[0-5]|[01]?\\d\\d?)\\.){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)$ 网址：^(https?:\\/\\/)?([\\da-z\\.-]+)\\.([a-z\\.]{2,6})([\\/\\w \\.-]*)*\\/?$ 3. 正则表达式案例 3.1. 匹配邮箱(Java 与 js) js代码 1 2 3 var pattern = /\\w[-\\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\\.)+[A-Za-z]{2,14}/g, str = \u0026#39;\u0026#39;; console.log(pattern.test(str)); java代码 1 2 3 4 5 6 7 8 9 10 11 12 13 import java.util.regex.Matcher; import java.util.regex.Pattern; public class RegexMatches { public static void main(String args[]) { String str = \u0026#34;\u0026#34;; String pattern = \u0026#34;\\\\w[-\\\\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\\\\.)+[A-Za-z]{2,14}\u0026#34;; Pattern r = Pattern.compile(pattern); Matcher m = r.matcher(str); System.out.println(m.matches()); } } 3.2. 替换字符串中的回车、换行符、空格(Java 与 js) js代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var resultStr = testStr.replace(/\\ +/g, \u0026#34;\u0026#34;); //去掉空格 resultStr = testStr.replace(/[ ]/g, \u0026#34;\u0026#34;); //去掉空格 resultStr = testStr.replace(/[\\r\\n]/g, \u0026#34;\u0026#34;); //去掉回车换行 resultStr = testStr.replace(/[\\n]/g, \u0026#34;\u0026#34;); //去掉换行 resultStr = testStr.replace(/[\\r]/g, \u0026#34;\u0026#34;); //去掉回车 /************************************************/ // 原始字符串 var string = \u0026#34;欢迎访问!\\r\\nhangge.com 做最好的开发者知识平台\u0026#34;; // 去掉所有的换行符(注：需要同时执行两个语句) string = string.replace(/\\r\\n/g, \u0026#34;\u0026#34;) string = string.replace(/\\n/g, \u0026#34;\u0026#34;); // 去掉所有的空格（中文空格、英文空格都会被替换） string = string.replace(/\\s/g, \u0026#34;\u0026#34;); java代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // String类的replaceAll就有正则替换功能。 \\t为制表符 \\n为换行 \\r为回车 String str = \u0026#34;sds ss\u0026#34;; str = str.replaceAll(\u0026#34;[\\\\t\\\\n\\\\r]\u0026#34;, \u0026#34;\u0026#34;); // 不替换空格 /************************************************/ public class StringUtil { public static String getStringNoBlank(String str) { if(str!=null \u0026amp;\u0026amp; !\u0026#34;\u0026#34;.equals(str)) { Pattern p = Pattern.compile(\u0026#34;\\\\s*|\\t|\\r|\\n\u0026#34;); Matcher m = p.matcher(str); String strNoBlank = m.replaceAll(\u0026#34;\u0026#34;); return strNoBlank; } else { return str; } } } 4. 参考资料 在线正则表达式 ","permalink":"https://ktzxy.top/posts/rntdgk3fq8/","summary":"Java基础 正则表达式","title":"Java基础 正则表达式"},{"content":"sqlx的使用 在项目中我们通常可能会使用database/sql连接MySQL数据库。本文借助使用sqlx实现批量插入数据的例子，介绍了sqlx中可能被你忽视了的sqlx.In和DB.NamedExec方法。\nsqlx介绍 在项目中我们通常可能会使用database/sql连接MySQL数据库。sqlx可以认为是Go语言内置database/sql的超集，它在优秀的内置database/sql基础上提供了一组扩展。这些扩展中除了大家常用来查询的Get(dest interface{}, ...) error和Select(dest interface{}, ...) error外还有很多其他强大的功能。\n安装sqlx 1 go get github.com/jmoiron/sqlx 基本使用 连接数据库 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 var db *sqlx.DB type user struct { ID string Name string Age int } func initDB() (err error) { dsn := \u0026#34;user:password@tcp(127.0.0.1:3306)/sql_test?charset=utf8mb4\u0026amp;parseTime=True\u0026#34; // 也可以使用MustConnect连接不成功就panic db, err = sqlx.Connect(\u0026#34;mysql\u0026#34;, dsn) if err != nil { fmt.Printf(\u0026#34;connect DB failed, err:%v\\n\u0026#34;, err) return } db.SetMaxOpenConns(20) db.SetMaxIdleConns(10) return } 查询 查询单行数据示例代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 // 查询单条数据示例 func queryRowDemo() { sqlStr := \u0026#34;select id, name, age from user where id=?\u0026#34; var u user // 需要传递指针，因为要修改里面的值 err := db.Get(\u0026amp;u, sqlStr, 1) if err != nil { fmt.Printf(\u0026#34;get failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;id:%d name:%s age:%d\\n\u0026#34;, u.ID, u.Name, u.Age) } 查询多行数据示例代码如下：\n1 2 3 4 5 6 7 8 9 10 11 // 查询多条数据示例 func queryMultiRowDemo() { sqlStr := \u0026#34;select id, name, age from user where id \u0026gt; ?\u0026#34; var users []user err := db.Select(\u0026amp;users, sqlStr, 0) if err != nil { fmt.Printf(\u0026#34;query failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;users:%#v\\n\u0026#34;, users) } 插入、更新和删除 sqlx中的exec方法与原生sql中的exec使用基本一致：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 // 插入数据 func insertRowDemo() { sqlStr := \u0026#34;insert into user(name, age) values (?,?)\u0026#34; ret, err := db.Exec(sqlStr, \u0026#34;沙河小王子\u0026#34;, 19) if err != nil { fmt.Printf(\u0026#34;insert failed, err:%v\\n\u0026#34;, err) return } theID, err := ret.LastInsertId() // 新插入数据的id if err != nil { fmt.Printf(\u0026#34;get lastinsert ID failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;insert success, the id is %d.\\n\u0026#34;, theID) } // 更新数据 func updateRowDemo() { sqlStr := \u0026#34;update user set age=? where id = ?\u0026#34; ret, err := db.Exec(sqlStr, 39, 6) if err != nil { fmt.Printf(\u0026#34;update failed, err:%v\\n\u0026#34;, err) return } n, err := ret.RowsAffected() // 操作影响的行数 if err != nil { fmt.Printf(\u0026#34;get RowsAffected failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;update success, affected rows:%d\\n\u0026#34;, n) } // 删除数据 func deleteRowDemo() { sqlStr := \u0026#34;delete from user where id = ?\u0026#34; ret, err := db.Exec(sqlStr, 6) if err != nil { fmt.Printf(\u0026#34;delete failed, err:%v\\n\u0026#34;, err) return } n, err := ret.RowsAffected() // 操作影响的行数 if err != nil { fmt.Printf(\u0026#34;get RowsAffected failed, err:%v\\n\u0026#34;, err) return } fmt.Printf(\u0026#34;delete success, affected rows:%d\\n\u0026#34;, n) } NamedExec DB.NamedExec方法用来绑定SQL语句与结构体或map中的同名字段。\n1 2 3 4 5 6 7 8 9 func insertUserDemo()(err error){ sqlStr := \u0026#34;INSERT INTO user (name,age) VALUES (:name,:age)\u0026#34; _, err = db.NamedExec(sqlStr, map[string]interface{}{ \u0026#34;name\u0026#34;: \u0026#34;七米\u0026#34;, \u0026#34;age\u0026#34;: 28, }) return } NamedQuery 与DB.NamedExec同理，这里是支持查询。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 func namedQuery(){ sqlStr := \u0026#34;SELECT * FROM user WHERE name=:name\u0026#34; // 使用map做命名查询 rows, err := db.NamedQuery(sqlStr, map[string]interface{}{\u0026#34;name\u0026#34;: \u0026#34;七米\u0026#34;}) if err != nil { fmt.Printf(\u0026#34;db.NamedQuery failed, err:%v\\n\u0026#34;, err) return } defer rows.Close() for rows.Next(){ var u user err := rows.StructScan(\u0026amp;u) if err != nil { fmt.Printf(\u0026#34;scan failed, err:%v\\n\u0026#34;, err) continue } fmt.Printf(\u0026#34;user:%#v\\n\u0026#34;, u) } u := user{ Name: \u0026#34;七米\u0026#34;, } // 使用结构体命名查询，根据结构体字段的 db tag进行映射 rows, err = db.NamedQuery(sqlStr, u) if err != nil { fmt.Printf(\u0026#34;db.NamedQuery failed, err:%v\\n\u0026#34;, err) return } defer rows.Close() for rows.Next(){ var u user err := rows.StructScan(\u0026amp;u) if err != nil { fmt.Printf(\u0026#34;scan failed, err:%v\\n\u0026#34;, err) continue } fmt.Printf(\u0026#34;user:%#v\\n\u0026#34;, u) } } 事务操作 对于事务操作，我们可以使用sqlx中提供的db.Beginx()和tx.Exec()方法。示例代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 func transactionDemo2()(err error) { tx, err := db.Beginx() // 开启事务 if err != nil { fmt.Printf(\u0026#34;begin trans failed, err:%v\\n\u0026#34;, err) return err } defer func() { if p := recover(); p != nil { tx.Rollback() panic(p) // re-throw panic after Rollback } else if err != nil { fmt.Println(\u0026#34;rollback\u0026#34;) tx.Rollback() // err is non-nil; don\u0026#39;t change it } else { err = tx.Commit() // err is nil; if Commit returns error update err fmt.Println(\u0026#34;commit\u0026#34;) } }() sqlStr1 := \u0026#34;Update user set age=20 where id=?\u0026#34; rs, err := tx.Exec(sqlStr1, 1) if err!= nil{ return err } n, err := rs.RowsAffected() if err != nil { return err } if n != 1 { return errors.New(\u0026#34;exec sqlStr1 failed\u0026#34;) } sqlStr2 := \u0026#34;Update user set age=50 where i=?\u0026#34; rs, err = tx.Exec(sqlStr2, 5) if err!=nil{ return err } n, err = rs.RowsAffected() if err != nil { return err } if n != 1 { return errors.New(\u0026#34;exec sqlStr1 failed\u0026#34;) } return err } sqlx.In sqlx.In是sqlx提供的一个非常方便的函数。\nsqlx.In的批量插入示例 表结构 为了方便演示插入数据操作，这里创建一个user表，表结构如下：\n1 2 3 4 5 6 CREATE TABLE `user` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `name` VARCHAR(20) DEFAULT \u0026#39;\u0026#39;, `age` INT(11) DEFAULT \u0026#39;0\u0026#39;, PRIMARY KEY(`id`) )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; 结构体 定义一个user结构体，字段通过tag与数据库中user表的列一致。\n1 2 3 4 type User struct { Name string `db:\u0026#34;name\u0026#34;` Age int `db:\u0026#34;age\u0026#34;` } bindvars（绑定变量） 查询占位符?在内部称为bindvars（查询占位符）,它非常重要。你应该始终使用它们向数据库发送值，因为它们可以防止SQL注入攻击。database/sql不尝试对查询文本进行任何验证；它与编码的参数一起按原样发送到服务器。除非驱动程序实现一个特殊的接口，否则在执行之前，查询是在服务器上准备的。因此bindvars是特定于数据库的:\nMySQL中使用? PostgreSQL使用枚举的$1、$2等bindvar语法 SQLite中?和$1的语法都支持 Oracle中使用:name的语法 bindvars的一个常见误解是，它们用来在sql语句中插入值。它们其实仅用于参数化，不允许更改SQL语句的结构。例如，使用bindvars尝试参数化列或表名将不起作用：\n1 2 3 4 5 // ？不能用来插入表名（做SQL语句中表名的占位符） db.Query(\u0026#34;SELECT * FROM ?\u0026#34;, \u0026#34;mytable\u0026#34;) // ？也不能用来插入列名（做SQL语句中列名的占位符） db.Query(\u0026#34;SELECT ?, ? FROM people\u0026#34;, \u0026#34;name\u0026#34;, \u0026#34;location\u0026#34;) 自己拼接语句实现批量插入 比较笨，但是很好理解。就是有多少个User就拼接多少个(?, ?)。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // BatchInsertUsers 自行构造批量插入的语句 func BatchInsertUsers(users []*User) error { // 存放 (?, ?) 的slice valueStrings := make([]string, 0, len(users)) // 存放values的slice valueArgs := make([]interface{}, 0, len(users) * 2) // 遍历users准备相关数据 for _, u := range users { // 此处占位符要与插入值的个数对应 valueStrings = append(valueStrings, \u0026#34;(?, ?)\u0026#34;) valueArgs = append(valueArgs, u.Name) valueArgs = append(valueArgs, u.Age) } // 自行拼接要执行的具体语句 stmt := fmt.Sprintf(\u0026#34;INSERT INTO user (name, age) VALUES %s\u0026#34;, strings.Join(valueStrings, \u0026#34;,\u0026#34;)) _, err := DB.Exec(stmt, valueArgs...) return err } 使用sqlx.In实现批量插入 前提是需要我们的结构体实现driver.Valuer接口：\n1 2 3 func (u User) Value() (driver.Value, error) { return []interface{}{u.Name, u.Age}, nil } 使用sqlx.In实现批量插入代码如下：\n1 2 3 4 5 6 7 8 9 10 11 // BatchInsertUsers2 使用sqlx.In帮我们拼接语句和参数, 注意传入的参数是[]interface{} func BatchInsertUsers2(users []interface{}) error { query, args, _ := sqlx.In( \u0026#34;INSERT INTO user (name, age) VALUES (?), (?), (?)\u0026#34;, users..., // 如果arg实现了 driver.Valuer, sqlx.In 会通过调用 Value()来展开它 ) fmt.Println(query) // 查看生成的querystring fmt.Println(args) // 查看生成的args _, err := DB.Exec(query, args...) return err } 使用NamedExec实现批量插入 注意 ：该功能目前有人已经推了#285 PR，但是作者还没有发release，所以想要使用下面的方法实现批量插入需要暂时使用master分支的代码：\n在项目目录下执行以下命令下载并使用master分支代码：\n1 go get github.com/jmoiron/sqlx@master 使用NamedExec实现批量插入的代码如下：\n1 2 3 4 5 // BatchInsertUsers3 使用NamedExec实现批量插入 func BatchInsertUsers3(users []*User) error { _, err := DB.NamedExec(\u0026#34;INSERT INTO user (name, age) VALUES (:name, :age)\u0026#34;, users) return err } 把上面三种方法综合起来试一下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 func main() { err := initDB() if err != nil { panic(err) } defer DB.Close() u1 := User{Name: \u0026#34;七米\u0026#34;, Age: 18} u2 := User{Name: \u0026#34;q1mi\u0026#34;, Age: 28} u3 := User{Name: \u0026#34;小王子\u0026#34;, Age: 38} // 方法1 users := []*User{\u0026amp;u1, \u0026amp;u2, \u0026amp;u3} err = BatchInsertUsers(users) if err != nil { fmt.Printf(\u0026#34;BatchInsertUsers failed, err:%v\\n\u0026#34;, err) } // 方法2 users2 := []interface{}{u1, u2, u3} err = BatchInsertUsers2(users2) if err != nil { fmt.Printf(\u0026#34;BatchInsertUsers2 failed, err:%v\\n\u0026#34;, err) } // 方法3 users3 := []*User{\u0026amp;u1, \u0026amp;u2, \u0026amp;u3} err = BatchInsertUsers3(users3) if err != nil { fmt.Printf(\u0026#34;BatchInsertUsers3 failed, err:%v\\n\u0026#34;, err) } } sqlx.In的查询示例 关于sqlx.In这里再补充一个用法，在sqlx查询语句中实现In查询和FIND_IN_SET函数。即实现SELECT * FROM user WHERE id in (3, 2, 1);和SELECT * FROM user WHERE id in (3, 2, 1) ORDER BY FIND_IN_SET(id, '3,2,1');。\nin查询 查询id在给定id集合中的数据。\n1 2 3 4 5 6 7 8 9 10 11 12 13 // QueryByIDs 根据给定ID查询 func QueryByIDs(ids []int)(users []User, err error){ // 动态填充id query, args, err := sqlx.In(\u0026#34;SELECT name, age FROM user WHERE id IN (?)\u0026#34;, ids) if err != nil { return } // sqlx.In 返回带 `?` bindvar的查询语句, 我们使用Rebind()重新绑定它 query = DB.Rebind(query) err = DB.Select(\u0026amp;users, query, args...) return } in查询和FIND_IN_SET函数 查询id在给定id集合的数据并维持给定id集合的顺序。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // QueryAndOrderByIDs 按照指定id查询并维护顺序 func QueryAndOrderByIDs(ids []int)(users []User, err error){ // 动态填充id strIDs := make([]string, 0, len(ids)) for _, id := range ids { strIDs = append(strIDs, fmt.Sprintf(\u0026#34;%d\u0026#34;, id)) } query, args, err := sqlx.In(\u0026#34;SELECT name, age FROM user WHERE id IN (?) ORDER BY FIND_IN_SET(id, ?)\u0026#34;, ids, strings.Join(strIDs, \u0026#34;,\u0026#34;)) if err != nil { return } // sqlx.In 返回带 `?` bindvar的查询语句, 我们使用Rebind()重新绑定它 query = DB.Rebind(query) err = DB.Select(\u0026amp;users, query, args...) return } 当然，在这个例子里面你也可以先使用IN查询，然后通过代码按给定的ids对查询结果进行排序。\n参考链接：\nIllustrated guide to SQLX\n","permalink":"https://ktzxy.top/posts/5xswupdziz/","summary":"sqlx库的使用","title":"sqlx库的使用"},{"content":"备份 使用gitee远程仓库进行笔记备份\nObsidian简介 中文名：黑曜石\n[官网](Obsidian - Sharpen your thinking)\n插件 Obsidian Git 自动提交\nImage Auto Upload Plugin 配合PicGo+gitee/github 仓库 图片上传\nEditor Syntax Highlight 代码高亮\nCalendar 日历 依赖于 核心插件 -\u0026gt; 日记\nPandoc 导出word\nFile Explorer Note Count 笔记数量\nRecent Files 最近文章\nMindmap 思维导图形成大纲\nTasks 管理待办事项\nObsidian memos 记录灵感\nExcalidraw 绘制流程图\ncMenu\nQuick Explorer 笔记快速选择功能\nDataView 用来创建基于元字段的数据查询\nQuickAdd 快速添加\nAuto Link Title 粘贴链接后显示为网页的标题\nAdvanced Tables 增强表格功能\nOutliner 使用快捷键快速进行操作\nWeread Plugin 同步微信读书的笔记到obsidian中\nCopy Image and URL context menu 笔记中拷贝图片文件\nStyle Settings\nWeread Plugin 微信读书笔记\nButtons 运行命令并通过单击打开链接或按钮\nTemplater 创建模板，可以将变量和函数结果插入到注释中\nObsidian使用技巧 模板 开启日记功能（核心插件 -\u0026gt; 日记）\n开启模板功能（核心插件 -\u0026gt; 模板）\n运用了 Obsidian 的 YAML front matter 方法，它的好处是导出时不会出现在正文中\n官方文档-YAML front matter - Obsidian 中文帮助 - Obsidian Publish\n一文掌握Obsidian模板\nTotal Commander 软件 资源管理器\n双链 创建链接\n链接到文章：[[1. Obsidian简介]]\n链接到某个标题：[[1. Obsidian简介#3 Obsidian定位]]\n链接到文本块：[[1. Obsidian简介#^e3f585]]\n链接别名：[[1. Obsidian简介#^e3f585 | Obsidian中文名]]\n查看链接\n查看链接内容 核心插件 -\u0026gt; 页面预览 按住 Ctrl 预览 显示内容 ！ 打开链接面板\n核心插件 -\u0026gt; 反向链接 核心插件 -\u0026gt; 出链 在Obsidian中搜索 快捷键\n搜索当前文档：Ctrl + F 搜索整个资料库：Ctrl / Cmd + Shfift + F 搜索技巧\n直接搜索关键词\n搜索包含多个关键词的文档（空格间隔）\n搜索包含某一个关键词的文档（OR）\n指定搜索范围\n搜索文件名 file:word\n搜索文本内容 content:word\n搜索标签 tag:#tag:word\n搜索同一行中的多个关键词 line:word1 word2\n搜索同一章节中的多个关键词 section:word1 word2\n搜索同一段落（块）中的多个关键词 block:word1 word2\n搜索任务\n搜索任务 task:\u0026quot;\u0026quot; 搜索未完成任务 task-todo:\u0026quot;\u0026quot; 搜索已完成任务 task-done:\u0026quot;\u0026quot; Data View查询 定义：Obsidian资料库的查询工具/插件\n查询依据：YAML数据/Meatainfo\nYAML\n​ 位于Markdown文件开头\n​ 首尾三个 -\nObsidian支持的YMAL字段\ntags publish cssclass aliases 自定义字段\ncategory date time title rating 行内标记\nOne Field:: Value 这份文档，可以打 [rating:: 5] 分 Obsidian文件属性\nfile.name: 文件标题(字符串) file.folder: 文件所属文件夹路径 file.path: 文件路径 file.size: (in bytes) 文件大小 file.ctime: 文件的创建时间（包含日期和时间） file.mtime: 文件的修改时间 file.cday: 文件创建的日期 file.mday: 文件修改的日期 file.tags: 笔记中所有标签数组 file.etags: 除去子标签的数组 file.inlinks: 指向此文件的所有传入链接的数组 file.outlinks: 此文件所有出站的链接数组 file.aliases: 文件别名数组 file.day：如果文件名中有日期，那么会以这个字段显示。比如文 件名中包含 yyyy-mm-dd（年-月-日，例如2021-03-21），那么 就会存在这个 metadata。 任务属性\nTask 会继承所在文件的所有字段，比如 Task 所在的页面中已经包 含了 rating 信息了，那么 task 也会有 completed: 任务是否完成 fullyCompleted: 任务以及所有的子任务是否完成 text: 任务名 line: task 所在行 path: task 所在路径 section: 连接到任务所在区块 link: 连接到距离任务最近的可连接的区块 subtasks: 子任务 real: 如果为 true, 则是一个真正的任务，否则就是一个任务之前 或之后的元素列表 completion: 任务完成的日期 due: 任务到期时间 created: 创建日期 annotated: 如果任务有自定义标记则为 True，否则为 False DataView\n展示方式\nTable List Task 语法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ```dataview TABLE|LIST|TASK \u0026lt;field\u0026gt; [AS \u0026#34;Column Name\u0026#34;], \u0026lt;field\u0026gt;, ..., \u0026lt;field\u0026gt; FROM \u0026lt;source\u0026gt; (like #tag or \u0026#34;folder\u0026#34;) WHERE \u0026lt;expression\u0026gt; (like \u0026#39;field = value\u0026#39;) SORT \u0026lt;expression\u0026gt; [ASC/DESC] (like \u0026#39;field ASC\u0026#39;) ... other data commands ``` ```dataview TABLE|LIST|TASK \u0026lt;字段\u0026gt; [AS \u0026#34;表格列名\u0026#34;], \u0026lt;字段\u0026gt;, ..., \u0026lt;字段\u0026gt; FROM \u0026lt;源\u0026gt; (like #标签 or \u0026#34;文件夹\u0026#34;) WHERE \u0026lt;表达式\u0026gt; (like \u0026#39;字段 = 值\u0026#39;) SORT \u0026lt;表达式\u0026gt; [ASC/DESC] (like \u0026#39;字段 ASC\u0026#39;) ... other data commands ``` dataview\nlist|table|task\nfrom\nwhere\nsort\nasc，升序\ndesc，降序\n查询方式\n文件夹\n标签\n使用建议\n保存常用查询\n生成文件夹索引\n","permalink":"https://ktzxy.top/posts/c3b5uqabt2/","summary":"Obsidian学习笔记","title":"Obsidian"},{"content":"﻿# Typora到CSDN博客图片上传问题\n问题描述： ​\t通常在Typora中写笔记，上传到CSDN博客中去，那么就会遇到图片上传问题，一个一个上传？又或者是图片还存留到本地？\n问题解决： ​\t在Github或者Gitee中注册一个仓库，用来储存图片，那么我们就可以随时随地访问，写笔记再也不用担心图片了。\n前期准备： 注册一个Gitee账号\n下载nodejs，并安装\nhttps://nodejs.org/en/\n下载PicGo，并安装\nhttps://molunerfinn.com/PicGo/\nTypora\n创建仓库，并设置令牌 然后点击设置，创建令牌\n然后会要求你输入账户密码，然后进入\n==注意：将令牌记住，后面有用，万一忘记可重新点回此界面，进行修改重新生成令牌或者删除重头来过。==\nPicGo图床设置 配置Typora 测验 总结：\n然后利用Typora写笔记就会发现图片地址变成Gitee仓库储存图片地址 以前的笔记需要上传CSDN，需要重新复制，新建Typora文件，将之前图片地址转换成Gitee仓库储存图片地址 某些博客对于gitee仓库并不友好，所有最好采用github仓库，然后通过cdn/fastly改变前缀 1 2 3 https://fastly.jsdelivr.net/gh/ktzxy/blog-img@main https://fastly.jsdelivr.net/gh/ktzxy/blog-img@main ","permalink":"https://ktzxy.top/posts/fxmzy316ih/","summary":"Typora到CSDN博客图片上传问题","title":"Typora到CSDN博客图片上传问题"},{"content":"Kubernetes搭建高可用集群 前言 之前我们搭建的集群，只有一个master节点，当master节点宕机的时候，通过node将无法继续访问，而master主要是管理作用，所以整个集群将无法提供服务\n高可用集群 下面我们就需要搭建一个多master节点的高可用集群，不会存在单点故障问题\n但是在node 和 master节点之间，需要存在一个 LoadBalancer组件，作用如下：\n负载 检查master节点的状态 对外有一个统一的VIP：虚拟ip来对外进行访问\n高可用集群技术细节 高可用集群技术细节如下所示：\nkeepalived：配置虚拟ip，检查节点的状态 haproxy：负载均衡服务【类似于nginx】 apiserver： controller： manager： scheduler： 高可用集群步骤 我们采用2个master节点，一个node节点来搭建高可用集群，下面给出了每个节点需要做的事情\n初始化操作 我们需要在这三个节点上进行操作\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 # 关闭防火墙 systemctl stop firewalld systemctl disable firewalld # 关闭selinux # 永久关闭 sed -i \u0026#39;s/enforcing/disabled/\u0026#39; /etc/selinux/config # 临时关闭 setenforce 0 # 关闭swap # 临时 swapoff -a # 永久关闭 sed -ri \u0026#39;s/.*swap.*/#\u0026amp;/\u0026#39; /etc/fstab # 根据规划设置主机名【master1节点上操作】 hostnamectl set-hostname master1 # 根据规划设置主机名【master2节点上操作】 hostnamectl set-hostname master1 # 根据规划设置主机名【node1节点操作】 hostnamectl set-hostname node1 # r添加hosts cat \u0026gt;\u0026gt; /etc/hosts \u0026lt;\u0026lt; EOF 192.168.44.158 k8smaster 192.168.44.155 master01.k8s.io master1 192.168.44.156 master02.k8s.io master2 192.168.44.157 node01.k8s.io node1 EOF # 将桥接的IPv4流量传递到iptables的链【3个节点上都执行】 cat \u0026gt; /etc/sysctl.d/k8s.conf \u0026lt;\u0026lt; EOF net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 EOF # 生效 sysctl --system # 时间同步 yum install ntpdate -y ntpdate time.windows.com 部署keepAlived 下面我们需要在所有的master节点【master1和master2】上部署keepAlive\n安装相关包 1 2 3 4 # 安装相关工具 yum install -y conntrack-tools libseccomp libtool-ltdl # 安装keepalived yum install -y keepalived 配置master节点 添加master1的配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 cat \u0026gt; /etc/keepalived/keepalived.conf \u0026lt;\u0026lt;EOF ! Configuration File for keepalived global_defs { router_id k8s } vrrp_script check_haproxy { script \u0026#34;killall -0 haproxy\u0026#34; interval 3 weight -2 fall 10 rise 2 } vrrp_instance VI_1 { state MASTER interface ens33 virtual_router_id 51 priority 250 advert_int 1 authentication { auth_type PASS auth_pass ceb1b3ec013d66163d6ab } virtual_ipaddress { 192.168.44.158 } track_script { check_haproxy } } EOF 添加master2的配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 cat \u0026gt; /etc/keepalived/keepalived.conf \u0026lt;\u0026lt;EOF ! Configuration File for keepalived global_defs { router_id k8s } vrrp_script check_haproxy { script \u0026#34;killall -0 haproxy\u0026#34; interval 3 weight -2 fall 10 rise 2 } vrrp_instance VI_1 { state BACKUP interface ens33 virtual_router_id 51 priority 200 advert_int 1 authentication { auth_type PASS auth_pass ceb1b3ec013d66163d6ab } virtual_ipaddress { 192.168.44.158 } track_script { check_haproxy } } EOF 启动和检查 在两台master节点都执行\n1 2 3 4 5 6 # 启动keepalived systemctl start keepalived.service # 设置开机启动 systemctl enable keepalived.service # 查看启动状态 systemctl status keepalived.service 启动后查看master的网卡信息\n1 ip a s ens33 部署haproxy haproxy主要做负载的作用，将我们的请求分担到不同的node节点上\n安装 在两个master节点安装 haproxy\n1 2 3 4 5 6 # 安装haproxy yum install -y haproxy # 启动 haproxy systemctl start haproxy # 开启自启 systemctl enable haproxy 启动后，我们查看对应的端口是否包含 16443\n1 netstat -tunlp | grep haproxy 配置 两台master节点的配置均相同，配置中声明了后端代理的两个master节点服务器，指定了haproxy运行的端口为16443等，因此16443端口为集群的入口\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 cat \u0026gt; /etc/haproxy/haproxy.cfg \u0026lt;\u0026lt; EOF #--------------------------------------------------------------------- # Global settings #--------------------------------------------------------------------- global # to have these messages end up in /var/log/haproxy.log you will # need to: # 1) configure syslog to accept network log events. This is done # by adding the \u0026#39;-r\u0026#39; option to the SYSLOGD_OPTIONS in # /etc/sysconfig/syslog # 2) configure local2 events to go to the /var/log/haproxy.log # file. A line like the following can be added to # /etc/sysconfig/syslog # # local2.* /var/log/haproxy.log # log 127.0.0.1 local2 chroot /var/lib/haproxy pidfile /var/run/haproxy.pid maxconn 4000 user haproxy group haproxy daemon # turn on stats unix socket stats socket /var/lib/haproxy/stats #--------------------------------------------------------------------- # common defaults that all the \u0026#39;listen\u0026#39; and \u0026#39;backend\u0026#39; sections will # use if not designated in their block #--------------------------------------------------------------------- defaults mode http log global option httplog option dontlognull option http-server-close option forwardfor except 127.0.0.0/8 option redispatch retries 3 timeout http-request 10s timeout queue 1m timeout connect 10s timeout client 1m timeout server 1m timeout http-keep-alive 10s timeout check 10s maxconn 3000 #--------------------------------------------------------------------- # kubernetes apiserver frontend which proxys to the backends #--------------------------------------------------------------------- frontend kubernetes-apiserver mode tcp bind *:16443 option tcplog default_backend kubernetes-apiserver #--------------------------------------------------------------------- # round robin balancing between the various backends #--------------------------------------------------------------------- backend kubernetes-apiserver mode tcp balance roundrobin server master01.k8s.io 192.168.44.155:6443 check server master02.k8s.io 192.168.44.156:6443 check #--------------------------------------------------------------------- # collection haproxy statistics message #--------------------------------------------------------------------- listen stats bind *:1080 stats auth admin:awesomePassword stats refresh 5s stats realm HAProxy\\ Statistics stats uri /admin?stats EOF 安装Docker、Kubeadm、kubectl 所有节点安装Docker/kubeadm/kubelet ，Kubernetes默认CRI（容器运行时）为Docker，因此先安装Docker\n安装Docker 首先配置一下Docker的阿里yum源\n1 2 3 4 5 6 7 8 cat \u0026gt;/etc/yum.repos.d/docker.repo\u0026lt;\u0026lt;EOF [docker-ce-edge] name=Docker CE Edge - \\$basearch baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/7/\\$basearch/edge enabled=1 gpgcheck=1 gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg EOF 然后yum方式安装docker\n1 2 3 4 5 6 7 8 9 # yum安装 yum -y install docker-ce # 查看docker版本 docker --version # 启动docker systemctl enable docker systemctl start docker 配置docker的镜像源\n1 2 3 4 5 cat \u0026gt;\u0026gt; /etc/docker/daemon.json \u0026lt;\u0026lt; EOF { \u0026#34;registry-mirrors\u0026#34;: [\u0026#34;https://b9pmyelo.mirror.aliyuncs.com\u0026#34;] } EOF 然后重启docker\n1 systemctl restart docker 添加kubernetes软件源 然后我们还需要配置一下yum的k8s软件源\n1 2 3 4 5 6 7 8 9 cat \u0026gt; /etc/yum.repos.d/kubernetes.repo \u0026lt;\u0026lt; EOF [kubernetes] name=Kubernetes baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64 enabled=1 gpgcheck=0 repo_gpgcheck=0 gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg EOF 安装kubeadm，kubelet和kubectl 由于版本更新频繁，这里指定版本号部署：\n1 2 3 4 # 安装kubelet、kubeadm、kubectl，同时指定版本 yum install -y kubelet-1.18.0 kubeadm-1.18.0 kubectl-1.18.0 # 设置开机启动 systemctl enable kubelet 部署Kubernetes Master【master节点】 创建kubeadm配置文件 在具有vip的master上进行初始化操作，这里为master1\n1 2 3 4 5 6 # 创建文件夹 mkdir /usr/local/kubernetes/manifests -p # 到manifests目录 cd /usr/local/kubernetes/manifests/ # 新建yaml文件 vi kubeadm-config.yaml yaml内容如下所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 apiServer: certSANs: - master1 - master2 - master.k8s.io - 192.168.44.158 - 192.168.44.155 - 192.168.44.156 - 127.0.0.1 extraArgs: authorization-mode: Node,RBAC timeoutForControlPlane: 4m0s apiVersion: kubeadm.k8s.io/v1beta1 certificatesDir: /etc/kubernetes/pki clusterName: kubernetes controlPlaneEndpoint: \u0026#34;master.k8s.io:16443\u0026#34; controllerManager: {} dns: type: CoreDNS etcd: local: dataDir: /var/lib/etcd imageRepository: registry.aliyuncs.com/google_containers kind: ClusterConfiguration kubernetesVersion: v1.16.3 networking: dnsDomain: cluster.local podSubnet: 10.244.0.0/16 serviceSubnet: 10.1.0.0/16 scheduler: {} 然后我们在 master1 节点执行\n1 kubeadm init --config kubeadm-config.yaml 执行完成后，就会在拉取我们的进行了【需要等待\u0026hellip;】\n按照提示配置环境变量，使用kubectl工具\n1 2 3 4 5 6 7 8 # 执行下方命令 mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config # 查看节点 kubectl get nodes # 查看pod kubectl get pods -n kube-system 按照提示保存以下内容，一会要使用：\n1 2 3 kubeadm join master.k8s.io:16443 --token jv5z7n.3y1zi95p952y9p65 \\ --discovery-token-ca-cert-hash sha256:403bca185c2f3a4791685013499e7ce58f9848e2213e27194b75a2e3293d8812 \\ --control-plane \u0026ndash;control-plane ： 只有在添加master节点的时候才有\n查看集群状态\n1 2 3 4 # 查看集群状态 kubectl get cs # 查看pod kubectl get pods -n kube-system 安装集群网络 从官方地址获取到flannel的yaml，在master1上执行\n1 2 3 4 5 # 创建文件夹 mkdir flannel cd flannel # 下载yaml文件 wget -c https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml 安装flannel网络\n1 kubectl apply -f kube-flannel.yml 检查\n1 kubectl get pods -n kube-system master2节点加入集群 复制密钥及相关文件 从master1复制密钥及相关文件到master2\n1 2 3 4 5 6 7 # ssh root@192.168.44.156 mkdir -p /etc/kubernetes/pki/etcd # scp /etc/kubernetes/admin.conf root@192.168.44.156:/etc/kubernetes # scp /etc/kubernetes/pki/{ca.*,sa.*,front-proxy-ca.*} root@192.168.44.156:/etc/kubernetes/pki # scp /etc/kubernetes/pki/etcd/ca.* root@192.168.44.156:/etc/kubernetes/pki/etcd master2加入集群 执行在master1上init后输出的join命令,需要带上参数--control-plane表示把master控制节点加入集群\n1 kubeadm join master.k8s.io:16443 --token ckf7bs.30576l0okocepg8b --discovery-token-ca-cert-hash sha256:19afac8b11182f61073e254fb57b9f19ab4d798b70501036fc69ebef46094aba --control-plane 检查状态\n1 2 3 kubectl get node kubectl get pods --all-namespaces 加入Kubernetes Node 在node1上执行\n向集群添加新节点，执行在kubeadm init输出的kubeadm join命令：\n1 kubeadm join master.k8s.io:16443 --token ckf7bs.30576l0okocepg8b --discovery-token-ca-cert-hash sha256:19afac8b11182f61073e254fb57b9f19ab4d798b70501036fc69ebef46094aba 集群网络重新安装，因为添加了新的node节点\n检查状态\n1 2 kubectl get node kubectl get pods --all-namespaces 测试kubernetes集群 在Kubernetes集群中创建一个pod，验证是否正常运行：\n1 2 3 4 5 6 # 创建nginx deployment kubectl create deployment nginx --image=nginx # 暴露端口 kubectl expose deployment nginx --port=80 --type=NodePort # 查看状态 kubectl get pod,svc 然后我们通过任何一个节点，都能够访问我们的nginx页面\n","permalink":"https://ktzxy.top/posts/xfqke7p6so/","summary":"18 Kubernetes搭建高可用集群","title":"18 Kubernetes搭建高可用集群"},{"content":"Windows下Go语言的安装 前言 这阵子因为以后工作的原因，所以开始了go语言的学习之旅，工欲善其事必先利其器，首先就得把go语言环境搭建完成\n下载Go 因为go语言的官网经常打不开，所以我就找了一个 镜像网站，里面有很多版本的Go语言，选择自己合适的，比如我的是Windows电脑，所以我选中里面的Windows版本的\n下载完成是一个安装文件，我们需要进行安装，同时需要注意的就是安装目录，因为事后还需要配置环境变量，下面是安装成功后的图片\n配置环境变量 根据windows系统在查找可执行程序的原理，可以将Go所在路径定义到环境变量中，让系统帮我们去找运行的执行程序，这样在任何目录下都可以执行go指令，需要配置的环境变量有：\n环境变量 说明 GOROOT 指定SDK的安装目录 Path 添加SDK的/binmulu GOPATH 工作目录 首先我们需要打开我们的环境变量，然后添加上GOROOT\n然后我们在PATH上添加我们的bin目录\n添加完成后，我们输入下面的命令，查看是否配置成功\n1 go version 下载Jetbrain下的GoLang 在我们配置好环境，我们就可以使用Jetbrain公司开发的Goland编辑器了，首先进入官网下载\nhttps://www.jetbrains.com/\n下载完成后，进行启动\n启动完成后，我们需要配置一下环境，点击：File -\u0026gt;settings -\u0026gt; GOROOT，配置一下刚刚go安装的目录\n以及GOPATH项目所在的目录\nhello world 在上面的方法都完成以后，我来来输出hello world吧~\n1 2 3 4 5 6 7 package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;hello world!\u0026#34;) } 代码的说明\ngo文件的后缀是.go package main：表示该hello.go文件所在的包是main，在go中，每个文件都归属与一个包 import \u0026ldquo;fmt\u0026rdquo;：表示引入一个包，可以调用里面的函数 func main()：表示程序入口，是一个主函数 输出结果\n编译和执行 我们可以通过使用下面命令进行编译和执行\n1 2 3 4 # 编译 hello.go 后 会生成一个 hello.exe文件 go build hello.go # 运行 hello.ext hello.ext 需要注意的是，我们也可以使用下面的方式，来直接运行的（使用go run会比较慢，因为内部有个编译的过程）\n1 go run hello.go 但是在生产环境中，是需要先编译在执行的\n","permalink":"https://ktzxy.top/posts/7ebm5s6gbc/","summary":"0 Go语言的安装","title":"0 Go语言的安装"},{"content":"番外篇 1. MySQL目录结构 windows版本 bin：存放可执行文件，比如MySQL.exe data：存储的是MySQL默认的数据库 include：存放的C语言的头文件 lib：存放的C++的动态链接库 my.ini：数据库的配置文件 2. 启动 MySQL 服务器程序的相关可执行文件 启动 MySOL 服务器程序的可执行文件有很多，大多在 MySQL 安装目录的 bin 目录下。\nmysqld（不常用） mysqld 这个可执行文件就代表着 MySOL 服务器程序，运行这个可执行文件就可以直接启动一个服务器进程。 mysqld_safe mysqld_safe是一个启动脚本，它会间接的调用mysqld，而且还顺便启动了另外一个监控进程，这个监控进程在服务器进程挂了的时候，可以帮助重启它。 除外，使用mysqld_safe启动服务器程序时，它会将服务器程序的出错信息和其他诊断信息重定向到某个文件中，产生出错日志，方便找出发生错误的原因。 mysql.server mysql.server也是一个启动脚本，它会间接的调用mysqld_safe。需要注意的是，这个mysql.server文件其实是一个链接文件，它的实际文件位置是support-files/mysql.server，所以如果在 bin 目录找不到，则到support-files去找，也可以自行用 ln 命令在 bin 创建一个链接。\n在调用mysql.server时在后边指定start参数就可以启动服务器程序；如指定stop参数则关闭正在运行的服务器程序。\n1 2 3 4 # 开启MySQL服务 mysql.server start # 关闭MySQL服务 mysql.server stop mysqld_multi mysqld_multi可以一台计算机上也可以运行多个服务器实例（即运行了多个MySQL服务器进程）。此可执行文件可以对每一个服务器进程的启动或停止进行监控。\n3. Linux 系统安装MySQL 3.1. 下载 Linux 安装包 下载地址：https://dev.mysql.com/downloads/mysql/\n3.2. 安装 MySQL 卸载 centos 中预安装的 mysql 1 2 rpm -qa | grep -i mysql rpm -e mysql-libs-5.1.71-1.el6.x86_64 --nodeps 上传 mysql 的安装包 1 alt + p --\u0026gt; put E:/test/MySQL-5.6.22-1.el6.i686.rpm-bundle.tar 解压 mysql 的安装包 1 2 mkdir mysql tar -xvf MySQL-5.6.22-1.el6.i686.rpm-bundle.tar -C /root/mysql 安装依赖包 1 2 yum -y install libaio.so.1 libgcc_s.so.1 libstdc++.so.6 libncurses.so.5 --setopt=protected_multilib=false yum update libstdc++-4.4.7-4.el6.x86_64 安装 mysql-client 1 rpm -ivh MySQL-client-5.6.22-1.el6.i686.rpm 安装 mysql-server 1 rpm -ivh MySQL-server-5.6.22-1.el6.i686.rpm 3.3. 启动/停止 MySQL 服务 1 2 3 4 5 6 7 service mysql start service mysql stop service mysql status service mysql restart 3.4. 登录 MySQL mysql 安装完成之后, 会自动生成一个随机的密码, 并且保存在一个密码文件中：/root/.mysql_secret\n1 mysql -u root -p 登录之后，修改密码：\n1 set password = password(\u0026#39;123456\u0026#39;); 进入mysql，授权远程访问：\n1 2 grant all privileges on *.* to \u0026#39;root\u0026#39; @\u0026#39;%\u0026#39; identified by \u0026#39;123456\u0026#39;; flush privileges; 如果此时使用SSH软件还是无法远程连接mysql数据库，可能就是linux系统的防火墙导致的，所以需要设置关闭防火墙\n1 2 3 4 5 # 查询看防火墙状态 service iptables status # 关闭防火墙（全部、暴力） service iptables stop 4. CentOS 7.4 安装与配置MySql 5.7.21（项目B安装） 4.1. 环境 系统环境：centos-7.4 64位 安装方式：rpm安装 软件：mysql-5.7.21-1.el7.x86_64.rpm-bundle.tar 描述：上述的tar包中已经包含需要安装的rpm，所以只需要将其放置到系统中使用tar命令解包即可。 Mysql的下载地址：http://dev.mysql.com/downloads/mysql/ 4.2. 系统原mariadb版本 1 2 3 4 5 6 7 # 查看MySql与mariadb安装情况 # grep -i是不分大小写字符查询，只要含有mysql就显示 rpm -qa | grep -i mysql rpm -qa | grep mariadb # 卸载mariadb(会与mysql冲突) rpm -e --nodeps mariadb-libs-5.5.56-2.el7.x86_64 4.3. 安装新MySQL 使用winSCP将下载的“mysql-5.7.21-1.el7.x86_64.rpm-bundle.tar”传到虚拟机系统的/root目录下：\n在终端上进入/root目录；解包.tar包\n1 2 # 对” mysql-5.7.21-1.el7.x86_64.rpm-bundle.tar”解包，不是压缩文件不需要解压缩 tar -xvf mysql-5.7.21-1.el7.x86_64.rpm-bundle.tar 执行如下安装命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 1、安装 mysql-community-common rpm -ivh mysql-community-common-5.7.21-1.el7.x86_64.rpm # 2、安装 mysql-community-libs rpm -ivh mysql-community-libs-5.7.21-1.el7.x86_64.rpm # 3、安装 mysql-community-client rpm -ivh mysql-community-client-5.7.21-1.el7.x86_64.rpm # 4、安装 mysql-community-server yum -y install perl rpm -ivh mysql-community-server-5.7.21-1.el7.x86_64.rpm # 5、安装 mysql-community-devel rpm -ivh mysql-community-devel-5.7.21-1.el7.x86_64.rpm 安装完成。MySql默认安装文件位置：\n1 2 3 4 /var/lib/mysql/ # 数据库目录 /usr/share/mysql # 配置文件目录 /usr/bin # 相关命令目录 /etc/my.cnf # 核心配置文件 4.4. 配置MySQL 4.4.1. 启动mysql 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #启动mysql service mysqld start #重启mysql service mysqld restart #停止mysql service mysqld stop #查看mysql状态 service mysqld status # 设置开机启动Mysql systemctl enable mysqld # 设置开机不启动Mysql systemctl disable mysqld 4.4.2. 修改root密码 MySQL安装成功后，会生成一个临时密码，第一次登录需要输入这个密码，所以查看该临时密码，然后修改密码。\n1 2 3 4 5 6 7 8 # 查看临时密码(/var/log/mysqld.log) grep password /var/log/mysqld.log # 使用root登录 mysql –uroot –p # 然后输入/var/log/mysqld.log文件中的临时密码。登录后；修改密码为Root_123 set password = password(\u0026#39;Root_123\u0026#39;); 注意：密码必须包含大小写字母、数字、特殊符号\n在 mysql 内置密码校验器，用于校验用户设置的密码复杂度。使用以下命令可以查看密码校验的规则：\n1 2 3 4 5 6 7 8 9 10 11 12 mysql\u0026gt; SHOW VARIABLES LIKE \u0026#39;validate_password.%\u0026#39;; +--------------------------------------+--------+ | Variable_name | Value | +--------------------------------------+--------+ | validate_password.check_user_name | ON | | validate_password.dictionary_file | | | validate_password.length | 8 | | validate_password.mixed_case_count | 1 | | validate_password.number_count | 1 | | validate_password.policy | MEDIUM | | validate_password.special_char_count | 1 | +--------------------------------------+--------+ 通过以下命令，降低密码的校验规则\n1 2 set global validate_password.policy = 0; set global validate_password.length = 4; 设置简单的密码\n1 ALTER USER \u0026#39;root\u0026#39;@\u0026#39;localhost\u0026#39; IDENTIFIED BY \u0026#39;123456\u0026#39;; Notes: 官方文档地址：https://dev.mysql.com/doc/refman/8.0/en/validate-password-options-variables.html\n4.4.3. 设置允许远程访问 因为 root 用户默认的主机是 localhost，为了可以远程访问 mysql 服务，需要将 root 用户的主机地址修改为 *。（或者创建主机地址为 * 的其他用户）\n1 2 3 4 5 6 #登录，密码为新修改的密码Root_123 mysql -uroot –p #设置远程访问（使用root密码）： mysql\u0026gt; grant all privileges on *.* to \u0026#39;root\u0026#39; @\u0026#39;%\u0026#39; identified by \u0026#39;Root_123\u0026#39;; mysql\u0026gt; flush privileges; 4.4.4. 设置3306端口可以被访问 1 2 # 退出mysql，防火墙中打开3306端口 firewall-cmd --zone=public --add-port=3306/tcp --permanent 参数说明：\n–zone：作用域 -–add-port=3306/tcp：添加端口，格式为：端口/通讯协议 -–permanent：永久生效，没有此参数重启后失效 1 2 3 4 5 6 7 8 9 10 11 # 重启防火墙 firewall-cmd --reload # 查看已经开放的端口 firewall-cmd --list-ports # 停止防火墙 systemctl stop firewalld.service # 启动防火墙 systemctl start firewalld.service # 禁止防火墙开机启动 systemctl disable firewalld.service 5. MySQl 安装（Windows 免安装版） 5.1. 解压与创建配置文件 将MySQL软件包解压在没有中文和空格的目录下 在解压目录创建my.ini文件并添加内容如下： 5.2. 配置环境变量 在【我的电脑】右键 -\u0026gt; 选择【高级系统设置】 -\u0026gt; 选择【高级】 -\u0026gt; 【环境变量】。设置环境变量将【MYSQL_HOME】添加到PATH环境变量 5.3. 配置系统开启服务 使用管理员权限进入DOS，在cmd中，进入解压目录下的bin目录依次执行以下命令：\n对mysql进行初始化。请注意，这里会生成一个临时密码，后边要使用这个临时密码 1 mysqld --initialize --user=mysql --console 安装mysql服务 1 mysqld --install 启动mysql服务 1 net start mysql 登录mysql，这里需要使用之前生成的临时密码。如果窗口关了没有记录临时密码，可以将mysql目录下的data目录删除，然后再进行初始化 1 mysql -uroot –p 修改root用户密码 1 ALTER USER \u0026#39;root\u0026#39;@\u0026#39;localhost\u0026#39; IDENTIFIED WITH mysql_native_password BY \u0026#39;123456\u0026#39;; 修改root用户权限 1 create user \u0026#39;root\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED WITH mysql_native_password BY \u0026#39;123456\u0026#39;; 5.4. mysql 服务启动时报“某些服务在未由其他服务或程序使用时将自动停止” 在配置文件中有 secure-file-priv 的配置，如果设置了此目录，本来解压后是没有此目录的，如果不手动创建，启动时会报错。\n当时排查很久都不知道是什么原因，后来查询资料，发现 mysqld --console 命令可以将错误信息输出到控制台上，然后就看到具体的无法启动的报错日志\n然后在配置的位置手动创建相应的目录，问题就解决了。\n6. 安装 MySQL 8.0（Windows版本） 6.1. 安装包下载 下载地址：https://downloads.mysql.com/archives/installer/\n6.2. 安装步骤（截图，不完整，日后优化） 此安装示例使用 mysql-installer-community-8.0.27.1.msi\n6.2.1. 自定义安装 注：以上可以不选择相关文档与示例的组件\n6.2.2. 默认选择安装 当所有的状态都变成Complete之后，点击 Next\n设置密码，用于登陆数据，建议使用学习的设置简单的密码\n6.3. 配置环境变量 配置环境变量与windows免安装版一样\n7. 开启/停止 MySQL 服务（Windows版本） 7.1. 启动MySQL服务方式1 MySQL会以windows服务的方式为我们提供数据存储功能。开启和关闭服务的操作：\n右键点击我的电脑 \u0026ndash;\u0026gt; 管理 \u0026ndash;\u0026gt; 服务与应用程序 \u0026ndash;\u0026gt; 服务 \u0026ndash;\u0026gt; 找到 MySQL 服务开启或停止。 或者：开始 \u0026ndash;\u0026gt; 搜索 \u0026ndash;\u0026gt; services.msc \u0026ndash;\u0026gt; 服务 \u0026ndash;\u0026gt; 可以找到 MySQL 服务开启或停止。 （如果不需要开机时就启动MySQL，右键MySQL \u0026ndash;\u0026gt; 属性 \u0026ndash;\u0026gt; 启动类型选“手动”）\n7.2. 启动MySQL服务方式2 在DOS窗口，通过命令完成MySQL服务的启动和停止（必须以管理员身份运行cmd命令窗口）\nMySQL 启动: net start mysql MySQL 关闭: net stop mysql Tips: 上述命令的 mysql 是在安装 MySQL 时，默认指定的 mysql 的系统服务名，不是固定的，如果未改动，5.7版本默认是mysql，8.0版本默认是mysql80。\n8. 客户端连接 MySQL MySQL 是一个需要账户名密码登录的数据库，登陆后使用，它提供了一个默认的root账号，使用安装时设置的密码即可登录。\n8.1. 方式一：使用 MySQL 提供的客户端命令行工具 8.2. 方式二：使用系统自带的命令行工具执行指令 使用 CMD 命令行窗口，可以操作登陆与退出 MySQL，命令语法如下：\n1 mysql [-h 127.0.0.1] [-P 3306] -u root -p 参数说明：\n-h：MySQL 服务所在的主机 IP -P：MySQL 服务端口号，默认3306。注意：参数是大写 -u：MySQL 数据库用户名 -p：MySQL 数据库用户名对应的密码。注意：参数是小写 [] 内为可选参数，如果需要连接远程的 MySQL，需要加上这两个参数来指定远程主机 IP、端口，如果连接本地的 MySQL，则无需指定这两个参数。\nTips: 使用这种方式进行连接时，需要安装完毕后配置PATH环境变量。\n示例1：使用 cmd 命令行输入 mysql –u用户名 –p密码\n示例2：使用 cmd 命令行输入 mysql --user=用户名 --password=密码 --host=ip地址 --port=端口号（这种方式一般用来登陆别人的数据库）\n8.3. 退出连接 MySQL 直接使用 exit 命令即可退出 MySQL 连接\n8.4. 其他操作数据库方式 通过第三方图形化界面操作 通过Java代码操作 9. 卸载MySQL（Windows版本） 先停止MySQL服务 方式1：输入命令行net stop mysql。注：命令中的mysql是服务名称，需要输入根据实际的名称 方式2：【win+r】打开“运行”面板，输入services.msc，进行服务窗口中关闭mysql服务 卸载程序。到【控制面板】中的【程序和功能】中卸载，或者使用第三方的卸载工具，如：Geek Uninstaller 删除根文件夹。进入sql安装位置，手动删除mysql的解压（安装）的文件夹 删除C盘隐藏的数据文件夹（可选）：”C:\\ProgramData\\MySQL“。注：如果安装或者改了配置文件，此数据文件夹目录可能不一样 删除注册表信息。【win+r】打开“运行”面板，输入regedit命令打开注册表窗口，删除以下文件： 删除环境变量的配置。进行【高级系统设置】中的【环境变量】，删除安装后配置MYSQL_HOME变量和删除path变量中的mysql路径 删除MySQL服务。使用管理员方式打开cmd命令行窗口，输入命令sc delete Mysql服务名称 10. MySQL 常用图形管理工具 10.1. Navicat Navicat是一套快速、可靠的数据库管理工具，Navicat 是以直觉化的图形用户界面而建的，可以兼容多种数据库，支持多种操作系统。\n官网：http://www.navicat.com.cn/download/navicat-premium\n10.2. SQLyog SQLyog 是一个快速而简洁的图形化管理MySQL数据库的工具，它能够在任何地点有效地管理你的数据库，由业界著名的Webyog公司出品。\n官网：https://sqlyog.en.softonic.com/\n安装：提供的SQLyog软件为免安装版，可直接使用\n使用：输入用户名、密码，点击连接按钮，进行访问MySQL数据库进行操作\n在Query窗口中，输入SQL代码，选中要执行的SQL代码，按F8键运行，或按执行按钮运行。\n10.3. MySQL Workbench MySQL Workbench MySQL 是官方提供的图形化管理工具，分为社区版和商业版，社区版完全免费，而商业版则是按年收费。支持数据库的创建、设计、迁移、备份、导出和导入等功能，并且支持 Windows、Linux 和 mac 等主流操作系统。\n10.4. DataGrip DataGrip 是一款由JetBrains公司出品的数据库管理客户端工具，方便连接到数据库服务器，执行sql、创建表、创建索引以及导出数据等\n使用说明 添加数据源 配置以及驱动 jar 包下载完毕之后，就可以点击 \u0026ldquo;Test Connection\u0026rdquo; 就可以测试，是否可以连接 MySQL，如果出现 \u0026ldquo;Successed\u0026rdquo;，就表示连接成功了。\n展示所有数据库。连接上了 MySQL 服务之后，并未展示出所有的数据库，此时，需要设置展示所有的数据库。 创建数据库 以下两种方式都可以创建数据库：\ncreate database db01; create schema db01; 创建表。在指定的数据库上面右键，选择 new -\u0026gt; Table 修改表结构。在需要修改的表上，右键选择 \u0026ldquo;Modify Table\u0026hellip;\u0026rdquo; 如果想增加字段，直接点击+号，录入字段信息，然后点击Execute即可。 如果想删除字段，直接点击-号，就可以删除字段，然后点击Execute即可。 如果想修改字段，双击对应的字段，修改字段信息，然后点击Execute即可。 如果要修改表名，或表的注释，直接在输入框修改，然后点击Execute即可。 在 DataGrip 中执行 SQL 语句。在指定的数据库上，右键，选择 New -\u0026gt; Query Console 然后就可以在打开的 Query Console 控制台，并在控制台中编写 SQL，执行 SQL。\n10.5. HeidiSQL HeidiSQL 是免费软件，其目标是易于学习。“Heidi”让您可以从运行 MariaDB、MySQL、Microsoft SQL、PostgreSQL 和 SQLite 数据库系统之一的计算机上查看和编辑数据和结构。HeidiSQL 由 Ansgar 于 2002 年发明，属于全球最流行的 MariaDB 和 MySQL 工具。\n官网：https://www.heidisql.com/\n10.6. DBeaver DBeaver是一款世界有名的数据库管理软件。DBeaver64位(数据库管理软件)给大家提供的是dbeaver中文版64位版本，支持MySQL、PostgreSQL和任何数据库，其中有一个JDBC驱动程序，可以查看数据库的结构、导出数据库或者执行相应的脚本等操作。\n下载地址：DBeaver Community | Free Universal Database Tool\n10.7. 其他工具 phpMyAdmin MySQLDumper MySQL GUI Tools MySQL ODBC Connector ","permalink":"https://ktzxy.top/posts/8q8nmfnyld/","summary":"MySQL准备知识","title":"MySQL准备知识"},{"content":"Kubernetes中的CRI 前言 Kubernetes 节点的底层由一个叫做容器运行时的软件进行支撑，它主要负责启停容器。\nDocker 是目前最广为人知的容器运行时软件，但是它并非唯一。在这几年中，容器运行时这个领域发展的迅速。为了使得 Kubernetes 的扩展变得更加容易，一直在打磨支持容器运行时的 K8S插件 API，也就是 容器运行时接口 ( Container Runtime Interface, CRI) 。\nk8s架构 这里通过分析 k8s 目前默认的一种容器运行时架构，来帮助我们更好的理解 k8s 运行时的背后逻辑，同时引出 CRI 和 OCI 提出的背景。\n我们在创建 k8s 集群的时候，首先需要搭建 master 节点，其次需要创建 node 节点，并将 node 节点加入到 k8s 集群中。当我们构建好 k8s 集群后，可以通过 下面命令来创建应用对应的pod\n1 kubectl create -f nginx.yml 执行完成后，该命令首先会提交给 API Server ，然后解析 yml 文件，并对其以 API 对象的形式存到 etcd 里。\n这时候，master 组件中的 Controller Manager 会通过控制循环的方式来做编排工作，创建应用所需的Pod。同时 Scheduler 会 watch etcd 中新 pod 的变化，如果他发现有一个新的 pod 的变化。\n如果 Scheduler 发现有一个新的 pod 出现，它会运行调度算法，然后选择出最佳的 Node 节点，并将这个节点的名字写到 pod 对象的 NodeName 字段上，这一步就是所谓的 Bind Pod to Node，然后把 bind 的结果写到 etcd。\n其次，当我们在构建 k8s 集群的时候，默认每个节点都会初始化创建一个 kubectl 进程，kubectl 进程会 watch etcd 中 pod 的变化，当 kubectl 进程监听到 pod 的 bind 的更新操作，并且 bind 的节点是本节点时，它会接管接下来的所有事情，如镜像下载，创建容器等。\nk8s默认容器运行时架构 接下来将通过 k8s 默认集成的容器运行时架构，来看 kubernetes 如何创建一个容器 （如下图）\nkubernetes 通过 CRI (Container Runtime Interface) 接口调用 dockershim，请求创建一个容器。这一步中，Kubectl 可以视作一个简单的 CRI Client，而 dockershim 就是接收的 Server。 dockershim 收到请求后，通过适配的方式，适配成 Docker Daemon 的请求格式，发到 Docker Daemon 上请求创建一个容器。在 docker 1.12 后的版本，docker daemon 被拆分成了 dockerd 和 containerd，其中，containerd 负责操作容器。 dockerd 收到请求后，会调用 containerd 进程去创建一个容器 containerd 收到请求后，并不会自己直接去操作容器，而是创建一个叫做 containerd-shim 的进程，让 containerd-shim 去操作容器，创建 containerd-shim 的目的主要有以下几个 让 containerd-shim 做诸如收集状态，维持 stdin 等 fd 打开等工作。 允许容器运行时( runC ) 启动容器后退出，不必为每个容器一直运行一个容器运行时的 runC 即使在 containerd 和 dockerd 都挂掉的情况下，容器的标准 IO 和其它的文件描述符也是可以用的 向 containerd 报告容器的退出状态 在不中断容器运行时的情况下，升级或重启 dockerd 而 containerd-shim 在这一步需要调用 runC 这个命令行工具，来启动容器，runC 是 OCI (Open Container Initiative， 开放标准协议) 的一个参考实现。主要用来设置 namespaces 和 cgroups，挂载 root filesystem等操作。 runC 启动完容器后，本身会直接退出。containerd-shim 则会成为容器进程的父进程，负责收集容器进程的状态，上报给 containerd，并在容器中的 pid 为 1 的进程退出后接管容器中的子进程进行清理，确保不会出现僵尸进程 (关闭进程描述符)。 容器与容器编排背景 从 k8s 的容器运行时可以看出，kubectl 启动容器的过程经过了很长的一段调用链路。这个是由于在容器及编排领域各大厂商与 docker 之间的竞争以及 docker 公司为了抢占 Pass ( Platform-as-a-service，平台服务) 领域市场，对架构做出的一系列调整。\n其实 k8s 最开始的运行时架构链路调用没有这么复杂：kubelet想要创建容器直接通过 docker api 调用 Docker Daemon ， 然后Docker Daemon 调用 libcontainer 这个库来启动容器。\n后面为了防止 docker 垄断以及受控 docker 运行时，各大厂商于是就联合起来，制订出 开放容器标准OCI ( Open Containers Initiative ) 。大家可以基于这个标准开发自己的容器运行时。Docker公司则把 libcontainer 做了一层封装，变成 runC 捐献给 CNCF 作为 OCI 的参考实现。\n接下来就是 Docker 要搞 Swarm 进军 PaaS 市场，于是做了个架构切分，把容器操作都移动到一个单独的 Daemon 进程的 containerd 中去，让 Docker Daemon专门负责上层封装编排，但是最终 Swarm 败给了 K8S ，于是Docker公司就把 Containerd 捐给了 CNCF，专注于搞 Docker 企业版了。\n与此同时，容器领域 core os 公司推出了 rkt 容器运行时，希望 k8s 原生支持 rkt 作为运行时，由于 core os 与 Google 的关系，最终 rkt 运行时的支持在 2016 年也被合并进 kubelet 主干代码里，这样做反而给 k8s 中负责维护 kubelet 的小组 SIG-Node 带来了更大的负担，每一次 kubectl 的更新都要维护 docker 和 rkt 作为两部分代码。与此同时，随着虚拟化技术强隔离容器技术 runV (Kata Containers 前身, 后与 intel clear container 合并)的逐渐成熟。K8S 上游对虚拟化容器的支持很快被提上日程。为了从集成每一种运行时都要维护的一份代码中解救出来，K8S SIG-Node 工作组决定对容器的操作统一地抽象成一个接口，这样 kubelet 只需要跟这个接口打交道，而具体地容器运行时，他们只需要实现该接口，并对kubelet暴露 gRPC 服务即可。这个统一地抽象的接口就是 k8s 中俗称的 CRI。\nCRI接口 CRI (容器运行时接口)基于 gRPC 定义了 RuntimeService 和 ImageService 等两个 gRPC 服务，分别用于容器运行时和镜像的管理。如下所示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 // Runtime service defines the public APIs for remote container runtimes service RuntimeService { // Version returns the runtime name, runtime version, and runtime API version. rpc Version(VersionRequest) returns (VersionResponse) {} // RunPodSandbox creates and starts a pod-level sandbox. Runtimes must ensure // the sandbox is in the ready state on success. rpc RunPodSandbox(RunPodSandboxRequest) returns (RunPodSandboxResponse) {} // StopPodSandbox stops any running process that is part of the sandbox and // reclaims network resources (e.g., IP addresses) allocated to the sandbox. // If there are any running containers in the sandbox, they must be forcibly // terminated. // This call is idempotent, and must not return an error if all relevant // resources have already been reclaimed. kubelet will call StopPodSandbox // at least once before calling RemovePodSandbox. It will also attempt to // reclaim resources eagerly, as soon as a sandbox is not needed. Hence, // multiple StopPodSandbox calls are expected. rpc StopPodSandbox(StopPodSandboxRequest) returns (StopPodSandboxResponse) {} // RemovePodSandbox removes the sandbox. If there are any running containers // in the sandbox, they must be forcibly terminated and removed. // This call is idempotent, and must not return an error if the sandbox has // already been removed. rpc RemovePodSandbox(RemovePodSandboxRequest) returns (RemovePodSandboxResponse) {} // PodSandboxStatus returns the status of the PodSandbox. If the PodSandbox is not // present, returns an error. rpc PodSandboxStatus(PodSandboxStatusRequest) returns (PodSandboxStatusResponse) {} // ListPodSandbox returns a list of PodSandboxes. rpc ListPodSandbox(ListPodSandboxRequest) returns (ListPodSandboxResponse) {} // CreateContainer creates a new container in specified PodSandbox rpc CreateContainer(CreateContainerRequest) returns (CreateContainerResponse) {} // StartContainer starts the container. rpc StartContainer(StartContainerRequest) returns (StartContainerResponse) {} // StopContainer stops a running container with a grace period (i.e., timeout). // This call is idempotent, and must not return an error if the container has // already been stopped. // TODO: what must the runtime do after the grace period is reached? rpc StopContainer(StopContainerRequest) returns (StopContainerResponse) {} // RemoveContainer removes the container. If the container is running, the // container must be forcibly removed. // This call is idempotent, and must not return an error if the container has // already been removed. rpc RemoveContainer(RemoveContainerRequest) returns (RemoveContainerResponse) {} // ListContainers lists all containers by filters. rpc ListContainers(ListContainersRequest) returns (ListContainersResponse) {} // ContainerStatus returns status of the container. If the container is not // present, returns an error. rpc ContainerStatus(ContainerStatusRequest) returns (ContainerStatusResponse) {} // UpdateContainerResources updates ContainerConfig of the container. rpc UpdateContainerResources(UpdateContainerResourcesRequest) returns (UpdateContainerResourcesResponse) {} // ReopenContainerLog asks runtime to reopen the stdout/stderr log file // for the container. This is often called after the log file has been // rotated. If the container is not running, container runtime can choose // to either create a new log file and return nil, or return an error. // Once it returns error, new container log file MUST NOT be created. rpc ReopenContainerLog(ReopenContainerLogRequest) returns (ReopenContainerLogResponse) {} // ExecSync runs a command in a container synchronously. rpc ExecSync(ExecSyncRequest) returns (ExecSyncResponse) {} // Exec prepares a streaming endpoint to execute a command in the container. rpc Exec(ExecRequest) returns (ExecResponse) {} // Attach prepares a streaming endpoint to attach to a running container. rpc Attach(AttachRequest) returns (AttachResponse) {} // PortForward prepares a streaming endpoint to forward ports from a PodSandbox. rpc PortForward(PortForwardRequest) returns (PortForwardResponse) {} // ContainerStats returns stats of the container. If the container does not // exist, the call returns an error. rpc ContainerStats(ContainerStatsRequest) returns (ContainerStatsResponse) {} // ListContainerStats returns stats of all running containers. rpc ListContainerStats(ListContainerStatsRequest) returns (ListContainerStatsResponse) {} // UpdateRuntimeConfig updates the runtime configuration based on the given request. rpc UpdateRuntimeConfig(UpdateRuntimeConfigRequest) returns (UpdateRuntimeConfigResponse) {} // Status returns the status of the runtime. rpc Status(StatusRequest) returns (StatusResponse) {} } // ImageService defines the public APIs for managing images. service ImageService { // ListImages lists existing images. rpc ListImages(ListImagesRequest) returns (ListImagesResponse) {} // ImageStatus returns the status of the image. If the image is not // present, returns a response with ImageStatusResponse.Image set to // nil. rpc ImageStatus(ImageStatusRequest) returns (ImageStatusResponse) {} // PullImage pulls an image with authentication config. rpc PullImage(PullImageRequest) returns (PullImageResponse) {} // RemoveImage removes the image. // This call is idempotent, and must not return an error if the image has // already been removed. rpc RemoveImage(RemoveImageRequest) returns (RemoveImageResponse) {} // ImageFSInfo returns information of the filesystem that is used to store images. rpc ImageFsInfo(ImageFsInfoRequest) returns (ImageFsInfoResponse) {} } 具体容器运行时则需要实现 CRI 定义的接口（即 gRPC Server，通常称为 CRI shim）。容器运行时在启动 gRPC server 时需要监听在本地的 Unix Socket （Windows 使用 tcp 格式）。\n参考 https://www.kubernetes.org.cn/1079.html\nhttps://www.cnblogs.com/justinli/p/11578951.html\n","permalink":"https://ktzxy.top/posts/x8kwttpa0m/","summary":"52 Kubernetes中的CRI","title":"52 Kubernetes中的CRI"},{"content":"一、redis启动： 本地启动：redis-cli 远程启动：redis-cli -h host -p port -a password Redis 连接命令\nAUTH password 验证密码是否正确 ECHO message 打印字符串 PING 查看服务是否运行 QUIT 关闭当前连接 SELECT index 切换到指定的数据库 二、redis keys命令 1、DEL key\nDUMP key 序列化给定的key并返回序列化的值\n2、EXISTS key\n检查给定的key是否存在\n3、EXPIRE key seconds\n为key设置过期时间\n4、EXPIRE key timestamp\n用时间戳的方式给key设置过期时间\n5、PEXPIRE key milliseconds\n设置key的过期时间以毫秒计\n6、KEYS pattern\n查找所有符合给定模式的key\n7、MOVE key db\n将当前数据库的key移动到数据库db当中\n8、PERSIST key\n移除key的过期时间，key将持久保存\n9、PTTL key\n以毫秒为单位返回key的剩余过期时间\n10、TTL key\n以秒为单位，返回给定key的剩余生存时间\n11、RANDOMKEY\n从当前数据库中随机返回一个key\n12、RENAME key newkey\n修改key的名称\n13、RENAMENX key newkey\n仅当newkey不存在时，将key改名为newkey\n14、TYPE key\n返回key所存储的值的类型\n三、reids字符串命令 1、SET key value\n2、GET key\n3、GETRANGE key start end\n返回key中字符串值的子字符\n4、GETSET key value\n将给定key的值设为value，并返回key的旧值\n5、GETBIT KEY OFFSET\n对key所储存的字符串值，获取指定偏移量上的位\n6、MGET KEY1 KEY2\n获取一个或者多个给定key的值\n7、SETBIT KEY OFFSET VALUE\n对key所是存储的字符串值，设置或清除指定偏移量上的位\n8、SETEX key seconds value\n将值 value 关联到 key ，并将 key 的过期时间设为 seconds (以秒为单位)。\n9、SETNX key value\n只有在 key 不存在时设置 key 的值。\n10、SETRANGE key offset value\n用 value 参数覆写给定 key 所储存的字符串值，从偏移量 offset 开始。\n11、STRLEN key\n返回 key 所储存的字符串值的长度。\n12、MSET key value [key value …]\n同时设置一个或多个 key-value 对。\n13、MSETNX key value [key value …]\n同时设置一个或多个 key-value 对，当且仅当所有给定 key 都不存在。\n14、PSETEX key milliseconds value\n这个命令和 SETEX 命令相似，但它以毫秒为单位设置 key 的生存时间，而不是像 SETEX 命令那样，以秒为单位。\n15、INCR key\n将 key 中储存的数字值增一。\n16、INCRBY key increment\n将 key 所储存的值加上给定的增量值（increment） 。\n17、INCRBYFLOAT key increment\n将 key 所储存的值加上给定的浮点增量值（increment） 。\n18、DECR key\n将 key 中储存的数字值减一。\n19、DECRBY key decrement\nkey 所储存的值减去给定的减量值（decrement） 。\n20、APPEND key value\n如果 key 已经存在并且是一个字符串， APPEND 命令将 指定value 追加到改 key 原来的值（value）的末尾。\n四、Redis hash 命令 1、HDEL key field1 [field2]\n删除一个或多个哈希表字段\n2、HEXISTS key field\n查看哈希表 key 中，指定的字段是否存在。\n3、HGET key field\n获取存储在哈希表中指定字段的值。\n4、HGETALL key\n获取在哈希表中指定 key 的所有字段和值\n5、HINCRBY key field increment\n为哈希表 key 中的指定字段的整数值加上增量 increment 。\n6、HINCRBYFLOAT key field increment\n为哈希表 key 中的指定字段的浮点数值加上增量 increment 。\n7、HKEYS key\n获取所有哈希表中的字段\n8、HLEN key\n获取哈希表中字段的数量\n9、HMGET key field1 [field2]\n获取所有给定字段的值\n10、HMSET key field1 value1 [field2 value2 ]\n同时将多个 field-value (域-值)对设置到哈希表 key 中。\n11、HSET key field value\n将哈希表 key 中的字段 field 的值设为 value 。\n12、HSETNX key field value\n只有在字段 field 不存在时，设置哈希表字段的值。\n13、HVALS key\n获取哈希表中所有值\n14、HSCAN key cursor [MATCH pattern] [COUNT count]\n迭代哈希表中的键值对。\n五、Redis 列表命令 1、BLPOP key1 [key2 ] timeout\n移出并获取列表的第一个元素， 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。\n2、BRPOP key1 [key2 ] timeout\n移出并获取列表的最后一个元素， 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。\n3、BRPOPLPUSH source destination timeout\n从列表中弹出一个值，将弹出的元素插入到另外一个列表中并返回它； 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。\n4、LINDEX key index\n通过索引获取列表中的元素\n5、LINSERT key BEFORE|AFTER pivot value\n在列表的元素前或者后插入元素\n6、LLEN key\n获取列表长度\n7、LPOP key\n移出并获取列表的第一个元素\n8、LPUSH key value1 [value2]\n将一个或多个值插入到列表头部\n9、LPUSHX key value\n将一个值插入到已存在的列表头部\n10、LRANGE key start stop\n获取列表指定范围内的元素\n11、LREM key count value\n移除列表元素\n12、LSET key index value\n通过索引设置列表元素的值\n13、LTRIM key start stop\n对一个列表进行修剪(trim)，就是说，让列表只保留指定区间内的元素，不在指定区间之内的元素都将被删除。\n14、RPOP key\n移除并获取列表最后一个元素\n15、RPOPLPUSH source destination\n移除列表的最后一个元素，并将该元素添加到另一个列表并返回\n16、RPUSH key value1 [value2]\n在列表中添加一个或多个值\n17、RPUSHX key value\n为已存在的列表添加值\n六、Redis 集合命令 1、SADD key member1 [member2]\n向集合添加一个或多个成员\n2、SCARD key\n获取集合的成员数\n3、SDIFF key1 [key2]\n返回给定所有集合的差集\n4、SDIFFSTORE destination key1 [key2]\n返回给定所有集合的差集并存储在 destination 中\n5 、SINTER key1 [key2]\n返回给定所有集合的交集\n6、SINTERSTORE destination key1 [key2]\n返回给定所有集合的交集并存储在 destination 中\n7、SISMEMBER key member\n判断 member 元素是否是集合 key 的成员\n8、SMEMBERS key\n返回集合中的所有成员\n9、SMOVE source destination member\n将 member 元素从 source 集合移动到 destination 集合\n10、SPOP key\n移除并返回集合中的一个随机元素\n11、SRANDMEMBER key [count]\n返回集合中一个或多个随机数\n12、SREM key member1 [member2]\n移除集合中一个或多个成员\n13、SUNION key1 [key2]\n返回所有给定集合的并集\n14、SUNIONSTORE destination key1 [key2]\n所有给定集合的并集存储在 destination 集合中\n15、SSCAN key cursor [MATCH pattern] [COUNT count]\n迭代集合中的元素\n七、Redis 有序集合命令 1、ZADD key score1 member1 [score2 member2]\n向有序集合添加一个或多个成员，或者更新已存在成员的分数\n2、ZCARD key\n获取有序集合的成员数\n3、ZCOUNT key min max\n计算在有序集合中指定区间分数的成员数\n4、ZINCRBY key increment member\n有序集合中对指定成员的分数加上增量 increment\n5、ZINTERSTORE destination numkeys key [key …]\n计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 key 中\n6、ZLEXCOUNT key min max\n在有序集合中计算指定字典区间内成员数量\n7、ZRANGE key start stop [WITHSCORES]\n通过索引区间返回有序集合成指定区间内的成员\n8、ZRANGEBYLEX key min max [LIMIT offset count]\n通过字典区间返回有序集合的成员\n9、ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT]\n通过分数返回有序集合指定区间内的成员\n10、ZRANK key member\n返回有序集合中指定成员的索引\n11、ZREM key member [member …]\n移除有序集合中的一个或多个成员\n12、ZREMRANGEBYLEX key min max\n移除有序集合中给定的字典区间的所有成员\n13、ZREMRANGEBYRANK key start stop\n移除有序集合中给定的排名区间的所有成员\n14、ZREMRANGEBYSCORE key min max\n移除有序集合中给定的分数区间的所有成员\n15、ZREVRANGE key start stop [WITHSCORES]\n返回有序集中指定区间内的成员，通过索引，分数从高到底\n16、ZREVRANGEBYSCORE key max min [WITHSCORES]\n返回有序集中指定分数区间内的成员，分数从高到低排序\n17、ZREVRANK key member\n返回有序集合中指定成员的排名，有序集成员按分数值递减(从大到小)排序\n18、ZSCORE key member\n返回有序集中，成员的分数值\n19、ZUNIONSTORE destination numkeys key [key …]\n计算给定的一个或多个有序集的并集，并存储在新的 key 中\n20、ZSCAN key cursor [MATCH pattern] [COUNT count]\n迭代有序集合中的元素（包括元素成员和元素分值）\n八、Redis 发布订阅命令 1、PSUBSCRIBE pattern [pattern …]\n订阅一个或多个符合给定模式的频道。\n2、PUBSUB subcommand [argument [argument …]\n查看订阅与发布系统状态。\n3、PUBLISH channel message\n将信息发送到指定的频道。\n4、PUNSUBSCRIBE [pattern [pattern …]]\n退订所有给定模式的频道。\n5、SUBSCRIBE channel [channel …]\n订阅给定的一个或多个频道的信息。\n6、UNSUBSCRIBE [channel [channel …]]\n指退订给定的频道。\n示例：\n1 2 3 4 5 6 redis 127.0.0.1:6379\u0026gt; SUBSCRIBE redisChat Reading messages... (press Ctrl-C to quit) \u0026#34;subscribe\u0026#34; \u0026#34;redisChat\u0026#34; (integer) 1 1.2.3.4.5. 现在，我们先重新开启个 redis 客户端，然后在同一个频道 redisChat 发布两次消息，订阅者就能接收到消息。 redis 127.0.0.1:6379\u0026gt; PUBLISH redisChat “Redis is a great caching technique”\n(integer) 1\n1 2 3 4 5 6 订阅者的客户端会显示如下消息 1) \u0026#34;message\u0026#34; 2) \u0026#34;redisChat\u0026#34; 3) \u0026#34;Redis is a great caching technique\u0026#34; 1.2.3.4.5. 九、Redis 事务命令 1、DISCARD\n取消事务，放弃执行事务块内的所有命令。\n2、EXEC\n执行所有事务块内的命令。\n3、MULTI\n标记一个事务块的开始。\n4、UNWATCH\n取消 WATCH 命令对所有 key 的监视。\n5、WATCH key [key …]\n监视一个(或多个) key ，如果在事务执行之前这个(或这些) key 被其他命令所改动，那么事务将被打断。\n十、Redis 脚本命令 1、EVAL script numkeys key [key …] arg [arg …]\n执行 Lua 脚本。\n2、EVALSHA sha1 numkeys key [key …] arg [arg ….]\n执行 Lua 脚本。\n3、SCRIPT EXISTS script [script …]\n查看指定的脚本是否已经被保存在缓存当中。\n4、SCRIPT FLUSH\n从脚本缓存中移除所有脚本。\n5、SCRIPT KILL\n杀死当前正在运行的 Lua 脚本。\n6、SCRIPT LOAD script\n将脚本 script 添加到脚本缓存中，但并不立即执行这个脚本。\n十一、Redis 服务器命令 1、BGREWRITEAOF\n异步执行一个 AOF（AppendOnly File） 文件重写操作\n2、BGSAVE\n在后台异步保存当前数据库的数据到磁盘\n3、CLIENT KILL [ip:port] [ID client-id]\n关闭客户端连接\n4、CLIENT LIST\n获取连接到服务器的客户端连接列表\n5、CLIENT GETNAME\n获取连接的名称\n6、CLIENT PAUSE timeout\n在指定时间内终止运行来自客户端的命令\n7、CLIENT SETNAME connection-name\n设置当前连接的名称\n8、CLUSTER SLOTS\n获取集群节点的映射数组\n9、COMMAND\n获取 Redis 命令详情数组\n10、COMMAND COUNT\n获取 Redis 命令总数\n11、COMMAND GETKEYS\n获取给定命令的所有键\n12、TIME 返\n回当前服务器时间\n13、COMMAND INFO command-name [command-name …]\n获取指定 Redis 命令描述的数组\n14、CONFIG GET parameter\n获取指定配置参数的值\n15、CONFIG REWRITE\n对启动 Redis 服务器时所指定的 redis.conf 配置文件进行改写\n16、CONFIG SET\narameter value 修改 redis 配置参数，无需重启\n17、CONFIG RESETSTAT\n重置 INFO 命令中的某些统计数据\n18、DBSIZE\n返回当前数据库的 key 的数量\n19、DEBUG OBJECT key\n获取 key 的调试信息\n20、DEBUG SEGFAULT\n让 Redis 服务崩溃\n21、、FLUSHALL\n删除所有数据库的所有key\n22、FLUSHDB\n删除当前数据库的所有key\n23 、INFO [section]\n获取 Redis 服务器的各种信息和统计数值\n24、LASTSAVE\n返回最近一次 Redis 成功将数据保存到磁盘上的时间，以 UNIX 时间戳格式表示\n25、MONITOR\n实时打印出 Redis 服务器接收到的命令，调试用\n26、ROLE\n返回主从实例所属的角色\n27、SAVE\n同步保存数据到硬盘\n28、SHUTDOWN [NOSAVE] [SAVE]\n异步保存数据到硬盘，并关闭服务器\n29、SLAVEOF host port\n将当前服务器转变为指定服务器的从属服务器(slave server)\n30、SLOWLOG subcommand [argument]\n管理 redis 的慢日志\n31、SYNC\n用于复制功能(replication)的内部命令\n","permalink":"https://ktzxy.top/posts/2b42ssofe8/","summary":"Redis命令总结","title":"Redis命令总结"},{"content":"1. Redis 的部署方案简述 Redis 的部署分为：Redis 单机版安装、Redis 主从模式安装、Redis 哨兵模式安装和 Redis Cluster（集群模式）安装。\n单机版：单机部署，单机redis能够承载的 QPS 大概就在上万到几万不等。实际生产这种部署方式很少使用，一般都是用于本地测试开发。存在的问题： 内存容量有限 处理能力有限 无法高可用 主从模式：一主多从，主负责写，并且将数据复制到其它的 slave 节点，从节点负责处理所有的读请求。这样也可以很轻松实现水平扩容，支撑读高并发。master 节点挂掉后，需要手动指定新的 master，可用性不高，基本不用。 哨兵模式：通过哨兵机制（Sentinel）可以自动切换主从节点，因此解决了主从复制存在不能自动故障转移、达不到高可用的问题。master 节点挂掉后，哨兵进程会主动选举新的 master，可用性高，但是每个节点存储的数据是一样的，浪费内存空间。数据量不是很多，集群规模不是很大，需要自动容错容灾的时候使用。 Redis cluster：服务端分片技术，3.0版本开始正式提供。Redis Cluster 并没有使用一致性 hash，而是采用slot(槽)的概念，一共分成16384个槽。将请求发送到任意节点，接收到请求的节点会将查询请求发送到正确的节点上执行。主要是针对海量数据+高并发+高可用的场景，如果是海量数据，那么建议使用 Redis cluster，所有主节点的容量总和就是 Redis cluster 可缓存的数据容量。 2. Redis 单机模式 2.1. windows 单机版 2.1.1. 直接运行 Redis 服务 Windows 版本的 redis 下载地址：https://github.com/MicrosoftArchive/redis/tags\n下载 Redis-x64-3.2.100 版本，解压 Redis-x64-3.2.100.zip 到无中文与空格的目录中。\n进入 cmd 命令行，进入 Redis-x64-3.2.100 目录，运行以下命令。（注：如果使用powershell打开，需要在命令前增加“./”）\n1 redis-server redis.windows.conf 出现下图说明，redis启动成功\n2.1.2. 将 Redis 注册为服务 进入 cmd 命令行，进入 redis 所在目录，运行以下命令（注：如果使用powershell打开，需要在命令前增加“./”） 1 redis-server --service-install redis.windows-service.conf --loglevel verbose 成功执行命令后，刷新服务，会看到多了一个redis服务 进入redis所在目录，输入常用的redis服务命令 1 2 3 redis-server.exe --service-uninstall # 卸载服务 redis-server.exe --service-start # 开启服务 redis-server.exe --service-stop # 停止服务 2.2. linux 单机版 2.2.1. 安装（压缩包安装） 参考文档： E:\\07-编程工具资料\\04-数据库\\Redis\\Redis安装和使用.docx E:\\07-编程工具资料\\04-数据库\\Redis\\Redis安装.doc 安装 redis 的依赖环境 1 [root@localhost src]# yum -y install gcc automake autoconf libtool make 上传安装包 获取到安装包，使用 rz 命令（需要系统支持）并将它上传到 linux 的 /usr/local/src/ 目录下\n1 2 [root@localhost src]# ls redis-5.0.4.tar.gz 解压 解压安装包，得到一个redis-5.0.4目录\n1 2 3 [root@localhost src]# tar -zxvf redis-5.0.4.tar.gz [root@localhost src]# ls redis-5.0.4 redis-5.0.4.tar.gz 编译 进入 redis 目录，在目录下执行 make 命令\n1 2 [root@localhost src]# cd redis-5.0.4 [root@localhost redis-5.0.4]# make 安装 执行安装命令，注意此处指定了安装目录为 /usr/local/redis\n1 [root@localhost redis-5.0.4]# make PREFIX=/usr/local/redis install 复制配置文件 将配置文件复制到 redis 的安装目录的bin目录下\n1 2 3 4 5 6 [root@localhost redis-5.0.4]# cd /usr/local/redis/bin/ [root@localhost bin]# ls redis-benchmark redis-check-aof redis-check-rdb redis-cli redis-sentinelredis-server [root@localhost bin]# cp /usr/local/src/redis-5.0.4/redis.conf ./ [root@localhost bin]# ls redis-benchmark redis-check-aof redis-check-rdb redis-cli redis.conf redis-sentinel redis-server 修改redis的配置文件 修改 redis 的配置文件，将注解绑定和保护模式关闭，方便从客户端连接测试\n1 [root@localhost bin]# vim redis.conf 启动redis服务 1 [root@localhost bin]# ./src/redis-server redis.conf \u0026amp; 2.2.2. 可执行相关文件 可执行文件 作用 redis-server 启动 redis redis-cli redis 命令行客户端 redis-benchmark 基准测试工具 redis-check-aof AOF 持久化文件检测和修复工具 redis-check-dump RDB 持久化文件检测和修复工具 redis-sentinel 启动哨兵 2.2.3. redis-server 服务端启动 默认配置：redis-server，日志输出版本信息，端口：6379\n启动方式1（不建议），--port 指定端口号 1 redis-server --port 6380 启动方式2，以配置文件方式启动 1 redis-server /opt/redis/redis.conf 启动方式3：修改 redis.conf 配置文件，增加 daemonize yes 配置以后端模式启动。启动时，指定配置文件。 1 ./redis-server redis.conf 2.2.4. redis-cli 客户端启动与停止 交互式启动。-h用于指定服务器ip，默认是127.0.0.1；-p用于指定服务的端口，默认是 6379；-a用于指定密码 1 redis-cli -h {host} -p {prot} -a {password} 命令式启动，直接连接并且操作 1 redis-cli -h 127.0.0.1 -p 6379 get hello 停止 redis 服务 1 2 3 4 # 使用客户端登陆 redis-cli -a 123456 # 关闭前生成持久化文件 shutdown nosave|save 使用以上命令断开连接，持久化文件生成，相对安全。还可以用 kill -9 pid 命令关闭，但此方式不会做持久化，还会造成缓冲区非法关闭，可能会造成 AOF 和丢失数据，所以不推荐。\n2.2.5. redis-cli 客户端监控命令 打开 redis-cli 客户端后，输入以下命令打开 redis 服务的数据监控\n1 monitor 3. Redis 主从模式 3.1. 概述 部署 Redis 主从结构，是为了 Redis 服务的高可用。Redis 的复制功能是支持多个数据库之间的数据同步，主从的所存取数据是一样的。在复制的概念中，数据库分为两类，一类是主数据库（master），另一类是从数据库（slave）。\n主数据库主要用于执行读写操作，当写操作导致数据变化时会自动将数据同步给从数据库。 从数据库一般只用于读操作，并接受主数据库同步过来的数据。 一个主数据库可以拥有多个从数据库，而一个从数据库只能拥有一个主数据库，从库还可以作为其他数据库的主库。\n3.2. 常用的主从结构 一主一从：用于主节点故障转移从节点，当主节点的“写”命令并发高且需要持久化，可以只在从节点开启AOF（主节点不需要） 一主多从：针对“读”较多的场景，“读”由多个从节点来分担，但节点越多，主节点同步到多节点的次数也越多，影响带宽，也加重主节点的稳定 树状主从：一主多从的缺点（主节点推送次数多压力大）可用些方案解决，主节点只推送一次数据到从节点1，再由从节点2推送到11，减轻主节点推送的压力 3.3. 主从数据复制的原理 Redis 提供了复制功能，可以实现在主数据库（Master）中的数据更新后，自动将更新的数据同步到从数据库（Slave）。一个主数据库可以拥有多个从数据库，而一个从数据库只能拥有一个主数据库。主从数据复制原理图如下：\n一个从数据库在启动后，会向主数据库发送 SYNC 命令。 主数据库在接收到 SYNC 命令后会开始在后台保存快照（即 RDB 持久化的过程），并将保存快照期间客户端（client）接收到的命令缓存起来。在该持久化过程中会生成一个.rdb快照文件。 在主数据库快照执行完成后，Redis 会将快照文件和客户端（client）所有缓存的命令以.rdb快照文件的形式发送给从数据库。 从数据库收到主数据库的.rdb快照文件后，载入该快照文件到本地磁盘。 从数据库执行载入后的.rdb快照文件，再将数据从本地磁盘加载到内存中。以上过程被称为复制初始化。 在复制初始化结束后，主数据库在每次收到写命令时都会将命令同步给从数据库，从而保证主从数据库的数据一致。 如果从节点跟主节点之间网络出现故障，连接断开了，会自动重连，连接之后主节点仅会将部分缺失的数据同步给从节点。 3.4. 主从复制配置 Redis 开启复制功能时，主数据库无须进行任何配置，而从数据库需要在配置文件中增加以下内容：\n1 2 3 4 # slaveof master_address master_port slaveof 127.0.0.1 9000 # 如果 master 有密码，则需要设置 masterauth masterauth=123456 在上述配置中，slaveof 后面的配置分别为主数据库的IP地址和端口，在主数据库开启了密码认证后需要将 masterauth 设置为主数据库的密码，在配置完成后重启 Redis，主数据库上的数据就会同步到从数据库上。\n4. Redis 哨兵模式 4.1. 概述 在主从架构中，当主数据库遇到异常中断服务后，开发者可以通过手动的方式选择一个从数据库来升格为主数据库，以使得系统能够继续提供服务。然而整个过程相对麻烦且需要人工介入，难以实现自动化。\nRedis 2.8 开始在主从模式上添加了一个哨兵工具，实现自动化监控集群的运行状态和故障恢复功能。哨兵是一个独立运行的进程，通过发送命令让 Redis 服务器返回其运行状态，监控 redis 主、从数据库是否正常运行。\n客户端连接 Redis 的时候，首先会连接哨兵，哨兵会返回客户端 Redis 主节点的地址，让客户端连接上 Redis 并进行后续的操作。在哨兵监测到 Master 库宕机时会自动推选出某个表现良好的 Slave 库切换成新的 Master 库，然后通过发布与订阅模式通知其他从服务器修改配置文件，完成主备热切。\n4.1.1. 高可用 高可用，它与被认为是不间断操作的容错技术有所不同。是目前企业防止核心系统因故障而无法工作的最有效保护手段。高可用一般指服务的冗余，一个服务挂了，可以自动切换到另外一个服务上，不影响客户体验\n4.1.2. 主从如何进行故障转移 当主节点(master)故障，从节点 slave-1 端执行 slaveof no one 后变成新主节点。其它的节点成为新主节点的从节点，并从新节点复制数据。\n主从模式下的故障转移需要人工干预，无法实现高可用。\n4.2. 搭建哨兵集群（待整理） TODO: 待整理\n4.3. 哨兵机制(sentinel)实现高可用原理 当主节点出现故障时，由 Redis Sentinel 会自动完成故障发现和转移，并通知应用方，实现高可用性。其工作原理如下：\n每个 Sentinel 以每秒钟一次的频率向它所知道的 Master，Slave 以及其他 Sentinel 实例发送一个 PING 命令。 如果一个实例距离最后一次有效回复 PING 命令的时间超过指定值，则该实例会被 Sentine 标记为主观下线。 如果一个 Master 被标记为主观下线，则正在监视这个 Master 的所有 Sentinel 要以每秒一次的频率确认 Master 是否真正进入主观下线状态。 当有足够数量的 Sentinel（大于等于配置文件指定值）在指定的时间范围内确认 Master 的确进入了主观下线状态，则 Master 会被标记为客观下线。若没有足够数量的 Sentinel 同意 Master 已经下线，Master 的客观下线状态就会被解除。若 Master 重新向 Sentinel 的 PING 命令返回有效回复，Master 的主观下线状态就会被移除。 哨兵节点会选举出哨兵 leader，负责故障转移的工作。 哨兵 leader 会推选出某个表现良好的从节点成为新的主节点，然后通知其他从节点更新主节点信息。 5. Redis cluster 哨兵模式解决了主从复制不能自动故障转移、达不到高可用的问题，但还是存在无法扩展主节点的写能力、存储容量受限于 master 节点能够承载的上限的问题。并且使用哨兵，redis 每个实例存储的内容都是全量、完整的数据，这样会造成内存的浪费。为了最大化利用内存与扩展写能力，可以采用 Redis cluster（集群）模式。\n5.1. 概述 Redis cluster（集群）模式，实现在多个 Redis 节点之间的数据分片（分布式存储）和数据复制。『数据分片』即每台 redis 分别存储不同的内容，共有16384个 slot。每个 redis 分得一些 slot，通过算法hash_slot = crc16(key) mod 16384 找到对应 slot，从而知道数据是存在那个 redis 节点中。\n基于 Redis 集群的数据自动分片能力，能够方便地对 Redis 集群进行横向扩展，以提高 Redis 集群的吞吐量。 基于 Redis 集群的数据复制能力，在集群中的一部分节点失效或者无法进行通信时，Redis 仍然可以基于副本数据对外提供服务，这提高了集群的可用性。 Tips: Redis 集群目前无法做数据库选择，默认在 0 数据库。\n5.2. 搭建分片集群（待整理） TODO: 待整理\n5.3. Redis cluster 实现原理 Redis cluster 采用虚拟槽分区，所有的键根据哈希函数映射到0～16383个整数槽内，每个节点负责维护一部分槽以及槽所映射的键值数据。\n哈希槽是以过以下步骤，映射到 Redis 实例上：\n对键值对的 key 使用 crc16 算法计算一个结果 将结果对 16384 取余，得到的值表示 key 对应的哈希槽 根据该槽信息定位到对应的实例 5.3.1. 哈希分区各类算法 节点取余分区。使用特定的数据，如 Redis 的键或用户 ID，对节点数量 N 取余：hash(key)%N 计算出哈希值，用来决定数据映射到哪一个节点上。优点是简单性。扩容时通常采用翻倍扩容，避免数据映射全部被打乱导致全量迁移的情况。 一致性哈希分区。为系统中每个节点分配一个 token，范围一般在0~232，这些 token 构成一个哈希环。数据读写执行节点查找操作时，先根据 key 计算 hash 值，然后顺时针找到第一个大于等于该哈希值的 token 节点。这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点，对其他节点无影响。 虚拟槽分区，所有的键根据哈希函数映射到 0~16383 整数槽内，计算公式：slot=CRC16(key)\u0026amp;16383。每一个节点负责维护一部分槽以及槽所映射的键值数据。Redis Cluser 采用虚拟槽分区算法。 5.4. Redis cluster 优劣分析 优点：\n无中心架构，支持动态扩容。 数据按照 slot 存储分布在多个节点，节点间数据共享，可动态调整数据分布。 高可用性。部分节点不可用时，集群仍可用。集群模式能够实现自动故障转移（failover），节点之间通过 gossip 协议交换状态信息，用投票机制完成 Slave 到 Master 的角色转换。 缺点：\n不支持批量操作（pipeline）。 数据通过异步复制，不保证数据的强一致性。 事务操作支持有限，只支持多 key 在同一节点上的事务操作，当多个 key 分布于不同的节点上时无法使用事务功能。 key 作为数据分区的最小粒度，不能将一个很大的键值对象如 hash、list 等映射到不同的节点。 不支持多数据库空间，单机下的 Redis 可以支持到 16 个数据库，集群模式下只能使用 1 个数据库空间。 6. Redis 集群总结 6.1. Redis 的三种主流集群方案总结 Redis 有三种集群模式：主从模式、哨兵模式和集群模式。\nRedis集群遵循如下原则：\n所有 Redis 节点彼此都通过 PING-PONG 机制互联，内部使用二进制协议优化传输速度和带宽。 在集群中超过半数的节点检测到某个节点 Fail 后将该节点设置为 Fail 状态。 客户端与 Redis 节点直连，客户端连接集群中任何一个可用节点即可对集群进行操作。 Redis-Cluster 把所有的物理节点都映射到 0～16383 的 slot（槽）上，Cluster 负责维护每个节点上数据槽的分配。Redis 的具体数据分配策略为：在 Redis 集群中内置了16384个散列槽；在需要在Redis集群中放置一个Key-Value时，Redis 会先对 Key 使用 CRC16 算法算出一个结果，然后把结果对 16384 求余数，这样每个Key都会对应一个编号为0～16383的散列槽；Redis 会根据节点的数量大致均等地将散列槽映射到不同的节点。 6.2. 扩展：Redis 其他集群方案 Twemproxy：它类似于一个代理方式，使用方法和普通 redis 无任何区别，设置好它下属的多个 redis 实例后，使用时在本需要连接 redis 的地方改为连接 twemproxy，它会以一个代理的身份接收请求并使用一致性 hash 算法，将请求转接到具体 redis，将结果再返回 twemproxy。使用方式简便(相对 redis 只需修改连接端口)，对旧项目扩展的首选。但存在也有问题：twemproxy 自身单端口实例的压力，使用一致性 hash 后，对 redis 节点数量改变时候的计算值的改变，数据无法自动移动到新的节点。 codis：目前用的最多的集群方案，基本和 twemproxy 一致的效果，但它支持在节点数量改变情况下，旧节点数据可恢复到新 hash 节点。 在业务代码层实现，对于几个毫无关联的 redis 实例，在代码层对 key 进行 hash 计算，然后去对应的 redis 实例操作数据。这种方式对 hash 层代码要求比较高，考虑部分包括，节点失效后的替代算法方案，数据震荡后的自动脚本恢复，实例的监控等等。 6.2.1. Twemproxy 概述 Twemproxy 是 Twitter 维护的（缓存）代理系统，代理 Memcached 的 ASCII 协议和 Redis 协议。它是单线程程序，使用 c 语言编写，运行起来非常快。它是采用 Apache 2.0 license 的开源软件。\nTwemproxy 支持自动分区，如果其代理的其中一个 Redis 节点不可用时，会自动将该节点排除（这将改变原来的 keys-instances 的映射关系，所以应该仅在把 Redis 当缓存时使用 Twemproxy）。\nTwemproxy 本身不存在单点问题，因为可以启动多个 Twemproxy 实例，然后让客户端去连接任意一个 Twemproxy 实例。Twemproxy 是 Redis 客户端和服务器端的一个中间层，由它来处理分区功能应该不算复杂，并且应该算比较可靠的。\n7. Redis 图形化客户端 7.1. iredis 命令行工具 iredis，用 | 将 redis 通过 pipe 用 shell 的其他工具，比如 jq/fx/rg/sort/uniq/cut/sed/awk 等处理可以实现json格式化。还能自动补全，高亮显示，功能很多。\n官网：https://iredis.io/\n7.2. Redis Desktop Manager Redis Desktop Manager，界面比较简洁，功能很全。key 的显示可以支持按冒号分割的键名空间，除了基本的五大数据类型之外，还支持 redis 5.0 新出的 Stream 数据类型。在 value 的显示方面。支持多达9种的数据显示方式。\n最新官网（需要登陆）：https://resp.app/\n旧官网：https://redisdesktop.com/download\n下载 redis-desktop-manager-0.9.2.806.exe，安装后启动redis客户端。【据说0.9.3（最后一个免费版本），待测试】\n配置redis链接：选择连接到Redis服务器，配置主机地址与端口号\n7.3. medis http://getmedis.com/\n免费 redis 可视化工具，界面布局简洁，跨平台支持。对 key 有颜色鲜明的图标标识。在 key 的搜索上挺方便的，可以模糊搜索出匹配的 key，渐进式的 scan，无明显卡顿。在搜索的体验上还是比较出色的。\n缺点是不支持 key 的命名空间展示，不支持 redis 5.0 的 stream 数据类型，命令行比较单一，不支持自动匹配和提示。支持的 value 的展现方式也只有3种\n7.4. Another Redis Desktop Manager https://github.com/qishibo/AnotherRedisDesktopManager\n一款比较稳定简洁免费的 redis UI 可视化工具，基本的功能都有。有监控统计，支持暗黑主题，还支持集群的添加。\n缺点是没什么亮点，UI 很简单，不支持 stream 数据类型。命令行模式也比较单一。value 展示支持的类型也只有3种。\n","permalink":"https://ktzxy.top/posts/sfmj09zg3t/","summary":"Redis 安装部署","title":"Redis 安装部署"},{"content":"MyBatis-Plus [TOC]\n课程了解 了解 MyBatis-Plus 整合 MyBatis-Plus 通用 CRUD MyBatis-Plus 的配置 条件构造器 ActiveRecord Oracle 主键 Sequence Mybatis-Plus的插件 自动填充功能 逻辑删除 通用枚举 代码生成器 MybatisX 快速开发插件 了解 MyBatis-Plus Mybatis-Plus介绍 MyBatis-Plus（简称 MP）是一个 MyBatis 的增强工具，在 MyBatis 的基础上只做增强不做改变，为简化开发、提高 效率而生。 官网：https://mybatis.plus/ 或 https://mp.baomidou.com/\n愿景 我们的愿景是成为 MyBatis 最好的搭档，就像 魂斗罗 中的 1P、2P，基友搭配，效率翻倍。\n代码以及文档 文档地址：https://mybatis.plus/guide/\n源码地址：https://github.com/baomidou/mybatis-plus\n特性 无侵入：只做增强不做改变，引入它不会对现有工程产生影响，如丝般顺滑 损耗小：启动即会自动注入基本 CURD，性能基本无损耗，直接面向对象操作 强大的 CRUD 操作：内置通用 Mapper、通用 Service，仅仅通过少量配置即可实现单表大部分 CRUD 操作， 更有强大的条件构造器，满足各类使用需求 支持 Lambda 形式调用：通过 Lambda 表达式，方便的编写各类查询条件，无需再担心字段写错 支持多种数据库：支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、 SQLServer2005、SQLServer 等多种数据库 支持主键自动生成：支持多达 4 种主键策略（内含分布式唯一 ID 生成器 - Sequence），可自由配置，完美解 决主键问题 支持 XML 热加载：Mapper 对应的 XML 支持热加载，对于简单的 CRUD 操作，甚至可以无 XML 启动 支持 ActiveRecord 模式：支持 ActiveRecord 形式调用，实体类只需继承 Model 类即可进行强大的 CRUD 操 作 支持自定义全局通用操作：支持全局通用方法注入（ Write once, use anywhere ） 支持关键词自动转义：支持数据库关键词（order、key\u0026hellip;\u0026hellip;）自动转义，还可自定义关键词 内置代码生成器：采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码， 支持模板引擎，更有超多自定义配置等您来使用 内置分页插件：基于 MyBatis 物理分页，开发者无需关心具体操作，配置好插件之后，写分页等同于普通 List 查询 内置性能分析插件：可输出 Sql 语句以及其执行时间，建议开发测试时启用该功能，能快速揪出慢查询 内置全局拦截插件：提供全表 delete 、 update 操作智能分析阻断，也可自定义拦截规则，预防误操作 内置 Sql 注入剥离器：支持 Sql 注入剥离，有效预防 Sql 注入攻击 作者 Mybatis-Plus是由baomidou（苞米豆）组织开发并且开源的，目前该组织大概有30人左右。\n码云地址：https://gitee.com/organizations/baomidou\n快速开始 对于Mybatis整合MP有常常有三种用法，分别是Mybatis+MP、Spring+Mybatis+MP、Spring Boot+Mybatis+MP。\n创建数据库以及表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 -- 创建库 create database mp; -- 创建测试表 use mp; CREATE TABLE `tb_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;主键ID\u0026#39;, `user_name` varchar(20) NOT NULL COMMENT \u0026#39;用户名\u0026#39;, `password` varchar(20) NOT NULL COMMENT \u0026#39;密码\u0026#39;, `name` varchar(30) DEFAULT NULL COMMENT \u0026#39;姓名\u0026#39;, `age` int(11) DEFAULT NULL COMMENT \u0026#39;年龄\u0026#39;, `email` varchar(50) DEFAULT NULL COMMENT \u0026#39;邮箱\u0026#39;, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8; -- 插入测试数据 INSERT INTO `tb_user` (`id`, `user_name`, `password`, `name`, `age`, `email`) VALUES (\u0026#39;1\u0026#39;, \u0026#39;zhangsan\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;张三\u0026#39;, \u0026#39;18\u0026#39;, \u0026#39;test1@itcast.cn\u0026#39;); INSERT INTO `tb_user` (`id`, `user_name`, `password`, `name`, `age`, `email`) VALUES (\u0026#39;2\u0026#39;, \u0026#39;lisi\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;李四\u0026#39;, \u0026#39;20\u0026#39;, \u0026#39;test2@itcast.cn\u0026#39;); INSERT INTO `tb_user` (`id`, `user_name`, `password`, `name`, `age`, `email`) VALUES (\u0026#39;3\u0026#39;, \u0026#39;wangwu\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;王五\u0026#39;, \u0026#39;28\u0026#39;, \u0026#39;test3@itcast.cn\u0026#39;); INSERT INTO `tb_user` (`id`, `user_name`, `password`, `name`, `age`, `email`) VALUES (\u0026#39;4\u0026#39;, \u0026#39;zhaoliu\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;赵六\u0026#39;, \u0026#39;21\u0026#39;, \u0026#39;test4@itcast.cn\u0026#39;); INSERT INTO `tb_user` (`id`, `user_name`, `password`, `name`, `age`, `email`) VALUES (\u0026#39;5\u0026#39;, \u0026#39;sunqi\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;孙七\u0026#39;, \u0026#39;24\u0026#39;, \u0026#39;test5@itcast.cn\u0026#39;); 导入Maven依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 \u0026lt;dependencies\u0026gt; \u0026lt;!-- mybatis-plus插件依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.baomidou\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-plus\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.4.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- MySql --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.1.47\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 连接池 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;druid\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.11\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--简化bean代码的工具包--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;optional\u0026gt;true\u0026lt;/optional\u0026gt; \u0026lt;version\u0026gt;1.18.4\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.12\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-log4j12\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.6.4\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; Mybatis + MP 下面演示，通过纯Mybatis与Mybatis-Plus整合。\n创建子Module log4j.properties：\n1 2 3 4 5 log4j.rootLogger=DEBUG,A1 log4j.appender.A1=org.apache.log4j.ConsoleAppender log4j.appender.A1.layout=org.apache.log4j.PatternLayout log4j.appender.A1.layout.ConversionPattern=[%t] [%c]-[%p] %m%n Mybatis实现查询User 第一步，编写mybatis-config.xml文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE configuration PUBLIC \u0026#34;-//mybatis.org//DTD Config 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-config.dtd\u0026#34;\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;environments default=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;environment id=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;transactionManager type=\u0026#34;JDBC\u0026#34;/\u0026gt; \u0026lt;dataSource type=\u0026#34;POOLED\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driver\u0026#34; value=\u0026#34;com.mysql.jdbc.Driver\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true\u0026amp;amp;characterEncoding=utf8\u0026amp;amp;autoReconnect=true\u0026amp;amp;allowMultiQueries=true\u0026amp;amp;useSSL=false\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;root\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;1234\u0026#34;/\u0026gt; \u0026lt;/dataSource\u0026gt; \u0026lt;/environment\u0026gt; \u0026lt;/environments\u0026gt; \u0026lt;mappers\u0026gt; \u0026lt;mapper resource=\u0026#34;UserMapper.xml\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; \u0026lt;/configuration\u0026gt; 第二步，编写User实体对象：（这里使用lombok进行了简化bean操作）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package org.hong.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class User { private Long id; private String userName; private String password; private String name; private Integer age; private String email; } 第三步，编写UserMapper接口：\n1 2 3 4 5 6 7 8 9 package org.hong.mapper; import org.hong.pojo.User; import java.util.List; public interface UserMapper { List\u0026lt;User\u0026gt; findAll(); } 第四步，编写UserMapper.xml文件：\n1 2 3 4 5 6 7 8 9 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;cn.itcast.mp.simple.mapper.UserMapper\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;findAll\u0026#34; resultType=\u0026#34;org.hong.pojo.User\u0026#34;\u0026gt; select * from tb_user \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; 第五步，编写TestMybatis测试用例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package org.hong; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.hong.mapper.UserMapper; import org.hong.pojo.User; import org.junit.Test; import java.io.IOException; import java.io.InputStream; import java.util.List; public class TestMyBatis { public SqlSessionFactory getSqlSessionFactory() { try { String config = \u0026#34;mybatis-config.xml\u0026#34;; InputStream resourceAsStream = Resources.getResourceAsStream(config); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); return sqlSessionFactory; } catch (IOException e) { e.printStackTrace(); return null; } } @Test public void testFindAll() { SqlSession sqlSession = getSqlSessionFactory().openSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); List\u0026lt;User\u0026gt; users = mapper.findAll(); for (User user : users) { System.out.println(user); } } } 测试结果：\n1 2 3 4 5 6 7 [main] [org.hong.mapper.UserMapper.findAll]-[DEBUG] ==\u0026gt; Parameters: [main] [org.hong.mapper.UserMapper.findAll]-[DEBUG] \u0026lt;== Total: 5 User(id=1, userName=null, password=123456, name=张三, age=18, email=test1@itcast.cn) User(id=2, userName=null, password=123456, name=李四, age=20, email=test2@itcast.cn) User(id=3, userName=null, password=123456, name=王五, age=28, email=test3@itcast.cn) User(id=4, userName=null, password=123456, name=赵六, age=21, email=test4@itcast.cn) User(id=5, userName=null, password=123456, name=孙七, age=24, email=test5@itcast.cn) Mybatis+MP实现查询User 第一步，将UserMapper继承BaseMapper，将拥有了BaseMapper中的所有方法：\n1 2 3 4 5 6 7 8 9 10 package org.hong.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.hong.pojo.User; import java.util.List; public interface UserMapper extends BaseMapper\u0026lt;User\u0026gt; { List\u0026lt;User\u0026gt; findAll(); } 第二步，使用MP中的MybatisSqlSessionFactoryBuilder进程构建：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package org.hong; import com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.hong.mapper.UserMapper; import org.hong.pojo.User; import org.junit.Test; import java.io.IOException; import java.io.InputStream; import java.util.List; public class TestMyBatis { public SqlSessionFactory getSqlSessionFactory() { try { String config = \u0026#34;mybatis-config.xml\u0026#34;; InputStream resourceAsStream = Resources.getResourceAsStream(config); // 这里使用的是MP中的MybatisSqlSessionFactoryBuilder SqlSessionFactory sqlSessionFactory = new MybatisSqlSessionFactoryBuilder().build(resourceAsStream); return sqlSessionFactory; } catch (IOException e) { e.printStackTrace(); return null; } } @Test public void testFindAll() { SqlSession sqlSession = getSqlSessionFactory().openSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); // 可以调用BaseMapper中定义的方法 List\u0026lt;User\u0026gt; users = mapper.selectList(null); for (User user : users) { System.out.println(user); } } } 运行报错：\n解决：在User类中添加@TableName，指定数据库表名\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package org.hong.pojo; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor @TableName(\u0026#34;tb_user\u0026#34;) public class User { private Long id; private String userName; private String password; private String name; private Integer age; private String email; } 再次运行：\n1 2 3 4 5 6 7 8 9 [main] [cn.itcast.mp.simple.mapper.UserMapper.selectList]-[DEBUG] ==\u0026gt; Preparing: SELECT id,user_name,password,name,age,email FROM tb_user [main] [cn.itcast.mp.simple.mapper.UserMapper.selectList]-[DEBUG] ==\u0026gt; Parameters: [main] [cn.itcast.mp.simple.mapper.UserMapper.selectList]-[DEBUG] \u0026lt;== Total: 5 User(id=1, userName=zhangsan, password=123456, name=张三, age=18, email=test1@itcast.cn) User(id=2, userName=lisi, password=123456, name=李四, age=20, email=test2@itcast.cn) User(id=3, userName=wangwu, password=123456, name=王五, age=28, email=test3@itcast.cn) User(id=4, userName=zhaoliu, password=123456, name=赵六, age=21, email=test4@itcast.cn) User(id=5, userName=sunqi, password=123456, name=孙七, age=24, email=test5@itcast.cn) 简单说明：\n由于使用了MybatisSqlSessionFactoryBuilder进行了构建，继承的BaseMapper中的方法就载入到了 SqlSession中，所以就可以直接使用相关的方法；\nSpring + Mybatis + MP 引入了Spring框架，数据源、构建等工作就交给了Spring管理。\n创建子Module 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;properties\u0026gt; \u0026lt;spring.version\u0026gt;5.1.6.RELEASE\u0026lt;/spring.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-webmvc\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-jdbc\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-test\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 实现查询Use 第一步，编写jdbc.properties\n1 2 3 4 5 jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/mp? useUnicode=true\u0026amp;characterEncoding=utf8\u0026amp;autoReconnect=true\u0026amp;allowMultiQueries=true\u0026amp;useSSL=false jdbc.username=root jdbc.password=1234 第二步，编写applicationContext.xml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:context=\u0026#34;http://www.springframework.org/schema/context\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd\u0026#34;\u0026gt; \u0026lt;context:property-placeholder location=\u0026#34;classpath:*.properties\u0026#34;/\u0026gt; \u0026lt;!-- 定义数据源 --\u0026gt; \u0026lt;bean id=\u0026#34;dataSource\u0026#34; class=\u0026#34;com.alibaba.druid.pool.DruidDataSource\u0026#34; destroy-method=\u0026#34;close\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;${jdbc.url}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;${jdbc.username}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;${jdbc.password}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;driverClassName\u0026#34; value=\u0026#34;${jdbc.driver}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;maxActive\u0026#34; value=\u0026#34;10\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;minIdle\u0026#34; value=\u0026#34;5\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!--这里使用MP提供的sqlSessionFactory，完成了Spring与MP的整合--\u0026gt; \u0026lt;bean id=\u0026#34;sqlSessionFactory\u0026#34; class=\u0026#34;com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!--扫描mapper接口，使用的依然是Mybatis原生的扫描器--\u0026gt; \u0026lt;bean class=\u0026#34;org.mybatis.spring.mapper.MapperScannerConfigurer\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;basePackage\u0026#34; value=\u0026#34;org.hong.mapper\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/beans\u0026gt; 第三步，编写User对象以及UserMapper接口：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package org.hong.pojo; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor @TableName(\u0026#34;tb_user\u0026#34;) public class User { private Long id; private String userName; private String password; private String name; private Integer age; private String email; } 1 2 3 4 5 6 7 8 package org.hong.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.hong.pojo.User; public interface UserMapper extends BaseMapper\u0026lt;User\u0026gt; { } 第四步，编写测试用例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package org.hong; import org.hong.mapper.UserMapper; import org.hong.pojo.User; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.List; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {\u0026#34;classpath:applicationContext.xml\u0026#34;}) public class TestMyBatisSpring { @Autowired private UserMapper userMapper; @Test public void testSelectList(){ List\u0026lt;User\u0026gt; users = this.userMapper.selectList(null); for (User user : users) { System.out.println(user); } } } SpringBoot + Mybatis + MP 使用SpringBoot将进一步的简化MP的整合，需要注意的是，由于使用SpringBoot需要继承parent，所以需要重新创建工程，并不是创建子Module。\n创建工程 导入依赖 pom.xml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.1.4.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;groupId\u0026gt;org.hong\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;MyBatis-Plus-SpringBoot\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-logging\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--简化代码的工具包--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;optional\u0026gt;true\u0026lt;/optional\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--mybatis-plus的springboot支持--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.baomidou\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-plus-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.4.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--mysql驱动--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.1.47\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-log4j12\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/project\u0026gt; log4j.properties 1 2 3 4 log4j.rootLogger=DEBUG,A1 log4j.appender.A1=org.apache.log4j.ConsoleAppender log4j.appender.A1.layout=org.apache.log4j.PatternLayout log4j.appender.A1.layout.ConversionPattern=[%t] [%c]-[%p] %m%n 编写application.yaml 1 2 3 4 5 6 spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true\u0026amp;characterEncoding=utf8\u0026amp;autoReconnect=true\u0026amp;allowMultiQueries=true\u0026amp;useSSL=false username: root password: 1234 编写pojo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package org.hong.pojo; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor @TableName(\u0026#34;tb_user\u0026#34;) public class User { private Long id; private String userName; private String password; private String name; private Integer age; private String email; } 编写 Mapper 接口 1 2 3 4 5 6 package org.hong.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.hong.pojo.User; public interface UserMapper extends BaseMapper\u0026lt;User\u0026gt; {} 编写启动类 1 2 3 4 5 6 7 8 9 10 11 12 13 package org.hong; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @MapperScan({\u0026#34;org.hong\u0026#34;})// 设置mapper接口的扫描包 @SpringBootApplication public class MybatisPlusSpringbootApplication { public static void main(String[] args) { SpringApplication.run(MybatisPlusSpringbootApplication.class, args); } } 编写测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package org.hong; import org.hong.mapper.UserMapper; import org.hong.pojo.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest class MybatisPlusSpringbootApplicationTests { @Autowired private UserMapper userMapper; @Test void contextLoads() { List\u0026lt;User\u0026gt; users = userMapper.selectList(null); for (User user : users) { System.out.println(user); } } } 运行结果 1 2 3 4 5 6 7 8 9 [main] [org.hong.mapper.UserMapper.selectList]-[DEBUG] ==\u0026gt; Preparing: SELECT id,user_name,password,name,age,email FROM tb_user [main] [org.hong.mapper.UserMapper.selectList]-[DEBUG] ==\u0026gt; Parameters: [main] [org.hong.mapper.UserMapper.selectList]-[DEBUG] \u0026lt;== Total: 5 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3ee69ad8] User(id=1, userName=zhangsan, password=123456, name=张三, age=18, email=test1@itcast.cn) User(id=2, userName=lisi, password=123456, name=李四, age=20, email=test2@itcast.cn) User(id=3, userName=wangwu, password=123456, name=王五, age=28, email=test3@itcast.cn) User(id=4, userName=zhaoliu, password=123456, name=赵六, age=21, email=test4@itcast.cn) User(id=5, userName=sunqi, password=123456, name=孙七, age=24, email=test5@itcast.cn) 通用 CRUD 通过前面的学习，我们了解到通过继承BaseMapper就可以获取到各种各样的单表操作，接下来我们将详细讲解这些 操作。\n插入操作 方法定义 1 2 3 4 5 6 /** * 插入一条记录 * * @param entity 实体对象 */ int insert(T entity); 测试用例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package org.hong; import org.hong.mapper.UserMapper; import org.hong.pojo.User; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @SpringBootTest @RunWith(SpringRunner.class) public class TestUserMapper { @Autowired private UserMapper userMapper; @Test public void testInsert(){ User user = new User(); user.setEmail(\u0026#34;baomidou@qq.com\u0026#34;); user.setAge(8); user.setUserName(\u0026#34;mybatis-plus\u0026#34;); user.setName(\u0026#34;baomidou\u0026#34;); user.setPassword(\u0026#34;123456\u0026#34;); // 返回数据库受影响的行数 int result = this.userMapper.insert(user); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); // 获取自增后的id值, 自增后的id值会回填到user对象中 System.out.println(\u0026#34;id =\u0026gt; \u0026#34; + user.getId()); } } 控制台输出 1 2 3 4 5 6 [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] ==\u0026gt; Preparing: INSERT INTO tb_user ( id, user_name, password, name, age, email ) VALUES ( ?, ?, ?, ?, ?, ? ) [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] ==\u0026gt; Parameters: 1370007317083680769(Long), mybatis-plus(String), 123456(String), baomidou(String), 8(Integer), baomidou@qq.com(String) [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] \u0026lt;== Updates: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@43a51d00] result =\u0026gt; 1 id =\u0026gt; 1370007317083680769 可以看到，数据已经写入到了数据库，但是，id的值不正确，我们期望的是数据库自增长，实际是MP生成了id的值写入到了数据库\n@TableId 如何设置id的生成策略呢？ MP支持的id策略：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.baomidou.mybatisplus.annotation; import lombok.Getter; /** * 生成ID类型枚举类 * * @author hubin * @since 2015-11-10 */ @Getter public enum IdType { /** * 数据库ID自增 */ AUTO(0), /** * 该类型为未设置主键类型 */ NONE(1), /** * 用户输入ID * \u0026lt;p\u0026gt;该类型可以通过自己注册自动填充插件进行填充\u0026lt;/p\u0026gt; */ INPUT(2), /* 以下3种类型、只有当插入对象ID 为空，才自动填充。 */ /** * 全局唯一ID (idWorker) */ ID_WORKER(3), /** * 全局唯一ID (UUID) */ UUID(4), /** * 字符串全局唯一ID (idWorker 的字符串表示) */ ID_WORKER_STR(5); private final int key; IdType(int key) { this.key = key; } } 修改User类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package org.hong.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor @TableName(\u0026#34;tb_user\u0026#34;) public class User { @TableId(type = IdType.AUTO) private Long id; private String userName; private String password; private String name; private Integer age; private String email; } 再次运行，数据插入成功。\n@TableField 在MP中通过@TableField注解可以指定字段的一些属性，常常解决的问题有2个：\n1、对象中的属性名和字段名不一致的问题（非驼峰）\n2、对象中的属性字段在表中不存在的问题\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package org.hong.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor @TableName(\u0026#34;tb_user\u0026#34;) public class User { @TableId(type = IdType.AUTO) private Long id; private String userName; @TableField(select = false) // select: 设置是否在查询带上该字段; 如果为false, 则在查询时就不会带上该字段, 更不会封装这个字段 private String password; private String name; private Integer age; @TableField(value = \u0026#34;email\u0026#34;) // value: 指定数据库表中的字段名; 查询时会使用 (email as mail) 的方式 private String mail; @TableField(exist = false) // exist: 设置属性是否在数据库中存在 private String address; // 这个属性在数据库表中是不存在的 } 更新操作 在MP中，更新操作有2种，一种是根据id更新，另一种是根据条件更新。\n根据id更新 方法定义 1 2 3 4 5 6 /** * 根据 ID 修改 * * @param entity 实体对象 */ int updateById(@Param(Constants.ENTITY) T entity); 测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package org.hong; import org.hong.mapper.UserMapper; import org.hong.pojo.User; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @SpringBootTest @RunWith(SpringRunner.class) public class TestUserMapper { @Autowired private UserMapper userMapper; @Test public void testUpdate(){ User user = new User(); user.setId(1L); user.setAge(18); int result = userMapper.updateById(user); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); } } 控制台输出 1 2 3 4 5 [main] [org.hong.mapper.UserMapper.updateById]-[DEBUG] ==\u0026gt; Preparing: UPDATE tb_user SET age=? WHERE id=? [main] [org.hong.mapper.UserMapper.updateById]-[DEBUG] ==\u0026gt; Parameters: 18(Integer), 1(Long) [main] [org.hong.mapper.UserMapper.updateById]-[DEBUG] \u0026lt;== Updates: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@363a3d15] result =\u0026gt; 1 根据条件更新 方法定义 1 2 3 4 5 6 7 /** * 根据 whereEntity 条件，更新记录 * * @param entity 实体对象 (set 条件值, 默认为null的字段不会加入到set中) * @param updateWrapper 实体对象封装操作类(可以为 null, 用于生成 where 语句) */ int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper\u0026lt;T\u0026gt; updateWrapper); 测试用例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @SpringBootTest @RunWith(SpringRunner.class) public class TestUserMapper { @Test public void testUpdate(){ User user = new User(); user.setAge(25);// 更新的字段 user.setPassword(\u0026#34;666888\u0026#34;); QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;\u0026gt;(); // 更新条件: 匹配 user_name = zhangsan 的用户 wrapper.eq(\u0026#34;user_name\u0026#34;, \u0026#34;zhangsan\u0026#34;); // 根据条件跟新 int result = userMapper.update(user, wrapper); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); } } 测试结果\n1 2 3 4 5 [main] [org.hong.mapper.UserMapper.update]-[DEBUG] ==\u0026gt; Preparing: UPDATE tb_user SET password=?, age=? WHERE user_name = ? [main] [org.hong.mapper.UserMapper.update]-[DEBUG] ==\u0026gt; Parameters: 666888(String), 25(Integer), zhangsan(String) [main] [org.hong.mapper.UserMapper.update]-[DEBUG] \u0026lt;== Updates: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fad6218] result =\u0026gt; 1 或者，通过UpdateWrapper进行更新\n1 2 3 4 5 6 7 8 9 10 @Test public void testUpdate2(){ UpdateWrapper\u0026lt;User\u0026gt; wrapper = new UpdateWrapper\u0026lt;\u0026gt;(); wrapper.set(\u0026#34;age\u0026#34;, 21).set(\u0026#34;password\u0026#34;, \u0026#34;999999\u0026#34;) // 更新的字段 .eq(\u0026#34;user_name\u0026#34;, \u0026#34;zhangsan\u0026#34;); // 更新条件 // 根据条件跟新 int result = userMapper.update(null, wrapper); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); } 测试结果\n1 2 3 4 5 [main] [org.hong.mapper.UserMapper.update]-[DEBUG] ==\u0026gt; Preparing: UPDATE tb_user SET age=?,password=? WHERE user_name = ? [main] [org.hong.mapper.UserMapper.update]-[DEBUG] ==\u0026gt; Parameters: 21(Integer), 999999(String), zhangsan(String) [main] [org.hong.mapper.UserMapper.update]-[DEBUG] \u0026lt;== Updates: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6ea04618] result =\u0026gt; 1 均可达到更新的效果。 关于wrapper更多的用法后面会详细讲解。\n删除操作 deleteById 方法定义 1 2 3 4 5 6 /** * 根据 ID 删除 * * @param id 主键ID */ int deleteById(Serializable id); 测试用例 1 2 3 4 5 6 @Test public void testDeleteById(){ // 根据id删除数据 int result = userMapper.deleteById(9); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); } 控制台打印 1 2 3 4 5 [main] [org.hong.mapper.UserMapper.deleteById]-[DEBUG] ==\u0026gt; Preparing: DELETE FROM tb_user WHERE id=? [main] [org.hong.mapper.UserMapper.deleteById]-[DEBUG] ==\u0026gt; Parameters: 9(Integer) [main] [org.hong.mapper.UserMapper.deleteById]-[DEBUG] \u0026lt;== Updates: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1640c151] result =\u0026gt; 1 deleteByMap 方法定义 1 2 3 4 5 6 /** * 根据 columnMap 条件，删除记录，条件之间是and的关系 * * @param columnMap 表字段 map 对象 */ int deleteByMap(@Param(Constants.COLUMN_MAP) Map\u0026lt;String, Object\u0026gt; columnMap); 测试用例 1 2 3 4 5 6 7 8 9 10 @Test public void testDeleteMap(){ HashMap\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;user_name\u0026#34;, \u0026#34;zhangsan\u0026#34;); map.put(\u0026#34;password\u0026#34;, \u0026#34;123456\u0026#34;); // 根据map删除数据, 多条件之间是and的关系 int result = userMapper.deleteByMap(map); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); } 控制台打印 1 2 3 4 5 [main] [org.hong.mapper.UserMapper.deleteByMap]-[DEBUG] ==\u0026gt; Preparing: DELETE FROM tb_user WHERE password = ? AND user_name = ? [main] [org.hong.mapper.UserMapper.deleteByMap]-[DEBUG] ==\u0026gt; Parameters: 123456(String), zhangsan(String) [main] [org.hong.mapper.UserMapper.deleteByMap]-[DEBUG] \u0026lt;== Updates: 0 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5c20aab9] result =\u0026gt; 0 delete 方法定义 1 2 3 4 5 6 /** * 根据 entity 条件，删除记录 * * @param wrapper 实体对象封装操作类（可以为 null） */ int delete(@Param(Constants.WRAPPER) Wrapper\u0026lt;T\u0026gt; wrapper); 测试用例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void testDelete(){ // 用法一: // QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;\u0026gt;(); // wrapper.eq(\u0026#34;user_name\u0026#34;, \u0026#34;mybatis-plus\u0026#34;) // .eq(\u0026#34;password\u0026#34;, \u0026#34;123456\u0026#34;); // 用法二: User user = new User(); user.setUserName(\u0026#34;mybatis-plus\u0026#34;); user.setPassword(\u0026#34;123456\u0026#34;); // 将实体对象进行包装, 包装为操作条件 QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;User\u0026gt;(user); // 根据包装条件做删除 int result = userMapper.delete(wrapper); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); } 控制台打印 1 2 3 4 5 [main] [org.hong.mapper.UserMapper.delete]-[DEBUG] ==\u0026gt; Preparing: DELETE FROM tb_user WHERE user_name = ? AND password = ? [main] [org.hong.mapper.UserMapper.delete]-[DEBUG] ==\u0026gt; Parameters: mybatis-plus(String), 123456(String) [main] [org.hong.mapper.UserMapper.delete]-[DEBUG] \u0026lt;== Updates: 2 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6dd82486] result =\u0026gt; 2 deleteBatchIds 方法定义 1 2 3 4 5 6 /** * 删除（根据ID 批量删除） * * @param idList 主键ID列表(不能为 null 以及 empty) */ int deleteBatchIds(@Param(Constants.COLLECTION) Collection\u0026lt;? extends Serializable\u0026gt; idList); 测试用例 1 2 3 4 5 6 @Test public void testDelteBatchIds(){ // 根据id批量删除数据 int result = userMapper.deleteBatchIds(Arrays.asList(10, 11, 12)); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); } 控制台打印 1 2 3 4 5 [main] [org.hong.mapper.UserMapper.deleteBatchIds]-[DEBUG] ==\u0026gt; Preparing: DELETE FROM tb_user WHERE id IN ( ? , ? , ? ) [main] [org.hong.mapper.UserMapper.deleteBatchIds]-[DEBUG] ==\u0026gt; Parameters: 10(Integer), 11(Integer), 12(Integer) [main] [org.hong.mapper.UserMapper.deleteBatchIds]-[DEBUG] \u0026lt;== Updates: 3 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@74aa9c72] result =\u0026gt; 3 查询操作 MP提供了多种查询操作，包括根据id查询、批量查询、查询单条数据、查询列表、分页查询等操作。\nselectById 方法定义 1 2 3 4 5 6 /** * 根据 ID 查询 * * @param id 主键ID */ T selectById(Serializable id); 测试用例 1 2 3 4 5 @Test public void testSelectById(){ User user = userMapper.selectById(1L); System.out.println(user); } 控制台打印 1 2 3 4 5 [main] [org.hong.mapper.UserMapper.selectById]-[DEBUG] ==\u0026gt; Preparing: SELECT id,user_name,name,age,email FROM tb_user WHERE id=? [main] [org.hong.mapper.UserMapper.selectById]-[DEBUG] ==\u0026gt; Parameters: 1(Long) [main] [org.hong.mapper.UserMapper.selectById]-[DEBUG] \u0026lt;== Total: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e48bf9a] User(id=1, userName=zhangsan, password=null, name=张三, age=21, email=test1@itcast.cn, address=null) selectBatchId 方法定义 1 2 3 4 5 6 /** * 查询（根据ID 批量查询） * * @param idList 主键ID列表(不能为 null 以及 empty) */ List\u0026lt;T\u0026gt; selectBatchIds(@Param(Constants.COLLECTION) Collection\u0026lt;? extends Serializable\u0026gt; idList); 测试用例 1 2 3 4 5 6 7 @Test public void testSelectBatchIds(){ List\u0026lt;User\u0026gt; users = userMapper.selectBatchIds(Arrays.asList(1L, 2L, 3L)); for (User user : users) { System.out.println(user); } } 控制台打印 1 2 3 4 5 6 7 [main] [org.hong.mapper.UserMapper.selectBatchIds]-[DEBUG] ==\u0026gt; Preparing: SELECT id,user_name,name,age,email FROM tb_user WHERE id IN ( ? , ? , ? ) [main] [org.hong.mapper.UserMapper.selectBatchIds]-[DEBUG] ==\u0026gt; Parameters: 1(Long), 2(Long), 3(Long) [main] [org.hong.mapper.UserMapper.selectBatchIds]-[DEBUG] \u0026lt;== Total: 3 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@c1fca2a] User(id=1, userName=zhangsan, password=null, name=张三, age=21, email=test1@itcast.cn, address=null) User(id=2, userName=lisi, password=null, name=李四, age=20, email=test2@itcast.cn, address=null) User(id=3, userName=wangwu, password=null, name=王五, age=28, email=test3@itcast.cn, address=null) selectOne 方法定义 1 2 3 4 5 6 /** * 根据 entity 条件，查询一条记录 * * @param queryWrapper 实体对象封装操作类（可以为 null） */ T selectOne(@Param(Constants.WRAPPER) Wrapper\u0026lt;T\u0026gt; queryWrapper); 测试用例 1 2 3 4 5 6 7 8 @Test public void testSelectOne(){ QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;\u0026gt;(); wrapper.eq(\u0026#34;id\u0026#34;, \u0026#34;3\u0026#34;); // 查询的数据如果大于1则会保存, 因此查询结果要么为null要么为1 User user = userMapper.selectOne(wrapper); System.out.println(user); } 控制台打印 1 2 3 4 5 [main] [org.hong.mapper.UserMapper.selectOne]-[DEBUG] ==\u0026gt; Preparing: SELECT id,user_name,name,age,email FROM tb_user WHERE id = ? [main] [org.hong.mapper.UserMapper.selectOne]-[DEBUG] ==\u0026gt; Parameters: 3(String) [main] [org.hong.mapper.UserMapper.selectOne]-[DEBUG] \u0026lt;== Total: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1aac188d] User(id=3, userName=wangwu, password=null, name=王五, age=28, email=test3@itcast.cn, address=null) selectCount 方法定义 1 2 3 4 5 6 /** * 根据 Wrapper 条件，查询总记录数 * * @param queryWrapper 实体对象封装操作类（可以为 null） */ Integer selectCount(@Param(Constants.WRAPPER) Wrapper\u0026lt;T\u0026gt; queryWrapper); 测试用例 1 2 3 4 5 6 @Test public void testSelectCount(){ // 如果wrapper为null, 则没有查询条件, 就是查询所有 Integer count = userMapper.selectCount(null); System.out.println(\u0026#34;count =\u0026gt; \u0026#34; + count); } 控制台打印 1 2 3 4 5 [main] [org.hong.mapper.UserMapper.selectCount]-[DEBUG] ==\u0026gt; Preparing: SELECT COUNT( 1 ) FROM tb_user [main] [org.hong.mapper.UserMapper.selectCount]-[DEBUG] ==\u0026gt; Parameters: [main] [org.hong.mapper.UserMapper.selectCount]-[DEBUG] \u0026lt;== Total: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@164a62bf] count =\u0026gt; 5 selectList 方法定义 1 2 3 4 5 6 /** * 根据 entity 条件，查询全部记录 * * @param queryWrapper 实体对象封装操作类（可以为 null） */ List\u0026lt;T\u0026gt; selectList(@Param(Constants.WRAPPER) Wrapper\u0026lt;T\u0026gt; queryWrapper); 测试用例 1 2 3 4 5 6 7 8 9 10 @Test public void testSelectList(){ QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;User\u0026gt;(); // 设置查询条件 wrapper.like(\u0026#34;email\u0026#34;, \u0026#34;itcast\u0026#34;); List\u0026lt;User\u0026gt; users = userMapper.selectList(wrapper); for (User user : users) { System.out.println(user); } } 控制台打印 1 2 3 4 5 6 7 8 9 [main] [org.hong.mapper.UserMapper.selectList]-[DEBUG] ==\u0026gt; Preparing: SELECT id,user_name,name,age,email FROM tb_user WHERE email LIKE ? [main] [org.hong.mapper.UserMapper.selectList]-[DEBUG] ==\u0026gt; Parameters: %itcast%(String) [main] [org.hong.mapper.UserMapper.selectList]-[DEBUG] \u0026lt;== Total: 5 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4fad6218] User(id=1, userName=zhangsan, password=null, name=张三, age=21, email=test1@itcast.cn, address=null) User(id=2, userName=lisi, password=null, name=李四, age=20, email=test2@itcast.cn, address=null) User(id=3, userName=wangwu, password=null, name=王五, age=28, email=test3@itcast.cn, address=null) User(id=4, userName=zhaoliu, password=null, name=赵六, age=21, email=test4@itcast.cn, address=null) User(id=5, userName=sunqi, password=null, name=孙七, age=24, email=test5@itcast.cn, address=null) selectPage 方法定义 1 2 3 4 5 6 7 /** * 根据 entity 条件，查询全部记录（并翻页） * * @param page 分页查询条件（可以为 RowBounds.DEFAULT） * @param queryWrapper 实体对象封装操作类（可以为 null） */ IPage\u0026lt;T\u0026gt; selectPage(IPage\u0026lt;T\u0026gt; page, @Param(Constants.WRAPPER) Wrapper\u0026lt;T\u0026gt; queryWrapper); 配置分页插件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package org.hong.config; import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @MapperScan({\u0026#34;org.hong.mapper\u0026#34;}) @Configuration public class MyBatisPlusConfig { // 配置分页插件 @Bean public PaginationInterceptor paginationInterceptor(){ PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); return paginationInterceptor; } } 测试用例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void testSelectPage(){ QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;User\u0026gt;(); // 设置查询条件 wrapper.like(\u0026#34;email\u0026#34;, \u0026#34;itcast\u0026#34;); Page\u0026lt;User\u0026gt; page = new Page\u0026lt;User\u0026gt;(); // 设置当前页数 page.setCurrent(1); // 设置每页的数量 page.setSize(1); IPage\u0026lt;User\u0026gt; userIPage = userMapper.selectPage(page, wrapper); System.out.println(\u0026#34;数据总条数:\u0026#34; + userIPage.getTotal()); System.out.println(\u0026#34;数据总页数:\u0026#34; + userIPage.getPages()); System.out.println(\u0026#34;当前页数:\u0026#34; + userIPage.getCurrent()); // 获取查询数据 List\u0026lt;User\u0026gt; users = userIPage.getRecords(); for (User user : users) { System.out.println(user); } } 控制台打印 1 2 3 4 5 6 7 8 9 10 11 12 13 [main] [com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize]-[DEBUG] JsqlParserCountOptimize sql=SELECT id,user_name,name,age,email FROM tb_user WHERE email LIKE ? [main] [org.hong.mapper.UserMapper.selectPage]-[DEBUG] ==\u0026gt; Preparing: SELECT COUNT(1) FROM tb_user WHERE email LIKE ? [main] [org.hong.mapper.UserMapper.selectPage]-[DEBUG] ==\u0026gt; Parameters: %itcast%(String) [main] [org.hong.mapper.UserMapper.selectPage]-[DEBUG] ==\u0026gt; Preparing: SELECT id,user_name,name,age,email FROM tb_user WHERE email LIKE ? LIMIT ?,? [main] [org.hong.mapper.UserMapper.selectPage]-[DEBUG] ==\u0026gt; Parameters: %itcast%(String), 0(Long), 1(Long) [main] [org.hong.mapper.UserMapper.selectPage]-[DEBUG] \u0026lt;== Total: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7c9bdee9] 数据总条数:5 数据总页数:5 当前页数:1 User(id=1, userName=zhangsan, password=null, name=张三, age=21, email=test1@itcast.cn, address=null) 配置 在MP中有大量的配置，其中有一部分是Mybatis原生的配置，另一部分是MP的配置，详情：https://mybatis.plus/config/ 。\n基本配置 configLocation MyBatis 配置文件位置，如果您有单独的 MyBatis 配置，请将其路径配置到 configLocation 中。 MyBatis Configuration 的具体内容请参考MyBatis 官方文档\nSpring Boot：\n1 2 mybatis-plus: config-location: classpath:mybatis-config.xml Spring MVC\n1 2 3 \u0026lt;bean id=\u0026#34;sqlSessionFactory\u0026#34; class=\u0026#34;com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;configLocation\u0026#34; value=\u0026#34;classpath:mybatis-config.xml\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; mapperLocations MyBatis Mapper 所对应的 XML 文件位置，如果您在 Mapper 中有自定义方法（XML 中有自定义实现），需要进行该配置，告诉 Mapper 所对应的 XML 文件位置。\nSpring Boot：\n1 2 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml # 默认就是这个值 Spring MVC：\n1 2 3 \u0026lt;bean id=\u0026#34;sqlSessionFactory\u0026#34; class=\u0026#34;com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;mapperLocations\u0026#34; value=\u0026#34;classpath*:mapper/**/*.xml\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; Maven 多模块项目的扫描路径需以 classpath*: 开头 （即加载多个 jar 包下的 XML 文件）\ntypeAliasesPackage MyBaits 别名包扫描路径，通过该属性可以给包中的类注册别名，注册后在 Mapper 对应的 XML 文件中可以直接使 用类名，而不用使用全限定的类名（即 XML 中调用的时候不用包含包名）。\nSpring Boot：\n1 2 mybatis-plus: type-aliases-package: org.hong.pojo Spring MVC：\n1 2 3 \u0026lt;bean id=\u0026#34;sqlSessionFactory\u0026#34; class=\u0026#34;com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;typeAliasesPackage\u0026#34; value=\u0026#34;com.baomidou.mybatisplus.samples.quickstart.entity\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 进阶配置 本部分（Configuration）的配置大都为 MyBatis 原生支持的配置，这意味着您可以通过 MyBatis XML 配置文件的形 式进行配置。\nmapUnderscoreToCamelCase 类型： boolean 默认值： true 是否开启自动驼峰命名规则（camel case）映射，即从经典数据库列名 A_COLUMN（下划线命名） 到经典 Java 属 性名 aColumn（驼峰命名） 的类似映射。\n注意： 此属性在 MyBatis 中原默认值为 false，在 MyBatis-Plus 中，此属性也将用于生成最终的 SQL 的 select body 如果您的数据库命名符合规则无需使用 @TableField 注解指定数据库字段名\n示例（SpringBoot）：\n1 2 3 4 #关闭自动驼峰映射，该参数不能和mybatis-plus.config-location同时存在 mybatis-plus: configuration: map-underscore-to-camel-case: false cacheEnabled 类型： boolean 默认值： true 全局地开启或关闭配置文件中的所有映射器已经配置的任何缓存，默认为 true。\n示例（SpringBoot）：\n1 2 3 mybatis-plus: configuration: cache-enabled: false DB 策略配置 idType 类型： com.baomidou.mybatisplus.annotation.IdType 默认值： ID_WORKER 全局默认主键类型，设置后，即可省略实体对象中的 @TableId(type = IdType.AUTO) 配置。\nSpringBoot：\n1 2 3 4 mybatis-plus: global-config: db-config: id-type: auto SpringMVC：\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;!--这里使用MP提供的sqlSessionFactory，完成了Spring与MP的整合--\u0026gt; \u0026lt;bean id=\u0026#34;sqlSessionFactory\u0026#34; class=\u0026#34;com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;globalConfig\u0026#34; ref=\u0026#34;globalConfig\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;globalConfig\u0026#34; class=\u0026#34;com.baomidou.mybatisplus.core.config.GlobalConfig\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dbConfig\u0026#34; ref=\u0026#34;dbConfig\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;dbConfig\u0026#34; class=\u0026#34;com.baomidou.mybatisplus.core.config.GlobalConfig$DbConfig\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;idType\u0026#34; value=\u0026#34;AUTO\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; tablePrefix 类型： String 默认值： null 表名前缀，全局配置后可省略符合前缀规范的@TableName()配置。\nSpringBoot：\n1 2 3 4 mybatis-plus: global-config: db-config: table-prefix: tb_ SpringMVC：\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;!--这里使用MP提供的sqlSessionFactory，完成了Spring与MP的整合--\u0026gt; \u0026lt;bean id=\u0026#34;sqlSessionFactory\u0026#34; class=\u0026#34;com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;globalConfig\u0026#34; ref=\u0026#34;globalConfig\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;globalConfig\u0026#34; class=\u0026#34;com.baomidou.mybatisplus.core.config.GlobalConfig\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dbConfig\u0026#34; ref=\u0026#34;dbConfig\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;dbConfig\u0026#34; class=\u0026#34;com.baomidou.mybatisplus.core.config.GlobalConfig$DbConfig\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;idType\u0026#34; value=\u0026#34;AUTO\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;tablePrefix\u0026#34; value=\u0026#34;tb_\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 条件构造器 在MP中，Wrapper接口的实现类关系如下：\n可以看到，AbstractWrapper和AbstractChainWrapper是重点实现，接下来我们重点学习AbstractWrapper以及其 子类。\n说明: QueryWrapper(LambdaQueryWrapper) 和 UpdateWrapper(LambdaUpdateWrapper) 的父类 用于生成 sql 的 where 条件, entity 属性也用于生成 sql 的 where 条件 注意: entity 生成的 where 条件与 使用各个 api 生成 的 where 条件没有任何关联行为\n官网文档地址：https://mybatis.plus/guide/wrapper.html\nallEq 说明 1 2 3 allEq(Map\u0026lt;R, V\u0026gt; params) allEq(Map\u0026lt;R, V\u0026gt; params, boolean null2IsNull) allEq(boolean condition, Map\u0026lt;R, V\u0026gt; params, boolean null2IsNull) 全部eq(或个别isNull)\n个别参数说明: params : key 为数据库字段名, value 为字段值\nnull2IsNull : 为 false 时忽略为 null 的 entry，为 true 时 map 中的所有 entry 都会作为条件；默认为 true\ncondition : 为 true 时 map 生效，为 false 时 map 失效；默认为 true\n例1: allEq({id:1,name:\u0026ldquo;老王\u0026rdquo;,age:null}) \u0026mdash;\u0026gt; id = 1 and name = \u0026lsquo;老王\u0026rsquo; and age is null\n例2: allEq({id:1,name:\u0026ldquo;老王\u0026rdquo;,age:null}, false) \u0026mdash;\u0026gt; id = 1 and name = \u0026lsquo;老王\u0026rsquo;\n1 2 3 allEq(BiPredicate\u0026lt;R, V\u0026gt; filter, Map\u0026lt;R, V\u0026gt; params) allEq(BiPredicate\u0026lt;R, V\u0026gt; filter, Map\u0026lt;R, V\u0026gt; params, boolean null2IsNull) allEq(boolean condition, BiPredicate\u0026lt;R, V\u0026gt; filter, Map\u0026lt;R, V\u0026gt; params, boolean null2IsNull) 个别参数说明: filter : 过滤函数，是否允许字段传入比对条件中\n例1: allEq((k,v) -\u0026gt; k.indexOf(\u0026ldquo;a\u0026rdquo;) \u0026gt; 0, {id:1,name:\u0026ldquo;老王\u0026rdquo;,age:null}) \u0026mdash;\u0026gt; name = \u0026lsquo;老王\u0026rsquo; and age is null\n例2: allEq((k,v) -\u0026gt; k.indexOf(\u0026ldquo;a\u0026rdquo;) \u0026gt; 0, {id:1,name:\u0026ldquo;老王\u0026rdquo;,age:null}, false) \u0026mdash;\u0026gt; name = \u0026lsquo;老王\u0026rsquo;\n测试用例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void testAllEp(){ QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;\u0026gt;(); Map\u0026lt;String, Object\u0026gt; params = new HashMap\u0026lt;String, Object\u0026gt;(); params.put(\u0026#34;name\u0026#34;, \u0026#34;张三\u0026#34;); params.put(\u0026#34;age\u0026#34;, \u0026#34;21\u0026#34;); params.put(\u0026#34;password\u0026#34;, null); // SELECT id,user_name,name,age,email FROM tb_user WHERE password IS NULL AND name = ? AND age = ? //wrapper.allEq(params); // SELECT id,user_name,name,age,email FROM tb_user WHERE name = ? AND age = ? //wrapper.allEq(params, false); // SELECT id,user_name,name,age,email FROM tb_user WHERE age = ? wrapper.allEq((k, v) -\u0026gt; k.equals(\u0026#34;age\u0026#34;) || k.equals(\u0026#34;id\u0026#34;) || k.equals(\u0026#34;name\u0026#34;), params); List\u0026lt;User\u0026gt; users = userMapper.selectList(wrapper); for (User user : users) { System.out.println(user); } } 基本比较操作 eq 等于 = ne 不等于 \u0026lt;\u0026gt; gt 大于 \u0026gt; ge 大于等于 \u0026gt;= lt 小于 \u0026lt; le 小于等于 \u0026lt;= between BETWEEN 值1 AND 值2 notBetween NOT BETWEEN 值1 AND 值2 in 字段 IN (value.get(0), value.get(1), \u0026hellip;) notIn 字段 NOT IN (v0, v1, \u0026hellip;) 测试用例\n1 2 3 4 5 6 7 8 9 10 11 12 @Test public void testEq(){ QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;\u0026gt;(); // SELECT id,user_name,password,name,age,email FROM tb_user WHERE password = ? AND age \u0026gt;= ? AND id IN (?,?,?,?,?) wrapper.eq(\u0026#34;password\u0026#34;, \u0026#34;123456\u0026#34;). ge(\u0026#34;age\u0026#34;, 20). in(\u0026#34;id\u0026#34;, 1, 2, 3, 4, 5); List\u0026lt;User\u0026gt; users = userMapper.selectList(wrapper); for (User user : users) { System.out.println(user); } } 模糊查询 like LIKE \u0026lsquo;%值%\u0026rsquo; 例: like(\u0026ldquo;name\u0026rdquo;, \u0026ldquo;王\u0026rdquo;) \u0026mdash;\u0026gt; name like \u0026lsquo;%王%\u0026rsquo; notLike NOT LIKE \u0026lsquo;%值%\u0026rsquo; 例: notLike(\u0026ldquo;name\u0026rdquo;, \u0026ldquo;王\u0026rdquo;) \u0026mdash;\u0026gt; name not like \u0026lsquo;%王%\u0026rsquo; likeLeft LIKE \u0026lsquo;%值\u0026rsquo; 例: likeLeft(\u0026ldquo;name\u0026rdquo;, \u0026ldquo;王\u0026rdquo;) \u0026mdash;\u0026gt; name like \u0026lsquo;%王\u0026rsquo; likeRight LIKE \u0026lsquo;值%\u0026rsquo; 例: likeRight(\u0026ldquo;name\u0026rdquo;, \u0026ldquo;王\u0026rdquo;) \u0026mdash;\u0026gt; name like \u0026lsquo;王%\u0026rsquo; 测试用例\n1 2 3 4 5 6 7 8 9 10 11 @Test public void testLike(){ QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;\u0026gt;(); // SELECT id,user_name,password,name,age,email FROM tb_user WHERE user_name LIKE ? // Parameters: %s%(String) wrapper.like(\u0026#34;user_name\u0026#34;, \u0026#34;s\u0026#34;); List\u0026lt;User\u0026gt; users = userMapper.selectList(wrapper); for (User user : users) { System.out.println(user); } } 排序 orderBy 排序： ORDER BY 字段, \u0026hellip; 例: orderBy(true, true, \u0026ldquo;id\u0026rdquo;, \u0026ldquo;name\u0026rdquo;) \u0026mdash;\u0026gt; order by id ASC,name ASC orderByAsc 排序： ORDER BY 字段, \u0026hellip; ASC 例: orderByAsc(\u0026ldquo;id\u0026rdquo;, \u0026ldquo;name\u0026rdquo;) \u0026mdash;\u0026gt; order by id ASC,name ASC orderByDesc 排序： ORDER BY 字段, \u0026hellip; DESC 例: orderByDesc(\u0026ldquo;id\u0026rdquo;, \u0026ldquo;name\u0026rdquo;) \u0026mdash;\u0026gt; order by id DESC,name DESC 测试用例\n1 2 3 4 5 6 7 8 9 10 11 @Test public void testOrderBy(){ QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;\u0026gt;(); // 按照年龄降序 // SELECT id,user_name,password,name,age,email FROM tb_user ORDER BY age DESC wrapper.orderByDesc(\u0026#34;age\u0026#34;); List\u0026lt;User\u0026gt; users = userMapper.selectList(wrapper); for (User user : users) { System.out.println(user); } } 逻辑查询 or 拼接 OR 主动调用 or 表示紧接着下一个方法不是用 and 连接!(不调用 or 则默认为使用 and 连接) and AND 嵌套 例: and(i -\u0026gt; i.eq(\u0026ldquo;name\u0026rdquo;, \u0026ldquo;李白\u0026rdquo;).ne(\u0026ldquo;status\u0026rdquo;, \u0026ldquo;活着\u0026rdquo;)) \u0026mdash;\u0026gt; and (name = \u0026lsquo;李白\u0026rsquo; and status \u0026lt;\u0026gt; \u0026lsquo;活着\u0026rsquo;) 测试用例\n1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testOr(){ QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;\u0026gt;(); // SELECT id,user_name,password,name,age,email FROM tb_user WHERE name = ? OR age = ? //wrapper.eq(\u0026#34;name\u0026#34;, \u0026#34;张三\u0026#34;).or().eq(\u0026#34;age\u0026#34;, 24); //SELECT id,user_name,password,name,age,email FROM tb_user WHERE ( user_name = ? AND password = ? ) wrapper.and(fun -\u0026gt; fun.eq(\u0026#34;user_name\u0026#34;, \u0026#34;zhangsan\u0026#34;).eq(\u0026#34;password\u0026#34;, \u0026#34;123456\u0026#34;)); List\u0026lt;User\u0026gt; users = userMapper.selectList(wrapper); for (User user : users) { System.out.println(user); } } select 在MP查询中，默认查询所有的字段，如果有需要也可以通过select方法进行指定字段。\n测试用例\n1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testSelect(){ QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;\u0026gt;(); // SELECT user_name,password FROM tb_user WHERE name = ? OR age = ? wrapper.eq(\u0026#34;name\u0026#34;, \u0026#34;张三\u0026#34;) .or() .eq(\u0026#34;age\u0026#34;, 24) .select(\u0026#34;user_name\u0026#34;, \u0026#34;password\u0026#34;);// 指定查询的字段 List\u0026lt;User\u0026gt; users = userMapper.selectList(wrapper); for (User user : users) { System.out.println(user); } } ActiveRecord ActiveRecord（简称AR）一直广受动态语言（ PHP 、 Ruby 等）的喜爱，而 Java 作为准静态语言，对于 ActiveRecord 往往只能感叹其优雅，所以我们也在 AR 道路上进行了一定的探索，喜欢大家能够喜欢。\n什么是ActiveRecord？\nActiveRecord也属于ORM（对象关系映射）层，由Rails最早提出，遵循标准的ORM模型：表映射到记录，记 录映射到对象，字段映射到对象属性。配合遵循的命名和配置惯例，能够很大程度的快速实现模型的操作，而 且简洁易懂。\nActiveRecord的主要思想是：\n每一个数据库表对应创建一个类，类的每一个对象实例对应于数据库中表的一行记录； 通常表的每个字段 在类中都有相应的Field； ActiveRecord同时负责把自己持久化，在ActiveRecord中封装了对数据库的访问，即CURD； ActiveRecord是一种领域模型(Domain Model)，封装了部分业务逻辑； 开启AR之旅 在MP中，开启AR非常简单，只需要将实体对象继承Model即可。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package org.hong.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.activerecord.Model; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor @TableName(\u0026#34;tb_user\u0026#34;) public class User extends Model\u0026lt;User\u0026gt; { @TableId(type = IdType.AUTO) private Long id; private String userName; private String password; private String name; private Integer age; @TableField(value = \u0026#34;email\u0026#34;) private String email; } 根据主键查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package org.hong; import org.hong.pojo.User; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class TestUserMapper2 { @Test public void testSelectById(){ User user = new User(); user.setId(2L); User user1 = user.selectById(); System.out.println(user1); } } 新增数据 1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testInsert(){ User user = new User(); user.setUserName(\u0026#34;liubei\u0026#34;); user.setPassword(\u0026#34;123456\u0026#34;); user.setAge(30); user.setName(\u0026#34;刘备\u0026#34;); user.setEmail(\u0026#34;liubei@qq.com\u0026#34;); // 调用AR的insert方法插入数据 boolean result = user.insert(); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); } 控制台打印\n1 2 3 4 5 [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] ==\u0026gt; Preparing: INSERT INTO tb_user ( user_name, password, name, age, email ) VALUES ( ?, ?, ?, ?, ? ) [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] ==\u0026gt; Parameters: liubei(String), 123456(String), 刘备(String), 30(Integer), liubei@qq.com(String) [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] \u0026lt;== Updates: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5176d279] result =\u0026gt; true 更新操作 1 2 3 4 5 6 7 8 @Test public void testUpdate(){ User user = new User(); user.setId(6L); // 查询条件 user.setAge(31); // 更新的数据 boolean result = user.updateById(); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); } 控制台打印\n1 2 3 4 5 [main] [org.hong.mapper.UserMapper.updateById]-[DEBUG] ==\u0026gt; Preparing: UPDATE tb_user SET age=? WHERE id=? [main] [org.hong.mapper.UserMapper.updateById]-[DEBUG] ==\u0026gt; Parameters: 31(Integer), 6(Long) [main] [org.hong.mapper.UserMapper.updateById]-[DEBUG] \u0026lt;== Updates: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@4b7c4456] result =\u0026gt; true 删除操作 1 2 3 4 5 6 7 @Test public void testDelete(){ User user = new User(); user.setId(6L); boolean result = user.deleteById(); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); } 控制台打印\n1 2 3 4 5 [main] [org.hong.mapper.UserMapper.deleteById]-[DEBUG] ==\u0026gt; Preparing: DELETE FROM tb_user WHERE id=? [main] [org.hong.mapper.UserMapper.deleteById]-[DEBUG] ==\u0026gt; Parameters: 6(Long) [main] [org.hong.mapper.UserMapper.deleteById]-[DEBUG] \u0026lt;== Updates: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@74aa9c72] result =\u0026gt; true 根据条件查询 1 2 3 4 5 6 7 8 9 10 @Test public void testSelect(){ User user = new User(); QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;\u0026gt;(); wrapper.ge(\u0026#34;age\u0026#34;, 25); // 查询年龄大于等于25岁的用户 List\u0026lt;User\u0026gt; users = user.selectList(wrapper); for (User user1 : users) { System.out.println(user1); } } 控制台打印\n1 2 3 4 5 [main] [org.hong.mapper.UserMapper.selectList]-[DEBUG] ==\u0026gt; Preparing: SELECT id,user_name,password,name,age,email FROM tb_user WHERE age \u0026gt;= ? [main] [org.hong.mapper.UserMapper.selectList]-[DEBUG] ==\u0026gt; Parameters: 25(Integer) [main] [org.hong.mapper.UserMapper.selectList]-[DEBUG] \u0026lt;== Total: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1ac45389] User(id=3, userName=wangwu, password=123456, name=王五, age=28, email=test3@itcast.cn, address=null) Oracle 主键Sequence 在mysql中，主键往往是自增长的，这样使用起来是比较方便的，如果使用的是Oracle数据库，那么就不能使用自增 长了，就得使用Sequence 序列生成id值了。\n创建表以及序列 1 2 3 4 5 6 7 8 9 10 11 --创建表，表名以及字段名都要大写 CREATE TABLE \u0026#34;TB_USER\u0026#34; ( ID NUMBER(20) NOT NULL , USER_NAME VARCHAR2(255 BYTE) , PASSWORD VARCHAR2(255 BYTE) , NAME VARCHAR2(255 BYTE) , AGE NUMBER(10) , EMAIL VARCHAR2(255 BYTE) ) --创建序列 CREATE SEQUENCE SEQ_USER START WITH 1 INCREMENT BY 1 jdbc驱动包 由于版权原因，我们不能直接通过maven的中央仓库下载oracle数据库的jdbc驱动包，所以我们需要将驱动包安装到本地仓库。\n1 2 #ojdbc8.jar文件在资料中可以找到 mvn install:install-file -DgroupId=com.oracle -DartifactId=ojdbc8 -Dversion=12.1.0.1 -Dpackaging=jar -Dfile=ojdbc8.jar 安装完成后的坐标：\n修改application.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 #oracle连接信息 spring: datasource: driver-class-name: oracle.jdbc.driver.OracleDriver url: jdbc:oracle:thin:@127.0.0.1:1521:orcl username: scott password: ccat #id生成策略 mybatis-plus: global-config: db-config: id-type: input 配置序列 使用Oracle的序列需要做2件事情：\n第一，需要配置MP的序列生成器到Spring容器：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package org.hong.config; import com.baomidou.mybatisplus.extension.incrementer.OracleKeyGenerator; import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @MapperScan({\u0026#34;org.hong.mapper\u0026#34;}) @Configuration public class MyBatisPlusConfig { // 配置分页插件 @Bean public PaginationInterceptor paginationInterceptor(){ PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); return paginationInterceptor; } // Oracle的序列生成器 @Bean public OracleKeyGenerator oracleKeyGenerator(){ OracleKeyGenerator oracleKeyGenerator = new OracleKeyGenerator(); return oracleKeyGenerator; } } 第二，在实体对象中指定序列的名称：\n1 2 3 4 @KeySequence(value = \u0026#34;SEQ_USER\u0026#34;, clazz = Long.class) public class User{ ...... } 测试 1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testInsert(){ User user = new User(); user.setUserName(\u0026#34;liubei\u0026#34;); user.setPassword(\u0026#34;123456\u0026#34;); user.setAge(30); user.setName(\u0026#34;刘备\u0026#34;); user.setEmail(\u0026#34;liubei@qq.com\u0026#34;); // 调用AR的insert方法插入数据 boolean result = user.insert(); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); } 控制台打印\n1 2 3 4 5 6 7 8 9 #先发送一个查询序列的sql, 然后再发送insert语句。操作oracle数据库只需要添加oracle的配置, 使用还是与之前一样。 [main] [org.hong.mapper.UserMapper.insert!selectKey]-[DEBUG] ==\u0026gt; Preparing: SELECT SEQ_USER.NEXTVAL FROM DUAL [main] [org.hong.mapper.UserMapper.insert!selectKey]-[DEBUG] ==\u0026gt; Parameters: [main] [org.hong.mapper.UserMapper.insert!selectKey]-[DEBUG] \u0026lt;== Total: 1 [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] ==\u0026gt; Preparing: INSERT INTO tb_user ( id, user_name, password, name, age, email ) VALUES ( ?, ?, ?, ?, ?, ? ) [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] ==\u0026gt; Parameters: 1(Long), liubei(String), 123456(String), 刘备(String), 30(Integer), liubei@qq.com(String) [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] \u0026lt;== Updates: 1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5300cac] result =\u0026gt; true 插件 MyBatis 允许你在已映射语句执行过程中的某一点进行拦截调用。默认情况下，MyBatis 允许使用插件来拦截的方法 调用包括：\nExecutor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) ParameterHandler (getParameterObject, setParameters) ResultSetHandler (handleResultSets, handleOutputParameters) StatementHandler (prepare, parameterize, batch, update, query) 我们看到了可以拦截Executor接口的部分方法，比如update，query，commit，rollback等方法，还有其他接口的 一些方法等。\n总体概括为：\n拦截执行器的方法 拦截参数的处理 拦截结果集的处理 拦截Sql语法构建的处理 拦截器示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package org.hong.plugins; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import java.util.Properties; @Intercepts({@Signature( type= Executor.class, method = \u0026#34;update\u0026#34;, args = {MappedStatement.class,Object.class})}) public class MyInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { //拦截方法，具体业务逻辑编写的位置 return invocation.proceed(); } @Override public Object plugin(Object target) { //创建target对象的代理对象,目的是将当前拦截器加入到该对象中 return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 设置属性 } } 注入到Spring容器：\n1 2 3 4 @Bean public MyInterceptor myInterceptor(){ return new MyInterceptor(); } 或者通过xml配置，mybatis-config.xml：\n1 2 3 4 5 6 7 8 9 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE configuration PUBLIC \u0026#34;-//mybatis.org//DTD Config 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-config.dtd\u0026#34;\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin interceptor=\u0026#34;org.hon.plugins.MyInterceptor\u0026#34;\u0026gt;\u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/configuration\u0026gt; 分页插件 自定义 Mapper Method 使用分页。在 Mapper 接口方法参数列表中添加 IPage 对象，MyBatisPlus 会自动根据这个对象进行分页。\n属性介绍 属性名 类型 默认值 描述 overflow boolean false 溢出总页数后是否进行处理(默认不处理,参见 插件#continuePage 方法) maxLimit Long 单页分页条数限制(默认无限制,参见 插件#handlerLimit 方法) dbType DbType 数据库类型(根据类型获取应使用的分页方言,参见 插件#findIDialect 方法) dialect IDialect 方言实现类(参见 插件#findIDialect 方法) 配置分页插件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 配置分页插件 @Bean public PaginationInnerInterceptor paginationInterceptor(){ PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(); return paginationInterceptor; } /** * 将插件添加到MyBatisPlus中 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(PaginationInnerInterceptor paginationInterceptor) { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(paginationInterceptor); // 添加分页插件 return interceptor; } POJO 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package org.hong.pojo; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor @TableName(\u0026#34;tb_user\u0026#34;) public class User { @TableId(type = IdType.AUTO) private Long id; private String userName; private String password; private String name; private Integer age; private String email; } Mapper 1 2 3 4 5 6 7 8 9 10 11 12 13 package org.hong.mapper; import com.baomidou.mybatisplus.core.metadata.IPage; import org.apache.ibatis.annotations.Select; import org.hong.pojo.User; public interface UserMapper extends MyBaseMapper\u0026lt;User\u0026gt; { /* * 我们只需要在方法入参中添加IPage分页对象,MP会自动进行处理,我们不需要自己处理分页 */ @Select(\u0026#34;SELECT * FROM tb_user\u0026#34;) IPage\u0026lt;User\u0026gt; selectPage(IPage\u0026lt;?\u0026gt; page); } 测试用例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package org.hong; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.hong.mapper.UserMapper; import org.hong.pojo.User; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.List; @SpringBootTest @RunWith(SpringJUnit4ClassRunner.class) class MybatisPlusSpringbootApplicationTests { @Autowired private UserMapper userMapper; @Test void contextLoads() { Page\u0026lt;User\u0026gt; page = new Page\u0026lt;\u0026gt;(); page.setCurrent(2); page.setSize(2); IPage\u0026lt;User\u0026gt; userIPage = userMapper.selectPage(page); userIPage.getRecords().forEach(System.out :: println); System.out.println(userIPage == page); System.out.println(userIPage.getTotal()); } } 控制台打印 1 2 3 4 5 6 7 8 9 10 11 12 13 [main] [com.baomidou.mybatisplus.extension.plugins.pagination.optimize.JsqlParserCountOptimize]-[DEBUG] JsqlParserCountOptimize sql=SELECT * FROM tb_user # 查询总条数 [main] [org.hong.mapper.UserMapper.selectPage]-[DEBUG] ==\u0026gt; Preparing: SELECT COUNT(1) FROM tb_user [main] [org.hong.mapper.UserMapper.selectPage]-[DEBUG] ==\u0026gt; Parameters: # MP自动拼接分页条件 [main] [org.hong.mapper.UserMapper.selectPage]-[DEBUG] ==\u0026gt; Preparing: SELECT * FROM tb_user LIMIT ?,? [main] [org.hong.mapper.UserMapper.selectPage]-[DEBUG] ==\u0026gt; Parameters: 2(Long), 2(Long) [main] [org.hong.mapper.UserMapper.selectPage]-[DEBUG] \u0026lt;== Total: 2 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6a472566] User(id=3, userName=wangwu, password=123456, name=王五, age=28, email=test3@itcast.cn) User(id=4, userName=zhaoliu, password=123456, name=赵六, age=21, email=test4@itcast.cn) # 返回的IPage == 入参的IPage true 如果返回类型是 IPage 则入参的 IPage 不能为null,因为 返回的IPage == 入参的IPage 如果返回类型是 List 则入参的 IPage 可以为 null(为 null 则不分页),但需要你手动 入参的IPage.setRecords(返回的 List); 如果 xml 需要从 page 里取值,需要 page.属性 获取\n执行分析插件 在MP中提供了对SQL执行的分析的插件，可用作阻断全表更新、删除的操作\n注意：该插件仅适用于开发环境，不 适用于生产环境。\nSpringBoot配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /** * 防止全表更新与删除 * @return */ @Bean public BlockAttackInnerInterceptor blockAttackInnerInterceptor(){ BlockAttackInnerInterceptor blockAttackInnerInterceptor = new BlockAttackInnerInterceptor(); return blockAttackInnerInterceptor; } /** * 将插件添加到MyBatisPlus中 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(BlockAttackInnerInterceptor blockAttackInnerInterceptor) { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(blockAttackInnerInterceptor); return interceptor; } 测试用例 1 2 3 4 5 @Test public void testDelteAll(){ int delete = userMapper.delete(null);// 全表删除 System.out.println(delete); } 控制台打印 Prohibition of full table deletion：禁止全表删除\n1 2 3 4 5 6 org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: ### Error updating database. Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of full table deletion ### The error may exist in org/hong/mapper/UserMapper.java (best guess) ### The error may involve org.hong.mapper.UserMapper.delete ### The error occurred while executing an update ### Cause: com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of full table deletion 乐观锁插件 乐观锁插件 意图：\n​\t当要更新一条记录的时候，希望这条记录没有被别人更新\n乐观锁实现方式：\n取出记录时，获取当前version 更新时，带上这个version 执行更新时， set version = newVersion where version = oldVersion 如果version不对，就更新失败 插件配置 spring boot:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /** * 乐观锁 * @return */ @Bean public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor(){ OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor = new OptimisticLockerInnerInterceptor(); return optimisticLockerInnerInterceptor; } /** * 将插件添加到MyBatisPlus中 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor(OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor) { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(optimisticLockerInnerInterceptor); return interceptor; } 注解实体字段 需要为实体字段添加@Version注解。\n需要为实体字段添加@Version注解。\n1 2 3 ALTER TABLE `tb_user` ADD COLUMN `version` int(10) NULL AFTER `email`; UPDATE `tb_user` SET `version`=\u0026#39;1\u0026#39;; 为User实体对象添加version字段，并且添加@Version注解：\n1 2 @Version private Integer version; 测试 测试用例：\n1 2 3 4 5 6 7 8 9 @Test public void testLock(){ User user = new User(); user.setId(5L); // 查询条件 user.setAge(31); // 更新的数据 user.setVersion(1); // 当前版本信息 boolean result = user.updateById(); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); } 执行日志：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [main] [org.hong.mapper.UserMapper.updateById]-[DEBUG] ==\u0026gt; Preparing: UPDATE tb_user SET age=?, version=? WHERE id=? AND version=? [main] [org.hong.mapper.UserMapper.updateById]-[DEBUG] ==\u0026gt; Parameters: 31(Integer), 2(Integer), 5(Long), 1(Integer) [main] [org.hong.mapper.UserMapper.updateById]-[DEBUG] \u0026lt;== Updates: 1 Time：3 ms - ID：org.hong.mapper.UserMapper.updateById Execute SQL： UPDATE tb_user SET age=31, version=2 WHERE id=5 AND version=1 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@7a2b1eb4] result =\u0026gt; true 可以看到，更新的条件中有version条件，并且更新的version为2。 如果再次执行，更新则不成功。这样就避免了多人同时更新时导致数据的不一致。\n特别说明 支持的数据类型只有:int,Integer,long,Long,Date,Timestamp,LocalDateTime 整数类型下 newVersion = oldVersion + 1 newVersion 会回写到 entity 中 仅支持 updateById(id) 与 update(entity, wrapper) 方法 在 update(entity, wrapper) 方法下, wrapper 不能复用!!! 扩展 性能 SQL 分析打印 用于输出每条 SQL 语句及其执行时间，可以设置最大执行时间，超过时间会抛出异常。\n该插件只用于开发环境，不建议生产环境使用。\nMaven 1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;p6spy\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;p6spy\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;最新版本\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; application.yaml 1 2 3 4 5 6 7 8 9 spring: datasource: #修改驱动 driver-class-name: com.p6spy.engine.spy.P6SpyDriver #修改url前缀 url: jdbc:p6spy:mysql://127.0.0.1:3306/mp?useUnicode=true\u0026amp;characterEncoding=utf8\u0026amp;autoReconnect=true\u0026amp;allowMultiQueries=true\u0026amp;useSSL=false username: root password: 1234 type: com.zaxxer.hikari.HikariDataSource spy.properties 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #3.2.1以上使用 modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory # 自定义日志打印 logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger #日志输出到控制台 appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger # 使用日志系统记录 sql #appender=com.p6spy.engine.spy.appender.Slf4JLogger # 设置 p6spy driver 代理 deregisterdrivers=true # 取消JDBC URL前缀 useprefix=true # 配置记录 Log 例外,可去掉的结果集有error,info,batch,debug,statement,commit,rollback,result,resultset. excludecategories=info,debug,result,commit,resultset # 日期格式 dateformat=yyyy-MM-dd HH:mm:ss # 实际驱动可多个 #driverlist=org.h2.Driver # 是否开启慢SQL记录 outagedetection=true # 慢SQL记录标准 2 秒 outagedetectioninterval=2 测试用例 1 2 3 4 5 @Test void testP6spy(){ List\u0026lt;User\u0026gt; list = userMapper.selectList(null); list.forEach(System.out :: println); } 控制台打印 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [main] [org.mybatis.spring.transaction.SpringManagedTransaction]-[DEBUG] JDBC Connection [HikariProxyConnection@797313059 wrapping com.p6spy.engine.wrapper.ConnectionWrapper@3289079a] will not be managed by Spring [main] [org.hong.mapper.UserMapper.selectList]-[DEBUG] ==\u0026gt; Preparing: SELECT id,user_name,password,name,age,email,sex FROM tb_user [main] [org.hong.mapper.UserMapper.selectList]-[DEBUG] ==\u0026gt; Parameters: Consume Time：9 ms 2021-08-01 14:42:17 # 本次sql花费9ms Execute SQL：SELECT id,user_name,password,name,age,email,sex FROM tb_user # 本次执行的sql [main] [org.hong.mapper.UserMapper.selectList]-[DEBUG] \u0026lt;== Total: 7 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@f88bfbe] User(id=1, userName=zhangsan, password=123456, name=张三, age=18, email=test1@itcast.cn, sex=MAN) User(id=2, userName=lisi, password=123456, name=李四, age=20, email=test2@itcast.cn, sex=MAN) User(id=3, userName=wangwu, password=123, name=王五, age=28, email=test3@itcast.cn, sex=MAN) User(id=4, userName=zhaoliu, password=123456, name=赵六, age=21, email=test4@itcast.cn, sex=WOMAN) User(id=5, userName=sunqi, password=123456, name=孙七, age=24, email=test5@itcast.cn, sex=WOMAN) User(id=6, userName=苞米豆, password=88888, name=baomidou, age=8, email=baomidou@qq.com, sex=MAN) User(id=7, userName=苞米豆, password=123456, name=baomidou, age=8, email=baomidou@qq.com, sex=MAN) 自动填充功能 有些时候我们可能会有这样的需求，插入或者更新数据时，希望有些字段可以自动填充数据，比如密码、version 等、createTime、updateTime。在MP中提供了这样的功能，可以实现自动填充。即填充默认值。\n添加@TableField注解 为password添加自动填充功能，在新增数据时有效。\n1 2 @TableField(fill = FieldFill.INSERT) // 设置字段填充策略 private String password; FieldFill提供了多种模式选择：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public enum FieldFill { /** * 默认不处理 */ DEFAULT, /** * 插入时填充字段 */ INSERT, /** * 更新时填充字段 */ UPDATE, /** * 插入和更新时填充字段 */ INSERT_UPDATE } 编写MyMetaObjectHandler 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package org.hong.handler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; @Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { /** * 严格填充,只针对非主键的字段,只有该表种字段的@TableField注解使用了fill属性, * 并且传入的字段名和类中字段属性匹配、且类中字段属性类型与闯入的类型匹才会进行填充(传入null值不填充), * 如果在填充时, 该字段被赋值, 则不会进行填充 */ this.strictInsertFill(metaObject, \u0026#34;password\u0026#34;, String.class, \u0026#34;88888\u0026#34;); // 起始版本 3.3.0(推荐使用) } @Override public void updateFill(MetaObject metaObject) { /** * 举一反三 * strictUpdateFill代表修改时填充 */ } } 测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void testInsert(){ User user = new User(); user.setEmail(\u0026#34;baomidou@qq.com\u0026#34;); user.setAge(8); user.setUserName(\u0026#34;苞米豆\u0026#34;); user.setName(\u0026#34;baomidou\u0026#34;); //user.setPassword(\u0026#34;123456\u0026#34;); user.setAddress(\u0026#34;中国\u0026#34;); // 返回数据库受影响的行数 int result = this.userMapper.insert(user); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); // 获取自增后的id值, 自增后的id值会回填到user对象中 System.out.println(\u0026#34;id =\u0026gt; \u0026#34; + user.getId()); } 控制台打印 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] ==\u0026gt; Preparing: INSERT INTO tb_user ( user_name, password, name, age, email ) VALUES ( ?, ?, ?, ?, ? ) [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] ==\u0026gt; Parameters: 苞米豆(String), 888888(String), baomidou(String), 8(Integer), baomidou@qq.com(String) [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] \u0026lt;== Updates: 1 Time：19 ms - ID：org.hong.mapper.UserMapper.insert Execute SQL： INSERT INTO tb_user ( user_name, password, name, age, email ) VALUES ( \u0026#39;苞米豆\u0026#39;, \u0026#39;888888\u0026#39;, \u0026#39;baomidou\u0026#39;, 8, \u0026#39;baomidou@qq.com\u0026#39; ) [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@21e20ad5] result =\u0026gt; 1 id =\u0026gt; 9 Sql 注入器 创建自定义通用方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package org.hong.injectors; import com.baomidou.mybatisplus.core.injector.AbstractMethod; import com.baomidou.mybatisplus.core.metadata.TableInfo; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlSource; public class FindAll extends AbstractMethod { @Override public MappedStatement injectMappedStatement(Class\u0026lt;?\u0026gt; mapperClass, Class\u0026lt;?\u0026gt; modelClass, TableInfo tableInfo) { String id = \u0026#34;findAll\u0026#34;; // 通用方法的方法名 String sql = this.createSql(tableInfo); // 通用方法的执行SQL语句 SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass); // 创建SQL资源 return this.addSelectMappedStatementForTable(mapperClass, id, sqlSource, tableInfo); // 添加SQL资源 } /** * 创建SQL语句 * @param tableInfo * @return */ private String createSql (TableInfo tableInfo){ StringBuilder sql = new StringBuilder(\u0026#34;SELECT \u0026#34;); tableInfo.getFieldList().forEach(field -\u0026gt; sql.append(field.getColumn() + \u0026#34;, \u0026#34;)); int lastIndex = sql.lastIndexOf(\u0026#34;, \u0026#34;); sql.replace(lastIndex, lastIndex + 2, \u0026#34;\u0026#34;); sql.append(\u0026#34; FROM \u0026#34; + tableInfo.getTableName()); return sql.toString(); } } 将自定义通用方法注入到 MyBatisPlus 中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package org.hong.injectors; import com.baomidou.mybatisplus.core.injector.AbstractMethod; import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; import java.util.List; public class MySqlInjector extends DefaultSqlInjector { /** * 获取注入的方法 * * 如果只需增加方法，保留MP自带方法 * 可以super.getMethodList() 再 add * * @param mapperClass * @return 注入的方法集合 */ @Override public List\u0026lt;AbstractMethod\u0026gt; getMethodList(Class\u0026lt;?\u0026gt; mapperClass) { List\u0026lt;AbstractMethod\u0026gt; methodList = super.getMethodList(mapperClass); // 获取已经存在的通用方法集合 methodList.add(new FindAll()); // 添加我们自己的通用方法 return methodList; } } 继承 BaseMapper 接口添加自定义的通用方法 1 2 3 4 5 6 7 8 9 10 11 12 package org.hong.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import java.util.List; /** * 继承BaseMapper添加自定义通用方法 */ public interface MyBaseMapper\u0026lt;T\u0026gt; extends BaseMapper\u0026lt;T\u0026gt; { List\u0026lt;T\u0026gt; findAll(); // 方法名必须与FindAll类中设置的方法ID一致 } UserMapper 1 2 3 4 5 6 7 8 9 package org.hong.mapper; import org.hong.pojo.User; /** * 继承MyBaseMapper获得自定义的通用方法 */ public interface UserMapper extends MyBaseMapper\u0026lt;User\u0026gt; { } 测试用例 1 2 3 4 5 @Test public void testSqlInjector(){ List\u0026lt;User\u0026gt; all = userMapper.findAll(); all.forEach(System.out :: println); } 逻辑删除 开发系统时，有时候在实现功能时，删除操作需要实现逻辑删除，所谓逻辑删除就是将数据标记为删除，而并非真正 的物理删除（非DELETE操作），查询时需要携带状态条件，确保被标记的数据不被查询到。这样做的目的就是避免 数据被真正的删除。\nMP就提供了这样的功能，方便我们使用，接下来我们一起学习下。\n修改表结构 为tb_user表增加deleted字段，用于表示数据是否被删除，1代表删除，0代表未删除。\n1 2 3 ALTER TABLE `tb_user` ADD COLUMN `deleted` int(1) NULL DEFAULT 0 COMMENT \u0026#39;1代表删除，0代表未删除\u0026#39; AFTER `version`; 同时，也修改User实体，增加deleted属性并且添加@TableLogic注解：\n1 2 3 4 5 // value: 设置未删除的标识 // dvalue: 设置删除的标识 // 这两个值不写会默认获取全局配置的值 @TableLogic(value = \u0026#34;0\u0026#34;, delval = \u0026#34;1\u0026#34;) // 逻辑删除, 0:未删除 1:删除 private Integer deleted; application.yaml 1 2 3 4 5 6 # 也可以不配, 默认就是这些值, 如果需要更改才去配置 mybatis-plus: global-config: db-config: logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) 测试 1 2 3 4 @Test public void testDeleteById(){ this.userMapper.deleteById(2L); } 控制台打印 调用 deleteById 方法实际执行的是 update 操作，修改 deleted 列实现逻辑删除。\n并且在调用 select、update、delete 方法时都会多出一个 deleted = 0 的条件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [main] [org.hong.mapper.UserMapper.deleteById]-[DEBUG] ==\u0026gt; Preparing: UPDATE tb_user SET deleted=1 WHERE id=? AND deleted=0 [main] [org.hong.mapper.UserMapper.deleteById]-[DEBUG] ==\u0026gt; Parameters: 1(Integer) [main] [org.hong.mapper.UserMapper.deleteById]-[DEBUG] \u0026lt;== Updates: 1 Time：44 ms - ID：org.hong.mapper.UserMapper.deleteById Execute SQL： UPDATE tb_user SET deleted=1 WHERE id=1 AND deleted=0 [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@38af1bf6] result =\u0026gt; 1 通用枚举 解决了繁琐的配置，让 mybatis 优雅的使用枚举属性！需要注意的是通用枚举可能与 spring-boot-devtools 造成冲突\n修改表结构 1 2 ALTER TABLE `tb_user` ADD COLUMN `sex` int(1) NULL DEFAULT 1 COMMENT \u0026#39;1-男，2-女\u0026#39; AFTER `deleted`; 定义枚举 方式一 枚举属性，实现 IEnum 接口如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package org.hong.enums; import com.baomidou.mybatisplus.core.enums.IEnum; /** * 实现IEnum\u0026lt;E\u0026gt;接口, 泛型为该枚举实际值的类型 * 并重写getValue方法, 返回枚举对象的实际值 */ public enum SexEnum implements IEnum\u0026lt;Integer\u0026gt; { MAN(1,\u0026#34;男\u0026#34;), WOMAN(2,\u0026#34;女\u0026#34;); final Integer value; final String desc; SexEnum(Integer value, String desc) { this.value = value; this.desc = desc; } /** * 返回枚举的实际值 * @return */ @Override public Integer getValue() { return this.value; } public String getDesc() { return this.desc; } } 方式二 使用 @EnumValue 注解枚举属性\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package org.hong.enums; import com.baomidou.mybatisplus.annotation.EnumValue; public enum SexEnum { MAN(1,\u0026#34;男\u0026#34;), WOMAN(2,\u0026#34;女\u0026#34;); /** * 使用@EnumValue标记枚举实际值 */ @EnumValue final Integer value; final String desc; SexEnum(Integer value, String desc) { this.value = value; this.desc = desc; } } application.yaml 1 2 3 4 # 枚举包扫描 mybatis-plus: # 支持统配符 * 或者 ; 分割 type-enums-package: org.hong.enums 修改实体 1 private SexEnum sex; 测试 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void testInsert(){ User user = new User(); user.setUserName(\u0026#34;diaochan\u0026#34;); user.setPassword(\u0026#34;123456\u0026#34;); user.setAge(18); user.setName(\u0026#34;貂蝉\u0026#34;); user.setEmail(\u0026#34;diaochan@qq.com\u0026#34;); user.setSex(SexEnum.WOMAN); user.setVersion(1); // 调用AR的insert方法插入数据 boolean result = user.insert(); System.out.println(\u0026#34;result =\u0026gt; \u0026#34; + result); } 控制台打印 MyBatis-Plus 自动的将枚举类型的数据转换成了数字。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] ==\u0026gt; Preparing: INSERT INTO tb_user ( user_name, password, name, age, email, version, sex ) VALUES ( ?, ?, ?, ?, ?, ?, ? ) [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] ==\u0026gt; Parameters: diaochan(String), 123456(String), 貂蝉(String), 18(Integer), diaochan@qq.com(String), 1(Integer), 2(Integer) [main] [org.hong.mapper.UserMapper.insert]-[DEBUG] \u0026lt;== Updates: 1 Time：7 ms - ID：org.hong.mapper.UserMapper.insert Execute SQL： INSERT INTO tb_user ( user_name, password, name, age, email, version, sex ) VALUES ( \u0026#39;diaochan\u0026#39;, \u0026#39;123456\u0026#39;, \u0026#39;貂蝉\u0026#39;, 18, \u0026#39;diaochan@qq.com\u0026#39;, 1, 2 ) [main] [org.mybatis.spring.SqlSessionUtils]-[DEBUG] Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3db663d0] result =\u0026gt; true 序列化枚举值为数据库存储值 jackson 全局处理 编写配置类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.hong.config; import com.fasterxml.jackson.databind.SerializationFeature; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SpringMVCConfig { @Bean public Jackson2ObjectMapperBuilderCustomizer customizer(){ return builder -\u0026gt; builder.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING); } } 在枚举类种重写 toString() 方法，jackson在进行序列化时会返回 toString() 方法返回的内容\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package org.hong.enums; import com.baomidou.mybatisplus.core.enums.IEnum; /** * 实现IEnum\u0026lt;E\u0026gt;接口, 泛型为该枚举实际值的类型 * 并重写getValue方法, 返回枚举对象的实际值 */ public enum SexEnum implements IEnum\u0026lt;Integer\u0026gt; { MAN(1,\u0026#34;男\u0026#34;), WOMAN(2,\u0026#34;女\u0026#34;); final Integer value; final String desc; SexEnum(Integer value, String desc) { this.value = value; this.desc = desc; } /** * 返回枚举的实际值 * @return */ @Override public Integer getValue() { return this.value; } public String getDesc() { return this.desc; } @Override public String toString() { return this.value.toString(); } } 局部处理 使用枚举指定 jackson 序列化的字段\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package org.hong.enums; import com.baomidou.mybatisplus.core.enums.IEnum; import com.fasterxml.jackson.annotation.JsonValue; /** * 实现IEnum\u0026lt;E\u0026gt;接口, 泛型为该枚举实际值的类型 * 并重写getValue方法, 返回枚举对象的实际值 */ public enum SexEnum implements IEnum\u0026lt;Integer\u0026gt; { MAN(1,\u0026#34;男\u0026#34;), WOMAN(2,\u0026#34;女\u0026#34;); @JsonValue // 标记响应json值 final Integer value; final String desc; SexEnum(Integer value, String desc) { this.value = value; this.desc = desc; } /** * 返回枚举的实际值 * @return */ @Override public Integer getValue() { return this.value; } public String getDesc() { return this.desc; } } fastjson 局部处理 在需要序列化的字段上添加注解\n1 2 @JSONField(serialzeFeatures= SerializerFeature.WriteEnumUsingToString) final Integer value; 重写 toString() 方法\n1 2 3 4 @Override public String toString() { return this.value.toString(); } 代码生成器 AutoGenerator 是 MyBatis-Plus 的代码生成器，通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码，极大的提升了开发效率。\n演示效果图：\nMaven 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--mybatis-plus的springboot支持--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.baomidou\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-plus-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 代码生成器 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.baomidou\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-plus-generator\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-freemarker\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--mysql驱动--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.1.47\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.slf4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;slf4j-log4j12\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 package org.hong.generator; import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException; import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.core.toolkit.StringUtils; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.InjectionConfig; import com.baomidou.mybatisplus.generator.config.*; import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine; import java.util.ArrayList; import java.util.List; import java.util.Scanner; // 演示例子，执行 main 方法控制台输入模块表名回车自动生成对应项目目录中 public class CodeGenerator { /** * \u0026lt;p\u0026gt; * 读取控制台内容 * \u0026lt;/p\u0026gt; */ public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append(\u0026#34;请输入\u0026#34; + tip + \u0026#34;：\u0026#34;); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) { return ipt; } } throw new MybatisPlusException(\u0026#34;请输入正确的\u0026#34; + tip + \u0026#34;！\u0026#34;); } public static void main(String[] args) { // 代码生成器 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty(\u0026#34;user.dir\u0026#34;); gc.setOutputDir(projectPath + \u0026#34;/src/main/java\u0026#34;); gc.setAuthor(\u0026#34;jobob\u0026#34;); gc.setOpen(false); // gc.setSwagger2(true); 实体属性 Swagger2 注解 mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl(\u0026#34;jdbc:mysql://localhost:3306/mp?useUnicode=true\u0026amp;useSSL=false\u0026amp;characterEncoding=utf8\u0026#34;); // dsc.setSchemaName(\u0026#34;public\u0026#34;); dsc.setDriverName(\u0026#34;com.mysql.jdbc.Driver\u0026#34;); dsc.setUsername(\u0026#34;root\u0026#34;); dsc.setPassword(\u0026#34;1234\u0026#34;); mpg.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig(); pc.setModuleName(scanner(\u0026#34;模块名\u0026#34;)); pc.setParent(\u0026#34;org.hong\u0026#34;); mpg.setPackageInfo(pc); // 自定义配置 InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; // 如果模板引擎是 freemarker String templatePath = \u0026#34;/templates/mapper.xml.ftl\u0026#34;; // 如果模板引擎是 velocity // String templatePath = \u0026#34;/templates/mapper.xml.vm\u0026#34;; // 自定义输出配置 List\u0026lt;FileOutConfig\u0026gt; focList = new ArrayList\u0026lt;\u0026gt;(); // 自定义配置会被优先输出 focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // 自定义输出文件名 ， 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化！！ return projectPath + \u0026#34;/src/main/resources/mapper/\u0026#34; + pc.getModuleName() + \u0026#34;/\u0026#34; + tableInfo.getEntityName() + \u0026#34;Mapper\u0026#34; + StringPool.DOT_XML; } }); /* cfg.setFileCreate(new IFileCreate() { @Override public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) { // 判断自定义文件夹是否需要创建 checkDir(\u0026#34;调用默认方法创建的目录，自定义目录用\u0026#34;); if (fileType == FileType.MAPPER) { // 已经生成 mapper 文件判断存在，不想重新生成返回 false return !new File(filePath).exists(); } // 允许生成模板文件 return true; } }); */ cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); // 配置模板 TemplateConfig templateConfig = new TemplateConfig(); // 配置自定义输出模板 //指定自定义模板路径，注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别 // templateConfig.setEntity(\u0026#34;templates/entity2.java\u0026#34;); // templateConfig.setService(); // templateConfig.setController(); templateConfig.setXml(null); mpg.setTemplate(templateConfig); // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setColumnNaming(NamingStrategy.underline_to_camel); strategy.setSuperEntityClass(\u0026#34;你自己的父类实体,没有就不用设置!\u0026#34;); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); // 公共父类 strategy.setSuperControllerClass(\u0026#34;你自己的父类控制器,没有就不用设置!\u0026#34;); // 写于父类中的公共字段 strategy.setSuperEntityColumns(\u0026#34;id\u0026#34;); strategy.setInclude(scanner(\u0026#34;表名，多个英文逗号分割\u0026#34;).split(\u0026#34;,\u0026#34;)); strategy.setControllerMappingHyphenStyle(true); strategy.setTablePrefix(pc.getModuleName() + \u0026#34;_\u0026#34;); mpg.setStrategy(strategy); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); mpg.execute(); } } ","permalink":"https://ktzxy.top/posts/wp6b2ya1q1/","summary":"MyBatis Plus","title":"MyBatis Plus"},{"content":"1.group_concat 在我们平常的工作中，使用group by进行分组的场景，是非常多的。\n比如想统计出用户表中，名称不同的用户的具体名称有哪些？\n具体sql如下：\n1 2 select name from `user` group by name; 但如果想把name相同的code拼接在一起，放到另外一列中该怎么办呢？\n答：使用group_concat函数。\n例如：\n1 2 select name,group_concat(code) from `user` group by name; 使用group_concat函数，可以轻松的把分组后，name相同的数据拼接到一起，组成一个字符串，用逗号分隔。\n2.char_length 有时候我们需要获取字符的长度，然后根据字符的长度进行排序。\nMYSQL给我们提供了一些有用的函数，比如：char_length。\n通过该函数就能获取字符长度。\n获取字符长度并且排序的sql如下：\n1 2 select * from brand where name like \u0026#39;%苏三%\u0026#39; order by char_length(name) asc limit 5; 执行效果如图所示：\nname字段使用关键字模糊查询之后，再使用char_length函数获取name字段的字符长度，然后按长度升序。\n3.locate 有时候我们在查找某个关键字，比如：苏三，需要明确知道它在某个字符串中的位置时，该怎么办呢？\n答：使用locate函数。\n使用locate函数改造之后sql如下：\n1 2 select * from brand where name like \u0026#39;%苏三%\u0026#39; order by char_length(name) asc, locate(\u0026#39;苏三\u0026#39;,name) asc limit 5,5; 先按长度排序，小的排在前面。如果长度相同，则按关键字从左到右进行排序，越靠左的越排在前面。\n除此之外，我们还可以使用：instr和position函数，它们的功能跟locate函数类似，在这里我就不一一介绍了，感兴趣的小伙伴可以找我私聊。\n4.replace 我们经常会有替换字符串中部分内容的需求，比如：将字符串中的字符A替换成B。\n这种情况就能使用replace函数。\n例如：\n1 2 update brand set name=REPLACE(name,\u0026#39;A\u0026#39;,\u0026#39;B\u0026#39;) where id=1; 这样就能轻松实现字符替换功能。\n也能用该函数去掉前后空格：\n1 2 update brand set name=REPLACE(name,\u0026#39; \u0026#39;,\u0026#39;\u0026#39;) where name like \u0026#39; %\u0026#39;; update brand set name=REPLACE(name,\u0026#39; \u0026#39;,\u0026#39;\u0026#39;) where name like \u0026#39;% \u0026#39;; 使用该函数还能替换json格式的数据内容，真的非常有用。\n5.now 时间是个好东西，用它可以快速缩小数据范围，我们经常有获取当前时间的需求。\n在MYSQL中获取当前时间，可以使用now()函数，例如：\n1 select now() from brand limit 1; 返回结果为下面这样的： 它会包含年月日时分秒。\n如果你还想返回毫秒，可以使用now(3)，例如：\n1 select now(3) from brand limit 1; 返回结果为下面这样的： 使用起来非常方便好记。\n6.insert into \u0026hellip; select 在工作中很多时候需要插入数据。\n传统的插入数据的sql是这样的：\n1 2 INSERT INTO `brand`(`id`, `code`, `name`, `edit_date`) VALUES (5, \u0026#39;108\u0026#39;, \u0026#39;苏三\u0026#39;, \u0026#39;2022-09-02 19:42:21\u0026#39;); 它主要是用于插入少量并且已经确定的数据。但如果有大批量的数据需要插入，特别是是需要插入的数据来源于，另外一张表或者多张表的结果集中。\n这种情况下，使用传统的插入数据的方式，就有点束手无策了。\n这时候就能使用MYSQL提供的：insert into ... select语法。\n例如：\n1 2 INSERT INTO `brand`(`id`, `code`, `name`, `edit_date`) select null,code,name,now(3) from `order` where code in (\u0026#39;004\u0026#39;,\u0026#39;005\u0026#39;); 这样就能将order表中的部分数据，非常轻松插入到brand表中。\n7.insert into \u0026hellip; ignore 不知道你有没有遇到过这样的场景：在插入1000个品牌之前，需要先根据name，判断一下是否存在。如果存在，则不插入数据。如果不存在，才需要插入数据。\n如果直接这样插入数据：\n1 2 INSERT INTO `brand`(`id`, `code`, `name`, `edit_date`) VALUES (123, \u0026#39;108\u0026#39;, \u0026#39;苏三\u0026#39;, now(3)); 肯定不行，因为brand表的name字段创建了唯一索引，同时该表中已经有一条name等于苏三的数据了。\n这就需要在插入之前加一下判断。\n当然很多人通过在sql语句后面拼接not exists语句，也能达到防止出现重复数据的目的，比如：\n1 2 3 INSERT INTO `brand`(`id`, `code`, `name`, `edit_date`) select null,\u0026#39;108\u0026#39;, \u0026#39;苏三\u0026#39;,now(3) from dual where not exists (select * from `brand` where name=\u0026#39;苏三\u0026#39;); 这条sql确实能够满足要求，但是总觉得有些麻烦。那么，有没有更简单的做法呢？\n答：可以使用insert into ... ignore语法。\n例如：\n1 2 INSERT ignore INTO `brand`(`id`, `code`, `name`, `edit_date`) VALUES (123, \u0026#39;108\u0026#39;, \u0026#39;苏三\u0026#39;, now(3)); 这样改造之后，如果brand表中没有name为苏三的数据，则可以直接插入成功。\n但如果brand表中已经存在name为苏三的数据了，则该sql语句也能正常执行，并不会报错。因为它会忽略异常，返回的执行结果影响行数为0，它不会重复插入数据。\n8.select \u0026hellip; for update MYSQL数据库自带了悲观锁，它是一种排它锁，根据锁的粒度从大到小分为：表锁、间隙锁和行锁。\n在我们的实际业务场景中，有些情况并发量不太高，为了保证数据的正确性，使用悲观锁也可以。\n比如：用户扣减积分，用户的操作并不集中。但也要考虑系统自动赠送积分的并发情况，所以有必要加悲观锁限制一下，防止出现积分加错的情况发生。\n这时候就可以使用MYSQL中的select ... for update语法了。\n例如：\n1 2 3 4 5 6 7 8 begin; select * from `user` where id=1 for update; //业务逻辑处理 update `user` set score=score-1 where id=1; commit; 这样在一个事务中使用for update锁住一行记录，其他事务就不能在该事务提交之前，去更新那一行的数据。\n需要注意的是for update前的id条件，必须是表的主键或者唯一索引，不然行锁可能会失效，有可能变成表锁。\n9.on duplicate key update 通常情况下，我们在插入数据之前，一般会先查询一下，该数据是否存在。如果不存在，则插入数据。如果已存在，则不插入数据，而直接返回结果。\n在没啥并发量的场景中，这种做法是没有什么问题的。但如果插入数据的请求，有一定的并发量，这种做法就可能会产生重复的数据。\n当然防止重复数据的做法很多，比如：加唯一索引、加分布式锁等。\n但这些方案，都没法做到让第二次请求也更新数据，它们一般会判断已经存在就直接返回了。\n这种情况可以使用on duplicate key update语法。\n该语法会在插入数据之前判断，如果主键或唯一索引不存在，则插入数据。如果主键或唯一索引存在，则执行更新操作。\n具体需要更新的字段可以指定，例如：\n1 2 3 INSERT INTO `brand`(`id`, `code`, `name`, `edit_date`) VALUES (123, \u0026#39;108\u0026#39;, \u0026#39;苏三\u0026#39;, now(3)) on duplicate key update name=\u0026#39;苏三\u0026#39;,edit_date=now(3); 这样一条语句就能轻松搞定需求，既不会产生重复数据，也能更新最新的数据。\n但需要注意的是，在高并发的场景下使用on duplicate key update语法，可能会存在死锁的问题，所以要根据实际情况酌情使用。\n10.show create table 有时候，我们想快速查看某张表的字段情况，通常会使用desc命令，比如：\n1 desc `order`; 结果如图所示： 确实能够看到order表中的字段名称、字段类型、字段长度、是否允许为空，是否主键、默认值等信息。\n但看不到该表的索引信息，如果想看创建了哪些索引，该怎么办呢？\n答：使用show index命令。\n比如：\n1 show index from `order`; 也能查出该表所有的索引： 但查看字段和索引数据呈现方式，总觉得有点怪怪的，有没有一种更直观的方式？\n答：这就需要使用show create table命令了。\n例如：\n1 show create table `order`; 执行结果如图所示： 其中Table表示表名，Create Table就是我们需要看的建表信息，将数据展开： 我们能够看到非常完整的建表语句，表名、字段名、字段类型、字段长度、字符集、主键、索引、执行引擎等都能看到。\n非常直接明了。\n11.create table \u0026hellip; select 有时候，我们需要快速备份表。\n通常情况下，可以分两步走：\n创建一张临时表 将数据插入临时表 创建临时表可以使用命令：\n1 create table order_2022121819 like `order`; 创建成功之后，就会生成一张名称叫：order_2022121819，表结构跟order一模一样的新表，只是该表的数据为空而已。\n接下来使用命令：\n1 insert into order_2022121819 select * from `order`; 执行之后就会将order表的数据插入到order_2022121819表中，也就是实现数据备份的功能。\n但有没有命令，一个命令就能实现上面这两步的功能呢？\n答：用create table ... select命令。\n例如：\n1 2 create table order_2022121820 select * from `order`; 执行完之后，就会将order_2022121820表创建好，并且将order表中的数据自动插入到新创建的order_2022121820中。\n一个命令就能轻松搞定表备份。\n12.explain 很多时候，我们优化一条sql语句的性能，需要查看索引执行情况。\n答：可以使用explain命令，查看mysql的执行计划，它会显示索引的使用情况。\n例如：\n1 explain select * from `order` where code=\u0026#39;002\u0026#39;; 结果：\n通过这几列可以判断索引使用情况，执行计划包含列的含义如下图所示： 说实话，sql语句没有走索引，排除没有建索引之外，最大的可能性是索引失效了。\n下面说说索引失效的常见原因： 如果不是上面的这些原因，则需要再进一步排查一下其他原因。\n13.show processlist 有些时候我们线上sql或者数据库出现了问题。比如出现了数据库连接过多问题，或者发现有一条sql语句的执行时间特别长。\n这时候该怎么办呢？\n答：我们可以使用show processlist命令查看当前线程执行情况。\n如图所示： 从执行结果中，我们可以查看当前的连接状态，帮助识别出有问题的查询语句。\nid 线程id User 执行sql的账号 Host 执行sql的数据库的ip和端号 db 数据库名称 Command 执行命令，包括：Daemon、Query、Sleep等。 Time 执行sql所消耗的时间 State 执行状态 info 执行信息，里面可能包含sql信息。 如果发现了异常的sql语句，可以直接kill掉，确保数据库不会出现严重的问题。\n14.mysqldump 有时候我们需要导出MYSQL表中的数据。\n这种情况就可以使用mysqldump工具，该工具会将数据查出来，转换成insert语句，写入到某个文件中，相当于数据备份。\n我们获取到该文件，然后执行相应的insert语句，就能创建相关的表，并且写入数据了，这就相当于数据还原。\nmysqldump命令的语法为：mysqldump -h主机名 -P端口 -u用户名 -p密码 参数1,参数2.... \u0026gt; 文件名称.sql\n备份远程数据库中的数据库：\n1 mysqldump -h 192.22.25.226 -u root -p123456 dbname \u0026gt; backup.sql ","permalink":"https://ktzxy.top/posts/c6wwqur40z/","summary":"MySQL 的 14个 小技巧！","title":"MySQL 的 14个 小技巧！"},{"content":"Go Context的使用 在 Go http包的Server中，每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务，比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据，比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时，所有用来处理该请求的 goroutine 都应该迅速退出，然后系统才能释放这些 goroutine 占用的资源。\n来源 https://www.liwenzhou.com/posts/Go/go_context/\n为什么需要Context 基本示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) var wg sync.WaitGroup // 初始的例子 func worker() { for { fmt.Println(\u0026#34;worker\u0026#34;) time.Sleep(time.Second) } // 如何接收外部命令实现退出 wg.Done() } func main() { wg.Add(1) go worker() // 如何优雅的实现结束子goroutine wg.Wait() fmt.Println(\u0026#34;over\u0026#34;) } 全局变量方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) var wg sync.WaitGroup var exit bool // 全局变量方式存在的问题： // 1. 使用全局变量在跨包调用时不容易统一 // 2. 如果worker中再启动goroutine，就不太好控制了。 func worker() { for { fmt.Println(\u0026#34;worker\u0026#34;) time.Sleep(time.Second) if exit { break } } wg.Done() } func main() { wg.Add(1) go worker() time.Sleep(time.Second * 3) // sleep3秒以免程序过快退出 exit = true // 修改全局变量实现子goroutine的退出 wg.Wait() fmt.Println(\u0026#34;over\u0026#34;) } 通道方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) var wg sync.WaitGroup // 管道方式存在的问题： // 1. 使用全局变量在跨包调用时不容易实现规范和统一，需要维护一个共用的channel func worker(exitChan chan struct{}) { LOOP: for { fmt.Println(\u0026#34;worker\u0026#34;) time.Sleep(time.Second) select { case \u0026lt;-exitChan: // 等待接收上级通知 break LOOP default: } } wg.Done() } func main() { var exitChan = make(chan struct{}) wg.Add(1) go worker(exitChan) time.Sleep(time.Second * 3) // sleep3秒以免程序过快退出 exitChan \u0026lt;- struct{}{} // 给子goroutine发送退出信号 close(exitChan) wg.Wait() fmt.Println(\u0026#34;over\u0026#34;) } 官方版的方案 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) var wg sync.WaitGroup func worker(ctx context.Context) { LOOP: for { fmt.Println(\u0026#34;worker\u0026#34;) time.Sleep(time.Second) select { case \u0026lt;-ctx.Done(): // 等待上级通知 break LOOP default: } } wg.Done() } func main() { ctx, cancel := context.WithCancel(context.Background()) wg.Add(1) go worker(ctx) time.Sleep(time.Second * 3) cancel() // 通知子goroutine结束 wg.Wait() fmt.Println(\u0026#34;over\u0026#34;) } 当子goroutine又开启另外一个goroutine时，只需要将ctx传入即可：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) var wg sync.WaitGroup func worker(ctx context.Context) { go worker2(ctx) LOOP: for { fmt.Println(\u0026#34;worker\u0026#34;) time.Sleep(time.Second) select { case \u0026lt;-ctx.Done(): // 等待上级通知 break LOOP default: } } wg.Done() } func worker2(ctx context.Context) { LOOP: for { fmt.Println(\u0026#34;worker2\u0026#34;) time.Sleep(time.Second) select { case \u0026lt;-ctx.Done(): // 等待上级通知 break LOOP default: } } } func main() { ctx, cancel := context.WithCancel(context.Background()) wg.Add(1) go worker(ctx) time.Sleep(time.Second * 3) cancel() // 通知子goroutine结束 wg.Wait() fmt.Println(\u0026#34;over\u0026#34;) } Context初识 Go1.7加入了一个新的标准库context，它定义了Context类型，专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作，这些操作可能涉及多个 API 调用。\n对服务器传入的请求应该创建上下文，而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文，或者可以使用WithCancel、WithDeadline、WithTimeout或WithValue创建的派生上下文。当一个上下文被取消时，它派生的所有上下文也被取消。\nContext接口 context.Context是一个接口，该接口定义了四个需要实现的方法。具体签名如下：\n1 2 3 4 5 6 type Context interface { Deadline() (deadline time.Time, ok bool) Done() \u0026lt;-chan struct{} Err() error Value(key interface{}) interface{} } 其中：\nDeadline方法需要返回当前Context被取消的时间，也就是完成工作的截止时间（deadline）；\nDone方法需要返回一个Channel，这个Channel会在当前工作完成或者上下文被取消之后关闭，多次调用Done方法会返回同一个Channel；\n1 Err 方法会返回当前\n1 Context 结束的原因，它只会在\n1 Done 返回的Channel被关闭时才会返回非空的值；\n如果当前Context被取消就会返回Canceled错误； 如果当前Context超时就会返回DeadlineExceeded错误； Value方法会从Context中返回键对应的值，对于同一个上下文来说，多次调用Value 并传入相同的Key会返回相同的结果，该方法仅用于传递跨API和进程间跟请求域的数据；\nBackground()和TODO() Go内置两个函数：Background()和TODO()，这两个函数分别返回一个实现了Context接口的background和todo。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context，衍生出更多的子上下文对象。\nBackground()主要用于main函数、初始化以及测试代码中，作为Context这个树结构的最顶层的Context，也就是根Context。\nTODO()，它目前还不知道具体的使用场景，如果我们不知道该使用什么Context的时候，可以使用这个。\nbackground和todo本质上都是emptyCtx结构体类型，是一个不可取消，没有设置截止时间，没有携带任何值的Context。\nWith系列函数 此外，context包中还定义了四个With系列函数。\nWithCancel WithCancel的函数签名如下：\n1 func WithCancel(parent Context) (ctx Context, cancel CancelFunc) WithCancel返回带有新Done通道的父节点的副本。当调用返回的cancel函数或当关闭父上下文的Done通道时，将关闭返回上下文的Done通道，无论先发生什么情况。\n取消此上下文将释放与其关联的资源，因此代码应该在此上下文中运行的操作完成后立即调用cancel。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func gen(ctx context.Context) \u0026lt;-chan int { dst := make(chan int) n := 1 go func() { for { select { case \u0026lt;-ctx.Done(): return // return结束该goroutine，防止泄露 case dst \u0026lt;- n: n++ } } }() return dst } func main() { // context.Background：传递的是根节点 ctx, cancel := context.WithCancel(context.Background()) defer cancel() // 当我们取完需要的整数后调用cancel，相当于向ctx里面添加值 // 遍历chan for n := range gen(ctx) { fmt.Println(n) if n == 5 { break } } } 上面的示例代码中，gen函数在单独的goroutine中生成整数并将它们发送到返回的通道。 gen的调用者在使用生成的整数之后需要取消上下文，以免gen启动的内部goroutine发生泄漏。\nWithDeadline WithDeadline的函数签名如下：\n1 func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) 返回父上下文的副本，并将deadline调整为不迟于d。如果父上下文的deadline已经早于d，则WithDeadline(parent, d)在语义上等同于父上下文。当截止日过期时，当调用返回的cancel函数时，或者当父上下文的Done通道关闭时，返回上下文的Done通道将被关闭，以最先发生的情况为准。\n取消此上下文将释放与其关联的资源，因此代码应该在此上下文中运行的操作完成后立即调用cancel。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func main() { // 设置过期时间50毫秒 d := time.Now().Add(50 * time.Millisecond) ctx, cancel := context.WithDeadline(context.Background(), d) // 尽管ctx会过期，但在任何情况下调用它的cancel函数都是很好的实践。 // 如果不这样做，可能会使上下文及其父类存活的时间超过必要的时间。 defer cancel() select { case \u0026lt;-time.After(1 * time.Second): fmt.Println(\u0026#34;overslept\u0026#34;) case \u0026lt;-ctx.Done(): fmt.Println(ctx.Err()) } } 上面的代码中，定义了一个50毫秒之后过期的deadline，然后我们调用context.WithDeadline(context.Background(), d)得到一个上下文（ctx）和一个取消函数（cancel），然后使用一个select让主程序陷入等待：等待1秒后打印overslept退出或者等待ctx过期后退出。 因为ctx50秒后就过期，所以ctx.Done()会先接收到值，上面的代码会打印ctx.Err()取消原因。\nWithTimeout WithTimeout的函数签名如下：\n1 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) WithTimeout返回WithDeadline(parent, time.Now().Add(timeout))。\n取消此上下文将释放与其相关的资源，因此代码应该在此上下文中运行的操作完成后立即调用cancel，通常用于数据库或者网络连接的超时控制。具体示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) // context.WithTimeout var wg sync.WaitGroup func worker(ctx context.Context) { LOOP: for { fmt.Println(\u0026#34;db connecting ...\u0026#34;) time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒 select { case \u0026lt;-ctx.Done(): // 50毫秒后自动调用 break LOOP default: } } fmt.Println(\u0026#34;worker done!\u0026#34;) wg.Done() } func main() { // 设置一个50毫秒的超时 ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) wg.Add(1) go worker(ctx) time.Sleep(time.Second * 5) cancel() // 通知子goroutine结束 wg.Wait() fmt.Println(\u0026#34;over\u0026#34;) } WithValue WithValue函数能够将请求作用域的数据与 Context 对象建立关系。声明如下：\n1 func WithValue(parent Context, key, val interface{}) Context WithValue返回父节点的副本，其中与key关联的值为val。\n仅对API和进程间传递请求域的数据使用上下文值，而不是使用它来传递可选参数给函数。\n所提供的键必须是可比较的，并且不应该是string类型或任何其他内置类型，以避免使用上下文在包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在分配给interface{}时进行分配，上下文键通常具有具体类型struct{}。或者，导出的上下文关键变量的静态类型应该是指针或接口。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) // context.WithValue type TraceCode string var wg sync.WaitGroup func worker(ctx context.Context) { key := TraceCode(\u0026#34;TRACE_CODE\u0026#34;) // .(string) 是类型断言 traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code if !ok { fmt.Println(\u0026#34;invalid trace code\u0026#34;) } LOOP: for { fmt.Printf(\u0026#34;worker, trace code:%s\\n\u0026#34;, traceCode) time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒 select { case \u0026lt;-ctx.Done(): // 50毫秒后自动调用 break LOOP default: } } fmt.Println(\u0026#34;worker done!\u0026#34;) wg.Done() } func main() { // 设置一个50毫秒的超时 ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50) // 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合 ctx = context.WithValue(ctx, TraceCode(\u0026#34;TRACE_CODE\u0026#34;), \u0026#34;12512312234\u0026#34;) wg.Add(1) go worker(ctx) time.Sleep(time.Second * 5) cancel() // 通知子goroutine结束 wg.Wait() fmt.Println(\u0026#34;over\u0026#34;) } 使用Context的注意事项 推荐以参数的方式显示传递Context 以Context作为参数的函数方法，应该把Context作为第一个参数。 给一个函数方法传递Context的时候，不要传递nil，如果不知道传递什么，就使用context.TODO() Context的Value相关方法应该传递请求域的必要数据，不应该用于传递可选参数 Context是线程安全的，可以放心的在多个goroutine中传递 客户端超时取消示例 调用服务端API时如何在客户端实现超时控制？\nserver端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // context_timeout/server/main.go package main import ( \u0026#34;fmt\u0026#34; \u0026#34;math/rand\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;time\u0026#34; ) // server端，随机出现慢响应 func indexHandler(w http.ResponseWriter, r *http.Request) { number := rand.Intn(2) if number == 0 { time.Sleep(time.Second * 10) // 耗时10秒的慢响应 fmt.Fprintf(w, \u0026#34;slow response\u0026#34;) return } fmt.Fprint(w, \u0026#34;quick response\u0026#34;) } func main() { http.HandleFunc(\u0026#34;/\u0026#34;, indexHandler) err := http.ListenAndServe(\u0026#34;:8000\u0026#34;, nil) if err != nil { panic(err) } } client端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 // context_timeout/client/main.go package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;io/ioutil\u0026#34; \u0026#34;net/http\u0026#34; \u0026#34;sync\u0026#34; \u0026#34;time\u0026#34; ) // 客户端 type respData struct { resp *http.Response err error } func doCall(ctx context.Context) { transport := http.Transport{ // 请求频繁可定义全局的client对象并启用长链接 // 请求不频繁使用短链接 DisableKeepAlives: true, } client := http.Client{ Transport: \u0026amp;transport, } respChan := make(chan *respData, 1) req, err := http.NewRequest(\u0026#34;GET\u0026#34;, \u0026#34;http://127.0.0.1:8000/\u0026#34;, nil) if err != nil { fmt.Printf(\u0026#34;new requestg failed, err:%v\\n\u0026#34;, err) return } req = req.WithContext(ctx) // 使用带超时的ctx创建一个新的client request var wg sync.WaitGroup wg.Add(1) defer wg.Wait() go func() { resp, err := client.Do(req) fmt.Printf(\u0026#34;client.do resp:%v, err:%v\\n\u0026#34;, resp, err) rd := \u0026amp;respData{ resp: resp, err: err, } respChan \u0026lt;- rd wg.Done() }() select { case \u0026lt;-ctx.Done(): //transport.CancelRequest(req) fmt.Println(\u0026#34;call api timeout\u0026#34;) case result := \u0026lt;-respChan: fmt.Println(\u0026#34;call server api success\u0026#34;) if result.err != nil { fmt.Printf(\u0026#34;call server api failed, err:%v\\n\u0026#34;, result.err) return } defer result.resp.Body.Close() data, _ := ioutil.ReadAll(result.resp.Body) fmt.Printf(\u0026#34;resp:%v\\n\u0026#34;, string(data)) } } func main() { // 定义一个100毫秒的超时 ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) defer cancel() // 调用cancel释放子goroutine资源 doCall(ctx) } ","permalink":"https://ktzxy.top/posts/vbauji3p3n/","summary":"GoContext的使用","title":"GoContext的使用"},{"content":"一、概述 在之前的文章《企业级监控平台如何选择？》中，我们了解到目前社区活跃度比较广，互联网大厂倾向的监控平台Promethes，今天我们就实战搭建一个企业级的监控平台，用来监控服务器、MySQL数据库、Redis、Docker等。\n需要搭建的环境有：\nDocker-用来部署各个软件的基础环境 Prometheus-时序数据库 Grafana-大屏风格的Web可视化监控方案 AlertManager-配置告警选项 webhook-实现钉钉告警 二、Centos部署Docker环境 2.1 方式一：yum方式安装 1 2 3 4 5 6 7 8 9 10 11 # 更新yum源 yum update # 安装所需环境 yum install -y yum-utils device-mapper-persistent-data lvm2 # 配置yum仓库 yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo # 安装Docker yum install docker-ce # 启动Docker systemctl start docker systemctl enable docker 2.2 方式二：（推荐安装方式） 1 curl -sSL https://get.daocloud.io/docker | sh 2.3 离线安装 下载docker的安装文件：https://download.docker.com/linux/static/stable/x86_64/\n将下载后的tgz文件传至服务器，通过FTP工具上传即可\n解压tar -zxvf docker-19.03.8-ce.tgz\n将解压出来的docker文件复制到 /usr/bin/ 目录下：cp docker/* /usr/bin/\n进入**/etc/systemd/system/目录,并创建docker.service**文件\n1 2 [root@localhost java]# cd /etc/systemd/system/ [root@localhost system]# touch docker.service 打开docker.service文件,将以下内容复制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 [Unit] Description=Docker Application Container Engine Documentation=https://docs.docker.com After=network-online.target firewalld.service Wants=network-online.target [Service] Type=notify # the default is not to use systemd for cgroups because the delegate issues still # exists and systemd currently does not support the cgroup feature set required # for containers run by docker ExecStart=/usr/bin/dockerd --selinux-enabled=false --insecure-registry=192.168.200.128 ExecReload=/bin/kill -s HUP $MAINPID # Having non-zero Limit*s causes performance problems due to accounting overhead # in the kernel. We recommend using cgroups to do container-local accounting. LimitNOFILE=infinity LimitNPROC=infinity LimitCORE=infinity # Uncomment TasksMax if your systemd version supports it. # Only systemd 226 and above support this version. #TasksMax=infinity TimeoutStartSec=0 # set delegate yes so that systemd does not reset the cgroups of docker containers Delegate=yes # kill only the docker process, not all processes in the cgroup KillMode=process # restart the docker process if it exits prematurely Restart=on-failure StartLimitBurst=3 StartLimitInterval=60s [Install] WantedBy=multi-user.target 给docker.service文件添加执行权限：chmod 777 /etc/systemd/system/docker.service 重新加载配置文件：systemctl daemon-reload 启动Docker systemctl start docker\n设置开机启动：systemctl enable docker.service\n查看Docker状态：systemctl status docker\n如出现如图界面，则表示安装成功！\n三、Docker部署监控平台 3.1 Docker部署Grafana 1 docker run -d --restart=always --name=grafana -p 3000:3000 grafana/grafana -d：后台启动 \u0026ndash;restart=always：重启 -p：端口映射 grafana/grafana：后面没有指定版本号，默认拉取latest版本，如需其他版本，指定相应版本即可 搭建完成后，在浏览器访问：http://localhost:3000\n默认账号密码：admin，admin\n第一次登陆后，会提示修改密码，按照要求修改密码即可。\n3.2 Docker部署Prometheus 创建一个文件夹用来存放Prometheus的配置文件 1 mkdir -p /var/project/prometheus 编写prometheus.yml配置文件 1 2 3 4 5 6 7 8 # 全局配置 global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: \u0026#39;prometheus监控\u0026#39; static_configs: - targets: [\u0026#39;127.0.0.1:9090\u0026#39;] 启动Prometheus 1 docker run -d --restart=always --name=prometheus -p 9090:9090 -v /etc/localtime:/etc/localtime:ro -v /data/prometheus:/etc/prometheus prom/prometheus -d：后台启动 \u0026ndash;restart=always：重启 -p：端口映射 -v /etc/localtime:/etc/localtime:ro：同步本地时间和容器时间一致 -v /var/project/prometheus:/etc/prometheus：将容器内的prometheus配置文件路径映射在容器外部 prom/prometheus：后面没有指定版本号，默认拉取latest版本，如需其他版本，指定相应版本即可 浏览器访问验证：http://localhost:9090 出现如图所示界面，则表示安装成功！\n3.3 Docker部署Webhook 此处我使用的告警方式是钉钉群主告警，故需添加自定义钉钉机器人实现告警。\n创建一个自定义的钉钉机器人，创建一个钉钉群，找到只能群助手\n添加一个机器人\n选择自定义Webhook机器人\n添加相关信息\n完成后，出现如下界面，复制Webhook。\nDocker部署Webhook钉钉\n1 docker run -d --restart=always -p 8060:8060 --name webhook timonwong/prometheus-webhook --ding.profile=\u0026#34;webhook1=https://oapi.dingtalk.com/robot/send?access_token={替换成自己的dingding token} -d：后台启动 \u0026ndash;restart=always：重启 -p：端口映射 \u0026ndash;ding.profile：配置webhook 3.4 Docker部署AlertManager 创建一个文件夹用来存放AlertManager的配置文件 1 mkdir -p /var/project/alertmanager 编辑alertmanager.yml配置文件 1 vi alertmanager.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 global: resolve_timeout: 5m route: receiver: webhook group_wait: 30s group_interval: 5m repeat_interval: 5m group_by: [alertname] routes: - receiver: webhook group_wait: 10s receivers: - name: webhook webhook_configs: - url: http://127.0.0.1:8060/dingtalk/webhook1/send send_resolved: true 启动AlertManager 1 docker run -d --restart=always --name alertmanager -p 9093:9093 -v /var/project/prometheus:/etc/alertmanager prom/alertmanager:latest -d：后台启动\n\u0026ndash;restart=always：重启\n-p：端口映射\n-v /var/project/prometheus:/etc/alertmanager：将容器内的配置文件路径映射在容器外部\n二、配置文件 2.1 prometheus.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 # 全局配置 global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: # 监控prometheus本身 - job_name: \u0026#39;服务器Prometheus\u0026#39; static_configs: - targets: [\u0026#39;127.0.0.1:9090\u0026#39;] # 通过node_exporter将监控数据传给prometheus，如果要监控多台服务器，只要在每个服务器上安装node_exporter，指定不同多ip地址就好了 - job_name: \u0026#39;Linux服务器监控\u0026#39; file_sd_configs: - refresh_interval: 1m files: - \u0026#34;/etc/prometheus/node_exporter.yml\u0026#34; # 监控mysql - job_name: \u0026#39;MySql实例监控\u0026#39; static_configs: - targets: [\u0026#39;127.0.0.1:9104\u0026#39;] # 监控Docker #- job_name: \u0026#39;Docker实例监控\u0026#39; # file_sd_configs: # - refresh_interval: 1m # files: # - \u0026#34;/var/project/prometheus/docker_exporter.yml\u0026#34; #监控Java，SpringBoot应用 - job_name: \u0026#39;SpringBoot应用监控\u0026#39; metrics_path: \u0026#39;/actuator/prometheus\u0026#39; file_sd_configs: - refresh_interval: 1m files: - \u0026#34;/etc/prometheus/springboot_exporter.yml\u0026#34; #监控Redis集群 - job_name: \u0026#39;Redis集群实例监控\u0026#39; static_configs: - targets: - redis://127.0.0.1:7000 - redis://127.0.0.1:7001 - redis://127.0.0.1:7002 - redis://127.0.0.1:7003 - redis://127.0.0.1:7004 - redis://127.0.0.1:7005 metrics_path: /scrape relabel_configs: - source_labels: [__address__] target_label: __param_target - source_labels: [__param_target] target_label: instance - target_label: __address__ replacement: 127.0.0.1:9121 - job_name: http-blackbox metrics_path: /probe params: module: [http_2xx] static_configs: - targets: [\u0026#39;www.baidu.com\u0026#39;] labels: instance: 百度 group: \u0026#39;web\u0026#39; relabel_configs: - source_labels: [__address__] target_label: __param_target - target_label: __address__ replacement: 127.0.0.1:9115 # Alertting告警平台 alerting: alertmanagers: - static_configs: - targets: - 127.0.0.1:9093 rule_files: - \u0026#34;/etc/prometheus/rules/node_down.yml\u0026#34; # 实例存活报警规则文件 - \u0026#34;/etc/prometheus/rules/memory_over.yml\u0026#34; # 内存报警规则文件 - \u0026#34;/etc/prometheus/rules/cpu_over.yml\u0026#34; # cpu报警规则文件 - \u0026#34;/etc/prometheus/rules/black_exporter.yml\u0026#34; # 黑盒监测 2.2 black_exporter.yml 1 2 3 4 5 6 7 8 9 10 11 12 groups: - name: 网站URL告警规则 rules: - alert: 网站URL存活告警 # 这里我后续想了下还是换成 联通性为离线好点 expr: probe_http_status_code{job=\u0026#34;http-blackbox\u0026#34;}\u0026gt;=399 or probe_success{job=\u0026#34;http-blackbox\u0026#34;}==0 for: 1m labels: user: prometheus severity: warning annotations: description: \u0026#34;{{ $labels.instance }} of job {{ $labels.job }} {{ $value }} 网页无法连接\u0026#34; 2.3 blackbox-exporter：config.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 modules: http_2xx: # http 监测模块 prober: http http_post_2xx: # http post 监测模块 prober: http http: method: POST tcp_connect: # tcp 监测模块 prober: tcp pop3s_banner: prober: tcp tcp: query_response: - expect: \u0026#34;^+OK\u0026#34; tls: true tls_config: insecure_skip_verify: false ssh_banner: prober: tcp tcp: query_response: - expect: \u0026#34;^SSH-2.0-\u0026#34; irc_banner: prober: tcp tcp: query_response: - send: \u0026#34;NICK prober\u0026#34; - send: \u0026#34;USER prober prober prober :prober\u0026#34; - expect: \u0026#34;PING :([^ ]+)\u0026#34; send: \u0026#34;PONG ${1}\u0026#34; - expect: \u0026#34;^:[^ ]+ 001\u0026#34; icmp: prober: icmp 2.4 alertmanager.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 global: resolve_timeout: 5m #在没有报警的情况下声明为已解决的时间 route: # route用来设置报警的分发策略 receiver: webhook # 设置默认接收人 group_wait: 60m # 组告警等待时间。也就是告警产生后等待10s，如果有同组告警一起发出 group_interval: 60m # 两组告警的间隔时间 repeat_interval: 60m # 重复告警的间隔时间，减少相同邮件的发送频率 group_by: [alertname] routes: - receiver: webhook group_wait: 10s receivers: - name: webhook webhook_configs: - url: http://10.19.64.63:8060/dingtalk/webhook1/send send_resolved: true 2.5 node_exporter.yml 1 2 3 4 5 6 7 8 - targets: - \u0026#34;127.0.0.1:9100\u0026#34; labels: instance: 服务器A - targets: - \u0026#34;127.0.0.1:9101\u0026#34; labels: instance: 服务器B 2.6 springboot_exporter.yml 1 2 3 4 5 6 7 8 - targets: - \u0026#34;127.0.0.1:8085\u0026#34; labels: instance: 服务A - targets: - \u0026#34;127.0.0.1:8086\u0026#34; labels: instance: 服务B 三、rules 3.1 black_exporter.yml 1 2 3 4 5 6 7 8 9 10 11 12 groups: - name: 网站URL告警规则 rules: - alert: 网站URL存活告警 # 这里我后续想了下还是换成 联通性为离线好点 expr: probe_http_status_code{job=\u0026#34;http-blackbox\u0026#34;}\u0026gt;=399 or probe_success{job=\u0026#34;http-blackbox\u0026#34;}==0 for: 1m labels: user: prometheus severity: warning annotations: description: \u0026#34;{{ $labels.instance }} of job {{ $labels.job }} {{ $value }} 网页无法连接\u0026#34; 3.2 cpu_over.yml 1 2 3 4 5 6 7 8 9 10 11 groups: - name: CPU报警规则 rules: - alert: CPU使用率告警 expr: 100 - (avg by (instance)(irate(node_cpu_seconds_total{mode=\u0026#34;idle\u0026#34;}[1m]) )) * 100 \u0026gt; 90 for: 1m labels: user: prometheus severity: warning annotations: description: \u0026#34;服务器: CPU使用超过90%！(当前值: {{ $value }}%)\u0026#34; 3.3 memory_over.yml 1 2 3 4 5 6 7 8 9 10 11 groups: - name: 内存报警规则 rules: - alert: 内存使用率告警 expr: (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes+node_memory_Buffers_bytes+node_memory_Cached_bytes )) / node_memory_MemTotal_bytes * 100 \u0026gt; 90 for: 1m labels: user: prometheus severity: warning annotations: description: \u0026#34;服务器: 内存使用超过90%！(当前值: {{ $value }}%)\u0026#34; 3.4 node_down.yml 1 2 3 4 5 6 7 8 9 10 11 groups: - name: 实例存活告警规则 rules: - alert: 实例存活告警 expr: up == 0 for: 1m labels: user: prometheus severity: warning annotations: description: \u0026#34;{{ $labels.instance }} of job {{ $labels.job }} 已经停止1分钟.\u0026#34; ","permalink":"https://ktzxy.top/posts/xxtdm5olyk/","summary":"Docker搭建Prometheus+Grafana+Alertmanager监控告警平台","title":"Docker搭建Prometheus+Grafana+Alertmanager监控告警平台"},{"content":"Go的依赖管理Go Module 来源 https://www.liwenzhou.com/posts/Go/go_dependency/\n历史遗留问题 如果是使用Go1.11和Go1.12版本，需要手动开启Go module支持\n为什么需要依赖管理 最早的时候，Go所依赖的所有的第三方库都放在GOPATH这个目录下面。这就导致了同一个库只能保存一个版本的代码。如果不同的项目依赖同一个第三方的库的不同版本，应该怎么解决？\ngodep Go语言从v1.5开始开始引入vendor模式，如果项目目录下有vendor目录，那么go工具链会优先使用vendor内的包进行编译、测试等。\ngodep是一个通过vender模式实现的Go语言的第三方依赖管理工具，类似的还有由社区维护准官方包管理工具dep。\n安装 执行以下命令安装godep工具。\n1 go get github.com/tools/godep 基本命令 安装好godep之后，在终端输入godep查看支持的所有命令。\n1 2 3 4 5 6 7 8 godep save 将依赖项输出并复制到Godeps.json文件中 godep go 使用保存的依赖项运行go工具 godep get 下载并安装具有指定依赖项的包 godep path 打印依赖的GOPATH路径 godep restore 在GOPATH中拉取依赖的版本 godep update 更新选定的包或go版本 godep diff 显示当前和以前保存的依赖项集之间的差异 godep version 查看版本信息 使用godep help [command]可以看看具体命令的帮助信息。\n使用godep 在项目目录下执行godep save命令，会在当前项目中创建Godeps和vender两个文件夹。\n其中Godeps文件夹下有一个Godeps.json的文件，里面记录了项目所依赖的包信息。 vender文件夹下是项目依赖的包的源代码文件。\nvender机制 Go1.5版本之后开始支持，能够控制Go语言程序编译时依赖包搜索路径的优先级。\n例如查找项目的某个依赖包，首先会在项目根目录下的vender文件夹中查找，如果没有找到就会去$GOAPTH/src目录下查找。\ngodep开发流程 保证程序能够正常编译 执行godep save保存当前项目的所有第三方依赖的版本信息和代码 提交Godeps目录和vender目录到代码库。 如果要更新依赖的版本，可以直接修改Godeps.json文件中的对应项 go module go module是Go1.11版本之后官方推出的版本管理工具，并且从Go1.13版本开始，go module将是Go语言默认的依赖管理工具。\nGO111MODULE 要启用go module支持首先要设置环境变量GO111MODULE，通过它可以开启或关闭模块支持，它有三个可选值：off、on、auto，默认值是auto。\nGO111MODULE=off禁用模块支持，编译时会从GOPATH和vendor文件夹中查找包。 GO111MODULE=on启用模块支持，编译时会忽略GOPATH和vendor文件夹，只根据 go.mod下载依赖。 GO111MODULE=auto，当项目在$GOPATH/src外且项目根目录有go.mod文件时，开启模块支持。 简单来说，设置GO111MODULE=on之后就可以使用go module了，以后就没有必要在GOPATH中创建项目了，并且还能够很好的管理项目依赖的第三方包信息。\n使用 go module 管理依赖后会在项目根目录下生成两个文件go.mod和go.sum。\nGOPROXY Go1.11之后设置GOPROXY命令为：\n1 export GOPROXY=https://goproxy.cn Go1.13之后GOPROXY默认值为https://proxy.golang.org，在国内是无法访问的，所以十分建议大家设置GOPROXY，这里我推荐使用goproxy.cn。\n1 go env -w GOPROXY=https://goproxy.cn,direct go mod命令 常用的go mod命令如下：\n1 2 3 4 5 6 7 8 go mod download 下载依赖的module到本地cache（默认为$GOPATH/pkg/mod目录） go mod edit 编辑go.mod文件 go mod graph 打印模块依赖图 go mod init 初始化当前文件夹, 创建go.mod文件 go mod tidy 增加缺少的module，删除无用的module go mod vendor 将依赖复制到vendor下 go mod verify 校验依赖 go mod why 解释为什么需要依赖 go.mod go.mod文件记录了项目所有的依赖信息，其结构大致如下：\n1 2 3 4 5 6 7 8 9 10 11 12 module github.com/Q1mi/studygo/blogger go 1.12 require ( github.com/DeanThompson/ginpprof v0.0.0-20190408063150-3be636683586 github.com/gin-gonic/gin v1.4.0 github.com/go-sql-driver/mysql v1.4.1 github.com/jmoiron/sqlx v1.2.0 github.com/satori/go.uuid v1.2.0 google.golang.org/appengine v1.6.1 // indirect ) 其中，\nmodule用来定义包名 require用来定义依赖包及版本 indirect表示间接引用 依赖的版本 go mod支持语义化版本号，比如go get foo@v1.2.3，也可以跟git的分支或tag，比如go get foo@master，当然也可以跟git提交哈希，比如go get foo@e3702bed2。关于依赖的版本支持以下几种格式：\n1 2 3 4 5 gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 gopkg.in/vmihailenco/msgpack.v2 v2.9.1 gopkg.in/yaml.v2 \u0026lt;=v2.2.1 github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e latest replace 在国内访问golang.org/x的各个包都需要翻墙，你可以在go.mod中使用replace替换成github上对应的库。\n1 2 3 4 5 replace ( golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac =\u0026gt; github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac golang.org/x/net v0.0.0-20180821023952-922f4815f713 =\u0026gt; github.com/golang/net v0.0.0-20180826012351-8a410e7b638d golang.org/x/text v0.3.0 =\u0026gt; github.com/golang/text v0.3.0 ) go get 在项目中执行go get命令可以下载依赖包，并且还可以指定下载的版本。\n运行go get -u将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号， y是次要版本号) 运行go get -u=patch将会升级到最新的修订版本 运行go get package@version将会升级到指定的版本号version 如果下载所有依赖可以使用go mod download命令。\n整理依赖 我们在代码中删除依赖代码后，相关的依赖库并不会在go.mod文件中自动移除。这种情况下我们可以使用go mod tidy命令更新go.mod中的依赖关系。\ngo mod edit 格式化 因为我们可以手动修改go.mod文件，所以有些时候需要格式化该文件。Go提供了一下命令：\n1 go mod edit -fmt 添加依赖项 1 go mod edit -require=golang.org/x/text 移除依赖项 如果只是想修改go.mod文件中的内容，那么可以运行go mod edit -droprequire=package path，比如要在go.mod中移除golang.org/x/text包，可以使用如下命令：\n1 go mod edit -droprequire=golang.org/x/text 关于go mod edit的更多用法可以通过go help mod edit查看。\n在项目中使用go module 既有项目 如果需要对一个已经存在的项目启用go module，可以按照以下步骤操作：\n在项目目录下执行go mod init，生成一个go.mod文件。 执行go get，查找并记录当前项目的依赖，同时生成一个go.sum记录每个依赖库的版本和哈希值。 新项目 对于一个新创建的项目，我们可以在项目文件夹下按照以下步骤操作：\n执行go mod init 项目名命令，在当前项目文件夹下创建一个go.mod文件。 手动编辑go.mod中的require依赖项或执行go get自动发现、维护依赖。 ","permalink":"https://ktzxy.top/posts/l0vr8c6fsb/","summary":"Go的依赖管理GoModule","title":"Go的依赖管理GoModule"},{"content":"﻿\n45、web实验-抽取公共页面 官方文档 - Template Layout\n公共页面/templates/common.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34; xmlns:th=\u0026#34;http://www.thymeleaf.org\u0026#34;\u0026gt;\u0026lt;!--注意要添加xmlns:th才能添加thymeleaf的标签--\u0026gt; \u0026lt;head th:fragment=\u0026#34;commonheader\u0026#34;\u0026gt; \u0026lt;!--common--\u0026gt; \u0026lt;link href=\u0026#34;css/style.css\u0026#34; th:href=\u0026#34;@{/css/style.css}\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; \u0026lt;link href=\u0026#34;css/style-responsive.css\u0026#34; th:href=\u0026#34;@{/css/style-responsive.css}\u0026#34; rel=\u0026#34;stylesheet\u0026#34;\u0026gt; ... \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!-- left side start--\u0026gt; \u0026lt;div id=\u0026#34;leftmenu\u0026#34; class=\u0026#34;left-side sticky-left-side\u0026#34;\u0026gt; ... \u0026lt;div class=\u0026#34;left-side-inner\u0026#34;\u0026gt; ... \u0026lt;!--sidebar nav start--\u0026gt; \u0026lt;ul class=\u0026#34;nav nav-pills nav-stacked custom-nav\u0026#34;\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a th:href=\u0026#34;@{/main.html}\u0026#34;\u0026gt;\u0026lt;i class=\u0026#34;fa fa-home\u0026#34;\u0026gt;\u0026lt;/i\u0026gt; \u0026lt;span\u0026gt;Dashboard\u0026lt;/span\u0026gt;\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; ... \u0026lt;li class=\u0026#34;menu-list nav-active\u0026#34;\u0026gt;\u0026lt;a href=\u0026#34;#\u0026#34;\u0026gt;\u0026lt;i class=\u0026#34;fa fa-th-list\u0026#34;\u0026gt;\u0026lt;/i\u0026gt; \u0026lt;span\u0026gt;Data Tables\u0026lt;/span\u0026gt;\u0026lt;/a\u0026gt; \u0026lt;ul class=\u0026#34;sub-menu-list\u0026#34;\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a th:href=\u0026#34;@{/basic_table}\u0026#34;\u0026gt; Basic Table\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a th:href=\u0026#34;@{/dynamic_table}\u0026#34;\u0026gt; Advanced Table\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a th:href=\u0026#34;@{/responsive_table}\u0026#34;\u0026gt; Responsive Table\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;\u0026lt;a th:href=\u0026#34;@{/editable_table}\u0026#34;\u0026gt; Edit Table\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/li\u0026gt; ... \u0026lt;/ul\u0026gt; \u0026lt;!--sidebar nav end--\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;!-- left side end--\u0026gt; \u0026lt;!-- header section start--\u0026gt; \u0026lt;div th:fragment=\u0026#34;headermenu\u0026#34; class=\u0026#34;header-section\u0026#34;\u0026gt; \u0026lt;!--toggle button start--\u0026gt; \u0026lt;a class=\u0026#34;toggle-btn\u0026#34;\u0026gt;\u0026lt;i class=\u0026#34;fa fa-bars\u0026#34;\u0026gt;\u0026lt;/i\u0026gt;\u0026lt;/a\u0026gt; \u0026lt;!--toggle button end--\u0026gt; ... \u0026lt;/div\u0026gt; \u0026lt;!-- header section end--\u0026gt; \u0026lt;div id=\u0026#34;commonscript\u0026#34;\u0026gt; \u0026lt;!-- Placed js at the end of the document so the pages load faster --\u0026gt; \u0026lt;script th:src=\u0026#34;@{/js/jquery-1.10.2.min.js}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script th:src=\u0026#34;@{/js/jquery-ui-1.9.2.custom.min.js}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script th:src=\u0026#34;@{/js/jquery-migrate-1.2.1.min.js}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script th:src=\u0026#34;@{/js/bootstrap.min.js}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script th:src=\u0026#34;@{/js/modernizr.min.js}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script th:src=\u0026#34;@{/js/jquery.nicescroll.js}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;!--common scripts for all pages--\u0026gt; \u0026lt;script th:src=\u0026#34;@{/js/scripts.js}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; /templates/table/basic_table.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34; xmlns:th=\u0026#34;http://www.thymeleaf.org\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;viewport\u0026#34; content=\u0026#34;width=device-width, initial-scale=1.0, maximum-scale=1.0\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;description\u0026#34; content=\u0026#34;\u0026#34;\u0026gt; \u0026lt;meta name=\u0026#34;author\u0026#34; content=\u0026#34;ThemeBucket\u0026#34;\u0026gt; \u0026lt;link rel=\u0026#34;shortcut icon\u0026#34; href=\u0026#34;#\u0026#34; type=\u0026#34;image/png\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Basic Table\u0026lt;/title\u0026gt; \u0026lt;div th:include=\u0026#34;common :: commonheader\u0026#34;\u0026gt; \u0026lt;/div\u0026gt;\u0026lt;!--将common.html的代码段 插进来--\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body class=\u0026#34;sticky-header\u0026#34;\u0026gt; \u0026lt;section\u0026gt; \u0026lt;div th:replace=\u0026#34;common :: #leftmenu\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;!-- main content start--\u0026gt; \u0026lt;div class=\u0026#34;main-content\u0026#34; \u0026gt; \u0026lt;div th:replace=\u0026#34;common :: headermenu\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; ... \u0026lt;/div\u0026gt; \u0026lt;!-- main content end--\u0026gt; \u0026lt;/section\u0026gt; \u0026lt;!-- Placed js at the end of the document so the pages load faster --\u0026gt; \u0026lt;div th:replace=\u0026#34;common :: #commonscript\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Difference between th:insert and th:replace (and th:include)\n46、web实验-遍历数据与页面bug修改 控制层代码：\n1 2 3 4 5 6 7 8 9 10 11 @GetMapping(\u0026#34;/dynamic_table\u0026#34;) public String dynamic_table(Model model){ //表格内容的遍历 List\u0026lt;User\u0026gt; users = Arrays.asList(new User(\u0026#34;zhangsan\u0026#34;, \u0026#34;123456\u0026#34;), new User(\u0026#34;lisi\u0026#34;, \u0026#34;123444\u0026#34;), new User(\u0026#34;haha\u0026#34;, \u0026#34;aaaaa\u0026#34;), new User(\u0026#34;hehe \u0026#34;, \u0026#34;aaddd\u0026#34;)); model.addAttribute(\u0026#34;users\u0026#34;,users); return \u0026#34;table/dynamic_table\u0026#34;; } 页面代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;table class=\u0026#34;display table table-bordered\u0026#34; id=\u0026#34;hidden-table-info\u0026#34;\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;#\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;用户名\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;密码\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; \u0026lt;tr class=\u0026#34;gradeX\u0026#34; th:each=\u0026#34;user,stats:${users}\u0026#34;\u0026gt; \u0026lt;td th:text=\u0026#34;${stats.count}\u0026#34;\u0026gt;Trident\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${user.userName}\u0026#34;\u0026gt;Internet\u0026lt;/td\u0026gt; \u0026lt;td \u0026gt;[[${user.password}]]\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/tbody\u0026gt; \u0026lt;/table\u0026gt; 47、视图解析-【源码分析】-视图解析器与视图 视图解析原理流程：\n目标方法处理的过程中（阅读DispatcherServlet源码），所有数据都会被放在 ModelAndViewContainer 里面，其中包括数据和视图地址。 方法的参数是一个自定义类型对象（从请求参数中确定的），把他重新放在 ModelAndViewContainer 。 任何目标方法执行完成以后都会返回ModelAndView（数据和视图地址）。 processDispatchResult()处理派发结果（页面改如何响应） render(mv, request, response); 进行页面渲染逻辑 根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】 所有的视图解析器尝试是否能根据当前返回值得到View对象 得到了 redirect:/main.html --\u0026gt; Thymeleaf new RedirectView()。 ContentNegotiationViewResolver 里面包含了下面所有的视图解析器，内部还是利用下面所有视图解析器得到视图对象。 view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作。 RedirectView 如何渲染【重定向到一个页面】 获取目标url地址 response.sendRedirect(encodedURL); 视图解析： - 返回值以 forward: 开始： new InternalResourceView(forwardUrl); \u0026ndash;\u0026gt; 转发request.getRequestDispatcher(path).forward(request, response); - 返回值以 redirect: 开始： new RedirectView() \u0026ndash;\u0026gt; render就是重定向 - 返回值是普通字符串：new ThymeleafView()\u0026mdash;\u0026gt;\n阅读源码：最好自己在IDE，打断点，且Debug模式运行实例，这样比较没那么沉闷。\n48、拦截器-登录检查与静态资源放行 编写一个拦截器实现HandlerInterceptor接口\n拦截器注册到容器中（实现WebMvcConfigurer的addInterceptors()）\n指定拦截规则（注意，如果是拦截所有，静态资源也会被拦截】\n编写一个实现HandlerInterceptor接口的拦截器：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Slf4j public class LoginInterceptor implements HandlerInterceptor { /** * 目标方法执行之前 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI = request.getRequestURI(); log.info(\u0026#34;preHandle拦截的请求路径是{}\u0026#34;,requestURI); //登录检查逻辑 HttpSession session = request.getSession(); Object loginUser = session.getAttribute(\u0026#34;loginUser\u0026#34;); if(loginUser != null){ //放行 return true; } //拦截住。未登录。跳转到登录页 request.setAttribute(\u0026#34;msg\u0026#34;,\u0026#34;请先登录\u0026#34;); // re.sendRedirect(\u0026#34;/\u0026#34;); request.getRequestDispatcher(\u0026#34;/\u0026#34;).forward(request,response); return false; } /** * 目标方法执行完成以后 */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info(\u0026#34;postHandle执行{}\u0026#34;,modelAndView); } /** * 页面渲染以后 */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info(\u0026#34;afterCompletion执行异常{}\u0026#34;,ex); } } 拦截器注册到容器中 \u0026amp;\u0026amp; 指定拦截规则：\n1 2 3 4 5 6 7 8 9 @Configuration public class AdminWebConfig implements WebMvcConfigurer{ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor())//拦截器注册到容器中 .addPathPatterns(\u0026#34;/**\u0026#34;) //所有请求都被拦截包括静态资源 .excludePathPatterns(\u0026#34;/\u0026#34;,\u0026#34;/login\u0026#34;,\u0026#34;/css/**\u0026#34;,\u0026#34;/fonts/**\u0026#34;,\u0026#34;/images/**\u0026#34;, \u0026#34;/js/**\u0026#34;,\u0026#34;/aa/**\u0026#34;); //放行的请求 } 49、拦截器-【源码分析】-拦截器的执行时机和原理 根据当前请求，找到HandlerExecutionChain（可以处理请求的handler以及handler的所有 拦截器） 先来顺序执行 所有拦截器的 preHandle()方法。 如果当前拦截器preHandle()返回为true。则执行下一个拦截器的preHandle() 如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的 afterCompletion();。 如果任何一个拦截器返回false，直接跳出不执行目标方法。 所有拦截器都返回true，才执行目标方法。 倒序执行所有拦截器的postHandle()方法。 前面的步骤有任何异常都会直接倒序触发 afterCompletion()。 页面成功渲染完成以后，也会倒序触发 afterCompletion()。 DispatcherServlet中涉及到HandlerInterceptor的地方：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public class DispatcherServlet extends FrameworkServlet { ... protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; ... //该方法内调用HandlerInterceptor的preHandle() if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); ... //该方法内调用HandlerInterceptor的postHandle() mappedHandler.applyPostHandle(processedRequest, response, mv); }\tprocessDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { //该方法内调用HandlerInterceptor接口的afterCompletion方法 triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { //该方法内调用HandlerInterceptor接口的afterCompletion方法 triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException(\u0026#34;Handler processing failed\u0026#34;, err)); } finally { ... } } private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception { if (mappedHandler != null) { //该方法内调用HandlerInterceptor接口的afterCompletion方法 mappedHandler.triggerAfterCompletion(request, response, ex); } throw ex; } private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { ... if (mappedHandler != null) { //该方法内调用HandlerInterceptor接口的afterCompletion方法 // Exception (if any) is already handled.. mappedHandler.triggerAfterCompletion(request, response, null); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 public class HandlerExecutionChain { ... boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { for (int i = 0; i \u0026lt; this.interceptorList.size(); i++) { HandlerInterceptor interceptor = this.interceptorList.get(i); //HandlerInterceptor的preHandle方法 if (!interceptor.preHandle(request, response, this.handler)) { triggerAfterCompletion(request, response, null); return false; } this.interceptorIndex = i; } return true; } void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception { for (int i = this.interceptorList.size() - 1; i \u0026gt;= 0; i--) { HandlerInterceptor interceptor = this.interceptorList.get(i); //HandlerInterceptor接口的postHandle方法 interceptor.postHandle(request, response, this.handler, mv); } } void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) { for (int i = this.interceptorIndex; i \u0026gt;= 0; i--) { HandlerInterceptor interceptor = this.interceptorList.get(i); try { //HandlerInterceptor接口的afterCompletion方法 interceptor.afterCompletion(request, response, this.handler, ex); } catch (Throwable ex2) { logger.error(\u0026#34;HandlerInterceptor.afterCompletion threw exception\u0026#34;, ex2); } } } } 50、文件上传-单文件与多文件上传的使用 页面代码/static/form/form_layouts.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 \u0026lt;form role=\u0026#34;form\u0026#34; th:action=\u0026#34;@{/upload}\u0026#34; method=\u0026#34;post\u0026#34; enctype=\u0026#34;multipart/form-data\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;exampleInputEmail1\u0026#34;\u0026gt;邮箱\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;email\u0026#34; name=\u0026#34;email\u0026#34; class=\u0026#34;form-control\u0026#34; id=\u0026#34;exampleInputEmail1\u0026#34; placeholder=\u0026#34;Enter email\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;exampleInputPassword1\u0026#34;\u0026gt;名字\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;username\u0026#34; class=\u0026#34;form-control\u0026#34; id=\u0026#34;exampleInputPassword1\u0026#34; placeholder=\u0026#34;Password\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;exampleInputFile\u0026#34;\u0026gt;头像\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;headerImg\u0026#34; id=\u0026#34;exampleInputFile\u0026#34;\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;form-group\u0026#34;\u0026gt; \u0026lt;label for=\u0026#34;exampleInputFile\u0026#34;\u0026gt;生活照\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;photos\u0026#34; multiple\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;checkbox\u0026#34;\u0026gt; \u0026lt;label\u0026gt; \u0026lt;input type=\u0026#34;checkbox\u0026#34;\u0026gt; Check me out \u0026lt;/label\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;button type=\u0026#34;submit\u0026#34; class=\u0026#34;btn btn-primary\u0026#34;\u0026gt;提交\u0026lt;/button\u0026gt; \u0026lt;/form\u0026gt; 控制层代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Slf4j @Controller public class FormTestController { @GetMapping(\u0026#34;/form_layouts\u0026#34;) public String form_layouts(){ return \u0026#34;form/form_layouts\u0026#34;; } @PostMapping(\u0026#34;/upload\u0026#34;) public String upload(@RequestParam(\u0026#34;email\u0026#34;) String email, @RequestParam(\u0026#34;username\u0026#34;) String username, @RequestPart(\u0026#34;headerImg\u0026#34;) MultipartFile headerImg, @RequestPart(\u0026#34;photos\u0026#34;) MultipartFile[] photos) throws IOException { log.info(\u0026#34;上传的信息：email={}，username={}，headerImg={}，photos={}\u0026#34;, email,username,headerImg.getSize(),photos.length); if(!headerImg.isEmpty()){ //保存到文件服务器，OSS服务器 String originalFilename = headerImg.getOriginalFilename(); headerImg.transferTo(new File(\u0026#34;H:\\\\cache\\\\\u0026#34;+originalFilename)); } if(photos.length \u0026gt; 0){ for (MultipartFile photo : photos) { if(!photo.isEmpty()){ String originalFilename = photo.getOriginalFilename(); photo.transferTo(new File(\u0026#34;H:\\\\cache\\\\\u0026#34;+originalFilename)); } } } return \u0026#34;main\u0026#34;; } } 文件上传相关的配置类：\norg.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration org.springframework.boot.autoconfigure.web.servlet.MultipartProperties 文件大小相关配置项：\n1 2 spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=100MB 51、文件上传-【源码流程】文件上传参数解析器 文件上传相关的自动配置类MultipartAutoConfiguration有创建文件上传参数解析器StandardServletMultipartResolver。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class }) @ConditionalOnProperty(prefix = \u0026#34;spring.servlet.multipart\u0026#34;, name = \u0026#34;enabled\u0026#34;, matchIfMissing = true) @ConditionalOnWebApplication(type = Type.SERVLET) @EnableConfigurationProperties(MultipartProperties.class) public class MultipartAutoConfiguration { private final MultipartProperties multipartProperties; public MultipartAutoConfiguration(MultipartProperties multipartProperties) { this.multipartProperties = multipartProperties; } @Bean @ConditionalOnMissingBean({ MultipartConfigElement.class, CommonsMultipartResolver.class }) public MultipartConfigElement multipartConfigElement() { return this.multipartProperties.createMultipartConfig(); } @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) @ConditionalOnMissingBean(MultipartResolver.class) public StandardServletMultipartResolver multipartResolver() { //配置好文件上传解析器 StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver(); multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily()); return multipartResolver; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 //文件上传解析器 public class StandardServletMultipartResolver implements MultipartResolver { private boolean resolveLazily = false; public void setResolveLazily(boolean resolveLazily) { this.resolveLazily = resolveLazily; } @Override public boolean isMultipart(HttpServletRequest request) { return StringUtils.startsWithIgnoreCase(request.getContentType(), \u0026#34;multipart/\u0026#34;); } @Override public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { return new StandardMultipartHttpServletRequest(request, this.resolveLazily); } @Override public void cleanupMultipart(MultipartHttpServletRequest request) { if (!(request instanceof AbstractMultipartHttpServletRequest) || ((AbstractMultipartHttpServletRequest) request).isResolved()) { // To be on the safe side: explicitly delete the parts, // but only actual file parts (for Resin compatibility) try { for (Part part : request.getParts()) { if (request.getFile(part.getName()) != null) { part.delete(); } } } catch (Throwable ex) { LogFactory.getLog(getClass()).warn(\u0026#34;Failed to perform cleanup of multipart items\u0026#34;, ex); } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public class DispatcherServlet extends FrameworkServlet { @Nullable private MultipartResolver multipartResolver; private void initMultipartResolver(ApplicationContext context) { ... //这个就是配置类配置的StandardServletMultipartResolver文件上传解析器 this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class); ... } protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false;//最后finally的回收flag ... try { ModelAndView mv = null; Exception dispatchException = null; try { //做预处理,如果有上传文件 就new StandardMultipartHttpServletRequest包装类 processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. mappedHandler = getHandler(processedRequest); ... // Determine handler adapter for the current request. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); ... // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); } .... finally { ... if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { if (this.multipartResolver != null \u0026amp;\u0026amp; this.multipartResolver.isMultipart(request)) { ... return this.multipartResolver.resolveMultipart(request); ... } } protected void cleanupMultipart(HttpServletRequest request) { if (this.multipartResolver != null) { MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); if (multipartRequest != null) { this.multipartResolver.cleanupMultipart(multipartRequest); } } } } mv = ha.handle(processedRequest, response, mappedHandler.getHandler());跳到以下的类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { @Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; ... mav = invokeHandlerMethod(request, response, handlerMethod); ... return mav; } @Nullable protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) {//关注点 invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } ... invocableMethod.invokeAndHandle(webRequest, mavContainer); ... return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } } } this.argumentResolvers其中主角类RequestPartMethodArgumentResolver用来生成\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { ... public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); ... } @Nullable public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); ... return doInvoke(args);//反射调用 } @Nullable protected Object doInvoke(Object... args) throws Exception { Method method = getBridgedMethod(); ReflectionUtils.makeAccessible(method); return method.invoke(getBean(), args); ... } //处理得出multipart参数，准备稍后的反射调用（@PostMapping标记的上传方法） protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { MethodParameter[] parameters = getMethodParameters(); ... Object[] args = new Object[parameters.length]; for (int i = 0; i \u0026lt; parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); args[i] = findProvidedArgument(parameter, providedArgs); if (args[i] != null) { continue; } //关注点1 if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, \u0026#34;No suitable resolver\u0026#34;)); } try { //关注点2 args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); } catch (Exception ex) { ... } } return args; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver { //对应上面代码关注点1 @Override public boolean supportsParameter(MethodParameter parameter) { //标注@RequestPart的参数 if (parameter.hasParameterAnnotation(RequestPart.class)) { return true; } else { if (parameter.hasParameterAnnotation(RequestParam.class)) { return false; } return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional()); } } //对应上面代码关注点2 @Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); Assert.state(servletRequest != null, \u0026#34;No HttpServletRequest\u0026#34;); RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class); boolean isRequired = ((requestPart == null || requestPart.required()) \u0026amp;\u0026amp; !parameter.isOptional()); String name = getPartName(parameter, requestPart); parameter = parameter.nestedIfOptional(); Object arg = null; //封装成MultipartFile类型的对象作参数 Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { arg = mpArg; } ... return adaptArgumentIfNecessary(arg, parameter); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 public final class MultipartResolutionDelegate { ... @Nullable public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request) throws Exception { MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); boolean isMultipart = (multipartRequest != null || isMultipartContent(request)); if (MultipartFile.class == parameter.getNestedParameterType()) { if (!isMultipart) { return null; } if (multipartRequest == null) { multipartRequest = new StandardMultipartHttpServletRequest(request); } return multipartRequest.getFile(name); } else if (isMultipartFileCollection(parameter)) { if (!isMultipart) { return null; } if (multipartRequest == null) { multipartRequest = new StandardMultipartHttpServletRequest(request); } List\u0026lt;MultipartFile\u0026gt; files = multipartRequest.getFiles(name); return (!files.isEmpty() ? files : null); } else if (isMultipartFileArray(parameter)) { if (!isMultipart) { return null; } if (multipartRequest == null) { multipartRequest = new StandardMultipartHttpServletRequest(request); } List\u0026lt;MultipartFile\u0026gt; files = multipartRequest.getFiles(name); return (!files.isEmpty() ? files.toArray(new MultipartFile[0]) : null); } else if (Part.class == parameter.getNestedParameterType()) { if (!isMultipart) { return null; } return request.getPart(name); } else if (isPartCollection(parameter)) { if (!isMultipart) { return null; } List\u0026lt;Part\u0026gt; parts = resolvePartList(request, name); return (!parts.isEmpty() ? parts : null); } else if (isPartArray(parameter)) { if (!isMultipart) { return null; } List\u0026lt;Part\u0026gt; parts = resolvePartList(request, name); return (!parts.isEmpty() ? parts.toArray(new Part[0]) : null); } else { return UNRESOLVABLE; } } ... } 52、错误处理-SpringBoot默认错误处理机制 Spring Boot官方文档 - Error Handling\n默认规则：\n默认情况下，Spring Boot提供/error处理所有错误的映射\n机器客户端，它将生成JSON响应，其中包含错误，HTTP状态和异常消息的详细信息。对于浏览器客户端，响应一个“ whitelabel”错误视图，以HTML格式呈现相同的数据\n1 2 3 4 5 6 7 { \u0026#34;timestamp\u0026#34;: \u0026#34;2020-11-22T05:53:28.416+00:00\u0026#34;, \u0026#34;status\u0026#34;: 404, \u0026#34;error\u0026#34;: \u0026#34;Not Found\u0026#34;, \u0026#34;message\u0026#34;: \u0026#34;No message available\u0026#34;, \u0026#34;path\u0026#34;: \u0026#34;/asadada\u0026#34; } 要对其进行自定义，添加View解析为error\n要完全替换默认行为，可以实现 ErrorController 并注册该类型的Bean定义，或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。\n/templates/error/下的4xx，5xx页面会被自动解析\n53、错误处理-【源码分析】底层组件功能分析 ErrorMvcAutoConfiguration 自动配置异常处理规则 容器中的组件：类型：DefaultErrorAttributes -\u0026gt; id：errorAttributes public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver DefaultErrorAttributes：定义错误页面中可以包含数据（异常明细，堆栈信息等）。 容器中的组件：类型：BasicErrorController \u0026ndash;\u0026gt; id：basicErrorController（json+白页 适配响应） 处理默认 /error 路径的请求，页面响应 new ModelAndView(\u0026quot;error\u0026quot;, model); 容器中有组件 View-\u0026gt;id是error；（响应默认错误页） 容器中放组件 BeanNameViewResolver（视图解析器）；按照返回的视图名作为组件的id去容器中找View对象。 容器中的组件：类型：DefaultErrorViewResolver -\u0026gt; id：conventionErrorViewResolver 如果发生异常错误，会以HTTP的状态码 作为视图页地址（viewName），找到真正的页面（主要作用）。 error/404、5xx.html 如果想要返回页面，就会找error视图（StaticView默认是一个白页）。 54、错误处理-【源码流程】异常处理流程 譬如写一个会抛出异常的控制层：\n1 2 3 4 5 6 7 8 9 10 11 12 13 @Slf4j @RestController public class HelloController { @RequestMapping(\u0026#34;/hello\u0026#34;) public String handle01(){ int i = 1 / 0;//将会抛出ArithmeticException log.info(\u0026#34;Hello, Spring Boot 2!\u0026#34;); return \u0026#34;Hello, Spring Boot 2!\u0026#34;; } } 当浏览器发出/hello请求，DispatcherServlet的doDispatch()的mv = ha.handle(processedRequest, response, mappedHandler.getHandler());将会抛出ArithmeticException。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 public class DispatcherServlet extends FrameworkServlet { ... protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ... // Actually invoke the handler. //将会抛出ArithmeticException mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { //将会捕捉ArithmeticException dispatchException = ex; } catch (Throwable err) { ... } //捕捉后，继续运行 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException(\u0026#34;Handler processing failed\u0026#34;, err)); } finally { ... } } private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { ... } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); //ArithmeticException将在这处理 mv = processHandlerException(request, response, handler, exception); errorView = (mv != null); } } ... } protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { // Success and error responses may use different content types request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); // Check registered HandlerExceptionResolvers... ModelAndView exMv = null; if (this.handlerExceptionResolvers != null) { //遍历所有的 handlerExceptionResolvers，看谁能处理当前异常HandlerExceptionResolver处理器异常解析器 for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) { exMv = resolver.resolveException(request, response, handler, ex); if (exMv != null) { break; } } } ... //若只有系统的自带的异常解析器（没有自定义的），异常还是会抛出 throw ex; } } 系统自带的异常解析器：\nDefaultErrorAttributes先来处理异常，它主要功能把异常信息保存到request域，并且返回null。 1 2 3 4 5 6 7 8 9 10 11 12 13 public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered { ... public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { this.storeErrorAttributes(request, ex); return null; } private void storeErrorAttributes(HttpServletRequest request, Exception ex) { request.setAttribute(ERROR_ATTRIBUTE, ex);//把异常信息保存到request域 } ... } 默认没有任何解析器（上图的HandlerExceptionResolverComposite）能处理异常，所以最后异常会被抛出。\n最终底层就会转发/error 请求。会被底层的BasicErrorController处理。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Controller @RequestMapping(\u0026#34;${server.error.path:${error.path:/error}}\u0026#34;) public class BasicErrorController extends AbstractErrorController { @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map\u0026lt;String, Object\u0026gt; model = Collections .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); //如果/template/error内没有4**.html或5**.html， //modelAndView为空，最终还是返回viewName为error的modelAndView return (modelAndView != null) ? modelAndView : new ModelAndView(\u0026#34;error\u0026#34;, model); } ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ... protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ... // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); ... //渲染页面 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); ... } private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; ... // Did the handler return a view to render? if (mv != null \u0026amp;\u0026amp; !mv.wasCleared()) { render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } ... } protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { ... View view; String viewName = mv.getViewName(); if (viewName != null) { // We need to resolve the view name. //找出合适error的View，如果/template/error内没有4**.html或5**.html， //将会返回默认异常页面ErrorMvcAutoConfiguration.StaticView //这里按需深究代码吧！ view = resolveViewName(viewName, mv.getModelInternal(), locale, request); ... } ... try { if (mv.getStatus() != null) { response.setStatus(mv.getStatus().value()); } //看下面代码块的StaticView的render块 view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { ... } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) // Load before the main WebMvcAutoConfiguration so that the error View is available @AutoConfigureBefore(WebMvcAutoConfiguration.class) @EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, WebMvcProperties.class }) public class ErrorMvcAutoConfiguration { ... @Configuration(proxyBeanMethods = false) @ConditionalOnProperty(prefix = \u0026#34;server.error.whitelabel\u0026#34;, name = \u0026#34;enabled\u0026#34;, matchIfMissing = true) @Conditional(ErrorTemplateMissingCondition.class) protected static class WhitelabelErrorViewConfiguration { //将创建一个名为error的系统默认异常页面View的Bean private final StaticView defaultErrorView = new StaticView(); @Bean(name = \u0026#34;error\u0026#34;) @ConditionalOnMissingBean(name = \u0026#34;error\u0026#34;) public View defaultErrorView() { return this.defaultErrorView; } // If the user adds @EnableWebMvc then the bean name view resolver from // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment. @Bean @ConditionalOnMissingBean public BeanNameViewResolver beanNameViewResolver() { BeanNameViewResolver resolver = new BeanNameViewResolver(); resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); return resolver; } } private static class StaticView implements View { private static final MediaType TEXT_HTML_UTF8 = new MediaType(\u0026#34;text\u0026#34;, \u0026#34;html\u0026#34;, StandardCharsets.UTF_8); private static final Log logger = LogFactory.getLog(StaticView.class); @Override public void render(Map\u0026lt;String, ?\u0026gt; model, HttpServletRequest request, HttpServletResponse response) throws Exception { if (response.isCommitted()) { String message = getMessage(model); logger.error(message); return; } response.setContentType(TEXT_HTML_UTF8.toString()); StringBuilder builder = new StringBuilder(); Object timestamp = model.get(\u0026#34;timestamp\u0026#34;); Object message = model.get(\u0026#34;message\u0026#34;); Object trace = model.get(\u0026#34;trace\u0026#34;); if (response.getContentType() == null) { response.setContentType(getContentType()); } //系统默认异常页面html代码 builder.append(\u0026#34;\u0026lt;html\u0026gt;\u0026lt;body\u0026gt;\u0026lt;h1\u0026gt;Whitelabel Error Page\u0026lt;/h1\u0026gt;\u0026#34;).append( \u0026#34;\u0026lt;p\u0026gt;This application has no explicit mapping for /error, so you are seeing this as a fallback.\u0026lt;/p\u0026gt;\u0026#34;) .append(\u0026#34;\u0026lt;div id=\u0026#39;created\u0026#39;\u0026gt;\u0026#34;).append(timestamp).append(\u0026#34;\u0026lt;/div\u0026gt;\u0026#34;) .append(\u0026#34;\u0026lt;div\u0026gt;There was an unexpected error (type=\u0026#34;).append(htmlEscape(model.get(\u0026#34;error\u0026#34;))) .append(\u0026#34;, status=\u0026#34;).append(htmlEscape(model.get(\u0026#34;status\u0026#34;))).append(\u0026#34;).\u0026lt;/div\u0026gt;\u0026#34;); if (message != null) { builder.append(\u0026#34;\u0026lt;div\u0026gt;\u0026#34;).append(htmlEscape(message)).append(\u0026#34;\u0026lt;/div\u0026gt;\u0026#34;); } if (trace != null) { builder.append(\u0026#34;\u0026lt;div style=\u0026#39;white-space:pre-wrap;\u0026#39;\u0026gt;\u0026#34;).append(htmlEscape(trace)).append(\u0026#34;\u0026lt;/div\u0026gt;\u0026#34;); } builder.append(\u0026#34;\u0026lt;/body\u0026gt;\u0026lt;/html\u0026gt;\u0026#34;); response.getWriter().append(builder.toString()); } private String htmlEscape(Object input) { return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null; } private String getMessage(Map\u0026lt;String, ?\u0026gt; model) { Object path = model.get(\u0026#34;path\u0026#34;); String message = \u0026#34;Cannot render error page for request [\u0026#34; + path + \u0026#34;]\u0026#34;; if (model.get(\u0026#34;message\u0026#34;) != null) { message += \u0026#34; and exception [\u0026#34; + model.get(\u0026#34;message\u0026#34;) + \u0026#34;]\u0026#34;; } message += \u0026#34; as the response has already been committed.\u0026#34;; message += \u0026#34; As a result, the response may have the wrong status code.\u0026#34;; return message; } @Override public String getContentType() { return \u0026#34;text/html\u0026#34;; } } } 55、错误处理-【源码流程】几种异常处理原理 自定义错误页\nerror/404.html error/5xx.html；有精确的错误状态码页面就匹配精确，没有就找 4xx.html；如果都没有就触发白页 @ControllerAdvice+@ExceptionHandler处理全局异常；底层是 ExceptionHandlerExceptionResolver 支持的\n1 2 3 4 5 6 7 8 9 10 11 @Slf4j @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler({ArithmeticException.class,NullPointerException.class}) //处理异常 public String handleArithException(Exception e){ log.error(\u0026#34;异常是：{}\u0026#34;,e); return \u0026#34;login\u0026#34;; //视图地址 } } @ResponseStatus+自定义异常 ；底层是 ResponseStatusExceptionResolver ，把responseStatus注解的信息底层调用 response.sendError(statusCode, resolvedReason)，tomcat发送的/error 1 2 3 4 5 6 7 8 9 10 @ResponseStatus(value= HttpStatus.FORBIDDEN,reason = \u0026#34;用户数量太多\u0026#34;) public class UserTooManyException extends RuntimeException { public UserTooManyException(){ } public UserTooManyException(String message){ super(message); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Controller public class TableController { @GetMapping(\u0026#34;/dynamic_table\u0026#34;) public String dynamic_table(@RequestParam(value=\u0026#34;pn\u0026#34;,defaultValue = \u0026#34;1\u0026#34;) Integer pn,Model model){ //表格内容的遍历 List\u0026lt;User\u0026gt; users = Arrays.asList(new User(\u0026#34;zhangsan\u0026#34;, \u0026#34;123456\u0026#34;), new User(\u0026#34;lisi\u0026#34;, \u0026#34;123444\u0026#34;), new User(\u0026#34;haha\u0026#34;, \u0026#34;aaaaa\u0026#34;), new User(\u0026#34;hehe \u0026#34;, \u0026#34;aaddd\u0026#34;)); model.addAttribute(\u0026#34;users\u0026#34;,users); if(users.size()\u0026gt;3){ throw new UserTooManyException();//抛出自定义异常 } return \u0026#34;table/dynamic_table\u0026#34;; } } Spring自家异常如 org.springframework.web.bind.MissingServletRequestParameterException，DefaultHandlerExceptionResolver 处理Spring自家异常。\nresponse.sendError(HttpServletResponse.SC_BAD_REQUEST/*400*/, ex.getMessage()); 自定义实现 HandlerExceptionResolver 处理异常；可以作为默认的全局异常处理规则\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Order(value= Ordered.HIGHEST_PRECEDENCE) //优先级，数字越小优先级越高 @Component public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { response.sendError(511,\u0026#34;我喜欢的错误\u0026#34;); } catch (IOException e) { e.printStackTrace(); } return new ModelAndView(); } } ErrorViewResolver 实现自定义处理异常 response.sendError()，error请求就会转给controller。 你的异常没有任何人能处理，tomcat底层调用response.sendError()，error请求就会转给controller。 basicErrorController 要去的页面地址是 ErrorViewResolver 。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Controller @RequestMapping(\u0026#34;${server.error.path:${error.path:/error}}\u0026#34;) public class BasicErrorController extends AbstractErrorController { ... @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map\u0026lt;String, Object\u0026gt; model = Collections .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView(\u0026#34;error\u0026#34;, model); } protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, Map\u0026lt;String, Object\u0026gt; model) { //这里用到ErrorViewResolver接口 for (ErrorViewResolver resolver : this.errorViewResolvers) { ModelAndView modelAndView = resolver.resolveErrorView(request, status, model); if (modelAndView != null) { return modelAndView; } } return null; } ... } 1 2 3 4 5 6 @FunctionalInterface public interface ErrorViewResolver { ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map\u0026lt;String, Object\u0026gt; model); } 56、原生组件注入-原生注解与Spring方式注入 官方文档 - Servlets, Filters, and listeners\n使用原生的注解 1 2 3 4 5 6 7 8 @WebServlet(urlPatterns = \u0026#34;/my\u0026#34;) public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write(\u0026#34;66666\u0026#34;); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Slf4j @WebFilter(urlPatterns={\u0026#34;/css/*\u0026#34;,\u0026#34;/images/*\u0026#34;}) //my public class MyFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { log.info(\u0026#34;MyFilter初始化完成\u0026#34;); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { log.info(\u0026#34;MyFilter工作\u0026#34;); chain.doFilter(request,response); } @Override public void destroy() { log.info(\u0026#34;MyFilter销毁\u0026#34;); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Slf4j @WebListener public class MyServletContextListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { log.info(\u0026#34;MySwervletContextListener监听到项目初始化完成\u0026#34;); } @Override public void contextDestroyed(ServletContextEvent sce) { log.info(\u0026#34;MySwervletContextListener监听到项目销毁\u0026#34;); } } 最后还要在主启动类添加注解@ServletComponentScan\n1 2 3 4 5 6 7 8 @ServletComponentScan(basePackages = \u0026#34;com.lun\u0026#34;)// @SpringBootApplication(exclude = RedisAutoConfiguration.class) public class Boot05WebAdminApplication { public static void main(String[] args) { SpringApplication.run(Boot05WebAdminApplication.class, args); } } Spring方式注入 ServletRegistrationBean, FilterRegistrationBean, and ServletListenerRegistrationBean\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Configuration(proxyBeanMethods = true) public class MyRegistConfig { @Bean public ServletRegistrationBean myServlet(){ MyServlet myServlet = new MyServlet(); return new ServletRegistrationBean(myServlet,\u0026#34;/my\u0026#34;,\u0026#34;/my02\u0026#34;); } @Bean public FilterRegistrationBean myFilter(){ MyFilter myFilter = new MyFilter(); // return new FilterRegistrationBean(myFilter,myServlet()); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter); filterRegistrationBean.setUrlPatterns(Arrays.asList(\u0026#34;/my\u0026#34;,\u0026#34;/css/*\u0026#34;)); return filterRegistrationBean; } @Bean public ServletListenerRegistrationBean myListener(){ MySwervletContextListener mySwervletContextListener = new MySwervletContextListener(); return new ServletListenerRegistrationBean(mySwervletContextListener); } } 57、原生组件注入-【源码分析】DispatcherServlet注入原理 org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration配置类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass(DispatcherServlet.class) @AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) public class DispatcherServletAutoConfiguration { /* * The bean name for a DispatcherServlet that will be mapped to the root URL \u0026#34;/\u0026#34; */ public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = \u0026#34;dispatcherServlet\u0026#34;; /* * The bean name for a ServletRegistrationBean for the DispatcherServlet \u0026#34;/\u0026#34; */ public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = \u0026#34;dispatcherServletRegistration\u0026#34;; @Configuration(proxyBeanMethods = false) @Conditional(DefaultDispatcherServletCondition.class) @ConditionalOnClass(ServletRegistration.class) @EnableConfigurationProperties(WebMvcProperties.class) protected static class DispatcherServletConfiguration { //创建DispatcherServlet类的Bean @Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) { DispatcherServlet dispatcherServlet = new DispatcherServlet(); dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest()); dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest()); dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound()); dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents()); dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails()); return dispatcherServlet; } @Bean @ConditionalOnBean(MultipartResolver.class) @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) public MultipartResolver multipartResolver(MultipartResolver resolver) { // Detect if the user has created a MultipartResolver but named it incorrectly return resolver; } } @Configuration(proxyBeanMethods = false) @Conditional(DispatcherServletRegistrationCondition.class) @ConditionalOnClass(ServletRegistration.class) @EnableConfigurationProperties(WebMvcProperties.class) @Import(DispatcherServletConfiguration.class) protected static class DispatcherServletRegistrationConfiguration { //注册DispatcherServlet类 @Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME) @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider\u0026lt;MultipartConfigElement\u0026gt; multipartConfig) { DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath()); registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME); registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup()); multipartConfig.ifAvailable(registration::setMultipartConfig); return registration; } } ... } DispatcherServlet默认映射的是 / 路径，可以通过在配置文件修改spring.mvc.servlet.path=/mvc。\n58、嵌入式Servlet容器-【源码分析】切换web服务器与定制化 默认支持的WebServer\nTomcat, Jetty, or Undertow。 ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器。 原理\nSpringBoot应用启动发现当前是Web应用，web场景包-导入tomcat。 web应用会创建一个web版的IOC容器 ServletWebServerApplicationContext 。 ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory （Servlet 的web服务器工厂——\u0026gt;Servlet 的web服务器）。 SpringBoot底层默认有很多的WebServer工厂（ServletWebServerFactoryConfiguration内创建Bean），如： TomcatServletWebServerFactory JettyServletWebServerFactory UndertowServletWebServerFactory 底层直接会有一个自动配置类ServletWebServerFactoryAutoConfiguration。 ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration（配置类）。 ServletWebServerFactoryConfiguration 根据动态判断系统中到底导入了那个Web服务器的包。（默认是web-starter导入tomcat包），容器中就有 TomcatServletWebServerFactory TomcatServletWebServerFactory 创建出Tomcat服务器并启动；TomcatWebServer 的构造器拥有初始化方法initialize——this.tomcat.start(); 内嵌服务器，与以前手动把启动服务器相比，改成现在使用代码启动（tomcat核心jar包存在）。 Spring Boot默认使用Tomcat服务器，若需更改其他服务器，则修改工程pom.xml：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-tomcat\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-jetty\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 官方文档 - Use Another Web Server\n定制Servlet容器 实现WebServerFactoryCustomizer\u0026lt;ConfigurableServletWebServerFactory\u0026gt;\n把配置文件的值和ServletWebServerFactory进行绑定 修改配置文件 server.xxx\n直接自定义 ConfigurableServletWebServerFactory\nxxxxxCustomizer：定制化器，可以改变xxxx的默认规则\n1 2 3 4 5 6 7 8 9 10 11 12 13 import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.stereotype.Component; @Component public class CustomizationBean implements WebServerFactoryCustomizer\u0026lt;ConfigurableServletWebServerFactory\u0026gt; { @Override public void customize(ConfigurableServletWebServerFactory server) { server.setPort(9000); } } 59、定制化原理-SpringBoot定制化组件的几种方式（小结） 定制化的常见方式 修改配置文件\nxxxxxCustomizer\n编写自定义的配置类 xxxConfiguration + @Bean替换、增加容器中默认组件，视图解析器\nWeb应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能 + @Bean给容器中再扩展一些组件\n1 2 3 @Configuration public class AdminWebConfig implements WebMvcConfigurer{ } @EnableWebMvc + WebMvcConfigurer — @Bean 可以全面接管SpringMVC，所有规则全部自己重新配置； 实现定制和扩展功能（高级功能，初学者退避三舍）。 原理： WebMvcAutoConfiguration默认的SpringMVC的自动配置功能类，如静态资源、欢迎页等。 一旦使用 @EnableWebMvc ，会@Import(DelegatingWebMvcConfiguration.class)。 DelegatingWebMvcConfiguration的作用，只保证SpringMVC最基本的使用 把所有系统中的WebMvcConfigurer拿过来，所有功能的定制都是这些WebMvcConfigurer合起来一起生效。 自动配置了一些非常底层的组件，如RequestMappingHandlerMapping，这些组件依赖的组件都是从容器中获取如。 public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport。 WebMvcAutoConfiguration里面的配置要能生效必须 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)。 @EnableWebMvc 导致了WebMvcAutoConfiguration 没有生效。 原理分析套路 场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties - 绑定配置文件项。\n60、数据访问-数据库场景的自动配置分析与整合测试 导入JDBC场景 1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-data-jdbc\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 接着导入数据库驱动包（MySQL为例）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026lt;!--默认版本：--\u0026gt; \u0026lt;mysql.version\u0026gt;8.0.22\u0026lt;/mysql.version\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;!--\u0026lt;version\u0026gt;5.1.49\u0026lt;/version\u0026gt;--\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 想要修改版本 1、直接依赖引入具体版本（maven的就近依赖原则） 2、重新声明版本（maven的属性的就近优先原则） --\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;java.version\u0026gt;1.8\u0026lt;/java.version\u0026gt; \u0026lt;mysql.version\u0026gt;5.1.49\u0026lt;/mysql.version\u0026gt; \u0026lt;/properties\u0026gt; 相关数据源配置类 DataSourceAutoConfiguration ： 数据源的自动配置。\n修改数据源相关的配置：spring.datasource。 数据库连接池的配置，是自己容器中没有DataSource才自动配置的。 底层配置好的连接池是：HikariDataSource。 DataSourceTransactionManagerAutoConfiguration： 事务管理器的自动配置。\nJdbcTemplateAutoConfiguration： JdbcTemplate的自动配置，可以来对数据库进行CRUD。\n可以修改前缀为spring.jdbc的配置项来修改JdbcTemplate。 @Bean @Primary JdbcTemplate：Spring容器中有这个JdbcTemplate组件，使用@Autowired。 JndiDataSourceAutoConfiguration： JNDI的自动配置。\nXADataSourceAutoConfiguration： 分布式事务相关的。\n修改配置项 1 2 3 4 5 6 spring: datasource: url: jdbc:mysql://localhost:3306/db_account username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver 单元测试数据源 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.jdbc.core.JdbcTemplate; @SpringBootTest class Boot05WebAdminApplicationTests { @Autowired JdbcTemplate jdbcTemplate; @Test//用@org.junit.Test会报空指针异常，可能跟JUnit新版本有关 void contextLoads() { // jdbcTemplate.queryForObject(\u0026#34;select * from account_tbl\u0026#34;) // jdbcTemplate.queryForList(\u0026#34;select * from account_tbl\u0026#34;,) Long aLong = jdbcTemplate.queryForObject(\u0026#34;select count(*) from account_tbl\u0026#34;, Long.class); log.info(\u0026#34;记录总数：{}\u0026#34;,aLong); } } 61、数据访问-自定义方式整合druid数据源 Druid官网\nDruid是什么？ 它是数据库连接池，它能够提供强大的监控和扩展功能。\n官方文档 - Druid连接池介绍\nSpring Boot整合第三方技术的两种方式：\n自定义\n找starter场景\n自定义方式 添加依赖：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;druid\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.17\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 配置Druid数据源：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Configuration public class MyConfig { @Bean @ConfigurationProperties(\u0026#34;spring.datasource\u0026#34;)//复用配置文件的数据源配置 public DataSource dataSource() throws SQLException { DruidDataSource druidDataSource = new DruidDataSource(); // druidDataSource.setUrl(); // druidDataSource.setUsername(); // druidDataSource.setPassword(); return druidDataSource; } } 更多配置项\n配置Druid的监控页功能：\nDruid内置提供了一个StatViewServlet用于展示Druid的统计信息。官方文档 - 配置_StatViewServlet配置。这个StatViewServlet的用途包括：\n提供监控信息展示的html页面 提供监控信息的JSON API Druid内置提供一个StatFilter，用于统计监控信息。官方文档 - 配置_StatFilter\nWebStatFilter用于采集web-jdbc关联监控的数据，如SQL监控、URI监控。官方文档 - 配置_配置WebStatFilter\nDruid提供了WallFilter，它是基于SQL语义分析来实现防御SQL注入攻击的。官方文档 - 配置 wallfilter\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Configuration public class MyConfig { @Bean @ConfigurationProperties(\u0026#34;spring.datasource\u0026#34;) public DataSource dataSource() throws SQLException { DruidDataSource druidDataSource = new DruidDataSource(); //加入监控和防火墙功能功能 druidDataSource.setFilters(\u0026#34;stat,wall\u0026#34;); return druidDataSource; } /** * 配置 druid的监控页功能 * @return */ @Bean public ServletRegistrationBean statViewServlet(){ StatViewServlet statViewServlet = new StatViewServlet(); ServletRegistrationBean\u0026lt;StatViewServlet\u0026gt; registrationBean = new ServletRegistrationBean\u0026lt;\u0026gt;(statViewServlet, \u0026#34;/druid/*\u0026#34;); //监控页账号密码： registrationBean.addInitParameter(\u0026#34;loginUsername\u0026#34;,\u0026#34;admin\u0026#34;); registrationBean.addInitParameter(\u0026#34;loginPassword\u0026#34;,\u0026#34;123456\u0026#34;); return registrationBean; } /** * WebStatFilter 用于采集web-jdbc关联监控的数据。 */ @Bean public FilterRegistrationBean webStatFilter(){ WebStatFilter webStatFilter = new WebStatFilter(); FilterRegistrationBean\u0026lt;WebStatFilter\u0026gt; filterRegistrationBean = new FilterRegistrationBean\u0026lt;\u0026gt;(webStatFilter); filterRegistrationBean.setUrlPatterns(Arrays.asList(\u0026#34;/*\u0026#34;)); filterRegistrationBean.addInitParameter(\u0026#34;exclusions\u0026#34;,\u0026#34;*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*\u0026#34;); return filterRegistrationBean; } } 62、数据访问-druid数据源starter整合方式 官方文档 - Druid Spring Boot Starter\n引入依赖：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;druid-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.17\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 分析自动配置：\n扩展配置项 spring.datasource.druid 自动配置类DruidDataSourceAutoConfigure DruidSpringAopConfiguration.class, 监控SpringBean的；配置项：spring.datasource.druid.aop-patterns DruidStatViewServletConfiguration.class, 监控页的配置。spring.datasource.druid.stat-view-servlet默认开启。 DruidWebStatFilterConfiguration.class，web监控配置。spring.datasource.druid.web-stat-filter默认开启。 DruidFilterConfiguration.class所有Druid的filter的配置： 1 2 3 4 5 6 7 8 private static final String FILTER_STAT_PREFIX = \u0026#34;spring.datasource.druid.filter.stat\u0026#34;; private static final String FILTER_CONFIG_PREFIX = \u0026#34;spring.datasource.druid.filter.config\u0026#34;; private static final String FILTER_ENCODING_PREFIX = \u0026#34;spring.datasource.druid.filter.encoding\u0026#34;; private static final String FILTER_SLF4J_PREFIX = \u0026#34;spring.datasource.druid.filter.slf4j\u0026#34;; private static final String FILTER_LOG4J_PREFIX = \u0026#34;spring.datasource.druid.filter.log4j\u0026#34;; private static final String FILTER_LOG4J2_PREFIX = \u0026#34;spring.datasource.druid.filter.log4j2\u0026#34;; private static final String FILTER_COMMONS_LOG_PREFIX = \u0026#34;spring.datasource.druid.filter.commons-log\u0026#34;; private static final String FILTER_WALL_PREFIX = \u0026#34;spring.datasource.druid.filter.wall\u0026#34;; 配置示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 spring: datasource: url: jdbc:mysql://localhost:3306/db_account username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver druid: aop-patterns: com.atguigu.admin.* #监控SpringBean filters: stat,wall # 底层开启功能，stat（sql监控），wall（防火墙） stat-view-servlet: # 配置监控页功能 enabled: true login-username: admin login-password: admin resetEnable: false web-stat-filter: # 监控web enabled: true urlPattern: /* exclusions: \u0026#39;*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*\u0026#39; filter: stat: # 对上面filters里面的stat的详细配置 slow-sql-millis: 1000 logSlowSql: true enabled: true wall: enabled: true config: drop-table-allow: false 63、数据访问-整合MyBatis-配置版 MyBatis的GitHub仓库\nMyBatis官方\nstarter的命名方式：\nSpringBoot官方的Starter：spring-boot-starter-* 第三方的： *-spring-boot-starter 引入依赖：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis.spring.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.1.4\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 配置模式:\n全局配置文件\nSqlSessionFactory：自动配置好了\nSqlSession：自动配置了SqlSessionTemplate 组合了SqlSession\n@Import(AutoConfiguredMapperScannerRegistrar.class)\nMapper： 只要我们写的操作MyBatis的接口标准了@Mapper就会被自动扫描进来\n1 2 3 4 5 6 7 8 9 10 @EnableConfigurationProperties(MybatisProperties.class) ： MyBatis配置项绑定类。 @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }) public class MybatisAutoConfiguration{ ... } @ConfigurationProperties(prefix = \u0026#34;mybatis\u0026#34;) public class MybatisProperties{ ... } 配置文件：\n1 2 3 4 5 6 7 8 9 10 11 spring: datasource: username: root password: 1234 url: jdbc:mysql://localhost:3306/my driver-class-name: com.mysql.jdbc.Driver # 配置mybatis规则 mybatis: config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置 mapper-locations: classpath:mybatis/*.xml #sql映射文件位置 mybatis-config.xml:\n1 2 3 4 5 6 7 8 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE configuration PUBLIC \u0026#34;-//mybatis.org//DTD Config 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-config.dtd\u0026#34;\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- 由于Spring Boot自动配置缘故，此处不必配置，只用来做做样。--\u0026gt; \u0026lt;/configuration\u0026gt; Mapper接口：\n1 2 3 4 5 6 7 8 9 10 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;com.lun.boot.mapper.UserMapper\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;getUser\u0026#34; resultType=\u0026#34;com.lun.boot.bean.User\u0026#34;\u0026gt; select * from user where id=#{id} \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; 1 2 3 4 5 6 7 import com.lun.boot.bean.User; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper { public User getUser(Integer id); } POJO：\n1 2 3 4 5 6 public class User { private Integer id; private String name; //getters and setters... } DB：\n1 2 3 4 5 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(45) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4; Controller and Service：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Controller public class UserController { @Autowired private UserService userService; @ResponseBody @GetMapping(\u0026#34;/user/{id}\u0026#34;) public User getUser(@PathVariable(\u0026#34;id\u0026#34;) Integer id){ return userService.getUser(id); } } 1 2 3 4 5 6 7 8 9 10 11 @Service public class UserService { @Autowired private UserMapper userMapper;//IDEA下标红线，可忽视这红线 public User getUser(Integer id){ return userMapper.getUser(id); } } 配置private Configuration configuration; 也就是配置mybatis.configuration相关的，就是相当于改mybatis全局配置文件中的值。（也就是说配置了mybatis.configuration，就不需配置mybatis全局配置文件了）\n1 2 3 4 5 6 7 # 配置mybatis规则 mybatis: mapper-locations: classpath:mybatis/mapper/*.xml # 可以不写全局配置文件，所有全局配置文件的配置都放在configuration配置项中了。 # config-location: classpath:mybatis/mybatis-config.xml configuration: map-underscore-to-camel-case: true 小结 导入MyBatis官方Starter。 编写Mapper接口，需@Mapper注解。 编写SQL映射文件并绑定Mapper接口。 在application.yaml中指定Mapper配置文件的所处位置，以及指定全局配置文件的信息 （建议：配置在mybatis.configuration）。 64、数据访问-整合MyBatis-注解配置混合版 你可以通过Spring Initializr添加MyBatis的Starer。\n注解与配置混合搭配，干活不累：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Mapper public interface UserMapper { public User getUser(Integer id); @Select(\u0026#34;select * from user where id=#{id}\u0026#34;) public User getUser2(Integer id); public void saveUser(User user); @Insert(\u0026#34;insert into user(`name`) values(#{name})\u0026#34;) @Options(useGeneratedKeys = true, keyProperty = \u0026#34;id\u0026#34;) public void saveUser2(User user); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;com.lun.boot.mapper.UserMapper\u0026#34;\u0026gt; \u0026lt;select id=\u0026#34;getUser\u0026#34; resultType=\u0026#34;com.lun.boot.bean.User\u0026#34;\u0026gt; select * from user where id=#{id} \u0026lt;/select\u0026gt; \u0026lt;insert id=\u0026#34;saveUser\u0026#34; useGeneratedKeys=\u0026#34;true\u0026#34; keyProperty=\u0026#34;id\u0026#34;\u0026gt; insert into user(`name`) values(#{name}) \u0026lt;/insert\u0026gt; \u0026lt;/mapper\u0026gt; 简单DAO方法就写在注解上。复杂的就写在配置文件里。\n使用@MapperScan(\u0026quot;com.lun.boot.mapper\u0026quot;) 简化，Mapper接口就可以不用标注@Mapper注解。\n1 2 3 4 5 6 7 8 9 @MapperScan(\u0026#34;com.lun.boot.mapper\u0026#34;) @SpringBootApplication public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } } 65、数据访问-整合MyBatisPlus操作数据库 IDEA的MyBatis的插件 - MyBatisX\nMyBatisPlus官网\nMyBatisPlus官方文档\nMyBatisPlus是什么 MyBatis-Plus（简称 MP）是一个 MyBatis的增强工具，在 MyBatis 的基础上只做增强不做改变，为简化开发、提高效率而生。\n添加依赖：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.baomidou\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-plus-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.4.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; MybatisPlusAutoConfiguration配置类，MybatisPlusProperties配置项绑定。\nSqlSessionFactory自动配置好，底层是容器中默认的数据源。\nmapperLocations自动配置好的，有默认值classpath*:/mapper/**/*.xml，这表示任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件放在 mapper下。\n容器中也自动配置好了SqlSessionTemplate。\n@Mapper 标注的接口也会被自动扫描，建议直接 @MapperScan(\u0026quot;com.lun.boot.mapper\u0026quot;)批量扫描。\nMyBatisPlus优点之一：只需要我们的Mapper继承MyBatisPlus的BaseMapper 就可以拥有CRUD能力，减轻开发工作。\n1 2 3 4 5 6 import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.lun.hellomybatisplus.model.User; public interface UserMapper extends BaseMapper\u0026lt;User\u0026gt; { } 66、数据访问-CRUD实验-数据列表展示 官方文档 - CRUD接口\n使用MyBatis Plus提供的IService，ServiceImpl，减轻Service层开发工作。\n1 2 3 4 5 6 7 8 9 10 11 import com.lun.hellomybatisplus.model.User; import com.baomidou.mybatisplus.extension.service.IService; import java.util.List; /** * Service 的CRUD也不用写了 */ public interface UserService extends IService\u0026lt;User\u0026gt; { //此处故意为空 } 1 2 3 4 5 6 7 8 9 10 11 12 13 import com.lun.hellomybatisplus.model.User; import com.lun.hellomybatisplus.mapper.UserMapper; import com.lun.hellomybatisplus.service.UserService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class UserServiceImpl extends ServiceImpl\u0026lt;UserMapper,User\u0026gt; implements UserService { //此处故意为空 } 与下一节联合在一起\n67、数据访问-CRUD实验-分页数据展示 与下一节联合在一起\n68、数据访问-CRUD实验-删除用户完成 添加分页插件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Configuration public class MyBatisConfig { /** * MybatisPlusInterceptor * @return */ @Bean public MybatisPlusInterceptor paginationInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); // 设置请求的页面大于最大页后操作， true调回到首页，false 继续请求 默认false // paginationInterceptor.setOverflow(false); // 设置最大单页限制数量，默认 500 条，-1 不受限制 // paginationInterceptor.setLimit(500); // 开启 count 的 join 优化,只针对部分 left join //这是分页拦截器 PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); paginationInnerInterceptor.setOverflow(true); paginationInnerInterceptor.setMaxLimit(500L); mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor); return mybatisPlusInterceptor; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 \u0026lt;table class=\u0026#34;display table table-bordered table-striped\u0026#34; id=\u0026#34;dynamic-table\u0026#34;\u0026gt; \u0026lt;thead\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;#\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;name\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;age\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;email\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;操作\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/thead\u0026gt; \u0026lt;tbody\u0026gt; \u0026lt;tr class=\u0026#34;gradeX\u0026#34; th:each=\u0026#34;user: ${users.records}\u0026#34;\u0026gt; \u0026lt;td th:text=\u0026#34;${user.id}\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt;[[${user.name}]]\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${user.age}\u0026#34;\u0026gt;Win 95+\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${user.email}\u0026#34;\u0026gt;4\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;a th:href=\u0026#34;@{/user/delete/{id}(id=${user.id},pn=${users.current})}\u0026#34; class=\u0026#34;btn btn-danger btn-sm\u0026#34; type=\u0026#34;button\u0026#34;\u0026gt;删除\u0026lt;/a\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/tfoot\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;div class=\u0026#34;row-fluid\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;span6\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;dataTables_info\u0026#34; id=\u0026#34;dynamic-table_info\u0026#34;\u0026gt; 当前第[[${users.current}]]页 总计 [[${users.pages}]]页 共[[${users.total}]]条记录 \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;div class=\u0026#34;span6\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;dataTables_paginate paging_bootstrap pagination\u0026#34;\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li class=\u0026#34;prev disabled\u0026#34;\u0026gt;\u0026lt;a href=\u0026#34;#\u0026#34;\u0026gt;← 前一页\u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;li th:class=\u0026#34;${num == users.current?\u0026#39;active\u0026#39;:\u0026#39;\u0026#39;}\u0026#34; th:each=\u0026#34;num:${#numbers.sequence(1,users.pages)}\u0026#34; \u0026gt; \u0026lt;a th:href=\u0026#34;@{/dynamic_table(pn=${num})}\u0026#34;\u0026gt;[[${num}]]\u0026lt;/a\u0026gt; \u0026lt;/li\u0026gt; \u0026lt;li class=\u0026#34;next disabled\u0026#34;\u0026gt;\u0026lt;a href=\u0026#34;#\u0026#34;\u0026gt;下一页 → \u0026lt;/a\u0026gt;\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; #numbers表示methods for formatting numeric objects.link\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @GetMapping(\u0026#34;/user/delete/{id}\u0026#34;) public String deleteUser(@PathVariable(\u0026#34;id\u0026#34;) Long id, @RequestParam(value = \u0026#34;pn\u0026#34;,defaultValue = \u0026#34;1\u0026#34;)Integer pn, RedirectAttributes ra){ userService.removeById(id); ra.addAttribute(\u0026#34;pn\u0026#34;,pn); return \u0026#34;redirect:/dynamic_table\u0026#34;; } @GetMapping(\u0026#34;/dynamic_table\u0026#34;) public String dynamic_table(@RequestParam(value=\u0026#34;pn\u0026#34;,defaultValue = \u0026#34;1\u0026#34;) Integer pn,Model model){ //表格内容的遍历 //从数据库中查出user表中的用户进行展示 //构造分页参数 Page\u0026lt;User\u0026gt; page = new Page\u0026lt;\u0026gt;(pn, 2); //调用page进行分页 Page\u0026lt;User\u0026gt; userPage = userService.page(page, null); model.addAttribute(\u0026#34;users\u0026#34;,userPage); return \u0026#34;table/dynamic_table\u0026#34;; } 69、数据访问-准备阿里云Redis环境 添加依赖：\n1 2 3 4 5 6 7 8 9 10 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-data-redis\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--导入jedis--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;redis.clients\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jedis\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; RedisAutoConfiguration自动配置类，RedisProperties 属性类 \u0026ndash;\u0026gt; spring.redis.xxx是对redis的配置。 连接工厂LettuceConnectionConfiguration、JedisConnectionConfiguration是准备好的。 自动注入了RedisTemplate\u0026lt;Object, Object\u0026gt;，xxxTemplate。 自动注入了StringRedisTemplate，key，value都是String 底层只要我们使用StringRedisTemplate、RedisTemplate就可以操作Redis。 外网Redis环境搭建：\n阿里云按量付费Redis，其中选择经典网络。\n申请Redis的公网连接地址。\n修改白名单，允许0.0.0.0/0访问。\n70、数据访问-Redis操作与统计小实验 相关Redis配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 spring: redis: # url: redis://lfy:Lfy123456@r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com:6379 host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com port: 6379 password: lfy:Lfy123456 client-type: jedis jedis: pool: max-active: 10 # lettuce:# 另一个用来连接redis的java框架 # pool: # max-active: 10 # min-idle: 5 测试Redis连接：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @SpringBootTest public class Boot05WebAdminApplicationTests { @Autowired StringRedisTemplate redisTemplate; @Autowired RedisConnectionFactory redisConnectionFactory; @Test void testRedis(){ ValueOperations\u0026lt;String, String\u0026gt; operations = redisTemplate.opsForValue(); operations.set(\u0026#34;hello\u0026#34;,\u0026#34;world\u0026#34;); String hello = operations.get(\u0026#34;hello\u0026#34;); System.out.println(hello); System.out.println(redisConnectionFactory.getClass()); } } Redis Desktop Manager：可视化Redis管理软件。\nURL统计拦截器：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Component public class RedisUrlCountInterceptor implements HandlerInterceptor { @Autowired StringRedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String uri = request.getRequestURI(); //默认每次访问当前uri就会计数+1 redisTemplate.opsForValue().increment(uri); return true; } } 注册URL统计拦截器：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Configuration public class AdminWebConfig implements WebMvcConfigurer{ @Autowired RedisUrlCountInterceptor redisUrlCountInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(redisUrlCountInterceptor) .addPathPatterns(\u0026#34;/**\u0026#34;) .excludePathPatterns(\u0026#34;/\u0026#34;,\u0026#34;/login\u0026#34;,\u0026#34;/css/**\u0026#34;,\u0026#34;/fonts/**\u0026#34;,\u0026#34;/images/**\u0026#34;, \u0026#34;/js/**\u0026#34;,\u0026#34;/aa/**\u0026#34;); } } Filter、Interceptor 几乎拥有相同的功能？\nFilter是Servlet定义的原生组件，它的好处是脱离Spring应用也能使用。 Interceptor是Spring定义的接口，可以使用Spring的自动装配等功能。 调用Redis内的统计数据：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Slf4j @Controller public class IndexController { @Autowired StringRedisTemplate redisTemplate; @GetMapping(\u0026#34;/main.html\u0026#34;) public String mainPage(HttpSession session,Model model){ log.info(\u0026#34;当前方法是：{}\u0026#34;,\u0026#34;mainPage\u0026#34;); ValueOperations\u0026lt;String, String\u0026gt; opsForValue = redisTemplate.opsForValue(); String s = opsForValue.get(\u0026#34;/main.html\u0026#34;); String s1 = opsForValue.get(\u0026#34;/sql\u0026#34;); model.addAttribute(\u0026#34;mainCount\u0026#34;,s); model.addAttribute(\u0026#34;sqlCount\u0026#34;,s1); return \u0026#34;main\u0026#34;; } } 71、单元测试-JUnit5简介 Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库\nJUnit 5官方文档\n作为最新版本的JUnit框架，JUnit5与之前版本的JUnit框架有很大的不同。由三个不同子项目的几个不同模块组成。\nJUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage\nJUnit Platform: Junit Platform是在JVM上启动测试框架的基础，不仅支持Junit自制的测试引擎，其他测试引擎也都可以接入。\nJUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型，是JUnit5新特性的核心。内部包含了一个测试引擎，用于在Junit Platform上运行。\nJUnit Vintage: 由于JUint已经发展多年，为了照顾老的项目，JUnit Vintage提供了兼容JUnit4.x，JUnit3.x的测试引擎。\n注意：\nSpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容JUnit4需要自行引入（不能使用JUnit4的功能 @Test）\nJUnit 5’s Vintage已经从spring-boot-starter-test从移除。如果需要继续兼容Junit4需要自行引入Vintage依赖：\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.junit.vintage\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit-vintage-engine\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;groupId\u0026gt;org.hamcrest\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hamcrest-core\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; 使用添加JUnit 5，添加对应的starter： 1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; Spring的JUnit 5的基本单元测试模板（Spring的JUnit4的是@SpringBootTest+@RunWith(SpringRunner.class)）： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test;//注意不是org.junit.Test（这是JUnit4版本的） import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class SpringBootApplicationTests { @Autowired private Component component; @Test //@Transactional 标注后连接数据库有回滚功能 public void contextLoads() { Assertions.assertEquals(5, component.getFive()); } } Jupiter\n英 [ˈdʒuːpɪtə(r)] 美 [ˈdʒuːpɪtər]\nn. 木星(太阳系中最大的行星)\nvintage\n英 [ˈvɪntɪdʒ] 美 [ˈvɪntɪdʒ]\nn. 特定年份(或地方)酿制的酒;酿造年份;采摘葡萄酿酒的期间(或季节);葡萄收获期(或季节)\nadj. (指葡萄酒)优质的，上等的，佳酿的;古色古香的(指1917–1930年间制造，车型和品味受人青睐的);(过去某个时期)典型的，优质的;(某人的)最佳作品的\n72、单元测试-常用测试注解 官方文档 - Annotations\n@Test：表示方法是测试方法。但是与JUnit4的@Test不同，他的职责非常单一不能声明任何属性，拓展的测试将会由Jupiter提供额外测试 @ParameterizedTest：表示方法是参数化测试。 @RepeatedTest：表示方法可重复执行。 @DisplayName：为测试类或者测试方法设置展示名称。 @BeforeEach：表示在每个单元测试之前执行。 @AfterEach：表示在每个单元测试之后执行。 @BeforeAll：表示在所有单元测试之前执行。 @AfterAll：表示在所有单元测试之后执行。 @Tag：表示单元测试类别，类似于JUnit4中的@Categories。 @Disabled：表示测试类或测试方法不执行，类似于JUnit4中的@Ignore。 @Timeout：表示测试方法运行如果超过了指定时间将会返回错误。 @ExtendWith：为测试类或测试方法提供扩展类引用。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import org.junit.jupiter.api.*; @DisplayName(\u0026#34;junit5功能测试类\u0026#34;) public class Junit5Test { @DisplayName(\u0026#34;测试displayname注解\u0026#34;) @Test void testDisplayName() { System.out.println(1); System.out.println(jdbcTemplate); } @ParameterizedTest @ValueSource(strings = { \u0026#34;racecar\u0026#34;, \u0026#34;radar\u0026#34;, \u0026#34;able was I ere I saw elba\u0026#34; }) void palindromes(String candidate) { assertTrue(StringUtils.isPalindrome(candidate)); } @Disabled @DisplayName(\u0026#34;测试方法2\u0026#34;) @Test void test2() { System.out.println(2); } @RepeatedTest(5) @Test void test3() { System.out.println(5); } /** * 规定方法超时时间。超出时间测试出异常 * * @throws InterruptedException */ @Timeout(value = 500, unit = TimeUnit.MILLISECONDS) @Test void testTimeout() throws InterruptedException { Thread.sleep(600); } @BeforeEach void testBeforeEach() { System.out.println(\u0026#34;测试就要开始了...\u0026#34;); } @AfterEach void testAfterEach() { System.out.println(\u0026#34;测试结束了...\u0026#34;); } @BeforeAll static void testBeforeAll() { System.out.println(\u0026#34;所有测试就要开始了...\u0026#34;); } @AfterAll static void testAfterAll() { System.out.println(\u0026#34;所有测试以及结束了...\u0026#34;); } } 73、单元测试-断言机制 断言Assertion是测试方法中的核心部分，用来对测试需要满足的条件进行验证。这些断言方法都是org.junit.jupiter.api.Assertions的静态方法。检查业务逻辑返回的数据是否合理。所有的测试运行结束以后，会有一个详细的测试报告。\nJUnit 5 内置的断言可以分成如下几个类别：\n简单断言 用来对单个值进行简单的验证。如：\n方法 说明 assertEquals 判断两个对象或两个原始类型是否相等 assertNotEquals 判断两个对象或两个原始类型是否不相等 assertSame 判断两个对象引用是否指向同一个对象 assertNotSame 判断两个对象引用是否指向不同的对象 assertTrue 判断给定的布尔值是否为 true assertFalse 判断给定的布尔值是否为 false assertNull 判断给定的对象引用是否为 null assertNotNull 判断给定的对象引用是否不为 null 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test @DisplayName(\u0026#34;simple assertion\u0026#34;) public void simple() { assertEquals(3, 1 + 2, \u0026#34;simple math\u0026#34;); assertNotEquals(3, 1 + 1); assertNotSame(new Object(), new Object()); Object obj = new Object(); assertSame(obj, obj); assertFalse(1 \u0026gt; 2); assertTrue(1 \u0026lt; 2); assertNull(null); assertNotNull(new Object()); } 数组断言 通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等。\n1 2 3 4 5 @Test @DisplayName(\u0026#34;array assertion\u0026#34;) public void array() { assertArrayEquals(new int[]{1, 2}, new int[] {1, 2}); } 组合断言 assertAll()方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言，可以通过 lambda 表达式很容易的提供这些断言。\n1 2 3 4 5 6 7 8 @Test @DisplayName(\u0026#34;assert all\u0026#34;) public void all() { assertAll(\u0026#34;Math\u0026#34;, () -\u0026gt; assertEquals(2, 1 + 1), () -\u0026gt; assertTrue(1 \u0026gt; 0) ); } 异常断言 在JUnit4时期，想要测试方法的异常情况时，需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows()，配合函数式编程就可以进行使用。\n1 2 3 4 5 6 7 @Test @DisplayName(\u0026#34;异常测试\u0026#34;) public void exceptionTest() { ArithmeticException exception = Assertions.assertThrows( //扔出断言异常 ArithmeticException.class, () -\u0026gt; System.out.println(1 % 0)); } 超时断言 JUnit5还提供了Assertions.assertTimeout()为测试方法设置了超时时间。\n1 2 3 4 5 6 @Test @DisplayName(\u0026#34;超时测试\u0026#34;) public void timeoutTest() { //如果测试方法时间超过1s将会异常 Assertions.assertTimeout(Duration.ofMillis(1000), () -\u0026gt; Thread.sleep(500)); } 快速失败 通过 fail 方法直接使得测试失败。\n1 2 3 4 5 @Test @DisplayName(\u0026#34;fail\u0026#34;) public void shouldFail() { fail(\u0026#34;This should fail\u0026#34;); } 74、单元测试-前置条件 Unit 5 中的前置条件（assumptions【假设】）类似于断言，不同之处在于不满足的断言assertions会使得测试方法失败，而不满足的前置条件只会使得测试方法的执行终止。\n前置条件可以看成是测试方法执行的前提，当该前提不满足时，就没有继续执行的必要。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @DisplayName(\u0026#34;前置条件\u0026#34;) public class AssumptionsTest { private final String environment = \u0026#34;DEV\u0026#34;; @Test @DisplayName(\u0026#34;simple\u0026#34;) public void simpleAssume() { assumeTrue(Objects.equals(this.environment, \u0026#34;DEV\u0026#34;)); assumeFalse(() -\u0026gt; Objects.equals(this.environment, \u0026#34;PROD\u0026#34;)); } @Test @DisplayName(\u0026#34;assume then do\u0026#34;) public void assumeThenDo() { assumingThat( Objects.equals(this.environment, \u0026#34;DEV\u0026#34;), () -\u0026gt; System.out.println(\u0026#34;In DEV\u0026#34;) ); } } assumeTrue 和 assumFalse 确保给定的条件为 true 或 false，不满足条件会使得测试执行终止。\nassumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时，Executable 对象才会被执行；当条件不满足时，测试执行并不会终止。\n75、单元测试-嵌套测试 官方文档 - Nested Tests\nJUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试，从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach注解，而且嵌套的层次没有限制。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 @DisplayName(\u0026#34;A stack\u0026#34;) class TestingAStackDemo { Stack\u0026lt;Object\u0026gt; stack; @Test @DisplayName(\u0026#34;is instantiated with new Stack()\u0026#34;) void isInstantiatedWithNew() { new Stack\u0026lt;\u0026gt;(); } @Nested @DisplayName(\u0026#34;when new\u0026#34;) class WhenNew { @BeforeEach void createNewStack() { stack = new Stack\u0026lt;\u0026gt;(); } @Test @DisplayName(\u0026#34;is empty\u0026#34;) void isEmpty() { assertTrue(stack.isEmpty()); } @Test @DisplayName(\u0026#34;throws EmptyStackException when popped\u0026#34;) void throwsExceptionWhenPopped() { assertThrows(EmptyStackException.class, stack::pop); } @Test @DisplayName(\u0026#34;throws EmptyStackException when peeked\u0026#34;) void throwsExceptionWhenPeeked() { assertThrows(EmptyStackException.class, stack::peek); } @Nested @DisplayName(\u0026#34;after pushing an element\u0026#34;) class AfterPushing { String anElement = \u0026#34;an element\u0026#34;; @BeforeEach void pushAnElement() { stack.push(anElement); } @Test @DisplayName(\u0026#34;it is no longer empty\u0026#34;) void isNotEmpty() { assertFalse(stack.isEmpty()); } @Test @DisplayName(\u0026#34;returns the element when popped and is empty\u0026#34;) void returnElementWhenPopped() { assertEquals(anElement, stack.pop()); assertTrue(stack.isEmpty()); } @Test @DisplayName(\u0026#34;returns the element when peeked but remains not empty\u0026#34;) void returnElementWhenPeeked() { assertEquals(anElement, stack.peek()); assertFalse(stack.isEmpty()); } } } } 76、单元测试-参数化测试 官方文档 - Parameterized Tests\n参数化测试是JUnit5很重要的一个新特性，它使得用不同的参数多次运行测试成为了可能，也为我们的单元测试带来许多便利。\n利用@ValueSource等注解，指定入参，我们将可以使用不同的参数进行多次单元测试，而不需要每新增一个参数就新增一个单元测试，省去了很多冗余代码。\n利用**@ValueSource**等注解，指定入参，我们将可以使用不同的参数进行多次单元测试，而不需要每新增一个参数就新增一个单元测试，省去了很多冗余代码。\n@ValueSource: 为参数化测试指定入参来源，支持八大基础类以及String类型,Class类型 @NullSource: 表示为参数化测试提供一个null的入参 @EnumSource: 表示为参数化测试提供一个枚举入参 @CsvFileSource：表示读取指定CSV文件内容作为参数化测试入参 @MethodSource：表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流) 当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现**ArgumentsProvider**接口，任何外部文件都可以作为它的入参。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @ParameterizedTest @ValueSource(strings = {\u0026#34;one\u0026#34;, \u0026#34;two\u0026#34;, \u0026#34;three\u0026#34;}) @DisplayName(\u0026#34;参数化测试1\u0026#34;) public void parameterizedTest1(String string) { System.out.println(string); Assertions.assertTrue(StringUtils.isNotBlank(string)); } @ParameterizedTest @MethodSource(\u0026#34;method\u0026#34;) //指定方法名 @DisplayName(\u0026#34;方法来源参数\u0026#34;) public void testWithExplicitLocalMethodSource(String name) { System.out.println(name); Assertions.assertNotNull(name); } static Stream\u0026lt;String\u0026gt; method() { return Stream.of(\u0026#34;apple\u0026#34;, \u0026#34;banana\u0026#34;); } 迁移指南 官方文档 - Migrating from JUnit 4\n在进行迁移的时候需要注意如下的变化：\n注解在 org.junit.jupiter.api 包中，断言在 org.junit.jupiter.api.Assertions 类中，前置条件在 org.junit.jupiter.api.Assumptions 类中。 把@Before 和@After 替换成@BeforeEach 和@AfterEach。 把@BeforeClass 和@AfterClass 替换成@BeforeAll 和@AfterAll。 把@Ignore 替换成@Disabled。 把@Category 替换成@Tag。 把@RunWith、@Rule 和@ClassRule 替换成@ExtendWith。 77、指标监控-SpringBoot Actuator与Endpoint 未来每一个微服务在云上部署以后，我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景，使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。\n官方文档 - Spring Boot Actuator: Production-ready Features\n1.x与2.x的不同：\nSpringBoot Actuator 1.x\n支持SpringMVC 基于继承方式进行扩展 层级Metrics配置 自定义Metrics收集 默认较少的安全策略 SpringBoot Actuator 2.x\n支持SpringMVC、JAX-RS以及Webflux 注解驱动进行扩展 层级\u0026amp;名称空间Metrics 底层使用MicroMeter，强大、便捷默认丰富的安全策略 如何使用 添加依赖： 1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-actuator\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 访问http://localhost:8080/actuator/**。 暴露所有监控信息为HTTP。 1 2 3 4 5 6 management: endpoints: enabled-by-default: true #暴露所有端点信息 web: exposure: include: \u0026#39;*\u0026#39; #以web方式暴露 测试例子 http://localhost:8080/actuator/beans http://localhost:8080/actuator/configprops http://localhost:8080/actuator/metrics http://localhost:8080/actuator/metrics/jvm.gc.pause http://localhost:8080/actuator/metrics/endpointName/detailPath actuator\n英 [ˈæktjʊeɪtə] 美 [ˈæktjuˌeɪtər]\nn. 致动（促动，激励，调节）器；传动（装置，机构）；拖动装置；马达；操作机构；执行机构（元件）；（电磁铁）螺线管；操纵装置（阀门）；调速控制器；往复运动油（气）缸；作动筒\nmetric\n英 [ˈmetrɪk] 美 [ˈmetrɪk]\nadj. 米制的;公制的;按公制制作的;用公制测量的\nn. 度量标准;[数学]度量;诗体;韵文;诗韵\n78、指标监控-常使用的端点及开启与禁用 常使用的端点 ID 描述 auditevents 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件。 beans 显示应用程序中所有Spring Bean的完整列表。 caches 暴露可用的缓存。 conditions 显示自动配置的所有条件信息，包括匹配或不匹配的原因。 configprops 显示所有@ConfigurationProperties。 env 暴露Spring的属性ConfigurableEnvironment flyway 显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。 health 显示应用程序运行状况信息。 httptrace 显示HTTP跟踪信息（默认情况下，最近100个HTTP请求-响应）。需要一个HttpTraceRepository组件。 info 显示应用程序信息。 integrationgraph 显示Spring integrationgraph 。需要依赖spring-integration-core。 loggers 显示和修改应用程序中日志的配置。 liquibase 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。 metrics 显示当前应用程序的“指标”信息。 mappings 显示所有@RequestMapping路径列表。 scheduledtasks 显示应用程序中的计划任务。 sessions 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。 shutdown 使应用程序正常关闭。默认禁用。 startup 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup。 threaddump 执行线程转储。 如果您的应用程序是Web应用程序（Spring MVC，Spring WebFlux或Jersey），则可以使用以下附加端点：\nID 描述 heapdump 返回hprof堆转储文件。 jolokia 通过HTTP暴露JMX bean（需要引入Jolokia，不适用于WebFlux）。需要引入依赖jolokia-core。 logfile 返回日志文件的内容（如果已设置logging.file.name或logging.file.path属性）。支持使用HTTPRange标头来检索部分日志文件的内容。 prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus。 其中最常用的Endpoint：\nHealth：监控状况 Metrics：运行时指标 Loggers：日志记录 Health Endpoint 健康检查端点，我们一般用于在云平台，平台会定时的检查应用的健康状况，我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。\n重要的几点：\nhealth endpoint返回的结果，应该是一系列健康检查后的一个汇总报告。 很多的健康检查默认已经自动配置好了，比如：数据库、redis等。 可以很容易的添加自定义的健康检查机制。 Metrics Endpoint 提供详细的、层级的、空间指标信息，这些信息可以被pull（主动推送）或者push（被动获取）方式得到：\n通过Metrics对接多种监控系统。 简化核心Metrics开发。 添加自定义Metrics或者扩展已有Metrics。 开启与禁用Endpoints 默认所有的Endpoint除过shutdown都是开启的。 需要开启或者禁用某个Endpoint。配置模式为management.endpoint.\u0026lt;endpointName\u0026gt;.enabled = true 1 2 3 4 management: endpoint: beans: enabled: true 或者禁用所有的Endpoint然后手动开启指定的Endpoint。 1 2 3 4 5 6 7 8 management: endpoints: enabled-by-default: false endpoint: beans: enabled: true health: enabled: true 暴露Endpoints 支持的暴露方式\nHTTP：默认只暴露health和info。 JMX：默认暴露所有Endpoint。 除过health和info，剩下的Endpoint都应该进行保护访问。如果引入Spring Security，则会默认配置安全访问规则。 ID JMX Web auditevents Yes No beans Yes No caches Yes No conditions Yes No configprops Yes No env Yes No flyway Yes No health Yes Yes heapdump N/A No httptrace Yes No info Yes Yes integrationgraph Yes No jolokia N/A No logfile N/A No loggers Yes No liquibase Yes No metrics Yes No mappings Yes No prometheus N/A No scheduledtasks Yes No sessions Yes No shutdown Yes No startup Yes No threaddump Yes No 若要更改公开的Endpoint，请配置以下的包含和排除属性：\nProperty Default management.endpoints.jmx.exposure.exclude management.endpoints.jmx.exposure.include * management.endpoints.web.exposure.exclude management.endpoints.web.exposure.include info, health 官方文档 - Exposing Endpoints\n79、指标监控-定制Endpoint 定制 Health 信息 1 2 3 4 management: health: enabled: true show-details: always #总是显示详细信息。可显示每个模块的状态信息 通过实现HealthIndicator 接口，或继承MyComHealthIndicator 类。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.stereotype.Component; @Component public class MyHealthIndicator implements HealthIndicator { @Override public Health health() { int errorCode = check(); // perform some specific health check if (errorCode != 0) { return Health.down().withDetail(\u0026#34;Error Code\u0026#34;, errorCode).build(); } return Health.up().build(); } } /* 构建Health Health build = Health.down() .withDetail(\u0026#34;msg\u0026#34;, \u0026#34;error service\u0026#34;) .withDetail(\u0026#34;code\u0026#34;, \u0026#34;500\u0026#34;) .withException(new RuntimeException()) .build(); */ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Component public class MyComHealthIndicator extends AbstractHealthIndicator { /** * 真实的检查方法 * @param builder * @throws Exception */ @Override protected void doHealthCheck(Health.Builder builder) throws Exception { //mongodb。 获取连接进行测试 Map\u0026lt;String,Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); // 检查完成 if(1 == 2){ // builder.up(); //健康 builder.status(Status.UP); map.put(\u0026#34;count\u0026#34;,1); map.put(\u0026#34;ms\u0026#34;,100); }else { // builder.down(); builder.status(Status.OUT_OF_SERVICE); map.put(\u0026#34;err\u0026#34;,\u0026#34;连接超时\u0026#34;); map.put(\u0026#34;ms\u0026#34;,3000); } builder.withDetail(\u0026#34;code\u0026#34;,100) .withDetails(map); } } 定制info信息 常用两种方式：\n编写配置文件 1 2 3 4 5 info: appName: boot-admin version: 2.0.1 mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值 mavenProjectVersion: @project.version@ 编写InfoContributor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.util.Collections; import org.springframework.boot.actuate.info.Info; import org.springframework.boot.actuate.info.InfoContributor; import org.springframework.stereotype.Component; @Component public class ExampleInfoContributor implements InfoContributor { @Override public void contribute(Info.Builder builder) { builder.withDetail(\u0026#34;example\u0026#34;, Collections.singletonMap(\u0026#34;key\u0026#34;, \u0026#34;value\u0026#34;)); } } http://localhost:8080/actuator/info 会输出以上方式返回的所有info信息\n定制Metrics信息 Spring Boot支持的metrics\n增加定制Metrics：\n1 2 3 4 5 6 7 8 9 10 class MyService{ Counter counter; public MyService(MeterRegistry meterRegistry){ counter = meterRegistry.counter(\u0026#34;myservice.method.running.counter\u0026#34;); } public void hello() { counter.increment(); } } 1 2 3 4 5 //也可以使用下面的方式 @Bean MeterBinder queueSize(Queue queue) { return (registry) -\u0026gt; Gauge.builder(\u0026#34;queueSize\u0026#34;, queue::size).register(registry); } 定制Endpoint 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component @Endpoint(id = \u0026#34;container\u0026#34;) public class DockerEndpoint { @ReadOperation public Map getDockerInfo(){ return Collections.singletonMap(\u0026#34;info\u0026#34;,\u0026#34;docker started...\u0026#34;); } @WriteOperation private void restartDocker(){ System.out.println(\u0026#34;docker restarted....\u0026#34;); } } 场景：\n开发ReadinessEndpoint来管理程序是否就绪。 开发LivenessEndpoint来管理程序是否存活。 80、指标监控-Boot Admin Server 官方Github\n官方文档\n可视化指标监控\nWhat is Spring Boot Admin?\ncodecentric’s Spring Boot Admin is a community project to manage and monitor your Spring Boot ® applications. The applications register with our Spring Boot Admin Client (via HTTP) or are discovered using Spring Cloud ® (e.g. Eureka, Consul). The UI is just a Vue.js application on top of the Spring Boot Actuator endpoints.\n开始使用方法\n81、高级特性-Profile环境切换 为了方便多环境适配，Spring Boot简化了profile功能。\n默认配置文件application.yaml任何时候都会加载。 指定环境配置文件application-{env}.yaml，env通常替代为test， 激活指定环境 配置文件激活：spring.profiles.active=prod 命令行激活：java -jar xxx.jar --spring.profiles.active=prod --person.name=haha（修改配置文件的任意值，命令行优先） 默认配置与环境配置同时生效 同名配置项，profile配置优先 @Profile条件装配功能 1 2 3 4 5 6 7 @Data @Component @ConfigurationProperties(\u0026#34;person\u0026#34;)//在配置文件中配置 public class Person{ private String name; private Integer age; } application.properties\n1 2 3 person: name: lun age: 8 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public interface Person { String getName(); Integer getAge(); } @Profile(\u0026#34;test\u0026#34;)//加载application-test.yaml里的 @Component @ConfigurationProperties(\u0026#34;person\u0026#34;) @Data public class Worker implements Person { private String name; private Integer age; } @Profile(value = {\u0026#34;prod\u0026#34;,\u0026#34;default\u0026#34;})//加载application-prod.yaml里的 @Component @ConfigurationProperties(\u0026#34;person\u0026#34;) @Data public class Boss implements Person { private String name; private Integer age; } application-test.yaml\n1 2 3 4 5 person: name: test-张三 server: port: 7000 application-prod.yaml\n1 2 3 4 5 person: name: prod-张三 server: port: 8000 application.properties\n1 2 # 激活prod配置文件 spring.profiles.active=prod 1 2 3 4 5 6 7 8 @Autowired private Person person; @GetMapping(\u0026#34;/\u0026#34;) public String hello(){ //激活了prod，则返回Boss；激活了test，则返回Worker return person.getClass().toString(); } @Profile还可以修饰在方法上：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Color { } @Configuration public class MyConfig { @Profile(\u0026#34;prod\u0026#34;) @Bean public Color red(){ return new Color(); } @Profile(\u0026#34;test\u0026#34;) @Bean public Color green(){ return new Color(); } } 可以激活一组：\n1 2 3 4 spring.profiles.active=production spring.profiles.group.production[0]=proddb spring.profiles.group.production[1]=prodmq 82、高级特性-配置加载优先级 外部化配置 官方文档 - Externalized Configuration\nSpring Boot uses a very particular PropertySource order that is designed to allow sensible overriding of values. Properties are considered in the following order (with values from lower items overriding earlier ones)（1优先级最低，14优先级最高）:\nDefault properties (specified by setting SpringApplication.setDefaultProperties). @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins. Config data (such as application.properties files) A RandomValuePropertySource that has properties only in random.*. OS environment variables. Java System properties (System.getProperties()). JNDI attributes from java:comp/env. ServletContext init parameters. ServletConfig init parameters. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property). Command line arguments. properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application. @TestPropertySource annotations on your tests. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active. 1 2 3 4 5 6 7 8 9 10 11 12 import org.springframework.stereotype.*; import org.springframework.beans.factory.annotation.*; @Component public class MyBean { @Value(\u0026#34;${name}\u0026#34;)//以这种方式可以获得配置值 private String name; // ... } 外部配置源 Java属性文件。 YAML文件。 环境变量。 命令行参数。 配置文件查找位置 classpath 根路径。 classpath 根路径下config目录。 jar包当前目录。 jar包当前目录的config目录。 /config子目录的直接子目录。 配置文件加载顺序： 当前jar包内部的application.properties和application.yml。 当前jar包内部的application-{profile}.properties 和 application-{profile}.yml。 引用的外部jar包的application.properties和application.yml。 引用的外部jar包的application-{profile}.properties和application-{profile}.yml。 指定环境优先，外部优先，后面的可以覆盖前面的同名配置项。 83、高级特性-自定义starter细节 starter启动原理 starter的pom.xml引入autoconfigure依赖 1 2 3 graph LR A[starter] --\u0026gt;B[autoconfigure] B --\u0026gt; C[spring-boot-starter] autoconfigure包中配置使用META-INF/spring.factories中EnableAutoConfiguration的值，使得项目启动加载指定的自动配置类\n编写自动配置类 xxxAutoConfiguration -\u0026gt; xxxxProperties\n@Configuration @Conditional @EnableConfigurationProperties @Bean \u0026hellip;\u0026hellip; 引入starter \u0026mdash; xxxAutoConfiguration \u0026mdash; 容器中放入组件 \u0026mdash;- 绑定xxxProperties \u0026mdash;- 配置项\n自定义starter 目标：创建HelloService的自定义starter。\n创建两个工程，分别命名为hello-spring-boot-starter（普通Maven工程），hello-spring-boot-starter-autoconfigure（需用用到Spring Initializr创建的Maven工程）。\nhello-spring-boot-starter无需编写什么代码，只需让该工程引入hello-spring-boot-starter-autoconfigure依赖：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;groupId\u0026gt;com.lun\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hello-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.lun\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hello-spring-boot-starter-autoconfigure\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; hello-spring-boot-starter-autoconfigure的pom.xml如下： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.4.2\u0026lt;/version\u0026gt; \u0026lt;relativePath/\u0026gt; \u0026lt;!-- lookup parent from repository --\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;groupId\u0026gt;com.lun\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hello-spring-boot-starter-autoconfigure\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;name\u0026gt;hello-spring-boot-starter-autoconfigure\u0026lt;/name\u0026gt; \u0026lt;description\u0026gt;Demo project for Spring Boot\u0026lt;/description\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;java.version\u0026gt;1.8\u0026lt;/java.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/project\u0026gt; 创建4个文件： com/lun/hello/auto/HelloServiceAutoConfiguration com/lun/hello/bean/HelloProperties com/lun/hello/service/HelloService src/main/resources/META-INF/spring.factories 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import com.lun.hello.bean.HelloProperties; import com.lun.hello.service.HelloService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConditionalOnMissingBean(HelloService.class) @EnableConfigurationProperties(HelloProperties.class)//默认HelloProperties放在容器中 public class HelloServiceAutoConfiguration { @Bean public HelloService helloService(){ return new HelloService(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(\u0026#34;hello\u0026#34;) public class HelloProperties { private String prefix; private String suffix; public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public String getSuffix() { return suffix; } public void setSuffix(String suffix) { this.suffix = suffix; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import com.lun.hello.bean.HelloProperties; import org.springframework.beans.factory.annotation.Autowired; /** * 默认不要放在容器中 */ public class HelloService { @Autowired private HelloProperties helloProperties; public String sayHello(String userName){ return helloProperties.getPrefix() + \u0026#34;: \u0026#34; + userName + \u0026#34; \u0026gt; \u0026#34; + helloProperties.getSuffix(); } } 1 2 3 # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\ com.lun.hello.auto.HelloServiceAutoConfiguration 用maven插件，将两工程install到本地。\n接下来，测试使用自定义starter，用Spring Initializr创建名为hello-spring-boot-starter-test工程，引入hello-spring-boot-starter依赖，其pom.xml如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;project xmlns=\u0026#34;http://maven.apache.org/POM/4.0.0\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd\u0026#34;\u0026gt; \u0026lt;modelVersion\u0026gt;4.0.0\u0026lt;/modelVersion\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.4.2\u0026lt;/version\u0026gt; \u0026lt;relativePath/\u0026gt; \u0026lt;!-- lookup parent from repository --\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;groupId\u0026gt;com.lun\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hello-spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;name\u0026gt;hello-spring-boot-starter-test\u0026lt;/name\u0026gt; \u0026lt;description\u0026gt;Demo project for Spring Boot\u0026lt;/description\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;java.version\u0026gt;1.8\u0026lt;/java.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 引入`hello-spring-boot-starter`依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.lun\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hello-spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0-SNAPSHOT\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; \u0026lt;/project\u0026gt; 添加配置文件application.properties： 1 2 hello.prefix=hello hello.suffix=666 添加单元测试类： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import com.lun.hello.service.HelloService;//来自自定义starter import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class HelloSpringBootStarterTestApplicationTests { @Autowired private HelloService helloService; @Test void contextLoads() { // System.out.println(helloService.sayHello(\u0026#34;lun\u0026#34;)); Assertions.assertEquals(\u0026#34;hello: lun \u0026gt; 666\u0026#34;, helloService.sayHello(\u0026#34;lun\u0026#34;)); } } 84、原理解析-SpringApplication创建初始化流程 SpringBoot启动过程 Spring Boot应用的启动类：\n1 2 3 4 5 6 7 8 9 10 11 import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class HelloSpringBootStarterTestApplication { public static void main(String[] args) { SpringApplication.run(HelloSpringBootStarterTestApplication.class, args); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public class SpringApplication { ... public static ConfigurableApplicationContext run(Class\u0026lt;?\u0026gt; primarySource, String... args) { return run(new Class\u0026lt;?\u0026gt;[] { primarySource }, args); } public static ConfigurableApplicationContext run(Class\u0026lt;?\u0026gt;[] primarySources, String[] args) { return new SpringApplication(primarySources).run(args); } //先看看new SpringApplication(primarySources)，下一节再看看run() public SpringApplication(Class\u0026lt;?\u0026gt;... primarySources) { this(null, primarySources); } public SpringApplication(ResourceLoader resourceLoader, Class\u0026lt;?\u0026gt;... primarySources) { this.resourceLoader = resourceLoader; Assert.notNull(primarySources, \u0026#34;PrimarySources must not be null\u0026#34;); this.primarySources = new LinkedHashSet\u0026lt;\u0026gt;(Arrays.asList(primarySources)); //WebApplicationType是枚举类，有NONE,SERVLET,REACTIVE,下行webApplicationType是SERVLET this.webApplicationType = WebApplicationType.deduceFromClasspath(); //初始启动引导器，去spring.factories文件中找org.springframework.boot.Bootstrapper，但我找不到实现Bootstrapper接口的类 this.bootstrappers = new ArrayList\u0026lt;\u0026gt;(getSpringFactoriesInstances(Bootstrapper.class)); //去spring.factories找 ApplicationContextInitializer setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); //去spring.factories找 ApplicationListener setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); this.mainApplicationClass = deduceMainApplicationClass(); } private Class\u0026lt;?\u0026gt; deduceMainApplicationClass() { try { StackTraceElement[] stackTrace = new RuntimeException().getStackTrace(); for (StackTraceElement stackTraceElement : stackTrace) { if (\u0026#34;main\u0026#34;.equals(stackTraceElement.getMethodName())) { return Class.forName(stackTraceElement.getClassName()); } } } catch (ClassNotFoundException ex) { // Swallow and continue } return null; } ... } spring.factories：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ... # Application Context Initializers org.springframework.context.ApplicationContextInitializer=\\ org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\\ org.springframework.boot.context.ContextIdApplicationContextInitializer,\\ org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\\ org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\\ org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer # Application Listeners org.springframework.context.ApplicationListener=\\ org.springframework.boot.ClearCachesApplicationListener,\\ org.springframework.boot.builder.ParentContextCloserApplicationListener,\\ org.springframework.boot.context.FileEncodingApplicationListener,\\ org.springframework.boot.context.config.AnsiOutputApplicationListener,\\ org.springframework.boot.context.config.DelegatingApplicationListener,\\ org.springframework.boot.context.logging.LoggingApplicationListener,\\ org.springframework.boot.env.EnvironmentPostProcessorApplicationListener,\\ org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener ... 85、原理解析-SpringBoot完整启动过程 继续上一节，接着讨论return new SpringApplication(primarySources).run(args)的run方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 public class SpringApplication { ... public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch();//开始计时器 stopWatch.start();//开始计时 //1. //创建引导上下文（Context环境）createBootstrapContext() //获取到所有之前的 bootstrappers 挨个执行 intitialize() 来完成对引导启动器上下文环境设置 DefaultBootstrapContext bootstrapContext = createBootstrapContext(); //2.到最后该方法会返回这context ConfigurableApplicationContext context = null; //3.让当前应用进入headless模式 configureHeadlessProperty(); //4.获取所有 RunListener（运行监听器）,为了方便所有Listener进行事件感知 SpringApplicationRunListeners listeners = getRunListeners(args); //5. 遍历 SpringApplicationRunListener 调用 starting 方法； // 相当于通知所有感兴趣系统正在启动过程的人，项目正在 starting。 listeners.starting(bootstrapContext, this.mainApplicationClass); try { //6.保存命令行参数 ApplicationArguments ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); //7.准备环境 ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments); configureIgnoreBeanInfo(environment); /*打印标志 . ____ _ __ _ _ /\\\\ / ___\u0026#39;_ __ _ _(_)_ __ __ _ \\ \\ \\ \\ ( ( )\\___ | \u0026#39;_ | \u0026#39;_| | \u0026#39;_ \\/ _` | \\ \\ \\ \\ \\\\/ ___)| |_)| | | | | || (_| | ) ) ) ) \u0026#39; |____| .__|_| |_|_| |_\\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.4.2) */ Banner printedBanner = printBanner(environment); // 创建IOC容器（createApplicationContext（）） // 根据项目类型webApplicationType（NONE,SERVLET,REACTIVE）创建容器， // 当前会创建 AnnotationConfigServletWebServerApplicationContext context = createApplicationContext(); context.setApplicationStartup(this.applicationStartup); //8.准备ApplicationContext IOC容器的基本信息 prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner); //9.刷新IOC容器,创建容器中的所有组件,Spring框架的内容 refreshContext(context); //该方法没内容，大概为将来填入 afterRefresh(context, applicationArguments); stopWatch.stop();//停止计时 if (this.logStartupInfo) {//this.logStartupInfo默认是true new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch); } //10. listeners.started(context); //11.调用所有runners callRunners(context, applicationArguments); } catch (Throwable ex) { //13. handleRunFailure(context, ex, listeners); throw new IllegalStateException(ex); } try { //12. listeners.running(context); } catch (Throwable ex) { //13. handleRunFailure(context, ex, null); throw new IllegalStateException(ex); } return context; } //1. private DefaultBootstrapContext createBootstrapContext() { DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext(); this.bootstrappers.forEach((initializer) -\u0026gt; initializer.intitialize(bootstrapContext)); return bootstrapContext; } //3. private void configureHeadlessProperty() { //this.headless默认为true System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless))); } private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = \u0026#34;java.awt.headless\u0026#34;; //4. private SpringApplicationRunListeners getRunListeners(String[] args) { Class\u0026lt;?\u0026gt;[] types = new Class\u0026lt;?\u0026gt;[] { SpringApplication.class, String[].class }; //getSpringFactoriesInstances 去 spring.factories 找 SpringApplicationRunListener return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args), this.applicationStartup); } //7.准备环境 private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners, DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) { // Create and configure the environment //返回或者创建基础环境信息对象，如：StandardServletEnvironment, StandardReactiveWebEnvironment ConfigurableEnvironment environment = getOrCreateEnvironment(); //配置环境信息对象,读取所有的配置源的配置属性值。 configureEnvironment(environment, applicationArguments.getSourceArgs()); //绑定环境信息 ConfigurationPropertySources.attach(environment); //7.1 通知所有的监听器当前环境准备完成 listeners.environmentPrepared(bootstrapContext, environment); DefaultPropertiesPropertySource.moveToEnd(environment); configureAdditionalProfiles(environment); bindToSpringApplication(environment); if (!this.isCustomEnvironment) { environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment, deduceEnvironmentClass()); } ConfigurationPropertySources.attach(environment); return environment; } //8. private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) { //保存环境信息 context.setEnvironment(environment); //IOC容器的后置处理流程 postProcessApplicationContext(context); //应用初始化器 applyInitializers(context); //8.1 遍历所有的 listener 调用 contextPrepared。 //EventPublishRunListenr通知所有的监听器contextPrepared listeners.contextPrepared(context); bootstrapContext.close(context); if (this.logStartupInfo) { logStartupInfo(context.getParent() == null); logStartupProfileInfo(context); } // Add boot specific singleton beans ConfigurableListableBeanFactory beanFactory = context.getBeanFactory(); beanFactory.registerSingleton(\u0026#34;springApplicationArguments\u0026#34;, applicationArguments); if (printedBanner != null) { beanFactory.registerSingleton(\u0026#34;springBootBanner\u0026#34;, printedBanner); } if (beanFactory instanceof DefaultListableBeanFactory) { ((DefaultListableBeanFactory) beanFactory) .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); } if (this.lazyInitialization) { context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor()); } // Load the sources Set\u0026lt;Object\u0026gt; sources = getAllSources(); Assert.notEmpty(sources, \u0026#34;Sources must not be empty\u0026#34;); load(context, sources.toArray(new Object[0])); //8.2 listeners.contextLoaded(context); } //11.调用所有runners private void callRunners(ApplicationContext context, ApplicationArguments args) { List\u0026lt;Object\u0026gt; runners = new ArrayList\u0026lt;\u0026gt;(); //获取容器中的 ApplicationRunner runners.addAll(context.getBeansOfType(ApplicationRunner.class).values()); //获取容器中的 CommandLineRunner runners.addAll(context.getBeansOfType(CommandLineRunner.class).values()); //合并所有runner并且按照@Order进行排序 AnnotationAwareOrderComparator.sort(runners); //遍历所有的runner。调用 run 方法 for (Object runner : new LinkedHashSet\u0026lt;\u0026gt;(runners)) { if (runner instanceof ApplicationRunner) { callRunner((ApplicationRunner) runner, args); } if (runner instanceof CommandLineRunner) { callRunner((CommandLineRunner) runner, args); } } } //13. private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception, SpringApplicationRunListeners listeners) { try { try { handleExitCode(context, exception); if (listeners != null) { //14. listeners.failed(context, exception); } } finally { reportFailure(getExceptionReporters(context), exception); if (context != null) { context.close(); } } } catch (Exception ex) { logger.warn(\u0026#34;Unable to close ApplicationContext\u0026#34;, ex); } ReflectionUtils.rethrowRuntimeException(exception); } ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 //2. new SpringApplication(primarySources).run(args) 最后返回的接口类型 public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable { String CONFIG_LOCATION_DELIMITERS = \u0026#34;,; \\t\\n\u0026#34;; String CONVERSION_SERVICE_BEAN_NAME = \u0026#34;conversionService\u0026#34;; String LOAD_TIME_WEAVER_BEAN_NAME = \u0026#34;loadTimeWeaver\u0026#34;; String ENVIRONMENT_BEAN_NAME = \u0026#34;environment\u0026#34;; String SYSTEM_PROPERTIES_BEAN_NAME = \u0026#34;systemProperties\u0026#34;; String SYSTEM_ENVIRONMENT_BEAN_NAME = \u0026#34;systemEnvironment\u0026#34;; String APPLICATION_STARTUP_BEAN_NAME = \u0026#34;applicationStartup\u0026#34;; String SHUTDOWN_HOOK_THREAD_NAME = \u0026#34;SpringContextShutdownHook\u0026#34;; void setId(String var1); void setParent(@Nullable ApplicationContext var1); void setEnvironment(ConfigurableEnvironment var1); ConfigurableEnvironment getEnvironment(); void setApplicationStartup(ApplicationStartup var1); ApplicationStartup getApplicationStartup(); void addBeanFactoryPostProcessor(BeanFactoryPostProcessor var1); void addApplicationListener(ApplicationListener\u0026lt;?\u0026gt; var1); void setClassLoader(ClassLoader var1); void addProtocolResolver(ProtocolResolver var1); void refresh() throws BeansException, IllegalStateException; void registerShutdownHook(); void close(); boolean isActive(); ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException; } 1 2 3 4 5 #4. #spring.factories # Run Listeners org.springframework.boot.SpringApplicationRunListener=\\ org.springframework.boot.context.event.EventPublishingRunListener 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 class SpringApplicationRunListeners { private final Log log; private final List\u0026lt;SpringApplicationRunListener\u0026gt; listeners; private final ApplicationStartup applicationStartup; SpringApplicationRunListeners(Log log, Collection\u0026lt;? extends SpringApplicationRunListener\u0026gt; listeners, ApplicationStartup applicationStartup) { this.log = log; this.listeners = new ArrayList\u0026lt;\u0026gt;(listeners); this.applicationStartup = applicationStartup; } //5.遍历 SpringApplicationRunListener 调用 starting 方法； //相当于通知所有感兴趣系统正在启动过程的人，项目正在 starting。 void starting(ConfigurableBootstrapContext bootstrapContext, Class\u0026lt;?\u0026gt; mainApplicationClass) { doWithListeners(\u0026#34;spring.boot.application.starting\u0026#34;, (listener) -\u0026gt; listener.starting(bootstrapContext), (step) -\u0026gt; { if (mainApplicationClass != null) { step.tag(\u0026#34;mainApplicationClass\u0026#34;, mainApplicationClass.getName()); } }); } //7.1 void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) { doWithListeners(\u0026#34;spring.boot.application.environment-prepared\u0026#34;, (listener) -\u0026gt; listener.environmentPrepared(bootstrapContext, environment)); } //8.1 void contextPrepared(ConfigurableApplicationContext context) { doWithListeners(\u0026#34;spring.boot.application.context-prepared\u0026#34;, (listener) -\u0026gt; listener.contextPrepared(context)); } //8.2 void contextLoaded(ConfigurableApplicationContext context) { doWithListeners(\u0026#34;spring.boot.application.context-loaded\u0026#34;, (listener) -\u0026gt; listener.contextLoaded(context)); } //10. void started(ConfigurableApplicationContext context) { doWithListeners(\u0026#34;spring.boot.application.started\u0026#34;, (listener) -\u0026gt; listener.started(context)); } //12. void running(ConfigurableApplicationContext context) { doWithListeners(\u0026#34;spring.boot.application.running\u0026#34;, (listener) -\u0026gt; listener.running(context)); } //14. void failed(ConfigurableApplicationContext context, Throwable exception) { doWithListeners(\u0026#34;spring.boot.application.failed\u0026#34;, (listener) -\u0026gt; callFailedListener(listener, context, exception), (step) -\u0026gt; { step.tag(\u0026#34;exception\u0026#34;, exception.getClass().toString()); step.tag(\u0026#34;message\u0026#34;, exception.getMessage()); }); } private void doWithListeners(String stepName, Consumer\u0026lt;SpringApplicationRunListener\u0026gt; listenerAction, Consumer\u0026lt;StartupStep\u0026gt; stepAction) { StartupStep step = this.applicationStartup.start(stepName); this.listeners.forEach(listenerAction); if (stepAction != null) { stepAction.accept(step); } step.end(); } ... } 86、原理解析-自定义事件监听组件 MyApplicationContextInitializer.java\n1 2 3 4 5 6 7 8 9 import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; public class MyApplicationContextInitializer implements ApplicationContextInitializer { @Override public void initialize(ConfigurableApplicationContext applicationContext) { System.out.println(\u0026#34;MyApplicationContextInitializer ....initialize.... \u0026#34;); } } MyApplicationListener.java\n1 2 3 4 5 6 7 8 9 import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; public class MyApplicationListener implements ApplicationListener { @Override public void onApplicationEvent(ApplicationEvent event) { System.out.println(\u0026#34;MyApplicationListener.....onApplicationEvent...\u0026#34;); } } MyApplicationRunner.java\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Order(1) @Component//放入容器 public class MyApplicationRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) throws Exception { System.out.println(\u0026#34;MyApplicationRunner...run...\u0026#34;); } } MyCommandLineRunner.java\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 import org.springframework.boot.CommandLineRunner; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; /** * 应用启动做一个一次性事情 */ @Order(2) @Component//放入容器 public class MyCommandLineRunner implements CommandLineRunner { @Override public void run(String... args) throws Exception { System.out.println(\u0026#34;MyCommandLineRunner....run....\u0026#34;); } } MySpringApplicationRunListener.java\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import org.springframework.boot.ConfigurableBootstrapContext; import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplicationRunListener; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; public class MySpringApplicationRunListener implements SpringApplicationRunListener { private SpringApplication application; public MySpringApplicationRunListener(SpringApplication application, String[] args){ this.application = application; } @Override public void starting(ConfigurableBootstrapContext bootstrapContext) { System.out.println(\u0026#34;MySpringApplicationRunListener....starting....\u0026#34;); } @Override public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) { System.out.println(\u0026#34;MySpringApplicationRunListener....environmentPrepared....\u0026#34;); } @Override public void contextPrepared(ConfigurableApplicationContext context) { System.out.println(\u0026#34;MySpringApplicationRunListener....contextPrepared....\u0026#34;); } @Override public void contextLoaded(ConfigurableApplicationContext context) { System.out.println(\u0026#34;MySpringApplicationRunListener....contextLoaded....\u0026#34;); } @Override public void started(ConfigurableApplicationContext context) { System.out.println(\u0026#34;MySpringApplicationRunListener....started....\u0026#34;); } @Override public void running(ConfigurableApplicationContext context) { System.out.println(\u0026#34;MySpringApplicationRunListener....running....\u0026#34;); } @Override public void failed(ConfigurableApplicationContext context, Throwable exception) { System.out.println(\u0026#34;MySpringApplicationRunListener....failed....\u0026#34;); } } 注册MyApplicationContextInitializer，MyApplicationListener，MySpringApplicationRunListener:\nresources / META-INF / spring.factories:\n1 2 3 4 5 6 7 8 org.springframework.context.ApplicationContextInitializer=\\ com.lun.boot.listener.MyApplicationContextInitializer org.springframework.context.ApplicationListener=\\ com.lun.boot.listener.MyApplicationListener org.springframework.boot.SpringApplicationRunListener=\\ com.lun.boot.listener.MySpringApplicationRunListener 87、后会有期 路漫漫其修远兮，吾将上下而求索。\n纸上得来终觉浅，绝知此事要躬行。\nSpring Boot 2 场景整合篇\n虚拟化技术 安全控制 缓存技术 消息中间件 对象存储 定时调度 异步任务 分布式系统 Spring Boot 2 响应式编程\n响应式编程基础 Webflux开发Web应用 响应式访问持久化层 响应式安全开发 响应式原理 ","permalink":"https://ktzxy.top/posts/ejhw5zud65/","summary":"SpringBoot2学习笔记2","title":"SpringBoot2学习笔记2"},{"content":"[TOC]\nprocess exporter在prometheus中用于监控进程，通过process exporter，可从宏观角度监控应用的运行状态（譬如监控redis、mysql的进程资源等）。\n1、下载安装 下载地址：https://github.com/ncabatoff/process-exporter/releases/tag/v0.4.0 tar -zxvf process-exporter-0.4.0.linux-amd64.tar.gz -C /usr/local/process-exporter\n2、修改配置文件 1 2 3 4 5 6 7 8 process_names: - name: \u0026#34;{{.Matches}}\u0026#34; cmdline: - \u0026#39;redis\u0026#39; - name: \u0026#34;{{.Matches}} cmdline: - \u0026#39;mysql\u0026#39; 注意：如果一个进程符合多个匹配项，只会归属于第一个匹配的groupname组。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 process_names: - name: \u0026#34;{{.Matches}}\u0026#34; cmdline: - \u0026#39;redis-server\u0026#39; - name: \u0026#34;{{.Matches}}\u0026#34; cmdline: - \u0026#39;mysqld\u0026#39; - name: \u0026#34;{{.Matches}}\u0026#34; cmdline: - \u0026#39;org.apache.zookeeper.server.quorum.QuorumPeerMain\u0026#39; - name: \u0026#34;{{.Matches}}\u0026#34; cmdline: - \u0026#39;org.apache.hadoop.mapreduce.v2.hs.JobHistoryServer\u0026#39; - name: \u0026#34;{{.Matches}}\u0026#34; cmdline: - \u0026#39;org.apache.hadoop.hdfs.qjournal.server.JournalNode\u0026#39; 注意：cmdline: 所选进程的唯一标识，ps -ef 可以查询到。如果改进程不存在，则不会有该进程的数据采集到。\nname选项有几个（官方翻译https://github.com/ncabatoff/process-exporter）：\n{{.Comm}} 包含原始可执行文件的基本名称，即第二个字段 /proc//stat {{.ExeBase}} 包含可执行文件的基名 {{.ExeFull}} 包含可执行文件的完全限定路径 {{.Username}} 包含有效用户的用户名 {{.Matches}} map包含应用cmdline regexps产生的所有匹配项 {{.Comm}} groupname=\u0026ldquo;redis-server\u0026rdquo; exe或者sh文件名称 {{.ExeBase}} groupname=\u0026ldquo;redis-server *:6379\u0026rdquo; / {{.ExeFull}} groupname=\u0026quot;/usr/bin/redis-server *:6379\u0026quot; ps中的进程完成信息 {{.Username}} groupname=\u0026ldquo;redis\u0026rdquo; 使用进程所属的用户进行分组 {{.Matches}} groupname=\u0026ldquo;map[:redis]\u0026rdquo; 表示配置到关键字“redis” 3、启动 1 ./process-exporter -config.path process-name.yaml \u0026amp; 查看数据：\n1 curl http://localhost:9256/metrics \u0026gt; ccc ","permalink":"https://ktzxy.top/posts/fxup7jks83/","summary":"Process exporter进程监控","title":"Process exporter进程监控"},{"content":"Kubernetes核心技术-Controller 内容 什么是Controller Pod和Controller的关系 Deployment控制器应用场景 yaml文件字段说明 Deployment控制器部署应用 升级回滚 弹性伸缩 什么是Controller Controller是在集群上管理和运行容器的对象，Controller是实际存在的，Pod是虚拟机的\nPod和Controller的关系 Pod是通过Controller实现应用的运维，比如弹性伸缩，滚动升级等\nPod 和 Controller之间是通过label标签来建立关系，同时Controller又被称为控制器工作负载\nDeployment控制器应用 Deployment控制器可以部署无状态应用 管理Pod和ReplicaSet 部署，滚动升级等功能 应用场景：web服务，微服务 Deployment表示用户对K8S集群的一次更新操作。Deployment是一个比RS( Replica Set, RS) 应用模型更广的 API 对象，可以是创建一个新的服务，更新一个新的服务，也可以是滚动升级一个服务。滚动升级一个服务，实际是创建一个新的RS，然后逐渐将新 RS 中副本数增加到理想状态，将旧RS中的副本数减少到0的复合操作。\n这样一个复合操作用一个RS是不好描述的，所以用一个更通用的Deployment来描述。以K8S的发展方向，未来对所有长期伺服型的业务的管理，都会通过Deployment来管理。\nDeployment部署应用 之前我们也使用Deployment部署过应用，如下代码所示\n1 kubectrl create deployment web --image=nginx 但是上述代码不是很好的进行复用，因为每次我们都需要重新输入代码，所以我们都是通过YAML进行配置\n但是我们可以尝试使用上面的代码创建一个镜像【只是尝试，不会创建】\n1 kubectl create deployment web --image=nginx --dry-run -o yaml \u0026gt; nginx.yaml 然后输出一个yaml配置文件 nginx.yml ，配置文件如下所示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: web name: web spec: replicas: 1 selector: matchLabels: app: web strategy: {} template: metadata: creationTimestamp: null labels: app: web spec: containers: - image: nginx name: nginx resources: {} status: {} 我们看到的 selector 和 label 就是我们Pod 和 Controller之间建立关系的桥梁\n使用YAML创建Pod 通过刚刚的代码，我们已经生成了YAML文件，下面我们就可以使用该配置文件快速创建Pod镜像了\n1 kubectl apply -f nginx.yaml 但是因为这个方式创建的，我们只能在集群内部进行访问，所以我们还需要对外暴露端口\n1 kubectl expose deployment web --port=80 --type=NodePort --target-port=80 --name=web1 关于上述命令，有几个参数\n\u0026ndash;port：就是我们内部的端口号 \u0026ndash;target-port：就是暴露外面访问的端口号 \u0026ndash;name：名称 \u0026ndash;type：类型 同理，我们一样可以导出对应的配置文件\n1 kubectl expose deployment web --port=80 --type=NodePort --target-port=80 --name=web1 -o yaml \u0026gt; web1.yaml 得到的web1.yaml如下所示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 apiVersion: v1 kind: Service metadata: creationTimestamp: \u0026#34;2020-11-16T02:26:53Z\u0026#34; labels: app: web managedFields: - apiVersion: v1 fieldsType: FieldsV1 fieldsV1: f:metadata: f:labels: .: {} f:app: {} f:spec: f:externalTrafficPolicy: {} f:ports: .: {} k:{\u0026#34;port\u0026#34;:80,\u0026#34;protocol\u0026#34;:\u0026#34;TCP\u0026#34;}: .: {} f:port: {} f:protocol: {} f:targetPort: {} f:selector: .: {} f:app: {} f:sessionAffinity: {} f:type: {} manager: kubectl operation: Update time: \u0026#34;2020-11-16T02:26:53Z\u0026#34; name: web2 namespace: default resourceVersion: \u0026#34;113693\u0026#34; selfLink: /api/v1/namespaces/default/services/web2 uid: d570437d-a6b4-4456-8dfb-950f09534516 spec: clusterIP: 10.104.174.145 externalTrafficPolicy: Cluster ports: - nodePort: 32639 port: 80 protocol: TCP targetPort: 80 selector: app: web sessionAffinity: None type: NodePort status: loadBalancer: {} 然后我们可以通过下面的命令来查看对外暴露的服务\n1 kubectl get pods,svc 然后我们访问对应的url，即可看到 nginx了 http://192.168.177.130:32639/\n升级回滚和弹性伸缩 升级： 假设从版本为1.14 升级到 1.15 ，这就叫应用的升级【升级可以保证服务不中断】 回滚：从版本1.15 变成 1.14，这就叫应用的回滚 弹性伸缩：我们根据不同的业务场景，来改变Pod的数量对外提供服务，这就是弹性伸缩 应用升级和回滚 首先我们先创建一个 1.14版本的Pod\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: web name: web spec: replicas: 1 selector: matchLabels: app: web strategy: {} template: metadata: creationTimestamp: null labels: app: web spec: containers: - image: nginx:1.14 name: nginx resources: {} status: {} 我们先指定版本为1.14，然后开始创建我们的Pod\n1 kubectl apply -f nginx.yaml 同时，我们使用docker images命令，就能看到我们成功拉取到了一个 1.14版本的镜像\n我们使用下面的命令，可以将nginx从 1.14 升级到 1.15\n1 kubectl set image deployment web nginx=nginx:1.15 在我们执行完命令后，能看到升级的过程\n首先是开始的nginx 1.14版本的Pod在运行，然后 1.15版本的在创建 然后在1.15版本创建完成后，就会暂停1.14版本 最后把1.14版本的Pod移除，完成我们的升级 我们在下载 1.15版本，容器就处于ContainerCreating状态，然后下载完成后，就用 1.15版本去替换1.14版本了，这么做的好处就是：升级可以保证服务不中断\n我们到我们的node2节点上，查看我们的 docker images;\n能够看到，我们已经成功拉取到了 1.15版本的nginx了\n查看升级状态 下面可以，查看升级状态\n1 kubectl rollout status deployment web 查看历史版本 我们还可以查看历史版本\n1 kubectl rollout history deployment web 应用回滚 我们可以使用下面命令，完成回滚操作，也就是回滚到上一个版本\n1 kubectl rollout undo deployment web 然后我们就可以查看状态\n1 kubectl rollout status deployment web 同时我们还可以回滚到指定版本\n1 kubectl rollout undo deployment web --to-revision=2 弹性伸缩 弹性伸缩，也就是我们通过命令一下创建多个副本\n1 kubectl scale deployment web --replicas=10 能够清晰看到，我们一下创建了10个副本\n","permalink":"https://ktzxy.top/posts/hjpi1dqptq/","summary":"9 Kubernetes核心技术Controller","title":"9 Kubernetes核心技术Controller"},{"content":"Java 基础 - XML 1. XML 概述 XML 的全称 Extensible Markup Language，中文：可扩展标记语言。XML 是一门语言，由 w3c 组织制定。\nXML 是使用标记来描述数据格式，标记又叫标签，元素。标签又分为开始标签和结束标签，比如\u0026lt;name\u0026gt;是开始标签，\u0026lt;/name\u0026gt;是结束标签。\n1.1. 作用 用于描述数据(关系型数据)，存储数据，可以当成一个小型数据库使用。 作为框架的配置文件(structs.xml, hibernate.cfg.xml…)。好处是可以提升软件的灵活性。如：数据库的配置、JavaBean 的配置。使用时的数据可以查看不同的 xml 配置文件。 可以用于不同的平台之间进行数据交换# Java基础 - XML XML 被设计用来描述数据，其焦点是数据的内容。HTML 被设计用来显示数据，其焦点是数据的外观。\n1.2. XML 的约束 XML 约束是用来规定 XML 文件中应该出现哪些标签，哪些属性，每个属性的取值有哪些。其作用是：\n约束XML文件结构 让XML文档的书写更加规范 1.2.1. 约束文件分类 DTD 约束文件：\nDTD 约束文件是一个文本文件，语法结构相对简单，数据类型比较单一。 应用场景: struts.xml 和 hibernate.cfg.xml 文件后缀名：dtd Schema 约束\nSchema 约束文档本身也是一个 XML 文件 语法结构相关复杂，数据类型很丰富，可以约束文本的具体数据类型 应用场景：Spring 的配置文件：applicationContext.xml, tomcat 中的 web.xml 等 文件后缀名：xsd 2. XML 语法 xml 文件后缀名是.xml，可以使用浏览器去检查 xml 是否符合语法。因为浏览器内置了 xml 的解析引擎。 格式良好的 XML 文档规则 必须有声明语句 XML 的标签区分大小写 XML 文档有且只有一个根元素 属性值使用引号 所有的标记必须有相应的结束标记，空标记也必须关闭 标记必须正确嵌套 对于特殊字符要进行处理 一个 XML 文件分为几部分内容：文档声明、标签、属性、注释、转义字符、CDATA 区、处理指令。 2.1. 文档声明 在编写 XML 文档时，必须要有文档声明\n2.1.1. 声明格式 在 XML 文档的首行声明\n1 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34;?\u0026gt; 还有更简单的声明语法：\n1 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; ?\u0026gt; 标签参数说明：\nversion(版本)：表示 xml 的版本号。W3C 在 1998 年 2 月发布 1.0 版本，2004 年 2 月又发布 1.1 版本，但因为 1.1 版本不能向下兼容 1.0 版本，所以 1.1 没有人用。同时，在 2004 年 2 月 W3C 又发布了 1.0 版本的第三版。目前使用 1.0 版本 encoding(编码)：可以指定UTF-8或GBK，是可选属性。常用的是UTF-8。 Notes:\n参数的顺序必须是：version 在前，encoding 在后，否则会报错。 文档声明的语句必须牌XML文档的最左上角，不能有任何空格和换行。 2.1.2. 编码问题 用 encoding 属性说明文档的字符编码：\u0026lt;?xml version=\u0026quot;1.0\u0026quot; encoding=\u0026quot;GBK\u0026quot; ?\u0026gt;\n记事本默认是 ANSI 的编码，在简体中文 Windows 操作系统中，ANSI 编码代表 GBK 编码。XML 文件保存的编码要与 encoding 的编码相同，不然解析会出错。\n2.1.3. xml 文档编码问题注意 ecplise 工具会自动根据 xml 文件的文档声明自动设置保存时的编码，所以在 eclipse 中编写 xml 文件通常不会有编码问题。但是如果使用记事本工具，那么注意保存 xml 文件的编码和文件声明的编码保持一致！\n2.2. 标签（元素） 2.2.1. 标签的格式 1 \u0026lt;标签名\u0026gt;标签内容(数据部分)\u0026lt;/标签名\u0026gt; 例：\n1 \u0026lt;student\u0026gt;张三\u0026lt;/student\u0026gt; 2.2.2. 标签分类 有主体标签：\n1 \u0026lt;a\u0026gt;www.moonzero.com\u0026lt;/a\u0026gt; 无主体标签，即空标签：\n1 2 3 \u0026lt;a\u0026gt;\u0026lt;/a\u0026gt; \u0026lt;!-- 简写 --\u0026gt; \u0026lt;a/\u0026gt; 2.2.3. 标签命名规范 标签名严格区分大小写，比如\u0026lt;A\u0026gt;和\u0026lt;a\u0026gt;是完全不同的标签。 标签名不能以数字开头。只能以字母或下划线开头，可以是中文。 标签名不能包含空格，比如\u0026lt;name of\u0026gt;。但是\u0026lt;name \u0026gt;写法是可以，但不推荐使用。 标签名不能使用冒号(:)，是命名空间的特殊符号。 2.2.4. 标签的注意事项 在一个 xml 文档中，只允许有一个根标签。 标签中是可以有属性有多个，属性值必须使用引号括起来。 开始标签和结束标签必须成对出现。 标签可以嵌套标签，但是必须合理嵌套，不能出现交叉嵌套。 错误的嵌套示例：\n1 \u0026lt;a\u0026gt;welcome to \u0026lt;b\u0026gt;www.moonzero.com\u0026lt;/a\u0026gt;\u0026lt;/b\u0026gt; 对于 XML 标签中出现的所有空格和换行，XML 解析程序都会当作标签内容进行处理。\n2.3. 属性 2.3.1. 属性语法格式 1 \u0026lt;开始标签 属性名=\u0026#34;属性值\u0026#34; 属性值 = \u0026#39;属性值\u0026#39;\u0026gt;标签内容(数据部分)\u0026lt;/结束标签\u0026gt; 示例：\n1 \u0026lt;name id=\u0026#34;001\u0026#34;\u0026gt;李四\u0026lt;/name\u0026gt; 2.3.2. 注意事项 属性值必须放在双引号或单引号中，不能省略引号，也不能单双混用。 属性必须放在开始标签中，不能写在结束标签中。 在一个标签中，属性可以有多个，属性之前使用空格分隔即可。但不能出现同名的属性。 2.4. 注释 2.4.1. 注释语法格式 1 \u0026lt;!-- xml 注释内容 --\u0026gt; 2.4.2. 注意事项 注释不能嵌套。 XML 注释不能出现在文档声明语句之上。 2.5. 转义字符 有些符号在 XML 语言中是有特殊含义的，如果想原样输出 XML 中的特殊字符，那么就需要对其进行转义。\n转义的语法格式：以\u0026amp;开始，以;结束\n特殊字符 替代字符 英文 \u0026amp; \u0026amp;amp; ampersand \u0026lt; \u0026amp;lt; less than \u0026gt; \u0026amp;gt; greater than \u0026quot; \u0026amp;quot; quote ' \u0026amp;apos; apostrophe 2.6. CDATA 区 XML 文档中的所有文本均会被解析器解析。而 CDATA 区的作用是：能够保证在CDATA区的字符数据能够原样输出，不会被浏览器解析。\n2.6.1. 语法格式 CDATA 部分由 \u0026ldquo;\u0026lt;![CDATA[\u0026rdquo; 开始，由 \u0026ldquo;]]\u0026gt;\u0026rdquo; 结束，其内容可以换行\n1 \u0026lt;![CDATA[字符数据(文本内容)]] 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;script\u0026gt; \u0026lt;![CDATA[ function matchwo(a,b) { if (a \u0026lt; b \u0026amp;\u0026amp; a \u0026lt; 0) then { return 1; } else { return 0; } } ]]\u0026gt; \u0026lt;/script\u0026gt; 2.6.2. 注意事项 CDATA 部分不能包含字符串 \u0026ldquo;]]\u0026gt;\u0026quot;。也不允许嵌套的 CDATA 部分。 标记 CDATA 部分结尾的 \u0026ldquo;]]\u0026gt;\u0026rdquo; 不能包含空格或换行。 3. DTD 约束 DTD 全称：Document Type Definition 文档类型定义\n3.1. DTD 的分类 按引用位置来分类：\n内部 DTD：约束直接写在XML文档中（不使用） 外部 DTD：单独使用一个文件来写约束，通过引用来使用 3.2. DTD 文档声明的两种形式（引用方式） 3.2.1. SYSTEM 含义：表示该DTD约束是个人或是某个组织的，不公开的，私有的，使用范围比较窄\n语法格式：\n1 \u0026lt;!DOCTYPE 根标签 SYSTEM \u0026#34;DTD 文件的地址和名字\u0026#34;\u0026gt; 示例：\n1 \u0026lt;!DOCTYPE bookshelf SYSTEM \u0026#34;book.dtd\u0026#34;\u0026gt; 文件地址例子说明：\n\u0026ldquo;../book.dtd\u0026rdquo; 在上一层目录中 \u0026ldquo;../dtd/book.dtd\u0026rdquo; 在上一层目录的dtd文件夹中 3.2.2. PUBLIC 含义：当前的 DTD 是公开的，可以广泛使用的，使用范围比较广。\n语法格式：\n1 \u0026lt;!DOCTYPE 根元素 PUBLIC \u0026#34;DTD 的描述\u0026#34; \u0026#34;DTD 文件的地址\u0026#34;\u0026gt; 示例：\n1 2 \u0026lt;!DOCTYPE struts PUBLIC \u0026#34;-//Apache Software Foundation//DTD Struts Configuration 2.1//EN\u0026#34; \u0026#34;http://struts.apache.org/dtds/struts-2.1.dtd\u0026#34;\u0026gt; 3.2.3. DTD 文件描述 DTD 文件描述由四个组成部分：前缀//DTD 文件的所有者//DTD 版本信息//语言\n前缀部分有以下三个取值：\nISO： 当前的 DTD 是符合 ISO 标准的(国际化标准) +： 非 ISO 标准的 DTD，经过了改进 -： 非 ISO 标准的 DTD，没有改进过的 3.3. DTD语法：元素约束 3.3.1. 元素约束语法格式 1 \u0026lt;!ELEMENT 元素名(标签名) 元素类型或元素内容\u0026gt; 上面三个要素必须要空格分隔，空格不能省略。\n3.3.2. 元素的类别（3种） (#PCDATA)：Parsed Character Data 被解析的字符数据（文本内容） EMPTY：空的(表示一个空的标签) 如： \u0026lt;BR/\u0026gt; 的 DTD 为：\u0026lt;!ELEMENT BR EMPTY\u0026gt; ANY：任意的内容都可以 3.3.3. 元素的内容修饰符号 符号 描述 示例 ? 表示该对象可以出现，但只能出现一次 (author?) author 出现 0~1 次 * 表示该对象允许出现任意多次，也可以是零次 (author*) author 出现 0~n 次 + 表示该对象最少出现一次，可以出现多次 (contact+) contact 出现 1~n 次 () 用来给元素分组 (name,gender,phone) ` ` 表明在列出的对象中选择一个 (title|author|price) , 表示对象必须按指定的顺序出现 (title,author,price) 包含这三个子元素，而且元素要依次出现，出现一次。 3.3.4. 元素约束示例 1 2 3 4 5 \u0026lt;!ELEMENT bookshelf (book+)\u0026gt; \u0026lt;!ELEMENT book (title,author,price)\u0026gt; \u0026lt;!ELEMENT title (#PCDATA)\u0026gt; \u0026lt;!ELEMENT author (#PCDATA)\u0026gt; \u0026lt;!ELEMENT price (#PCDATA)\u0026gt; 3.4. DTD语法：属性约束 3.4.1. 语法格式 语法：\n1 2 3 4 5 \u0026lt;!ATTLIST 元素名 属性名 属性类型 类型选项 属性名 属性类型 类型选项 …… \u0026gt; 属性类型：\n属性类型 说明 CDATA Character Data字符数据类型，文本内容 `(enum1 enum2 ID 属性值在整个 XML 文件中是唯一，而且命名不能以数字开头 类型选项：\n类型选项 选项的含义 #REQUIRED 必须的属性 #IMPLIED 可选的属性 #FIXED value 属性的值是固定的，只能取 value 默认值 直接写就是默认值 3.4.2. 属性约束示例 属性约束（两种写法）\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;!ATTLIST article author CDATA #REQUIRED\u0026gt; \u0026lt;!ATTLIST article editor CDATA #IMPLIED\u0026gt; \u0026lt;!ATTLIST article date CDATA #IMPLIED\u0026gt; \u0026lt;!ATTLIST article edition CDATA #IMPLIED\u0026gt; \u0026lt;!ATTLIST article author CDATA #REQUIRED editor CDATA #IMPLIED date CDATA #IMPLIED edition CDATA #IMPLIED \u0026gt; 3.5. DTD 综合案例 案例需求：\n根元素是 contactList，其根元素包含 1~n 个 contact 子元素，contact 子元素包含 name,gender,phone,qq,email 元素这个元素必须依次出现，并且元素中都可以包含文本元素(#PCDATA) 其中 contact 有 id 属性，必须，并且不能重复，不能以数字开头 contact 有一个可选的属性 vip，取值是 true 或 false 按此 DTD 约束创建 XML 文件，并添加 3 条 contact 记录。 案例 DTD 约束文件：\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;!ELEMENT contactList (contact+)\u0026gt; \u0026lt;!ELEMENT contact (name,gender,phone,qq,email)\u0026gt; \u0026lt;!ELEMENT name (#PCDATA)\u0026gt; \u0026lt;!ELEMENT gender (#PCDATA)\u0026gt; \u0026lt;!ELEMENT phone (#PCDATA)\u0026gt; \u0026lt;!ELEMENT qq (#PCDATA)\u0026gt; \u0026lt;!ELEMENT email (#PCDATA)\u0026gt; \u0026lt;!ATTLIST contact id ID #REQUIRED vip (true|false) #IMPLIED \u0026gt; 案例 XML 文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE contactList SYSTEM \u0026#34;contact.dtd\u0026#34; \u0026gt; \u0026lt;contactList\u0026gt; \u0026lt;contact id=\u0026#34;S100\u0026#34; vip=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;name\u0026gt;张三\u0026lt;/name\u0026gt; \u0026lt;gender\u0026gt;男\u0026lt;/gender\u0026gt; \u0026lt;phone\u0026gt;13565784567\u0026lt;/phone\u0026gt; \u0026lt;qq\u0026gt;394832048\u0026lt;/qq\u0026gt; \u0026lt;email\u0026gt;zhang@moonzero.com\u0026lt;/email\u0026gt; \u0026lt;/contact\u0026gt; \u0026lt;contact id=\u0026#34;S200\u0026#34;\u0026gt; \u0026lt;name\u0026gt;李四\u0026lt;/name\u0026gt; \u0026lt;gender\u0026gt;女\u0026lt;/gender\u0026gt; \u0026lt;phone\u0026gt;15687960980\u0026lt;/phone\u0026gt; \u0026lt;qq\u0026gt;109456893\u0026lt;/qq\u0026gt; \u0026lt;email\u0026gt;li@moonzero.com\u0026lt;/email\u0026gt; \u0026lt;/contact\u0026gt; \u0026lt;/contactList\u0026gt; 4. Schema 约束 Schema /'ski:mə/ 概要，纲要\n4.1. Schema 约束概述 扩展名(XML Schema Definition) XML 模式定义：xsd Schema 约束文件本身也是个 XML 文件，所以它也有根元素，根标签(元素)的名字叫 schema 模式文档和实例文档的概念\n制定约束的 XML 文档称为：模式文档 (类似于：类) 被模式该当约束的 XML 文档称为：实例文档 (类似于：对象) Notes: Schema 也被约束，被官方Schema约束文档所约束。\n4.2. Schema 的命名空间 4.2.1. 概述 由于 XML 中的元素名是预定义的，当两个不同的文档使用相同的元素名时，就会发生命名冲突的问题。\nSchema 命名空间的作用：用来描述实例文档使用的标签，属性是来自于哪一个模式文档。避免标签名、属性名冲突的问题。\n4.2.2. 名称空间声明的一般形式 名称空间声明的一般形式为：\n第一部分是一个关键字xmlns: 第二部分是名称空间的前缀 第三部分是一个等号 第四部分是双引号，将第五部分的名称空间标识URI 包括起来 1 xmlns:xs=\u0026#34;http://www.w3.org/2001/XMLSchema\u0026#34; 4.2.3. XML Namespace (xmlns) 属性 XML 命名空间属性被放置于某个元素的开始标签之中，并使用以下的语法：\n1 xmlns:前缀=\u0026#34;URI 地址\u0026#34; 当一个命名空间被定义在某个元素的开始标签中时，所有带有相同前缀的子元素都会与同一个命名空间相关联。\nNotes: 用于标示命名空间的地址不会被解析器用于查找信息。其唯一的作用是赋予命名空间一个唯一的名称。\n4.2.4. 统一资源标示符（Uniform Resource Identifier (URI)） 统一资源标示符是一串可以标示因特网资源的字符。最常用的 URI\t是用来标示因特网域名地址的统一资源定位器(URL)。例如：\n1 http://www.baidu.com/xxx 4.2.5. 默认的命名空间（Default Namespaces） 默认命名空间格式：\n1 xmlns=\u0026#34;namespaceURI\u0026#34; 为某个元素定义默认的命名空间可以让我们省去在所有的子元素中使用前缀的工作。一个 XML 文件中只能使用一个默认的命名空间。例如：\n1 \u0026lt;table xmlns=\u0026#34;http://www.moonzero.com/html4\u0026#34;\u0026gt; 4.2.6. 命名冲突解决方案 使用前缀来避免命名冲突：\n1 2 3 4 5 6 7 8 9 10 11 12 13 // 代表表格 \u0026lt;h:table\u0026gt; \u0026lt;h:tr\u0026gt; \u0026lt;h:td\u0026gt;Apples\u0026lt;/h:td\u0026gt; \u0026lt;h:td\u0026gt;Bananas\u0026lt;/h:td\u0026gt; \u0026lt;/h:tr\u0026gt; \u0026lt;/h:table\u0026gt; // 代表家具 \u0026lt;f:table\u0026gt; \u0026lt;f:name\u0026gt;African Coffee Table\u0026lt;/f:name\u0026gt; \u0026lt;f:width\u0026gt;80\u0026lt;/f:width\u0026gt; \u0026lt;f:length\u0026gt;120\u0026lt;/f:length\u0026gt; \u0026lt;/f:table\u0026gt; 两个文档都使用了不同的名称来命名它们的 \u0026lt;table\u0026gt; 元素 (\u0026lt;h:table\u0026gt; 和 \u0026lt;f:table\u0026gt;)。通过使用前缀，创建了两种不同类型的 \u0026lt;table\u0026gt; 元素。\n使用命名空间（Namespaces）解决命名冲突：\n1 2 3 4 5 6 7 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;xs:schema xmlns:xs=\u0026#34;http://www.w3.org/2001/XMLSchema\u0026#34; targetNamespace=\u0026#34;http://www.example.org/NewXMLSchema\u0026#34; xmlns:tns=\u0026#34;http://www.example.org/NewXMLSchema\u0026#34; elementFormDefault=\u0026#34;qualified\u0026#34;\u0026gt; \u0026lt;/xs:schema\u0026gt; 与仅仅使用前缀不同，可以为标签添加了一个 xmlns 属性，这样就为前缀赋予了一个与某个命名空间相关联的限定名称\n4.3. Schema 文档使用与解析 4.3.1. 使用示例 示例 Schema 模式文档：note.xsd：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;?xml version=\u0026#34;1.0\u0026#34;?\u0026gt; \u0026lt;xs:schema xmlns:xs=\u0026#34;http://www.w3.org/2001/XMLSchema\u0026#34; targetNamespace=\u0026#34;http://www.moonzero.com/note\u0026#34; elementFormDefault=\u0026#34;qualified\u0026#34;\u0026gt; \u0026lt;xs:element name=\u0026#34;note\u0026#34;\u0026gt; \u0026lt;xs:complexType\u0026gt; \u0026lt;xs:sequence\u0026gt; \u0026lt;xs:element name=\u0026#34;to\u0026#34; type=\u0026#34;xs:string\u0026#34;/\u0026gt; \u0026lt;xs:element name=\u0026#34;from\u0026#34; type=\u0026#34;xs:string\u0026#34;/\u0026gt; \u0026lt;xs:element name=\u0026#34;heading\u0026#34; type=\u0026#34;xs:string\u0026#34;/\u0026gt; \u0026lt;xs:element name=\u0026#34;body\u0026#34; type=\u0026#34;xs:string\u0026#34;/\u0026gt; \u0026lt;/xs:sequence\u0026gt; \u0026lt;/xs:complexType\u0026gt; \u0026lt;/xs:element\u0026gt; \u0026lt;/xs:schema\u0026gt; 实例文档：note.xml\n1 2 3 4 5 6 7 8 9 \u0026lt;?xml version=\u0026#34;1.0\u0026#34;?\u0026gt; \u0026lt;note xmlns=\u0026#34;http://www.moonzero.com/note\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://www.moonzero.com/note note.xsd\u0026#34;\u0026gt; \u0026lt;to\u0026gt;Tove\u0026lt;/to\u0026gt; \u0026lt;from\u0026gt;Jani\u0026lt;/from\u0026gt; \u0026lt;heading\u0026gt;Reminder\u0026lt;/heading\u0026gt; \u0026lt;body\u0026gt;Don\u0026#39;t forget me this weekend!\u0026lt;/body\u0026gt; \u0026lt;/note\u0026gt; 4.3.2. Schema 元素解析说明 schema 是每一个 XML 模式文档的根元素 \u0026lt;schema\u0026gt; 元素包含属性 xmlns:xs=\u0026quot;http://www.w3.org/2001/XMLSchema\u0026quot; 作用一：表示 schema 中用到的元素和数据类型来自命名空间 \u0026quot;http://www.w3.org/2001/XMLSchema\u0026quot;。 作用二：规定了来自命名空间 \u0026quot;http://www.w3.org/2001/XMLSchema\u0026quot; 的元素和数据类型使用前缀 xs。 这个引用官方指定的约束，约束来自于该命名空间。一般生成不用去修改。 targetNamespace=\u0026quot;http://www.moonzero.com/note\u0026quot; 表示被此 schema 定义的元素 (note, to, from, heading, body) ，被绑定到了命名空间：\u0026quot;http://www.moonzero.com/note\u0026quot;。 targetNamespace = \u0026quot;URI字符串\u0026quot; 注意 URI 字符串，一般设置的公司的域名或网址，不查找该网址是否存在。它的作用就是用来唯一标识一个命名空间。 该模式文档定义的约束将被绑定到\u0026quot;http://xx//xxx/xxx\u0026quot;，命名空间(包)中 注：用于标示命名空间的地址不会被解析器用于查找信息。其唯一的作用是赋予命名空间一个唯一的名称。 elementFormDefault=\u0026quot;qualified\u0026quot; 使用此 xsd 的实例文档必须遵守此文档的约束。 取值qualified: 必须遵循该模式文档定义的约束条件 取值unqualified: 使用该模式文档的实例文档不需要遵循约束条件 note 元素是一个复合类型，因为它包含其他的子元素。 \u0026lt;xs:sequence\u0026gt; 表示其中的子元素要按顺序出现 其他元素 (to, from, heading, body) 是简易类型，因为它们没有包含其他元素。 4.3.3. Schema 使用说明 定义位置：在模式文档的根标签中定义\n引用格式：\n没有使用前缀 xmlns = \u0026quot;包名\u0026quot; 如果没有使用前缀：标识使用 引用位置：在实例文档的根标签中引用\n指定 XML Schema 实例命名空间：xmlns:xsi=\u0026quot;http://www.w3.org/2001/XMLSchema-instance\u0026quot; 使用 xsi:schemaLocation 属性： 第一个值是需要使用的命名空间。 第二个值是供命名空间使用的 XML schema 的位置。 如：xsi:schemaLocation=\u0026quot;http://www.moonzero.com/note note.xsd\u0026quot;，xsi 即 xml schema instance 简写 xmlns=\u0026quot;http://www.moonzero.com/note\u0026quot; 规定了默认命名空间的声明。此声明会告知 schema 验证器，在此 XML 文档中使用的所有元素的约束都来源于这个命名空间。也可以指定一个前缀，这里使用默认的命名空间，则可以省略前缀。 使用 eclipse 创建 XML 文件关联 xsd 文件：新建项目 -\u0026gt; 选择XML File -\u0026gt; 选择【Create XML file from an XML schema file】 -\u0026gt; 选择相应的 schema.xsd 文件 -\u0026gt; 最后的 Prefix 是设置前缀名，可以修改设置为空\n4.4. Schema 与DTD 的区别 XML: DTD 只是个普通的文本文件，并不是 XML 文件。而 Schema 本身也是一个 XML 文件。 数据类型： DTD 的类型比较单一，Schema 的数据类型丰富得多，可以指定如：integer、date、double。 约束功能：约束功能更加强大，可以使用更多规则，甚至可以使用正则表达式来约束。一个 XML 可以有多个 Schema 约束文档。 复杂度：比 DTD 要复杂。 可以多个 Schema 模式文档约束一个 XML 实例文档，而一个 DTD 文件只能约束一个 XML 文档。 5. XML 解析 5.1. XML 的解析概述 XML 解析是指，使用 Java 技术从 XML 文档中获取数据的过程。\n5.2. XML 解析的方式 5.2.1. DOM 解析 DOM 解析是直接将 XML 文档加载到内存中，在内存中生成一个 DOM 树。DOM 树上的每一个对象都是一个节点(Node)对象。DOM 解析优缺点如下：\n优点：xml 中的每个元素都是 dom 树上的一个节点，可以在解析的过程中对结点上的每个元素进行增删改操作。一般使用在PC端开发。 缺点：如果 XML 文档内容特别大，则会导致内存占用也比较大。 5.2.2. SAX 解析 SAX 解析是指，从上往下一行一行解析，解析一行释放一行。SAX 解析优缺点如下：\n优点：占用内存空间小，一般使用客户端App开发。 缺点：以只读的方式解析，不能对节点进行增删改操作。 5.3. DOM4J（第三方解析工具） DOM4J 是一个最优秀的第三方 Java 的 XML API 解析框架，是 jdom 的升级品，采用 dom 方式来解析 xml 文件（Hibernate 框架就是使用 Dom4j 解析 XML）。该框架具有性能优异、功能强大和极其易使用的特点，它的性能超过 sun 公司官方的dom 技术，同时它也是一个开放源代码的软件，可以在 SourceForge 上找到。\nDOM4J 官网地址：http://www.dom4j.org/\n在 eclipse 中使用 dom4j 的步骤：\n在项目中创建一个文件夹：lib 将 dom4j-1.6.1.jar 文件复制到 lib 文件夹 在 jar 文件上点右键，选 Builder Path -\u0026gt; Add to Builder Path 在类中导包使用 6. 使用 DOM4J 解析 XML 6.1. DOM 树的组成元素 文档对象：Document 节点：Node 元素：Element 属性：Attribute 文本：Text 6.1.1. Document 文档对象 每一个 XML 文档加载到内存中后会生成一个 Document 对象，通过 Document 对象就可以获取 XML 文档中的所有内容。获得 Document 对象步骤如下：\n创建一个 SAXReader 对象，用于读取 xml 文件。 通过 SAXReader 对象调用 read 方法，读取 xml 文件得到 Document 对象 6.1.2. Node 节点信息 DOM 树中每个元素都抽象成一个节点对象 Node，节点是所有元素的父元素。\n获得 Node 对象步骤：\n通过 Element 类的 nodeIterator() 方法获取 Node 对象迭代器对象 通过迭代器对象就可以遍历当前元素节点下的所有 Node 对象 获取 Node 的对象后，通过判断 Node 对象的类型来筛选需要的对象 6.1.3. Element 标签信息 所有元素(标签)都是一个 Element 对象\n6.1.4. Attribute 属性信息 每一个元素上的每一个属性都是一个 Attribute 对象。获得 Attribute 属性的前提，先获得标签 Element 对象。\n获取属性值的两种方式：\n先得到 Attribute 对象，再通过对象的方法得到属性名和属性值。 直接通过属性的名字得到字符串类型的属性值。 6.1.5. Text 文本元素 Text 文本元素就是指标签体的内容，获取步骤如下：\n得到文本元素的前提：先得到元素 Element 调用相关操作文本信息的方法 Notes: 空格、换行、制表符：也是属于文本的一部分，所以在解析 xml 文件的时候，格式化 XML 文件要注意。\n6.2. DOM4J 工具包常用类 6.2.1. SAXReader 类 1 public Document read(String name); 返回一个 Document 对象，name 是 xml 文档名字 1 public Document read(File file) 返回一个 Document 对象，参数是一个 File 文件路径对象 6.2.2. Document 类 1 public Element getRootElement() 获得根标签(元素)对象 1 public void setRootElement(Element rootElement) 将标签(元素)对象添加到该 document 对象中。(调用此方法的 Document 对象) 6.2.3. Node 类 1 public short getNodeType() 得到节点的类型，所有的节点类型在 Node 接口中定义了常量。注：通过判断返回的类型来筛选需要得到的Node对象。常用 Node 常量对象类型如下： Node.ELEMENT_NODE 标签节点 Node.TEXT_NODE 文本节点 Node.COMMENT_NODE 注释节点 Node.ATTRIBUTE_NODE 属性节点 1 public String getName() 获取当前节点的名称，返回字符串 1 public void setText(String text) 给当前的节点对象设置text文本内容 6.2.4. Element 类 所有元素(标签)都是一个 Element 对象，是 Branch 的子类\n1 public interface Element extends Branch 6.2.4.1. Element 类对节点 Node 的操作相关方法 1 public Iterator\u0026lt;Node\u0026gt; nodeIterator() Element继承父类的方法，方法返回当前标签下所有的子节点（Node对象）的迭代器，获取的包含了注解、空白内容等等元素。 6.2.4.2. Element 类对元素（标签）Element 的操作相关方法 1 public Iterator\u0026lt;Element\u0026gt; elementIterator() 获得该元素节点下所有子元素(Element、标签)的迭代器对象。(只获取到标签对象) 原文档解释：Returns an iterator over all this elements child elements.\n1 public Iterator elementIterator(String name) 获得该元素节点下指定名字的所有子元素(Element、标签)的迭代器对象。(只获取到标签名为name的对象) 1 public List\u0026lt;Element\u0026gt; elements(); 获得当前元素下的所有子元素Element对象的List集合 1 public List\u0026lt;Element\u0026gt; elements(String name); 根据元素名name获得所有的该名称的所有元素对象的List集合。 1 public Element element(String name); 得到指定名字name的标签对象，如果多个同名标签则默认返回第一个元素对象 1 public String getName(); 继承父类(Node)的方法，获取当前标签的名称，返回字符串 1 public Element addElement(String name) 继承父类(Branch)的方法，给当前元素标签对象新增一个新的子标签，并返回该新增的标签对象 原文档解释：Adds a new Element node with the given name to this branch and returns a reference to the new node.\n1 public Element addAttribute(String name, String value) 给当前标签对象新增一个属性(包括名称和值)，如：\u0026quot;name\u0026quot;=\u0026quot;value\u0026quot; 原文档解释：Adds the attribute value of the given local name. If an attribute already exists for the given name it will be replaced. Attributes with null values are silently ignored. If the value of the attribute is null then this method call will remove any attributes with the given name.\n1 public void add(Element element) 继承父类(Branch)的方法，给当前标签对象新增一个子标签对象element，无返回值。 1 public void setText(String text) 继承父类(Node)方法，给当前的标签对象设置text文本内容 6.2.4.3. Element 类对属性 Attribute 的操作相关方法 1 public Attribute attribute(String name); 通过指定属性的名字name得到一个属性对象 原文档解释：Returns the attribute with the given name\n1 public String attributeValue(String name); 通过属性名字直接得到当前标签element对象的属性值，返回 String 类型 1 public List\u0026lt;Attribute\u0026gt; attributes(); 得到当前标签的所有的属性对象(Attribute)的List集合 1 public Iterator\u0026lt;Attribute\u0026gt; attributeIterator(); 获得当前元素属性的迭代器对象。 6.2.4.4. Element类对文本Text的操作相关方法 1 public String getText(); 通过元素对象得到元素的文本内容（包括了左右空格，换行符等内容） 1 public String getTextTrim(); 通过元素对象得到元素的文本内容（会自动去掉包括了左右空格，换行符等内容） 1 public String elementText(String name); 通过元素对象根据子元素名name得到该元素的文本内容（包括了左右空格，换行符等内容） 1 public String elementTextTrim(String name); 通过元素对象根据子元素名name得到该元素的文本内容（会自动去掉包括了左右空格，换行符等内容） 6.2.5. Attribute 类 1 public String getText(); 获取属性的值。(继承父类Node的方法) 1 public String getName(); 得到属性的名字。(继承父类Node的方法) 1 public String getValue(); 得到属性的值。 6.2.6. XMLWriter 类 6.2.6.1. DocumentHelper 工具类 DocumentHelper 工具类可以用来创建文档对象，元素对象，属性对象\u0026hellip;\n1 public static Document createDocument() 用来创建空的文档对象 1 public static Document createDocument(Element rootElement) 以 rootElement 对象为根标签，并创建文档对象 1 public static Element createElement(String name) 创建名称是 name 的 Element 对象 6.2.6.2. OutputFormat 类（格式化） 1 public static OutputFormat createPrettyPrint() 静态方法，创建漂亮型格式化的对象。每个元素内容换新一行。 原文档解释：A static helper method to create the default pretty printing format. This format consists of an indent of 2 spaces, newlines after each element and all other whitespace trimmed, and XMTML is false.\n1 public static OutputFormat createCompactFormat() 静态方法，创建紧凑型格式化的对象，所有的内容一行显示 原文档解释：A static helper method to create the default compact format. This format does not have any indentation or newlines after an alement and all other whitespace trimmed\n6.2.6.3. XMLWriter 类 1 public XMLWriter(OutputStream out, OutputFormat format) XMLWriter 类的构造方法。参数说明： out：字节输出流对象，用来关联目标文件 format：格式化对象 1 public void write(Document doc); 将文档对象写出 XML 文件中 6.3. 使用示例 6.3.1. DOM4J 常用方法练习 示例 xml 文档\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;contactList\u0026gt; \u0026lt;!-- 注释内容 --\u0026gt; \u0026lt;contact id=\u0026#34;S100\u0026#34; vip=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;name\u0026gt; 潘金莲 \u0026lt;/name\u0026gt; \u0026lt;gender\u0026gt;女\u0026lt;/gender\u0026gt; \u0026lt;phone\u0026gt;136909877890\u0026lt;/phone\u0026gt; \u0026lt;qq\u0026gt;387439274\u0026lt;/qq\u0026gt; \u0026lt;email\u0026gt;panpan@moonzero.com\u0026lt;/email\u0026gt; \u0026lt;/contact\u0026gt; \u0026lt;contact id=\u0026#34;S200\u0026#34; vip=\u0026#34;false\u0026#34;\u0026gt; \u0026lt;name\u0026gt;武大狼\u0026lt;/name\u0026gt; \u0026lt;gender\u0026gt;男\u0026lt;/gender\u0026gt; \u0026lt;phone\u0026gt;13609876543\u0026lt;/phone\u0026gt; \u0026lt;qq\u0026gt;394535\u0026lt;/qq\u0026gt; \u0026lt;email\u0026gt;wuda@moonzero.com\u0026lt;/email\u0026gt; \u0026lt;/contact\u0026gt; \u0026lt;contact id=\u0026#34;X999\u0026#34; vip=\u0026#34;false\u0026#34;\u0026gt; \u0026lt;name\u0026gt;西蒙\u0026lt;/name\u0026gt; \u0026lt;gender\u0026gt;男\u0026lt;/gender\u0026gt; \u0026lt;phone\u0026gt;2987342394\u0026lt;/phone\u0026gt; \u0026lt;qq\u0026gt;29342394\u0026lt;/qq\u0026gt; \u0026lt;email\u0026gt;aaa@aa.com\u0026lt;/email\u0026gt; \u0026lt;/contact\u0026gt; \u0026lt;contact id=\u0026#34;9527\u0026#34; vip=\u0026#34;false\u0026#34;\u0026gt; \u0026lt;name\u0026gt;华安\u0026lt;/name\u0026gt; \u0026lt;phone\u0026gt;10086\u0026lt;/phone\u0026gt; \u0026lt;gender\u0026gt;男\u0026lt;/gender\u0026gt; \u0026lt;qq\u0026gt;10000\u0026lt;/qq\u0026gt; \u0026lt;email\u0026gt;10000@qq.com\u0026lt;/email\u0026gt; \u0026lt;/contact\u0026gt; \u0026lt;/contactList\u0026gt; DOM4J 解析 xml 示例代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 import java.util.Iterator; import java.util.List; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.Node; import org.dom4j.io.SAXReader; public class MoonZero { @SuppressWarnings(\u0026#34;unchecked\u0026#34;) public static void main(String[] args) throws Exception { // 创建SAXReader类的对象 SAXReader sax = new SAXReader(); // 获取xml文档的Document类对象 Document doc = sax.read(\u0026#34;contact.xml\u0026#34;); System.out.println(doc); // 获取标签对象 Element rootEle = doc.getRootElement(); System.out.println(rootEle); // 使用节点Node对象的迭代器遍历全部子标签元素 // getNodeIterator(rootEle); System.out.println(\u0026#34;-========\u0026#34;); // 获取当前标签下的子标签对象迭代器,输出所有子标签对象 getElementIterator(rootEle); // 两种获取当前标签下的所有子标签对象Element的List集合 getElementList(rootEle); // 得到指定名字的标签对象，如果多个同名标签则默认返回第一个元素对象 System.out.println(rootEle.element(\u0026#34;contact\u0026#34;).getName()); // 获取文本内容 第一个获取子标签名字\u0026#34;name\u0026#34; System.out.println(rootEle.element(\u0026#34;contact\u0026#34;).element(\u0026#34;name\u0026#34;).getName()); // 下面全部获取子标签的文本内容\u0026#34;潘金莲\u0026#34; System.out.println(rootEle.element(\u0026#34;contact\u0026#34;).element(\u0026#34;name\u0026#34;).getText()); System.out.println(rootEle.element(\u0026#34;contact\u0026#34;).element(\u0026#34;name\u0026#34;).getTextTrim()); System.out.println(\u0026#34;===================\u0026#34;); System.out.println(rootEle.element(\u0026#34;contact\u0026#34;).elementText(\u0026#34;name\u0026#34;)); System.out.println(rootEle.element(\u0026#34;contact\u0026#34;).elementTextTrim(\u0026#34;name\u0026#34;)); // 获取属性对象的迭代器 Element childEle = rootEle.element(\u0026#34;contact\u0026#34;); Iterator\u0026lt;Attribute\u0026gt; attIt = childEle.attributeIterator(); while (attIt.hasNext()) { System.out.println(attIt.next().getText()); } System.out.println(\u0026#34;=====================\u0026#34;); // 获取属性对象的List集合 List\u0026lt;Attribute\u0026gt; attList = childEle.attributes(); for (Attribute a : attList) { System.out.println(a.getText()); } // 直接获取属性值 System.out.println(childEle.attribute(\u0026#34;id\u0026#34;).getValue()); System.out.println(childEle.attribute(\u0026#34;id\u0026#34;).getText()); System.out.println(childEle.attributeValue(\u0026#34;id\u0026#34;)); // 获取属性的名称 System.out.println(childEle.attribute(\u0026#34;id\u0026#34;).getName()); } @SuppressWarnings(\u0026#34;unchecked\u0026#34;) public static void getElementList(Element rootEle) { List\u0026lt;Element\u0026gt; listEle = rootEle.elements(); for (Element childEle : listEle) { System.out.println(childEle); } System.out.println(\u0026#34;===============\u0026#34;); List\u0026lt;Element\u0026gt; listEle2 = rootEle.elements(\u0026#34;contact\u0026#34;); for (Element childEle : listEle2) { System.out.println(childEle); } } @SuppressWarnings(\u0026#34;unchecked\u0026#34;) public static void getElementIterator(Element rootEle) { // 获取当前标签下的子标签对象迭代器 Iterator\u0026lt;Element\u0026gt; eleIt = rootEle.elementIterator(\u0026#34;contact\u0026#34;); // 遍历迭代器，输出所有子标签对象 while (eleIt.hasNext()) { Element ele = eleIt.next(); System.out.println(ele); } } @SuppressWarnings({ \u0026#34;unchecked\u0026#34;, \u0026#34;static-access\u0026#34; }) public static void getNodeIterator(Element rootEle) { // 获取Node对象的迭代器 Iterator\u0026lt;Node\u0026gt; nodeIt = rootEle.nodeIterator(); // 遍历迭代器,判断是contact标签才输出 while (nodeIt.hasNext()) { Node node = nodeIt.next(); if (node.getNodeType() == node.ELEMENT_NODE) { System.out.println(node); } } } } 6.3.2. XML 数据的封装案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import java.io.File; import java.util.List; import org.apache.commons.beanutils.BeanUtils; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; /** * 需求 * 创建实体类Contact，包含属性：id、name、gender、phone、qq、email，属性全部使用String类型 * 将XML文件中的联系人封装成一个List\u0026lt;Contact\u0026gt;集合，并输出在控制台 * 1) 得到根元素 * 2）得到所有的contact元素 * 3) 得到contact元素中的下级元素封装成数据，其中id是属性，其它的都是文本。 */ public class MoonZero { @SuppressWarnings(\u0026#34;unchecked\u0026#34;) public static void main(String[] args) throws Exception { //创建SAXReader对象 SAXReader sax = new SAXReader(); //获取XML的document对象 Document doc = sax.read(new File(\u0026#34;contact.xml\u0026#34;)); //获取根元素对对象 Element ele = doc.getRootElement(); //获取子元素对象List集合 List\u0026lt;Element\u0026gt; eleList = ele.elements(); //遍历集合 for (Element childEle: eleList) { //创建contact对象 Contact c = new Contact(); //再获取子元素对象下的所有子子元素List集合 List\u0026lt;Element\u0026gt; childEleList = childEle.elements(); for (Element ccEle : childEleList) { String name = ccEle.getName(); Object value = ccEle.getTextTrim(); //使用BeanUtils方法给对象赋值 BeanUtils.setProperty(c, name, value); } //获取子元素属性对象集合 List\u0026lt;Attribute\u0026gt; attList = childEle.attributes(); for (Attribute att : attList) { BeanUtils.setProperty(c, att.getName(), att.getValue()); } System.out.println(c); } } } 6.3.3. XMLWriter 基础使用案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 import java.io.FileOutputStream; import java.io.IOException; import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.io.OutputFormat; import org.dom4j.io.SAXReader; import org.dom4j.io.XMLWriter; /* * 需求： * 生成一个空的XML文件 * 新增一个联系人对象到XML文件 */ public class MoonZero { public static void main(String[] args) throws Exception { // 生成一个空的XML文件 // createNewDocument(); // 给XML文件新增联系人 // 先读取文档，创建SAXReader对象获取Document对象 SAXReader sax = new SAXReader(); Document doc = sax.read(\u0026#34;newXML.xml\u0026#34;); // 获取根元素对象 Element rootEle = doc.getRootElement(); // 调用方法获取新增的标签对象，新增到根元素对象下 rootEle.add(createNewContact()); // 创建字节输出流对象，关联输出文件对象 FileOutputStream fos = new FileOutputStream(\u0026#34;newXML.xml\u0026#34;); // 创建格式化 OutputFormat format = OutputFormat.createPrettyPrint(); // 创建XMLWriter对象，将Document对象输出到文件、 XMLWriter xml = new XMLWriter(fos, format); xml.write(doc); // 关闭流对象 fos.close(); } public static Element createNewContact() { // 创建新增联系人对象 Contact con = new Contact(\u0026#34;剑圣主宰\u0026#34;, \u0026#34;男\u0026#34;, \u0026#34;10086\u0026#34;, \u0026#34;10000\u0026#34;, \u0026#34;10000@qq.com\u0026#34;, \u0026#34;9527\u0026#34;, true); // 将对象新增到根元素对象子标签中 // 给根元素新增一个子元素对象 Element childEle = DocumentHelper.createElement(\u0026#34;contact\u0026#34;); // 给子元素对象新增子元素对象和文本内容 childEle.addElement(\u0026#34;name\u0026#34;).setText(con.getName()); childEle.addElement(\u0026#34;gender\u0026#34;).setText(con.getGender()); childEle.addElement(\u0026#34;phone\u0026#34;).setText(con.getPhone()); childEle.addElement(\u0026#34;qq\u0026#34;).setText(con.getQq()); childEle.addElement(\u0026#34;email\u0026#34;).setText(con.getEmail()); // 给子元素对象新增属性对象和文本内容 childEle.addAttribute(\u0026#34;id\u0026#34;, con.getId()); childEle.addAttribute(\u0026#34;vip\u0026#34;, Boolean.toString(con.isVip())); return childEle; } // 生成一个空的XML文件 public static void createNewDocument() throws IOException { // 使用工具类DocumentHelper一个元素Element对象 Element rootEle = DocumentHelper.createElement(\u0026#34;contactList\u0026#34;); // 获取一个有根元素的Document对象 Document doc = DocumentHelper.createDocument(rootEle); // 创建格式化对象 OutputFormat format = OutputFormat.createPrettyPrint(); // 创建字节输出流对象，关联输出文件对象 FileOutputStream fos = new FileOutputStream(\u0026#34;newXML.xml\u0026#34;); // 创建XMLWriter对象 XMLWriter xml = new XMLWriter(fos, format); // 给根元素新增一个子元素对象 Element childEle = rootEle.addElement(\u0026#34;contact\u0026#34;); // 给子元素对象新增子元素对象和文本内容 childEle.addElement(\u0026#34;name\u0026#34;).setText(\u0026#34;敌法师\u0026#34;); childEle.addElement(\u0026#34;gender\u0026#34;).setText(\u0026#34;男\u0026#34;); childEle.addElement(\u0026#34;phone\u0026#34;).setText(\u0026#34;10086\u0026#34;); childEle.addElement(\u0026#34;qq\u0026#34;).setText(\u0026#34;10101010\u0026#34;); childEle.addElement(\u0026#34;email\u0026#34;).setText(\u0026#34;10101010@qq.com\u0026#34;); // 给子元素对象新增属性对象和文本内容 childEle.addAttribute(\u0026#34;id\u0026#34;, \u0026#34;s001\u0026#34;); childEle.addAttribute(\u0026#34;vip\u0026#34;, \u0026#34;true\u0026#34;); // 将Document输出到文件 xml.write(doc); // 关闭流对象 fos.close(); } } ","permalink":"https://ktzxy.top/posts/5vie41k26m/","summary":"Java基础 XML","title":"Java基础 XML"},{"content":"Go中的文件和目录操作 文件的读取 通过os.Open方法读取文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func main() { // 读取文件 方法1 file, err := os.Open(\u0026#34;./main/test.txt\u0026#34;) // 关闭文件流 defer file.Close(); if err != nil { fmt.Println(\u0026#34;打开文件出错\u0026#34;) } // 读取文件里面的内容 var tempSlice = make([]byte, 1024) var strSlice []byte for { n, err := file.Read(tempSlice) if err == io.EOF { fmt.Printf(\u0026#34;读取完毕\u0026#34;) break } fmt.Printf(\u0026#34;读取到了%v 个字节 \\n\u0026#34;, n) strSlice := append(strSlice, tempSlice...) fmt.Println(string(strSlice)) } } 通过bufio的方式读取 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func main() { // 读取文件 方法2 file, err := os.Open(\u0026#34;./main/test.txt\u0026#34;) // 关闭文件流 defer file.Close(); if err != nil { fmt.Println(\u0026#34;打开文件出错\u0026#34;) } // 通过创建bufio来读取 reader := bufio.NewReader(file) var fileStr string var count int = 0 for { // 相当于读取一行 str, err := reader.ReadString(\u0026#39;\\n\u0026#39;) if err == io.EOF { // 读取完成的时候，也会有内容 fileStr += str fmt.Println(\u0026#34;读取结束\u0026#34;, count) break } if err != nil { fmt.Println(err) break } count ++ fileStr += str } fmt.Println(fileStr) } 通过ioutil读取 文件比较少的时候，可以通过ioutil来读取文件\n1 2 3 // 通过IOUtil读取 byteStr, _ := ioutil.ReadFile(\u0026#34;./main/test.txt\u0026#34;) fmt.Println(string(byteStr)) 文件的写入 文件的写入，我们首先需要通过 os.OpenFile打开文件\n1 2 // 打开文件 file, _ := os.OpenFile(\u0026#34;./main/test.txt\u0026#34;, os.O_CREATE | os.O_RDWR, 777) 这里有三个参数\nname：要打开的文件名 flag：打开文件的模式 os.O_WRONLY：只读 os.O_CREATE：创建 os.O_RDONLY：只读 os.O_RDWR：读写 os.O_TRUNC：清空 os.O_APPEND：追加 perm：文件权限，一个八进制数，r（读）04，w（写）02，x（执行）01 通过OpenFile打开文件写入 1 2 3 4 5 // 打开文件 file, _ := os.OpenFile(\u0026#34;./main/test.txt\u0026#34;, os.O_CREATE | os.O_RDWR | os.O_APPEND, 777) defer file.Close() str := \u0026#34;啦啦啦 \\r\\n\u0026#34; file.WriteString(str) 通过bufio写入 1 2 3 4 5 6 7 8 9 10 11 12 // 打开文件 file, _ := os.OpenFile(\u0026#34;./main/test.txt\u0026#34;, os.O_CREATE | os.O_RDWR | os.O_APPEND, 777) defer file.Close() str := \u0026#34;啦啦啦 \\r\\n\u0026#34; file.WriteString(str) // 通过bufio写入 writer := bufio.NewWriter(file) // 先将数据写入缓存 writer.WriteString(\u0026#34;你好，我是通过writer写入的 \\r\\n\u0026#34;) // 将缓存中的内容写入文件 writer.Flush()\t通过ioutil写入 1 2 3 // 第三种方式，通过ioutil str2 := \u0026#34;hello\u0026#34; ioutil.WriteFile(\u0026#34;./main/test.txt\u0026#34;, []byte(str2), 777) 文件复制 通过ioutil读取和复制文件\n1 2 3 4 5 6 7 8 // 读取文件 byteStr, err := ioutil.ReadFile(\u0026#34;./main/test.txt\u0026#34;) if err != nil { fmt.Println(\u0026#34;读取文件出错\u0026#34;) return } // 写入指定的文件 ioutil.WriteFile(\u0026#34;./main/test2.txt\u0026#34;, byteStr, 777) 创建目录 1 os.Mkdir(\u0026#34;./abc\u0026#34;, 777) 删除操作 1 2 3 4 5 6 // 删除文件 os.Remove(\u0026#34;aaa.txt\u0026#34;) // 删除目录 os.Remove(\u0026#34;./aaa\u0026#34;) // 删除多个文件和目录 os.RemoveAll(\u0026#34;./aaa\u0026#34;) 重命名 1 os.Rename(\u0026#34;\u0026#34;) ","permalink":"https://ktzxy.top/posts/nmy2547926/","summary":"17 Go中的文件和目录操作","title":"17 Go中的文件和目录操作"},{"content":"ES介绍和使用 来源 https://www.liwenzhou.com/posts/Go/go_elasticsearch/\n今日内容 Logtransfer 从Kafka里面把日志取出来，写入ES，使用Kibana做可视化展示\n系统监控 psutil：采集系统信息的，写入到influxDB，使用 Grafana做展示\npromethenus监控：采集性能指标数据，保存起来，使用grafana做展示\nElasticSearch Elasticsearch（ES）是一个基于Lucene构建的开源、分布式、RESTful接口的全文搜索引擎。Elasticsearch还是一个分布式文档数据库，其中每个字段均可被索引，而且每个字段的数据均可被搜索，ES能够横向扩展至数以百计的服务器存储以及处理PB级的数据。可以在极短的时间内存储、搜索和分析大量的数据。通常作为具有复杂搜索场景情况下的核心发动机。\nKibana 图形化展示\nElasticSearch安装 去官网下载 ElasticSearch ，下载完成后，到bin目录，双击启动\n启动完成后，访问下面的URL\n1 http://127.0.0.1:9200/ 即可看到ElasticSearch的信息\nES API 以下示例使用curl演示。\n查看健康状态 1 curl -X GET 127.0.0.1:9200/_cat/health?v 输出：\n1 2 epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent 1564726309 06:11:49 elasticsearch yellow 1 1 3 3 0 0 1 0 - 75.0% 查询当前es集群中所有的indices 1 curl -X GET 127.0.0.1:9200/_cat/indices?v 输出：\n1 2 3 4 health status index uuid pri rep docs.count docs.deleted store.size pri.store.size green open .kibana_task_manager LUo-IxjDQdWeAbR-SYuYvQ 1 0 2 0 45.5kb 45.5kb green open .kibana_1 PLvyZV1bRDWex05xkOrNNg 1 0 4 1 23.9kb 23.9kb yellow open user o42mIpDeSgSWZ6eARWUfKw 1 1 0 0 283b 283b 创建索引 1 curl -X PUT 127.0.0.1:9200/www 输出：\n1 {\u0026#34;acknowledged\u0026#34;:true,\u0026#34;shards_acknowledged\u0026#34;:true,\u0026#34;index\u0026#34;:\u0026#34;www\u0026#34;} 删除索引 1 curl -X DELETE 127.0.0.1:9200/www 输出：\n1 {\u0026#34;acknowledged\u0026#34;:true} 插入记录 1 2 3 4 5 6 curl -H \u0026#34;ContentType:application/json\u0026#34; -X POST 127.0.0.1:9200/user/person -d \u0026#39; { \u0026#34;name\u0026#34;: \u0026#34;dsb\u0026#34;, \u0026#34;age\u0026#34;: 9000, \u0026#34;married\u0026#34;: true }\u0026#39; 输出：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 { \u0026#34;_index\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;_type\u0026#34;: \u0026#34;person\u0026#34;, \u0026#34;_id\u0026#34;: \u0026#34;MLcwUWwBvEa8j5UrLZj4\u0026#34;, \u0026#34;_version\u0026#34;: 1, \u0026#34;result\u0026#34;: \u0026#34;created\u0026#34;, \u0026#34;_shards\u0026#34;: { \u0026#34;total\u0026#34;: 2, \u0026#34;successful\u0026#34;: 1, \u0026#34;failed\u0026#34;: 0 }, \u0026#34;_seq_no\u0026#34;: 3, \u0026#34;_primary_term\u0026#34;: 1 } 也可以使用PUT方法，但是需要传入id\n1 2 3 4 5 6 curl -H \u0026#34;ContentType:application/json\u0026#34; -X PUT 127.0.0.1:9200/user/person/4 -d \u0026#39; { \u0026#34;name\u0026#34;: \u0026#34;sb\u0026#34;, \u0026#34;age\u0026#34;: 9, \u0026#34;married\u0026#34;: false }\u0026#39; 检索 Elasticsearch的检索语法比较特别，使用GET方法携带JSON格式的查询条件。\n全检索：\n1 curl -X GET 127.0.0.1:9200/user/person/_search 按条件检索：\n1 2 3 4 5 6 curl -H \u0026#34;ContentType:application/json\u0026#34; -X PUT 127.0.0.1:9200/user/person/4 -d \u0026#39; { \u0026#34;query\u0026#34;:{ \u0026#34;match\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;sb\u0026#34;} }\t}\u0026#39; ElasticSearch默认一次最多返回10条结果，可以像下面的示例通过size字段来设置返回结果的数目。\n1 2 3 4 5 6 7 curl -H \u0026#34;ContentType:application/json\u0026#34; -X PUT 127.0.0.1:9200/user/person/4 -d \u0026#39; { \u0026#34;query\u0026#34;:{ \u0026#34;match\u0026#34;: {\u0026#34;name\u0026#34;: \u0026#34;sb\u0026#34;}, \u0026#34;size\u0026#34;: 2 }\t}\u0026#39; Go操作Elasticsearch elastic client 我们使用第三方库https://github.com/olivere/elastic来连接ES并进行操作。\n注意下载与你的ES相同版本的client，例如我们这里使用的ES是7.2.1的版本，那么我们下载的client也要与之对应为github.com/olivere/elastic/v7。\n1 2 3 4 # 初始化mod go mod init es # 下载依赖 go get github.com/olivere/elastic/v7 使用go.mod来管理依赖：\n1 2 3 require ( github.com/olivere/elastic/v7 v7.0.4 ) 简单示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package main import ( \u0026#34;context\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;github.com/olivere/elastic/v7\u0026#34; ) // Elasticsearch demo type Person struct { Name string `json:\u0026#34;name\u0026#34;` Age int `json:\u0026#34;age\u0026#34;` Married bool `json:\u0026#34;married\u0026#34;` } func main() { client, err := elastic.NewClient(elastic.SetURL(\u0026#34;http://192.168.1.7:9200\u0026#34;)) if err != nil { // Handle error panic(err) } fmt.Println(\u0026#34;connect to es success\u0026#34;) p1 := Person{Name: \u0026#34;rion\u0026#34;, Age: 22, Married: false} put1, err := client.Index(). Index(\u0026#34;user\u0026#34;). BodyJson(p1). Do(context.Background()) if err != nil { // Handle error panic(err) } fmt.Printf(\u0026#34;Indexed user %s to index %s, type %s\\n\u0026#34;, put1.Id, put1.Index, put1.Type) } 更多使用详见文档：https://godoc.org/github.com/olivere/elastic\n","permalink":"https://ktzxy.top/posts/v10lejfv59/","summary":"ES介绍和使用","title":"ES介绍和使用"},{"content":"HTTP请求 本次内容 context 单元测试（给函数做单元测试） pprof调试工具（go语言内置工具） 可以看到代码的cpu和运行时的一些信息 能看到一些图表信息，如内存占用、cpu占用等 内容回顾 https://www.liwenzhou.com/posts/Go/14_concurrence/\n锁 sync.Mutex，底层是一个结构体，是值类型。给参数传递参数的时候，要传指针\n两个方法\n1 2 3 var lock sync.Mutex lock.lock() // 加锁 lock.unlock() //解锁 为什么要上锁？？\n防止多个goroutine同一时刻操作同一个资源。\n读写互斥锁 应用场景：适用于读多写少的场景下，也就是支持并发读，单个写\n特点\n读的goroutine来了获取的是读锁，后续的goroutine能读不能写 写的goroutine来了获取的是写锁，后续的goroutine不管是读还是写都要等待获取获取锁 使用\n1 2 3 4 5 6 var rwLock sync.RWMutex rwLock.RLock() // 获取读锁 rwLock.Runlock() // 释放锁 rwLock.Lock() // 获取写锁 rwLock.unlock() // 释放写锁 等待组 sync.waitgroup，用来等goroutine执行完在继续，是一个结构体，值类型，给函数传参数的时候要传指针\n1 2 3 4 5 var wg sync.WaitGroup wg.add(1) // 起几个goroutine就加几个数 wg.Done() // 在goroutine对应的函数中，函数要结束的时候调用，表示goroutine完成，计数器减1 wg.Wait() // 阻塞，等待所有的goroutine都结束 Sync.Once 某些函数只需要执行 一次的时候，就可以使用 sync.Once\n比如blog加载图片那个例子\n1 2 var once sync.Once once.Do() //接收一个没有参数也乜有返回值的函数，如有需要可以使用闭包 sync.Map 使用场景：并发操作一个map的时候，内置的map不是并发安全的\n而sync.map是一个开箱即用（不需要make初始化）的并发安全的map\n1 2 3 4 5 6 var syncMap sync.Map syncMap[key] = value //原生map syncMap.Store(key, value) // 存储 syncMap.LoadOrStore() // 获取 syncMap.Delete() syncMap.Range() 原子操作 Go语言内置了一些针对内置的基本数据类型的一些并发安全的操作\n1 2 var i int64 = 10 atomic.AddInt64(\u0026amp;i, 1) 网络编程 互联网的协议\nOSI七层模型：应用层、表示层、会话层、传输层、网络层、数据链路层、物理层\nTCP/IP协议：应用层、传输层、网络层、数据链路层、物理层\nHttp服务端 Go语言内置的net/http 包提供了HTTP客户端和服务端的实现\nHTTP协议 超文本传输协议（HTTP，Hyper Text Transfer Protocol）是互联网上应用最为广泛的一种网络传输协议，所有的wWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。\nHTTP客户端 使用net/http包编写一个简单的发送HTTP请求的Client端，代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 package main import \u0026#34;net/http\u0026#34; func f1(w http.ResponseWriter, r *http.Request) { str := \u0026#34;hello 沙河！\u0026#34; w.Write([]byte(str)) } func main() { http.HandleFunc(\u0026#34;/posts/Go/15_socket/\u0026#34;, f1) http.ListenAndServe(\u0026#34;127.0.0.1:9090\u0026#34;, nil) } 我们制作了一个最简单的api接口，最后返回的是我们的hello 沙河！\n我们也可以通过读取文件中的内容，然后进行显示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func f1(w http.ResponseWriter, r *http.Request) { index, err := ioutil.ReadFile(\u0026#34;./index.html\u0026#34;) if err != nil { w.Write([]byte(fmt.Sprintf(\u0026#34;%v\u0026#34;, err))) } w.Write([]byte(index)) } func main() { http.HandleFunc(\u0026#34;/index\u0026#34;, f1) http.HandleFunc(\u0026#34;/home\u0026#34;, f1) http.HandleFunc(\u0026#34;/about\u0026#34;, f1) http.ListenAndServe(\u0026#34;127.0.0.1:9090\u0026#34;, nil) } 自定义Server 要管理服务端的行为，可以创建一个自定义的Server：\n1 2 3 4 5 6 7 8 s := \u0026amp;http.Server{ Addr: \u0026#34;:8080\u0026#34;, Handler: myHandler, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, MaxHeaderBytes: 1 \u0026lt;\u0026lt; 20, } log.Fatal(s.ListenAndServe()) 网站运行运行流程 HTTP：超文本传输协议，规定了浏览器和网站服务器之间通信的规则\n规定了浏览器和网站服务器之间通信的规则 HTML：超文本标记语言，学的就是标记的符号，标签\nCSS：层叠样式表，规定了HTML中标签的具体样式（颜色/背景/大小/位置/浮动\u0026hellip;）\nJavaScript：一种跑在浏览器上的编程语言\nHTTP客户端 我们可以使用http客户端，去请求我们的URL地址，得到我们的内容\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func main() { res, err := http.Get(\u0026#34;http://127.0.0.1:9090/query?name=zansan\u0026amp;age=10\u0026#34;) if err != nil { fmt.Println(err) return } // 从res中把服务端返回的数据读取出来 b, err := ioutil.ReadAll(res.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(b)) } 对于GET请求，参数都放在URL上（query param），请求体上是没有数据的，我们可以通过以下方法来获取\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func f2(w http.ResponseWriter, r *http.Request) { fmt.Println(r.URL) fmt.Println(r.URL.Query()) // 识别URL中的参数 queryParams := r.URL.Query() name := queryParams.Get(\u0026#34;name\u0026#34;) age := queryParams.Get(\u0026#34;age\u0026#34;) fmt.Println(\u0026#34;传递来的name:\u0026#34;, name) fmt.Println(\u0026#34;传递来的age:\u0026#34;, age) fmt.Println(r.Method) fmt.Println(ioutil.ReadAll(r.Body)) } func main() { http.HandleFunc(\u0026#34;/query\u0026#34;, f2) http.ListenAndServe(\u0026#34;127.0.0.1:9090\u0026#34;, nil) } 同时上述的url也支持中文的请求，如下所示\n1 res, err := http.Get(\u0026#34;http://127.0.0.1:9090/query?name=张三\u0026amp;age=10\u0026#34;) 或者我们可以使用更为复杂的请求方式，可以使用下面的方式\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 data := url.Values{} urlObj, _ := url.Parse(\u0026#34;http://127.0.0.1:9090/query\u0026#34;) data.Set(\u0026#34;name\u0026#34;, \u0026#34;周林\u0026#34;) data.Set(\u0026#34;age\u0026#34;, \u0026#34;100\u0026#34;) // 对请求进行编码 queryStr := data.Encode() urlObj.RawQuery = queryStr req, err := http.NewRequest(\u0026#34;GET\u0026#34;, urlObj.String(), nil) if err != nil { fmt.Println(err) return } fmt.Println(req) 自定义Client 1 2 3 4 5 6 7 8 9 client := \u0026amp;http.Client{ CheckRedirect: redirectPolicyFunc, } resp, err := client.Get(\u0026#34;http://example.com\u0026#34;) // ... req, err := http.NewRequest(\u0026#34;GET\u0026#34;, \u0026#34;http://example.com\u0026#34;, nil) // ... req.Header.Add(\u0026#34;If-None-Match\u0026#34;, `W/\u0026#34;wyzzy\u0026#34;`) resp, err := client.Do(req) 自定义Transport 要管理代理、TLS配置、keep-alive、压缩和其他设置，创建一个Transport：\n1 2 3 4 5 6 tr := \u0026amp;http.Transport{ TLSClientConfig: \u0026amp;tls.Config{RootCAs: pool}, DisableCompression: true, } client := \u0026amp;http.Client{Transport: tr} resp, err := client.Get(\u0026#34;https://example.com\u0026#34;) Client和Transport类型都可以安全的被多个goroutine同时使用。出于效率考虑，应该一次建立、尽量重用。\n参考 Go语言基础之net/http ","permalink":"https://ktzxy.top/posts/kxi5mhlsyf/","summary":"HTTP请求","title":"HTTP请求"},{"content":"Kubernetes简介 来源 bilibili尚硅谷K8S视频：https://www.bilibili.com/video/BV1GT4y1A756\n中文官网：https://kubernetes.io/zh\n中文社区：https://www.kubernetes.org.cn/\n介绍 K8S主要讲的就是Kubernetes，首先Kubernetes首字母为K，末尾为s，中间一共有8个字母，所以简称K8s\nK8S概念和特性 部署发展历程 我们的项目部署也在经历下面的这样一个历程\n传统部署 -\u0026gt; 虚拟化部署时代 -\u0026gt; 容器部署时代\n传统部署时代：早期，组织在物理服务器上运行应用程序。无法为物理服务器中的应用程序定义资源边界，这会导致资源分配问题。例如，如果在物理服务器上运行多个应用程序，则可能会出现-一个应用程序占用大部分资源的情况，结果可能导致其他应用程序的性能下降。一种解决方案是在不同的物理服务器上运行每个应用程序，但是由于资源利用不足而无法扩展，并且组织维护许多物理服务器的成本很高。 虚拟化部署时代：作为解决方案，引入了虚拟化功能，它允许您在单个物理服务器的CPU上运行多个虚拟机（VM）。虚拟化功能允许应用程序在VM之间隔离，并提供安全级别，因为一个应用程序的信息不能被另一应用程序自由地访问。因为虚拟化可以轻松地添加或更新应用程序、降低硬件成本等等，所以虚拟化可以更好地利用物理服务器中的资源，并可以实现更好的可伸缩性。每个VM是一台完整的计算机，在虚拟化硬件之上运行所有组件，包括其自己的操作系统。 容器部署时代：容器类似于VM，但是它们具有轻量级的隔离属性，可以在应用程序之间共享操作系统 （OS），因此，容器被认为是轻量级的。容器与VM类似，具有自己的文件系统、CPU、内存、进程空间等。由于它们与基础架构分离，因此可以跨云和OS分发进行移植。 容器因具有许多优势而变得流行起来。下面列出了容器的一些好处：\n敏捷应用程序的创建和部署：与使用VM镜像相比，提高了容器镜像创建的简便性和效率。 持续开发、集成和部署：通过简单的回滚（由于镜像不可变性），提供可靠且频繁的容器镜像构建和部署。 关注开发与运维的分离：在构建时而不是在部署时创建应用程序容器镜像，将应用程序与基础架构分离。 可观察性：不仅可以显示操作系统级别的信息和指标，还可以显示应用程序的运行状况和其他指标信号。 跨开发、测试和生产的环境一致性：在便携式计算机上与在云中相同地运行。 云和操作系统分发的可移植性：可在Ubuntu、RHEL、RHEL、CoreOS、本地、Google Kubernetes Engine和其它任何其它地方运行。 以应用程序为中心的管理：提高抽象级别，从在虚拟硬件上运行OS到使用逻辑资源在OS上运行应用程序。 松散耦合、分布式、弹性、解放的微服务：应用程序被分解成较小的独立部分，并且可以动态部署和管理-而不是在一台大型单机上器体运行。 资源隔离：可预测的应用程序性能。 K8S概述 kubernetes，简称K8s，是用8 代替8 个字符“ubernete”而成的缩写。是一个开源的，用于管理云平台中多个主机上的容器化的应用，Kubernetes 的目标是让部署容器化的应用简单并且高效（powerful）,Kubernetes 提供了应用部署，规划，更新，维护的一种机制。\n传统的应用部署方式是通过插件或脚本来安装应用。这样做的缺点是应用的运行、配置、管理、所有生存周期将与当前操作系统绑定，这样做并不利于应用的升级更新/回滚等操作，当然也可以通过创建虚拟机的方式来实现某些功能，但是虚拟机非常重，并不利于可移植性。\n新的方式是通过部署容器方式实现，每个容器之间互相隔离，每个容器有自己的文件系统，容器之间进程不会相互影响，能区分计算资源。相对于虚拟机，容器能快速部署，由于容器与底层设施、机器文件系统解耦的。\n总结：\nK8s是谷歌在2014年发布的容器化集群管理系统 使用k8s进行容器化应用部署 使用k8s利于应用扩展 k8s目标实施让部署容器化应用更加简洁和高效 K8S概述 Kubernetes 是一个轻便的和可扩展的开源平台，用于管理容器化应用和服务。通过Kubernetes 能够进行应用的自动化部署和扩缩容。在Kubernetes 中，会将组成应用的容器组合成一个逻辑单元以更易管理和发现。\nKubernetes 积累了作为Google 生产环境运行工作负载15 年的经验，并吸收了来自于社区的最佳想法和实践。\nK8S功能 自动装箱 基于容器对应用运行环境的资源配置要求自动部署应用容器\n自我修复(自愈能力) 当容器失败时，会对容器进行重启\n当所部署的Node节点有问题时，会对容器进行重新部署和重新调度\n当容器未通过监控检查时，会关闭此容器直到容器正常运行时，才会对外提供服务\n如果某个服务器上的应用不响应了，Kubernetes会自动在其它的地方创建一个\n水平扩展 通过简单的命令、用户UI 界面或基于CPU 等资源使用情况，对应用容器进行规模扩大或规模剪裁\n当我们有大量的请求来临时，我们可以增加副本数量，从而达到水平扩展的效果\n当黄色应用过度忙碌，会来扩展一个应用\n服务发现 用户不需使用额外的服务发现机制，就能够基于Kubernetes 自身能力实现服务发现和负载均衡\n对外提供统一的入口，让它来做节点的调度和负载均衡， 相当于微服务里面的网关？\n滚动更新 可以根据应用的变化，对应用容器运行的应用，进行一次性或批量式更新\n添加应用的时候，不是加进去就马上可以进行使用，而是需要判断这个添加进去的应用是否能够正常使用\n版本回退 可以根据应用部署情况，对应用容器运行的应用，进行历史版本即时回退\n类似于Git中的回滚\n密钥和配置管理 在不需要重新构建镜像的情况下，可以部署和更新密钥和应用配置，类似热部署。\n存储编排 自动实现存储系统挂载及应用，特别对有状态应用实现数据持久化非常重要\n存储系统可以来自于本地目录、网络存储(NFS、Gluster、Ceph 等)、公共云存储服务\n批处理 提供一次性任务，定时任务；满足批量数据处理和分析的场景\nK8S架构组件 完整架构图 架构细节 K8S架构主要包含两部分：Master（主控节点）和 node（工作节点）\nmaster节点架构图\nNode节点架构图\nk8s 集群控制节点，对集群进行调度管理，接受集群外用户去集群操作请求；\nmaster：主控节点\nAPI Server：集群统一入口，以restful风格进行操作，同时交给etcd存储 提供认证、授权、访问控制、API注册和发现等机制 scheduler：节点的调度，选择node节点应用部署 controller-manager：处理集群中常规后台任务，一个资源对应一个控制器 etcd：存储系统，用于保存集群中的相关数据 Work node：工作节点\nKubelet：master派到node节点代表，管理本机容器 一个集群中每个节点上运行的代理，它保证容器都运行在Pod中 负责维护容器的生命周期，同时也负责Volume(CSI) 和 网络(CNI)的管理 kube-proxy：提供网络代理，负载均衡等操作 容器运行环境【Container Runtime】\n容器运行环境是负责运行容器的软件 Kubernetes支持多个容器运行环境：Docker、containerd、cri-o、rktlet以及任何实现Kubernetes CRI (容器运行环境接口) 的软件。 fluentd：是一个守护进程，它有助于提升 集群层面日志\nK8S核心概念 Pod Pod是K8s中最小的单元 一组容器的集合 共享网络【一个Pod中的所有容器共享同一网络】 生命周期是短暂的（服务器重启后，就找不到了） Volume 声明在Pod容器中可访问的文件目录 可以被挂载到Pod中一个或多个容器指定路径下 支持多种后端存储抽象【本地存储、分布式存储、云存储】 Controller 确保预期的pod副本数量【ReplicaSet】 无状态应用部署【Deployment】 无状态就是指，不需要依赖于网络或者ip 有状态应用部署【StatefulSet】 有状态需要特定的条件 确保所有的node运行同一个pod 【DaemonSet】 一次性任务和定时任务【Job和CronJob】 Deployment 定义一组Pod副本数目，版本等 通过控制器【Controller】维持Pod数目【自动回复失败的Pod】 通过控制器以指定的策略控制版本【滚动升级、回滚等】 Service 定义一组pod的访问规则 Pod的负载均衡，提供一个或多个Pod的稳定访问地址 支持多种方式【ClusterIP、NodePort、LoadBalancer】 可以用来组合pod，同时对外提供服务\nLabel label：标签，用于对象资源查询，筛选\nNamespace 命名空间，逻辑隔离\n一个集群内部的逻辑隔离机制【鉴权、资源】 每个资源都属于一个namespace 同一个namespace所有资源不能重复 不同namespace可以资源名重复 API 我们通过Kubernetes的API来操作整个集群\n同时我们可以通过 kubectl 、ui、curl 最终发送 http + json/yaml 方式的请求给API Server，然后控制整个K8S集群，K8S中所有的资源对象都可以采用 yaml 或 json 格式的文件定义或描述\n如下：使用yaml部署一个nginx的pod\n完整流程 通过Kubectl提交一个创建RC（Replication Controller）的请求，该请求通过APlserver写入etcd 此时Controller Manager通过API Server的监听资源变化的接口监听到此RC事件 分析之后，发现当前集群中还没有它所对应的Pod实例 于是根据RC里的Pod模板定义一个生成Pod对象，通过APIServer写入etcd 此事件被Scheduler发现，它立即执行执行一个复杂的调度流程，为这个新的Pod选定一个落户的Node，然后通过API Server讲这一结果写入etcd中 目标Node上运行的Kubelet进程通过APiserver监测到这个\u0026quot;新生的Pod.并按照它的定义，启动该Pod并任劳任怨地负责它的下半生，直到Pod的生命结束 随后，我们通过Kubectl提交一个新的映射到该Pod的Service的创建请求 ControllerManager通过Label标签查询到关联的Pod实例，然后生成Service的Endpoints信息，并通过APIServer写入到etod中， 接下来，所有Node上运行的Proxy进程通过APIServer查询并监听Service对象与其对应的Endponts信息，建立一个软件方式的负载均衡器来实现Service访问到后端Pod的流量转发功能 ","permalink":"https://ktzxy.top/posts/wwzizcapg7/","summary":"1 Kubernetes简介","title":"1 Kubernetes简介"},{"content":"Go中的日期函数 time包 时间和日期是我们编程中经常会用到的，在golang中time包提供了时间的显示和测量用的函数。\ntime.Now获取当前时间 1 2 3 4 5 timeObj := time.Now() year := timeObj.Year() month := timeObj.Month() day := timeObj.Day() fmt.Printf(\u0026#34;%d-%02d-%02d \\n\u0026#34;, year, month, day) 格式化日期 时间类型有一个自带的方法 Format进行格式化\n需要注意的是Go语言中格式化时间模板不是长久的 Y-m-d H:M:S\n而是使用Go的诞生时间 2006年1月2日 15点04分 （记忆口诀：2006 1 2 3 4 5）\n1 2 3 4 5 6 7 8 9 10 /** 时间类型有一个自带的方法 Format进行格式化 需要注意的是Go语言中格式化时间模板不是长久的 Y-m-d H:M:S 而是使用Go的诞生时间 2006年1月2日 15点04分 （记忆口诀：2006 1 2 3 4 5） */ timeObj2 := time.Now() // 24小时值 （15表示二十四小时） fmt.Println(timeObj2.Format(\u0026#34;2006-01-02 15:04:05\u0026#34;)) // 12小时制 fmt.Println(timeObj2.Format(\u0026#34;2006-01-02 03:04:05\u0026#34;)) 获取当前时间戳 时间戳是自1070年1月1日（08:00:00GMT）至当前时间的总毫秒数。它也被称为Unix时间戳\n1 2 3 4 5 6 7 8 /** 获取当前时间戳 */ timeObj3 := time.Now() // 获取毫秒时间戳 unixTime := timeObj3.Unix() // 获取纳秒时间戳 unixNaTime := timeObj3.UnixNano() 时间戳转日期字符串 通过将时间戳我们可以转换成日期字符串\n1 2 3 4 // 时间戳转换年月日时分秒（一个参数是秒，另一个参数是毫秒） var timeObj4 = time.Unix(1595289901, 0) var timeStr = timeObj4.Format(\u0026#34;2006-01-02 15:04:05\u0026#34;) fmt.Println(timeStr) 日期字符串转换成时间戳 1 2 3 4 5 // 日期字符串转换成时间戳 var timeStr2 = \u0026#34;2020-07-21 08:10:05\u0026#34;; var tmp = \u0026#34;2006-01-02 15:04:05\u0026#34; timeObj5, _ := time.ParseInLocation(tmp, timeStr2, time.Local) fmt.Println(timeObj5.Unix()) 时间间隔 time.Duration是time包定义的一个类型，它代表两个时间点之间经过的时间，以纳秒为单位。time.Duration表示一段时间间隔，可表示的最大长度段大约290年。\ntime包中定义的时间间隔类型的常量如下：\n时间操作函数 我们在日常的编码过程中可能会遇到要求时间+时间间隔的需求，Go语言的时间对象有提供Add方法如下\n1 func (t Time) Add(d Duration)Time 例如\n1 2 3 4 5 // 时间相加 now := time.Now() // 当前时间加1个小时后 later := now.Add(time.Hour) fmt.Println(later) 同理的方法还有：时间差、判断相等\n定时器 方式1：使用time.NewTicker（时间间隔）来设置定时器\n1 2 3 4 5 6 7 8 9 10 11 12 // 定时器, 定义一个1秒间隔的定时器 ticker := time.NewTicker(time.Second) n := 0 for i := range ticker.C { fmt.Println(i) n++ if n\u0026gt;5 { // 终止定时器 ticker.Stop() return } } 方式2：time.Sleep(time.Second)来实现定时器\n1 2 3 4 for { time.Sleep(time.Second) fmt.Println(\u0026#34;一秒后\u0026#34;) } ","permalink":"https://ktzxy.top/posts/9tnc5om0ag/","summary":"10 Go中的日期函数","title":"10 Go中的日期函数"},{"content":" Mac 系统请切换到root用户下使用\n-vv, -vvv : 打印详细信息 udp\\tcp：例如：udp port 5060 or udp portrange 10500-11652 tcp、ip、icmp、arp、rarp 和 tcp、udp、icmp这些选项等都要放到第一个参数的位置，用来过滤数据报的类型 -i eth1 : 只抓经过网卡eth1的包，取-i any 可捕获所有网卡数据包 -t : 不显示时间戳 -s 0 : 抓取数据包时默认抓取长度为68字节。加上-s 0 后可以抓到完整的数据包 -c 100 : 只抓取100个数据包 dst port ! 22 : 不抓取目标端口是22的数据包 src net 192.168.1.0/24 : 数据包的源网络地址为192.168.1.0/24 portrange : 指定端口范围，一般前面需要配合其他参数，如 udp portrange 10000-10100 -w ./target.cap : 保存成cap文件，方便用wireshark分析 host 指定 ip 收到和发出的所有分组 tcpdump host 192.168.131.128 示例：\n直接启动tcpdump将监视第一个网络接口上所有流过的数据包。\n1 tcpdump 截获所有192.168.131.128主机收到的和发出的所有的分组\n1 tcpdump host 192.168.131.128 截获主机192.168.131.130和主机192.168.131.128的通信\n1 2 tcpdump host 192.168.131.130 and 192.168.131.128 tcpdump -n -i eth1 host 192.168.131.130 and host 192.168.131.128 1 tcpdump tcp -i eth1 -t -s 0 -c 100 and dst port ! 22 and src net 192.168.1.0/24 -w ./target.cap 1 2 3 4 5 6 7 8 tcpdump -i eth0 udp port 5060 or tcp port 5060 or udp portrange 30000-30400 or tcp portrange 30000-30400 tcpdump -i eth0 port 5060 or portrange 30000-30100 tcpdump -i eno3 udp port 20080 or tcp port 20080 tcpdump -i eno4 host 59.206.39.116 nmcli connection show ","permalink":"https://ktzxy.top/posts/5hwp67rqn8/","summary":"tcpdump常见用法","title":"tcpdump常见用法"},{"content":"﻿# Day-09-Junit\u0026amp;注解\u0026amp;枚举\n【1】软件测试的目的: 软件测试的目的是在规定的条件下对程序进行操作,以发现程序错误,衡量软件质量,并对其是否能满足设计要求进行评估的过程\n【2】测试分类: (1)黑盒测试: 软件的黑盒测试意味着测试要在软件的接口处进行。这种方法是把测试对象看做一个黑盒子,测试人员完全不考虑程序内部的逻辑结构和内部特性只依据程序的需求规格说明书,检查程序的功能是否符合它的功能说明。因此黑盒测试又叫功能测试。 (2)白盒测试: 软件的白盒测试是对软件的过程性细节做细致的检查。这种方法是把测试对象看做一个打开的盒子,它允许测试人员利用程序内部的逻辑结构及有关信息设计或选择测试用例对程序的所有逻辑路径进行测试、通过在不同点检查程序状态.确定实际状态是否与预期的状态一致。因此白盒测试又称为结构测试。\n在没有使用Junit的时候，缺点:\n​\t(1）测试一定走main方法，是程序的入口，main方法的格式必须不能写错。\n​\t(2）要是在同一个main方法中测试的话，那么不需要测试的东西必须注释掉。\n​\t(3)测试逻辑如果分开的话，需要定义多个测试类，麻烦。\n​\t(4)业务逻辑和测试代码，都混淆了。\nJunit的使用 【1】一般测试和业务做一个分离，分离为不同的包：\n【2】测试类的名字：XXXTest\n【3】测试方法的定义 \u0026ndash;》这个方法可以独立运行，不依托于main方法\n建议：\n​\t参数：无参\n​\t返回值：void\n【4】测试方法定义完以后，不能直接就独立运行了，必须要在方法前加入一个注解：@Test\n【5】导入Junit依赖的环境：\n【6】代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.zy.test1; import com.zy.calculator.Calculator; import org.junit.Test; /** * @Auther: 赵羽 * @Description: com.zy.test1 * @version: 1.0 */ public class CalculatorTest { //测试add方法 @Test public void testAdd(){ System.out.println(\u0026#34;测试Add方法\u0026#34;); Calculator cal = new Calculator(); int add = cal.add(10, 20); System.out.println(add); } //测试sub方法 @Test public void testSub(){ System.out.println(\u0026#34;测试sub方法\u0026#34;); Calculator cal = new Calculator(); int sub = cal.sub(20, 30); System.out.println(sub); } } 【7】判定结果：\n绿色：正常结果\n红色：出现异常\n即使出现绿色效果，也有可能会发生代码中逻辑出现问题，则需要加入断言\n1 2 //加入断言：预测结果与实际结果进行匹对 Assert.assertEquals(30,add); //第一个参数为预测结果 第二个结果为实际结果 【8】@Before\u0026amp;@After\n@Before：某一个方法中，加入了@Before注解以后，那么这个方法中的功能会在测试方法执行前先执行。\n​\t一般会在@Before修饰的那个方法中加入：加入一些申请资源的代码：申请数据库资源，申请IO资源，申请网络资源。。\n@After：某一个方法中，加入了@After注解以后，那么这个方法中的功能会在测试方法执行后先执行。\n​\t一般会在@After修饰的那个方法中加入：加入一些释放资源的代码：释放数据库资源，释放IO资源，释放网络资源。。\n1 2 3 4 5 6 7 8 @Before public void init(){ System.out.println(\u0026#34;测试开始了\u0026#34;); } @After public void close(){ System.out.println(\u0026#34;测试结束了\u0026#34;); } 注解 注解初识 JDK5.0新增\u0026mdash;注解(Annotation),也叫元数据\n注解其实就是代码里的==特殊标记==，这些标记可以在编译,类加载运行时被读取.并执行相应的处理。==通过使用注解程序员可以在不改变原有逻辑的情况下，在源文件中嵌入一些补充信息。==代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。 使用注解时要在其前面增加@符号;并把该注解当成一个修饰符使用。用于修饰它支持的程序元素。\nAnnotation可以像修饰符一样被使用，可用于修饰包，类，构造器,方法，成员变量,参数，局部变量的声明，这些信息被保存在Annotation的\u0026quot;name=value\u0026quot;对中。在JavaSE中，注解的使用目的比较简单，例如标记过时的功能，忽略警告等。在JavaEE/Arldroid中注解占据了更重要的角色，例如用来配置应用程序的任何切面，==代替JavaE旧版中所遗留的繁冗代码和XML配置等。==未来的开发模式都是基于注解 的，JPA(java的持久化API)是基于注解的，Spring2.5以.E都是基于注解的，Hibernate3.x以后也是基于注解的，现在的Struts2有一部分也是基于注解的了，注解是一种趋势，一定程度上可以说︰==框架=注解+反射+设计模式==。\n文档相关注解 说明注释允许你在程序中嵌入关于程序的信息。你可以使用javadoc 工具软件来生成信息，并输出到HTML文件中。\n说明注释，使你更加方便的记录你的程序信息。 文档注解我们一般使用在文档注释中，配合javadoc工具javadoc工具软件识别以下标签:\n标签 描述 示例 @author 标识一个类的作者 @author description @deprecated 指名一个过期的类或成员 @deprecated description {@docRoot} 指明当前文档根目录的路径 Directory Path @exception 标志一个类抛出的异常 @exception exception-name explanation {@inheritDoc} 从直接父类继承的注释 Inherits a comment from the immediate surperclass. {@link} 插入一个到另一个主题的链接 {@link name text} {@linkplain} 插入一个到另一个主题的链接，但是该链接显示纯文本字体 Inserts an in-line link to another topic. @param 说明一个方法的参数 @param parameter-name explanation @return 说明返回值类型 @return explanation @see 指定一个到另一个主题的链接 @see anchor @serial 说明一个序列化属性 @serial description @serialData 说明通过writeObject( ) 和 writeExternal( )方法写的数据 @serialData description @serialField 说明一个ObjectStreamField组件 @serialField name type description @since 标记当引入一个特定的变化时 @since release @throws 和 @exception标签一样. The @throws tag has the same meaning as the @exception tag. {@value} 显示常量的值，该常量必须是static属性。 Displays the value of a constant, which must be a static field. @version 指定类的版本 @version info 需要注意这些标记的使用是有位置限制的：\n可以出现在类或者接口文档注释中的标记有：@see、@deprecated、@author、@version等。 可以出现在方法或者构造器文档注释中的标记有：@see、@deprecated、@param、@return、@throws、@exception等。 可以出现在文档注释中的标记有：@see、@deprecated等。 其中注意:\n@param @return和@exception这三个标记都是只用于方法的。\n@param的格式要求:@param形参名形参类型形参说明\n@return的格式要求:@return返回值类型返回值说明，如果方法的返回值类型是void就不能写\n@exception的格式要求:@exception异常类型异常说明\n@param和@exception可以并列多个\nIDEA中javadoc使用：\nJDK内置的三个注解 @Override:限定重写父类方法，该注解只能用于方法\n@Deprecated;用于表示所修饰的元素(类方法，构造器，属性等)已过时。通常是因为所修饰的结构危险或存在更好的选择\n@SuppressWarnings:抑制编译器警告\n自定义注解 注解内部：看上去是无参数方法，实际上理解为一个成员变量，一个属性无参数方法名字\u0026ndash;》成员变量的名字 无参数方法的返回值\u0026ndash;》成员变量的类型\n这个参数叫做 配置参数\n无参数方法的类型:基本数据类型（八种)，String，枚举，注解类型，还可以是以上类型对应的数组。\nPS:注意:如果只有一个成员变量的话，名字尽量叫value。\n使用注解：\n（1）使用注解的话，如果你定义了配置参数，就必须给赋值操作:\n1 2 3 @MyAnnotation(value = {\u0026#34;abc\u0026#34;,\u0026#34;def\u0026#34;}) public class Person { } （2）如果只有一个参数，并且这个参数的名字为value的话，那么value=可以省略不写。\n1 2 3 @MyAnnotation({\u0026#34;abc\u0026#34;,\u0026#34;def\u0026#34;}) public class Person { } （3）如果配置参数已经设置默认的值，那么使用的时候可以无需传值:\n1 2 3 public @interface MyAnnotation { String value() default \u0026#34;abc\u0026#34;; } 1 2 3 @MyAnnotation public class Person { } （4）一个注解的内部是可以不定义配置参数的:\n1 2 public @interface MyAnnotation { } 内部没有定义配置参数的注解\u0026ndash;》可以叫做标记\n内部定义配置参数的注解\u0026ndash;》元数据\n元注解 元注解是用于修饰其它注解的注解。\nJDK5.0提供了四种元注解: Retention,Target,Documented, Inherited\n【1】Retention\n@Retention:用于修饰注解，用于指定修饰的那个注解的生命周期，@Rentention包含一个RetentionPolicy枚举类型的成员变量使用@Rentention时必须为该value成员变量指定值:\nRetentionPolicy.SOURCE:在源文件中有效(即源文件保留)编译器直接丢弃这种策略的注释，在.class文件中不会保留注解信息 RetenlionPolicy.CLASS:在class文件中有效(即class保留)，保留在.class文件中，但是当运行Java程序时，他就不会继续加载了，不会保留在内存中，JVM不会保留注解。==如果注释没有加Retention元注解，那么相当于默认的注解就是这种状态。== RetentionPolicy.RUNTIME.在运行时有效(即运行时保留)当运行.Java程序时，JVM会保留注释，加载在内存中了，那么程序可以通过反射获取该注释。\n【2】Target\n用于修饰注解的注解，用于指定被修饰的注解能用于修饰哪些程序元素。@Target也包含一个名为value的成员变量。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.zy.anno; import java.lang.annotation.Target; import static java.lang.annotation.ElementType.*; /** * @Auther: 赵羽 * @Description: com.zy.anno * @version: 1.0 */ @Target({TYPE,CONSTRUCTOR,METHOD}) public @interface MyAnnotation2 { } 【3】Documented\n用于指定被该元注解修饰的注解类将被javadoc工具提取成文档。默认情况下，javadoc是不包括注解的，但是加上了这个注解生成的文档中就会带着注解了。\n案例：\n如果:Documented注解修饰了Deprecated注解，\n那么Deprecated注解就会在javadoc提取的时候，提取到API中:\n【4】Inherited\n被它修饰的Annotation将具有继承性。如果某个类使用了被Inherited修饰的Annotation,则其子类将自动具有该注解。\n案例：\n如果MyAnnotation2注解使用了@Inherited之后，就具备了继承性，那么相当于子类Student也使用了这个MyAnnotation2\n1 2 3 4 5 6 7 package com.zy.anno; import java.lang.annotation.Inherited; @Inherited public @interface MyAnnotation2 { } 父类：\n1 2 3 4 5 6 7 package com.zy.anno; import com.zy.anno2.MyAnnotation; @MyAnnotation public class Person { } 子类：\n1 2 3 4 package com.zy.anno; public class Student extends Person { } 枚举 【1】在java中，类的对象是有限个，确定的。这个类我们可以定义为枚举类。\n【2】自定义枚举类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.zy.enum01; /** * @Auther: 赵羽 * @Description: com.zy.enum01 * @version: 1.0 * 定义枚举类：季节 */ public class Season { //属性： private final String seasonName; //季节名字 private final String seasonDesc; //季节描述 //利用构造器对属性进行赋值操作： //构造器私有化，外界不能调用这个构造器，只能Season内部自己使用 private Season(String seasonName,String seasonDesc){ this.seasonName = seasonName; this.seasonDesc = seasonDesc; } //提供枚举类的有限的 确定的对象： public static final Season SPRING = new Season(\u0026#34;春天\u0026#34;,\u0026#34;春暖花开\u0026#34;); public static final Season SUMMER = new Season(\u0026#34;夏天\u0026#34;,\u0026#34;烈日炎炎\u0026#34;); public static final Season AUTUMN = new Season(\u0026#34;秋天\u0026#34;,\u0026#34;硕果累累\u0026#34;); public static final Season WINTER = new Season(\u0026#34;冬天\u0026#34;,\u0026#34;冰天雪地\u0026#34;); //额外因素： public String getSeasonName() { return seasonName; } public String getSeasonDesc() { return seasonDesc; } //toString(): @Override public String toString() { return \u0026#34;Season{\u0026#34; + \u0026#34;seasonName=\u0026#39;\u0026#34; + seasonName + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, seasonDesc=\u0026#39;\u0026#34; + seasonDesc + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } 测试类：\n1 2 3 4 5 6 7 public class TestSeason { public static void main(String[] args) { Season autumn = Season.AUTUMN; System.out.println(autumn); System.out.println(autumn.getSeasonName()); } } 【3】JDK1.5后使用enum关键字定义枚举类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.zy.enum02; /** * @Auther: 赵羽 * @Description: com.zy.enum01 * @version: 1.0 * 定义枚举类：季节 */ public enum Season { //提供枚举类的有限的 确定的对象： ---\u0026gt;enum枚举要求对象（常量）必须放在最开始位置 //多个对象之间用,进行连接，最后一个对象后面用;结束 SPRING (\u0026#34;春天\u0026#34;,\u0026#34;春暖花开\u0026#34;), SUMMER (\u0026#34;夏天\u0026#34;,\u0026#34;烈日炎炎\u0026#34;), AUTUMN(\u0026#34;秋天\u0026#34;,\u0026#34;硕果累累\u0026#34;), WINTER(\u0026#34;冬天\u0026#34;,\u0026#34;冰天雪地\u0026#34;); //属性： private final String seasonName; //季节名字 private final String seasonDesc; //季节描述 //利用构造器对属性进行赋值操作： //构造器私有化，外界不能调用这个构造器，只能Season内部自己使用 private Season(String seasonName, String seasonDesc){ this.seasonName = seasonName; this.seasonDesc = seasonDesc; } //额外因素： public String getSeasonName() { return seasonName; } public String getSeasonDesc() { return seasonDesc; } //toString(): @Override public String toString() { return \u0026#34;Season{\u0026#34; + \u0026#34;seasonName=\u0026#39;\u0026#34; + seasonName + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, seasonDesc=\u0026#39;\u0026#34; + seasonDesc + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } 使用枚举类：\n1 2 3 4 5 6 7 8 9 public class Test { public static void main(String[] args) { Season autumn = Season.AUTUMN; System.out.println(autumn); //enum关键字对应的枚举类的上层父类是: java.lang.Enum //但是我们自定义的枚举类的上层父类:Object System.out.println(Season.class.getSuperclass().getName()); //java.lang.Enum } } 源码中也有人这样写枚举类：\n1 2 3 4 5 6 public enum Season { SPRING, SUMMER, AUTUMN, WINTER; } 这个枚举类底层没有属性，属性，构造器，toString, get方法都删掉不写 看到的形态就剩:常量名(对象名)\n【4】enum类常用方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //用enum关键字创建的Season枚举类上面的父类是: java.Lang.Enum,常用方法子类Season可以直接拿过来使用 //toString();---\u0026gt;获取对象的名字 Season autumn = Season.AUTUMN; System.out.println(autumn); //AUTUMN //values:返回枚举类对象的数组 Season[] values = Season.values(); for (Season s:values){ System.out.println(s); } //valueOf:通过对象名字获取这个枚举对象 //注意：对象的名字必须传正确 Season autumn1 = Season.valueOf(\u0026#34;AUTUMN\u0026#34;); System.out.println(autumn1); 【5】枚举类实现接口：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.zy.enum04; /** * @Auther: 赵羽 * @Description: com.zy.enum03 * @version: 1.0 */ public enum Season implements TestInterface { SPRING, SUMMER, AUTUMN, WINTER; @Override public void show() { System.out.println(\u0026#34;你好\u0026#34;); } } 1 2 3 4 5 6 7 8 9 10 package com.zy.enum04; /** * @Auther: 赵羽 * @Description: com.zy.enum04 * @version: 1.0 */ public interface TestInterface { void show(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.zy.enum04; /** * @Auther: 赵羽 * @Description: com.zy.enum04 * @version: 1.0 */ public class Test { public static void main(String[] args) { Season autumn = Season.AUTUMN; autumn.show(); //你好 Season spring = Season.SPRING; spring.show(); //你好 } } ==上面发现所有的枚举对象，调用这个show方法的时候走的都是同一个方法，结果都一样。==\n实现：不同的对象调用的show方法也不同。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.zy.enum04; /** * @Auther: 赵羽 * @Description: com.zy.enum03 * @version: 1.0 */ public enum Season implements TestInterface { SPRING{ @Override public void show() { System.out.println(\u0026#34;这是春天。。\u0026#34;); } }, SUMMER{ @Override public void show() { System.out.println(\u0026#34;这是夏天。。\u0026#34;); } }, AUTUMN{ @Override public void show() { System.out.println(\u0026#34;这是秋天。。\u0026#34;); } }, WINTER{ @Override public void show() { System.out.println(\u0026#34;这是冬天。。\u0026#34;); } }; } 【6】枚举的应用：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package com.zy.enum05; /** * @Auther: 赵羽 * @Description: com.zy.enum05 * @version: 1.0 */ public class Person { //属性 private int age; private String name; private Gender sex; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Gender getSex() { return sex; } public void setSex(Gender sex) { this.sex = sex; } @Override public String toString() { return \u0026#34;Person{\u0026#34; + \u0026#34;age=\u0026#34; + age + \u0026#34;, name=\u0026#39;\u0026#34; + name + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, sex=\u0026#39;\u0026#34; + sex + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } 1 2 3 4 5 6 7 8 9 10 11 package com.zy.enum05; /** * @Auther: 赵羽 * @Description: com.zy.enum05 * @version: 1.0 */ public enum Gender { 男, 女; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.zy.enum05; /** * @Auther: 赵羽 * @Description: com.zy.enum05 * @version: 1.0 */ public class Test { public static void main(String[] args) { Person p = new Person(); p.setName(\u0026#34;张三\u0026#34;); p.setAge(18); p.setSex(Gender.男); System.out.println(p); } } 枚举和switch结合：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.zy.enum05; /** * @Auther: 赵羽 * @Description: com.zy.enum05 * @version: 1.0 */ public class Test02 { public static void main(String[] args) { Gender sex = Gender.男; //switch后面的（）中可以传入枚举类型 //switch后面的（）：int,short,byte,char,String,枚举 switch (sex){ case 女: System.out.println(\u0026#34;这是个女孩。。\u0026#34;); break; case 男: System.out.println(\u0026#34;这是个男孩。。\u0026#34;); break; } } } ","permalink":"https://ktzxy.top/posts/mzza080f4s/","summary":"Day 10 Junit\u0026amp;注解\u0026amp;枚举","title":"Day 10 Junit\u0026注解\u0026枚举"},{"content":"prometheus监控mongodb 1 mongodb_exporter部署 1.1 安装 1 2 3 4 wget https://github.com/percona/mongodb_exporter/releases/download/v0.11.2/mongodb_exporter-0.11.2.linux-amd64.tar.gz tar xf mongodb_exporter-0.11.2.linux-amd64.tar.gz -C /opt/ cd /opt \u0026amp;\u0026amp;mv mongodb_exporter-0.11.2.linux-amd64.tar.gz mongodb_exporter 1.2 启动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #无密码 /opt/mongodb_exporter/mongodb_exporter --mongodb.uri=mongodb://172.16.0.9:27017 #有密码 /opt/mongodb_exporter/mongodb_exporter --mongodb.uri=mongodb://user:password@172.16.0.9:27017 cat \u0026lt;\u0026lt;EOF \u0026gt;/usr/lib/systemd/system/mongodb_exporter.service [Unit] Description=MongoDB Exporter User=root [Service] Type=simple Restart=always ExecStart=/opt/mongodb_exporter/mongodb_exporter -- mongodb.uri=mongodb://172.16.0.9:27017 [Install] WantedBy=multi-user.target EOF systemctl start mongodb_exporter\u0026amp;\u0026amp;systemctl enable mongodb_exporter mongodb_exporter暴露的endpoint端口默认为9216\n1 curl http://172.16.0.9:9216/metrics 2 配置prometheus连接mongodb_exporter 1 2 3 4 5 - job_name: \u0026#39;elasticsearch_exporter\u0026#39; scrape_interval: 10s metrics_path: \u0026#34;/metrics\u0026#34; static_configs: - targets: [\u0026#39;172.16.0.9:9216\u0026#39;] 3 Grafana dashboard 导入mongodb监控模板，dashboard Id：2583 12079\n4 添加mongodb监控告警 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 groups: - name: mongodb rules: - alert: MongodbDown expr: mongodb_up == 0 for: 0m labels: severity: critical annotations: summary: MongoDB Down (instance {{ $labels.instance }}) description: \u0026#34;MongoDB instance is down\\n VALUE = {{ $value }}\\n LABELS = {{ $labels }}\u0026#34; - alert: MongodbReplicationLag expr: mongodb_mongod_replset_member_optime_date{state=\u0026#34;PRIMARY\u0026#34;} - ON (set) mongodb_mongod_replset_member_optime_date{state=\u0026#34;SECONDARY\u0026#34;} \u0026gt; 10 for: 0m labels: severity: critical annotations: summary: MongoDB replication lag (instance {{ $labels.instance }}) description: \u0026#34;Mongodb replication lag is more than 10s\\n VALUE = {{ $value }}\\n LABELS = {{ $labels }}\u0026#34; - alert: MongodbReplicationHeadroom expr: (avg(mongodb_mongod_replset_oplog_tail_timestamp - mongodb_mongod_replset_oplog_head_timestamp) - (avg(mongodb_mongod_replset_member_optime_date{state=\u0026#34;PRIMARY\u0026#34;}) - avg(mongodb_mongod_replset_member_optime_date{state=\u0026#34;SECONDARY\u0026#34;}))) \u0026lt;= 0 for: 0m labels: severity: critical annotations: summary: MongoDB replication headroom (instance {{ $labels.instance }}) description: \u0026#34;MongoDB replication headroom is \u0026lt;= 0\\n VALUE = {{ $value }}\\n LABELS = {{ $labels }}\u0026#34; - alert: MongodbTooManyConnections expr: avg by(instance) (rate(mongodb_connections{state=\u0026#34;current\u0026#34;}[1m])) / avg by(instance) (sum (mongodb_connections) by (instance)) * 100 \u0026gt; 80 for: 2m labels: severity: warning annotations: summary: MongoDB too many connections (instance {{ $labels.instance }}) description: \u0026#34;Too many connections (\u0026gt; 80%)\\n VALUE = {{ $value }}\\n LABELS = {{ $labels }}\u0026#34; ","permalink":"https://ktzxy.top/posts/h9bezlrabe/","summary":"Prometheus监控mongodb","title":"Prometheus监控mongodb"},{"content":"1. 异常概述 1.1. 什么是异常 Java程序在编译或运行过程中出现的问题就称为异常。\nJava 异常是 Java 提供的一种识别及响应错误的一致性机制。Java 异常机制可以使程序中异常处理代码和正常业务代码分离，保证程序代码更加优雅，并提高程序健壮性。\n1.2. 异常的继承体系 异常体系思维导图详见《01-Java SE 知识体系.xmind》\n1.2.1. Throwable Throwable 是 Java 异常体系的最顶层、所有错误与异常的超类。它包含了两个主要子类：Error（错误）和 Exception（异常）\n无论是错误还是异常，它们都有具体的子类体现每一个问题，它们的子类都有一个共性，就是都以父类名才作为子类的后缀名\n1.2.2. Error（错误） Error 类：是 Throwable 一个子类，用来指示运行时环境发生的错误。Java 程序通常不捕获错误，错误一般发生在严重故障时，它们在Java程序处理的范畴之外。一出现就是致命的，例如，服务器宕机，数据库崩溃，JVM 内存溢出等。一般地，程序不会从错误中恢复。\n特点：此类错误一般表示代码运行时 JVM 出现问题。通常有 VirtualMachineError（虚拟机运行错误）、NoClassDefFoundError（类定义错误）、OutOfMemoryError（内存不足错误）、StackOverflowError（栈溢出错误）等。此类错误发生时，JVM 将终止线程。\nTips: 这些错误是不受检异常，非代码性错误。因此，当此类错误发生时，应用程序不应该去处理此类错误。按照 Java 惯例，开发时是不应该实现任何新的 Error 子类的！\n1.2.3. Exception（异常） Exception（异常）指 Java 程序运行异常，即运行中的程序发生了不期望发生的事件，这种异常可以被 Java 异常处理机制处理。\nException 类是 Throwable 类的子类。所有的异常类是 java.lang.Exception 类的子类。这些异常又分为两类：RuntimeException(运行时异常) 和 CheckedException(编译时异常、检查异常)。\n1.3. 异常与错误的区别 异常：\n在编译或运行过程中出现的问题就称为异常 可以针对异常进行处理，处理后，后续的代码可以继续运行。如果不处理，否则程序直接崩溃 错误：\n程序在运行过程中出现的问题。 错误一般是由系统产生并反馈给JVM。 没有具体的处理方式，只能修改错误的代码。否则程序直接崩溃，无法运行 如何判断程序出现的问题是错误还是异常？\n根据控制台输入的错误信息，判断类名是以 Error 还是 Exception 结尾。 如果是 Error 则是错误，否是就是 Exception。 1.4. Java常见错误与异常 java.lang.IllegalAccessError：违法访问错误。当一个应用试图访问、修改某个类的域（Field）或者调用其方法，但是又违反域或方法的可见性声明，则抛出该异常。 java.lang.InstantiationError：实例化错误。当一个应用试图通过Java的 new 操作符构造一个抽象类或者接口时抛出该异常. java.lang.OutOfMemoryError：内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。 java.lang.StackOverflowError：堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。 java.lang.ClassCastException：类型转换异常。假设有类A和B（A不是B的父类或子类），O是A的实例，那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。 java.lang.ClassNotFoundException：找不到类异常。当应用试图根据字符串形式的类名构造类，而在遍历 CLASSPAH 之后找不到对应名称的 class 文件时，抛出该异常。 java.lang.ArithmeticException：算术条件异常。譬如：整数除零等。 java.lang.ArrayIndexOutOfBoundsException：数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。 java.lang.IndexOutOfBoundsException：索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时，抛出该异常。 java.lang.InstantiationException：实例化异常。当试图通过 newInstance() 方法创建某个类的实例，而该类是一个抽象类或接口时，抛出该异常。 java.lang.NoSuchFieldException：属性不存在异常。当访问某个类的不存在的属性时抛出该异常。 java.lang.NoSuchMethodException：方法不存在异常。当访问某个类的不存在的方法时抛出该异常。 java.lang.NullPointerException：空指针异常。当应用试图在要求使用对象的地方使用了 null 时，抛出该异常。譬如：调用 null 对象的实例方法、访问 null 对象的属性、计算 null 对象的长度、使用 throw 句抛出 null 等等。 java.lang.NumberFormatException：数字格式异常。当试图将一个 String 转换为指定的数字类型，而该字符串确不满足数字类型要求的格式时，抛出该异常。 java.lang.StringIndexOutOfBoundsException：字符串索引越界异常。当使用索引值访问某个字符串中的字符，而该索引值小于0或大于等于序列大小时，抛出该异常。 2. 异常的分类 Exception 异常类又分以下两种：\n编译时异常（检查性异常）：最具代表的检查性异常是用户错误或问题引起的异常，这是程序员无法预见的。例如要打开一个不存在文件时，一个异常就发生了，这些异常在编译时不能被简单地忽略。 运行时异常：运行时异常是可能被程序员避免的异常。与检查性异常相反，运行时异常可以在编译时被忽略。 2.1. 运行时异常 运行时异常，是指 RuntimeException及其子类。在编译时，Java 编译器不会对其进行检查，这类异常既不用抛出，也不用捕获，也会编译通过。一般都是程序员运行时才发现的异常，需要修改源码。以下是常见的内置运行时异常：\nArithmeticException NullPointerException(空指针) ArrayIndexOutOfBoundsException(数组越界) StringIndexOutOfBoundsException ClassCastException(类转换异常) ArrayStoreException(数据存储异常，操作数组时类型不一致) 还有IO操作的BufferOverflowException异常 2.2. 编译时异常 编译时异常，是指 Exception 类及其子类（除了 RuntimeException）。在编译时，Java 编译器会进行检查，如果程序中出现此类异常，必须进行处理，否则无法通过编译。\n比如：ClassNotFoundException（没有找到指定的类异常），IOException（IO流异常），要么通过 throws 进行声明抛出，要么通过 try-catch 进行捕获处理，否则不能通过编译。在程序中，通常不会自定义该类异常，而是直接使用系统提供的异常类。该异常必须手动在代码里添加捕获语句来处理该异常。\n2.3. 运行时异常和编译时异常的区别(理解) 运行时异常：只要是RuntimeException或其子类异常都是运行时异常。 编译时异常：除了运行时异常以外的所有异常都是编译时异常。 运行时异常特点 方法声明上如果声明的异常是运行时异常，则方法调用者可以处理，也可以不处理。 方法体内部抛出的异常是运行时异常，则方法声明上可以声明，也可以不声明。 编译时异常特点 方法声明上如果声明的异常是编译时异常，则要求方法调用者一定要处理。 方法体内部抛出的异常是编译时异常，则要求对该异常进行处理（捕获处理或者声明抛出处理）。 为什么java编译器对运行时异常管理如何松散？因为运行时异常一般通过程序员良好的编程习惯避免。\n3. 异常相关方法 Throwable 类是 Java 语言中所有错误或异常的超类，因此所有异常均继承了 Throwable 类中的以下主要方法：\n1 public String getMessage() 获得创建 Throwable 异常对象构造方法中指定的消息字符串。 1 public Throwable getCause() 返回一个 Throwable 对象代表异常原因 1 public String toString() 获得 Throwable 异常信息的类全名和消息字符串。 1 public void printStackTrace() 打印异常的栈信息，追溯异常根源。 将此 throwable 及其追踪输出至标准错误流。此方法将此 Throwable 对象的堆栈跟踪输出至错误输出流，作为字段 System.err 的值。输出的第一行包含此对象的 toString() 方法的结果。剩余行表示以前由方法 fillInStackTrace() 记录的数据。 1 public string getLocalizedMessage() 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法，可以声称本地化信息。如果子类没有覆盖该方法，则该方法返回的信息与 getMessage() 返回的结果相同 1 public StackTraceElement [] getStackTrace() 返回一个包含堆栈层次的数组。下标为0的元素代表栈顶，最后一个元素代表方法调用堆栈的栈底 1 public Throwable fillInStackTrace() 用当前的调用栈层次填充 Throwable 对象栈层次，添加到栈层次任何先前信息中。 1 public synchronized Throwable initCause(Throwable cause) 初始化原始异常 4. 异常处理 4.1. 异常的处理方式 JVM 对异常的默认处理方式是：\n将异常的类名，位置，原因等信息输出在控制台。 终止程序的运行。后续的代码没有办法执行。 在Java应用中，手动异常的处理机制分为：\n声明异常 抛出异常 捕获异常 这个体系中的所有类和对象都具备一个独有的特点；就是可抛性。可抛性的体现：就是这个体系中的类和对象都可以被 throws 和 throw 两个关键字所操作\n4.2. JVM 的异常处理 Java 通过面向对象的方法进行异常处理，一旦方法抛出异常，系统自动根据该异常对象寻找合适异常处理器（Exception Handler）来处理该异常，把各种不同的异常进行分类，并提供了良好的接口。\n在一个方法中如果发生异常，这个方法会创建一个异常对象，并转交给 JVM，该异常对象包含异常名称，异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用，最终才进入抛出异常的方法，这一系列方法调用的有序列表叫做调用栈。\nJVM 会顺着调用栈去查找看是否有可以处理异常的代码，如果有，则调用异常处理代码。当 JVM 发现可以处理异常的代码时，会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块，JVM 就会将该异常转交给默认的异常处理器（默认处理器为 JVM 的一部分），默认异常处理器打印出异常信息并终止应用程序。\n4.3. 异常处理之声明异常（throws 关键字） 当一个方法应该捕获一些已知的异常，但没有捕获到这些检查性异常时，那么该方法必须使用 throws 关键字来声明异常，传递异常给调用者去处理。throws 关键字放在方法签名的尾部。\n4.3.1. throws 语法格式 1 2 3 修饰符 返回值类型 方法名 (参数类型 参数名1, 参数类型 参数名2, ……) throws 异常类名1,异常类名2,…… { ...... } Notes: throws 后面跟多个异常，使用“,”逗号隔开。\n4.3.2. 异常声明抛出处理流程 在某个方法的声明上，让它(这个方法)抛出这个产生的异常，自己不处理，谁调用这个方法，谁处理 方法声明上加 throws 异常类名 如：方法a 中产生了异常，声明抛出 方法b 调用方法a，则该异常由a 转向了b 一般情况下，main 方法是不会声明抛出异常的，因为该方法抛出，直接抛出给了JVM，JVM 的默认处理机制是终止程序运行，则这样处理，相当于没有处理！ 4.4. 异常处理之抛出异常（throw 关键字） 当一个方法需要处理一些已知的异常，但方法自己不知道如何处理的异常，且不需要调用者处理，那么可以抛出异常。\nthrow 关键字作用是在方法内部抛出一个 Throwable 类型的异常。任何 Java 代码都可以通过 throw 语句抛出异常。\n4.4.1. throw 语法格式 1 throw new 异常类名(“异常信息”); Notes: throw 关键字之后的对象一定是异常对象，不能是其他对象。\n4.4.2. throws 和 throw 的区别 throws 和 throw 的作用：\nthrows：将异常类名标识出类，报告给方法调用者 throw：将异常对象抛出，抛给方法的调用者。 throws 和 throw 的使用位置：\nthrows：使用在方法声明上 throw：使用在方法体内部 调用者对 throws 和 throw 的处理：\nthrows：方法声明抛出的异常，在调用者的方法中必须包含可处理异常的代码，否则也要在方法签名中用 throws 关键字声明相应的异常。 throw：方法内部抛出的异常，方法调用者可以不进行处理 4.5. 异常处理之捕获异常（try catch finally） 在开发时，如果定义功能时，发现该功能会出现一些问题，应该将问题在定义功能时标示出来，这样调用者就可以在使用这个功能的时候，预先给出处理方式。\n如何标示呢？通过 throws 关键字完成，格式：throws 异常类名,异常类名...\n当方法声明异常后，调用者在使用该功能（调用该方法）时，就必须要处理，否则编译失败。当然调用者也可以继续将异常抛出\n4.5.1. 捕获处理总体格式 1 2 3 4 5 6 7 8 try { // 需要检测的异常； } catch(异常类名 变量名) { // 异常处理代码 // 通常我们只使用一个方法：printStackTrace 打印异常信息 } finally { // 不管是否有异常发生，都会执行该代码块中的代码。 } 4.5.2. 异常捕获处理-单 catch 处理 1 2 3 4 5 6 7 try{ // 需要检测的异常； } catch (异常类名 异常变量名) { // 异常处理代码 // 可以调用异常的方法 // 通常我们只使用一个方法：printStackTrace 打印异常信息 } 格式说明：\ntry 代码块中存放可能出现异常的代码。 catch 代码块中存放处理异常的代码。 执行流程：\n先执行try代码块中的代码，如果try代码块中的代码没有出现异常，则不会执行catch代码块的代码。 如果try代码块中的代码出现异常，则会进入catch代码块执行里面的代码。 4.5.3. 异常捕获处理-多 catch 处理 1 2 3 4 5 6 7 8 9 try { // 需要检测的异常； } catch(异常类名1 异常变量1) { // 异常处理代码，可以调用异常的方法 } catch(异常类名2 异常变量2) { // 异常处理代码，可以调用异常的方法 } catch(异常类名3 异常变量3 | 异常类名4 异常变量4 ...) { // 多个异常，都是同一种处理时，可以使用 \u0026#34;|\u0026#34; 来分隔 } …… // 可以有无数个catch 多 catch 处理注意事项\n多个catch 外理异常时，异常类名要有先后顺序。 如果多个异常类之间是平级关系(没有继承关系)，则没有先后顺序要求 如果多个异常类之间是上下级关系(有继承关系)，越高级的父类要写在越下面。 同一个 catch 代码块也可以捕获多种类型异常，用 | 分隔不同类型的异常 特殊情况：try对应多个catch时，如果有父类的catch语句块，一定要放在下面。 在实际开发中是不是只捕获Exception异常吗？因为实际开发中，需要针对不同的异常有不同的处理方式。\n4.5.4. finally 代码块 finally 代码块：只要进入了try的代码块，不管是否有异常发生，都会执行该代码块中的代码。因此通常用来释放资源，比如关闭IO流或数据库相关资源。\n1 2 3 4 5 6 7 8 9 10 11 12 13 try { // 需要检测的异常； } catch(异常类名1 异常变量1) { // 异常处理代码 // 可以调用异常的方法 // 通常我们只使用一个方法：printStackTrace 打印异常信息 } catch(异常类名2 异常变量2) { // 异常处理代码 // 可以调用异常的方法 // 通常我们只使用一个方法：printStackTrace 打印异常信息 } finally { // 不管是否有异常发生，都会执行该代码块中的代码。 } finally 代码块的注意事项：\n若 catch 代码块中包含 return 语句，finally 中的代码依然会执行。并且会先执行 finally 代码块中的逻辑后，再执行 catch 代码块的 return 语句。 若 catch 与 finally 代码块中都包含 return 语句，则只会执行 finally 中的 return 语句，不会执行 catch 中的 return 语句。 finally 中最好不要包含 return 语句，否则程序会提前退出，返回值不是 try 或 catch 中的返回值。 System.exit(0); 此方法是退出 jvm，只有这种情况 finally 代码块不执行。 finally 代码块是在 return 后面的表达式运算语句之后执行的。即 return 语句不会马上将运算后的值返回给调用者，而是先把要返回的值保存起来，待 finally 代码块执行完毕之后再向调用者返回其值。因此不管在 finally 中是否修改了返回值，返回的值都不会改变，仍然返回是之前保存的值。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void main(String[] args) { int sum = doSum(1, 2); System.out.println(sum); // 输出是 3 } private static int doSum(int a, int b) { int sum; try { sum = a + b; return sum; } finally { sum = 100; System.out.println(\u0026#34;finally sum: \u0026#34; + sum); // 输出是 100 } } 4.5.5. try-catch-finally 的几种组合方式 组合1：\n1 2 3 4 5 6 7 try { // ... do something } catch (Exception e) { // ... do something } finally { // ... do something } 组合2：\n1 2 3 4 5 try { // ... do something } catch (Exception e) { // ... do something } 组合3：\n1 2 3 4 5 try { // ... do something } finally { // ... do something } Tips: try finally 是针对出现了异常时，并不需要处理，但是资源一定关闭的这种特殊情况。记住 finally 很有用，主要用于关闭资源。无论是否发生异常，资源都必须进行关闭。\n4.6. try-with-resource （待整理） JAVA 7 以后提供了更优雅的方式来实现资源的自动释放，自动释放的资源需要是实现了 AutoCloseable 接口的类。例如：\n1 2 3 4 5 try (Scanner scanner = new Scanner(new FileInputStream(\u0026#34;c:/abc\u0026#34;), \u0026#34;UTF‐8\u0026#34;)) { // code } catch (IOException e) { // handle exception } 上例中，try 代码块退出时，会自动调用 scanner.close 方法，和把 scanner.close 方法放在 finally 代码块中不同的是，若 scanner.close 抛出异常，则会被抑制，抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常，如果想要获取被抑制的异常列表，可以调用 getSuppressed 方法来获取。\n4.7. 方法重写时异常处理（针对编译时异常） 父类方法声明上没有使用 throws 声明抛出异常，子类重写方法时不能声明抛出异常。 父类方法声明上使用 throws 声明一个异常时，子类重写方法时可以声明和父类一样的异常或其异常的子类。 父类方法声明上使用 throws 声明多个异常时，子类重写方法可以声明和父类多个异常的子集异常。 总结：子类不能声明抛出比父类大的异常。\nNotes:\n如果父类或者接口中的方法没有抛出过异常，那么子类是不可以抛出异常的，如果子类重写的方法中出现了异常，只能 try 不能 throws。 如果这个异常子类无法处理，已经影响了子类方法的具体运算，这时可以在子类方法中，通过 throw 抛出 RuntimeException 异常或者其子类，这样，子类的方法上是不需要 throws 声明的。 5. Java 异常处理最佳实践 在 Java 中处理异常并不是一个简单的事情。不仅仅初学者很难理解，即使一些有经验的开发者也需要花费很多时间来思考如何处理异常，包括需要处理哪些异常，怎样处理等等。\nTips: 也可以直接参考《阿里巴巴 Java 开发手册》中的“异常处理”部分内容\n5.1. 异常处理方式的选择 可以根据下图来选择是捕获异常，声明异常还是抛出异常\n相关异常处理选择的问题：\n定义异常处理时，什么时候定义try，什么时候定义throws呢？ 功能内部如果出现异常，如果内部可以处理，就用try； 如果功能内部处理不了，就必须声明出来，让调用者处理。 什么时候使用抛出处理？什么时候使用捕获处理? 如果当前代码是直接跟用户打交道的，则千万不要抛出异常，要捕获处理。 如果需要将异常报告给上一层(方法调用者)则要使用抛出处理。 父类方法没有声明抛出异常，子类覆盖方法时发生了异常，怎么办？ 子类重写方法时，可以抛出异常，但是只能抛出运行时异常或其子类。 5.2. 优先明确的异常 抛出的异常越明确越好，这样 API 更容易被理解。方法的调用者能够更好的处理异常并且避免额外的检查。因此，总是尝试寻找最适合异常事件的类，例如，抛出一个 NumberFormatException 来替换一个 IllegalArgumentException。避免抛出一个不明确的异常。\n1 2 3 4 5 6 7 public void doNotDoThis() throws Exception { // ... } public void doThis() throws NumberFormatException { // ... } 5.3. 对异常进行文档说明 当在方法上声明抛出异常时，也需要进行文档说明。目的是为了给调用者提供尽可能多的信息，从而可以更好地避免或处理异常。在 Javadoc 添加 @throws 声明，并且描述抛出异常的场景。\n1 2 3 4 5 6 7 8 /** * xxx * @param input xxx * @throws MyBusinessException 异常的详细描述 */ public void doSomething(String input) throws MyBusinessException { // ... } 5.4. 不要捕获 Throwable 类 Throwable 是所有异常和错误的超类。语法上可以允许 catch 子句中使用它，但是建议永远不应该这样做！\n如果在 catch 子句中使用 Throwable，它不仅会捕获所有异常，也将捕获所有的错误。JVM 抛出错误，指出不应该由应用程序处理的严重问题。典型的例子是 OutOfMemoryError 或者 StackOverflowError。两者都是由应用程序控制之外的情况引起的，无法处理。所以，最好不要捕获 Throwable，除非确定自己处于一种特殊的情况下能够处理错误。\n1 2 3 4 5 6 7 public void doNotCatchThrowable() { try { // do something } catch (Throwable t) { // don\u0026#39;t do this! } } 5.5. 不要忽略异常 很多时候，开发者很有自信不抛出异常，因此写了一个 catch 块，但是没有做任何处理或者记录日志。但现实是经常会出现无法预料的异常，或者无法确定这里的代码未来会不会被改动(删除了阻止异常抛出的代码)，而此时由于异常被捕获，使得无法拿到足够的错误信息来定位问题。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void doNotIgnoreExceptions() { try { // do something } catch (NumberFormatException e) { // this will never happen } } public void logAnException() { try { // do something } catch (NumberFormatException e) { log.error(\u0026#34;This should never happen: \u0026#34; + e); } } 5.6. 不要记录并抛出异常 很多代码甚至类库中都会有捕获异常、记录日志并再次抛出的逻辑。\n1 2 3 4 5 6 try { // do something } catch (NumberFormatException e) { log.error(e); throw e; } 这种处理逻辑看着是合理的，但这会导致给同一个异常输出多条日志。建议可以将异常包装为自定义异常，让仅仅当想要处理异常时才去捕获，否则只需要在方法签名中声明让调用者去处理。\n1 2 3 4 5 6 7 public void doNotThrowAgainExceptions() throws MyBusinessException { try { // do something } catch (NumberFormatException e) { throw new MyBusinessException(\u0026#34;A message that describes the error.\u0026#34;, e); } } 5.7. 包装异常时不要抛弃原始的异常 捕获标准异常并包装为自定义异常是一个很常见的做法，这样可以添加更为具体的异常信息并能够做针对的异常处理。\n在包装异常时，请确保将原始异常设置为原因。否则，将会丢失堆栈跟踪和原始异常的消息，这将会使分析导致异常的异常事件变得困难。\n1 2 3 4 5 6 7 public void wrapException() throws MyBusinessException { try { // do something } catch (NumberFormatException e) { throw new MyBusinessException(\u0026#34;A message that describes the error.\u0026#34;, e); } } Notes：上例代码 NumberFormatException e 中的原始异常 e）。Exception 类提供了特殊的构造函数方法，它接受一个 Throwable 作为参数。\n5.8. 不要使用异常来控制程序的流程 不应该使用异常控制应用的执行流程，例如，本应该使用 if 语句进行条件判断的情况下，却使用异常处理，这是非常不好的习惯，会严重影响应用的性能。\n5.9. 使用标准异常 如果使用内建的异常可以解决问题，就不要用自定义异常。Java API 提供了上百种针对不同情况的异常类型，在开发中首先尽可能使用 Java API 提供的异常，如果标准的异常不能满足要求，这时候创建自己的定制异常。尽可能得使用标准异常有利于新加入的开发者看懂项目代码。\n5.10. 异常会影响性能 异常处理的性能成本非常高，每个 Java 程序员在开发时都应牢记这句话。创建一个异常非常慢，抛出一个异常又会消耗1~5ms，当一个异常在应用的多个层级之间传递时，会拖累整个应用的性能。\n仅在异常情况下使用异常 在可恢复的异常情况下使用异常 尽管使用异常有利于 Java 开发，但是在应用中最好不要捕获太多的调用栈，因为在很多情况下都不需要打印调用栈就知道哪里出错了。因此，异常消息应该提供恰到好处的信息。\n6. 自定义异常 命名规范：XxxException 继承关系：继承 Exception 或 RuntimeException 构造方法：一般提供无参构造方法和1个有参构造方法，用于记录详细异常描述信息 6.1. 异常链 异常链是指，同时有很多异常抛出，即一个异常引发另一个异常，另一个异常引发更多异常。一般会在在开头或结尾处找它的原始异常来解决问题，异常可通过 initCause 方法串起来，可以通过自定义异常。\n","permalink":"https://ktzxy.top/posts/seivp9mp4j/","summary":"Java基础 异常","title":"Java基础 异常"},{"content":"原文链接：https://www.jianshu.com/p/dde0dc1761ec\n一、Prometheus简介 Prometheus是由SoundCloud开发的开源监控报警系统和时序列数据库(TSDB)。\nPrometheus使用Go语言开发，是Google BorgMon监控系统的开源版本。2016年由Google发起Linux基金会旗下的原生云基金会(Cloud Native Computing Foundation), 将Prometheus纳入其下第二大开源项目。Prometheus目前在开源社区相当活跃。Prometheus和Heapster(Heapster是K8S的一个子项目，用于获取集群的性能数据。)相比功能更完善、更全面。Prometheus性能也足够支撑上万台规模的集群。\n1.系统架构图 2.基本原理 Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态，任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做非常适合做虚拟化环境监控系统，比如VM、Docker、Kubernetes等。输出被监控组件信息的HTTP接口被叫做exporter 。目前互联网公司常用的组件大部分都有exporter可以直接使用，比如Varnish、Haproxy、Nginx、MySQL、Linux系统信息(包括磁盘、内存、CPU、网络等等)。\n其大概的工作流程是：\nPrometheus server 定期从配置好的 jobs 或者 exporters 中拉 metrics，或者接收来自 Pushgateway 发过来的 metrics，或者从其他的 Prometheus server 中拉 metrics。 Prometheus server 在本地存储收集到的 metrics，并运行已定义好的 alert.rules，记录新的时间序列或者向 Alertmanager 推送警报。 Alertmanager 根据配置文件，对接收到的警报进行处理，发出告警。 在Grafana图形界面中，可视化查看采集数据。 3.Prometheus的特性 多维度数据模型。 灵活的查询语言。 不依赖分布式存储，单个服务器节点是自主的。 通过基于HTTP的pull方式采集时序数据。 可以通过中间网关进行时序列数据推送。 通过服务发现或者静态配置来发现目标服务对象。 支持多种多样的图表和界面展示，比如Grafana等。 4.Prometheus的组件 Prometheus Server 主要负责数据采集和存储，提供PromQL查询语言的支持。 Alertmanager 警告管理器，用来进行报警。 Push Gateway 支持临时性Job主动推送指标的中间网关。 Exporters 输出被监控组件信息的HTTP接口。 Grafana 监控数据展示Web UI。 5.服务发现 由于 Prometheus 是通过 Pull 的方式主动获取监控数据，也就是每隔几秒钟去各个target采集一次metric。所以需要手工指定监控节点的列表，当监控的节点增多之后，每次增加节点都需要更改配置文件，尽管可以使用接口去热更新配置文件，但仍然非常麻烦，这个时候就需要通过服务发现（service discovery，SD）机制去解决。\nPrometheus 支持多种服务发现机制，可以自动获取要收集的 targets，包含的服务发现机制包括：azure、consul、dns、ec2、openstack、file、gce、kubernetes、marathon、triton、zookeeper（nerve、serverset），配置方法可以参考手册的配置页面。可以说 SD 机制是非常丰富的，但目前由于开发资源有限，已经不再开发新的 SD 机制，只对基于文件的 SD 机制进行维护。针对我们现有的系统情况，我们选择了静态配置方式。\n二、部署PrometheusServer 1. 使用官方镜像运行 由于Prometheus官方镜像没有开启热加载功能，而且时区相差八小时，所以我们选择了自己制作镜像，当然你也可以使用官方的镜像，提前创建Prometheus配置文件prometheus.yml和Prometheus规则文件rules.yml，然后通过如下命令挂载到官方镜像中运行：\n1 2 3 $ docker run -d -p 9090:9090 --name=prometheus \\ -v /root/prometheus/conf/:/etc/prometheus/ \\ prom/prometheus 使用官方镜像部署可以参考我的这篇文章：Docker部署Prometheus实现微信邮件报警。\n2. 制作镜像 现在我们创建自己的Prometheus镜像，当然你也可以直接使用我制作的镜像\n1 $ docker pull zhanganmin2017/prometheus:v2.9.0 首先去Prometheus下载二进制文件安装包解压到package目录下，我的Dockerfile目录结构如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $ tree prometheus-2.9.0/ prometheus-2.9.0/ ├── conf │ ├── CentOS7-Base-163.repo │ ├── container-entrypoint │ ├── epel-7.repo │ ├── prometheus-start.conf │ ├── prometheus-start.sh │ ├── prometheus.yml │ ├── rules │ │ └── service_down.yml │ └── supervisord.conf ├── Dockerfile └── package ├── console_libraries ├── consoles ├── LICENSE ├── NOTICE ├── prometheus ├── prometheus.yml └── promtool 5 directories, 26 files 分别创建图中的目录，可以看到conf目录中有一些名为supervisord的文件，这是因为在容器中的进程我们选择使用supervisor进行管理，当然如果不想使用的化可以进行相应的修改。\n制作prometheus-start.sh启动脚本，Supervisor启动Prometheus会调用该脚本\n1 2 3 4 5 6 7 #!/bin/bash /bin/prometheus \\ --config.file=/data/prometheus/prometheus.yml \\ --storage.tsdb.path=/data/prometheus/data \\ --web.console.libraries=/data/prometheus/console_libraries \\ --web.enable-lifecycle \\ --web.console.templates=/data/prometheus/consoles 制作Prometheus-start.conf启动文件,Supervisord的配置文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 [program:prometheus] command=sh /etc/supervisord.d/prometheus-start.sh ; 程序启动命令 autostart=false ; 在supervisord启动的时候不自动启动 startsecs=10 ; 启动10秒后没有异常退出，就表示进程正常启动了，默认1秒 autorestart=false ; 关闭程序退出后自动重启，可选值：[unexpected,true,false]，默认为unexpected,表示进程意外杀死才重启 startretries=0 ; 启动失败自动重试次数，默认是3 user=root ; 用哪个用户启动进程，默认是root redirect_stderr=true ; 把stderr重定向到stdout，默认false stdout_logfile_maxbytes=20MB ; stdout 日志文件大小，默认是50MB stdout_logfile_backups=30 ; stdout 日志文件备份数，默认是10; # stdout 日志文件，需要注意当指定目录不存在时无法正常启动，所以需要手动创建目录(supervisord 会自动创建日志文件) stdout_logfile=/data/prometheus/prometheus.log stopasgroup=true killasgroup=tru 制作supervisord.conf启动文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 [unix_http_server] file=/var/run/supervisor.sock ; (the path to the socket file) chmod=0700 ; sockef file mode (default 0700) [supervisord] logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log) pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid) childlogdir=/var/log/supervisor ; (\u0026#39;AUTO\u0026#39; child log dir, default $TEMP) user=root minfds=10240 minprocs=200 [rpcinterface:supervisor] supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [supervisorctl] serverurl=unix:///var/run/supervisor.sock ; use a unix:// URL for a unix socket [program:sshd] command=/usr/sbin/sshd -D autostart=true autorestart=true stdout_logfile=/var/log/supervisor/ssh_out.log stderr_logfile=/var/log/supervisor/ssh_err.log [include] files = /etc/supervisord.d/*.conf 制作container-entrypoint守护文件，容器启动后执行的脚本\n1 2 3 4 5 6 7 8 #!/bin/sh set -x if [ ! -d \u0026#34;/data/prometheus\u0026#34; ];then mkdir -p /data/prometheus/data fi mv /usr/local/src/* /data/prometheus/ exec /usr/bin/supervisord -n exit 在conf目录下新建Prometheus.yml配置文件，这个是Prometheus配置监控主机的文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 global: scrape_interval: 60s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 60s # Evaluate rules every 15 seconds. The default is every 1 minute. alerting: alertmanagers: - static_configs: - targets: [ \u0026#39;192.168.133.110:9093\u0026#39;] rule_files: - \u0026#34;rules/host_sys.yml\u0026#34; scrape_configs: - job_name: \u0026#39;Host\u0026#39; static_configs: - targets: [\u0026#39;10.1.250.36:9100\u0026#39;] labels: appname: \u0026#39;DEV01_250.36\u0026#39; - job_name: \u0026#39;prometheus\u0026#39; static_configs: - targets: [ \u0026#39;10.1.133.210:9090\u0026#39;] labels: appname: \u0026#39;Prometheus\u0026#39; 在conf目录下新建rules目录，编写service_down.yml规则文件，这个也可以等到容器创建后再编写，这里我们就直接写好添加到镜像中\n1 2 3 4 5 6 7 8 9 10 11 12 13 groups: - name: servicedown rules: - alert: InstanceDown expr: up == 0 for: 1m labels: name: instance severity: Critical annotations: summary: \u0026#34; {{ $labels.appname }}\u0026#34; description: \u0026#34; 服务停止运行 \u0026#34; value: \u0026#34;{{ $value }}\u0026#34; 制作dockerfile 镜像文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 FROM docker.io/centos:7 MAINTAINER from zhanmin@1an.com # install repo RUN rm -rf /etc/yum.repos.d/*.repo ADD conf/CentOS7-Base-163.repo /etc/yum.repos.d/ ADD conf/epel-7.repo /etc/yum.repos.d/ # yum install RUN yum install -q -y openssh-server openssh-clients net-tools \\ vim supervisor \u0026amp;\u0026amp; yum clean all # install sshd RUN ssh-keygen -q -N \u0026#34;\u0026#34; -t rsa -f /etc/ssh/ssh_host_rsa_key \\ \u0026amp;\u0026amp; ssh-keygen -q -N \u0026#34;\u0026#34; -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key \\ \u0026amp;\u0026amp; ssh-keygen -q -N \u0026#34;\u0026#34; -t ed25519 -f /etc/ssh/ssh_host_ed25519_key \\ \u0026amp;\u0026amp; sed -i \u0026#39;s/#UseDNS yes/UseDNS no/g\u0026#39; /etc/ssh/sshd_config # UTF-8 and CST +0800 ENV LANG=zh_CN.UTF-8 RUN echo \u0026#34;export LANG=zh_CN.UTF-8\u0026#34; \u0026gt;\u0026gt; /etc/profile.d/lang.sh \\ \u0026amp;\u0026amp; ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \\ \u0026amp;\u0026amp; localedef -c -f UTF-8 -i zh_CN zh_CN.utf8 # install Prometheus COPY package/prometheus /bin/prometheus COPY package/promtool /bin/promtool COPY package/console_libraries/ /usr/local/src/console_libraries/ COPY package/consoles/ /usr/local/src/consoles/ COPY conf/prometheus.yml /usr/local/src/prometheus.yml COPY conf/rules/ /usr/local/src/rules/ # create user RUN echo \u0026#34;root:123456\u0026#34; | chpasswd # supervisord ADD conf/supervisord.conf /etc/supervisord.conf ADD conf/prometheus-start.conf /etc/supervisord.d/prometheus-start.conf ADD conf/container-entrypoint /container-entrypoint ADD conf/prometheus-start.sh /etc/supervisord.d/prometheus-start.sh RUN chmod +x /container-entrypoint # cmd CMD [\u0026#34;/container-entrypoint\u0026#34;] Dockerfile中安装了supervisor进程管理工具和SSH服务，指定了字符集和时区。\n生成镜像并运行容器服务\n1 2 3 4 $ docker build -t zhanganmin2017/prometheus:v2.9.0 . $ docker run -itd -h prometheus139-210 -m 8g --cpuset-cpus=28-31 --name=prometheus139-210 --network trust139 --ip=10.1.133.28 -v /data/works/prometheus139-210:/data 192.168.166.229/1an/prometheus:v2.9.0 $ docker exec -it prometheus139-210 /bin/bash $ supervisorctl start prometheus首先去Prometheus 访问prometheus Web页面 IP:9090\n三、部署监控组件Exporter Prometheus 是使用 Pull 的方式来获取指标数据的，要让 Prometheus 从目标处获得数据，首先必须在目标上安装指标收集的程序，并暴露出 HTTP 接口供 Prometheus 查询，这个指标收集程序被称为 Exporter ，不同的指标需要不同的 Exporter 来收集，目前已经有大量的 Exporter 可供使用，几乎囊括了我们常用的各种系统和软件，官网列出了一份常用Exporter的清单 ，各个 Exporter 都遵循一份端口约定，避免端口冲突，即从 9100 开始依次递增，这里是完整的 Exporter端口列表 。另外值得注意的是，有些软件和系统无需安装 Exporter，这是因为他们本身就提供了暴露 Prometheus 格式的指标数据的功能，比如 Kubernetes、Grafana、Etcd、Ceph 等。\n1. 部署主机监控组件 各节点主机使用主机网络模式部署主机监控组件node-exporter，官方不建议将其部署为Docker容器，因为该node_exporter设计用于监控主机系统。需要访问主机系统，而且通过容器的方式部署发现磁盘数据不太准确。二进制部署就去看项目文档吧\n1 2 3 4 5 6 $ docker run -d \\ --net=\u0026#34;host\u0026#34; \\ --pid=\u0026#34;host\u0026#34; \\ -v \u0026#34;/:/host:ro,rslave\u0026#34; \\ quay.io/prometheus/node-exporter \\ --path.rootfs=/host 容器正常运行后，进入Prometheus容器，在Prometheus.yml 文件中添加node-exporter组件地址\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 $ docker exec -it prometheus-133-210 /bin/bash $ vim /data/prometheus/prometheus.yml global: scrape_interval: 60s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 60s # Evaluate rules every 15 seconds. The default is every 1 minute. rule_files: - \u0026#34;rules/service_down.yml\u0026#34; scrape_configs: - job_name: \u0026#39;Host\u0026#39; static_configs: - targets: [\u0026#39;10.1.250.36:9100\u0026#39;] #node-exporter地址 labels: appname: \u0026#39;DEV01_250.36\u0026#39; #添加的标签 - job_name: \u0026#39;prometheus\u0026#39; static_configs: - targets: [ \u0026#39;10.2.139.210:9090\u0026#39;] labels: appname: \u0026#39;prometheus\u0026#39; 热加载更新Prometheus\n1 $ curl -X POST http://10.1.133.210:9090/-/reload 查看Prometheus的web页面已经可以看到node-exporter，然后我们就可以定义报警规则和展示看板了，这部分内容在后面配置Alertmanager和Grafana上会详细介绍。\n2.部署容器监控组件 各节点主机部署容器监控组件cadvisor-exporter，我这边Docker网络使用的macvlan方式，所以直接给容器分配了IP地址。\n1 # docker run -d -h cadvisor139-216 --name=cadvisor139-216 --net=none -m 8g --cpus=4 --ip=10.1.139.216 --volume=/:/rootfs:ro --volume=/var/run:/var/run:rw --volume=/sys:/sys:ro --volume=/var/lib/docker/:/var/lib/docker:ro --volume=/dev/disk/:/dev/disk:ro google/cadvisor:latest 同样的，容器正常运行后，我们访问Cadvisor的Web页面 IP+8080 端口\n现在我们进入Prometheus容器，在prometheus.yml主机文件中添加cadvisor组件\n1 2 3 4 5 6 ----------- - job_name: \u0026#39;Cadvisor\u0026#39; static_configs: - targets: [ \u0026#39;10.1.139.216:8080\u0026#39;] labels: appname: \u0026#39;DEV_Cadvisor01\u0026#39; 热加载更新Prometheus\n1 $ curl -X POST http://10.1.133.210:9090/-/reload 可以看到，Prometheus添加的cadvisor状态为UP，说明正常接收数据。\n3. 部署Redis监控组件 容器部署Redis服务监控组件redis_exporter，--redis.passwd指定认证口令，如果你的redis访问没有密码那么就无需指定后面参数。\n1 $ docker run -d -h redis_exporter139-218 --name redis_exporter139-218 --network trust139 --ip=10.1.139.218 -m 8g --cpus=4 oliver006/redis_exporter --redis.passwd 123456 在prometheus.yml 添加redis-exporter\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 --------- - job_name: \u0026#39;Redis-exporter\u0026#39; #exporter地址 static_configs: - targets: [\u0026#39;10.2.139.218:9121\u0026#39;\u0026#39;] labels: appname: \u0026#39;redis-exporter\u0026#39; - job_name: \u0026#39;RedisProxy\u0026#39; #需要监控的redis地址 static_configs: - targets: - redis://10.2.139.70:6379 - redis://10.2.139.71:6379 labels: appname: RedisProxy metrics_path: /scrape relabel_configs: - source_labels: [__address__] target_label: __param_target - source_labels: [__param_target] target_label: instance - target_label: __address__ replacement: 10.2.139.218:9121 然后热加载更新，步骤同上。\n4.部署应用监控组件 中间件部署JVM监控组件jmx_exporter, 这种方式是适用于代码中没有暴露应用metrics信息的服务，无需进行代码改动，在应用启动时调用该jar包暴露jmx信息，然后在Prometheus分别指定应用的地址即可。\n首先下载jar ：https://github.com/prometheus/jmx_exporter（jmx_prometheus_javaagent-0.11.0.jar ） 下载配置文件，有tomcat和weblogic注意区分：https://github.com/prometheus/jmx_exporter/tree/master/example_configs 然后在中间件启动参数添加以下内容，指定配置文件和jar包的路径： 1 CATALINA_OPTS=\u0026#34;-javaagent:/app/tomcat-8.5.23/lib/jmx_prometheus_javaagent-0.11.0.jar=12345:/app/tomcat-8.5.23/conf/config.yaml\u0026#34; 上面我指定暴露metrics信息的端口为12345，所以我们在prometheus.yml文件中添加即可：\n1 2 3 4 5 6 7 8 9 --------- - job_name: \u0026#39;MIDL\u0026#39; static_configs: - targets: [\u0026#39;192.168.166.18:12345\u0026#39;,\u0026#39;192.168.166.19:12345\u0026#39;] labels: appname: \u0026#39;ORDER\u0026#39; - targets: [\u0026#39;10.2.139.111:12345\u0026#39;,\u0026#39;10.2.139.112:12345\u0026#39;] labels: appname: \u0026#39;WEB\u0026#39; 其他步骤同上，Prometheus热加载更新即可。\n5. 部署进程监控组件 因为我们容器是使用单独的网络部署的，相当于胖容器的方式，所以需要在监控的容器中部署process-exporter进程监控组件来监控容器的进程，\n软件包下载：\n1 wget https://github.com/ncabatoff/process-exporter/releases/download/v0.5.0/process-exporter-0.5.0.linux-amd64.tar.gz 配置文件：process-name.yaml\n1 2 3 4 5 6 7 process_names: - name: \u0026#34;{{.Matches}}\u0026#34; cmdline: - \u0026#39;redis-shake\u0026#39; #匹配进程，支持正则 启动参数：\n1 $ nohup ./process-exporter -config.path process-name.yaml \u0026amp; 在Prometheus.yml 添加该容器的IP地址，端口号为9256\n1 2 3 4 5 6 ----------- - job_name: \u0026#39;process\u0026#39; static_configs: - targets: [ \u0026#39;10.2.139.186:9256\u0026#39;] labels: appname: \u0026#39;Redis-shake\u0026#39; ok，现在我们热加载更新Prometheus的主机文件\n1 $ curl -X POSThttp://10.2.139.210:9090/-/reload 四、部署Alertmanager报警组件 1. Alertmanager 概述 Alertmanager处理客户端应用程序（如Prometheus服务器）发送的告警。它负责对它们进行重复数据删除，分组和路由，以及正确的接收器集成，例如电子邮件，PagerDuty或OpsGenie。它还负责警报的静默和抑制。\n以下描述了Alertmanager实现的核心概念。请参阅配置文档以了解如何更详细地使用它们。\n分组(Grouping) 分组将类似性质的告警分类为单个通知。这在大型中断期间尤其有用，因为许多系统一次失败，并且可能同时发射数百到数千个警报。 示例：发生网络分区时，群集中正在运行数十或数百个服务实例。一半的服务实例无法再访问数据库。Prometheus中的告警规则配置为在每个服务实例无法与数据库通信时发送告警。结果，数百个告警被发送到Alertmanager。 作为用户，只能想要获得单个页面，同时仍能够确切地看到哪些服务实例受到影响。因此，可以将Alertmanager配置为按群集和alertname对警报进行分组，以便发送单个紧凑通知。 这些通知的接收器通过配置文件中的路由树配置告警的分组，定时的进行分组通知。 抑制(Inhibition) 如果某些特定的告警已经触发，则某些告警需要被抑制。 示例：如果某个告警触发，通知无法访问整个集群。Alertmanager可以配置为在该特定告警触发时将与该集群有关的所有其他告警静音。这可以防止通知数百或数千个与实际问题无关的告警触发。 静默(SILENCES) 静默是在给定时间内简单地静音告警的方法。基于匹配器配置静默，就像路由树一样。检查告警是否匹配或者正则表达式匹配静默。如果匹配，则不会发送该告警的通知。在Alertmanager的Web界面中可以配置静默。 客户端行为(Client behavior) Alertmanager对其客户的行为有特殊要求。这些仅适用于不使用Prometheus发送警报的高级用例。#制作镜像方式和Prometheus类似，稍作更改即可，此步省略。 设置警报和通知的主要步骤如下：\n设置并配置Alertmanager； 配置Prometheus对Alertmanager访问； 在普罗米修斯创建警报规则； 2. 部署Alertmanager组件 首先需要创建Alertmanager的报警通知文件，我这里使用企业微信报警，其中企业微信需要申请账号认证，方式如下：\n访问网站注册企业微信账号（不需要企业认证）。 访问apps创建第三方应用，点击创建应用按钮 -\u0026gt; 填写应用信息： 创建报警组，获取组ID： 新建alertmanager.yml报警通知文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 global: resolve_timeout: 2m smtp_smarthost: smtp.163.com:25 smtp_from: 15xxx@163.com smtp_auth_username: 15xxxx@163.com smtp_auth_password: zxxx templates: - \u0026#39;/data/alertmanager/conf/template/wechat.tmpl\u0026#39; route: group_by: [\u0026#39;alertname_wechat\u0026#39;] group_wait: 1s group_interval: 1s receiver: \u0026#39;wechat\u0026#39; repeat_interval: 1h routes: - receiver: wechat match_re: serverity: wechat receivers: - name: \u0026#39;email\u0026#39; email_configs: - to: \u0026#39;8xxxxx@qq.com\u0026#39; send_resolved: true - name: \u0026#39;wechat\u0026#39; wechat_configs: - corp_id: \u0026#39;wwd402ce40b4720f24\u0026#39; to_party: \u0026#39;2\u0026#39; agent_id: \u0026#39;1000002\u0026#39; api_secret: \u0026#39;9nmYa4p12OkToCbh_oNc\u0026#39; send_resolved: true ## 发送已解决通知 参数说明：\ncorp_id: 企业微信账号唯一 ID， 可以在我的企业中查看。 to_party: 需要发送的组。 agent_id: 第三方企业应用的 ID，可以在自己创建的第三方企业应用详情页面查看。 api_secret: 第三方企业应用的密钥，可以在自己创建的第三方企业应用详情页面查看。 然后我们创建企业微信的消息模板，template/wechat.tmpl\n1 2 3 4 5 6 7 8 9 10 11 12 {{ define \u0026#34;wechat.default.message\u0026#34; }} {{ range $i, $alert :=.Alerts }} 【系统报警】 告警状态：{{ .Status }} 告警级别：{{ $alert.Labels.severity }} 告警应用：{{ $alert.Annotations.summary }} 告警详情：{{ $alert.Annotations.description }} 触发阀值：{{ $alert.Annotations.value }} 告警主机：{{ $alert.Labels.instance }} 告警时间：{{ $alert.StartsAt.Format \u0026#34;2006-01-02 15:04:05\u0026#34; }} {{ end }} {{ end }} 这个报警的模板其中的值是在Prometheus触发的报警信息中提取的，所以你可以根据自己的定义进行修改。\n运行Alertmanager容器\n1 $ docker run -d -p 9093:9093 --name alertmanager -m 8g --cpus=4 -v /opt/alertmanager.yml:/etc/alertmanager/alertmanager.yml -v /opt/template:/etc/alertmanager/template docker.io/prom/alertmanager:latest 容器运行完成后查看web页面 IP:9093\n3. 配置报警规则 Prometheus的报警规则通过PromQL语句编写\n进入Prometheus容器的rules目录，上面我们制作镜像的时候已经创建好并挂载到了容器中，现在我们编写其他的规则文件\n编写主机监控规则文件，rules/host_sys.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 cat host_sys.yml groups: - name: Host rules: - alert: HostMemory Usage expr: (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes + node_memory_Buffers_bytes + node_memory_Cached_bytes)) / node_memory_MemTotal_bytes * 100 \u0026gt; 90 for: 1m labels: name: Memory severity: Warning annotations: summary: \u0026#34; {{ $labels.appname }} \u0026#34; description: \u0026#34;宿主机内存使用率超过90%.\u0026#34; value: \u0026#34;{{ $value }}\u0026#34; - alert: HostCPU Usage expr: sum(avg without (cpu)(irate(node_cpu_seconds_total{mode!=\u0026#39;idle\u0026#39;}[5m]))) by (instance,appname) \u0026gt; 0.8 for: 1m labels: name: CPU severity: Warning annotations: summary: \u0026#34; {{ $labels.appname }} \u0026#34; description: \u0026#34;宿主机CPU使用率超过80%.\u0026#34; value: \u0026#34;{{ $value }}\u0026#34; - alert: HostLoad expr: node_load5 \u0026gt; 20 for: 1m labels: name: Load severity: Warning annotations: summary: \u0026#34;{{ $labels.appname }} \u0026#34; description: \u0026#34; 主机负载5分钟超过20.\u0026#34; value: \u0026#34;{{ $value }}\u0026#34; - alert: HostFilesystem Usage expr: (node_filesystem_size_bytes-node_filesystem_free_bytes)/node_filesystem_size_bytes*100\u0026gt;80 for: 1m labels: name: Disk severity: Warning annotations: summary: \u0026#34; {{ $labels.appname }} \u0026#34; description: \u0026#34; 宿主机 [ {{ $labels.mountpoint }} ]分区使用超过80%.\u0026#34; value: \u0026#34;{{ $value }}%\u0026#34; - alert: HostDiskio writes expr: irate(node_disk_writes_completed_total{job=~\u0026#34;Host\u0026#34;}[1m]) \u0026gt; 10 for: 1m labels: name: Diskio severity: Warning annotations: summary: \u0026#34; {{ $labels.appname }} \u0026#34; description: \u0026#34; 宿主机 [{{ $labels.device }}]磁盘1分钟平均写入IO负载较高.\u0026#34; value: \u0026#34;{{ $value }}iops\u0026#34; - alert: HostDiskio reads expr: irate(node_disk_reads_completed_total{job=~\u0026#34;Host\u0026#34;}[1m]) \u0026gt; 10 for: 1m labels: name: Diskio severity: Warning annotations: summary: \u0026#34; {{ $labels.appname }} \u0026#34; description: \u0026#34; 宿机 [{{ $labels.device }}]磁盘1分钟平均读取IO负载较高.\u0026#34; value: \u0026#34;{{ $value }}iops\u0026#34; - alert: HostNetwork_receive expr: irate(node_network_receive_bytes_total{device!~\u0026#34;lo|bond[0-9]|cbr[0-9]|veth.*|virbr.*|ovs-system\u0026#34;}[5m]) / 1048576 \u0026gt; 10 for: 1m labels: name: Network_receive severity: Warning annotations: summary: \u0026#34; {{ $labels.appname }} \u0026#34; description: \u0026#34; 宿主机 [{{ $labels.device }}] 网卡5分钟平均接收流量超过10Mbps.\u0026#34; value: \u0026#34;{{ $value }}3Mbps\u0026#34; - alert: hostNetwork_transmit expr: irate(node_network_transmit_bytes_total{device!~\u0026#34;lo|bond[0-9]|cbr[0-9]|veth.*|virbr.*|ovs-system\u0026#34;}[5m]) / 1048576 \u0026gt; 10 for: 1m labels: name: Network_transmit severity: Warning annotations: summary: \u0026#34; {{ $labels.appname }} \u0026#34; description: \u0026#34; 宿主机 [{{ $labels.device }}] 网卡5分钟内平均发送流量超过10Mbps.\u0026#34; value: \u0026#34;{{ $value }}3Mbps\u0026#34; 编写容器监控规则文件，rules/container_sys.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 groups: - name: Container rules: - alert: ContainerCPU expr: (sum by(name,instance) (rate(container_cpu_usage_seconds_total{image!=\u0026#34;\u0026#34;}[5m]))*100) \u0026gt; 200 for: 1m labels: name: CPU_Usage severity: Warning annotations: summary: \u0026#34;{{ $labels.name }} \u0026#34; description: \u0026#34; 容器CPU使用超200%.\u0026#34; value: \u0026#34;{{ $value }}%\u0026#34; - alert: Memory Usage expr: (container_memory_usage_bytes{name=~\u0026#34;.+\u0026#34;} - container_memory_cache{name=~\u0026#34;.+\u0026#34;}) / container_spec_memory_limit_bytes{name=~\u0026#34;.+\u0026#34;} * 100 \u0026gt; 200 for: 1m labels: name: Memory severity: Warning annotations: summary: \u0026#34;{{ $labels.name }} \u0026#34; description: \u0026#34; 容器内存使用超过200%.\u0026#34; value: \u0026#34;{{ $value }}%\u0026#34; - alert: Network_receive expr: irate(container_network_receive_bytes_total{name=~\u0026#34;.+\u0026#34;,interface=~\u0026#34;eth.+\u0026#34;}[5m]) / 1048576 \u0026gt; 10 for: 1m labels: name: Network_receive severity: Warning annotations: summary: \u0026#34;{{ $labels.name }} \u0026#34; description: \u0026#34;容器 [{{ $labels.device }}] 网卡5分钟平均接收流量超过10Mbps.\u0026#34; value: \u0026#34;{{ $value }}Mbps\u0026#34; - alert: Network_transmit expr: irate(container_network_transmit_bytes_total{name=~\u0026#34;.+\u0026#34;,interface=~\u0026#34;eth.+\u0026#34;}[5m]) / 1048576 \u0026gt; 10 for: 1m labels: name: Network_transmit severity: Warning annotations: summary: \u0026#34;{{ $labels.name }} \u0026#34; description: \u0026#34;容器 [{{ $labels.device }}] 网卡5分钟平均发送流量超过10Mbps.\u0026#34; value: \u0026#34;{{ $value }}Mbps\u0026#34; 编写redis监控规则文件，redis_check.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 groups: - name: redisdown rules: - alert: RedisDown expr: redis_up == 0 for: 1m labels: name: instance severity: Critical annotations: summary: \u0026#34; {{ $labels.alias }}\u0026#34; description: \u0026#34; 服务停止运行 \u0026#34; value: \u0026#34;{{ $value }}\u0026#34; - alert: Redis linked too many clients expr: redis_connected_clients / redis_config_maxclients * 100 \u0026gt; 80 for: 1m labels: name: instance severity: Warning annotations: summary: \u0026#34; {{ $labels.alias }}\u0026#34; description: \u0026#34; Redis连接数超过最大连接数的80%. \u0026#34; value: \u0026#34;{{ $value }}\u0026#34; - alert: Redis linked expr: redis_connected_clients / redis_config_maxclients * 100 \u0026gt; 80 for: 1m labels: name: instance severity: Warning annotations: summary: \u0026#34; {{ $labels.alias }}\u0026#34; description: \u0026#34; Redis连接数超过最大连接数的80%. \u0026#34; value: \u0026#34;{{ $value }}\u0026#34; 编写服务停止监控规则，rules/service_down.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 - alert: ProcessDown expr: namedprocess_namegroup_num_procs == 0 for: 1m labels: name: instance severity: Critical annotations: summary: \u0026#34; {{ $labels.appname }}\u0026#34; description: \u0026#34; 进程停止运行 \u0026#34; value: \u0026#34;{{ $value }}\u0026#34; - alert: Grafana down expr: absent(container_last_seen{name=~\u0026#34;grafana.+\u0026#34;} ) == 1 for: 1m labels: name: grafana severity: Critical annotations: summary: \u0026#34;Grafana\u0026#34; description: \u0026#34;Grafana容器停止运行\u0026#34; value: \u0026#34;{{ $value }}\u0026#34; 编写报警规则可以参考后面Grafana展示看板后的数据展示语句，需要注意的是，我们容器使用的是胖容器的方式，即当作虚拟机来使用，所以需要添加应用和服务停止的Exporter，如果你的容器守护进程直接就是应用的话，只需要监控容器的启停就可以了。\n测试微信报警\n五、Grafana展示组件 虽然 Prometheus 提供的 Web UI 也可以很好的查看不同指标的视图，但是这个功能非常简单，只适合用来调试。要实现一个强大的监控系统，还需要一个能定制展示不同指标的面板，能支持不同类型的展现方式（曲线图、饼状图、热点图、TopN 等），这就是仪表盘（Dashboard）功能。\nPrometheus 开发了一套仪表盘系统PromDash，不过很快这套系统就被废弃了，官方开始推荐使用 Grafana 来对 Prometheus 的指标数据进行可视化，这不仅是因为 Grafana 的功能非常强大，而且它和 Prometheus 可以完美的无缝融合。\nGrafana是一个用于可视化大型测量数据的开源系统，它的功能非常强大，界面也非常漂亮，使用它可以创建自定义的控制面板，你可以在面板中配置要显示的数据和显示方式，它支持很多不同的数据源，比如：Graphite、InfluxDB、OpenTSDB、Elasticsearch、Prometheus 等，而且它也支持众多的插件 。\n1. 部署Grafana服务容器 1 $ docker run -d -h grafana139-211 -m 8g --network trust139 --ip=10.2.139.211 --cpus=4 --name=grafana139-211 -e \u0026#34;GF_SERVER_ROOT_URL=http://10.2.139.211\u0026#34; -e \u0026#34;GF_SECURITY_ADMIN_PASSWORD=passwd\u0026#34; grafana/grafana 运行后访问IP:3000，user:admin pass:passwd\n2. 添加Prometheus数据源 3. 导入监控模板 使用编号导入模板，Grafana服务需要联网，否则需要到Grafana模板下载JSON文件导入。\n下面是我使用的几个模板，导入后可以根据自己的情况定义变量值\n主机监控展示看板Node-exporter导入 8919 模板 容器监控展示看板cadvisor-exporter导入193 模板 应用监控展示看板jmx-exporter导入8563 模板 Redis监控展示看板Redis-exporter导入2751 模板 进程监控展示看板Process-exporter导入249 模板 六、PromQL语句 七、使用Concul HTTP注册方式实现服务发现 一般是用服务发现需要应用需要服务注册，我们这边因为微服务改造还没完成，还有一些tomcat和weblogic中间件，而且选用的注册中心是Eurka，所以为了在代码不改动的情况下使用服务发现，选择了concul 作为注册中心，因为是consul是可以通过http方式注册的。\n1. consul 内部原理 Consul分为Client和Server两种节点（所有的节点也被称为Agent），Server节点保存数据，Client负责健康检查及转发数据请求到Server；Server节点有一个Leader和多个Follower，Leader节点会将数据同步到Follower，Server的数量推荐是3个或者5个，在Leader挂掉的时候会启动选举机制产生一个新的Leader。\n集群内的Consul节点通过gossip协议（流言协议）维护成员关系，也就是说某个节点了解集群内现在还有哪些节点，这些节点是Client还是Server。单个数据中心的流言协议同时使用TCP和UDP通信，并且都使用8301端口。跨数据中心的流言协议也同时使用TCP和UDP通信，端口使用8302。\n集群内数据的读写请求既可以直接发到Server，也可以通过Client使用RPC转发到Server，请求最终会到达Leader节点，在允许数据轻微陈旧的情况下，读请求也可以在普通的Server节点完成，集群内数据的读写和复制都是通过TCP的8300端口完成。\n具体consul的原理及架构请访问：http://blog.didispace.com/consul-service-discovery-exp/\n2. 使用docker部署consul 集群 1 2 3 4 5 6 7 8 9 10 11 #启动第1个Server节点，集群要求要有3个Server，将容器8500端口映射到主机8900端口，同时开启管理界面 docker run -d --name=consul1 -p 8900:8500 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --bootstrap-expect=3 --client=0.0.0.0 -ui #启动第2个Server节点，并加入集群 docker run -d --name=consul2 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --client=0.0.0.0 --join 172.17.0.1 #启动第3个Server节点，并加入集群 docker run -d --name=consul3 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=true --client=0.0.0.0 --join 172.17.0.2 #启动第4个Client节点，并加入集群 docker run -d --name=consul4 -e CONSUL_BIND_INTERFACE=eth0 consul agent --server=false --client=0.0.0.0 --join 172.17.0.2 浏览器访问容器映射的8900端口：\n3. 服务注册到Consul 使用HTTP API 方式注册node-exporter服务到Consul\n1 curl -X PUT -d \u0026#39;{\u0026#34;id\u0026#34;: \u0026#34;192.168.16.173\u0026#34;,\u0026#34;name\u0026#34;: \u0026#34;node-exporter\u0026#34;,\u0026#34;address\u0026#34;: \u0026#34;192.168.16.173\u0026#34;,\u0026#34;port\u0026#34;: \u0026#39;\u0026#39;9100,\u0026#34;tags\u0026#34;: [\u0026#34;DEV\u0026#34;], \u0026#34;checks\u0026#34;: [{\u0026#34;http\u0026#34;: \u0026#34;http://192.168.16.173:9100/\u0026#34;,\u0026#34;interval\u0026#34;: \u0026#34;5s\u0026#34;}]}\u0026#39; http://172.17.0.4:8500/v1/agent/service/register 解注册：\n1 curl --request PUT http://172.17.0.4:8500/v1/agent/service/deregister/192.168.166.14 注册多个服务到consul，使用脚本：\n1 2 3 4 5 6 7 8 #!/bin/bash all_IP=`cat /opt/ip` name=cadvisor port=9100 for I in $all_IP do curl -X PUT -d \u0026#39;{\u0026#34;id\u0026#34;: \u0026#34;\u0026#39;$I\u0026#39;\u0026#34;,\u0026#34;name\u0026#34;: \u0026#34;\u0026#39;$name\u0026#39;\u0026#34;,\u0026#34;address\u0026#34;: \u0026#34;\u0026#39;$I\u0026#39;\u0026#34;,\u0026#34;port\u0026#34;: \u0026#39;$port\u0026#39;,\u0026#34;tags\u0026#34;: [\u0026#34;cadvisor\u0026#34;], \u0026#34;checks\u0026#34;: [{\u0026#34;http\u0026#34;: \u0026#34;http://\u0026#39;$I\u0026#39;:\u0026#39;$port\u0026#39;/\u0026#34;,\u0026#34;interval\u0026#34;: \u0026#34;5s\u0026#34;}]}\u0026#39; http://172.17.0.4:8500/v1/agent/service/register done 4. Prometheus 配置consul 服务发现 consul 可以使用的元标签：\n1 2 3 4 5 6 7 8 9 10 11 __meta_consul_address：目标的地址 __meta_consul_dc：目标的数据中心名称 __meta_consul_tagged_address_\u0026lt;key\u0026gt;：每个节点标记目标的地址键值 __meta_consul_metadata_\u0026lt;key\u0026gt;：目标的每个节点元数据键值 __meta_consul_node：为目标定义的节点名称 __meta_consul_service_address：目标的服务地址 __meta_consul_service_id：目标的服务ID __meta_consul_service_metadata_\u0026lt;key\u0026gt;：目标的每个服务元数据键值 __meta_consul_service_port：目标的服务端口 __meta_consul_service：目标所属服务的名称 __meta_consul_tags：标记分隔符连接的目标的标记列表 修改Prometheus.yml 文件，使用relabel将consul的元标签重写便于查看\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 - job_name: \u0026#39;consul\u0026#39; consul_sd_configs: - server: \u0026#39;192.168.16.173:8900\u0026#39; services: [] #匹配所有service relabel_configs: - source_labels: [__meta_consul_service] #service 源标签 regex: \u0026#34;consul\u0026#34; #匹配为\u0026#34;consul\u0026#34; 的service action: drop # 执行的动作 - source_labels: [__meta_consul_service] # 将service 的label重写为appname target_label: appname - source_labels: [__meta_consul_service_address] target_label: instance - source_labels: [__meta_consul_tags] target_label: job Prometheus 热加载更新\n1 curl -X POST http://192.168.16.173:9090/-/reload 访问Prometheus web页面\n应用注册到consul\n在不需要开发修改代码的前提下，我们可以使用Prometheus的jmx-exporter收集应用的相关指标，在应用中间件tomcat/weblogic等调用jmx-exporter，具体方式查看https://www.jianshu.com/p/dfd6ba5206dc\n启动应用后会启动12345端口暴露jvm数据，现在我们要做的就是将这个端口注册到Consul上，然后Prometheus会从consul 拉取应用主机。\n使用脚本实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 $ cat ip TEST 192.168.166.10 192.168.166.11 UNMIN 192.168.166.12 192.168.166.13 --------------- $ cat consul.sh #!/bin/bash port=12345 while read app do echo ${app} app_tmp=(${app}) echo ${app_tmp[0]} length=${#app_tmp[@]} echo ${length} for((k=1;k\u0026lt;${length};k++)); do echo ${app_tmp[k]} curl -X PUT -d \u0026#39;{\u0026#34;id\u0026#34;: \u0026#34;\u0026#39;${app_tmp[k]}\u0026#39;\u0026#34;,\u0026#34;name\u0026#34;: \u0026#34;\u0026#39;${app_tmp[0]}\u0026#39;\u0026#34;,\u0026#34;address\u0026#34;: \u0026#34;\u0026#39;${app_tmp[k]}\u0026#39;\u0026#34;,\u0026#34;port\u0026#34;: \u0026#39;$port\u0026#39;,\u0026#34;tags\u0026#34;: [\u0026#34;MIDL\u0026#34;],\u0026#34;checks\u0026#34;: [{\u0026#34;http\u0026#34;: \u0026#34;http://\u0026#39;${app_tmp[k]}\u0026#39;:\u0026#39;$port\u0026#39;/\u0026#34;,\u0026#34;interval\u0026#34;: \u0026#34;5s\u0026#34;}]}\u0026#39; http://172.17.0.4:8500/v1/agent/service/register done done \u0026lt; ip 执行脚本注册到consul\n配置Grafana JVM 监控模板\nLoad 8563模板\n","permalink":"https://ktzxy.top/posts/xjlwo3vfyl/","summary":"Docker环境部署Prometheus+Grafana监控系统","title":"Docker环境部署Prometheus+Grafana监控系统 "},{"content":" 实战 Prometheus 搭建监控系统 Prometheus 是一款基于时序数据库的开源监控告警系统，说起 Prometheus 则不得不提 SoundCloud，这是一个在线音乐分享的平台，类似于做视频分享的 YouTube，由于他们在微服务架构的道路上越走越远，出现了成百上千的服务，使用传统的监控系统 StatsD 和 Graphite 存在大量的局限性。\n于是他们在 2012 年开始着手开发一套全新的监控系统。Prometheus 的原作者是 Matt T. Proud，他也是在 2012 年加入 SoundCloud 的，实际上，在加入 SoundCloud 之前，Matt 一直就职于 Google，他从 Google 的集群管理器 Borg 和它的监控系统 Borgmon 中获取灵感，开发了开源的监控系统 Prometheus，和 Google 的很多项目一样，使用的编程语言是 Go。\n很显然，Prometheus 作为一个微服务架构监控系统的解决方案，它和容器也脱不开关系。早在 2006 年 8 月 9 日，Eric Schmidt 在搜索引擎大会上首次提出了云计算（Cloud Computing）的概念，在之后的十几年里，云计算的发展势如破竹。\n在 2013 年，Pivotal 的 Matt Stine 又提出了云原生（Cloud Native）的概念，云原生由微服务架构、DevOps 和以容器为代表的敏捷基础架构组成，帮助企业快速、持续、可靠、规模化地交付软件。\n为了统一云计算接口和相关标准，2015 年 7 月，隶属于 Linux 基金会的 云原生计算基金会（CNCF，Cloud Native Computing Foundation） 应运而生。第一个加入 CNCF 的项目是 Google 的 Kubernetes，而 Prometheus 是第二个加入的（2016 年）。\n目前 Prometheus 已经广泛用于 Kubernetes 集群的监控系统中，对 Prometheus 的历史感兴趣的同学可以看看 SoundCloud 的工程师 Tobias Schmidt 在 2016 年的 PromCon 大会上的演讲：The History of Prometheus at SoundCloud 。\n1 Prometheus 概述 我们在 SoundCloud 的官方博客中可以找到一篇关于他们为什么需要新开发一个监控系统的文章 Prometheus: Monitoring at SoundCloud，在这篇文章中，他们介绍到，他们需要的监控系统必须满足下面四个特性：\nA multi-dimensional data model, so that data can be sliced and diced at will, along dimensions like instance, service, endpoint, and method. Operational simplicity, so that you can spin up a monitoring server where and when you want, even on your local workstation, without setting up a distributed storage backend or reconfiguring the world. Scalable data collection and decentralized architecture, so that you can reliably monitor the many instances of your services, and independent teams can set up independent monitoring servers. Finally, a powerful query language that leverages the data model for meaningful alerting (including easy silencing) and graphing (for dashboards and for ad-hoc exploration). 简单来说，就是下面四个特性：\n多维度数据模型 方便的部署和维护 灵活的数据采集 强大的查询语言 实际上，多维度数据模型和强大的查询语言这两个特性，正是时序数据库所要求的，所以 Prometheus 不仅仅是一个监控系统，同时也是一个时序数据库。那为什么 Prometheus 不直接使用现有的时序数据库作为后端存储呢？这是因为 SoundCloud 不仅希望他们的监控系统有着时序数据库的特点，而且还需要部署和维护非常方便。\n纵观比较流行的时序数据库（参见下面的附录），他们要么组件太多，要么外部依赖繁重，比如：Druid 有 Historical、MiddleManager、Broker、Coordinator、Overlord、Router 一堆的组件，而且还依赖于 ZooKeeper、Deep storage（HDFS 或 S3 等），Metadata store（PostgreSQL 或 MySQL），部署和维护起来成本非常高。而 Prometheus 采用去中心化架构，可以独立部署，不依赖于外部的分布式存储，你可以在几分钟的时间里就可以搭建出一套监控系统。\n此外，Prometheus 数据采集方式也非常灵活。要采集目标的监控数据，首先需要在目标处安装数据采集组件，这被称之为 Exporter，它会在目标处收集监控数据，并暴露出一个 HTTP 接口供 Prometheus 查询，Prometheus 通过 Pull 的方式来采集数据，这和传统的 Push 模式不同。\n不过 Prometheus 也提供了一种方式来支持 Push 模式，你可以将你的数据推送到 Push Gateway，Prometheus 通过 Pull 的方式从 Push Gateway 获取数据。目前的 Exporter 已经可以采集绝大多数的第三方数据，比如 Docker、HAProxy、StatsD、JMX 等等，官网有一份 Exporter 的列表。\n除了这四大特性，随着 Prometheus 的不断发展，开始支持越来越多的高级特性，比如：服务发现，更丰富的图表展示，使用外部存储，强大的告警规则和多样的通知方式。\n下图是 Prometheus 的整体架构图:\n从上图可以看出，Prometheus 生态系统包含了几个关键的组件：Prometheus server、Pushgateway、Alertmanager、Web UI 等，但是大多数组件都不是必需的，其中最核心的组件当然是 Prometheus server，它负责收集和存储指标数据，支持表达式查询，和告警的生成。接下来我们就来安装 Prometheus server。\n2 安装 Prometheus server Prometheus 可以支持多种安装方式，包括 Docker、Ansible、Chef、Puppet、Saltstack 等。下面介绍最简单的两种方式，一种是直接使用编译好的可执行文件，开箱即用，另一种是使用 Docker 镜像。\n2.1 开箱即用 首先从 官网的下载页面 获取 Prometheus 的最新版本和下载地址，目前最新版本是 2.4.3（2018年10月），执行下面的命令下载并解压：\n1 2 $ wget https://github.com/prometheus/prometheus/releases/download/v2.4.3/prometheus-2.4.3.linux-amd64.tar.gz $ tar xvfz prometheus-2.4.3.linux-amd64.tar.gz 然后切换到解压目录，检查 Prometheus 版本：\n1 2 3 4 5 6 $ cd prometheus-2.4.3.linux-amd64 $ ./prometheus --version prometheus, version 2.4.3 (branch: HEAD, revision: 167a4b4e73a8eca8df648d2d2043e21bdb9a7449) build user: root@1e42b46043e9 build date: 20181004-08:42:02 go version: go1.11.1 运行 Prometheus server：\n1 $ ./prometheus --config.file=prometheus.yml 2.2 使用 Docker 镜像 Docker 基础就不介绍了，参考我之前写的系列教程：博客地址：http://www.javastack.cn/\n基础教程：Docker 教程，详细到令人发指。\n使用 Docker 安装 Prometheus 更简单，运行下面的命令即可：\n1 $ sudo docker run -d -p 9090:9090 prom/prometheus 一般情况下，我们还会指定配置文件的位置：\n1 2 3 $ sudo docker run -d -p 9090:9090 \\ -v ~/docker/prometheus/:/etc/prometheus/ \\ prom/prometheus 我们把配置文件放在本地 ~/docker/prometheus/prometheus.yml，这样可以方便编辑和查看，通过 -v 参数将本地的配置文件挂载到 /etc/prometheus/ 位置，这是 prometheus 在容器中默认加载的配置文件位置。\n如果我们不确定默认的配置文件在哪，可以先执行上面的不带 -v 参数的命令，然后通过 docker inspect 命名看看容器在运行时默认的参数有哪些（下面的 Args 参数）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 $ sudo docker inspect 0c [...] \u0026#34;Id\u0026#34;: \u0026#34;0c4c2d0eed938395bcecf1e8bb4b6b87091fc4e6385ce5b404b6bb7419010f46\u0026#34;, \u0026#34;Created\u0026#34;: \u0026#34;2018-10-15T22:27:34.56050369Z\u0026#34;, \u0026#34;Path\u0026#34;: \u0026#34;/bin/prometheus\u0026#34;, \u0026#34;Args\u0026#34;: [ \u0026#34;--config.file=/etc/prometheus/prometheus.yml\u0026#34;, \u0026#34;--storage.tsdb.path=/prometheus\u0026#34;, \u0026#34;--web.console.libraries=/usr/share/prometheus/console_libraries\u0026#34;, \u0026#34;--web.console.templates=/usr/share/prometheus/consoles\u0026#34; ], [...] 2.3 配置 Prometheus 正如上面两节看到的，Prometheus 有一个配置文件，通过参数 --config.file 来指定，配置文件格式为 YAML。我们可以打开默认的配置文件 prometheus.yml 看下里面的内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /etc/prometheus $ cat prometheus.yml # my global config global: scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. # scrape_timeout is set to the global default (10s). # Alertmanager configuration alerting: alertmanagers: - static_configs: - targets: # - alertmanager:9093 # Load rules once and periodically evaluate them according to the global \u0026#39;evaluation_interval\u0026#39;. rule_files: # - \u0026#34;first_rules.yml\u0026#34; # - \u0026#34;second_rules.yml\u0026#34; # A scrape configuration containing exactly one endpoint to scrape: # Here it\u0026#39;s Prometheus itself. scrape_configs: # The job name is added as a label `job=\u0026lt;job_name\u0026gt;` to any timeseries scraped from this config. - job_name: \u0026#39;prometheus\u0026#39; # metrics_path defaults to \u0026#39;/metrics\u0026#39; # scheme defaults to \u0026#39;http\u0026#39;. static_configs: - targets: [\u0026#39;localhost:9090\u0026#39;] Prometheus 默认的配置文件分为四大块：\nglobal 块：Prometheus 的全局配置，比如 scrape_interval 表示 Prometheus 多久抓取一次数据，evaluation_interval 表示多久检测一次告警规则； alerting 块：关于 Alertmanager 的配置，这个我们后面再看； rule_files 块：告警规则，这个我们后面再看； scrape_config 块：这里定义了 Prometheus 要抓取的目标，我们可以看到默认已经配置了一个名称为 prometheus 的 job，这是因为 Prometheus 在启动的时候也会通过 HTTP 接口暴露自身的指标数据，这就相当于 Prometheus 自己监控自己，虽然这在真正使用 Prometheus 时没啥用处，但是我们可以通过这个例子来学习如何使用 Prometheus；可以访问 http://localhost:9090/metrics 查看 Prometheus 暴露了哪些指标； 3 学习 PromQL 通过上面的步骤安装好 Prometheus 之后，我们现在可以开始体验 Prometheus 了。Prometheus 提供了可视化的 Web UI 方便我们操作，直接访问 http://localhost:9090/ 即可，它默认会跳转到 Graph 页面：\n第一次访问这个页面可能会不知所措，我们可以先看看其他菜单下的内容，比如：Alerts 展示了定义的所有告警规则，Status 可以查看各种 Prometheus 的状态信息，有 Runtime \u0026amp; Build Information、Command-Line Flags、Configuration、Rules、Targets、Service Discovery 等等。\n实际上 Graph 页面才是 Prometheus 最强大的功能，在这里我们可以使用 Prometheus 提供的一种特殊表达式来查询监控数据，这个表达式被称为 PromQL（Prometheus Query Language）。通过 PromQL 不仅可以在 Graph 页面查询数据，而且还可以通过 Prometheus 提供的 HTTP API 来查询。查询的监控数据有列表和曲线图两种展现形式（对应上图中 Console 和 Graph 这两个标签）。\n我们上面说过，Prometheus 自身也暴露了很多的监控指标，也可以在 Graph 页面查询，展开 Execute 按钮旁边的下拉框，可以看到很多指标名称，我们随便选一个，譬如：promhttp_metric_handler_requests_total，这个指标表示 /metrics 页面的访问次数，Prometheus 就是通过这个页面来抓取自身的监控数据的。在 Console 标签中查询结果如下：\n上面在介绍 Prometheus 的配置文件时，可以看到 scrape_interval 参数是 15s，也就是说 Prometheus 每 15s 访问一次 /metrics 页面，所以我们过 15s 刷新下页面，可以看到指标值会自增。在 Graph 标签中可以看得更明显：\n3.1 数据模型 要学习 PromQL，首先我们需要了解下 Prometheus 的数据模型，一条 Prometheus 数据由一个指标名称（metric）和 N 个标签（label，N \u0026gt;= 0）组成的，比如下面这个例子：\n1 promhttp_metric_handler_requests_total{code=\u0026#34;200\u0026#34;,instance=\u0026#34;192.168.0.107:9090\u0026#34;,job=\u0026#34;prometheus\u0026#34;} 106 这条数据的指标名称为 promhttp_metric_handler_requests_total，并且包含三个标签 code、instance 和 job，这条记录的值为 106。上面说过，Prometheus 是一个时序数据库，相同指标相同标签的数据构成一条时间序列。如果以传统数据库的概念来理解时序数据库，可以把指标名当作表名，标签是字段，timestamp 是主键，还有一个 float64 类型的字段表示值（Prometheus 里面所有值都是按 float64 存储）。\n这种数据模型和 OpenTSDB 的数据模型是比较类似的，详细的信息可以参考官网文档 Data model。另外，关于指标和标签的命名，官网有一些指导性的建议，可以参考 Metric and label naming 。\n虽然 Prometheus 里存储的数据都是 float64 的一个数值，但如果我们按类型来分，可以把 Prometheus 的数据分成四大类：\nCounter Gauge Histogram Summary Counter 用于计数，例如：请求次数、任务完成数、错误发生次数，这个值会一直增加，不会减少。Gauge 就是一般的数值，可大可小，例如：温度变化、内存使用变化。Histogram 是直方图，或称为柱状图，常用于跟踪事件发生的规模，例如：请求耗时、响应大小。\n它特别之处是可以对记录的内容进行分组，提供 count 和 sum 的功能。Summary 和 Histogram 十分相似，也用于跟踪事件发生的规模，不同之处是，它提供了一个 quantiles 的功能，可以按百分比划分跟踪的结果。例如：quantile 取值 0.95，表示取采样值里面的 95% 数据。更多信息可以参考官网文档 Metric types，Summary 和 Histogram 的概念比较容易混淆，属于比较高阶的指标类型，可以参考 Histograms and summaries 这里的说明。\n这四种类型的数据只在指标的提供方作区分，也就是上面说的 Exporter，如果你需要编写自己的 Exporter 或者在现有系统中暴露供 Prometheus 抓取的指标，你可以使用 Prometheus client libraries，这个时候你就需要考虑不同指标的数据类型了。如果你不用自己实现，而是直接使用一些现成的 Exporter，然后在 Prometheus 里查查相关的指标数据，那么可以不用太关注这块，不过理解 Prometheus 的数据类型，对写出正确合理的 PromQL 也是有帮助的。\n3.2 PromQL 入门 我们从一些例子开始学习 PromQL，最简单的 PromQL 就是直接输入指标名称，比如：\n1 2 # 表示 Prometheus 能否抓取 target 的指标，用于 target 的健康检查 up 这条语句会查出 Prometheus 抓取的所有 target 当前运行情况，譬如下面这样：\n1 2 3 4 up{instance=\u0026#34;192.168.0.107:9090\u0026#34;,job=\u0026#34;prometheus\u0026#34;} 1 up{instance=\u0026#34;192.168.0.108:9090\u0026#34;,job=\u0026#34;prometheus\u0026#34;} 1 up{instance=\u0026#34;192.168.0.107:9100\u0026#34;,job=\u0026#34;server\u0026#34;} 1 up{instance=\u0026#34;192.168.0.108:9104\u0026#34;,job=\u0026#34;mysql\u0026#34;} 0 也可以指定某个 label 来查询：\n1 up{job=\u0026#34;prometheus\u0026#34;} 这种写法被称为 Instant vector selectors，这里不仅可以使用 = 号，还可以使用 !=、=~、!~，比如下面这样：\n1 2 3 up{job!=\u0026#34;prometheus\u0026#34;} up{job=~\u0026#34;server|mysql\u0026#34;} up{job=~\u0026#34;192\\.168\\.0\\.107.+\u0026#34;} =~ 是根据正则表达式来匹配，必须符合 RE2 的语法。\n和 Instant vector selectors 相应的，还有一种选择器，叫做 Range vector selectors，它可以查出一段时间内的所有数据：\n1 http_requests_total[5m] 这条语句查出 5 分钟内所有抓取的 HTTP 请求数，注意它返回的数据类型是 Range vector，没办法在 Graph 上显示成曲线图，一般情况下，会用在 Counter 类型的指标上，并和 rate() 或 irate() 函数一起使用（注意 rate 和 irate 的区别）。\n1 2 3 4 5 6 7 # 计算的是每秒的平均值，适用于变化很慢的 counter # per-second average rate of increase, for slow-moving counters rate(http_requests_total[5m]) # 计算的是每秒瞬时增加速率，适用于变化很快的 counter # per-second instant rate of increase, for volatile and fast-moving counters irate(http_requests_total[5m]) 此外，PromQL 还支持 count、sum、min、max、topk 等 聚合操作，还支持 rate、abs、ceil、floor 等一堆的 内置函数，更多的例子，还是上官网学习吧。如果感兴趣，我们还可以把 PromQL 和 SQL 做一个对比，会发现 PromQL 语法更简洁，查询性能也更高。\n3.3 HTTP API 我们不仅仅可以在 Prometheus 的 Graph 页面查询 PromQL，Prometheus 还提供了一种 HTTP API 的方式，可以更灵活的将 PromQL 整合到其他系统中使用，譬如下面要介绍的 Grafana，就是通过 Prometheus 的 HTTP API 来查询指标数据的。实际上，我们在 Prometheus 的 Graph 页面查询也是使用了 HTTP API。\n我们看下 Prometheus 的 HTTP API 官方文档，它提供了下面这些接口：\nGET /api/v1/query GET /api/v1/query_range GET /api/v1/series GET /api/v1/label/\u0026lt;label_name\u0026gt;/values GET /api/v1/targets GET /api/v1/rules GET /api/v1/alerts GET /api/v1/targets/metadata GET /api/v1/alertmanagers GET /api/v1/status/config GET /api/v1/status/flags 从 Prometheus v2.1 开始，又新增了几个用于管理 TSDB 的接口：\nPOST /api/v1/admin/tsdb/snapshot POST /api/v1/admin/tsdb/delete_series POST /api/v1/admin/tsdb/clean_tombstones 4 安装 Grafana 虽然 Prometheus 提供的 Web UI 也可以很好的查看不同指标的视图，但是这个功能非常简单，只适合用来调试。要实现一个强大的监控系统，还需要一个能定制展示不同指标的面板，能支持不同类型的展现方式（曲线图、饼状图、热点图、TopN 等），这就是仪表盘（Dashboard）功能。\n因此 Prometheus 开发了一套仪表盘系统 PromDash，不过很快这套系统就被废弃了，官方开始推荐使用 Grafana 来对 Prometheus 的指标数据进行可视化，这不仅是因为 Grafana 的功能非常强大，而且它和 Prometheus 可以完美的无缝融合。\nGrafana 是一个用于可视化大型测量数据的开源系统，它的功能非常强大，界面也非常漂亮，使用它可以创建自定义的控制面板，你可以在面板中配置要显示的数据和显示方式，它 支持很多不同的数据源，比如：Graphite、InfluxDB、OpenTSDB、Elasticsearch、Prometheus 等，而且它也 支持众多的插件。\n下面我们就体验下使用 Grafana 来展示 Prometheus 的指标数据。首先我们来安装 Grafana，我们使用最简单的 Docker 安装方式：\n1 $ docker run -d -p 3000:3000 grafana/grafana 运行上面的 docker 命令，Grafana 就安装好了！你也可以采用其他的安装方式，参考 官方的安装文档。安装完成之后，我们访问 http://localhost:3000/ 进入 Grafana 的登陆页面，输入默认的用户名和密码（admin/admin）即可。\n要使用 Grafana，第一步当然是要配置数据源，告诉 Grafana 从哪里取数据，我们点击 Add data source 进入数据源的配置页面：\n我们在这里依次填上：\nName: prometheus Type: Prometheus URL: http://localhost:9090 Access: Browser 要注意的是，这里的 Access 指的是 Grafana 访问数据源的方式，有 Browser 和 Proxy 两种方式。Browser 方式表示当用户访问 Grafana 面板时，浏览器直接通过 URL 访问数据源的；而 Proxy 方式表示浏览器先访问 Grafana 的某个代理接口（接口地址是 /api/datasources/proxy/），由 Grafana 的服务端来访问数据源的 URL，如果数据源是部署在内网，用户通过浏览器无法直接访问时，这种方式非常有用。\n配置好数据源，Grafana 会默认提供几个已经配置好的面板供你使用，如下图所示，默认提供了三个面板：Prometheus Stats、Prometheus 2.0 Stats 和 Grafana metrics。点击 Import 就可以导入并使用该面板。\n我们导入 Prometheus 2.0 Stats 这个面板，可以看到下面这样的监控面板。如果你的公司有条件，可以申请个大显示器挂在墙上，将这个面板投影在大屏上，实时观察线上系统的状态，可以说是非常 cool 的。\n5 使用 Exporter 收集指标 目前为止，我们看到的都还只是一些没有实际用途的指标，如果我们要在我们的生产环境真正使用 Prometheus，往往需要关注各种各样的指标，譬如服务器的 CPU负载、内存占用量、IO开销、入网和出网流量等等。\n正如上面所说，Prometheus 是使用 Pull 的方式来获取指标数据的，要让 Prometheus 从目标处获得数据，首先必须在目标上安装指标收集的程序，并暴露出 HTTP 接口供 Prometheus 查询，这个指标收集程序被称为 Exporter，不同的指标需要不同的 Exporter 来收集，目前已经有大量的 Exporter 可供使用，几乎囊括了我们常用的各种系统和软件。\n官网列出了一份 常用 Exporter 的清单，各个 Exporter 都遵循一份端口约定，避免端口冲突，即从 9100 开始依次递增，这里是 完整的 Exporter 端口列表。另外值得注意的是，有些软件和系统无需安装 Exporter，这是因为他们本身就提供了暴露 Prometheus 格式的指标数据的功能，比如 Kubernetes、Grafana、Etcd、Ceph 等。\n这一节就让我们来收集一些有用的数据。\n5.1 收集服务器指标 首先我们来收集服务器的指标，这需要安装 node_exporter，这个 exporter 用于收集 *NIX 内核的系统，如果你的服务器是 Windows，可以使用 WMI exporter。\n和 Prometheus server 一样，node_exporter 也是开箱即用的：\n1 2 3 4 $ wget https://github.com/prometheus/node_exporter/releases/download/v0.16.0/node_exporter-0.16.0.linux-amd64.tar.gz $ tar xvfz node_exporter-0.16.0.linux-amd64.tar.gz $ cd node_exporter-0.16.0.linux-amd64 $ ./node_exporter node_exporter 启动之后，我们访问下 /metrics 接口看看是否能正常获取服务器指标：\n1 $ curl http://localhost:9100/metrics 如果一切 OK，我们可以修改 Prometheus 的配置文件，将服务器加到 scrape_configs 中：\n1 2 3 4 5 6 7 scrape_configs: - job_name: \u0026#39;prometheus\u0026#39; static_configs: - targets: [\u0026#39;192.168.0.107:9090\u0026#39;] - job_name: \u0026#39;server\u0026#39; static_configs: - targets: [\u0026#39;192.168.0.107:9100\u0026#39;] 修改配置后，需要重启 Prometheus 服务，或者发送 HUP 信号也可以让 Prometheus 重新加载配置：\n1 $ killall -HUP prometheus 在 Prometheus Web UI 的 Status -\u0026gt; Targets 中，可以看到新加的服务器：\n在 Graph 页面的指标下拉框可以看到很多名称以 node 开头的指标，譬如我们输入 node_load1 观察服务器负载：\n如果想在 Grafana 中查看服务器的指标，可以在 Grafana 的 Dashboards 页面 搜索 node exporter，有很多的面板模板可以直接使用，譬如：Node Exporter Server Metrics 或者 Node Exporter Full 等。我们打开 Grafana 的 Import dashboard 页面，输入面板的 URL（https://grafana.com/dashboards/405）或者 ID（405）即可。\n注意事项 一般情况下，node_exporter 都是直接运行在要收集指标的服务器上的，官方不推荐用 Docker 来运行 node_exporter。如果逼不得已一定要运行在 Docker 里，要特别注意，这是因为 Docker 的文件系统和网络都有自己的 namespace，收集的数据并不是宿主机真实的指标。可以使用一些变通的方法，比如运行 Docker 时加上下面这样的参数：\n1 2 3 4 5 6 docker run -d \\ --net=\u0026#34;host\u0026#34; \\ --pid=\u0026#34;host\u0026#34; \\ -v \u0026#34;/:/host:ro,rslave\u0026#34; \\ quay.io/prometheus/node-exporter \\ --path.rootfs /host 关于 node_exporter 的更多信息，可以参考 node_exporter 的文档 和 Prometheus 的官方指南 Monitoring Linux host metrics with the Node Exporter，另外，Julius Volz 的这篇文章 How To Install Prometheus using Docker on Ubuntu 14.04 也是很好的入门材料。\n5.2 收集 MySQL 指标 mysqld_exporter 是 Prometheus 官方提供的一个 exporter，我们首先 下载最新版本 并解压（开箱即用）：\n1 2 3 $ wget https://github.com/prometheus/mysqld_exporter/releases/download/v0.11.0/mysqld_exporter-0.11.0.linux-amd64.tar.gz $ tar xvfz mysqld_exporter-0.11.0.linux-amd64.tar.gz $ cd mysqld_exporter-0.11.0.linux-amd64/ mysqld_exporter 需要连接到 mysqld 才能收集它的指标，可以通过两种方式来设置 mysqld 数据源。第一种是通过环境变量 DATA_SOURCE_NAME，这被称为 DSN（数据源名称），它必须符合 DSN 的格式，一个典型的 DSN 格式像这样：user:password@(host:port)/。\n1 2 $ export DATA_SOURCE_NAME=\u0026#39;root:123456@(192.168.0.107:3306)/\u0026#39; $ ./mysqld_exporter 另一种方式是通过配置文件，默认的配置文件是 ~/.my.cnf，或者通过 --config.my-cnf 参数指定：\n1 $ ./mysqld_exporter --config.my-cnf=\u0026#34;.my.cnf\u0026#34; 配置文件的格式如下：\n1 2 3 4 5 6 $ cat .my.cnf [client] host=localhost port=3306 user=root password=123456 如果要把 MySQL 的指标导入 Grafana，可以参考 这些 Dashboard JSON。\n注意事项 这里为简单起见，在 mysqld_exporter 中直接使用了 root 连接数据库，在真实环境中，可以为 mysqld_exporter 创建一个单独的用户，并赋予它受限的权限（PROCESS、REPLICATION CLIENT、SELECT），最好还限制它的最大连接数（MAX_USER_CONNECTIONS）。\n1 2 CREATE USER \u0026#39;exporter\u0026#39;@\u0026#39;localhost\u0026#39; IDENTIFIED BY \u0026#39;password\u0026#39; WITH MAX_USER_CONNECTIONS 3; GRANT PROCESS, REPLICATION CLIENT, SELECT ON *.* TO \u0026#39;exporter\u0026#39;@\u0026#39;localhost\u0026#39;; 5.3 收集 Nginx 指标 官方提供了两种收集 Nginx 指标的方式。\n第一种是 Nginx metric library，这是一段 Lua 脚本（prometheus.lua），Nginx 需要开启 Lua 支持（libnginx-mod-http-lua 模块）。为方便起见，也可以使用 OpenResty 的 OPM（OpenResty Package Manager） 或者 luarocks（The Lua package manager） 来安装。\n第二种是 Nginx VTS exporter，这种方式比第一种要强大的多，安装要更简单，支持的指标也更丰富，它依赖于 nginx-module-vts 模块，vts 模块可以提供大量的 Nginx 指标数据，可以通过 JSON、HTML 等形式查看这些指标。Nginx VTS exporter 就是通过抓取 /status/format/json 接口来将 vts 的数据格式转换为 Prometheus 的格式。不过，在 nginx-module-vts 最新的版本中增加了一个新接口：/status/format/prometheus，这个接口可以直接返回 Prometheus 的格式，从这点这也能看出 Prometheus 的影响力，估计 Nginx VTS exporter 很快就要退役了（TODO：待验证）。\n除此之外，还有很多其他的方式来收集 Nginx 的指标，比如：nginx_exporter 通过抓取 Nginx 自带的统计页面 /nginx_status 可以获取一些比较简单的指标（需要开启 ngx_http_stub_status_module 模块）；nginx_request_exporter 通过 syslog 协议 收集并分析 Nginx 的 access log 来统计 HTTP 请求相关的一些指标；nginx-prometheus-shiny-exporter 和 nginx_request_exporter 类似，也是使用 syslog 协议来收集 access log，不过它是使用 Crystal 语言 写的。还有 vovolie/lua-nginx-prometheus 基于 Openresty、Prometheus、Consul、Grafana 实现了针对域名和 Endpoint 级别的流量统计。\n有需要或感兴趣的同学可以对照说明文档自己安装体验下，这里就不一一尝试了。\n5.4 收集 JMX 指标 最后让我们来看下如何收集 Java 应用的指标，Java 应用的指标一般是通过 JMX（Java Management Extensions） 来获取的，顾名思义，JMX 是管理 Java 的一种扩展，它可以方便的管理和监控正在运行的 Java 程序。\nJMX Exporter 用于收集 JMX 指标，很多使用 Java 的系统，都可以使用它来收集指标，比如：Kafaka、Cassandra 等。首先我们下载 JMX Exporter：\n1 $ wget https://repo1.maven.org/maven2/io/prometheus/jmx/jmx_prometheus_javaagent/0.3.1/jmx_prometheus_javaagent-0.3.1.jar JMX Exporter 是一个 Java Agent 程序，在运行 Java 程序时通过 -javaagent 参数来加载：\n1 $ java -javaagent:jmx_prometheus_javaagent-0.3.1.jar=9404:config.yml -jar spring-boot-sample-1.0-SNAPSHOT.jar 其中，9404 是 JMX Exporter 暴露指标的端口，config.yml 是 JMX Exporter 的配置文件，它的内容可以 参考 JMX Exporter 的配置说明 。\nSpring Boot 教程和示例源码推荐：https://github.com/javastacks/spring-boot-best-practice\n然后检查下指标数据是否正确获取：\n1 $ curl http://localhost:9404/metrics 6 告警和通知 至此，我们能收集大量的指标数据，也能通过强大而美观的面板展示出来。不过作为一个监控系统，最重要的功能，还是应该能及时发现系统问题，并及时通知给系统负责人，这就是 Alerting（告警）。\nPrometheus 的告警功能被分成两部分：一个是告警规则的配置和检测，并将告警发送给 Alertmanager，另一个是 Alertmanager，它负责管理这些告警，去除重复数据，分组，并路由到对应的接收方式，发出报警。常见的接收方式有：Email、PagerDuty、HipChat、Slack、OpsGenie、WebHook 等。\n6.1 配置告警规则 我们在上面介绍 Prometheus 的配置文件时了解到，它的默认配置文件 prometheus.yml 有四大块：global、alerting、rule_files、scrape_config，其中 rule_files 块就是告警规则的配置项，alerting 块用于配置 Alertmanager，这个我们下一节再看。现在，先让我们在 rule_files 块中添加一个告警规则文件：\n1 2 rule_files: - \u0026#34;alert.rules\u0026#34; 然后参考 官方文档，创建一个告警规则文件 alert.rules：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 groups: - name: example rules: # Alert for any instance that is unreachable for \u0026gt;5 minutes. - alert: InstanceDown expr: up == 0 for: 5m labels: severity: page annotations: summary: \u0026#34;Instance {{ $labels.instance }} down\u0026#34; description: \u0026#34;{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 5 minutes.\u0026#34; # Alert for any instance that has a median request latency \u0026gt;1s. - alert: APIHighRequestLatency expr: api_http_request_latencies_second{quantile=\u0026#34;0.5\u0026#34;} \u0026gt; 1 for: 10m annotations: summary: \u0026#34;High request latency on {{ $labels.instance }}\u0026#34; description: \u0026#34;{{ $labels.instance }} has a median request latency above 1s (current value: {{ $value }}s)\u0026#34; 这个规则文件里，包含了两条告警规则：InstanceDown 和 APIHighRequestLatency。顾名思义，InstanceDown 表示当实例宕机时（up === 0）触发告警，APIHighRequestLatency 表示有一半的 API 请求延迟大于 1s 时（api_http_request_latencies_second{quantile=\u0026quot;0.5\u0026quot;} \u0026gt; 1）触发告警。\n配置好后，需要重启下 Prometheus server，然后访问 http://localhost:9090/rules 可以看到刚刚配置的规则：\n访问 http://localhost:9090/alerts 可以看到根据配置的规则生成的告警：\n这里我们将一个实例停掉，可以看到有一条 alert 的状态是 PENDING，这表示已经触发了告警规则，但还没有达到告警条件。这是因为这里配置的 for 参数是 5m，也就是 5 分钟后才会触发告警，我们等 5 分钟，可以看到这条 alert 的状态变成了 FIRING。\n6.2 使用 Alertmanager 发送告警通知 虽然 Prometheus 的 /alerts 页面可以看到所有的告警，但是还差最后一步：触发告警时自动发送通知。这是由 Alertmanager 来完成的，我们首先 下载并安装 Alertmanager，和其他 Prometheus 的组件一样，Alertmanager 也是开箱即用的：\n1 2 3 4 $ wget https://github.com/prometheus/alertmanager/releases/download/v0.15.2/alertmanager-0.15.2.linux-amd64.tar.gz $ tar xvfz alertmanager-0.15.2.linux-amd64.tar.gz $ cd alertmanager-0.15.2.linux-amd64 $ ./alertmanager Alertmanager 启动后默认可以通过 http://localhost:9093/ 来访问，但是现在还看不到告警，因为我们还没有把 Alertmanager 配置到 Prometheus 中，我们回到 Prometheus 的配置文件 prometheus.yml，添加下面几行：\n1 2 3 4 5 6 alerting: alertmanagers: - scheme: http static_configs: - targets: - \u0026#34;192.168.0.107:9093\u0026#34; 这个配置告诉 Prometheus，当发生告警时，将告警信息发送到 Alertmanager，Alertmanager 的地址为 http://192.168.0.107:9093。也可以使用命名行的方式指定 Alertmanager：\n1 $ ./prometheus -alertmanager.url=http://192.168.0.107:9093 这个时候再访问 Alertmanager，可以看到 Alertmanager 已经接收到告警了：\n下面的问题就是如何让 Alertmanager 将告警信息发送给我们了，我们打开默认的配置文件 alertmanager.ym：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 global: resolve_timeout: 5m route: group_by: [\u0026#39;alertname\u0026#39;] group_wait: 10s group_interval: 10s repeat_interval: 1h receiver: \u0026#39;web.hook\u0026#39; receivers: - name: \u0026#39;web.hook\u0026#39; webhook_configs: - url: \u0026#39;http://127.0.0.1:5001/\u0026#39; inhibit_rules: - source_match: severity: \u0026#39;critical\u0026#39; target_match: severity: \u0026#39;warning\u0026#39; equal: [\u0026#39;alertname\u0026#39;, \u0026#39;dev\u0026#39;, \u0026#39;instance\u0026#39;] 参考 官方的配置手册 了解各个配置项的功能，其中 global 块表示一些全局配置；route 块表示通知路由，可以根据不同的标签将告警通知发送给不同的 receiver，这里没有配置 routes 项，表示所有的告警都发送给下面定义的 web.hook 这个 receiver；如果要配置多个路由，可以参考 这个例子：\n1 2 3 4 5 6 7 8 9 10 routes: - receiver: \u0026#39;database-pager\u0026#39; group_wait: 10s match_re: service: mysql|cassandra - receiver: \u0026#39;frontend-pager\u0026#39; group_by: [product, environment] match: team: frontend 紧接着，receivers 块表示告警通知的接收方式，每个 receiver 包含一个 name 和一个 xxx_configs，不同的配置代表了不同的接收方式，Alertmanager 内置了下面这些接收方式：\nemail_config hipchat_config pagerduty_config pushover_config slack_config opsgenie_config victorops_config wechat_configs webhook_config 虽然接收方式很丰富，但是在国内，其中大多数接收方式都很少使用。最常用到的，莫属 email_config 和 webhook_config，另外 wechat_configs 可以支持使用微信来告警，也是相当符合国情的了。\n其实告警的通知方式是很难做到面面俱到的，因为消息软件各种各样，每个国家还可能不同，不可能完全覆盖到，所以 Alertmanager 已经决定不再添加新的 receiver 了，而是推荐使用 webhook 来集成自定义的接收方式。可以参考 这些集成的例子，譬如 将钉钉接入 Prometheus AlertManager WebHook。\n7 学习更多 到这里，我们已经学习了 Prometheus 的大多数功能，结合 Prometheus + Grafana + Alertmanager 完全可以搭建一套非常完整的监控系统。不过在真正使用时，我们会发现更多的问题。\n7.1 服务发现 由于 Prometheus 是通过 Pull 的方式主动获取监控数据，所以需要手工指定监控节点的列表，当监控的节点增多之后，每次增加节点都需要更改配置文件，非常麻烦，这个时候就需要通过服务发现（service discovery，SD）机制去解决。\nPrometheus 支持多种服务发现机制，可以自动获取要收集的 targets，可以参考 这里，包含的服务发现机制包括：azure、consul、dns、ec2、openstack、file、gce、kubernetes、marathon、triton、zookeeper（nerve、serverset），配置方法可以参考手册的 Configuration 页面。可以说 SD 机制是非常丰富的，但目前由于开发资源有限，已经不再开发新的 SD 机制，只对基于文件的 SD 机制进行维护。\n关于服务发现网上有很多教程，譬如 Prometheus 官方博客中这篇文章 Advanced Service Discovery in Prometheus 0.14.0 对此有一个比较系统的介绍，全面的讲解了 relabeling 配置，以及如何使用 DNS-SRV、Consul 和文件来做服务发现。\n另外，官网还提供了 一个基于文件的服务发现的入门例子，Julius Volz 写的 Prometheus workshop 入门教程中也 使用了 DNS-SRV 来当服务发现。\n7.2 告警配置管理 无论是 Prometheus 的配置还是 Alertmanager 的配置，都没有提供 API 供我们动态的修改。一个很常见的场景是，我们需要基于 Prometheus 做一套可自定义规则的告警系统，用户可根据自己的需要在页面上创建修改或删除告警规则，或者是修改告警通知方式和联系人，正如在 Prometheus Google Groups 里的 这个用户的问题：How to dynamically add alerts rules in rules.conf and prometheus yml file via API or something？\n不过遗憾的是，Simon Pasquier 在下面说到，目前并没有这样的 API，而且以后也没有这样的计划来开发这样的 API，因为这样的功能更应该交给譬如 Puppet、Chef、Ansible、Salt 这样的配置管理系统。\n7.3 使用 Pushgateway Pushgateway 主要用于收集一些短期的 jobs，由于这类 jobs 存在时间较短，可能在 Prometheus 来 Pull 之前就消失了。官方对 什么时候该使用 Pushgateway 有一个很好的说明。\n附录：什么是时序数据库？ 上文提到 Prometheus 是一款基于时序数据库的监控系统，时序数据库常简写为 TSDB（Time Series Database）。很多流行的监控系统都在使用时序数据库来保存数据，这是因为时序数据库的特点和监控系统不谋而合。\n增：需要频繁的进行写操作，而且是按时间排序顺序写入 删：不需要随机删除，一般情况下会直接删除一个时间区块的所有数据 改：不需要对写入的数据进行更新 查：需要支持高并发的读操作，读操作是按时间顺序升序或降序读，数据量非常大，缓存不起作用 DB-Engines 上有一个关于时序数据库的排名，下面是排名靠前的几个：\nInfluxDB：https://influxdata.com/ Kdb+：http://kx.com/ Graphite：http://graphiteapp.org/ RRDtool：http://oss.oetiker.ch/rrdtool/ OpenTSDB：http://opentsdb.net/ Prometheus：https://prometheus.io/ Druid：http://druid.io/ ","permalink":"https://ktzxy.top/posts/ldk10fsgsb/","summary":"实战 Prometheus 搭建监控系统","title":"实战 Prometheus 搭建监控系统 "},{"content":" MySQL 定时备份数据库（非常全） 在操作数据过程中，可能会导致数据错误，甚至数据库奔溃，而有效的定时备份能很好地保护数据库。本篇文章主要讲述了几种方法进行 MySQL 定时备份数据库。\n一、MySQL数据备份 1.1 mysqldump命令备份数据 在MySQL中提供了命令行导出数据库数据以及文件的一种方便的工具mysqldump ,我们可以通过命令行直接实现数据库内容的导出dump,首先我们简单了解一下mysqldump命令用法:\n1 2 #MySQLdump常用 mysqldump -u root -p --databases 数据库1 数据库2 \u0026gt; xxx.sql 1.2 mysqldump常用操作示例 备份全部数据库的数据和结构 1 mysqldump -uroot -p123456 -A \u0026gt; /data/mysqlDump/mydb.sql 2.备份全部数据库的结构（加 -d 参数）\n1 mysqldump -uroot -p123456 -A -d \u0026gt; /data/mysqlDump/mydb.sql 备份全部数据库的数据(加 -t 参数) 1 mysqldump -uroot -p123456 -A -t \u0026gt; /data/mysqlDump/mydb.sql 4.备份单个数据库的数据和结构(,数据库名mydb)\n1 mysqldump -uroot-p123456 mydb \u0026gt; /data/mysqlDump/mydb.sql 备份单个数据库的结构 1 mysqldump -uroot -p123456 mydb -d \u0026gt; /data/mysqlDump/mydb.sql 备份单个数据库的数据 1 mysqldump -uroot -p123456 mydb -t \u0026gt; /data/mysqlDump/mydb.sql 备份多个表的数据和结构（数据，结构的单独备份方法与上同） 1 mysqldump -uroot -p123456 mydb t1 t2 \u0026gt; /data/mysqlDump/mydb.sql 一次备份多个数据库 1 mysqldump -uroot -p123456 --databases db1 db2 \u0026gt; /data/mysqlDump/mydb.sql 1.3 还原 MySQL 备份内容 有两种方式还原，第一种是在 MySQL 命令行中，第二种是使用 SHELL 行完成还原\n在系统命令行中，输入如下实现还原： 1 mysql -uroot -p123456 \u0026lt; /data/mysqlDump/mydb.sql 在登录进入mysql系统中,通过source指令找到对应系统中的文件进行还原： 1 mysql\u0026gt; source /data/mysqlDump/mydb.sql 推荐下自己做的 Spring Boot 的实战项目：\nhttps://github.com/YunaiV/ruoyi-vue-pro\n二、 编写脚本维护备份的数据库文件 在 Linux中，通常使用BASH 脚本对需要执行的内容进行编写，加上定时执行命令crontab 实现日志自动化生成。\n以下代码功能就是针对mysql进行备份，配合crontab，实现备份的内容为近一个月（31天）内的每天的mysql数据库记录。\n2.1 编写BASH维护固定数量备份文件 在Linux中，使用vi或者vim编写脚本内容并命名为：mysql_dump_script.sh\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #!/bin/bash #保存备份个数，备份31天数据 number=31 #备份保存路径 backup_dir=/root/mysqlbackup #日期 dd=`date +%Y-%m-%d-%H-%M-%S` #备份工具 tool=mysqldump #用户名 username=root #密码 password=TankB214 #将要备份的数据库 database_name=edoctor #如果文件夹不存在则创建 if [ ! -d $backup_dir ]; then mkdir -p $backup_dir; fi #简单写法 mysqldump -u root -p123456 users \u0026gt; /root/mysqlbackup/users-$filename.sql $tool -u $username -p$password $database_name \u0026gt; $backup_dir/$database_name-$dd.sql #写创建备份日志 echo \u0026#34;create $backup_dir/$database_name-$dd.dupm\u0026#34; \u0026gt;\u0026gt; $backup_dir/log.txt #找出需要删除的备份 delfile=`ls -l -crt $backup_dir/*.sql | awk \u0026#39;{print $9 }\u0026#39; | head -1` #判断现在的备份数量是否大于$number count=`ls -l -crt $backup_dir/*.sql | awk \u0026#39;{print $9 }\u0026#39; | wc -l` if [ $count -gt $number ] then #删除最早生成的备份，只保留number数量的备份 rm $delfile #写删除文件日志 echo \u0026#34;delete $delfile\u0026#34; \u0026gt;\u0026gt; $backup_dir/log.txt fi 如上代码主要含义如下：\n1.首先设置各项参数，例如number最多需要备份的数目，备份路径，用户名，密码等。\n2.执行mysqldump命令保存备份文件，并将操作打印至同目录下的log.txt中标记操作日志。\n3.定义需要删除的文件：通过ls命令获取第九列，即文件名列，再通过实现定义操作时间最晚的那个需要删除的文件。\n4.定义备份数量：通过ls命令加上\n统计以sql结尾的文件的行数。\n5.如果文件超出限制大小，就删除最早创建的sql文件\n2.2 使用crontab定期执行备份脚本 在 Linux 中，周期执行的任务一般由cron这个守护进程来处理[ps -ef|grep cron]。cron读取一个或多个配置文件，这些配置文件中包含了命令行及其调用时间。cron的配置文件称为“crontab”，是“cron table”的简写。\ncron服务 cron是一个 Liunx 下 的定时执行工具，可以在无需人工干预的情况下运行作业。\nservice crond start //启动服务 service crond stop //关闭服务 service crond restart //重启服务 service crond reload //重新载入配置 service crond status //查看服务状态\ncrontab语法 crontab命令用于安装、删除或者列出用于驱动cron后台进程的表格。用户把需要执行的命令序列放到crontab文件中以获得执行。每个用户都可以有自己的crontab文件。/var/spool/cron下的crontab文件不可以直接创建或者直接修改。该crontab文件是通过crontab命令创建的。\n在crontab文件中如何输入需要执行的命令和时间。该文件中每行都包括六个域，其中前五个域是指定命令被执行的时间，最后一个域是要被执行的命令。每个域之间使用空格或者制表符分隔。\n格式如下：minute hour day-of-month month-of-year day-of-week commands 合法值 00-59 00-23 01-31 01-12 0-6 (0 is sunday)\n除了数字还有几个个特殊的符号就是\u0026quot;*\u0026quot;、\u0026quot;/\u0026ldquo;和\u0026rdquo;-\u0026quot;、\u0026quot;,\u0026quot;，*代表所有的取值范围内的数字，\u0026quot;/\u0026ldquo;代表每的意思,\u0026quot;/5\u0026quot;表示每5个单位，\u0026rdquo;-\u0026ldquo;代表从某个数字到某个数字,\u0026rdquo;,\u0026ldquo;分开几个离散的数字。\n-l 在标准输出上显示当前的crontab。-r 删除当前的crontab文件。-e 使用VISUAL或者EDITOR环境变量所指的编辑器编辑当前的crontab文件。当结束编辑离开时，编辑后的文件将自动安装。\n创建cron脚本 第一步：写cron脚本文件,命名为mysqlRollBack.cron。15,30,45,59 * * * * echo \u0026ldquo;xgmtest\u0026hellip;..\u0026rdquo; \u0026raquo; xgmtest.txt 表示，每隔15分钟，执行打印一次命令 第二步：添加定时任务。执行命令 “crontab crontest.cron”。搞定 第三步：\u0026ldquo;crontab -l\u0026rdquo; 查看定时任务是否成功或者检测/var/spool/cron下是否生成对应cron脚本\n注意：这操作是直接替换该用户下的crontab，而不是新增\n定期执行编写的定时任务脚本（记得先给shell脚本执行权限）\n1 0 2 * * * /root/mysql_backup_script.sh 随后使用crontab命令定期指令编写的定时脚本\n1 crontab mysqlRollback.cron 再通过命令检查定时任务是否已创建：\n附 crontab 的使用示例：\n每天早上6点 1 0 6 * * * echo \u0026#34;Good morning.\u0026#34; \u0026gt;\u0026gt; /tmp/test.txt //注意单纯echo，从屏幕上看不到任何输出，因为cron把任何输出都email到root的信箱了。 每两个小时 1 0 */2 * * * echo \u0026#34;Have a break now.\u0026#34; \u0026gt;\u0026gt; /tmp/test.txt 晚上11点到早上8点之间每两个小时和早上八点 1 0 23-7/2，8 * * * echo \u0026#34;Have a good dream\u0026#34; \u0026gt;\u0026gt; /tmp/test.txt 每个月的4号和每个礼拜的礼拜一到礼拜三的早上11点 1 0 11 4 * 1-3 command line 5.1 月 1 日早上 4 点\n1 0 4 1 1 * command line SHELL=/bin/bash PATH=/sbin:/bin:/usr/sbin:/usr/bin MAILTO=root //如果出现错误，或者有数据输出，数据作为邮件发给这个帐号 HOME=/ 每小时执行/etc/cron.hourly内的脚本 1 01 * * * * root run-parts /etc/cron.hourly 每天执行/etc/cron.daily内的脚本 1 02 4 * * * root run-parts /etc/cron.daily 每星期执行/etc/cron.weekly内的脚本 1 22 4 * * 0 root run-parts /etc/cron.weekly 每月去执行/etc/cron.monthly内的脚本 1 42 4 1 * * root run-parts /etc/cron.monthly 注意: \u0026ldquo;run-parts\u0026rdquo; 这个参数了，如果去掉这个参数的话，后面就可以写要运行的某个脚本名，而不是文件夹名。\n每天的下午4点、5点、6点的5 min、15 min、25 min、35 min、45 min、55 min时执行命令。 1 5，15，25，35，45，55 16，17，18 * * * command 每周一，三，五的下午3：00系统进入维护状态，重新启动系统。 1 00 15 * * 1，3，5 shutdown -r +5 每小时的10分，40分执行用户目录下的innd/bbslin这个指令： 1 10，40 * * * * innd/bbslink 每小时的1分执行用户目录下的bin/account这个指令： 以下是我的测试每分钟的截图效果，其对应代码如下：\n1 * * * * * /root/mysql_backup_script.sh ","permalink":"https://ktzxy.top/posts/239zs40dad/","summary":"MySQL 定时备份数据库（非常全）","title":"MySQL 定时备份数据库（非常全） "},{"content":"1、Spring简介 1.1、Spring概述 官网地址：https://spring.io/\nSpring 是最受欢迎的企业级 Java 应用程序开发框架，数以百万的来自世界各地的开发人员使用\nSpring 框架来创建性能好、易于测试、可重用的代码。\nSpring 框架是一个开源的 Java 平台，它最初是由 Rod Johnson 编写的，并且于 2003 年 6 月首\n次在 Apache 2.0 许可下发布。\nSpring 是轻量级的框架，其基础版本只有 2 MB 左右的大小。\nSpring 框架的核心特性是可以用于开发任何 Java 应用程序，但是在 Java EE 平台上构建 web 应\n用程序是需要扩展的。 Spring 框架的目标是使 J2EE 开发变得更容易使用，通过启用基于 POJO\n编程模型来促进良好的编程实践。\n1.2、Spring家族 项目列表：https://spring.io/projects\n1.3、Spring Framework Spring 基础框架，可以视为 Spring 基础设施，基本上任何其他 Spring 项目都是以 Spring Framework为基础的。\n1.3.1、Spring Framework特性 非侵入式：使用 Spring Framework 开发应用程序时，Spring 对应用程序本身的结构影响非常小。对领域模型可以做到零污染；对功能性组件也只需要使用几个简单的注解进行标记，完全不会破坏原有结构，反而能将组件结构进一步简化。这就使得基于 Spring Framework 开发应用程序时结构清晰、简洁优雅。\n控制反转：IOC——Inversion of Control，翻转资源获取方向。把自己创建资源、向环境索取资源变成环境将资源准备好，我们享受资源注入。\n面向切面编程：AOP——Aspect Oriented Programming，在不修改源代码的基础上增强代码功能。\n容器：Spring IOC 是一个容器，因为它包含并且管理组件对象的生命周期。组件享受到了容器化的管理，替程序员屏蔽了组件创建过程中的大量细节，极大的降低了使用门槛，大幅度提高了开发效率。\n组件化：Spring 实现了使用简单的组件配置组合成一个复杂的应用。在 Spring 中可以使用 XML和 Java 注解组合这些对象。这使得我们可以基于一个个功能明确、边界清晰的组件有条不紊的搭建超大型复杂应用系统。\n声明式：很多以前需要编写代码才能实现的功能，现在只需要声明需求即可由框架代为实现。\n一站式：在 IOC 和 AOP 的基础上可以整合各种企业应用的开源框架和优秀的第三方类库。而且Spring 旗下的项目已经覆盖了广泛领域，很多方面的功能性需求可以在 Spring Framework 的基础上全部使用 Spring 来实现。\n1.3.2、Spring Framework五大功能模块 功能模块 功能介绍 Core Container 核心容器，在 Spring 环境下使用任何功能都必须基于 IOC 容器。 AOP\u0026amp;Aspects 面向切面编程 Testing 提供了对 junit 或 TestNG 测试框架的整合。 Data Access/Integration 提供了对数据访问/集成的功能。 Spring MVC 提供了面向Web应用程序的集成功能。 2、IOC 2.1、IOC容器 2.1.1、IOC思想 IOC：Inversion of Control，翻译过来是反转控制。\n①获取资源的传统方式 自己做饭：买菜、洗菜、择菜、改刀、炒菜，全过程参与，费时费力，必须清楚了解资源创建整个过程中的全部细节且熟练掌握。\n在应用程序中的组件需要获取资源时，传统的方式是组件主动的从容器中获取所需要的资源，在这样的模式下开发人员往往需要知道在具体容器中特定资源的获取方式，增加了学习成本，同时降低了开发效率。\n②反转控制方式获取资源 点外卖：下单、等、吃，省时省力，不必关心资源创建过程的所有细节。\n反转控制的思想完全颠覆了应用程序组件获取资源的传统方式：反转了资源的获取方向——改由容器主动的将资源推送给需要的组件，开发人员不需要知道容器是如何创建资源对象的，只需要提供接收资源的方式即可，极大的降低了学习成本，提高了开发的效率。这种行为也称为查找的被动形式。\n③DI DI：Dependency Injection，翻译过来是依赖注入。\nDI 是 IOC 的另一种表述方式：即组件以一些预先定义好的方式（例如：setter 方法）接受来自于容器的资源注入。相对于IOC而言，这种表述更直接。\n所以结论是：IOC 就是一种反转控制的思想， 而 DI 是对 IOC 的一种具体实现。\n2.1.2、IOC容器在Spring中的实现 Spring 的 IOC 容器就是 IOC 思想的一个落地的产品实现。IOC 容器中管理的组件也叫做 bean。在创建bean 之前，首先需要创建 IOC 容器。Spring 提供了 IOC 容器的两种实现方式：\n①BeanFactory 这是 IOC 容器的基本实现，是 Spring 内部使用的接口。面向 Spring 本身，不提供给开发人员使用。\n②ApplicationContext BeanFactory 的子接口，提供了更多高级特性。面向 Spring 的使用者，几乎所有场合都使用ApplicationContext 而不是底层的BeanFactory。\n③ApplicationContext的主要实现类 类型名 简介 ClassPathXmlApplicationContext 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象 FileSystemXmlApplicationContext 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象 ConfigurableApplicationContext ApplicationContext 的子接口，包含一些扩展方法refresh() 和 close() ，让 ApplicationContext 具有启动、关闭和刷新上下文的能力。 WebApplicationContext 专门为 Web 应用准备，基于 Web 环境创建 IOC 容器对象，并将对象引入存入 ServletContext 域中。 2.2、基于XML管理bean 2.2.1、实验一：入门案例 ①创建Maven Module ②引入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;dependencies\u0026gt; \u0026lt;!-- 基于Maven依赖传递性，导入spring-context依赖即可导入当前所需所有jar包 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-context\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- junit测试 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.12\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; ③创建类HelloWorld 1 2 3 4 5 public class HelloWorld { public void sayHello(){ System.out.println(\u0026#34;helloworld\u0026#34;); } } ⑤在Spring的配置文件中配置bean 1 2 3 4 5 6 7 8 \u0026lt;!-- 配置HelloWorld所对应的bean，即将HelloWorld的对象交给Spring的IOC容器管理 通过bean标签配置IOC容器所管理的bean 属性： id：设置bean的唯一标识 class：设置bean所对应类型的全类名 --\u0026gt; \u0026lt;bean id=\u0026#34;helloworld\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.HelloWorld\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; ⑥创建测试类测试 1 2 3 4 5 6 7 8 @Test public void testHelloWorld(){ //获取IOC容器 ApplicationContext ioc = newClassPathXmlApplicationContext(\u0026#34;applicationContext.xml\u0026#34;); //获取IOC容器中的Bean HelloWorld helloworld = (HelloWorld) ioc.getBean(\u0026#34;helloworld\u0026#34;); helloworld.sayHello(); } ⑦思路 ⑧注意 Spring 底层默认通过反射技术调用组件类的无参构造器来创建组件对象，这一点需要注意。如果在需要无参构造器时，没有无参构造器，则会抛出下面的异常：\norg.springframework.beans.factory.BeanCreationException: Error creating bean with name \u0026lsquo;Student\u0026rsquo; defined in class path resource [applicationContext.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.hbnu.spring.pojo.Student]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.hbnu.spring.pojo.Student.\n()\n2.2.2、实验二：获取bean ①方式一：根据id获取 由于 id 属性指定了 bean 的唯一标识，所以根据 bean 标签的 id 属性可以精确获取到一个组件对象。\n上个实验中我们使用的就是这种方式。\n②方式二：根据类型获取 1 2 3 4 5 6 @Test public void testXml() { ApplicationContext ioc = new ClassPathXmlApplicationContext(\u0026#34;spring-ioc.xml\u0026#34;); Student student = ioc.getBean(Student.class); System.out.println(student); } ③方式三：根据id和类型 1 2 3 4 5 6 @Test public void testXml() { ApplicationContext ioc = new ClassPathXmlApplicationContext(\u0026#34;spring-ioc.xml\u0026#34;); Student student = ioc.getBean(\u0026#34;studentOne\u0026#34;, Student.class); System.out.println(student); } ④注意 当根据类型获取bean时，要求IOC容器中指定类型的bean有且只能有一个\n当IOC容器中一共配置了两个：\n1 2 \u0026lt;bean id=\u0026#34;studentOne\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Student\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;studentTwo\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Student\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; 若没有任何一个类型匹配的bean，此时抛出异常：NoUniqueBeanDefinitionException\n若有多个类型匹配的bean，此时抛出异常：NoSuchBeanDefinitionException\n⑤扩展 如果组件类实现了接口，根据接口类型可以获取 bean 吗？\n可以，前提是bean唯一\n如果一个接口有多个实现类，这些实现类都配置了 bean，根据接口类型可以获取 bean 吗？\n不行，因为bean不唯一\n⑥结论 根据类型来获取bean时，在满足bean唯一性的前提下，其实只是看：『对象 instanceof 指定的类型』的返回结果，只要返回的是true就可以认定为和类型匹配，能够获取到。\n2.2.3、实验三：依赖注入之setter注入 ①创建学生类Student 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 package com.hbnu.spring.pojo; /** * @Auther: 赵羽 * @Date: 2023/4/12 - 04 - 12 - 22:08 * @Description: com.hbnu.spring.pojo * @version: 1.0 */ public class Student { private Integer sid; private String sname; private Integer age; private String gender; public Student() { } public Student(Integer sid, String sname, Integer age, String gender) { this.sid = sid; this.sname = sname; this.age = age; this.gender = gender; } public Integer getSid() { return sid; } public void setSid(Integer sid) { this.sid = sid; } public String getSname() { return sname; } public void setSname(String sname) { this.sname = sname; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } @Override public String toString() { return \u0026#34;Student{\u0026#34; + \u0026#34;sid=\u0026#34; + sid + \u0026#34;, sname=\u0026#39;\u0026#34; + sname + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#34;, gender=\u0026#39;\u0026#34; + gender + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } ②配置bean时为属性赋值 1 2 3 4 5 6 7 8 9 \u0026lt;!-- property标签：通过组件类的setXxx()方法给组件对象设置属性 --\u0026gt; \u0026lt;!-- name属性：指定属性名（这个属性名是getXxx()、setXxx()方法定义的，和成员变量无关）--\u0026gt; \u0026lt;!-- value属性：指定属性值 --\u0026gt; \u0026lt;bean id=\u0026#34;studentTwo\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Student\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;sid\u0026#34; value=\u0026#34;1001\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;sname\u0026#34; value=\u0026#34;张三\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;age\u0026#34; value=\u0026#34;20\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;gender\u0026#34; value=\u0026#34;男\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; ③测试 1 2 3 4 5 6 @Test public void testDIBySet(){ ApplicationContext ac = new ClassPathXmlApplicationContext(\u0026#34;spring-ioc.xml\u0026#34;); Student studentTwo = ac.getBean(\u0026#34;studentTwo\u0026#34;, Student.class); System.out.println(studentTwo); } 2.2.4、实验四：依赖注入之构造器注入 ①在Student类中添加有参构造 1 2 3 4 5 6 public Student(Integer sid, String sname, Integer age, String gender) { this.sid = sid; this.sname = sname; this.age = age; this.gender = gender; } ②配置bean 1 2 3 4 5 6 \u0026lt;bean id=\u0026#34;studentThree\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Student\u0026#34;\u0026gt; \u0026lt;constructor-arg value=\u0026#34;1002\u0026#34;\u0026gt;\u0026lt;/constructor-arg\u0026gt; \u0026lt;constructor-arg value=\u0026#34;李四\u0026#34;\u0026gt;\u0026lt;/constructor-arg\u0026gt; \u0026lt;constructor-arg value=\u0026#34;30\u0026#34;\u0026gt;\u0026lt;/constructor-arg\u0026gt; \u0026lt;constructor-arg value=\u0026#34;男\u0026#34;\u0026gt;\u0026lt;/constructor-arg\u0026gt; \u0026lt;/bean\u0026gt; 注意：\nconstructor-arg标签还有两个属性可以进一步描述构造器参数：\nindex属性：指定参数所在位置的索引（从0开始） name属性：指定参数名 ③测试 1 2 3 4 5 6 @Test public void testDI() { ApplicationContext ioc = new ClassPathXmlApplicationContext(\u0026#34;spring-ioc.xml\u0026#34;); Student student = ioc.getBean(\u0026#34;studentThree\u0026#34;, Student.class); System.out.println(student); } 2.2.5、实验五：特殊值处理 ①字面量赋值 什么是字面量？\nint a = 10;\n声明一个变量a，初始化为10，此时a就不代表字母a了，而是作为一个变量的名字。当我们引用a\n的时候，我们实际上拿到的值是10。\n而如果a是带引号的：\u0026lsquo;a\u0026rsquo;，那么它现在不是一个变量，它就是代表a这个字母本身，这就是字面\n量。所以字面量没有引申含义，就是我们看到的这个数据本身。\n1 2 \u0026lt;!-- 使用value属性给bean的属性赋值时，Spring会把value属性的值看做字面量 --\u0026gt; \u0026lt;property name=\u0026#34;name\u0026#34; value=\u0026#34;张三\u0026#34;/\u0026gt; ②null值 1 2 3 \u0026lt;property name=\u0026#34;name\u0026#34;\u0026gt; \u0026lt;null /\u0026gt; \u0026lt;/property\u0026gt; 注意：\n1 \u0026lt;property name=\u0026#34;name\u0026#34; value=\u0026#34;null\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; 以上写法，为name所赋的值是字符串null\n③xml实体 1 2 3 4 \u0026lt;!-- 小于号在XML文档中用来定义标签的开始，不能随便使用 --\u0026gt; \u0026lt;!-- 解决方案一：使用XML实体来代替 --\u0026gt; \u0026lt;property name=\u0026#34;studentFour\u0026#34; value=\u0026#34;\u0026amp;lt;王五\u0026amp;gt;\u0026#34;/\u0026gt; \u0026lt;!--\u0026lt;王五\u0026gt;--\u0026gt; ④CDATA节 1 2 3 4 5 6 7 \u0026lt;property name=\u0026#34;studentFour\u0026#34;\u0026gt; \u0026lt;!-- 解决方案二：使用CDATA节 --\u0026gt; \u0026lt;!-- CDATA中的C代表Character，是文本、字符的含义，CDATA就表示纯文本数据 --\u0026gt; \u0026lt;!-- XML解析器看到CDATA节就知道这里是纯文本，就不会当作XML标签或属性来解析 --\u0026gt; \u0026lt;!-- 所以CDATA节中的内容会原样解析\u0026lt;![CDATA[...]]\u0026gt; CDATA节是xml中一个特殊的标签，因此不能写在一个属性中--\u0026gt; \u0026lt;value\u0026gt;\u0026lt;![CDATA[a \u0026lt; b]]\u0026gt;\u0026lt;/value\u0026gt; \u0026lt;/property\u0026gt; 2.2.6、实验六：为类类型属性赋值 ①创建班级类Class 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.hbnu.spring.pojo; /** * @Auther: 赵羽 * @Date: 2023/4/13 - 04 - 13 - 19:55 * @Description: com.hbnu.spring.pojo * @version: 1.0 */ public class Class { private Integer cid; private String cname; public Class() { } public Class(Integer cid, String cname) { this.cid = cid; this.cname = cname; } public Integer getCid() { return cid; } public void setCid(Integer cid) { this.cid = cid; } public String getCname() { return cname; } public void setCname(String cname) { this.cname = cname; } @Override public String toString() { return \u0026#34;Class{\u0026#34; + \u0026#34;cid=\u0026#34; + cid + \u0026#34;, cname=\u0026#39;\u0026#34; + cname + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } ②修改Student类 在Student类中添加以下代码：\n1 2 3 4 5 6 7 8 private Class aClass; public Class getaClass() { return aClass; } public void setaClass(Class aClass) { this.aClass = aClass; } ③方式一：引用外部已声明的bean 配置Clazz类型的bean：\n1 2 3 4 \u0026lt;bean id=\u0026#34;class\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Class\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;cid\u0026#34; value=\u0026#34;1111\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;cname\u0026#34; value=\u0026#34;大数据2010班\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 为Student中的clazz属性赋值：\n1 2 3 4 5 6 7 8 \u0026lt;bean id=\u0026#34;studentsix\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Student\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;sid\u0026#34; value=\u0026#34;1005\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;sname\u0026#34; value=\u0026#34;张三萨\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;age\u0026#34; value=\u0026#34;20\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;gender\u0026#34; value=\u0026#34;男\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;!-- ref属性：引用IOC容器中某个bean的id，将所对应的bean为属性赋值 --\u0026gt; \u0026lt;property name=\u0026#34;aClass\u0026#34; ref=\u0026#34;class\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 错误演示：\n1 2 3 4 5 6 7 \u0026lt;bean id=\u0026#34;studentsix\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Student\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;sid\u0026#34; value=\u0026#34;1005\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;sname\u0026#34; value=\u0026#34;张三萨\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;age\u0026#34; value=\u0026#34;20\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;gender\u0026#34; value=\u0026#34;男\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;aClass\u0026#34; value=\u0026#34;class\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 如果错把ref属性写成了value属性，会抛出异常：Caused by: java.lang.IllegalStateException: Cannot convert value of type \u0026lsquo;java.lang.String\u0026rsquo; to required type \u0026lsquo;com.hbnu.spring.pojo.Class\u0026rsquo; for property \u0026lsquo;aClass\u0026rsquo;: no matching editors or conversion strategy found\n意思是不能把String类型转换成我们要的Class类型，说明我们使用value属性时，Spring只把这个\n属性看做一个普通的字符串，不会认为这是一个bean的id，更不会根据它去找到bean来赋值\n④方式二：内部bean 1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;bean id=\u0026#34;studentseven\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Student\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;sid\u0026#34; value=\u0026#34;1005\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;sname\u0026#34; value=\u0026#34;张三萨\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;age\u0026#34; value=\u0026#34;20\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;gender\u0026#34; value=\u0026#34;男\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;aClass\u0026#34;\u0026gt; \u0026lt;bean id=\u0026#34;class\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Class\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;cid\u0026#34; value=\u0026#34;1111\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;cname\u0026#34; value=\u0026#34;大数据2010班\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; ③方式三：级联属性赋值 1 2 3 4 5 6 7 8 9 10 \u0026lt;bean id=\u0026#34;studenteight\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Student\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;sid\u0026#34; value=\u0026#34;1005\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;sname\u0026#34; value=\u0026#34;张三萨\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;age\u0026#34; value=\u0026#34;20\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;gender\u0026#34; value=\u0026#34;男\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;!-- ref属性：引用IOC容器中某个bean的id，将所对应的bean为属性赋值 级联的方式，要保证提前为class属性赋值或者实例化--\u0026gt; \u0026lt;property name=\u0026#34;aClass\u0026#34; ref=\u0026#34;class\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;aClass.cid\u0026#34; value=\u0026#34;1111\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;aClass.cname\u0026#34; value=\u0026#34;大数据2010班\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 2.2.7、实验七：为数组类型属性赋值 ①修改Student类 在Student类中添加以下代码：\n1 2 3 4 5 6 7 private String[] hobbies; public String[] getHobbies() { return hobbies; } public void setHobbies(String[] hobbies) { this.hobbies = hobbies; } ②配置bean 1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;bean id=\u0026#34;studenteight\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Student\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;sid\u0026#34; value=\u0026#34;1005\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;sname\u0026#34; value=\u0026#34;张三萨\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;age\u0026#34; value=\u0026#34;20\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;gender\u0026#34; value=\u0026#34;男\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;hobbies\u0026#34;\u0026gt; \u0026lt;array\u0026gt; \u0026lt;value\u0026gt;唱歌\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;跳舞\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;阅读\u0026lt;/value\u0026gt; \u0026lt;/array\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 2.2.8、实验八：为集合类型属性赋值 ①为List集合类型属性赋值 在Clazz类中添加以下代码：\n1 2 3 4 5 6 7 private List\u0026lt;Student\u0026gt; students; public List\u0026lt;Student\u0026gt; getStudents() { return students; } public void setStudents(List\u0026lt;Student\u0026gt; students) { this.students = students; } 配置bean：\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;bean id=\u0026#34;classTwo\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Class\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;cid\u0026#34; value=\u0026#34;1111\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;cname\u0026#34; value=\u0026#34;大数据2010班\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;students\u0026#34;\u0026gt; \u0026lt;list\u0026gt; \u0026lt;ref bean=\u0026#34;studentThree\u0026#34;\u0026gt;\u0026lt;/ref\u0026gt; \u0026lt;ref bean=\u0026#34;studentFour\u0026#34;\u0026gt;\u0026lt;/ref\u0026gt; \u0026lt;ref bean=\u0026#34;studentFive\u0026#34;\u0026gt;\u0026lt;/ref\u0026gt; \u0026lt;/list\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 若为Set集合类型属性赋值，只需要将其中的list标签改为set标签即可\n②为Map集合类型属性赋值 创建教师类Teacher：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package com.hbnu.spring.pojo; /** * @Auther: 赵羽 * @Date: 2023/4/13 - 04 - 13 - 22:45 * @Description: com.hbnu.spring.pojo * @version: 1.0 */ public class Teacher { private Integer tid; private String tname; public Teacher() { } public Teacher(Integer tid, String tname) { this.tid = tid; this.tname = tname; } public Integer getTid() { return tid; } public void setTid(Integer tid) { this.tid = tid; } public String getTname() { return tname; } public void setTname(String tname) { this.tname = tname; } @Override public String toString() { return \u0026#34;Teacher{\u0026#34; + \u0026#34;tid=\u0026#34; + tid + \u0026#34;, tname=\u0026#39;\u0026#34; + tname + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } 在Student类中添加以下代码：\n1 2 3 4 5 6 7 private Map\u0026lt;String, Teacher\u0026gt; teacherMap; public Map\u0026lt;String, Teacher\u0026gt; getTeacherMap() { return teacherMap; } public void setTeacherMap(Map\u0026lt;String, Teacher\u0026gt; teacherMap) { this.teacherMap = teacherMap; } 配置bean：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 \u0026lt;bean id=\u0026#34;classTwo\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Class\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;cid\u0026#34; value=\u0026#34;1111\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;cname\u0026#34; value=\u0026#34;大数据2010班\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;students\u0026#34; ref=\u0026#34;studentList\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;teacherMap\u0026#34;\u0026gt; \u0026lt;map\u0026gt; \u0026lt;entry key=\u0026#34;10001\u0026#34; value-ref=\u0026#34;teacherOne\u0026#34;\u0026gt;\u0026lt;/entry\u0026gt; \u0026lt;entry key=\u0026#34;10002\u0026#34; value-ref=\u0026#34;teacherTwo\u0026#34;\u0026gt;\u0026lt;/entry\u0026gt; \u0026lt;/map\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;teacherOne\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Teacher\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;tid\u0026#34; value=\u0026#34;10001\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;tname\u0026#34; value=\u0026#34;景天\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;teacherTwo\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Teacher\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;tid\u0026#34; value=\u0026#34;10002\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;tname\u0026#34; value=\u0026#34;雪见\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; ③引用集合类型的bean 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;!--配置一个集合类型的bean，需要util的约束--\u0026gt; \u0026lt;utils:list id=\u0026#34;studentList\u0026#34;\u0026gt; \u0026lt;ref bean=\u0026#34;studentFive\u0026#34;\u0026gt;\u0026lt;/ref\u0026gt; \u0026lt;ref bean=\u0026#34;studentseven\u0026#34;\u0026gt;\u0026lt;/ref\u0026gt; \u0026lt;ref bean=\u0026#34;studentsix\u0026#34;\u0026gt;\u0026lt;/ref\u0026gt; \u0026lt;/utils:list\u0026gt; \u0026lt;!--map集合类型的bean--\u0026gt; \u0026lt;utils:map id=\u0026#34;teacherMap\u0026#34;\u0026gt; \u0026lt;entry key=\u0026#34;10001\u0026#34; value-ref=\u0026#34;teacherOne\u0026#34;\u0026gt;\u0026lt;/entry\u0026gt; \u0026lt;entry key=\u0026#34;10002\u0026#34; value-ref=\u0026#34;teacherTwo\u0026#34;\u0026gt;\u0026lt;/entry\u0026gt; \u0026lt;/utils:map\u0026gt; \u0026lt;bean id=\u0026#34;classTwo\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Class\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;cid\u0026#34; value=\u0026#34;1111\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;cname\u0026#34; value=\u0026#34;大数据2010班\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;students\u0026#34; ref=\u0026#34;studentList\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;teacherMap\u0026#34; ref=\u0026#34;teacherMap\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 使用util:list、util:map标签必须引入相应的命名空间，可以通过idea的提示功能选择\n2.2.9、实验九：p命名空间 引入p命名空间后，可以通过以下方式为bean的各个属性赋值\n1 2 \u0026lt;bean id=\u0026#34;studentNine\u0026#34; class=\u0026#34;com.hbnu.spring.pojo.Class\u0026#34; P:cid=\u0026#34;10002\u0026#34; P:cname=\u0026#34;云计算\u0026#34; P:teacherMap-ref=\u0026#34;teacherMap\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; 2.2.10、实验十：引入外部属性文件 ①加入依赖 1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;!-- MySQL驱动 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;8.0.27\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 数据源 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;druid\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.31\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; ②创建外部属性文件 1 2 3 4 jdbc.user=root jdbc.password=123456 jdbc.url=jdbc:mysql://localhost:3306/class2013?serverTimezone=UTC jdbc.driver=com.mysql.cj.jdbc.Driver ③引入属性文件 1 2 \u0026lt;!-- 引入外部属性文件 --\u0026gt; \u0026lt;context:property-placeholder location=\u0026#34;classpath:jdbc.properties\u0026#34;/\u0026gt; ④配置bean 1 2 3 4 5 6 \u0026lt;bean id=\u0026#34;druidDataSource\u0026#34; class=\u0026#34;com.alibaba.druid.pool.DruidDataSource\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;${jdbc.url}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;driverClassName\u0026#34; value=\u0026#34;${jdbc.driver}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;${jdbc.username}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;${jdbc.password}\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; ⑤测试 1 2 3 4 5 6 @Test public void testDataSource() throws SQLException { ApplicationContext ioc = new ClassPathXmlApplicationContext(\u0026#34;spring-datasource.xml\u0026#34;); DruidDataSource dataSource = ioc.getBean(DruidDataSource.class); System.out.println(dataSource.getConnection()); } 2.2.11、实验十一：bean的作用域 ①概念 在Spring中可以通过配置bean标签的scope属性来指定bean的作用域范围，各取值含义参加下表：\n取值 含义 创建对象的时机 singleton（默认） 在IOC容器中，这个bean的对象始终为单实例 IOC容器初始化时 prototype 这个bean在IOC容器中有多个实例 获取bean时 如果是在WebApplicationContext环境下还会有另外两个作用域（但不常用）：\n取值 含义 request 在一个请求范围内有效 session 在一个会话范围内有效 ②创建类User 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class User { private Integer id; private String username; private String password; private Integer age; public User() { } public User(Integer id, String username, String password, Integer age) { this.id = id; this.username = username; this.password = password; this.age = age; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return \u0026#34;User{\u0026#34; + \u0026#34;id=\u0026#34; + id + \u0026#34;, username=\u0026#39;\u0026#34; + username + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, password=\u0026#39;\u0026#34; + password + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#39;}\u0026#39;; } } ③配置bean 1 2 3 4 \u0026lt;!-- scope属性：取值singleton（默认值），bean在IOC容器中只有一个实例，IOC容器初始化时创建 对象 --\u0026gt; \u0026lt;!-- scope属性：取值prototype，bean在IOC容器中可以有多个实例，getBean()时创建对象 --\u0026gt; \u0026lt;bean class=\u0026#34;com.atguigu.bean.User\u0026#34; scope=\u0026#34;prototype\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; ④测试 1 2 3 4 5 6 7 @Test public void testBeanScope(){ ApplicationContext ac = new ClassPathXmlApplicationContext(\u0026#34;spring-scope.xml\u0026#34;); User user1 = ac.getBean(User.class); User user2 = ac.getBean(User.class); System.out.println(user1==user2); } 2.2.12、实验十二：bean的生命周期 ①具体的生命周期过程 bean对象创建（调用无参构造器） 给bean对象设置属性 bean对象初始化之前操作（由bean的后置处理器负责） bean对象初始化（需在配置bean时指定初始化方法） bean对象初始化之后操作（由bean的后置处理器负责） bean对象就绪可以使用 bean对象销毁（需在配置bean时指定销毁方法） IOC容器关闭 ②修改类User 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public class User { private Integer id; private String username; private String password; private Integer age; public User() { System.out.println(\u0026#34;生命周期：1、创建对象\u0026#34;); } public User(Integer id, String username, String password, Integer age) { this.id = id; this.username = username; this.password = password; this.age = age; } public Integer getId() { return id; } public void setId(Integer id) { System.out.println(\u0026#34;生命周期：2、依赖注入\u0026#34;); this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public void initMethod(){ System.out.println(\u0026#34;生命周期：3、初始化\u0026#34;); } public void destroyMethod(){ System.out.println(\u0026#34;生命周期：5、销毁\u0026#34;); } @Override public String toString() { return \u0026#34;User{\u0026#34; + \u0026#34;id=\u0026#34; + id + \u0026#34;, username=\u0026#39;\u0026#34; + username + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, password=\u0026#39;\u0026#34; + password + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#39;}\u0026#39;; } } 注意其中的initMethod()和destroyMethod()，可以通过配置bean指定为初始化和销毁的方法\n③配置bean 1 2 3 4 5 6 7 8 \u0026lt;!-- 使用init-method属性指定初始化方法 --\u0026gt; \u0026lt;!-- 使用destroy-method属性指定销毁方法 --\u0026gt; \u0026lt;bean class=\u0026#34;com.hbnu.spring.pojo.User\u0026#34; scope=\u0026#34;prototype\u0026#34; init-method=\u0026#34;initMethod\u0026#34;destroy-method=\u0026#34;destroyMethod\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;id\u0026#34; value=\u0026#34;1001\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;admin\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;123456\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;age\u0026#34; value=\u0026#34;23\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 注意：\n若bean的作用域为单例时，生命周期的前三个步骤会在获取IOC容器时执行\n若bean的作用域为多例时，生命周期的前三个步骤会在获取bean时执行\n④测试 1 2 3 4 5 6 7 8 @Test public void testLiftCycle() throws Exception { //ConfigurableApplicationContext是ApplicationContext的子接口，其中拓展了刷新和关闭容器的方法 ConfigurableApplicationContext ioc = new ClassPathXmlApplicationContext(\u0026#34;spring-liftcycle.xml\u0026#34;); User user = ioc.getBean(User.class); System.out.println(user); ioc.close(); } ⑤bean的后置处理器 bean的后置处理器会在生命周期的初始化前后添加额外的操作，需要实现BeanPostProcessor接口，\n且配置到IOC容器中，需要注意的是，bean后置处理器不是单独针对某一个bean生效，而是针对IOC容器中所有bean都会执行\n创建bean的后置处理器：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.hbnu.spring.process; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanPostProcessor; /** * @Auther: 赵羽 * @Date: 2023/4/16 - 04 - 16 - 14:12 * @Description: com.hbnu.spring.process * @version: 1.0 */ public class MyBeanProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { //此方法在bean的生命周期初始化之前执行 System.out.println(\u0026#34;MyBeanProcessor ==\u0026gt; bean的后置处理器postProcessBeforeInitialization\u0026#34;); return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { //此方法在bean的生命周期初始化之后执行 System.out.println(\u0026#34;MyBeanProcessor ==\u0026gt; bean的后置处理器postProcessAfterInitialization\u0026#34;); return bean; } } 在IOC容器中配置后置处理器：\n\u0026lt;bean id=\u0026ldquo;myBeanProcessor\u0026quot;class=\u0026ldquo;com.hbnu.spring.process.MyBeanProcessor\u0026rdquo;/\u0026gt;\n2.2.13、实验十三：FactoryBean ①简介 FactoryBean是Spring提供的一种整合第三方框架的常用机制。和普通的bean不同，配置一个\nFactoryBean类型的bean，在获取bean的时候得到的并不是class属性中配置的这个类的对象，而是\ngetObject()方法的返回值。通过这种机制，Spring可以帮我们把复杂组件创建的详细过程和繁琐细节都屏蔽起来，只把最简洁的使用界面展示给我们。\n将来我们整合Mybatis时，Spring就是通过FactoryBean机制来帮我们创建SqlSessionFactory对象的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 /* * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the \u0026#34;License\u0026#34;); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an \u0026#34;AS IS\u0026#34; BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.beans.factory; import org.springframework.lang.Nullable; /** * Interface to be implemented by objects used within a {@link BeanFactory} which * are themselves factories for individual objects. If a bean implements this * interface, it is used as a factory for an object to expose, not directly as a * bean instance that will be exposed itself. * * \u0026lt;p\u0026gt;\u0026lt;b\u0026gt;NB: A bean that implements this interface cannot be used as a normal bean.\u0026lt;/b\u0026gt; * A FactoryBean is defined in a bean style, but the object exposed for bean * references ({@link #getObject()}) is always the object that it creates. * * \u0026lt;p\u0026gt;FactoryBeans can support singletons and prototypes, and can either create * objects lazily on demand or eagerly on startup. The {@link SmartFactoryBean} * interface allows for exposing more fine-grained behavioral metadata. * * \u0026lt;p\u0026gt;This interface is heavily used within the framework itself, for example for * the AOP {@link org.springframework.aop.framework.ProxyFactoryBean} or the * {@link org.springframework.jndi.JndiObjectFactoryBean}. It can be used for * custom components as well; however, this is only common for infrastructure code. * * \u0026lt;p\u0026gt;\u0026lt;b\u0026gt;{@code FactoryBean} is a programmatic contract. Implementations are not * supposed to rely on annotation-driven injection or other reflective facilities.\u0026lt;/b\u0026gt; * {@link #getObjectType()} {@link #getObject()} invocations may arrive early in the * bootstrap process, even ahead of any post-processor setup. If you need access to * other beans, implement {@link BeanFactoryAware} and obtain them programmatically. * * \u0026lt;p\u0026gt;\u0026lt;b\u0026gt;The container is only responsible for managing the lifecycle of the FactoryBean * instance, not the lifecycle of the objects created by the FactoryBean.\u0026lt;/b\u0026gt; Therefore, * a destroy method on an exposed bean object (such as {@link java.io.Closeable#close()} * will \u0026lt;i\u0026gt;not\u0026lt;/i\u0026gt; be called automatically. Instead, a FactoryBean should implement * {@link DisposableBean} and delegate any such close call to the underlying object. * * \u0026lt;p\u0026gt;Finally, FactoryBean objects participate in the containing BeanFactory\u0026#39;s * synchronization of bean creation. There is usually no need for internal * synchronization other than for purposes of lazy initialization within the * FactoryBean itself (or the like). * * @author Rod Johnson * @author Juergen Hoeller * @since 08.03.2003 * @param \u0026lt;T\u0026gt; the bean type * @see org.springframework.beans.factory.BeanFactory * @see org.springframework.aop.framework.ProxyFactoryBean * @see org.springframework.jndi.JndiObjectFactoryBean */ public interface FactoryBean\u0026lt;T\u0026gt; { /** * The name of an attribute that can be * {@link org.springframework.core.AttributeAccessor#setAttribute set} on a * {@link org.springframework.beans.factory.config.BeanDefinition} so that * factory beans can signal their object type when it can\u0026#39;t be deduced from * the factory bean class. * @since 5.2 */ String OBJECT_TYPE_ATTRIBUTE = \u0026#34;factoryBeanObjectType\u0026#34;; /** * Return an instance (possibly shared or independent) of the object * managed by this factory. * \u0026lt;p\u0026gt;As with a {@link BeanFactory}, this allows support for both the * Singleton and Prototype design pattern. * \u0026lt;p\u0026gt;If this FactoryBean is not fully initialized yet at the time of * the call (for example because it is involved in a circular reference), * throw a corresponding {@link FactoryBeanNotInitializedException}. * \u0026lt;p\u0026gt;As of Spring 2.0, FactoryBeans are allowed to return {@code null} * objects. The factory will consider this as normal value to be used; it * will not throw a FactoryBeanNotInitializedException in this case anymore. * FactoryBean implementations are encouraged to throw * FactoryBeanNotInitializedException themselves now, as appropriate. * @return an instance of the bean (can be {@code null}) * @throws Exception in case of creation errors * @see FactoryBeanNotInitializedException */ @Nullable T getObject() throws Exception; /** * Return the type of object that this FactoryBean creates, * or {@code null} if not known in advance. * \u0026lt;p\u0026gt;This allows one to check for specific types of beans without * instantiating objects, for example on autowiring. * \u0026lt;p\u0026gt;In the case of implementations that are creating a singleton object, * this method should try to avoid singleton creation as far as possible; * it should rather estimate the type in advance. * For prototypes, returning a meaningful type here is advisable too. * \u0026lt;p\u0026gt;This method can be called \u0026lt;i\u0026gt;before\u0026lt;/i\u0026gt; this FactoryBean has * been fully initialized. It must not rely on state created during * initialization; of course, it can still use such state if available. * \u0026lt;p\u0026gt;\u0026lt;b\u0026gt;NOTE:\u0026lt;/b\u0026gt; Autowiring will simply ignore FactoryBeans that return * {@code null} here. Therefore it is highly recommended to implement * this method properly, using the current state of the FactoryBean. * @return the type of object that this FactoryBean creates, * or {@code null} if not known at the time of the call * @see ListableBeanFactory#getBeansOfType */ @Nullable Class\u0026lt;?\u0026gt; getObjectType(); /** * Is the object managed by this factory a singleton? That is, * will {@link #getObject()} always return the same object * (a reference that can be cached)? * \u0026lt;p\u0026gt;\u0026lt;b\u0026gt;NOTE:\u0026lt;/b\u0026gt; If a FactoryBean indicates to hold a singleton object, * the object returned from {@code getObject()} might get cached * by the owning BeanFactory. Hence, do not return {@code true} * unless the FactoryBean always exposes the same reference. * \u0026lt;p\u0026gt;The singleton status of the FactoryBean itself will generally * be provided by the owning BeanFactory; usually, it has to be * defined as singleton there. * \u0026lt;p\u0026gt;\u0026lt;b\u0026gt;NOTE:\u0026lt;/b\u0026gt; This method returning {@code false} does not * necessarily indicate that returned objects are independent instances. * An implementation of the extended {@link SmartFactoryBean} interface * may explicitly indicate independent instances through its * {@link SmartFactoryBean#isPrototype()} method. Plain {@link FactoryBean} * implementations which do not implement this extended interface are * simply assumed to always return independent instances if the * {@code isSingleton()} implementation returns {@code false}. * \u0026lt;p\u0026gt;The default implementation returns {@code true}, since a * {@code FactoryBean} typically manages a singleton instance. * @return whether the exposed object is a singleton * @see #getObject() * @see SmartFactoryBean#isPrototype() */ default boolean isSingleton() { return true; } } ②创建类UserFactoryBean 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.hbnu.spring.factory; import com.hbnu.spring.pojo.User; import org.springframework.beans.factory.FactoryBean; /** * @Auther: 赵羽 * @Date: 2023/4/16 - 04 - 16 - 14:28 * @Description: com.hbnu.spring.factory * @version: 1.0 */ public class UserFactoryBean implements FactoryBean { @Override public Object getObject() throws Exception { return new User(); } @Override public Class\u0026lt;?\u0026gt; getObjectType() { return User.class; } @Override public boolean isSingleton() { return false; } } FactoryBean是一个接口，需要创建一个类实现该接口\n其中有三个方法：\ngetObject()：通过一个对象交给IOC容器管理\ngetObjectType()：设置所提供对象的类型\nisSingleton()：所提供的对象是否单例\n当把FactoryBean的实现类配置为bean时，会将当前类中getObject()所返回的对象交给IOC容器管理\n③配置bean 1 \u0026lt;bean class=\u0026#34;com.hbnu.spring.pojo.UserFactoryBean\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; ④测试 1 2 3 4 5 6 @Test public void testFactoryBean() throws Exception { ApplicationContext ioc = new ClassPathXmlApplicationContext(\u0026#34;spring-factorybean.xml\u0026#34;); User user = ioc.getBean(User.class); System.out.println(user); } 2.2.14、实验十四：基于xml的自动装配 自动装配：\n根据指定的策略，在IOC容器中匹配某一个bean，自动为指定的bean中所依赖的类类型或接口类\n型属性赋值\n①场景模拟 创建类UserController\n1 2 3 4 5 6 7 8 9 public class UserController { private UserService userService; public void setUserService(UserService userService) { this.userService = userService; } public void saveUser(){ userService.saveUser(); } } 创建接口UserService\n1 2 3 public interface UserService { void saveUser(); } 创建类UserServiceImpl实现接口UserService\n1 2 3 4 5 6 7 8 9 10 public class UserServiceImpl implements UserService { private UserDao userDao; public void setUserDao(UserDao userDao) { this.userDao = userDao; } @Override public void saveUser() { userDao.saveUser(); } } 创建接口UserDao\n1 2 3 public interface UserDao { void saveUser(); } 创建类UserDaoImpl实现接口UserDao\n1 2 3 4 5 6 public class UserDaoImpl implements UserDao { @Override public void saveUser() { System.out.println(\u0026#34;保存成功\u0026#34;); } } ②配置bean 使用bean标签的autowire属性设置自动装配效果\n自动装配方式：byType\nbyType：根据类型匹配IOC容器中的某个兼容类型的bean，为属性自动赋值\n若在IOC中，没有任何一个兼容类型的bean能够为属性赋值，则该属性不装配，即值为默认值\nnull\n若在IOC中，有多个兼容类型的bean能够为属性赋值，则抛出异常\nNoUniqueBeanDefinitionException\n1 2 3 \u0026lt;bean id=\u0026#34;userController\u0026#34; class=\u0026#34;com.hbnu.spring.controller.UserController\u0026#34; autowire=\u0026#34;byType\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;userService\u0026#34; class=\u0026#34;com.hbnu.spring.service.impl.UserServiceImpl\u0026#34; autowire=\u0026#34;byType\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;userDao\u0026#34; class=\u0026#34;com.hbnu.spring.dao.impl.UserDaoImpl\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; 自动装配方式：byName\nbyName：将自动装配的属性的属性名，作为bean的id在IOC容器中匹配相对应的bean进行赋值\n1 2 3 4 5 \u0026lt;bean id=\u0026#34;userController\u0026#34; class=\u0026#34;com.hbnu.spring.controller.UserController\u0026#34; autowire=\u0026#34;byName\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;userService\u0026#34; class=\u0026#34;com.hbnu.spring.service.impl.UserServiceImpl\u0026#34; autowire=\u0026#34;byName\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;userServiceImpl\u0026#34; class=\u0026#34;com.hbnu.spring.service.impl.UserServiceImpl\u0026#34; autowire=\u0026#34;byName\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;userDao\u0026#34; class=\u0026#34;com.hbnu.spring.dao.impl.UserDaoImpl\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;userDaoImpl\u0026#34; class=\u0026#34;com.hbnu.spring.dao.impl.UserDaoImpl\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; ③测试 1 2 3 4 5 6 @Test public void testAutoWireByXml() throws Exception { ApplicationContext ioc = new ClassPathXmlApplicationContext(\u0026#34;spring-autowire-xml.xml\u0026#34;); UserController userController = ioc.getBean(UserController.class); userController.saveUser(); } 2.3、基于注解管理bean 2.3.1、实验一：标记与扫描 ①注解 和 XML 配置文件一样，注解本身并不能执行，注解本身仅仅只是做一个标记，具体的功能是框架检测\n到注解标记的位置，然后针对这个位置按照注解标记的功能来执行具体操作。\n本质上：所有一切的操作都是Java代码来完成的，XML和注解只是告诉框架中的Java代码如何执行。\n举例：元旦联欢会要布置教室，蓝色的地方贴上元旦快乐四个字，红色的地方贴上拉花，黄色的地方贴上气球。\n班长做了所有标记，同学们来完成具体工作。墙上的标记相当于我们在代码中使用的注解，后面同学们做的工作，相当于框架的具体操作。\n②扫描 Spring 为了知道程序员在哪些地方标记了什么注解，就需要通过扫描的方式，来进行检测。然后根据注解进行后续操作。\n③新建Maven Module 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;dependencies\u0026gt; \u0026lt;!-- 基于Maven依赖传递性，导入spring-context依赖即可导入当前所需所有jar包 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-context\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- junit测试 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.12\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; ④创建Spring配置文件 ⑤标识组件的常用注解 @Component：将类标识为普通组件\n@Controller：将类标识为控制层组件\n@Service：将类标识为业务层组件\n@Repository：将类标识为持久层组件\n问：以上四个注解有什么关系和区别？\n通过查看源码我们得知，@Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字。\n对于Spring使用IOC容器管理这些组件来说没有区别。所以@Controller、@Service、@Repository这\n三个注解只是给开发人员看的，让我们能够便于分辨组件的作用。\n注意：虽然它们本质上一样，但是为了代码的可读性，为了程序结构严谨我们肯定不能随便胡乱标记。\n⑥创建组件 创建控制层组件\n1 2 3 @Controller public class UserController { } 创建接口UserService\n1 2 public interface UserService { } 创建业务层组件UserServiceImpl\n1 2 3 @Service public class UserServiceImpl implements UserService { } 创建接口UserDao\n1 2 public interface UserDao { } 创建持久层组件UserDaoImpl\n1 2 3 @Repository public class UserDaoImpl implements UserDao { } ⑦扫描组件 情况一：最基本的扫描方式\n1 \u0026lt;context:component-scan base-package=\u0026#34;com.hbnu.spring\u0026#34;\u0026gt;\u0026lt;/context:component-scan\u0026gt; 情况二：指定要排除的组件\n1 2 3 4 5 6 7 8 9 10 \u0026lt;context:component-scan base-package=\u0026#34;com.hbnu.spring\u0026#34;\u0026gt; \u0026lt;!-- context:exclude-filter标签：指定排除规则 --\u0026gt; \u0026lt;!-- type：设置排除或包含的依据 type=\u0026#34;annotation\u0026#34;，根据注解排除，expression中设置要排除的注解的全类名 type=\u0026#34;assignable\u0026#34;，根据类型排除，expression中设置要排除的类型的全类名 --\u0026gt; \u0026lt;!--\u0026lt;context:exclude-filter type=\u0026#34;annotation\u0026#34; expression=\u0026#34;org.springframework.stereotype.Controller\u0026#34;/\u0026gt;--\u0026gt; \u0026lt;context:exclude-filter type=\u0026#34;assignable\u0026#34; expression=\u0026#34;com.hbnu.spring.controller.UserController\u0026#34;/\u0026gt; \u0026lt;/context:component-scan\u0026gt; 情况三：仅扫描指定组件\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;context:component-scan base-package=\u0026#34;com.hbnu.spring\u0026#34; use-default-filters=\u0026#34;false\u0026#34;\u0026gt; \u0026lt;!-- context:include-filter标签：指定在原有扫描规则的基础上追加的规则 --\u0026gt; \u0026lt;!-- use-default-filters属性：取值false表示关闭默认扫描规则 --\u0026gt; \u0026lt;!-- 此时必须设置use-default-filters=\u0026#34;false\u0026#34;，因为默认规则即扫描指定包下所有类 --\u0026gt; \u0026lt;!-- type：设置排除或包含的依据 type=\u0026#34;annotation\u0026#34;，根据注解排除，expression中设置要排除的注解的全类名 type=\u0026#34;assignable\u0026#34;，根据类型排除，expression中设置要排除的类型的全类名 --\u0026gt; \u0026lt;!--\u0026lt;context:include-filter type=\u0026#34;annotation\u0026#34; expression=\u0026#34;org.springframework.stereotype.Repository\u0026#34;/\u0026gt;--\u0026gt; \u0026lt;context:include-filter type=\u0026#34;assignable\u0026#34; expression=\u0026#34;com.hbnu.spring.dao.impl.UserDaoImpl\u0026#34;/\u0026gt; \u0026lt;/context:component-scan\u0026gt; ⑧测试 1 2 3 4 5 6 7 8 9 10 @Test public void test() { ApplicationContext ioc = new ClassPathXmlApplicationContext(\u0026#34;spring-ioc-annotation.xml\u0026#34;); UserController userController = ioc.getBean(UserController.class); System.out.println(userController); UserService userService = ioc.getBean(UserService.class); System.out.println(userService); UserDao userDao = ioc.getBean(UserDao.class); System.out.println(userDao); } ⑨组件所对应的bean的id 在我们使用XML方式管理bean的时候，每个bean都有一个唯一标识，便于在其他地方引用。现在使用\n注解后，每个组件仍然应该有一个唯一标识。\n默认情况\n类名首字母小写就是bean的id。例如：UserController类对应的bean的id就是userController。\n自定义bean的id\n可通过标识组件的注解的value属性设置自定义的bean的id\n@Service(\u0026ldquo;userService\u0026rdquo;)//默认为userServiceImpl public class UserServiceImpl implements\nUserService {}\n2.3.2、实验二：基于注解的自动装配 ①场景模拟 参考基于xml的自动装配\n在UserController中声明UserService对象\n在UserServiceImpl中声明UserDao对象\n②@Autowired注解 在成员变量上直接标记@Autowired注解即可完成自动装配，不需要提供setXxx()方法。以后我们在项\n目中的正式用法就是这样。\n1 2 3 4 5 6 7 8 @Controller public class UserController { @Autowired private UserService userService; public void saveUser(){ userService.saveUser(); } } 1 2 3 4 5 6 7 8 9 10 11 12 public interface UserService { void saveUser(); } @Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override public void saveUser() { userDao.saveUser(); } } 1 2 3 public interface UserDao { void saveUser(); }\t1 2 3 4 5 6 7 @Repository public class UserDaoImpl implements UserDao { @Override public void saveUser() { System.out.println(\u0026#34;保存成功\u0026#34;); } } ③@Autowired注解其他细节 @Autowired注解可以标记成员变量，构造器和set方法上\n1 2 3 4 5 6 7 8 9 10 11 @Controller public class UserController { private UserService userService; @Autowired public UserController(UserService userService){ this.userService = userService; } public void saveUser(){ userService.saveUser(); } } 1 2 3 4 5 6 7 8 9 10 11 @Controller public class UserController { private UserService userService; @Autowired public void setUserService(UserService userService){ this.userService = userService; } public void saveUser(){ userService.saveUser(); } } ④@Autowired工作流程 @Autowired注解的原理\na\u0026gt;默认通过byType的方式，在IOC容器中通过类型匹配某个bean为属性赋值\nb\u0026gt;若有多个类型匹配的bean，此时会自动转换为byName的方式实现自动装配的效果\n即将要赋值的属性的属性名作为bean的id匹配某个bean为属性赋值\nc\u0026gt;若byType和byName的方式都无法实现自动装配，即IOC容器中有多个类型匹配的bean\n且这些bean和id和要赋值的属性的属性名都不一致，此时抛异常：NoUniqueBeanDefinitionException\nd\u0026gt;此时可以在要赋值的属性上，添加一个注解@qualifier\n通过该注解的value属性值，指定某个bean的id，将这个bean为属性赋值\n首先根据所需要的组件类型到IOC容器中查找 能够找到唯一的bean：直接执行装配 如果完全找不到匹配这个类型的bean：装配失败 和所需类型匹配的bean不止一个 没有@Qualifier注解：根据@Autowired标记位置成员变量的变量名作为bean的id进行匹配 能够找到：执行装配 找不到：装配失败 使用@Qualifier注解：根据@Qualifier注解中指定的名称作为bean的id进行匹配 能够找到：执行装配 找不到：装配失败 1 2 3 4 5 6 7 8 9 @Controller public class UserController { @Autowired @Qualifier(\u0026#34;userServiceImpl\u0026#34;) private UserService userService; public void saveUser(){ userService.saveUser(); } } @Autowired中有属性required，默认值为true，因此在自动装配无法找到相应的bean时，会装配失败\n可以将属性required的值设置为true，则表示能装就装，装不上就不装，此时自动装配的属性为默认值\n但是实际开发时，基本上所有需要装配组件的地方都是必须装配的，用不上这个属性。\n3、AOP 3.1、场景模拟 3.1.1、声明接口 声明计算器接口Calculator，包含加减乘除的抽象方法\n1 2 3 4 5 6 public interface Calculator { int add(int i, int j); int sub(int i, int j); int mul(int i, int j); int div(int i, int j); } 3.1.2、创建实现类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class CalculatorPureImpl implements Calculator { @Override public int add(int i, int j) { int result = i + j; System.out.println(\u0026#34;方法内部 result = \u0026#34; + result); return result; } @Override public int sub(int i, int j) { int result = i - j; System.out.println(\u0026#34;方法内部 result = \u0026#34; + result); return result; } @Override public int mul(int i, int j) { int result = i * j; System.out.println(\u0026#34;方法内部 result = \u0026#34; + result); return result; } @Override public int div(int i, int j) { int result = i / j; System.out.println(\u0026#34;方法内部 result = \u0026#34; + result); return result; } } 3.1.3、创建带日志功能的实现类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class CalculatorLogImpl implements Calculator { @Override public int add(int i, int j) { System.out.println(\u0026#34;[日志] add 方法开始了，参数是：\u0026#34; + i + \u0026#34;,\u0026#34; + j); int result = i + j; System.out.println(\u0026#34;方法内部 result = \u0026#34; + result); System.out.println(\u0026#34;[日志] add 方法结束了，结果是：\u0026#34; + result); return result; } @Override public int sub(int i, int j) { System.out.println(\u0026#34;[日志] sub 方法开始了，参数是：\u0026#34; + i + \u0026#34;,\u0026#34; + j); int result = i - j; System.out.println(\u0026#34;方法内部 result = \u0026#34; + result); System.out.println(\u0026#34;[日志] sub 方法结束了，结果是：\u0026#34; + result); return result; } @Override public int mul(int i, int j) { System.out.println(\u0026#34;[日志] mul 方法开始了，参数是：\u0026#34; + i + \u0026#34;,\u0026#34; + j); int result = i * j; System.out.println(\u0026#34;方法内部 result = \u0026#34; + result); System.out.println(\u0026#34;[日志] mul 方法结束了，结果是：\u0026#34; + result); return result; } @Override public int div(int i, int j) { System.out.println(\u0026#34;[日志] div 方法开始了，参数是：\u0026#34; + i + \u0026#34;,\u0026#34; + j); int result = i / j; System.out.println(\u0026#34;方法内部 result = \u0026#34; + result); System.out.println(\u0026#34;[日志] div 方法结束了，结果是：\u0026#34; + result); return result; } } 3.1.4、提出问题 ①现有代码缺陷 针对带日志功能的实现类，我们发现有如下缺陷：\n对核心业务功能有干扰，导致程序员在开发核心业务功能时分散了精力 附加功能分散在各个业务功能方法中，不利于统一维护 ②解决思路 解决这两个问题，核心就是：解耦。我们需要把附加功能从业务功能代码中抽取出来。\n③困难 解决问题的困难：要抽取的代码在方法内部，靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。\n3.2、代理模式 3.2.1、概念 ①介绍 二十三种设计模式中的一种，属于结构型模式。它的作用就是通过提供一个代理类，让我们在调用目标方法的时候，不再是直接对目标方法进行调用，而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法，减少对目标方法的调用和打扰，同时让附加功能能够集中在一起也有利于统一维护。\n使用代理后：\n②生活中的代理 广告商找大明星拍广告需要经过经纪人 合作伙伴找大老板谈合作要约见面时间需要经过秘书 房产中介是买卖双方的代理 ③相关术语 代理：将非核心逻辑剥离出来以后，封装这些非核心逻辑的类、对象、方法。 目标：被代理“套用”了非核心逻辑代码的类、对象、方法。 3.2.2、静态代理 创建静态代理类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class CalculatorStaticProxy implements Calculator { // 将被代理的目标对象声明为成员变量 private Calculator target; public CalculatorStaticProxy(Calculator target) { this.target = target; } @Override public int add(int i, int j) { // 附加功能由代理类中的代理方法来实现 System.out.println(\u0026#34;[日志] add 方法开始了，参数是：\u0026#34; + i + \u0026#34;,\u0026#34; + j); // 通过目标对象来实现核心业务逻辑 int addResult = target.add(i, j); System.out.println(\u0026#34;[日志] add 方法结束了，结果是：\u0026#34; + addResult); return addResult; } } 静态代理确实实现了解耦，但是由于代码都写死了，完全不具备任何的灵活性。就拿日志功能来\n说，将来其他地方也需要附加日志，那还得再声明更多个静态代理类，那就产生了大量重复的代\n码，日志功能还是分散的，没有统一管理。\n提出进一步的需求：将日志功能集中到一个代理类中，将来有任何日志需求，都通过这一个代理\n类来实现。这就需要使用动态代理技术了。\n3.2.3、动态代理 动态代理有两种：\n1.jdk动态代理，要求必须有接口，最终生成的代理类和目标类实现相同的接口在com.sun.proxy包下，类名为$proxy2\n2.cglib动态代理，最终生成的代理类会继承目标类，并且和目标类在相同的包下\n生产代理对象的工厂类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class ProxyFactory { private Object target; public ProxyFactory(Object target) { this.target = target; } public Object getProxy(){ /** * newProxyInstance()：创建一个代理实例 * 其中有三个参数： * 1、classLoader：加载动态生成的代理类的类加载器 * 2、interfaces：目标对象实现的所有接口的class对象所组成的数组 * 3、invocationHandler：设置代理对象实现目标对象方法的过程，即代理类中如何重写接口中的抽象方法 */ ClassLoader classLoader = target.getClass().getClassLoader(); Class\u0026lt;?\u0026gt;[] interfaces = target.getClass().getInterfaces(); InvocationHandler invocationHandler = new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { /** * proxy：代理对象 * method：代理对象需要实现的方法，即其中需要重写的方法 * args：method所对应方法的参数 */ Object result = null; try { System.out.println(\u0026#34;[动态代理][日志] \u0026#34;+method.getName()+\u0026#34;，参数：\u0026#34;+ Arrays.toString(args)); result = method.invoke(target, args); System.out.println(\u0026#34;[动态代理][日志] \u0026#34;+method.getName()+\u0026#34;，结 果：\u0026#34;+ result); } catch (Exception e) { e.printStackTrace(); System.out.println(\u0026#34;[动态代理][日志] \u0026#34;+method.getName()+\u0026#34;，异常：\u0026#34;+e.getMessage()); } finally { System.out.println(\u0026#34;[动态代理][日志] \u0026#34;+method.getName()+\u0026#34;，方法执行完毕\u0026#34;); } return result; } }; return Proxy.newProxyInstance(classLoader, interfaces,invocationHandler); } } 3.2.4、测试 1 2 3 4 5 6 7 @Test public void testDynamicProxy(){ ProxyFactory factory = new ProxyFactory(new CalculatorLogImpl()); Calculator proxy = (Calculator) factory.getProxy(); proxy.div(1,0); //proxy.div(1,1); } 3.3、AOP概念及相关术语 3.3.1、概述 AOP（Aspect Oriented Programming）是一种设计思想，是软件设计领域中的面向切面编程，它是面向对象编程的一种补充和完善，它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。\n3.3.2、相关术语 ①横切关注点 从每个方法中抽取出来的同一类非核心业务。在同一个项目中，我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。\n这个概念不是语法层面天然存在的，而是根据附加功能的逻辑上的需要：有十个附加功能，就有十个横切关注点。\n②通知 每一个横切关注点上要做的事情都需要写一个方法来实现，这样的方法就叫通知方法。\n前置通知：在被代理的目标方法前执行 返回通知：在被代理的目标方法成功结束后执行（寿终正寝） 异常通知：在被代理的目标方法异常结束后执行（死于非命） 后置通知：在被代理的目标方法最终结束后执行（盖棺定论） 环绕通知：使用try\u0026hellip;catch\u0026hellip;finally结构围绕整个被代理的目标方法，包括上面四种通知对应的所有位置 ③切面 封装通知方法的类。\n④目标 被代理的目标对象。\n⑤代理 向目标对象应用通知之后创建的代理对象。\n⑥连接点 这也是一个纯逻辑概念，不是语法定义的。\n把方法排成一排，每一个横切位置看成x轴方向，把方法从上到下执行的顺序看成y轴，x轴和y轴的交叉点就是连接点。\n⑦切入点 定位连接点的方式。\n每个类的方法中都包含多个连接点，所以连接点是类中客观存在的事物（从逻辑上来说）。\n如果把连接点看作数据库中的记录，那么切入点就是查询记录的 SQL 语句。\nSpring 的 AOP 技术可以通过切入点定位到特定的连接点。\n切点通过 org.springframework.aop.Pointcut 接口进行描述，它使用类和方法作为连接点的查询条\n件。\n3.3.3、作用 简化代码：把方法中固定位置的重复的代码抽取出来，让被抽取的方法更专注于自己的核心功能，提高内聚性。\n代码增强：把特定的功能封装到切面类中，看哪里有需要，就往上套，被套用了切面逻辑的方法就被切面给增强了。\n3.4、基于注解的AOP 3.4.1、技术说明 动态代理（InvocationHandler）：JDK原生的实现方式，需要被代理的目标类必须实现接口。因 为这个技术要求代理对象和目标对象实现同样的接口（兄弟两个拜把子模式）。\ncglib：通过继承被代理的目标类（认干爹模式）实现代理，所以不需要目标类实现接口。\nAspectJ：本质上是静态代理，将代理逻辑“织入”被代理的目标类编译得到的字节码文件，所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。\n3.4.2、准备工作 ①添加依赖 在IOC所需依赖基础上再加入下面依赖即可：\n1 2 3 4 5 6 \u0026lt;!-- spring-aspects会帮我们传递过来aspectjweaver --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-aspects\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; ②准备被代理的目标资源 接口：\n1 2 3 4 5 6 public interface Calculator { int add(int i, int j); int sub(int i, int j); int mul(int i, int j); int div(int i, int j); } 实现类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Component public class CalculatorPureImpl implements Calculator { @Override public int add(int i, int j) { int result = i + j; System.out.println(\u0026#34;方法内部 result = \u0026#34; + result); return result; } @Override public int sub(int i, int j) { int result = i - j; System.out.println(\u0026#34;方法内部 result = \u0026#34; + result); return result; } @Override public int mul(int i, int j) { int result = i * j; System.out.println(\u0026#34;方法内部 result = \u0026#34; + result); return result; } @Override public int div(int i, int j) { int result = i / j; System.out.println(\u0026#34;方法内部 result = \u0026#34; + result); return result; } } 3.4.3、创建切面类并配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 package com.hbnu.spring; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.util.Arrays; /** * @Auther: 赵羽 * @Date: 2023/4/18 - 04 - 18 - 11:58 * @Description: com.hbnu.spring * @version: 1.0 * 1.在切面中，需要通过指定的注解将方法标识为通知方法 * @Before：前置通知，在目标对象方法执行之前执行 * @After：后置通知，在目标对象方法的finally子句中执行的 * @AfterReturning:返回通知，在目标对象方法返回值之后执行 * @AfterThrowing:异常通知，在目标对象方法的catch子句中执行的 * * * 2.切入点表达式：设置在标识通知的注解的value属性中 * execution(public int com.hbnu.spring.CalculatorPureImpl.add(int,int)) * execution(* com.hbnu.spring.CalculatorPureImpl.*(..)) * 第一个*表示任意的访问修饰符和返回值类型 * 第二个*表示类中任意的方法 * ..表示任意的参数列表 * 类的地方也可以使用*，表示包下所有的类 * * 3.重用切入点表达式 * //@Pointcut声明一个公共的切入点表达式 * @Pointcut(\u0026#34;execution(* com.hbnu.spring.CalculatorPureImpl.*(..))\u0026#34;) * public void pointCut() {} * 使用方式@After(\u0026#34;pointCut()\u0026#34;) * * 4.获取连接点的信息 * 在通知方法的参数位置，设置JoinPoint类型的参数，就可以获取连接点所对应方法的信息 * //获取连接点所对应方法的签名信息 * Signature methodName = joinPoint.getSignature(); * //获取连接点所对应方法的参数 * String args = Arrays.toString(joinPoint.getArgs()); * *5.切面的优先级 * 可以通过@order注解的value属性设置优先级，默认值Integer的最大值 * @order注解的value属性值越小，优先级越高 * * * */ @Component @Aspect //将当前组件标识为切面 public class LoggerAspect { @Pointcut(\u0026#34;execution(* com.hbnu.spring.CalculatorPureImpl.*(..))\u0026#34;) public void pointCut() {} //@Before(\u0026#34;execution(public int com.hbnu.spring.CalculatorPureImpl.add(int,int))\u0026#34;) @Before(\u0026#34;execution(* com.hbnu.spring.CalculatorPureImpl.*(..))\u0026#34;) public void beforeAdviceMethod(JoinPoint joinPoint) { //获取连接点所对应方法的签名信息 Signature methodName = joinPoint.getSignature(); //获取连接点所对应方法的参数 String args = Arrays.toString(joinPoint.getArgs()); System.out.println(\u0026#34;Logger--\u0026gt;前置通知，方法名：\u0026#34;+methodName.getName() +\u0026#34;，参数：\u0026#34;+ args); } @After(\u0026#34;pointCut()\u0026#34;) public void afterAdviceMethod(JoinPoint joinPoint) { Signature methodName = joinPoint.getSignature(); System.out.println(\u0026#34;Logger--\u0026gt;后置通知，方法名：\u0026#34;+methodName.getName()); } /** 在返回通知中若要获取目标对象方法的返回值 只需要通过@AfterReturning注解的returning属性 就可以将通知方法的某个参数指定为接收目标对象方法的返回值的参数 */ @AfterReturning(value = \u0026#34;pointCut()\u0026#34;, returning = \u0026#34;result\u0026#34;) public void afterReturningMethod(JoinPoint joinPoint,Object result) { Signature methodName = joinPoint.getSignature(); System.out.println(\u0026#34;Logger--\u0026gt;返回通知，方法名：\u0026#34;+methodName.getName() +\u0026#34;，结果：\u0026#34;+result); } /** 在异常通知中若要获取目标对象方法的异常 只需要通过@AfterThrowing注解的throwing属性 就可以将通知方法的某个参数指定为接收目标对象方法出现的异常的参数 */ @AfterThrowing(value = \u0026#34;pointCut()\u0026#34;, throwing = \u0026#34;ex\u0026#34;) public void afterThrowingMethod(JoinPoint joinPoint,Throwable ex) { Signature methodName = joinPoint.getSignature(); System.out.println(\u0026#34;Logger--\u0026gt;异常通知，方法名：\u0026#34;+methodName.getName() +\u0026#34;，异常：\u0026#34;+ex); } @Around(\u0026#34;pointCut()\u0026#34;) //环绕通知的方法的返回值一定要和目标对象方法的返回值一致 public Object aroundMethod(ProceedingJoinPoint joinPoint) { Object result = null; try { System.out.println(\u0026#34;环绕通知--\u0026gt;目标对象方法执行之前\u0026#34;); //表示目标对象方法的执行 result = joinPoint.proceed(); System.out.println(\u0026#34;环绕通知--\u0026gt;目标对象方法返回值之后\u0026#34;); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println(\u0026#34;环绕通知--\u0026gt;目标对象方法出现异常时\u0026#34;); } finally { System.out.println(\u0026#34;环绕通知--\u0026gt;目标对象方法执行完毕\u0026#34;); } return result; } } 在Spring的配置文件中配置：\n1 2 3 4 5 6 7 8 \u0026lt;!-- 基于注解的AOP的实现： 1、将目标对象和切面交给IOC容器管理（注解+扫描） 2、开启AspectJ的自动代理，为目标对象自动生成代理 3、将切面类通过注解@Aspect标识 --\u0026gt; \u0026lt;context:component-scan base-package=\u0026#34;com.atguigu.aop.annotation\u0026#34;\u0026gt;\u0026lt;/context:component-scan\u0026gt; \u0026lt;aop:aspectj-autoproxy /\u0026gt; 3.4.4、各种通知 前置通知：使用@Before注解标识，在被代理的目标方法前执行 返回通知：使用@AfterReturning注解标识，在被代理的目标方法成功结束后执行（寿终正寝） 异常通知：使用@AfterThrowing注解标识，在被代理的目标方法异常结束后执行（死于非命） 后置通知：使用@After注解标识，在被代理的目标方法最终结束后执行（盖棺定论） 环绕通知：使用@Around注解标识，使用try\u0026hellip;catch\u0026hellip;finally结构围绕整个被代理的目标方法，包 括上面四种通知对应的所有位置\n各种通知的执行顺序：\nSpring版本5.3.x以前： 前置通知 目标操作 后置通知 返回通知或异常通知 Spring版本5.3.x以后： 前置通知 目标操作 返回通知或异常通知 后置通知 3.4.5、切入点表达式语法 ①作用 ②语法细节 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限 在包名的部分，一个“”号只能代表包的层次结构中的一层，表示这一层是任意的。 例如：.Hello匹配com.Hello，不匹配com.atguigu.Hello 在包名的部分，使用“..”表示包名任意、包的层次深度任意 在类名的部分，类名部分整体用号代替，表示类名任意 在类名的部分，可以使用号代替类名的一部分 *例如：*Service匹配所有名称以Service结尾的类或接口 在方法名部分，可以使用号表示方法名任意 在方法名部分，可以使用号代替方法名的一部分 例如：*Operation匹配所有方法名以Operation结尾的方法 ​\t在方法参数列表部分，使用(..)表示参数列表任意 在方法参数列表部分，使用(int,..)表示参数列表以一个int类型的参数开头 在方法参数列表部分，基本数据类型和对应的包装类型是不一样的 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的 在方法返回值部分，如果想要明确指定一个返回值类型，那么必须同时写明权限修饰符 例如：execution(public int ..Service.(.., int)) 正确 例如：execution( int *..Service.(.., int)) 错误 3.4.6、重用切入点表达式 ①声明 1 2 @Pointcut(\u0026#34;execution(* com.atguigu.aop.annotation.*.*(..))\u0026#34;) public void pointCut(){} ②在同一个切面中使用 1 2 3 4 5 6 @Before(\u0026#34;pointCut()\u0026#34;) public void beforeMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println(\u0026#34;Logger--\u0026gt;前置通知，方法名：\u0026#34;+methodName+\u0026#34;，参数：\u0026#34;+args); } ③在不同切面中使用 1 2 3 4 5 6 @Before(\u0026#34;com.atguigu.aop.CommonPointCut.pointCut()\u0026#34;) public void beforeMethod(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println(\u0026#34;Logger--\u0026gt;前置通知，方法名：\u0026#34;+methodName+\u0026#34;，参数：\u0026#34;+args); } 3.4.7、获取通知的相关信息 ①获取连接点信息 获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参\n1 2 3 4 5 6 7 8 @Before(\u0026#34;execution(public int com.atguigu.aop.annotation.CalculatorImpl.*(..))\u0026#34;) public void beforeMethod(JoinPoint joinPoint){ //获取连接点的签名信息 String methodName = joinPoint.getSignature().getName(); //获取目标方法到的实参信息 String args = Arrays.toString(joinPoint.getArgs()); System.out.println(\u0026#34;Logger--\u0026gt;前置通知，方法名：\u0026#34;+methodName+\u0026#34;，参数：\u0026#34;+args); } ②获取目标方法的返回值 @AfterReturning中的属性returning，用来将通知方法的某个形参，接收目标方法的返回值\n1 2 3 4 5 @AfterReturning(value = \u0026#34;execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))\u0026#34;, returning = \u0026#34;result\u0026#34;) public void afterReturningMethod(JoinPoint joinPoint, Object result){ String methodName = joinPoint.getSignature().getName(); System.out.println(\u0026#34;Logger--\u0026gt;返回通知，方法名：\u0026#34;+methodName+\u0026#34;，结果：\u0026#34;+result); } ③获取目标方法的异常 @AfterThrowing中的属性throwing，用来将通知方法的某个形参，接收目标方法的异常\n1 2 3 4 5 @AfterThrowing(value = \u0026#34;execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))\u0026#34;, throwing = \u0026#34;ex\u0026#34;) public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){ String methodName = joinPoint.getSignature().getName(); System.out.println(\u0026#34;Logger--\u0026gt;异常通知，方法名：\u0026#34;+methodName+\u0026#34;，异常：\u0026#34;+ex); } 3.4.8、环绕通知 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Around(\u0026#34;execution(* com.atguigu.aop.annotation.CalculatorImpl.*(..))\u0026#34;) public Object aroundMethod(ProceedingJoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); Object result = null; try { System.out.println(\u0026#34;环绕通知--\u0026gt;目标对象方法执行之前\u0026#34;); //目标方法的执行，目标方法的返回值一定要返回给外界调用者 result = joinPoint.proceed(); System.out.println(\u0026#34;环绕通知--\u0026gt;目标对象方法返回值之后\u0026#34;); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println(\u0026#34;环绕通知--\u0026gt;目标对象方法出现异常时\u0026#34;); } finally { System.out.println(\u0026#34;环绕通知--\u0026gt;目标对象方法执行完毕\u0026#34;); } return result; } 3.4.9、切面的优先级 相同目标方法上同时存在多个切面时，切面的优先级控制切面的内外嵌套顺序。\n优先级高的切面：外面 优先级低的切面：里面 使用@Order注解可以控制切面的优先级：\n@Order(较小的数)：优先级高 @Order(较大的数)：优先级低 3.5，基于XML的AOP（了解） 3.5.1、准备工作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 package com.hbnu.spring.xml; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; import java.util.Arrays; /** * @Auther: 赵羽 * @Date: 2023/4/18 - 04 - 18 - 11:58 * @Description: com.hbnu.spring * @version: 1.0 * * */ @Component public class LoggerAspect { public void pointCut() {} public void beforeAdviceMethod(JoinPoint joinPoint) { //获取连接点所对应方法的签名信息 Signature methodName = joinPoint.getSignature(); //获取连接点所对应方法的参数 String args = Arrays.toString(joinPoint.getArgs()); System.out.println(\u0026#34;Logger--\u0026gt;前置通知，方法名：\u0026#34;+methodName.getName() +\u0026#34;，参数：\u0026#34;+ args); } public void afterAdviceMethod(JoinPoint joinPoint) { Signature methodName = joinPoint.getSignature(); System.out.println(\u0026#34;Logger--\u0026gt;后置通知，方法名：\u0026#34;+methodName.getName()); } public void afterReturningMethod(JoinPoint joinPoint,Object result) { Signature methodName = joinPoint.getSignature(); System.out.println(\u0026#34;Logger--\u0026gt;返回通知，方法名：\u0026#34;+methodName.getName() +\u0026#34;，结果：\u0026#34;+result); } public void afterThrowingMethod(JoinPoint joinPoint,Throwable ex) { Signature methodName = joinPoint.getSignature(); System.out.println(\u0026#34;Logger--\u0026gt;异常通知，方法名：\u0026#34;+methodName.getName() +\u0026#34;，异常：\u0026#34;+ex); } @Around(\u0026#34;pointCut()\u0026#34;) public Object aroundMethod(ProceedingJoinPoint joinPoint) { Object result = null; try { System.out.println(\u0026#34;环绕通知--\u0026gt;目标对象方法执行之前\u0026#34;); //表示目标对象方法的执行 result = joinPoint.proceed(); System.out.println(\u0026#34;环绕通知--\u0026gt;目标对象方法返回值之后\u0026#34;); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println(\u0026#34;环绕通知--\u0026gt;目标对象方法出现异常时\u0026#34;); } finally { System.out.println(\u0026#34;环绕通知--\u0026gt;目标对象方法执行完毕\u0026#34;); } return result; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.hbnu.spring.xml; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; /** * @Auther: 赵羽 * @Date: 2023/4/18 - 04 - 18 - 21:22 * @Description: com.hbnu.spring * @version: 1.0 */ @Component public class ValidateAspect { public void beforeMethod() { System.out.println(\u0026#34;ValidateAspect----》前置通知\u0026#34;); } } 3.5.2、实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:context=\u0026#34;http://www.springframework.org/schema/context\u0026#34; xmlns:aop=\u0026#34;http://www.springframework.org/schema/aop\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd\u0026#34;\u0026gt; \u0026lt;context:component-scan base-package=\u0026#34;com.hbnu.spring.xml\u0026#34;\u0026gt;\u0026lt;/context:component-scan\u0026gt; \u0026lt;aop:config\u0026gt; \u0026lt;!--设置一个公共的切入点表达式--\u0026gt; \u0026lt;aop:pointcut id=\u0026#34;pointCut\u0026#34; expression=\u0026#34;execution(* com.hbnu.spring.xml.CalculatorPureImpl.*(..))\u0026#34;/\u0026gt; \u0026lt;!--将IOC容器中的某个bean设置为切面--\u0026gt; \u0026lt;aop:aspect ref=\u0026#34;loggerAspect\u0026#34;\u0026gt; \u0026lt;aop:before method=\u0026#34;beforeAdviceMethod\u0026#34; pointcut-ref=\u0026#34;pointCut\u0026#34;\u0026gt;\u0026lt;/aop:before\u0026gt; \u0026lt;aop:after method=\u0026#34;afterAdviceMethod\u0026#34; pointcut-ref=\u0026#34;pointCut\u0026#34;\u0026gt;\u0026lt;/aop:after\u0026gt; \u0026lt;aop:after-returning method=\u0026#34;afterReturningMethod\u0026#34; returning=\u0026#34;result\u0026#34; pointcut-ref=\u0026#34;pointCut\u0026#34;\u0026gt;\u0026lt;/aop:after-returning\u0026gt; \u0026lt;aop:after-throwing method=\u0026#34;afterThrowingMethod\u0026#34; throwing=\u0026#34;ex\u0026#34; pointcut-ref=\u0026#34;pointCut\u0026#34;\u0026gt;\u0026lt;/aop:after-throwing\u0026gt; \u0026lt;aop:around method=\u0026#34;aroundMethod\u0026#34; pointcut-ref=\u0026#34;pointCut\u0026#34;\u0026gt;\u0026lt;/aop:around\u0026gt; \u0026lt;/aop:aspect\u0026gt; \u0026lt;aop:aspect ref=\u0026#34;validateAspect\u0026#34; order=\u0026#34;1\u0026#34;\u0026gt; \u0026lt;aop:before method=\u0026#34;beforeMethod\u0026#34; pointcut-ref=\u0026#34;pointCut\u0026#34;\u0026gt;\u0026lt;/aop:before\u0026gt; \u0026lt;/aop:aspect\u0026gt; \u0026lt;/aop:config\u0026gt; \u0026lt;/beans\u0026gt; 4、声明式事务 4.1、JdbcTemplate 4.1.1、简介 Spring 框架对 JDBC 进行封装，使用 JdbcTemplate 方便实现对数据库操作\n4.1.2、准备工作 ①加入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 \u0026lt;dependencies\u0026gt; \u0026lt;!-- 基于Maven依赖传递性，导入spring-context依赖即可导入当前所需所有jar包 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-context\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spring 持久化层支持jar包 --\u0026gt; \u0026lt;!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中，需要使用orm、jdbc、tx三个jar包 --\u0026gt; \u0026lt;!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-orm\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spring 测试相关 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-test\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- junit测试 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.12\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- MySQL驱动 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;8.0.16\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 数据源 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;druid\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.31\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; ②创建jdbc.properties 1 2 3 4 jdbc.driver = com.mysql.cj.jdbc.Driver jdbc.url = jdbc:mysql://localhost:3306/class2013?serverTimezone=UTC jdbc.username = root jdbc.password = 123456 ③配置Spring的配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;!-- 导入外部属性文件 --\u0026gt; \u0026lt;context:property-placeholder location=\u0026#34;classpath:jdbc.properties\u0026#34; /\u0026gt; \u0026lt;!-- 配置数据源 --\u0026gt; \u0026lt;bean id=\u0026#34;druidDataSource\u0026#34; class=\u0026#34;com.alibaba.druid.pool.DruidDataSource\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;${jdbc.url}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;driverClassName\u0026#34; value=\u0026#34;${jdbc.driver}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;${jdbc.username}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;${jdbc.password}\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- 配置 JdbcTemplate --\u0026gt; \u0026lt;bean id=\u0026#34;jdbcTemplate\u0026#34; class=\u0026#34;org.springframework.jdbc.core.JdbcTemplate\u0026#34;\u0026gt; \u0026lt;!-- 装配数据源 --\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;druidDataSource\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 4.1.3、测试 ①在测试类装配 JdbcTemplate 1 2 3 4 5 6 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(\u0026#34;classpath:spring-jdbc.xml\u0026#34;) public class JDBCTemplateTest { @Autowired private JdbcTemplate jdbcTemplate; } ②测试增删改功能 1 2 3 4 5 6 7 //测试增删改功能 @Test public void test() throws Exception { String sql = \u0026#34;insert into tb_user values (null,?,?,?,?,?)\u0026#34;; int result = jdbcTemplate.update(sql, \u0026#34;王望\u0026#34;, \u0026#34;1313\u0026#34;, 19, \u0026#34;男\u0026#34;, \u0026#34;123131@qq.com\u0026#34;); System.out.println(result); } ③查询一条数据为实体类对象 1 2 3 4 5 6 7 @Test //查询一条数据为一个实体类对象 public void getOneUser() throws Exception { String sql = \u0026#34;select * from tb_user where id = ?\u0026#34;; User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper\u0026lt;\u0026gt;(User.class), 1); System.out.println(user); } ④查询多条数据为一个list集合 1 2 3 4 5 6 7 @Test //查询多条数据为一个list集合 public void getManyUsers() throws Exception { String sql = \u0026#34;select * from tb_user\u0026#34;; List\u0026lt;User\u0026gt; list = jdbcTemplate.query(sql, new BeanPropertyRowMapper\u0026lt;\u0026gt;(User.class)); list.forEach(System.out::println); } ⑤查询单行单列的值 1 2 3 4 5 6 7 @Test //查询单行单列的值 public void getCountUsers() throws Exception { String sql = \u0026#34;select count(*) from tb_user\u0026#34;; Integer result = jdbcTemplate.queryForObject(sql, Integer.class); System.out.println(result); } 4.2、声明式事务概念 4.2.1、编程式事务 事务功能的相关操作全部通过自己编写代码来实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 Connection conn = ...; try { // 开启事务：关闭事务的自动提交 conn.setAutoCommit(false); // 核心操作 // 提交事务 conn.commit(); }catch(Exception e){ // 回滚事务 conn.rollBack(); }finally{ // 释放数据库连接 conn.close(); } 编程式的实现方式存在缺陷：\n细节没有被屏蔽：具体操作过程中，所有细节都需要程序员自己来完成，比较繁琐。 代码复用性不高：如果没有有效抽取出来，每次实现功能都需要自己编写代码，代码就没有得到复用。 4.2.2、声明式事务 既然事务控制的代码有规律可循，代码的结构基本是确定的，所以框架就可以将固定模式的代码抽取出来，进行相关的封装。\n封装起来后，我们只需要在配置文件中进行简单的配置即可完成操作。\n好处1：提高开发效率 好处2：消除了冗余的代码 好处3：框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题，进行了健壮性、性 能等各个方面的优化\n所以，我们可以总结下面两个概念：\n编程式：自己写代码实现功能 声明式：通过配置让框架实现功能 4.3、基于注解的声明式事务 4.3.1、准备工作 ①加入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 \u0026lt;dependencies\u0026gt; \u0026lt;!-- 基于Maven依赖传递性，导入spring-context依赖即可导入当前所需所有jar包 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-context\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spring 持久化层支持jar包 --\u0026gt; \u0026lt;!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中，需要使用orm、jdbc、tx三个jar包 --\u0026gt; \u0026lt;!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-orm\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spring 测试相关 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-test\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- junit测试 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.12\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- MySQL驱动 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;8.0.16\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 数据源 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;druid\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.31\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; ②创建jdbc.properties 1 2 3 4 jdbc.driver = com.mysql.cj.jdbc.Driver jdbc.url = jdbc:mysql://localhost:3306/class2013?serverTimezone=UTC jdbc.username = root jdbc.password = 123456 ③配置Spring的配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:context=\u0026#34;http://www.springframework.org/schema/context\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd\u0026#34;\u0026gt; \u0026lt;!--扫描组件--\u0026gt; \u0026lt;context:component-scan base-package=\u0026#34;com.hbnu.spring\u0026#34;\u0026gt;\u0026lt;/context:component-scan\u0026gt; \u0026lt;!-- 导入外部属性文件 --\u0026gt; \u0026lt;context:property-placeholder location=\u0026#34;classpath:jdbc.properties\u0026#34;\u0026gt;\u0026lt;/context:property-placeholder\u0026gt; \u0026lt;!-- 配置数据源 --\u0026gt; \u0026lt;bean id=\u0026#34;dataSource\u0026#34; class=\u0026#34;com.alibaba.druid.pool.DruidDataSource\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driverClassName\u0026#34; value=\u0026#34;${jdbc.driver}\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;${jdbc.url}\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;${jdbc.username}\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;${jdbc.password}\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- 配置 JdbcTemplate --\u0026gt; \u0026lt;bean id=\u0026#34;jdbcTemplate\u0026#34; class=\u0026#34;org.springframework.jdbc.core.JdbcTemplate\u0026#34;\u0026gt; \u0026lt;!-- 装配数据源 --\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/beans\u0026gt; ④创建表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CREATE TABLE `t_book` ( `book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;主键\u0026#39;, `book_name` varchar(20) DEFAULT NULL COMMENT \u0026#39;图书名称\u0026#39;, `price` int(11) DEFAULT NULL COMMENT \u0026#39;价格\u0026#39;, `stock` int(10) unsigned DEFAULT NULL COMMENT \u0026#39;库存（无符号）\u0026#39;, PRIMARY KEY (`book_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,\u0026#39;斗破苍穹\u0026#39;,80,100),(2,\u0026#39;斗罗大陆\u0026#39;,50,100); CREATE TABLE `t_user` ( `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;主键\u0026#39;, `username` varchar(20) DEFAULT NULL COMMENT \u0026#39;用户名\u0026#39;, `balance` int(10) unsigned DEFAULT NULL COMMENT \u0026#39;余额（无符号）\u0026#39;, PRIMARY KEY (`user_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; insert into `t_user`(`user_id`,`username`,`balance`) values (1,\u0026#39;admin\u0026#39;,50); ⑤创建组件 创建BookController：\n1 2 3 4 5 6 7 8 @Controller public class BookController { @Autowired private BookService bookService; public void buyBook(Integer bookId, Integer userId){ bookService.buyBook(bookId, userId); } } 创建接口BookService：\n1 2 3 public interface BookService { void buyBook(Integer bookId, Integer userId); } 创建实现类BookServiceImpl：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; @Override public void buyBook(Integer bookId, Integer userId) { //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); } } 创建接口BookDao：\n1 2 3 4 5 public interface BookDao { Integer getPriceByBookId(Integer bookId); void updateStock(Integer bookId); void updateBalance(Integer userId, Integer price); } 创建实现类BookDaoImpl：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Repository public class BookDaoImpl implements BookDao { @Autowired private JdbcTemplate jdbcTemplate; @Override public Integer getPriceByBookId(Integer bookId) { String sql = \u0026#34;select price from t_book where book_id = ?\u0026#34;; return jdbcTemplate.queryForObject(sql, Integer.class, bookId); } @Override public void updateStock(Integer bookId) { String sql = \u0026#34;update t_book set stock = stock - 1 where book_id = ?\u0026#34;; jdbcTemplate.update(sql, bookId); } @Override public void updateBalance(Integer userId, Integer price) { String sql = \u0026#34;update t_user set balance = balance - ? where user_id =?\u0026#34;; jdbcTemplate.update(sql, price, userId); } } 4.3.2、测试无事务情况 ①创建测试类 1 2 3 4 5 6 7 8 9 10 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(\u0026#34;classpath:tx-annotation.xml\u0026#34;) public class TxByAnnotationTest { @Autowired private BookController bookController; @Test public void testBuyBook(){ bookController.buyBook(1, 1); } } ②模拟场景 用户购买图书，先查询图书的价格，再更新图书的库存和用户的余额\n假设用户id为1的用户，购买id为1的图书\n用户余额为50，而图书价格为80\n购买图书之后，用户的余额为-30，数据库中余额字段设置了无符号，因此无法将-30插入到余额字段\n此时执行sql语句会抛出SQLException\n③观察结果 因为没有添加事务，图书的库存更新了，但是用户的余额没有更新\n显然这样的结果是错误的，购买图书是一个完整的功能，更新库存和更新余额要么都成功要么都失败\n4.3.3、加入事务 ①添加事务配置 在Spring的配置文件中添加配置：\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;bean id=\u0026#34;transactionManager\u0026#34; class=\u0026#34;org.springframework.jdbc.datasource.DataSourceTransactionManager\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- 开启事务的注解驱动 通过注解@Transactional所标识的方法或标识的类中所有的方法，都会被事务管理器管理事务 --\u0026gt; \u0026lt;!-- transaction-manager属性的默认值是transactionManager，如果事务管理器bean的id正好就 是这个默认值，则可以省略这个属性 --\u0026gt; \u0026lt;tx:annotation-driven transaction-manager=\u0026#34;transactionManager\u0026#34; /\u0026gt; 注意：导入的名称空间需要 tx 结尾的那个。\n②添加事务注解 因为service层表示业务逻辑层，一个方法表示一个完成的功能，因此处理事务一般在service层处理\n在BookServiceImpl的buybook()添加注解@Transactional\n③观察结果 由于使用了Spring的声明式事务，更新库存和更新余额都没有执行\n声明式事务的配置步骤：\n1.在spring的配置文件中配置事务管理器\n2.开启事务的注解驱动\n在需要被事务管理的方法上，添加@Transactional注解，该方法就会被事务管理\n@Transactional注解标识的位置：\n1.标识在方法上\n2.标识在类上，则类中所有的方法都会被事务管理\n4.3.4、@Transactional注解标识的位置 @Transactional标识在方法上，咋只会影响该方法\n@Transactional标识的类上，咋会影响类中所有的方法\n4.3.5、事务属性：只读 ①介绍 对一个查询操作来说，如果我们把它设置成只读，就能够明确告诉数据库，这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。\n②使用方式 1 2 3 4 5 6 7 8 9 10 @Transactional(readOnly = true) public void buyBook(Integer bookId, Integer userId) { //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); //System.out.println(1/0); } ③注意 对增删改操作设置只读会抛出下面异常：\nCaused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification\nare not allowed\n4.3.6、事务属性：超时 ①介绍 事务在执行过程中，有可能因为遇到某些问题，导致程序卡住，从而长时间占用数据库资源。而长时间占用资源，大概率是因为程序运行出现了问题（可能是Java程序或MySQL数据库或网络连接等等）。\n此时这个很可能出问题的程序应该被回滚，撤销它已做的操作，事务结束，把资源让出来，让其他正常程序可以执行。\n概括来说就是一句话：超时回滚，释放资源。\n②使用方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Transactional(timeout = 3) public void buyBook(Integer bookId, Integer userId) { try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); //System.out.println(1/0); } ③观察结果 执行过程中抛出异常：\norg.springframework.transaction.TransactionTimedOutException: Transaction timed out:\ndeadline was Fri Jun 04 16:25:39 CST 2022\n4.3.7、事务属性：回滚策略 ①介绍 声明式事务默认只针对运行时异常回滚，编译时异常不回滚。\n可以通过@Transactional中相关属性设置回滚策略\nrollbackFor属性：需要设置一个Class类型的对象 rollbackForClassName属性：需要设置一个字符串类型的全类名 noRollbackFor属性：需要设置一个Class类型的对象 rollbackFor属性：需要设置一个字符串类型的全类名 ②使用方式 1 2 3 4 5 6 7 8 9 10 11 @Transactional(noRollbackFor = ArithmeticException.class) //@Transactional(noRollbackForClassName = \u0026#34;java.lang.ArithmeticException\u0026#34;) public void buyBook(Integer bookId, Integer userId) { //查询图书的价格 Integer price = bookDao.getPriceByBookId(bookId); //更新图书的库存 bookDao.updateStock(bookId); //更新用户的余额 bookDao.updateBalance(userId, price); System.out.println(1/0); } ③观察结果 虽然购买图书功能中出现了数学运算异常（ArithmeticException），但是我们设置的回滚策略是，当\n出现ArithmeticException不发生回滚，因此购买图书的操作正常执行\n4.3.8、事务属性：事务隔离级别 ①介绍 数据库系统必须具有隔离并发运行各个事务的能力，使它们不会相互影响，避免各种并发问题。一个事\n务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别，不同隔离级别对应不同\n的干扰程度，隔离级别越高，数据一致性就越好，但并发性越弱。\n隔离级别一共有四种：\n读未提交：READ UNCOMMITTED 允许Transaction01读取Transaction02未提交的修改。\n读已提交：READ COMMITTED、 要求Transaction01只能读取Transaction02已提交的修改。\n可重复读：REPEATABLE READ 确保Transaction01可以多次从一个字段中读取到相同的值，即Transaction01执行期间禁止其它\n事务对这个字段进行更新。\n串行化：SERIALIZABLE 确保Transaction01可以多次从一个表中读取到相同的行，在Transaction01执行期间，禁止其它\n事务对这个表进行添加、更新、删除操作。可以避免任何并发问题，但性能十分低下。\n各个隔离级别解决并发问题的能力见下表：\n隔离级别 脏读 不可重复读 幻读 READ UNCOMMITTED 有 有 有 READ COMMITTED 无 有 有 REPEATABLE READ 无 无 有 SERIALIZABLE 无 无 无 各种数据库产品对事务隔离级别的支持程度：\n隔离级别 Oracle MySQL READ UNCOMMITTED × √ READ COMMITTED √(默认) √ REPEATABLE READ × √(默认) SERIALIZABLE √ √ ②使用方式 1 2 3 4 5 @Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别 @Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交 @Transactional(isolation = Isolation.READ_COMMITTED)//读已提交 @Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读 @Transactional(isolation = Isolation.SERIALIZABLE)//串行化 4.3.9、事务属性：事务传播行为 ①介绍 当事务方法被另一个事务方法调用时，必须指定事务应该如何传播。例如：方法可能继续在现有事务中运行，也可能开启一个新事务，并在自己的事务中运行。\n②测试 创建接口CheckoutService：\n1 2 3 public interface CheckoutService { void checkout(Integer[] bookIds, Integer userId); } 创建实现类CheckoutServiceImpl：\n1 2 3 4 5 6 7 8 9 10 11 12 13 @Service public class CheckoutServiceImpl implements CheckoutService { @Autowired private BookService bookService; @Override @Transactional //一次购买多本图书 public void checkout(Integer[] bookIds, Integer userId) { for (Integer bookId : bookIds) { bookService.buyBook(bookId, userId); } } } 在BookController中添加方法：\n1 2 3 4 5 @Autowired private CheckoutService checkoutService; public void checkout(Integer[] bookIds, Integer userId){ checkoutService.checkout(bookIds, userId); } 在数据库中将用户的余额修改为100元\n③观察结果 可以通过@Transactional中的propagation属性设置事务传播行为\n修改BookServiceImpl中buyBook()上，注解@Transactional的propagation属性\n@Transactional(propagation = Propagation.REQUIRED)，默认情况，表示如果当前线程上有已经开\n启的事务可用，那么就在这个事务中运行。经过观察，购买图书的方法buyBook()在checkout()中被调\n用，checkout()上有事务注解，因此在此事务中执行。所购买的两本图书的价格为80和50，而用户的余额为100，因此在购买第二本图书时余额不足失败，导致整个checkout()回滚，即只要有一本书买不\n了，就都买不了\n@Transactional(propagation = Propagation.REQUIRES_NEW)，表示不管当前线程上是否有已经开启的事务，都要开启新事务。同样的场景，每次购买图书都是在buyBook()的事务中执行，因此第一本图书购买成功，事务结束，第二本图书购买失败，只在第二次的buyBook()中回滚，购买第一本图书不受影响，即能买几本就买几本\n4.4、基于XML的声明式事务 4.3.1、场景模拟 参考基于注解的声明式事务\n4.3.2、修改Spring配置文件 将Spring配置文件中去掉tx:annotation-driven 标签，并添加配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 \u0026lt;aop:config\u0026gt; \u0026lt;!-- 配置事务通知和切入点表达式 --\u0026gt; \u0026lt;aop:advisor advice-ref=\u0026#34;txAdvice\u0026#34; pointcut=\u0026#34;execution(*com.atguigu.spring.tx.xml.service.impl.*.*(..))\u0026#34;\u0026gt;\u0026lt;/aop:advisor\u0026gt; \u0026lt;/aop:config\u0026gt; \u0026lt;!-- tx:advice标签：配置事务通知 --\u0026gt; \u0026lt;!-- id属性：给事务通知标签设置唯一标识，便于引用 --\u0026gt; \u0026lt;!-- transaction-manager属性：关联事务管理器 --\u0026gt; \u0026lt;tx:advice id=\u0026#34;txAdvice\u0026#34; transaction-manager=\u0026#34;transactionManager\u0026#34;\u0026gt; \u0026lt;tx:attributes\u0026gt; \u0026lt;!-- tx:method标签：配置具体的事务方法 --\u0026gt; \u0026lt;!-- name属性：指定方法名，可以使用星号代表多个字符 --\u0026gt; \u0026lt;tx:method name=\u0026#34;get*\u0026#34; read-only=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;tx:method name=\u0026#34;query*\u0026#34; read-only=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;tx:method name=\u0026#34;find*\u0026#34; read-only=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;!-- read-only属性：设置只读属性 --\u0026gt; \u0026lt;!-- rollback-for属性：设置回滚的异常 --\u0026gt; \u0026lt;!-- no-rollback-for属性：设置不回滚的异常 --\u0026gt; \u0026lt;!-- isolation属性：设置事务的隔离级别 --\u0026gt; \u0026lt;!-- timeout属性：设置事务的超时属性 --\u0026gt; \u0026lt;!-- propagation属性：设置事务的传播行为 --\u0026gt; \u0026lt;tx:method name=\u0026#34;save*\u0026#34; read-only=\u0026#34;false\u0026#34; rollback-for=\u0026#34;java.lang.Exception\u0026#34; propagation=\u0026#34;REQUIRES_NEW\u0026#34;/\u0026gt; \u0026lt;tx:method name=\u0026#34;update*\u0026#34; read-only=\u0026#34;false\u0026#34; rollback-for=\u0026#34;java.lang.Exception\u0026#34; propagation=\u0026#34;REQUIRES_NEW\u0026#34;/\u0026gt; \u0026lt;tx:method name=\u0026#34;delete*\u0026#34; read-only=\u0026#34;false\u0026#34; rollback-for=\u0026#34;java.lang.Exception\u0026#34; propagation=\u0026#34;REQUIRES_NEW\u0026#34;/\u0026gt; \u0026lt;/tx:attributes\u0026gt; \u0026lt;/tx:advice\u0026gt; 注意：基于xml实现的声明式事务，必须引入aspectJ的依赖\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-aspects\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; ","permalink":"https://ktzxy.top/posts/c61l4190ao/","summary":"Spring","title":"Spring"},{"content":"数据链路层 链路和数据链路 链路 一条点到点的物理线路段，中间没有其它的交换节点，一条链路只是一条通路的一个组成部分\n数据链路 除物理链路外，还必须有通信协议来控制这些数据的传输，若把实现这些协议的硬件和软件加到链路上，就构成了数据链路。现常见的方法就是使用适配器（网卡）来实现这些协议的硬件和软件。\n数据链路层相当于一个管道，传输的是一条条的帧信息\n数据链路层的主要功能 封装成帧 透明传输 差错控制 封装成帧 封装成帧就是一段数据的前后分别添加首部和尾部，然后构成一个帧，确定帧的界限\n首部和尾部的一个重要作用就是进行帧定界\n下图是使用控制字符进行帧定界的方法举例\n若帧数据部分有 开始标记 和 结束标记怎么办？\n发送端的数据链路层在数据中出现控制字符 “SOH” 或 “EOT”的前面插入一个转移字符 “ESC”\n这个时候就需要用到字节填充 或 字符填充，也就是接收端的数据链路层在数据送往网络层之间删除插入的转义字符。\n如果转移字符也出现在数据中，那么应该在转义字符的前面在插入一个转义字符。当接收端连续收到两个转义字符的时候，就删除掉前面的一个\n差错控制 传输的过程中可能会产生比特差错,1可能变成0，而0也可能变成1。\n在一段时间内，传输错误的比特占所传输比特总数的比率称为 误码率 BER（Bit error Rate），其中误码率和信噪比有很大的关系。\nCPC循环冗余检验 CRC是一种常用的检错方法，而FCS是添加在数据后面的冗余码\nFCS可以用CRC这种方法得出，并且CRC是用来获取FCS的唯一方法\n我们得到 FCS后，将其拼接到我们传送数据的末端\n接收端对收到的每一帧进行CRC检验\n若得出的余数 R = 0，则判定这个帧没有差错，就接受 若余数不为0，则判定有差错，丢弃 特点\n这种差错检测的方法不能确定是哪一位出现了问题 只需使用足够的余数，就能检测出来 两种情况的数据链路 点到点：PPP协议（全世界使用最多的），只实现无差错接收 零比特填充：5个连续的1，后面添加一个0 局域网的拓扑结构 CSMA/CD 多点接入：表示许多计算机以多点接入的方式连接在一根总线上 载波监听：是指每一站在发送数据之前，先检测一下总线上是否有其它数据 碰撞检测 碰撞检测就是计算机发送数据时，检测链路上信号电压的大小\n使用CSMA/CD协议的以太网不能进行全双工通信，而只能进行双向交替通信（半双工通信），每个站发送数据之后一小段时间，可能会遭遇碰撞的可能性。\n发生碰撞的站停止发送数据后，需要延迟一个随机时间 才能再发送数据，确定基本退避时间，一般是取2T，当重传16次后，仍然不成功，即丢弃该帧，并向高层报告。\n以太网 只要满足载波监听，多路访问的都是以太网。\n同时局域网数据链路层拆分成了两个子层\n逻辑链路控制LLC子层 媒体接入控制MAC子层，现在的网卡只有MAC子层 以太网提供的服务是不可靠的交付，即尽最大努力的交付。\n信道利用率 $$ \\partial=\\frac{\\tau}{T_{0}} $$流量控制协议 单工停等协议 stop and wait，链路不出错，但有可能出现流量不匹配的情况，发送方每发一帧停下来，每收到一帧后，上交网络层，再发一个确认给发送方，表示收到。发送方收到确认在发送下一个。\n连续ARQ 自动请求重发协议。\n发送端：在发送完一个数据帧后，不是停下来等待应答帧，而是可以连接再发送下面的数据帧。如果这时收到了接收端发来的确认帧，那么还可以接着再发送下面的数据帧。如果超时时间到，仍然没有收到相应的确认帧，则重新从这个帧开始重传。（go back N ARQ） 接收端：连续接收帧，当接收到一个坏帧时，简单丢弃这个帧和这个帧以后的所有帧，让他们在发送端超时，这道收到这个帧为止。 停止等待协议和连续ARQ协议的问题 停止等待协议 发送 - 停止 - 等待，效率较低，当传播时间比发送时间大得多时，性能变得不可接受\n连续ARQ协议 未经确定的帧一次传送过多，如果出错，重传的代价太大 序号站的位数过多，影响效率，一次能传送1024个帧，10位编号 实际协议中，一次连续传输的帧的个数是有限的 滑动窗口协议 它是停止等待协议 和 连续ARQ协议的折中\n一次发送为确定的帧的个数是有限的\n发送端：一次发送未经确定的帧是收到发送窗口的控制的，只有落在发送窗口的帧才是可以发送的 接收端：只有落在接收窗口的帧才是可以接收的。 出现差错的处理办法\n一段收到出错的帧后进行丢弃，不发送确定报文，让发送方超时重发。对后面陆续到达的正确的帧进行同样的处理办法\n当wr = 1时 接收方：全部丢弃（drop），链路层只按顺序接收帧 发送方：2号帧超时后，从2号帧开始发送 回退n帧，（go back N protocol） 当 wr \u0026gt; 1时 接收方：陆续接收出错的后续各个帧，但不提交给网络层，知道接收到2号帧以后，加上以后存储的各帧，按顺序交给网络层 发送方：2号帧超时后，发完2号帧之后，从第6号帧开始，选择性重传 ","permalink":"https://ktzxy.top/posts/9x2t681gmx/","summary":"数据链路层","title":"数据链路层"},{"content":"正则表达式 RegExp( regular expression ) 表达式全集 字符 描述 \\ 将下一个字符标记为一个特殊字符、或一个原义字符、或一个向后引用、或一个八进制转义符。例如，“n”匹配字符“n”。“\\n”匹配一个换行符。串行“\\\\”匹配“\\”而“\\(”则匹配“(”。 ^ 匹配输入字符串的开始位置。如果设置了RegExp对象的Multiline属性，^也匹配“\\n”或“\\r”之后的位置。 $ 匹配输入字符串的结束位置。如果设置了RegExp对象的Multiline属性，$也匹配“\\n”或“\\r”之前的位置。 * 匹配前面的子表达式零次或多次。例如，zo*能匹配“z”以及“zoo”。*等价于{0,}。 + 匹配前面的子表达式一次或多次。例如，“zo+”能匹配“zo”以及“zoo”，但不能匹配“z”。+等价于{1,}。 ? 匹配前面的子表达式零次或一次。例如，“do(es)?”可以匹配“does”或“does”中的“do”。?等价于{0,1}。 {n} n是一个非负整数。匹配确定的n次。例如，“o{2}”不能匹配“Bob”中的“o”，但是能匹配“food”中的两个o。 {n,} n是一个非负整数。至少匹配n次。例如，“o{2,}”不能匹配“Bob”中的“o”，但能匹配“foooood”中的所有o。“o{1,}”等价于“o+”。“o{0,}”则等价于“o*”。 {n,m} m和n均为非负整数，其中n\u0026lt;=m。最少匹配n次且最多匹配m次。例如，“o{1,3}”将匹配“fooooood”中的前三个o。“o{0,1}”等价于“o?”。请注意在逗号和两个数之间不能有空格。 ? 当该字符紧跟在任何一个其他限制符（*,+,?，{n}，{n,}，{n,m}）后面时，匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串，而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如，对于字符串“oooo”，“o+?”将匹配单个“o”，而“o+”将匹配所有“o”。 . 匹配除“\\n”之外的任何单个字符。要匹配包括“\\n”在内的任何字符，请使用像“`(. (pattern) 匹配pattern并获取这一匹配。所获取的匹配可以从产生的Matches集合得到，在VBScript中使用SubMatches集合，在JScript中则使用$0…$9属性。要匹配圆括号字符，请使用“\\(”或“\\)”。 (?:pattern) 匹配pattern但不获取匹配结果，也就是说这是一个非获取匹配，不进行存储供以后使用。这在使用或字符“`( (?=pattern) 正向肯定预查，在任何匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配，也就是说，该匹配不需要获取供以后使用。例如，“`Windows(?=95 (?!pattern) 正向否定预查，在任何不匹配pattern的字符串开始处匹配查找字符串。这是一个非获取匹配，也就是说，该匹配不需要获取供以后使用。例如“`Windows(?!95 (?\u0026lt;=pattern) 反向肯定预查，与正向肯定预查类拟，只是方向相反。例如，“`(?\u0026lt;=95 (?\u0026lt;!pattern) 反向否定预查，与正向否定预查类拟，只是方向相反。例如“(?”能匹配“3.1Windows”中的“Windows”，但不能匹配“2000Windows”中的“Windows`”。 x|y 匹配x或y。例如，“`z [xyz] 字符集合。匹配所包含的任意一个字符。例如，“[abc]”可以匹配“plain”中的“a”。 [^xyz] 负值字符集合。匹配未包含的任意字符。例如，“[^abc]”可以匹配“plain”中的“p”。 [a-z] 字符范围。匹配指定范围内的任意字符。例如，“[a-z]”可以匹配“a”到“z”范围内的任意小写字母字符。 [^a-z] 负值字符范围。匹配任何不在指定范围内的任意字符。例如，“[^a-z]”可以匹配任何不在“a”到“z”范围内的任意字符。 \\b 匹配一个单词边界，也就是指单词和空格间的位置。例如，“er\\b”可以匹配“never”中的“er”，但不能匹配“verb”中的“er”。 \\B 匹配非单词边界。“er\\B”能匹配“verb”中的“er”，但不能匹配“never”中的“er”。 \\cx 匹配由x指明的控制字符。例如，\\cM匹配一个Control-M或回车符。x的值必须为A-Z或a-z之一。否则，将c视为一个原义的“c”字符。 \\d 匹配一个数字字符。等价于[0-9]。 \\D 匹配一个非数字字符。等价于[^0-9]。 \\f 匹配一个换页符。等价于\\x0c和\\cL。 \\n 匹配一个换行符。等价于\\x0a和\\cJ。 \\r 匹配一个回车符。等价于\\x0d和\\cM。 \\s 匹配任何空白字符，包括空格、制表符、换页符等等。等价于[ \\f\\n\\r\\t\\v]。 \\S 匹配任何非空白字符。等价于[^ \\f\\n\\r\\t\\v]。 \\t 匹配一个制表符。等价于\\x09和\\cI。 \\v 匹配一个垂直制表符。等价于\\x0b和\\cK。 \\w 匹配包括下划线的任何单词字符。等价于“[A-Za-z0-9_]”。 \\W 匹配任何非单词字符。等价于“[^A-Za-z0-9_]”。 \\xn 匹配n，其中n为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如，“\\x41”匹配“A”。“\\x041”则等价于“\\x04\u0026amp;1”。正则表达式中可以使用ASCII编码。. *num* 匹配num，其中num是一个正整数。对所获取的匹配的引用。例如，“(.)\\1”匹配两个连续的相同字符。 *n* 标识一个八进制转义值或一个向后引用。如果*n之前至少n个获取的子表达式，则n为向后引用。否则，如果n为八进制数字（0-7），则n*为一个八进制转义值。 *nm* 标识一个八进制转义值或一个向后引用。如果*nm之前至少有nm个获得子表达式，则nm为向后引用。如果*nm之前至少有n个获取，则n为一个后跟文字m的向后引用。如果前面的条件都不满足，若n和m均为八进制数字（0-7），则*nm将匹配八进制转义值nm*。 *nml* 如果n为八进制数字（0-3），且m和l均为八进制数字（0-7），则匹配八进制转义值nml。 \\un 匹配n，其中n是一个用四个十六进制数字表示的Unicode字符。例如，\\u00A9匹配版权符号（©）。 常用正则表达式 用户名 /^[a-z0-9_-]{3,16}$/ 密码 /^[a-z0-9_-]{6,18}$/ 十六进制值 /^#?([a-f0-9]{6}|[a-f0-9]{3})$/ 电子邮箱 /^([a-z0-9_.-]+)@([\\da-z.-]+).([a-z.]{2,6})$/ /^[a-z\\d]+(\\.[a-z\\d]+)*@([\\da-z](-[\\da-z])?)+(\\.{1,2}[a-z]+)+$/ URL /^(https?://)?([\\da-z.-]+).([a-z.]{2,6})([/\\w .-])/?$/ IP 地址 /((2[0-4]\\d|25[0-5]|[01]?\\d\\d?).){3}(2[0-4]\\d|25[0-5]|[01]?\\d\\d?)/ /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ HTML 标签 /^\u0026lt;([a-z]+)([^\u0026lt;]+)(?:\u0026gt;(.)\u0026lt;/\\1\u0026gt;|\\s+/\u0026gt;)$/ 删除代码\\注释 (?\u0026lt;!http:|\\S)//.*$ Unicode编码中的汉字范围 /^[\\u2E80-\\u9FFF]+$/ 贪婪匹配和非贪婪匹配 默认是贪婪模式；在量词后面直接加上一个问号？就是非贪婪模式。\n常用的量词有：\n{m,n} {m,} ? * + 这些默认都是贪婪模式，若改成非贪婪模式，只需这样：\n{m,n}?\n{m,}?\n??\n*?\n+?\n1 2 3 \u0026lt;div\u0026gt;.*\u0026lt;/div\u0026gt; 非贪婪模式的匹配表达式为： \u0026lt;div\u0026gt;.*?\u0026lt;/div\u0026gt; ","permalink":"https://ktzxy.top/posts/8r9yv97ds9/","summary":"RegExp","title":"RegExp"},{"content":"1. 背景 在软件需求、开发、测试过程中，有时候需要使用一些测试数据，针对这种情况，我们一般要么使用已有的系统数据，要么需要手动制造一些数据。由于现在的业务系统数据多种多样，千变万化。在手动制造数据的过程中，可能需要花费大量精力和工作量，此项工作既繁复又容易出错，比如要构造一批用户三要素(姓名、手机号、身份证)、构造一批银行卡数据、或构造一批地址通讯录等。\n这时候，人们常常为了偷懒快捷，测试数据大多数可能是类似这样子的:\n1 2 3 4 测试, 1300000 000123456 张三, 1310000 000123456 李四, 1320000 000234567 王五, 1330000 000345678 测试数据中包括了大量的“测试XX”，要么就是随手在键盘上一顿乱敲，都是些无意义的假数据。\n你是不是这样做的呢？坦白的说，有过一段时间，笔者偶尔也是这么干的。\n但是，细想一下，这样的测试数据，不仅要自己手动敲，还假的不能再假，浪费时间、浪费人力、数据价值低。\n而且，部分数据的手工制造还无法保障：比如UUID类数据、MD5、SHA加密类数据等。\n为了帮助大家解决这个问题，更多还是提供种一种解决方案或思路，今天给大家分享一款Python造数据利器：Faker库，利用它可以生成一批各种各样的看起来“像真的一样”的假数据。\n2. Faker介绍 、安装 2.1 Faker是什么 Faker是一个Python包，主要用来创建伪数据，使用Faker包，无需再手动生成或者手写随机数来生成数据，只需要调用Faker提供的方法，即可完成数据的生成。\n项目地址：https://github.com/joke2k/faker\n2.2 安装 安装 Faker 很简单，使用 pip 方式安装：\n1 pip install Faker 除了pip 安装，也可以通过上方提供的github地址，来下载编译安装。\n1 2 3 4 5 6 7 8 9 10 11 (py3_env) ➜ py3_env pip show faker Name: Faker Version: 4.1.1 Summary: Faker is a Python package that generates fake data for you. Home-page: https://github.com/joke2k/faker Author: joke2k Author-email: joke2k@gmail.com License: MIT License Location: /Users/xxx/work_env/py3_env/lib/python3.7/site-packages Requires: python-dateutil, text-unidecode Required-by: 3. Faker常用使用 3.1 基本用法\nFaker 的使用也是很简单的，从 faker 模块中导入类，然后实例化这个类，就可以调用方法使用了：\n1 2 3 4 5 6 7 8 9 10 11 12 from faker import Faker fake = Faker() name = fake.name() address = fake.address() print(name) print(address) # 输出信息 Ashley Love 074 Lee Village Suite 464 Dawnborough, RI 44234 这里我们造了一个名字和一个地址，由于 Faker 默认是英文数据，所以如果我们需要造其他语言的数据，可以使用 locale参数，例如：\n1 2 3 4 5 6 7 8 9 10 11 from faker import Faker fake = Faker(locale=\u0026#39;zh_CN\u0026#39;) name = fake.name() address = fake.address() print(name) print(address) # 输出信息 张艳 海南省上海市朝阳邱路y座 175208 是不是看起来还不错，但是有一点需要注意，这里的地址并不是真实的地址，而是随机组合出来的，也就是将省、市、道路之类的随机组合在一起。\n这里介绍几个比较常见的语言代号：\n简体中文：zh_CN 繁体中文：zh_TW 美国英文：en_US 英国英文：en_GB 德文：de_DE 日文：ja_JP 韩文：ko_KR 法文：fr_FR 例如将语言修改为繁体中文fake = Faker(locale='zh_TW')，输出信息为：\n1 2 楊志宏 100 中壢博愛街10號9樓 3.2 常用函数 除了上述介绍的fake.name和fake.address生成姓名和地址两个函数外，常用的faker函数按类别划分有如下一些常用方法。\n1、地理信息类\nfake.city_suffix()：市，县 fake.country()：国家 fake.country_code()：国家编码 fake.district()：区 fake.geo_coordinate()：地理坐标 fake.latitude()：地理坐标(纬度) fake.longitude()：地理坐标(经度) fake.postcode()：邮编 fake.province()：省份 fake.address()：详细地址 fake.street_address()：街道地址 fake.street_name()：街道名 fake.street_suffix()：街、路 2、基础信息类\nssn()：生成身份证号 bs()：随机公司服务名 company()：随机公司名（长） company_prefix()：随机公司名（短） company_suffix()：公司性质 credit_card_expire()：随机信用卡到期日 credit_card_full()：生成完整信用卡信息 credit_card_number()：信用卡号 credit_card_provider()：信用卡类型 credit_card_security_code()：信用卡安全码 job()：随机职位 first_name_female()：女性名 first_name_male()：男性名 last_name_female()：女姓 last_name_male()：男姓 name()：随机生成全名 name_female()：男性全名 name_male()：女性全名 phone_number()：随机生成手机号 phonenumber_prefix()：随机生成手机号段 3、计算机基础、Internet信息类\nascii_company_email()：随机ASCII公司邮箱名 ascii_email()：随机ASCII邮箱： company_email()： email()： safe_email()：安全邮箱 4、网络基础信息类\ndomain_name()：生成域名 domain_word()：域词(即，不包含后缀) ipv4()：随机IP4地址 ipv6()：随机IP6地址 mac_address()：随机MAC地址 tld()：网址域名后缀(.com,.net.cn,等等，不包括.) uri()：随机URI地址 uri_extension()：网址文件后缀 uri_page()：网址文件（不包含后缀） uri_path()：网址文件路径（不包含文件名） url()：随机URL地址 user_name()：随机用户名 image_url()：随机URL地址 5、浏览器信息类\nchrome()：随机生成Chrome的浏览器user_agent信息 firefox()：随机生成FireFox的浏览器user_agent信息 internet_explorer()：随机生成IE的浏览器user_agent信息 opera()：随机生成Opera的浏览器user_agent信息 safari()：随机生成Safari的浏览器user_agent信息 linux_platform_token()：随机Linux信息 user_agent()：随机user_agent信息 6、数字类\nnumerify()：三位随机数字\nrandom_digit()：0~9随机数\nrandom_digit_not_null()：1~9的随机数\nrandom_int()：随机数字，默认0~9999，可以通过设置min,max来设置\nrandom_number()：随机数字，参数digits设置生成的数字位数\npyfloat()：\nleft_digits=5 #生成的整数位数, right_digits=2 #生成的小数位数, positive=True #是否只有正数\npyint()：随机Int数字（参考random_int()参数）\npydecimal()：随机Decimal数字（参考pyfloat参数）\n7、文本、加密类\npystr()：随机字符串 random_element()：随机字母 random_letter()：随机字母 paragraph()：随机生成一个段落 paragraphs()：随机生成多个段落，通过参数nb来控制段落数，返回数组 sentence()：随机生成一句话 sentences()：随机生成多句话，与段落类似 text()：随机生成一篇文章（不要幻想着人工智能了，至今没完全看懂一句话是什么意思） word()：随机生成词语 words()：随机生成多个词语，用法与段落，句子，类似 binary()：随机生成二进制编码 boolean()：True/False language_code()：随机生成两位语言编码 locale()：随机生成语言/国际 信息 md5()：随机生成MD5 null_boolean()：NULL/True/False password()：随机生成密码,可选参数：length：密码长度；special_chars：是否能使用特殊字符；digits：是否包含数字；upper_case：是否包含大写字母；lower_case：是否包含小写字母 sha1()：随机SHA1 sha256()：随机SHA256 uuid4()：随机UUID 8、时间信息类\ndate()：随机日期 date_between()：随机生成指定范围内日期，参数：start_date，end_date date_between_dates()：随机生成指定范围内日期，用法同上 date_object()：随机生产从1970-1-1到指定日期的随机日期。 date_time()：随机生成指定时间（1970年1月1日至今） date_time_ad()：生成公元1年到现在的随机时间 date_time_between()：用法同dates future_date()：未来日期 future_datetime()：未来时间 month()：随机月份 month_name()：随机月份（英文） past_date()：随机生成已经过去的日期 past_datetime()：随机生成已经过去的时间 time()：随机24小时时间 timedelta()：随机获取时间差 time_object()：随机24小时时间，time对象 time_series()：随机TimeSeries对象 timezone()：随机时区 unix_time()：随机Unix时间 year()：随机年份 9、python 相关方法\nprofile()：随机生成档案信息 simple_profile()：随机生成简单档案信息 pyiterable() pylist() pyset() pystruct() pytuple() pydict() 可以用dir(fake)，看Faker库都可以fake哪些数据，目前Faker支持近300种数据，此外还支持自己进行扩展。\n有了这些生成数据函数之后用fake对象就可以调用不同的方法生成各种数据了。\n3.3 常用数据场景 1、构造通讯录记录\n1 2 3 4 5 6 7 8 9 10 11 12 from faker import Faker fake = Faker(locale=\u0026#39;zh_CN\u0026#39;) for _ in range(5): print(\u0026#39;姓名：\u0026#39;, fake.name(), \u0026#39; 手机号：\u0026#39;, fake.phone_number()) # 输出信息： 姓名： 骆柳 手机号： 18674751460 姓名： 薛利 手机号： 13046558454 姓名： 翟丽丽 手机号： 15254904803 姓名： 宋秀珍 手机号： 13347585045 姓名： 孔桂珍 手机号： 18258911504 2、构造信用卡数据\n1 2 3 4 5 6 7 8 9 10 11 12 13 from faker import Faker fake = Faker(locale=\u0026#39;zh_CN\u0026#39;) print(\u0026#39;Card Number:\u0026#39;, fake.credit_card_number(card_type=None)) print(\u0026#39;Card Provider:\u0026#39;, fake.credit_card_provider(card_type=None)) print(\u0026#39;Card Security Code:\u0026#39;, fake.credit_card_security_code(card_type=None)) print(\u0026#39;Card Expire:\u0026#39;, fake.credit_card_expire()) # 输出信息： Card Number: 676181530350 Card Provider: Diners Club / Carte Blanche Card Security Code: 615 Card Expire: 09/21 3、生成个人档案信息\n1 2 3 4 5 6 7 from faker import Faker fake = Faker(locale=\u0026#39;zh_CN\u0026#39;) print(fake.profile()) # 输出信息 {\u0026#39;job\u0026#39;: \u0026#39;美术指导\u0026#39;, \u0026#39;company\u0026#39;: \u0026#39;易动力传媒有限公司\u0026#39;, \u0026#39;ssn\u0026#39;: \u0026#39;370703197807179500\u0026#39;, \u0026#39;residence\u0026#39;: \u0026#39;广西壮族自治区旭县蓟州东莞街L座 784064\u0026#39;, \u0026#39;current_location\u0026#39;: (Decimal(\u0026#39;78.3608745\u0026#39;), Decimal(\u0026#39;-95.946407\u0026#39;)), \u0026#39;blood_group\u0026#39;: \u0026#39;B+\u0026#39;, \u0026#39;website\u0026#39;: [\u0026#39;https://www.jiewang.org/\u0026#39;, \u0026#39;https://www.longsong.cn/\u0026#39;, \u0026#39;https://jingyong.net/\u0026#39;, \u0026#39;https://58.cn/\u0026#39;], \u0026#39;username\u0026#39;: \u0026#39;qinqiang\u0026#39;, \u0026#39;name\u0026#39;: \u0026#39;唐伟\u0026#39;, \u0026#39;sex\u0026#39;: \u0026#39;F\u0026#39;, \u0026#39;address\u0026#39;: \u0026#39;新疆维吾尔自治区建华市东丽拉萨街a座 875743\u0026#39;, \u0026#39;mail\u0026#39;: \u0026#39;shenyang@hotmail.com\u0026#39;, \u0026#39;birthdate\u0026#39;: datetime.date(2014, 4, 27)} 4、生成Python相关结构信息\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from faker import Faker fake = Faker(locale=\u0026#39;zh_CN\u0026#39;) print(\u0026#39;生成Python字典: {}\u0026#39;.format(fake.pydict( nb_elements=10, variable_nb_elements=True))) # Python字典 print(\u0026#39;生成Python可迭代对象:{}.\u0026#39;.format(fake.pyiterable( nb_elements=10, variable_nb_elements=True))) # Python可迭代对象 print(\u0026#39;生成Python结构：{}\u0026#39;.format(fake.pystruct(count=1))) # Python结构 # 输出信息 成Python字典: {\u0026#39;论坛\u0026#39;: \u0026#39;nVcSbHlrcrhIBtwByVUM\u0026#39;, \u0026#39;直接\u0026#39;: \u0026#39;drkyFUNcNxdbwYKhRLEZ\u0026#39;, \u0026#39;成功\u0026#39;: \u0026#39;https://fang.cn/main/search/blog/search/\u0026#39;, \u0026#39;没有\u0026#39;: datetime.datetime(2006, 2, 24, 15, 40, 14), \u0026#39;原因\u0026#39;: 404, \u0026#39;作者\u0026#39;: \u0026#39;OTJjsFHQklpUvTPtLCqP\u0026#39;} 生成Python可迭代对象:{1088, \u0026#39;ignqbohwYRxqolLEzSti\u0026#39;, \u0026#39;http://gang.cn/main/search.php\u0026#39;, \u0026#39;zRnNYdIpPXUxEVISHbvS\u0026#39;, \u0026#39;ToZxuBetghvlPHUumAvi\u0026#39;, 9830, \u0026#39;OYAjoKeVNGhHMLgnYUAw\u0026#39;, 970446.888, -17681479853.4069, 872236250787063.0, datetime.datetime(2017, 12, 24, 5, 58, 58), \u0026#39;aRSfxiUSuMqHXvKCCkMJ\u0026#39;} 生成Python结构：([\u0026#39;cKwOvdCEFOhCERMSMXSf\u0026#39;], {\u0026#39;只有\u0026#39;: \u0026#39;hhwGCmjkHMOUjBTDztXp\u0026#39;}, {\u0026#39;还有\u0026#39;: {0: \u0026#39;vjcNqpnRbNUUxXpgVyvh\u0026#39;, 1: [8725, 7125, \u0026#39;aTSJssAJUKpuRLcbiwyK\u0026#39;], 2: {0: \u0026#39;RmWlFQQpVZIQkxZPfJnq\u0026#39;, 1: \u0026#39;efsUVLgeStXbCOJDuJCf\u0026#39;, 2: [\u0026#39;FgZQLCRjUTmEbBdDMEPZ\u0026#39;, \u0026#39;https://min.cn/search/faq/\u0026#39;]}}}) 4. Faker常用使用 如果这些数据还不够生成数据使用，Faker还支持创建自定义的Provider生成数据。\n1 2 3 4 5 6 7 8 9 10 11 12 from faker import Faker from faker.providers import BaseProvider # 创建自定义Provider class CustomProvider(BaseProvider): def customize_type(self): return \u0026#39;test_Faker_customize_type\u0026#39; # 添加Provider fake = Faker() fake.add_provider(CustomProvider) print(fake.customize_type()) 是不是十分简单，以后常用的数据就可以自己创建Provider用自动化的方法生成了，不仅节省了时间，复用性也变高了。\n5. 总结 这些只是其中的一些常见的数据，Faker 可以造的数据远不止这些类型。相信通过本文的介绍，大家应该对 Faker 不陌生了吧。以后在需要造数据的时候，一定要想起 Faker 这个利器哦！\n此外，作为一个开源的库，Faker的源码是非常值得研究的，也是Python新手可以用来练开源项目的利器。\n","permalink":"https://ktzxy.top/posts/80xu3kueya/","summary":"自动造数据利器Faker","title":"自动造数据利器Faker"},{"content":"1. ArrayList 源码分析 1.1. 属性分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /** * 默认初始化容量 */ private static final int DEFAULT_CAPACITY = 10; /** * 如果自定义容量为0，则会默认用它来初始化ArrayList。或者用于空数组替换。 */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * 如果没有自定义容量，则会使用它来初始化ArrayList。或者用于空数组比对。 */ private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; /** * 这就是ArrayList底层用到的数组 * 非私有，以简化嵌套类访问 * transient 在已经实现序列化的类中，不允许某变量序列化 */ transient Object[] elementData; /** * 实际ArrayList集合大小 */ private int size; /** * 可分配的最大容量 */ private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 1.1.1. 扩展：什么是序列化 序列化是指：将对象转换成以字节序列的形式来表示，以便用于持久化和传输。\n实现方法：实现 Serializable 接口。\n然后用的时候拿出来进行反序列化即可又变成Java对象。\n1.1.2. transient 关键字修饰的 elementData 属性解析 1 2 3 4 5 6 7 /** * The array buffer into which the elements of the ArrayList are stored. * The capacity of the ArrayList is the length of this array buffer. Any * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA * will be expanded to DEFAULT_CAPACITY when the first element is added. */ transient Object[] elementData; // non-private to simplify nested class access Java中transient关键字的作用，简单地说，就是让某些被修饰的成员属性变量不被序列化。\nArrayList 实现了 Serializable 接口，意味着 ArrayList 支持序列化。而使用 transient 关键字声明的 elementData 属性，则这个变量不会参与序列化操作，即使所在类实现了Serializable接口，反序列化后该变量为空值。\n那么问题来了：ArrayList 中数组声明：transient Object[] elementData;，事实上使用 ArrayList 在网络传输用的很正常，并没有出现空值。\n原来是因为 ArrayList 还重写了 writeObject 方法实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone() s.writeInt(size); // Write out all elements in the proper order. for (int i=0; i\u0026lt;size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } ArrayList 在每次序列化时，会调用writeObject()方法，首先会调用 defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素，如size和element等写入ObjectOutputStream，然后遍历 elementData 属性，只序列化已存入的元素，这样既加快了序列化的速度，又减小了序列化之后的文件大小。反序列化时调用readObject()，从ObjectInputStream获取size和element，再恢复到elementData。\n那为什么不直接用elementData来序列化，而采用上诉的方式来实现序列化呢？\n原因在于elementData是一个缓存数组，它通常会预留一些容量，等容量不足时再扩充容量，那么有些空间可能就没有实际存储元素，采用上诉的方式来实现序列化时，就可以保证只序列化实际存储的那些元素，而不是整个数组，从而节省空间和时间。\n1.2. 构造方法分析 根据initialCapacity 初始化一个空数组，如果值为0，则初始化一个空数组:\n1 2 3 4 5 6 7 8 9 10 11 12 /** * 根据initialCapacity 初始化一个空数组 */ public ArrayList(int initialCapacity) { if (initialCapacity \u0026gt; 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException(\u0026#34;Illegal Capacity: \u0026#34; + initialCapacity); } } 不带参数初始化，默认容量为10:\n1 2 3 4 5 6 /** * 不带参数初始化，默认容量为10 */ public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } 通过集合做参数的形式初始化：如果集合为空，则初始化为空数组：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** * 通过集合做参数的形式初始化 */ public ArrayList(Collection\u0026lt;? extends E\u0026gt; c) { elementData = c.toArray(); if ((size = elementData.length) != 0) { // c.toArray might (incorrectly) not return Object[] (see 6260652) if (elementData.getClass() != Object[].class) elementData = Arrays.copyOf(elementData, size, Object[].class); } else { // replace with empty array. this.elementData = EMPTY_ELEMENTDATA; } } 1.3. 主干方法 1.3.1. trimToSize() 方法 用来最小化实例存储，将容器大小调整为当前元素所占用的容量大小。\n1 2 3 4 5 6 7 8 9 10 11 /** * 这个方法用来最小化实例存储。 */ public void trimToSize() { modCount++; if (size \u0026lt; elementData.length) { elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); } } 1.3.2. clone() 方法 用来克隆出一个新数组。\n1 2 3 4 5 6 7 8 9 10 11 public Object clone() { try { ArrayList\u0026lt;?\u0026gt; v = (ArrayList\u0026lt;?\u0026gt;) super.clone(); v.elementData = Arrays.copyOf(elementData, size); v.modCount = 0; return v; } catch (CloneNotSupportedException e) { // this shouldn\u0026#39;t happen, since we are Cloneable throw new InternalError(e); } } 通过调用Object的clone()方法来得到一个新的ArrayList对象，然后将elementData复制给该对象并返回。\n1.3.3. add(E e) 方法 在数组末尾添加元素\n1 2 3 4 5 6 7 8 /** * 在数组末尾添加元素 */ public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } 看到它首先调用了ensureCapacityInternal()方法.注意参数是size+1,这是个面试考点。\n1 2 3 private void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); } 这个方法里又嵌套调用了两个方法:计算容量+确保容量\n计算容量：如果elementData是空，则返回默认容量10和size+1的最大值，否则返回size+1\n1 2 3 4 5 6 private static int calculateCapacity(Object[] elementData, int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { return Math.max(DEFAULT_CAPACITY, minCapacity); } return minCapacity; } 计算完容量后，进行确保容量可用：(modCount不用理它，它用来计算修改次数)\n如果size+1 \u0026gt; elementData.length证明数组已经放满，则增加容量，调用grow()。\n1 2 3 4 5 6 7 private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code if (minCapacity - elementData.length \u0026gt; 0) grow(minCapacity); } 增加容量：默认1.5倍扩容。\n获取当前数组长度=\u0026gt;oldCapacity oldCapacity\u0026raquo;1 表示将oldCapacity右移一位(位运算)，相当于除2。再加上1，相当于新容量扩容1.5倍。 如果newCapacity\u0026amp;gt;1=1,1\u0026amp;lt;2所以如果不处理该情况，扩容将不能正确完成。 如果新容量比最大值还要大，则将新容量赋值为VM要求最大值。 将elementData拷贝到一个新的容量中。 1 2 3 4 5 6 7 8 9 10 11 private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity \u0026gt;\u0026gt; 1); if (newCapacity - minCapacity \u0026lt; 0) newCapacity = minCapacity; if (newCapacity - MAX_ARRAY_SIZE \u0026gt; 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: elementData = Arrays.copyOf(elementData, newCapacity); } 1.3.3.1. size+1的问题 好了，那到这里可以说一下为什么要size+1。\nsize+1代表的含义是：\n如果集合添加元素成功后，集合中的实际元素个数。 为了确保扩容不会出现错误。 假如不加一处理，如果默认size是0，则0+0\u0026raquo;1还是0。\n如果size是1，则1+1\u0026raquo;1还是1。有人问:不是默认容量大小是10吗?事实上，jdk1.8版本以后，ArrayList的扩容放在add()方法中。之前放在构造方法中。我用的是1.8版本，所以默认ArrayList arrayList = new ArrayList();后，size应该是0.所以,size+1对扩容来讲很必要.\n1 2 3 4 5 6 public static void main(String[] args) { ArrayList arrayList = new ArrayList(); System.out.println(arrayList.size()); } 输出:0 事实上上面的代码是证明不了容量大小的，因为size只会在调用add()方法时才会自增。有办法的小伙伴可以在评论区大显神通。\n1.3.4. add(int index, E element) 方法 1 2 3 4 5 6 7 8 9 public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; } rangeCheckForAdd() 是越界异常检测方法。ensureCapacityInternal()之前有讲，着重说一下System.arrayCopy方法：\n1 public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) 1.3.4.1. 代码解释 Object src : 原数组 int srcPos : 从元数据的起始位置开始 Object dest : 目标数组 int destPos : 目标数组的开始起始位置 int length : 要copy的数组的长度 示例：size为6，我们调用add(2,element)方法，则会从index=2+1=3的位置开始，将数组元素替换为从index起始位置为index=2，长度为6-2=4的数据。\n1.3.4.2. 异常处理 1 2 3 4 private void rangeCheckForAdd(int index) { if (index \u0026gt; size || index \u0026lt; 0) throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); } 1.3.5. set(int index, E element) 方法 1 2 3 4 5 6 7 8 9 10 11 public E set(int index, E element) { rangeCheck(index); E oldValue = elementData(index); elementData[index] = element; return oldValue; } E elementData(int index) { return (E) elementData[index]; } 逻辑很简单，覆盖旧值并返回。\n1.3.6. indexOf(Object o) 方法 根据Object对象获取数组中的索引值。\n1 2 3 4 5 6 7 8 9 10 11 12 public int indexOf(Object o) { if (o == null) { for (int i = 0; i \u0026lt; size; i++) if (elementData[i]==null) return i; } else { for (int i = 0; i \u0026lt; size; i++) if (o.equals(elementData[i])) return i; } return -1; } 如果o为空，则返回数组中第一个为空的索引；不为空也类似。\n注意：通过源码可以看到，该方法是允许传空值进来的。\n1.3.7. get(int index) 方法 返回指定下标处的元素的值。\n1 2 3 4 5 public E get(int index) { rangeCheck(index); return elementData(index); } rangeCheck(index)会检测index值是否合法，如果合法则返回索引对应的值。\n1.3.8. remove(int index) 方法 删除指定下标的元素。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public E remove(int index) { // 检测index是否合法 rangeCheck(index); // 数据结构修改次数 modCount++; E oldValue = elementData(index); // 记住这个算法 int numMoved = size - index - 1; if (numMoved \u0026gt; 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; } 这里又碰到了System.arraycopy()方法，详情请查阅上文。\n大概思路：将该元素后面的元素前移，最后一个元素置空。\n1.4. 集合的快速失败机制 “fail-fast” “fail-fast”，即快速失败，它是 Java 集合进行结构上的改变的操作时的一种错误检测机制。当多个线程对集合（非 fail-safe 的集合类）进行结构上的改变的操作时，有可能会产生 fail-fast 机制，这个时候就会抛出 ConcurrentModificationException（当方法检测到对象的并发修改，但不允许这种修改时就抛出该异常）。\n同时需要注意的是，即使不是多线程环境，如果单线程违反了规则，同样也有可能会抛出改异常。\n例如：假设存在两个线程（线程1、线程2），线程1通过 Iterator 在遍历集合A中的元素，在某个时候线程2修改了集合A的结构（是结构上面的修改，而不是简单的修改集合元素的内容），那么这个时候程序就会抛出 ConcurrentModificationException 异常，从而触发 fail-fast 机制。\n1.4.1. 源码分析 以下参考 ArrayList 源码的处理：\n1 2 3 4 5 6 7 8 9 10 11 12 13 public void forEach(Consumer\u0026lt;? super E\u0026gt; action) { Objects.requireNonNull(action); final int expectedModCount = modCount; @SuppressWarnings(\u0026#34;unchecked\u0026#34;) final E[] elementData = (E[]) this.elementData; final int size = this.size; for (int i=0; modCount == expectedModCount \u0026amp;\u0026amp; i \u0026lt; size; i++) { action.accept(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } 通过源码分析可知异常的原因：迭代器在遍历时直接访问集合中的内容，并且在遍历过程中使用一个 modCount 成员变量，它表示该集合实际被修改的次数。集合在被遍历期间如果内容发生变化，就会改变 modCount （用于记录集合操作过程的修改次数）的值，增加1。\nexpectedModCount 是 ArrayList 中的一个内部类 - Itr 中的成员变量（Itr 是一个 Iterator 的实现，使用 ArrayList.iterator 方法可以获取到的迭代器就是 Itr 类的实例。）。expectedModCount 表示这个迭代器期望该集合被修改的次数。其值是在 ArrayList.iterator 方法被调用的时候初始化的。只有通过迭代器对集合进行操作，该值才会改变。\n每当迭代器使用hashNext()/next() 遍历下一个元素之前，都会检测 modCount 变量是否为 expectedmodCount 值，是的话就返回遍历；否则抛出异常，终止遍历并抛出 ConcurrentModificationException。\n1.4.2. 对集合进行 add/remove 正常操作方式 直接使用普通 for 循环进行操作，因为普通 for 循环并没有用到 Iterator 的遍历，所以压根就没有进行 fail-fast 的检验。但这种方案其实存在一个问题，那就是 remove 操作会改变 List 中元素的下标，可能存在漏删的情况。 直接使用 Iterator 提供的 remove 方法进行操作。该方法可以修改到 expectedModCount 的值，那么就不会再抛出异常了。 1 2 3 4 5 6 7 8 9 10 11 12 13 List\u0026lt;String\u0026gt; userNames = new ArrayList\u0026lt;String\u0026gt;() {{ add(\u0026#34;MooN\u0026#34;); add(\u0026#34;Zero\u0026#34;); add(\u0026#34;L\u0026#34;); add(\u0026#34;kirA\u0026#34;); }}; Iterator\u0026lt;String\u0026gt; iterator = userNames.iterator(); while (iterator.hasNext()) { if (iterator.next().equals(\u0026#34;L\u0026#34;)) { iterator.remove(); } } 使用 Java 8 中 Stream 提供的 filter 过滤 使用增强 for 循环，并且非常确定在一个集合中，某个即将删除的元素只包含一个的时候（比如对 Set 集合进行操作），只要在删除元素后立刻结束循环体，不再继续进行遍历，也就是说不让代码执行到下一次的 next 方法。 1 2 3 4 5 6 7 8 9 10 11 12 13 List\u0026lt;String\u0026gt; userNames = new ArrayList\u0026lt;String\u0026gt;() {{ add(\u0026#34;MooN\u0026#34;); add(\u0026#34;Zero\u0026#34;); add(\u0026#34;L\u0026#34;); add(\u0026#34;kirA\u0026#34;); }}; for (String userName : userNames) { if (userName.equals(\u0026#34;L\u0026#34;)) { userNames.remove(userName); break; } } 不直接使用 fail-safe 的集合类，例如使用 CopyOnWriteArrayList、ConcurrentLinkedDeque 等来替换 ArrayList。这种集合容器在遍历时不是直接在集合内容上访问的，而是先复制原有集合内容，在拷贝的集合上进行遍历操作，操作完成后再把引用移到新的数组。因此在遍历过程中对原集合所作的修改并不能被迭代器检测到，所以不会触发 ConcurrentModificationException。 Tips: java.util.concurrent 包下的容器都是安全失败，可以在多线程下并发使用，并发修改。\n在遍历过程中，所有涉及到改变 modCount 值得地方全部加上 synchronized 1.5. 手写ArrayList(网上资料) 那面试手写ArrayList应该就不是问题了。下面网上资料的手写一个简单阉割版的ArrayList：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 public class MyArrayList { // 非私有，以简化嵌套类访问 // transient 在已经实现序列化的类中，不允许某变量序列化 transient Object[] elementData; //默认容量 private static final int DEFAULT_CAPACITY = 10; // 用于空实例的 空数组实例 private static final Object[] EMPTY_ELEMENTDATA = {}; private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 实际ArrayList集合大小 private int size; /** * 构造方法 */ public MyArrayList(int initialCapacity) { if (initialCapacity \u0026gt; 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException(\u0026#34;Illegal Capacity: \u0026#34;+ initialCapacity); } } public MyArrayList(){ this(DEFAULT_CAPACITY); } public void add(Object o){ //1. 判断数据容量是否大于 elementData ensureExplicitCapacity(size+1); //2. 使用下标进行赋值 elementData[size++] = o; } private void ensureExplicitCapacity(int minCapacity){ if (size == elementData.length){ // 需要扩容,扩容1.5倍(ArrayList默认扩容1.5倍) // 注意：如果oldCapacity值为1 int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity \u0026gt;\u0026gt; 1); // 如果新容量 \u0026lt; 最小容量， 则将最小容量赋值给新容量 // 如果 oldCapacity=1, 则 minCapacity=1+1=2 newCapacity=1+(1\u0026gt;\u0026gt;1)=1 if (newCapacity - minCapacity \u0026lt; 0){ newCapacity = minCapacity; } // 创建新数组 Object[] objects = new Object[newCapacity]; // 将数据复制给新数组 System.arraycopy(elementData, 0, objects, 0, elementData.length); // 修改引用 elementData = objects; } } public Object get(int index) { rangeCheck(index); return elementData[index]; } private void rangeCheck(int index) { if (index \u0026gt;= size) throw new IndexOutOfBoundsException(\u0026#34;下标越界\u0026#34;); } /** * 通过下标删除 * @param index * @return */ public Object remove(int index) { rangeCheck(index); // modCount++; // 先查出元素 Object oldValue = elementData[index]; // 找出置换结束位置 int numMoved = size - index - 1; if (numMoved \u0026gt; 0) // 从 index+1 开始 将值覆盖为 index-numMoved 的值 System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // clear to let GC do its work return oldValue; } public boolean remove(Object o) { for (int index = 0; index \u0026lt; size; index++){ if (o.equals(elementData[index])) { remove(index); return true; } } return false; } } 2. HashMap 源码分析 2.1. 解决哈希冲突的方案 开放定址法（Open Addressing）：也称为再散列法，基本思想就是，如果 p=H(key) 出现冲突时，则以 p 为基础，再次 hash，即 p1=H(p)，如果 p1 再次出现冲突，则以 p1 为基础，以此类推，直到找到一个不冲突的哈希地址 pi。因此开放定址法所需要的 hash 表的长度要大于等于所需要存放的元素，而且因为存在再次 hash，所以只能在删除的节点上做标记，而不能真正删除节点。 再哈希法（Rehashing）：双重散列，多重散列，提供多个不同的 hash 函数，当 R1=H1(key1) 发生冲突时，再计算 R2=H2(key1)，直到没有冲突为止。这样做虽然不易产生堆集，但增加了计算的时间，适用于元素数量较少的情况。 链地址法（Separate Chaining）：拉链法，将哈希值相同的元素构成一个同义词的单链表，并将单链表的头指针存放在哈希表的第i个单元中，查找、插入和删除主要在同义词链表中进行。链表法适用于经常进行插入和删除的情况。 建立公共溢出区：将哈希表分为公共表和溢出表，当溢出发生时，将所有溢出数据统一放到溢出区。 2.2. HashMap 数据存储实现原理 HashMap 是基于哈希表的 Map 接口的非同步实现。此实现提供所有可选的映射操作，并允许使用 null 值和 null 键。此类不保证映射的顺序，特别是它不保证该顺序恒久不变。\n在 Java 编程语言中，保存数据有两种比较简单的数据结构：数组和链表（模拟指针引用）。所有的数据结构都可以用这两个基本结构来构造的，HashMap 也不例外。\n数组的特点是：寻址容易，插入和删除困难。 链表的特点是：寻址困难，但插入和删除容易。 所以将数组和链表结合在一起，发挥两者各自的优势，使用一种叫做拉链法的方式可以解决哈希冲突。HashMap 的数据结构实际上是一个“链表散列”的数据结构，即数组和链表的结合体。\n2.2.1. Hash 算法实现过程 HashMap 基于 Hash 算法实现的，具体如下：\n当往 HashMap 中 put 元素时，利用 key 的 hashCode 重新 hash 计算出当前对象的元素在数组中的下标 存储时，如果出现 hash 值相同的 key，此时有两种情况。 如果 key 相同，则覆盖原始值。 如果 key 不同（出现冲突），则将当前的 key-value 放入链表或红黑树中。 获取时，直接找到 hash 值对应的下标，在进一步判断 key 是否相同，从而找到对应值。 理解了以上过程可知 HashMap 是如何解决 hash 冲突的问题，核心就是使用了数组的存储方式，然后将冲突的 key 的对象放入链表中，一旦发现冲突就在链表中做进一步的对比。\n需要注意 Jdk 1.8 中对 HashMap 的实现做了优化，当链表中的节点数据超过八个之后，该链表会转为红黑树来提高查询效率，从原来的 O(n) 到 O(logn)\n2.2.2. JDK1.7 解决哈希冲突 JDK1.7 采用的是拉链法。拉链法：将链表和数组相结合。也就是说创建一个链表数组，数组中每一格就是一个链表。若遇到哈希冲突，则将冲突的值加到链表中即可。\n2.2.3. JDK1.8 解决哈希冲突 相比于之前的版本，jdk1.8 在解决哈希冲突时有了较大的变化，当链表长度大于阈值（默认为 8）并且数组长度达到 64 时，将链表转化为红黑树，以减少搜索时间。\n数组+链表。通过计算 key 的 hashCode 的值，再去取模来决定当前 Entry 对象存储的索引位置，如果当前位置为空，则直接存储；如果当时位置已经存在内容，则将给存储的数据加上 next 指针，指向之前存在的数据。 jdk8 主要是对 HashMap 做了红黑树的优化，使树的结构相对平衡，减小链的长度，达到加快查询的速度\n2.2.4. JDK1.7 VS JDK1.8 JDK1.8 主要解决或优化了一下问题：\nresize 扩容优化 引入了红黑树，目的是避免单条链表过长而影响查询效率 解决了多线程死循环问题，但仍是非线程安全的，多线程时可能会造成数据丢失问题。 JDK1.7 VS JDK1.8 具体的区别：\n存储结构：JDK1.7 是数组+链表；JDK1.8 是数组+链表+红黑树。 初始化方式：JDK1.7 使用单独函数 inflateTable()；JDK1.8 直接集成到了扩容函数 resize() 中。 hash 值计算方式： JDK1.7 扰动处理=9 次扰动=4 次位运算+5 次异或运算 JDK1.8 扰动处理=2 次扰动=1 次位运算+1 次异或运算 存放数据的规则： JDK1.7 无冲突时，存放数组；冲突时，存放链表。 JDK1.8 无冲突时：存放数组；冲突并且链表长度 \u0026lt; 8：存放单链表；冲突并且链表长度 \u0026gt; 8：树化并存放红黑树。 插入数据方式：JDK1.7 头插法（先将原位置的数据移到后 1 位，再插入数据到该位置）；JDK1.8 尾插法（直接插入到链表尾部/红黑树）。 扩容后存储位置的计算方式： JDK1.7 全部按照原来方法进行计算（即hashCode -\u0026gt;\u0026gt; 扰动函数 -\u0026gt;\u0026gt; (h\u0026amp;length-1)） JDK1.8 按照扩容后的规律计算（即扩容后的位置 = 原位置 或者 扩容后的位置= 原位置 + 旧容量） 2.3. HashMap 重点属性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class HashMap\u0026lt;K,V\u0026gt; extends AbstractMap\u0026lt;K,V\u0026gt; implements Map\u0026lt;K,V\u0026gt;, Cloneable, Serializable { // 默认的初始容量 static final int DEFAULT_INITIAL_CAPACITY = 1 \u0026lt;\u0026lt; 4; // aka 16 // 默认的加载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 用于存储数据的是内部类 Node 数组 transient Node\u0026lt;K,V\u0026gt;[] table; transient int size; // ...省略其他属性 static class Node\u0026lt;K,V\u0026gt; implements Map.Entry\u0026lt;K,V\u0026gt; { final int hash; final K key; V value; Node\u0026lt;K,V\u0026gt; next; Node(int hash, K key, V value, Node\u0026lt;K,V\u0026gt; next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + \u0026#34;=\u0026#34; + value; } public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (o == this) return true; if (o instanceof Map.Entry) { Map.Entry\u0026lt;?,?\u0026gt; e = (Map.Entry\u0026lt;?,?\u0026gt;)o; if (Objects.equals(key, e.getKey()) \u0026amp;\u0026amp; Objects.equals(value, e.getValue())) return true; } return false; } } public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } } 从以上源码可知，HashMap 是懒惰加载，在创建对象时并没有初始化数组。在无参的构造函数中，设置了默认的加载因子是 0.75。\nTips: 扩容阈值 = 数组容量 × 加载因子\n2.4. put 方法设置值的具体流程 当 put 元素的时候，首先计算 key 的 hash 值，这里调用了 hash 方法，hash 方法实际是让key.hashCode()与key.hashCode()\u0026gt;\u0026gt;\u0026gt;16进行异或操作，高 16bit 补 0，一个数和 0 异或不变，所以 hash 函数大概的作用就是：高 16bit 不变，低 16bit 和高 16bit 做了一个异或，目的是减少碰撞。按照函数注释，因为 bucket 数组大小是 2 的幂，计算下标index = (table.length - 1) \u0026amp; hash，如果不做 hash 处理，相当于散列生效的只有几个低 bit 位，为了减少散列的碰撞，设计者综合考虑了速度、作用、质量之后，使用高 16bit 和低 16bit 异或来简单处理减少碰撞，而且 JDK8 中用了复杂度 O(logn)的树结构来提升碰撞下的性能。\n2.4.1. putVal 方法执行流程图 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) 流程图：\n具体流程：\n判断键值对数组 table 是否为空或为 null（没有初始化），否则执行 resize() 进行扩容（初始化）过程 根据键值 key 计算 hash 值得到数组索引 判断 table[i] == null 成立，则代表索引处有没有存在元素，直接新建节点添加 判断 table[i] != null 成立。则代表索引处存在元素，则需要遍历插入。有两种情况，一种是链表形式就直接遍历到尾端插入，一种是红黑树就按照红黑树结构插入 判断 table[i] 的首个元素是否和 key 一样，如果相同直接覆盖 value 判断 table[i] 是否为 treeNode，即 table[i] 是否是红黑树，如果是红黑树，则直接在树中插入键值对 遍历 table[i]，链表的尾部插入数据，然后判断链表长度是否大于8。若是，则把链表转换为红黑树，在红黑树中执行插入操作，遍历过程中若发现 key 已经存在直接覆盖 value 插入成功后，判断实际存在的键值对数量 size 是否超多了最大容量 threshold（数组长度 * 0.75），如果超过，执行 resize() 进行扩容。 2.4.2. putVal 方法源码分析 以下为 JDK 1.8 源码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; p; int n, i; // 判断数组是否未初始化 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 如果未初始化，调用 resize 方法 进行初始化 // 通过 \u0026amp; 运算求出该数据（key）的数组下标并判断该下标位置是否有数据 if ((p = tab[i = (n - 1) \u0026amp; hash]) == null) tab[i] = newNode(hash, key, value, null); // 如果没有，直接将数据放在该下标位置 else { // 该数组下标有数据的情况 Node\u0026lt;K,V\u0026gt; e; K k; // 判断该位置数据的 key 和新来的数据是否一样 if (p.hash == hash \u0026amp;\u0026amp; ((k = p.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) e = p; // 如果一样，证明为修改操作，该节点的数据赋值给 e，后边会用到 // 判断是不是红黑树 else if (p instanceof TreeNode) e = ((TreeNode\u0026lt;K,V\u0026gt;)p).putTreeVal(this, tab, hash, key, value); // 如果是红黑树的话，进行红黑树的操作 else { // 遍历链表 for (int binCount = 0; ; ++binCount) { // 判断 next 节点，如果为空的话，证明遍历到链表尾部了 if ((e = p.next) == null) { // 把新值放入链表尾部 p.next = newNode(hash, key, value, null); // 因为新插入了一条数据，所以判断链表长度是不是大于等于8 if (binCount \u0026gt;= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); // 如果是，进行转换红黑树操作 break; } // 判断链表当中有数据相同的值，如果一样，证明为修改操作 if (e.hash == hash \u0026amp;\u0026amp; ((k = e.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) break; p = e; // 把下一个节点赋值为当前节点 } } // 判断 e 是否为空（e 值为修改操作存放原数据的变量） if (e != null) { // existing mapping for key // 不为空的话证明是修改操作，取出旧值 V oldValue = e.value; // 此分支一定会执行，因为方法调用时传入的 onlyIfAbsent 的参数值是 false if (!onlyIfAbsent || oldValue == null) e.value = value; // 将新值赋值当前节点 afterNodeAccess(e); return oldValue; // 返回旧值 } } ++modCount; // 计数器，计算当前节点的修改次数 // 当前数组中的数据数量如果大于扩容阈值 if (++size \u0026gt; threshold) resize(); // 进行扩容操作 afterNodeInsertion(evict); // 空方法 return null; // 添加操作时 返回空值 } 2.5. HashMap 的 resize 扩容机制 当 HashMap 的数组大小达到一定的阈值（默认为 75%），会触发扩容操作。扩容的过程会重新计算每个键值对的哈希值，然后将其存储在新的数组位置上。扩容操作需要耗费一定的时间，因此需要在初始化时预估 HashMap 中键值对的数量，以便尽可能地减少扩容操作的次数。\n2.5.1. 扩容流程图 在添加元素或初始化的时候需要调用 resize 方法进行扩容，第一次添加数据初始化数组长度为16，以后每次每次扩容都是达到了扩容阈值（数组长度 * 0.75） 每次扩容的时候，都是扩容之前容量的2倍； 扩容之后，会新创建一个数组，需要把老数组中的数据挪动到新的数组中 没有 hash 冲突的节点，则直接使用 e.hash \u0026amp; (newCap - 1) 计算新数组的索引位置 如果是红黑树，走红黑树的添加 如果是链表，则需要遍历链表，可能需要拆分链表，判断 (e.hash \u0026amp; oldCap) 是否为0，该元素的位置要么停留在原始位置，要么移动到原始位置+增加的数组大小这个位置上 2.5.2. resize 方法源码分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 // 扩容、初始化数组 final Node\u0026lt;K,V\u0026gt;[] resize() { Node\u0026lt;K,V\u0026gt;[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; // 如果当前数组为 null 的时候，把 oldCap 老数组容量设置为 0 int oldThr = threshold; // 设置老的扩容阈值 int newCap, newThr = 0; // 判断数组容量是否大于0，大于0说明数组已经初始化 if (oldCap \u0026gt; 0) { // 判断当前数组长度是否大于最大数组长度 if (oldCap \u0026gt;= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; // 如果是，将扩容阈值直接设置为 int 类型的最大数值并直接返回 return oldTab; } // 如果在最大长度范围内，则需要扩容 OldCap \u0026lt;\u0026lt; 1 等价于 oldCap * 2 // 运算过后判断是不是最大值并且 oldCap 需要大于 16 else if ((newCap = oldCap \u0026lt;\u0026lt; 1) \u0026lt; MAXIMUM_CAPACITY \u0026amp;\u0026amp; oldCap \u0026gt;= DEFAULT_INITIAL_CAPACITY) newThr = oldThr \u0026lt;\u0026lt; 1; // double threshold 等价于 oldThr * 2 } // 如果 oldCap\u0026lt;0，但是已经初始化了，像把元素删除完之后的情况，那么它的临界值肯定还存在，如果是首次初始化，它的临界值则为0 else if (oldThr \u0026gt; 0) // initial capacity was placed in threshold newCap = oldThr; // 数组未初始化的情况，将阈值和扩容因子都设置为默认值 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 初始化容量小于16的时候，扩容阈值是没有赋值的 if (newThr == 0) { float ft = (float)newCap * loadFactor; // 创建阈值 // 判断新容量和新阈值是否大于最大容量 newThr = (newCap \u0026lt; MAXIMUM_CAPACITY \u0026amp;\u0026amp; ft \u0026lt; (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; // 计算出来的阈值赋值 @SuppressWarnings({\u0026#34;rawtypes\u0026#34;,\u0026#34;unchecked\u0026#34;}) Node\u0026lt;K,V\u0026gt;[] newTab = (Node\u0026lt;K,V\u0026gt;[])new Node[newCap]; // 根据上边计算得出的容量，创建新的数组 table = newTab; // 赋值到 table 属性 // 扩容操作，判断不为空证明不是初始化数组 if (oldTab != null) { // 遍历数组 for (int j = 0; j \u0026lt; oldCap; ++j) { Node\u0026lt;K,V\u0026gt; e; // 判断当前下标为j的数组如果不为空的话赋值个e，进行下一步操作 if ((e = oldTab[j]) != null) { oldTab[j] = null; // 将数组位置置空 // 判断是否有下个节点 if (e.next == null) newTab[e.hash \u0026amp; (newCap - 1)] = e; // 如果没有，就重新计算在新数组中的下标并放进去 // 有下个节点的情况，并且判断是否已经树化 else if (e instanceof TreeNode) ((TreeNode\u0026lt;K,V\u0026gt;)e).split(this, newTab, j, oldCap); // 进行红黑树的操作 // 有下个节点的情况，并且没有树化（链表形式） else { // preserve order // 比如老数组容量是 16，那下标就为 0-15 // 扩容操作 * 2，容量就变为 32，下标为 0-31 // 低位：0-15，高位1 6-31 // 定义了四个变量 // 低位头 低位尾 Node\u0026lt;K,V\u0026gt; loHead = null, loTail = null; // 高位头\t高位尾 Node\u0026lt;K,V\u0026gt; hiHead = null, hiTail = null; Node\u0026lt;K,V\u0026gt; next; // 下个节点 // 循环遍历 do { next = e.next; // 取出next节点 // 通过“与”操作，计算得出结果为0 if ((e.hash \u0026amp; oldCap) == 0) { // 如果低位尾为null，证明当前数组位置为空，没有任何数据 if (loTail == null) loHead = e; // 将e值放入低位头 // 低位尾不为null，证明已经有数据了 else loTail.next = e; // 将数据放入next节点 loTail = e; // 记录低位尾数据 } // 通过“与”操作，计算得出结果不为0 else { // 如果高位尾为null，证明当前数组位置为空，没有任何数据 if (hiTail == null) hiHead = e; // 将数据放入next节点 // 高位尾不为null，证明已经有数据了 else hiTail.next = e; // 将数据放入next节点 hiTail = e; // 记录高位尾数据 } } while ((e = next) != null); // 如果e不为空，证明没有到链表尾部，继续执行循环 // 低位尾如果记录的有数据，是链表 if (loTail != null) { loTail.next = null; // 将下一个元素置空 newTab[j] = loHead; // 将低位头放入新数组的原下标位置 } // 高位尾如果记录的有数据，是链表 if (hiTail != null) { hiTail.next = null; // 将下一个元素置空 newTab[j + oldCap] = hiHead; // 将高位头放入新数组的(原下标+原数组容量)位置 } } } } } return newTab; // 返回新的数组对象 } 2.5.3. （待整理）为什么 HashMap 默认加载因子是 0.75 而不是其他数值 参考整理：https://mp.weixin.qq.com/s/a3qfatEWizKK1CpYaxVBbA\nHashMap 负载因子 loadFactor 的默认值是 0.75，为什么是 0.75 呢？官方给的答案如下：\nAs a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.\n上面的意思，简单来说是默认负载因子为 0.75，是因为它提供了空间和时间复杂度之间的良好平衡。负载因子太低会导致大量的空桶浪费空间，负载因子太高会导致大量的碰撞，降低性能。0.75 的负载因子在这两个因素之间取得了良好的平衡。\n结论：负载因子 loadFactor 是 HashMap 在进行扩容时的一个阈值，扩容的计算公式是：initialCapacity * loadFactor = HashMap 扩容。作为一般规则，默认负载因子（0.75）提供了在时间复杂度和空间成本之间的良好平衡，是很好的折衷方案。\n2.5.4. 扩展：为何 HashMap 的数组长度一定是2的次幂？ 计算索引时效率更高。如果是 2 的 n 次幂可以使用位与运算代替取模 扩容时重新计算索引效率更高。hash \u0026amp; oldCap == 0 的元素留在原来位置，否则新位置 = 旧位置 + oldCap 2 的 N 次幂有助于减少碰撞的几率。如果 length 为 2 的幂次方，则 length-1 转化为二进制必定是11111……的形式，在和 hash 值的二进制与操作效率会非常的快，而且空间不浪费。 当 length=15 时，6 和 7 的结果一样，这样表示他们在 table 存储的位置是相同的，也就是产生了碰撞，6、7 就会在一个位置形成链表，4和5 的结果也是一样，这样就会导致查询速度降低。\n如果进一步分析，还会发现空间浪费非常大，以 length=15 为例，在 1、3、5、7、9、11、13、15 这8处没有存放数据。因为 hash 值在与14（即 1110）进行\u0026amp;运算时，得到的结果最后一位永远都是0，即 0001、0011、0101、0111、1001、1011、1101、1111 位置处是不可能存储数据的。\n2.6. HashMap 的寻址算法 2.6.1. 计算键的 hash 值的源码分析 在 HashMap 类的 put(K key, V value) 方法中，会调用 hash(key) 方法来计算 key 的 hash 值。\n1 2 3 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); } hash(key) 方法源码如下：\n1 2 3 4 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16); } 首先获取 key 的 hashCode 值，然后右移 16 位后，与原来的 hashCode 值进行异或运算，称为『扰动算法』。主要作用就是使原来的 hash 值更加均匀，减少 hash 冲突。\n有了 hash 值之后，就可以计算当前 key 的在数组中存储的下标。例如在 putVal 方法中，通过 (n-1) \u0026amp; hash 获取数组中的索引，代替取模，性能更好。值得注意：数组长度必须是 2 的 n 次幂\n2.6.1.1. JDK 8 为什么要 hashcode 异或其右移十六位的值 因为在 JDK 7 中扰动了 4 次，计算 hash 值的性能会稍差。从速度、功效、质量来考虑，JDK 8 优化了高位运算的算法，通过 hashCode() 的高 16 位异或低 16 位实现：(h = k.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16)。\n这么做可以在数组 table 的 length 比较小的时候，也能保证考虑到高低 Bit 都参与到 Hash 的计算中，同时不会有太大的开销。\n2.6.1.2. 计算数组下标时为什么要 hash 值与 length-1 相与 把 hash 值对数组长度取模运算，模运算的消耗很大，没有位运算快。\n当 length 总是 2 的 n 次方时，h \u0026amp; (length-1) 运算等价于对 length 取模，即 h % length，但是 \u0026amp; 比 % 具有更高的效率。\n2.6.2. get 方法源码分析 以下为 JDK 1.8 源码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public V get(Object key) { Node\u0026lt;K,V\u0026gt; e; // hash(key)，获取 key 的 hash 值，调用 getNode 方法获取数据 return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node\u0026lt;K,V\u0026gt; getNode(int hash, Object key) { Node\u0026lt;K,V\u0026gt;[] tab; Node\u0026lt;K,V\u0026gt; first, e; int n; K k; // 找到 key 对应的桶下标，赋值给 first 节点 if ((tab = table) != null \u0026amp;\u0026amp; (n = tab.length) \u0026gt; 0 \u0026amp;\u0026amp; (first = tab[(n - 1) \u0026amp; hash]) != null) { // 判断 hash 值和 key 是否相等，如果是，则直接返回，桶中只有一个数据（大部分的情况） if (first.hash == hash \u0026amp;\u0026amp; // always check first node ((k = first.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) return first; if ((e = first.next) != null) { // 该节点是红黑树，则需要通过红黑树查找数据 if (first instanceof TreeNode) return ((TreeNode\u0026lt;K,V\u0026gt;)first).getTreeNode(hash, key); // 链表的情况，则需要遍历链表查找数据 do { if (e.hash == hash \u0026amp;\u0026amp; ((k = e.key) == key || (key != null \u0026amp;\u0026amp; key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; } 2.7. JDK 1.7 版本 HashMap 多线程死循环问题 由于 JDK 1.7 的 HashMap 数据结构是：数组+链表。在数组进行扩容的时候，因为链表采用的是头插法，在进行数据迁移的过程中，有可能导致死循环。\n变量 e 指向的是需要迁移的对象 变量 next 指向的是下一个需要迁移的对象 JDK 1.7 中的链表采用的头插法 在数据迁移的过程中并没有新的对象产生，只是改变了对象的引用 2.7.1. 产生死循环的过程分析 假设线程1和线程2的变量 e 和 next 都引用了这个两个节点 线程2扩容后，由于头插法，链表顺序颠倒，但是线程1的临时变量 e 和 next 还引用了这两个节点 第1次循环。由于线程2 迁移的时候，已经把 B 的 next 指向了 A 第2次循环 第3次循环 总结：\n在 JDK 1.7 的 HashMap 中在数组进行扩容的时候，因为链表采用了头插法，在进行数据迁移的过程中，有可能导致死循环。比如，现在有两个线程：\n线程1：读取到当前的 HashMap 数据，数据中一个链表，在准备扩容时，线程2介入 线程2：也读取 HashMap，直接进行扩容。因为是头插法，链表的顺序会进行颠倒过来。比如原来的顺序是AB，扩容后的顺序是BA，线程2执行结束。 线程1：继续执行时，会先将 A 移入新的链表，再将 B 插入到链头，由于另外一个线程的原因，B 的 next 指向了 A，所以导致 B-\u0026gt;A-\u0026gt;B，形成死循环的问题。 在 JDK 8 以后，已经将扩容算法做了调整，不再将元素加入链表头，而是采用了保持与扩容前一样的顺序的尾插法，避免了 JDK 7 中死循环的问题。\n2.8. HashMap 综合问题小结 2.8.1. 为什么要使用红黑树而不是二叉树 主要是因为红黑树在插入和删除操作时，能够自动平衡树的结构，使得整棵树的高度保持在一个较小的范围内，从而保证查找、插入和删除操作的时间复杂度稳定在 O(logn)。而二叉树没有自平衡的特性，如果插入和删除操作不当，可能会导致树的高度过高，使得查找时间复杂度变为 O(n)，因此不适合用于高效的 Map 实现。\n2.8.2. 为什么使用 8 作为链表改为红黑树的阈值 从作者在源码中的注释可知，理想情况下使用随机的哈希码，容器中节点分布在 hash 桶中的频率遵循泊松分布，按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表，可以看到链表中元素个数为 8 时的概率已经非常小，再多的就更少了，所以原作者在选择链表元素个数时选择了 8，是根据概率统计而选择的。\n2.8.3. 解决 hash 冲突的时为什么选择先用链表，再转红黑树 当元素小于 8 个的时候，此时做查询操作，链表结构已经能保证查询性能。当元素大于 8 个的时候，红黑树搜索时间复杂度是 O(logn)，而链表是 O(n)，此时需要红黑树来加快查询速度，因为红黑树需要进行左旋，右旋，变色这些操作来保持平衡，而单链表不需要，但是此时新增节点的效率会变慢。\n因此，如果一开始就用红黑树结构，元素太少，新增效率又比较慢，无疑这是浪费性能的。\n3. HashSet 源码分析 1 2 3 4 5 6 7 8 9 10 public class HashSet\u0026lt;E\u0026gt; extends AbstractSet\u0026lt;E\u0026gt; implements Set\u0026lt;E\u0026gt;, Cloneable, java.io.Serializable { private transient HashMap\u0026lt;E,Object\u0026gt; map; // 基于 HashMap 实现 // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); // ...省略 } HashSet 是基于 HashMap 实现的，HashSet 的值实际是存放于 HashMap 的 key 中，HashMap 的 value 统一存储了一个静态的 Object 对象 PRESENT，因此 HashSet 的实现比较简单，相关 HashSet 的操作，基本上都是直接调用底层 HashMap 的相关方法来完成，HashSet 不允许重复的值\n","permalink":"https://ktzxy.top/posts/wkegtysq48/","summary":"Java扩展 集合类源码分析","title":"Java扩展 集合类源码分析"},{"content":"1. 性能优化总论 上图显示，越往上走，难度越来越高，收益却是越来越小的。\n比如硬件和 OS 调优，需要对硬件和 OS 有着非常深刻的了解，仅仅就磁盘一项来说，一般非 DBA 能想到的调整就是 SSD 盘比用机械硬盘更好，但其实它至少包括了，使用什么样的磁盘阵列（RAID）级别、是否可以分散磁盘 IO、是否使用裸设备存放数据，使用哪种文件系统（目前比较推荐的是 XFS），操作系统的磁盘调度算法（目前比较推荐 deadline，对机械硬盘和 SSD 都比较合适。从内核 2.5开始，默认的 I/O 调度算法是 Deadline，之后默认 I/O 调度算法为 Anticipatory，直到内核 2.6.17 为止，从内核 2.6.18 开始，CFQ 成为默认的 IO 调度算法，但CFQ 并不推荐作为数据库服务器的磁盘调度算法。）选择，是否需要调整操作系统文件管理方面比如 atime 属性等等。\nTIPS：裸设备(raw device)，也叫裸分区（原始分区），是一种没有经过格式化，不被 Unix 通过文件系统来读取的特殊块设备文件。由应用程序负责对它进行读写操作。不经过文件系统的缓冲。它是不被操作系统直接管理的设备。这种设备少了操作系统这一层，I/O 效率更高。\n查看磁盘调度算法：永久地修改 IO 调度算法，需要修改内核引导参数\n1 2 3 dmesg |grep -i scheduler df -m more /sys/block/vda/queue/scheduler 在进行优化时，首先需要关注和优化的应该是架构，对于架构调优，在系统设计时首先需要充分考虑业务的实际情况，是否可以把不适合数据库做的事情放到数据仓库、搜索引擎或者缓存中去做；然后考虑写的并发量有多大，是否需要采用分布式；最后考虑读的压力是否很大，是否需要读写分离。对于核心应用或者金融类的应用，需要额外考虑数据安全因素，数据是否不允许丢失。\n作为金字塔的底部的架构调优，采用更适合业务场景的架构能最大程度地提升系统的扩展性和可用性。在设计中进行垂直拆分能尽量解耦应用的依赖，对读压力比较大的业务进行读写分离能保证读性能线性扩展，而对于读写并发压力比较大的业务在 MySQL 上也有采用读写分离的大量案例。\n作为金字塔的底部，在底层硬件系统、SQL 语句和参数都基本定型的情况下，单个 MySQL 数据库能提供的性能、扩展性等就基本定型了。但是通过架构设计和优化，却能承载几倍、几十倍甚至百倍于单个 MySQL 数据库能力的业务请求能力。\n对于 MySQL 调优，需要确认业务表结构设计是否合理，SQL 语句优化是否足够，该添加的索引是否都添加了，是否可以剔除多余的索引等等。\n最后确定系统、硬件有哪些地方需要优化，系统瓶颈在哪里，哪些系统参数需要调整优化，进程资源限制是否提到足够高；在硬件方面是否需要更换为具有更高 I/O 性能的存储硬件，是否需要升级内存、CPU、网络等。\n如果在设计之初架构就不合理，比如没有进行读写分离，那么后期的 MySQL 和硬件、系统优化的成本就会很高，并且还不一定能最终解决问题。如果业务性能的瓶颈是由于索引等 MySQL 层的优化不够导致的，那么即使配置再高性能的I/O存储硬件或者 CPU 也无法支撑业务的全表扫描。\n所以重点关注 MySQL 方面的调优，特别是索引。SQL/索引调优要求对业务和数据流非常清楚。在阿里巴巴内部，有三分之二的 DBA 是业务 DBA，从业务需求讨论到表结构审核、SQL 语句审核、上线、索引更新、版本迭代升级，甚至哪些数据应该放到非关系型数据库中，哪些数据放到数据仓库、搜索引擎或者缓存中，都需要这些 DBA 跟踪和复审。他们甚至可以称为数据架构师（Data Architecher）。\n2. MySQL数据库优化准备工作 2.1. 慢查询的定义与作用 慢查询日志，是指 mysql 记录所有执行超过 long_query_time 参数设定的时间阈值的 SQL 语句的日志。该日志能为 SQL 语句的优化带来很好的帮助。默认情况下，慢查询日志是关闭的。\n2.2. 慢查询基础-优化数据访问 查询性能低下最基本的原因是访问的数据太多。大部分性能低下的查询都可以通过减少访问的数据量的方式进行优化。对于低效的查询，一般通过下面两个步骤来分析总是很有效：\n确认应用程序是否在检索大量超过需要的数据。这通常意味着访问了太多的行，但有时候也可能是访问了太多的列。 确认 MySQL 服务器层是否在分析大量超过需要的数据行。 如果对语句的优化已经无法进行，可以考虑表中的数据量是否太大，如果是的话可以进行横向或者纵向的分表。 2.2.1. 请求了不需要的数据 有些查询会请求超过实际需要的数据，然后这些多余的数据会被应用程序丢弃。这会给 MySQL 服务器带来额外的负担，并增加网络开销，另外也会消耗应用服务器的 CPU 和内存资源。\n查询不需要的记录：大数开发者使用习惯，先使用 SELECT 语句查询大量的结果，然后获取前面的 N 行后关闭结果集。认为 MySQL 会执行查询并只返回所需要的 N 条数据，然后停止查询。实际情况是 MySQL 会查询出全部的结果集，客户端的应用程序会接收全部的结果集数据，然后抛弃其中大部分数据。最简单有效的解决方法就是在这样的查询后面加上 LIMIT。 总是取出全部列：使用 SELECT * 的时候，MySQL 会取出全部列，会让优化器无法完成索引覆盖扫描这类优化，还会为服务器带来额外的 I/O、内存和 CPU 的消耗。有时可以允许查询返回超过需要的数据。如果这种做法可以简化开发，能提高相同代码片段的复用性，这种做法也是值得考虑的。如果应用程序使用了某种缓存机制，或者有其他考虑，获取超过需要的数据也可能有其好处，但不要忘记这样做的代价是什么。获取并缓存所有的列的查询，相比多个独立的只获取部分列的查询可能就更有好处。 重复查询相同的数据：不断地重复执行相同的查询，然后每次都返回完全相同的数据。比较好的方案是，当初次查询的时候将这个数据缓存起来，需要的时候从缓存中取出，这样性能显然会更好。 2.2.2. 是否在扫描额外的记录 对于 MySQL，最简单的衡量查询开销的三个指标：响应时间、扫描的行数、返回的行数\n响应时间：是两个部分之和，分别是服务时间和排队时间。 服务时间是指数据库处理这个查询真正花了多长时间。 排队时间是指服务器因为等待某些资源而没有真正执行查询的时间（可能是等 I/O 操作完成，也可能是等待行锁等。） 当看到一个查询的响应时间的时候，首先需要审视这个响应时间是否是一个合理的值。概括地说，了解这个查询需要哪些索引以及它的执行计划是什么，然后计算大概需要多少个顺序和随机 I/O，再用其乘以在具体硬件条件下一次 I/O 的消耗时间。最后把这些消耗都加起来，就可以获得一个大概参考值来判断当前响应时间是不是一个合理的值。\n扫描的行数和返回的行数 分析查询时，查看该查询扫描的行数是非常有帮助的。这在一定程度上能够说明该查询找到需要的数据的效率高不高。\n理想情况下扫描的行数和返回的行数应该是相同的。实际这种情况很少出现，扫描的行数对返回的行数的比率通常很小，一般在 1:1 和 10:1 之间，不过有时候这个值也可能非常非常大。\n扫描的行数和访问类型 评估查询开销的时候，需要考虑一下从表中找到某一行数据的成本。MySQL 有好几种访问方式可以查找并返回一行结果。有些访问方式可能需要扫描很多行才能返回一行结果，也有些访问方式可能无须扫描就能返回结果。\n在 EXPLAIN 语句中的 type 列反映了查询的访问类型。如果查询没有办法找到合适的访问类型，那么解决的最好办法通常就是增加一个合适的索引，为什么索引对于查询优化如此重要了。索引让 MySQL 以最高效、扫描行数最少的方式找到需要的记录。\n一般 MySQL 能够使用如下三种方式应用 WHERE 条件，从好到坏依次为：\n在索引中使用 WHERE 条件来过滤不匹配的记录。这是在存储引擎层完成。 使用索引覆盖扫描（在 Extra 列中出现了 Using index）来返回记录，直接从索引中过滤不需要的记录并返回命中的结果。这是在 MySQL 服务器层完成的，但无须再回表查询记录。 从数据表中返回数据，然后过滤不满足条件的记录（在 Extra 列中出现 Using Where）。这在 MySQL 服务器层完成，MySQL 需要先从数据表读出记录然后过滤。 好的索引可以让查询使用合适的访问类型，尽可能地只扫描需要的数据行。如果发现查询需要扫描大量的数据但只返回少数的行，那么通常可以尝试下面的技巧去优化它：\n使用索引覆盖扫描，把所有需要用的列都放到索引中，这样存储引擎无须回表获取对应行就可以返回结果了。 改变库表结构。例如使用单独的汇总表。 重写这个复杂的查询，让 MySQL 优化器能够以更优化的方式执行这个查询。 2.3. 重构查询的方式 优化有问题的查询时，目标应该是找到一个更优的方法获得实际需要的结果。有时候，可以将查询转换一种写法让其返回一样的结果，但是性能更好。也可以通过修改应用代码，用另一种方式完成查询，最终达到一样的目的。\n2.3.1. 一个复杂查询还是多个简单查询 设计查询时需要考虑是否需要将一个复杂的查询分成多个简单的查询。\nMySQL 从设计上让连接和断开连接都很轻量级，在返回一个小的查询结果方面很高效。现代的网络速度比以前要快很多，无论是带宽还是延迟。在某些版本的 MySQL 上，即使在一个通用服务器上，也能够运行每秒超过 10 万的查询，即使是一个千兆网卡也能轻松满足每秒超过 2000 次的查询。所以运行多个小查询现在已经不是大问题了。\nMySQL 内部每秒能够扫描内存中上百万行数据，相比之下，MySQL 响应数据给客户端就慢得多了。在其他条件都相同的时候，使用尽可能少的查询当然是更好的。但是有时候，将一个大查询分解为多个小查询是很有必要的。\n在应用设计的时候，如果一个查询能够胜任时还写成多个独立查询是不明智的。例如，应用对一个数据表做 10 次独立的查询来返回 10 行数据，每个查询返回一条结果，查询 10 次。\n2.3.2. 切分查询 将大查询切分成小查询，每个查询功能完全一样，只完成一小部分，每次只返回一小部分查询结果。\n删除旧的数据就是一个很好的例子。定期地清除大量数据时，如果用一个大的语句一次性完成的话，则可能需要一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。将一个大的 DELETE 语句切分成多个较小的查询可以尽可能小地影响 MySQL 性能，同时还可以减少 MySQL 复制的延迟。\n一次删除一万行数据一般来说是一个比较高效而且对服务器影响也最小的做法。同时需要注意的是，如果每次删除数据后，都暂停一会儿再做下一次删除，这样也可以将服务器上原本一次性的压力分散到一个很长的时间段中，就可以大大降低对服务器的影响，还可以大大减少删除时锁的持有时间。\n2.3.3. 分解关联查询 很多高性能的应用都会对关联查询进行分解。简单地，可以对每一个表进行一次单表查询，然后将结果在应用程序中进行关联。用分解关联查询的方式重构查询有如下的优势：\n让缓存的效率更高。许多应用程序可以方便地缓存单表查询对应的结果对象。将查询分解后，执行单个查询可以减少锁的竞争。 在应用层做关联，可以更容易对数据库进行拆分，更容易做到高性能和可扩展。查询本身效率也可能会有所提升。 可以减少冗余记录的查询。在应用层做关联查询，意味着对于某条记录应用只需要查询一次，而在数据库中做关联查询，则可能需要重复地访问一部分数据。从这点看，这样的重构还可能会减少网络和内存的消耗。 这样相当于在应用中实现了哈希关联，而不是使用 MySQL 的嵌套循环关联。某些场景哈希关联的效率要高很多。\n在很多场景下，通过重构查询将关联放到应用程序中将会更加高效，这样的场景有很多，比如：当应用能够方便地缓存单个查询的结果的时候、当可以将数据分布到不同的 MySQL 服务器上的时候、当能够使用IN()的方式代替关联查询的时候、当查询中使用同一个数据表的时候。\n2.4. SQL 执行效率查询 MySQL 客户端连接成功后，通过 show [session|global] status 命令可以查询服务器状态信息，查看当前数据库的 INSERT、UPDATE、DELETE、SELECT 的访问频次：\n通过上述指令，可以查看到当前数据库到底是以查询为主，还是以增删改为主，从而为数据库优化提供参考依据。如果是以增删改为主，可以考虑不对其进行索引的优化。如果是以查询为主，那么就要考虑对数据库的索引进行优化了。\n2.4.1. 查看当前会话统计结果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 mysql\u0026gt; show session status like \u0026#39;Com_______\u0026#39;; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | Com_binlog | 0 | | Com_commit | 0 | | Com_delete | 0 | | Com_insert | 0 | | Com_repair | 0 | | Com_revoke | 0 | | Com_select | 7 | | Com_signal | 0 | | Com_update | 0 | | Com_xa_end | 0 | +---------------+-------+ 重要指标说明：\nCom_delete: 删除次数 Com_insert: 插入次数 Com_select: 查询次数 Com_update: 更新次数 2.4.2. 查看自数据库上次启动至今统计结果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 mysql\u0026gt; show global status like \u0026#39;Com_______\u0026#39;; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | Com_binlog | 0 | | Com_commit | 0 | | Com_delete | 0 | | Com_insert | 0 | | Com_repair | 0 | | Com_revoke | 0 | | Com_select | 18 | | Com_signal | 0 | | Com_update | 0 | | Com_xa_end | 0 | +---------------+-------+ 2.4.3. 查看针对Innodb引擎的统计结果 1 2 3 4 5 6 7 8 9 mysql\u0026gt; show status like \u0026#39;Innodb_rows_%\u0026#39;; +----------------------+-------+ | Variable_name | Value | +----------------------+-------+ | Innodb_rows_deleted | 0 | | Innodb_rows_inserted | 650 | | Innodb_rows_read | 847 | | Innodb_rows_updated | 0 | +----------------------+-------+ 2.5. 慢查询日志配置 2.5.1. 查询慢查询日志相关参数 当查询超过一定的时间没有返回结果的时候，才会记录到慢查询日志中。默认不开启，采样的时候手工开启。可以帮助找出执行慢的sql语句\n查看慢SQL日志是否启用（on表示启用） 1 show variables like \u0026#39;slow_query_log\u0026#39;; MySQL 中可以设定一个阈值，将运行时间超过该值的所有SQL 语句都记录到慢查询日志中。long_query_time 参数就是这个阈值。默认值为10（10秒）。查看执行慢于多少秒的SQL会记录到日志文件中 1 show variables like \u0026#39;long_query_time\u0026#39;; 对于没有运行的 SQL 语句没有使用索引，则 MySQL 数据库也可以将这条 SQL 语句记录到慢查询日志文件，控制参数是： 1 show VARIABLES like \u0026#39;%log_queries_not_using_indexes%\u0026#39;; 对于产生的慢查询日志，可以指定输出的位置，通过参数log_output来控制，可以输出到 [TABLE][FILE][FILE,TABLE]。默认是输出到文件，可以配置把慢查询输出到表，不过一般不推荐输出到表。 1 show VARIABLES like \u0026#39;log_output\u0026#39;; 使用模糊搜索，查看所有含有query的变量信息 1 show variables like \u0026#39;%query%\u0026#39;; 2.5.2. 开启慢查询日志方式1 - 修改mysql配置参数 mysql 配置文件名称是：my.ini（Linux 系统下的文件名为/etc/my.cnf）\nwindow 下可以通过打开【服务】，右键点击 mysql 服务，查询【属性】。里面有--defaults-file=\u0026quot;D:\\development\\MySQL\\MySQL Server 5.5\\my.ini\u0026quot;，可以查看 mysql 配置文件的位置\n修改配置文件，1代表on，0做表off。(注：如果mysql5.5版本，配置文件是没有慢日志的配置，而5.7版本的配置文件里是默认有慢日志的配置。所以5.5需要自己手动增加)：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [mysqld] # ====== 5.5版本需要手动增加，5.7以上版本默认有慢日志配置 ====== log_slow_queries=ON # 是否开启慢查询日志 slow_query_log=ON # 指定保存路径及文件名，默认为数据文件目录 slow_query_log_file=\u0026#34;slow-query.log\u0026#34; # 指定多少秒返回查询结果为慢查询 long_query_time=2 # 记录所有没有使用到索引的查询语句 log_queries_not_using_indexes=ON # 记录那些由于查找了多于1000次而引发的慢查询 min_examined_row_limit=1000 # 记录那些慢的optimize table, analyze table和alter table语句 log_slow_admin_statements=1 # 记录由slave所产生的慢查询 log_slow_slave_statements=1 # The TCP/IP Port the MySQL Server will listen on port=3306 修改配置文件后需要重启mysql服务才能生效\n2.5.3. 开启慢查询日志方式2 - 命令行修改慢查询配置 命令行修改配置方式不需要重启即可生效，但如果重启之后会自动失效。因为mysql数据库每次启动，都是读取本身的my.ini配置文件，以配置文件的参数为准\n1 2 3 4 5 6 7 8 9 10 11 12 13 -- 启动停止慢查询日志 set global slow_query_log=1; -- 指定慢查询日志得存储路径及文件（默认和数据文件放一起） set global slow_query_log_file=\u0026#34;slow-query.log\u0026#34;; -- 指定记录慢查询日志 SQL 执行时间得伐值（单位：秒，默认 10 秒） set global long_query_time=2; -- 是否记录未使用索引的 SQL set global log_queries_not_using_indexes=1; set global min_examined_row_limit=1000; set global log_slow_admin_statements=1; set global log_slow_slave_statements=1; -- 指定日志输出的位置，日志存放的地方可以是[TABLE][FILE][FILE,TABLE] set global log_output=\u0026#39;FILE,TABLE\u0026#39;; 如果需要修改其他参数，可以通过以下命令查阅\n1 2 show variables like \u0026#39;%query%\u0026#39;; show variables like \u0026#39;%slow%\u0026#39;; 2.5.4. 慢查询日志保存位置 windows 与 linux 系统的慢查询日志默认是和数据文件放一起。linux 系统默认数据文件存储位置是 /var/lib/mysql/，可以通过查看 /etc/my.cnf 配置文件确认日志与数据的保存位置。\n2.6. 查询缓存 2.6.1. 查询缓存状态 Query Cache 会缓存 select 查询，安装 mysql 时默认是开启。但是如果对表进行了 insert, update, delete, truncate, alter table, drop table, or drop database 等操作时，之前的缓存会无效并且删除，这样一定程度上会影响数据库的性能。所以对一些频繁变动表的情况开启缓存是不明智的。还有一种情况是在测试数据库性能的时候也需要关闭缓存，避免缓存对于测试数据的影响。\n查询缓存的启用状态，query_cache_type 字段是 on 的时候，代表开启缓存。\n1 show variables like \u0026#39;%cache%\u0026#39;; 2.6.2. 关闭缓存 关闭缓存有两种方式：一种是临时关闭，一种是永久关闭\n临时关闭，直接在命令行执行 1 2 3 4 -- 一般建议缓存的大小为32M (33554432) set global query_cache_size=0; -- 如果配置文件中为关闭缓存的话，不能通过命令开启缓存 set global query_cache_type=0; 永久关闭，修改配置文件my.cnf(my.ini)，添加以下配置 1 2 3 4 # 设置缓存的大小 query_cache_size=0 # 设置关闭缓存 query_cache_type=0 2.6.3. sql_no_cache 关键字禁用缓存 可以使用sql_no_cache关键字在 sql 语句中直接禁用缓存。如：\n1 2 3 4 -- 指定当前查询不使用缓存 select sql_no_cache count(*) from test_table; -- 指定当前查询使用缓存（其实不需要加此关键字，因为默认缓存已经开启了） select sql_cache count(*) from test_table; 2.7. 评估表数据体量 可以从表容量、磁盘空间、实例容量三方面评估数据体量。\n2.7.1. 表容量 表容量主要从表的记录数、平均长度、增长量、读写量、总大小量进行评估。一般对于 OLTP 的表，建议单表不要超过 2000W 行数据量，总大小 15G 以内。访问量，单表读写量在 1600/s 以内。\n一般查询表有多少数据时用到的经典 count 语句如下：\n1 select count(*) from table; 但是当数据量过大的时候，以上查询就可能会超时，所以要换一种查询方式：\n1 2 3 4 use 库名; show table status like \u0026#39;表名\u0026#39;; -- 或者 show table status like \u0026#39;表名\u0026#39;\\G; 上述方法不仅可以查询表的数据，还可以输出表的详细信息，加 \\G 参加可以格式化输出。包括表名、存储引擎、版本、行数、每行的字节数等等。\n2.7.2. 磁盘空间 查看所有数据库容量大小：\n1 2 3 4 5 6 7 8 9 10 11 SELECT table_schema AS \u0026#39;数据库\u0026#39;, table_name AS \u0026#39;表名\u0026#39;, table_rows AS \u0026#39;记录数\u0026#39;, TRUNCATE ( data_length / 1024 / 1024, 2 ) AS \u0026#39;数据容量(MB)\u0026#39;, TRUNCATE ( index_length / 1024 / 1024, 2 ) AS \u0026#39;索引容量(MB)\u0026#39; FROM information_schema.TABLES ORDER BY data_length DESC, index_length DESC; 查询单个库中所有表磁盘占用大小：\n1 2 3 4 5 6 7 8 9 10 11 12 13 SELECT table_schema AS \u0026#39;数据库\u0026#39;, table_name AS \u0026#39;表名\u0026#39;, table_rows AS \u0026#39;记录数\u0026#39;, TRUNCATE ( data_length / 1024 / 1024, 2 ) AS \u0026#39;数据容量(MB)\u0026#39;, TRUNCATE ( index_length / 1024 / 1024, 2 ) AS \u0026#39;索引容量(MB)\u0026#39; FROM information_schema.TABLES WHERE table_schema = \u0026#39;mysql\u0026#39; ORDER BY data_length DESC, index_length DESC; 建议数据量占磁盘使用率的70%以内。同时，对于一些数据增长较快，可以考虑使用大的慢盘进行数据归档。\n2.7.3. 实例容量 MySQL 是基于线程的服务模型，因此在一些并发较高的场景下，单实例并不能充分利用服务器的 CPU 资源，吞吐量反而会卡在 MySQL 层，可以根据业务考虑相应的实例模式。\n3. SQL 语句性能分析 3.1. 定位低效率执行的 SQL 可以通过以下两种方式定位执行效率较低的 SQL 语句。\n通过慢查询日志定位那些执行效率较低的 SQL 语句。 通过 EXPLAIN 执行计划分析 使用 show processlist 命令查看当前 MySQL 在进行的线程，包括线程的状态、是否锁表等，可以实时地查看 SQL 的执行情况，同时对一些锁表操作进行优化。 使用 profile 详情分析 3.2. 慢查询解读分析 3.2.1. 慢日志格式 一条完整的日志包括：时间、主机信息、执行信息、执行时间、执行内容。示例如下：\n1 2 3 4 5 # Time: 2021-04-24T00:22:49.818178Z # User@Host: root[root] @ localhost [127.0.0.1] # Query_time: 0.011991 Lock_time: 0.000000 Rows_sent: 10374 Rows_examined: 14376 SET timestamp=1554258810; SELECT t.id,t.a_ids FROM t_main t WHERE t.a_ids LIKE \u0026#39;%,%\u0026#39;; 从慢查询日志里面摘选一条慢查询日志，数据组成如下：\nTime: 2021-04-24T00:22:49.818178Z：查询执行时间 User@Host: root[root] @ localhost [127.0.0.1]：用户名、用户的 IP 信息、线程 ID 号 Query_time: 0.011991：执行花费的时长【单位：毫秒】 Lock_time: 0.000000：执行获得锁的时长 Rows_sent: 10374：获得的结果行数 Rows_examined: 14376：扫描的数据行数 SET timestamp=1554258810;：这 SQL 执行的具体时间 最后一行：执行的 SQL 语句 3.2.2. 分析工具(了解) 3.2.2.1. Mysqldumpslow mysqldumpslow 是 mysql 自带的用来分析慢查询的工具，基于 perl 开发。常用的慢查询日志分析工具，汇总除查询条件外其他完全相同的 SQL，并将分析结果按照参数中所指定的顺序输出。语法格式如下：\n1 mysqldumpslow -s r -t 10 slow-mysql.log 参数说明\n1 2 3 4 5 6 7 8 9 -s order (c,t,l,r,at,al,ar) c:总次数 t:总时间 l:锁的时间 r:获得的结果行数 at,al,ar :指 t,l,r 平均数【例如：at = 总时间/总次数】 -s 对结果进行排序，怎么排，根据后面所带的 (c,t,l,r,at,al,ar)，缺省为 at -t NUM just show the top n queries：仅显示前 n 条查询 -g PATTERN grep: only consider stmts that include this string：通过 grep 来筛选语句。 示例：\n1 ./mysqldumpslow -s t -t 10 /home/mysql/mysql57/data/iZwz9j203ithc4gu1uvb2wZ-slow.log 1 ./mysqldumpslow -s t -t 10 /home/mysql/mysql57/data/iZwz9j203ithc4gu1uvb2wZ-slow.log -g select Windows 下需要下载安装 perl 编译器，下载地址：http://pan.baidu.com/s/1i3GLKAp\n参考资料：https://www.cnblogs.com/moss_tan_jun/p/8025504.html\n3.2.2.2. Mysqlsla Mysqlsla 是 daniel-nichter 用 perl 写的一个脚本，专门用于处理分析 Mysql 的日志而存在。通过 Mysql 的日志主要分为：General log，slow log，binary log 三种。通过 query 日志，可以分析业务的逻辑，业务特点。通过 slow log，可以找到服务器的瓶颈。通过 binary log，可以恢复数据。Mysqlsla 可以处理其中的任意日志。\n参考：https://yq.aliyun.com/articles/59260\n3.2.2.3. pt-query-digest pt-query-digest 是用于分析 mysql 慢查询的一个工具，它可以分析 binlog、General log、slowlog，也可以通过 SHOWPROCESSLIST 或者通过 tcpdump 抓取的 MySQL 协议数据来进行分析。可以把分析结果输出到文件中，分析过程是先对查询语句的条件进行参数化，然后对参数化以后的查询进行分组统计，统计出各查询的执行时间、次数、占比等，可以借助分析结果找出问题进行优化。\n参考：https://blog.csdn.net/seteor/article/details/24017913\n4. EXPLAIN 执行计划 4.1. explain 简介 一条查询语句在经过 MySQL 查询优化器的各种基于成本和规则的优化会后生成一个所谓的执行计划，这个执行计划展示了接下来具体执行查询的方式，比如多表连接的顺序是什么，对于每个表采用什么访问方法来具体执行查询等等。\nMySQL 数据库的 explain 关键字显示了 MySQL 如何使用索引来处理 select 语句以及连接表，explain 可以帮助分析 select 语句，知道查询效率低下的原因，从而改进查询语句，让查询优化器能够更好的工作。总体来说，通过 EXPLAIN 关键字可以了解以下信息：\n表的读取顺序 数据读取操作的操作类型 哪些索引可以使用 哪些索引被实际使用 表之间的引用 每张表有多少行被优化器查询 4.2. 执行计划的语法 在SQL查询语句前加上EXPLAIN关键字即可。 EXPLAIN后面即是要分析的SQL语句。示例如下：\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM order_exp; +----+-------------+-----------+------------+------+---------------+------+---------+------+-------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+------+---------+------+-------+----------+-------+ | 1 | SIMPLE | order_exp | NULL | ALL | NULL | NULL | NULL | NULL | 10311 | 100.00 | NULL | +----+-------------+-----------+------------+------+---------------+------+---------+------+-------+----------+-------+ 除了以 SELECT 开头的查询语句，其余的 DELETE、INSERT、REPLACE 以及 UPDATE 语句前面都可以加上 EXPLAIN，用来查看这些语句的执行计划。\n4.3. 字段类型汇总表 字段 说明 id 查询中执行 select 子句或操作表的顺序，每个SELECT关键字都对应一个唯一的id select_type 所使用的SELECT查询类型，常见类型详见各列说明章节 table 所使用的的数据表的名字 partitions 匹配的分区信息 type 表示 MySQL 在表中找到所需行的方式，又称“访问类型”。取值与优劣排序详见各列说明章节 possible_keys 可能使用哪个索引在表中找到记录 key 实际使用的索引 key_len 索引中使用的字节数长度 ref 当使用索引列等值查询时，显示哪个与索引列进行等值匹配的对象信息被使用了 rows 估算的找到所需的记录所需要读取的行数 filtered 通过条件过滤出后剩余行数的百分比估计值 extra 包含不适合在其他列中显示但十分重要的额外信息 4.4. id 列 选定的执行计划中查询的序列号。表示查询中执行 select 子句或操作表的顺序，id 值越大优先级越高，越先被执行。id 相同，执行顺序由上至下。\n4.4.1. 单 SELECT 关键字 1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no = \u0026#39;a\u0026#39;; +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | 1 | SIMPLE | s1 | NULL | ref | idx_order_no | idx_order_no | 152 | const | 1 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ 4.4.2. 连接查询 对于连接查询来说，一个SELECT关键字后边的FROM子句中可以跟随多个表，所以在连接查询的执行计划中，每个表都会对应一条记录，但是这些记录的 id 值都是相同的\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM s1 INNER JOIN s2; +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------------+ | 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL | | 1 | SIMPLE | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using join buffer (Block Nested Loop) | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------------+ 4.4.3. 包含子查询 对于包含子查的查询语句来说，就可能涉及多个SELECT关键字，所以在包含子查询的查询语句的执行计划中，每个SELECT关键字都会对应一个唯一的 id 值\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2) OR order_no = \u0026#39;a\u0026#39;; +----+--------------------+-------+------------+-----------------+---------------+---------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+--------------------+-------+------------+-----------------+---------------+---------+---------+------+------+----------+-------------+ | 1 | PRIMARY | s1 | NULL | ALL | idx_order_no | NULL | NULL | NULL | 1 | 100.00 | Using where | | 2 | DEPENDENT SUBQUERY | s2 | NULL | unique_subquery | PRIMARY | PRIMARY | 8 | func | 1 | 100.00 | Using index | +----+--------------------+-------+------------+-----------------+---------------+---------+---------+------+------+----------+-------------+ 需要特别注意，查询优化器可能对涉及子查询的查询语句进行重写，从而转换为连接查询。所以如果想知道查询优化器对某个包含子查询的语句是否进行了重写，直接查看执行计划即可\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2 WHERE order_no = \u0026#39;a\u0026#39;); +----+-------------+-------+------------+--------+----------------------+---------+---------+--------------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+--------+----------------------+---------+---------+--------------+------+----------+-------------+ | 1 | SIMPLE | s1 | NULL | ALL | PRIMARY | NULL | NULL | NULL | 1 | 100.00 | NULL | | 1 | SIMPLE | s2 | NULL | eq_ref | PRIMARY,idx_order_no | PRIMARY | 8 | tempdb.s1.id | 1 | 100.00 | Using where | +----+-------------+-------+------------+--------+----------------------+---------+---------+--------------+------+----------+-------------+ 虽然示例的查询语句是一个子查询，但是执行计划中 s1 和 s2 表对应的记录的 id 值全部是 1，这就表明了查询优化器将子查询转换为了连接查询\n4.4.4. 包含 UNION 子句 包含 UNION 子句的查询语句，UNION 子句会把多个查询的结果集合并起来并对结果集中的记录进行去重。MySQL 使用的是内部的临时表来实现去重。在查询计划中，在内部创建了一个名为 \u0026lt;union1,2\u0026gt; 的临时表（就是执行计划第三条记录的 table 列的名称)，id 为 NULL 表明这个临时表是为了合并两个查询的结果集而创建的。\n1 2 3 4 5 6 7 8 mysql\u0026gt; EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2; +------+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +------+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+ | 1 | PRIMARY | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL | | 2 | UNION | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL | | NULL | UNION RESULT | \u0026lt;union1,2\u0026gt; | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary | +------+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+ 跟 UNION 对比起来，UNION ALL 不需要为最终的结果集进行去重，它只是单纯的把多个查询的结果集中的记录合并成一个并返回给用户，所以也就不需要使用临时表。所以在包含 UNION ALL 子句的查询的执行计划中，就没有那个 id 为 NULL 的记录。\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM s1 UNION ALL SELECT * FROM s2; +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+ | 1 | PRIMARY | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL | | 2 | UNION | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+ 4.5. select_type 列 表示所使用 select 查询类型，MySQL 为每一个 SELECT 关键字代表的小查询都定义了一个称之为 select_type 的属性。通过某个小查询的 select_type 属性值，即可知道该小查询在整个大查询中扮演了一个什么角色。常见类型汇总如下：\nSIMPLE：简单的 select 查询，SQL 中不包含子查询或者 UNION。 PRIMARY：最外层的 select 查询，查询中包含复杂的子查询部分，最外层查询被标记为 PRIMARY。 UNION：UNION 中的第二个或随后的 select 查询，不依赖于外部查询的结果集。 UNION RESULT：从 UNION 表获取结果集的 SELECT 查询被标记为 UNION RESULT。 SUBQUERY：子查询中的第一个 select 查询，不依赖于外部查询的结果集。 DEPENDENT UNION：UNION 中的第二个或随后的 select 查询，依赖于外部查询的结果集。 DEPENDENT SUBQUERY：子查询中的第一个 select 查询，依赖于外部查询的结果集。 DERIVED（衍生）：用来表示包含在 from 子句中的子查询的 select 语句。若 UNION 包含在 FROM 子句的子查询中，外层 SELECT 将被标记为 DERIVED。mysql 会递归执行并将结果放到一个临时表中。服务器内部称为\u0026quot;派生表\u0026quot;，因为该临时表是从子查询中派生出来。 MATERIALIZED：物化子查询。 UNCACHEABLE SUBQUERY：结果集不能被缓存的子查询，必须重新为外层查询的每一行进行评估，极少出现。 UNCACHEABLE UNION：UNION 中的第二个或随后的select查询，属于不可缓存的子查询，极少出现。 DEPENDENT：意味着 select 依赖于外层查询中发现的数据。 UNCACHEABLE：意味着 select 中的某些特性阻止结果被缓存于一个 item_cache 中。 4.5.1. SIMPLE 类型 简单的 select 查询，查询中不包含子查询或者 UNION\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no = \u0026#39;a\u0026#39;; +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | 1 | SIMPLE | s1 | NULL | ref | idx_order_no | idx_order_no | 152 | const | 1 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ 连接查询也算是 SIMPLE 类型\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM s1 INNER JOIN s2; +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------------+ | 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL | | 1 | SIMPLE | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using join buffer (Block Nested Loop) | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+---------------------------------------+ 4.5.2. PRIMARY 类型 对于包含UNION、UNION ALL或者子查询的大查询来说，它是由几个小查询组成的，其中最左边的那个查询的select_type值就是PRIMARY\n1 2 3 4 5 6 7 8 mysql\u0026gt; EXPLAIN SELECT * FROM s1 UNION SELECT * FROM s2; +------+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +------+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+ | 1 | PRIMARY | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL | | 2 | UNION | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL | | NULL | UNION RESULT | \u0026lt;union1,2\u0026gt; | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary | +------+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+ 4.5.3. UNION 类型 对于包含 UNION 或者 UNION ALL 的大查询来说，它是由几个小查询组成的，其中除了最左边的那个小查询以外，其余的查询的 select_type 值就是 UNION。（参考上面PRIMARY的示例）\n4.5.4. UNION RESULT 类型 MySQL 选择使用临时表来完成 UNION 查询的去重工作，针对该临时表的查询的 select_type 就是 UNION RESULT。（参考上面PRIMARY的示例）\n4.5.5. SUBQUERY 类型 如果包含子查询的查询语句不能够转为对应的 semi-join 的形式，并且该子查询是不相关子查询，并且查询优化器决定采用将该子查询物化的方案来执行该子查询时，该子查询的第一个 SELECT 关键字代表的那个查询的 select_type 就是 SUBQUERY。\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2) OR order_no = \u0026#39;a\u0026#39;; +----+--------------------+-------+------------+-----------------+---------------+---------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+--------------------+-------+------------+-----------------+---------------+---------+---------+------+------+----------+-------------+ | 1 | PRIMARY | s1 | NULL | ALL | idx_order_no | NULL | NULL | NULL | 1 | 100.00 | Using where | | 2 | DEPENDENT SUBQUERY | s2 | NULL | unique_subquery | PRIMARY | PRIMARY | 8 | func | 1 | 100.00 | Using index | +----+--------------------+-------+------------+-----------------+---------------+---------+---------+------+------+----------+-------------+ 需要注意的是，由于 select_type 为 SUBQUERY 的子查询由于会被物化，所以只需要执行一遍。\n涉及相关名词解释：\nsemi-join：半连接优化技术，本质上是把子查询上拉到父查询中，与父查询的表做 join 操作。关键词是“上拉”。对于子查询，其子查询部分相对于父表的每个符合条件的元组，都要把子查询执行一轮。效率低下。用半连接操作优化子查询，是把子查询上拉到父查询中，这样子查询的表和父查询中的表是并列关系，父表的每个符合条件的元组，只需要在子表中找符合条件的元组即可。简单来说，就是通过将子查询上拉对父查询中的数据进行筛选，以使获取到最少量的足以对父查询记录进行筛选的信息就足够了。 子查询物化：子查询的结果通常缓存在内存或临时表中。 关联/相关子查询：子查询的执行依赖于外部查询。多数情况下是子查询的WHERE子句中引用了外部查询的表。自然“非关联/相关子查询”的执行则不依赖与外部的查询。 4.5.6. DEPENDENT UNION、DEPENDENT SUBQUERY 类型 在包含 UNION 或者 UNION ALL 的大查询中，如果各个小查询都依赖于外层查询的话，那除了最左边的那个小查询之外，其余的小查询的 select_type 的值就是 DEPENDENT UNION。例如以下查询：\n1 2 3 4 5 6 7 8 9 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2 WHERE id = 3 UNION SELECT id FROM s1 WHERE id = 1); +------+--------------------+------------+------------+-------+---------------+---------+---------+-------+------+----------+-----------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +------+--------------------+------------+------------+-------+---------------+---------+---------+-------+------+----------+-----------------+ | 1 | PRIMARY | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | Using where | | 2 | DEPENDENT SUBQUERY | s2 | NULL | const | PRIMARY | PRIMARY | 8 | const | 1 | 100.00 | Using index | | 3 | DEPENDENT UNION | s1 | NULL | const | PRIMARY | PRIMARY | 8 | const | 1 | 100.00 | Using index | | NULL | UNION RESULT | \u0026lt;union2,3\u0026gt; | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary | +------+--------------------+------------+------------+-------+---------------+---------+---------+-------+------+----------+-----------------+ 示例查询比较复杂，大查询里包含了一个子查询，子查询里又是由UNION连起来的两个小查询。从执行计划中可以看出来，SELECT id FROM s2 WHERE id = 3这个小查询由于是子查询中第一个查询，所以它的select_type是OEPENDENTSUBOUERY，而SELECT id FROM s1 WHERE id = 1这个查询的select_type就是DEPENDENT UNION。\nMySQL 优化器对IN操作符的优化会将IN中的非关联子查询优化成一个关联子查询。我们可以在执行上面那个执行计划后，马上执行show warnings\\G，可以看到MySQL对SQL语句的大致改写情况：\n1 2 3 4 5 6 7 8 9 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2 WHERE id = 716 UNION SELECT id FROM s1 WHERE id = 718); +------+--------------------+------------+------------+-------+---------------+---------+---------+-------+-------+----------+-----------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +------+--------------------+------------+------------+-------+---------------+---------+---------+-------+-------+----------+-----------------+ | 1 | PRIMARY | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10609 | 100.00 | Using where | | 2 | DEPENDENT SUBQUERY | s2 | NULL | const | PRIMARY | PRIMARY | 8 | const | 1 | 100.00 | Using index | | 3 | DEPENDENT UNION | s1 | NULL | const | PRIMARY | PRIMARY | 8 | const | 1 | 100.00 | Using index | | NULL | UNION RESULT | \u0026lt;union2,3\u0026gt; | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary | +------+--------------------+------------+------------+-------+---------------+---------+---------+-------+-------+----------+-----------------+ 4.5.7. DERIVED 类型 对于采用物化的方式执行的包含派生表的查询，该派生表对应的子查询的select_type就是DERIVED。\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM (SELECT id, count(*) as c FROM s1 GROUP BY id) AS derived_s1 where c \u0026gt; 1; +----+-------------+------------+------------+-------+-------------------------------------------------------+---------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+------------+------------+-------+-------------------------------------------------------+---------+---------+------+------+----------+-------------+ | 1 | PRIMARY | \u0026lt;derived2\u0026gt; | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 50.00 | Using where | | 2 | DERIVED | s1 | NULL | index | PRIMARY,u_idx_day_status,idx_order_no,idx_insert_time | PRIMARY | 8 | NULL | 2 | 100.00 | Using index | +----+-------------+------------+------------+-------+-------------------------------------------------------+---------+---------+------+------+----------+-------------+ 上面示例id为2的记录就代表子查询的执行方式，该子查询是以物化的方式执行的。id 为 1 的记录代表外层查询，注意到它的 table 列显示的是\u0026lt;derived2\u0026gt;，表示该查询是针对将派生表物化之后的表进行查询的。\n4.5.8. MATERIALIZED 类型 当查询优化器在执行包含子查询的语句时，选择将子查询物化之后与外层查询进行连接查询时，该子查询对应的 select_type 属性就是 MATERIALIZED，例如以下查询：\n1 2 3 4 5 6 7 8 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no IN (SELECT order_no FROM s2); +----+--------------+-------------+------------+--------+---------------------+---------------------+---------+--------------------+-------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+--------------+-------------+------------+--------+---------------------+---------------------+---------+--------------------+-------+----------+-------------+ | 1 | SIMPLE | s1 | NULL | ALL | idx_order_no | NULL | NULL | NULL | 10609 | 100.00 | NULL | | 1 | SIMPLE | \u0026lt;subquery2\u0026gt; | NULL | eq_ref | \u0026lt;auto_distinct_key\u0026gt; | \u0026lt;auto_distinct_key\u0026gt; | 152 | tempdb.s1.order_no | 1 | 100.00 | NULL | | 2 | MATERIALIZED | s2 | NULL | index | idx_order_no | idx_order_no | 152 | NULL | 10621 | 100.00 | Using index | +----+--------------+-------------+------------+--------+---------------------+---------------------+---------+--------------------+-------+----------+-------------+ 执行计划的id值为2的第三条记录，说明查询优化器是要把子查询先转换成物化表。\n执行计划的前两条记录的id值都为1，说明这两条记录对应的表进行连接查询，需要注意的是第二条记录的table列的值是\u0026lt;subquery2\u0026gt;，说明该表其实就是id为2对应的子查询执行之后产生的物化表，然后将 s1和该物化表进行连接查询\n4.5.9. UNCACHEABLE SUBQUERY、UNCACHEABLE UNION 类型 极少出现，示例如下：\n1 2 3 4 5 6 7 mysql\u0026gt; explain select * from s1 where id = ( select id from s2 where order_no=@@sql_log_bin); +----+----------------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+----------------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------------+ | 1 | PRIMARY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | no matching row in const table | | 2 | UNCACHEABLE SUBQUERY | s2 | NULL | index | idx_order_no | idx_order_no | 152 | NULL | 2 | 50.00 | Using where; Using index | +----+----------------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+--------------------------------+ 4.6. table 列 不论查询语句有多复杂，里边包含了多少个表，到最后也是需要对每个表进行单表访问。MySQL 规定 EXPLAIN 语句输出的每条记录都对应着某个单表的访问方式，该条记录的 table 列代表着该表的表名，是显示这一行的数据所使用的数据表的名字，是按被读取的先后顺序排列。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 mysql\u0026gt; EXPLAIN SELECT * FROM s1 INNER JOIN s2; +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------------------------+ | 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10609 | 100.00 | NULL | | 1 | SIMPLE | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 10621 | 100.00 | Using join buffer (hash join) | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------------------------+ mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no = \u0026#39;a\u0026#39;; +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | 1 | SIMPLE | s1 | NULL | ref | idx_order_no | idx_order_no | 152 | const | 1 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ 由示例可以看出：只涉及对 s1 表的单表查询，所以 EXPLAIN 输出中只有一条记录，其中的 table 列的值是 s1，而连接查询的执行计划中有两条记录，这两条记录的 table 列分别是 s1 和 s2。\n4.7. partitions 列 和分区表有关，一般情况下查询语句的执行计划的 partitions 列的值都是 NULL。\n4.8. type 列 type 列用于显示连接使用了何种类型，此列是分析执行计划重要的指标。连接类型结果值从最好到最差的排序如下：\n1 NULL \u0026gt; system \u0026gt; const \u0026gt; eq_ref \u0026gt; ref \u0026gt; fulltext \u0026gt; ref_or_null \u0026gt; index_merge \u0026gt; unique_subquery \u0026gt; index_subquery \u0026gt; range \u0026gt; index \u0026gt; ALL 实际开发中出现比较多的是 system | const | eq_ref | ref | range | index | ALL。一般来说，保证查询至少达到 range 级别，最好能达到 ref 级别。\n4.8.1. type 列常见的类型汇总 all：full table scan，MySQL 将遍历全表以找到匹配的行；全表扫描从磁盘中获取数据百万级别的数据 ALL 类型的数据尽量优化。 index：Full index scan，扫描遍历索引树(扫描全表的索引，从索引中获取数据)。index 和 all 的区别在于 index 类型只遍历索引。 range：索引范围扫描，对索引的扫描开始于某一点，返回匹配值的行，只检索给定范围的行，使用一个索引来选择行。此时对应 key 列显示使用了哪个索引。 一般在 WHERE 语句中出现between、\u0026lt;、\u0026gt;、in等查询，这种给定范围扫描比全表扫描要好。因为它只需要开始于索引的某一点，结束于另一点，不用扫描全部索引。\nindex_subquery：该联接类型类似于 unique_subquery。可以替换 IN 子查询，但只适合下列形式的子查询中的非唯一索引：value IN (SELECT key_column FROM single_table WHERE some_expr)。 unique_subquery：此类型是一个索引查找函数，可以完全替换子查询，效率更高。该类型替换了下面形式的 IN 子查询的 ref：value IN (SELECT primary_key FROM single_table WHERE some_expr) 。 index_merge：该联接类型表示使用了索引合并优化方法。 ref_or_null：该联接类型如同 ref，但是添加了 MySQL 可以专门搜索包含 NULL 值的行。 fulltext：全文索引。 ref：非唯一性索引扫描，返回匹配某个单独值的所有行，常见于使用非唯一索引即唯一索引的非唯一前缀进行查找；本质上是一种索引访问，它返回所有匹配某个单独值的行，就是说它可能会找到多条符合条件的数据，所以它是查找与扫描的混合体。 eq_ref：唯一性索引扫描，对于每个索引键，表中只有一条记录与之匹配，常用于主键或者唯一索引扫描。 const：当 MySQL 对某查询某部分进行优化，并转为一个常量时，使用这些访问类型。通过索引一次查到数据，该类型主要用于比较 primary key 或者 unique 索引，因为只匹配一行数据，所以很快；如果将主键置于 WHERE 语句后面，MySQL 就能将该查询转换为一个常量。 system：表只有一条记录(等于系统表)，这是 const 类型的特例，平时业务中不会出现。 NULL：MySQL 在优化过程中分解语句，执行时甚至不用访问表或索引，例如从一个索引列里选取最小值可以通过单独索引查找完成。 4.8.2. system 类型 当表中只有一条记录并且该表使用的存储引擎的统计数据是精确的，那么对该表的访问方式就是 system 级别。比如 MyISAM、Memory 之类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 mysql\u0026gt; SELECT * from test_myisam; +----+ | id | +----+ | 1 | +----+ mysql\u0026gt; EXPLAIN SELECT * FROM test_myisam; +----+-------------+-------------+------------+--------+---------------+------+---------+------+-------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------------+------------+--------+---------------+------+---------+------+-------+----------+-------+ | 1 | SIMPLE | test_myisam | NULL | system | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL | +----+-------------+-------------+------------+--------+---------------+------+---------+------+-------+----------+-------+ 注意：只有在 MyISAM 这种存储引擎中，才会精确统计数据，如果改成使用 InnoDB 存储引擎，因为该引擎的统计非精确的，所以使用 select * from table 的话，会进行全表扫描（即 all 类型）\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM city; +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+ | 1 | SIMPLE | city | NULL | ALL | NULL | NULL | NULL | NULL | 4035 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+ 4.8.3. const 类型 当根据主键或者唯一的二级索引列与常数进行等值匹配，来定位一条记录，对单表的访问的类型被 MySQL 定义为const（意思是常数级别的，代价是可以忽略不计的）。如果主键或者唯一的二级索引是由多个列构成的话，组成索引的每一个列都是与常数进行等值比较时，此时访问方式才是 const。\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE id = 1; +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ | 1 | SIMPLE | s1 | NULL | const | PRIMARY | PRIMARY | 8 | const | 1 | 100.00 | NULL | +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ B+树叶子节点中的记录是按照索引列排序的，对于的聚簇索引来说，它对应的 B+树叶子节点中的记录就是按照 id 列排序的。B+树矮胖，所以这样根据主键值定位一条记录的速度很快。同样，根据唯一联合索引列来定位一条记录的速度也很快，MySQL 会分两步执行，首先在唯一联合索引对应的 B+树中，根据索引列与常数的等值比较条件定位到一条联合索引记录，然后再根据该记录的 id 值到聚簇索引中获取到完整的行记录。\n值得注意的是：对于唯一的二级索引来说，查询该列包含为 NULL 值的情况比较特殊，在 MySQL 中认为每一个 null 都是独一无二的，并且会将所有为 null 值的行数据索引的叶子结点的最前面。而唯一的二级索引列并不限制 NULL 值的数量，所以可能会访问到多条记录，也就是说 is null 不可以使用 const 类型访问方法来执行。\n4.8.4. eq_ref 类型 在连接查询时，如果被驱动表是通过主键或者唯一的二级索引列等值匹配的方式进行访问的（如果该主键或者唯一的二级索引是联合索引的话，需要所有的索引列都必须进行等值比较），则对该被驱动表的访问类型就是 eq_ref。\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM s1 INNER JOIN s2 on s1.id = s2.id; +----+-------------+-------+------------+--------+---------------+---------+---------+--------------+-------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+--------+---------------+---------+---------+--------------+-------+----------+-------+ | 1 | SIMPLE | s1 | NULL | ALL | PRIMARY | NULL | NULL | NULL | 10609 | 100.00 | NULL | | 1 | SIMPLE | s2 | NULL | eq_ref | PRIMARY | PRIMARY | 8 | tempdb.s1.id | 1 | 100.00 | NULL | +----+-------------+-------+------------+--------+---------------+---------+---------+--------------+-------+----------+-------+ 涉及相关名词解释，驱动表与被驱动表：A表和B表join连接查询，如果通过A表的结果集作为循环基础数据，然后一条一条地通过该结果集中的数据作为过滤条件到B表中查询数据，然后合并结果。那么称A表为驱动表，B表为被驱动表。\n以上示例，MySQL 打算将 s2 作为驱动表，s1 作为被驱动表，重点关注 s1 的访问方法是eq_ref，表明在访问 s1 表的时候可以通过主键的等值匹配来进行访问。\n4.8.5. ref 类型 当通过普通的二级索引列与常量进行等值匹配来查询某个表，那么这种采用二级索引来执行查询该表的访问类型就可能是 ref。本质上也是一种索引访问，它返回所有匹配某个单独值的行，然而它可能会找到多个符合条件的行，所以它属于查找和扫描的混合体。\n由于普通二级索引并不限制索引列值的唯一性，所以通过普通的二级索引进行等值比较后，可能匹配到对应多条连续的记录，也就是说使用普通二级索引来执行查询的代价取决于等值匹配到的二级索引记录条数。因为普通二级索引不是像主键或者唯一二级索引那样最多只能匹配 1 条记录，所以这种 ref 访问类型比 const 要差些。\n普通二级索引等值匹配时，如果匹配的记录较少，则回表的代价还是比较低的，效率还行，所以 MySQL 可能选择使用索引而不是全表扫描的方式来执行查询；但如果记录太多那么回表的成本就变得很大，此时 MySQL 可能会直接选择全表扫描。\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no = \u0026#39;3\u0026#39;; +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | 1 | SIMPLE | s1 | NULL | ref | idx_order_no | idx_order_no | 152 | const | 1 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ 以上示例的查询，可以选择全表扫描来逐一对比搜索条件是否满足要求，也可以先使用二级索引找到对应记录的 id 值，然后再回表到聚簇索引中查找完整的行记录。\n需要注意以下两种情况：\n二级索引列值为 NULL 的情况。不论是普通的二级索引，还是唯一的二级索引，它们的索引列对包含 NULL 值的数量并不限制，所以采用 列名 IS NULL 这种形式的搜索条件最多只能使用 ref 的访问类型，而不是 const 的类型。 对于某个包含多个索引列的二级索引来说，只要是最左边的连续索引列是与常数的等值比较就可能采用 ref 的访问类型；如果最左边的连续索引列并不全部是等值比较的话，它的访问类型就不能称为 ref。 1 2 3 4 5 -- 是 ref 类型 SELECT * FROM order_exp WHERE insert_time = \u0026#39;2021-03-22 18:28:23\u0026#39;; SELECT * FROM order_exp WHERE insert_time = \u0026#39;2021-03-22 18:28:23\u0026#39; AND order_status = 0; -- 非 ref 类型 SELECT * FROM order_exp WHERE insert_time = \u0026#39;2021-03-22 18:28:23\u0026#39; AND order_status \u0026gt; -1; 4.8.6. fulltext 类型 全文索引，极少使用。暂不研究\n4.8.7. ref_or_null 类型 查询某个二级索引列的值等于某个常数的记录，和该列的值为 NULL 的记录\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM order_exp_cut WHERE order_no = \u0026#39;abc\u0026#39; or order_no IS NULL; +----+-------------+---------------+------------+-------------+---------------+--------------+---------+-------+------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------------+------------+-------------+---------------+--------------+---------+-------+------+----------+-----------------------+ | 1 | SIMPLE | order_exp_cut | NULL | ref_or_null | idx_order_no | idx_order_no | 153 | const | 2 | 100.00 | Using index condition | +----+-------------+---------------+------------+-------------+---------------+--------------+---------+-------+------+----------+-----------------------+ 1 row in set (0.08 sec) 其中 order_no 列是允许为空。\n上面的查询相当于先分别从 order_exp_cut 表的 idx_order_no 索引对应的 B+树中找出 order_no IS NULL 和 order_no= 'abc'的两个连续的记录范围，然后根据这些二级索引记录中的 id 值再回表查找完整的用户记录。\n4.8.8. index_merge 类型 一般情况下对于某个表的查询只能使用到一个索引，在某些场景下可以使用索引合并的方式来执行查询。例如：\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no = \u0026#39;a\u0026#39; OR insert_time = \u0026#39;2021-03-22 18:23:42\u0026#39;; +----+-------------+-------+------------+-------------+-----------------------------------------------+------------------------------+---------+------+------+----------+--------------------------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------------+-----------------------------------------------+------------------------------+---------+------+------+----------+--------------------------------------------------------+ | 1 | SIMPLE | s1 | NULL | index_merge | u_idx_day_status,idx_order_no,idx_insert_time | idx_order_no,idx_insert_time | 152,5 | NULL | 2 | 100.00 | Using union(idx_order_no,idx_insert_time); Using where | +----+-------------+-------+------------+-------------+-----------------------------------------------+------------------------------+---------+------+------+----------+--------------------------------------------------------+ 4.8.9. unique_subquery 类型 类似于两表连接中被驱动表的 eq_ref 访问类型，unique_subquery 是针对在一些包含IN子查询的查询语句中，如果查询优化器决定将IN子查询转换为EXISTS子查询，而且子查询可以使用到主键进行等值匹配的话，那么该子查询执行计划的 type 列的值就是 unique_subquery\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE id IN (SELECT id FROM s2 WHERE s1.insert_time = s2.insert_time) OR order_no = \u0026#39;a\u0026#39;; +----+--------------------+-------+------------+-----------------+------------------------------------------+---------+---------+------+-------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+--------------------+-------+------------+-----------------+------------------------------------------+---------+---------+------+-------+----------+-------------+ | 1 | PRIMARY | s1 | NULL | ALL | idx_order_no | NULL | NULL | NULL | 10609 | 100.00 | Using where | | 2 | DEPENDENT SUBQUERY | s2 | NULL | unique_subquery | PRIMARY,u_idx_day_status,idx_insert_time | PRIMARY | 8 | func | 1 | 10.00 | Using where | +----+--------------------+-------+------------+-----------------+------------------------------------------+---------+---------+------+-------+----------+-------------+ 执行计划的第二条记录的 type 值就是 unique_subquery，说明在执行子查询时会使用到 id 列的索引。\n4.8.10. index_subquery 类型 index_subquery 与 unique_subquery 类似，只不过访问子查询中的表时使用的是普通的索引。\n1 EXPLAIN SELECT * FROM s1 WHERE order_no IN (SELECT order_no FROM s2 where s1.insert_time = s2.insert_time) OR order_no = \u0026#39;a\u0026#39;; 这个语句和 unique_subquery 章节中的唯一不同是 in 子句的字段由 id 变成了 order_no。\n4.8.11. range 类型 利用索引进行范围匹配获取某些范围区间的记录，这种访问方式就可能使用到 range 访问类型。一般在 where 语句中出现了between、\u0026lt;、\u0026gt;、in等的查询。这种范围扫描索引扫描比全表扫描要好，因为它只需要开始于索引的某一点，结束于另一点，不用扫描全部索引。\n值得注意的是：使用索引进行范围匹配中的“索引”可以是聚簇索引，也可以是二级索引。\n1 2 3 4 5 6 7 8 9 10 11 12 13 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no IN (\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;); +----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ | 1 | SIMPLE | s1 | NULL | range | idx_order_no | idx_order_no | 152 | NULL | 3 | 100.00 | Using index condition | +----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no \u0026gt; \u0026#39;a\u0026#39; AND order_no \u0026lt; \u0026#39;b\u0026#39;; +----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ | 1 | SIMPLE | s1 | NULL | range | idx_order_no | idx_order_no | 152 | NULL | 1 | 100.00 | Using index condition | +----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ 4.8.12. index 类型 当可以使用索引覆盖，但需要扫描全部的索引记录时，该表的访问类型就是 index。\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT insert_time FROM s1 WHERE expire_time = \u0026#39;2021-03-22 18:28:28\u0026#39;; +----+-------------+-------+------------+-------+------------------+------------------+---------+------+-------+----------+--------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+------------------+------------------+---------+------+-------+----------+--------------------------+ | 1 | SIMPLE | s1 | NULL | index | u_idx_day_status | u_idx_day_status | 12 | NULL | 10609 | 10.00 | Using where; Using index | +----+-------------+-------+------------+-------+------------------+------------------+---------+------+-------+----------+--------------------------+ 4.8.13. all 类型 全表扫描，将遍历全表以找到匹配的行\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM s1; +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+ | 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10609 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+ 4.9. possible_keys 列 possible_keys 列，用于显示可能应用在这张表中的索引。如果为空，则没有可能的索引。可以为相关的域从 WHERE 语句中选择一个合适的语句。\n另外需要注意的一点是，possible keys 列中的值并不是越多越好，可能使用的索引越多，查询优化器计算查询成本时就得花费更长时间，所以如果可以的话，尽量删除那些用不到的索引。\n4.10. key 列 key 列是指实际使用的索引。如果为 NULL，则没有使用索引。很少的情况下，MySQL 会选择优化不足的索引。这种情况下，可以在 SELECT 语句中使用 FORCE INDEX(indexname) 来强制使用一个索引或者用 IGNORE INDEX(indexname) 来强制忽略索引。\n4.11. key_len 列 key_len 列表示索引中使用的字节数，可通过该列计算查询中使用的索引长度。在不损失精确性的情况下，长度越短越好。\n索引最大长度是 768 字节，当字符串过长时，mysql 会做一个类似左前缀索引的处理，将前半部分的字符提取出来做索引。\n4.11.1. key_len 计算规则 key_len 显示的值为索引字段的最大可能长度，并非实际使用长度，即 key_len 是根据表定义计算而得，不是通过表内检索出来的。计算方式是如下：\n对于使用固定长度类型的索引列来说，它实际占用的存储空间的最大长度就是该固定值，对于指定字符集的变长类型的索引列来说，比如某个索引列的类型是 VARCHAR(100)，使用的字符集是 utf8，那么该列实际占用的最大存储空间就是 100 x 3 = 300 个字节。 如果该索引列可以存储 NULL 值，则 key_len 比不可以存储 NULL 值时多 1 个字节；对于变长字段来说，都会有 2 个字节的空间来存储该变长列的实际长度。 4.11.2. 不同类型占用的字节数 字符串类型：5.0.3 以后版本中，定义时的 n 均代表字符数，而不是字节数。以下以 UTF-8 字符编码为例：\nchar(n)：如果存汉字长度就是 3n 字节 varchar(n)：如果存汉字则长度是 3n + 2 字节，加的 2 字节用来存储字符串长度，因为 varchar 是变长字符串 Notes: char和varchar 跟字符编码也有密切的联系，比如 latin1 占用 1 个字节，gbk 占用 2 个字节，utf8 占用 3 个字节。如果是 utf-8，一个数字或字母占 1 个字节，一个汉字占 3 个字节。\n数值类型：\ntinyint：1 字节 smallint：2 字节 int：4 字节 bigint：8字节 时间类型：\ndate：3 字节 timestamp：4 字节 datetime：8 字节 如果字段允许为 NULL，需要 1 字节记录是否为 NULL。\n4.11.3. 示例 由于 id 列的类型是 bigint，并且不可以存储 NULL 值，所以在使用该列的索引时 key_len 大小就是 8。\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE id = 4; +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ | 1 | SIMPLE | s1 | NULL | const | PRIMARY | PRIMARY | 8 | const | 1 | 100.00 | NULL | +----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+ 由于 order_no 列的类型是VARCHAR(50)，所以该列实际最多占用的存储空间就是 50 x 3 个字节，又因为该列是可变长度列，所以 key_len 需要加 2，所以最后 ken_len 的值就是 152。\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no = \u0026#39;4\u0026#39;; +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | 1 | SIMPLE | s1 | NULL | ref | idx_order_no | idx_order_no | 152 | const | 1 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ 4.12. ref 列 ref 列表示哪些字段或常量值被用于查找索引列。可能的值为：库.表.字段、const(常量)、null、func\n与索引作等值匹配的对象是一个常数：\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no = \u0026#39;4\u0026#39;; +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | 1 | SIMPLE | s1 | NULL | ref | idx_order_no | idx_order_no | 152 | const | 1 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ 聚簇索引与一个列进行等值匹配的条件：\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.id = s2.id; +----+-------------+-------+------------+--------+---------------+---------+---------+--------------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+--------+---------------+---------+---------+--------------+------+----------+-------+ | 1 | SIMPLE | s2 | NULL | ALL | PRIMARY | NULL | NULL | NULL | 3 | 100.00 | NULL | | 1 | SIMPLE | s1 | NULL | eq_ref | PRIMARY | PRIMARY | 8 | tempdb.s2.id | 1 | 100.00 | NULL | +----+-------------+-------+------------+--------+---------------+---------+---------+--------------+------+----------+-------+ 与索引列进行等值匹配的对象是一个函数：\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s2.order_no = UPPER(s1.order_no); +----+-------------+-------+------------+------+---------------+--------------+---------+------+------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+--------------+---------+------+------+----------+-----------------------+ | 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 4 | 100.00 | NULL | | 1 | SIMPLE | s2 | NULL | ref | idx_order_no | idx_order_no | 152 | func | 1 | 100.00 | Using index condition | +----+-------------+-------+------------+------+---------------+--------------+---------+------+------+----------+-----------------------+ 4.13. rows 列 MySQL 认为必须检查的用来返回请求数据的行数。根据表统计信息及索引选用的情况，大致估算找到所需记录需要读取的行数。\n查询优化器决定使用全表扫描的方式对某个表执行查询时，执行计划的 rows 列就代表预计需要扫描的行数；如果使用索引来执行查询时，执行计划的 rows 列就代表预计扫描的索引记录行数。\n1 2 3 4 5 6 7 8 9 10 11 12 13 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no \u0026gt; \u0026#39;z\u0026#39;; +----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ | 1 | SIMPLE | s1 | NULL | range | idx_order_no | idx_order_no | 152 | NULL | 1 | 100.00 | Using index condition | +----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+ mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no \u0026gt; \u0026#39;D\u0026#39;; +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+ | 1 | SIMPLE | s1 | NULL | ALL | idx_order_no | NULL | NULL | NULL | 10609 | 50.00 | Using where | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+ 4.14. filtered 列 filtered 列显示了通过条件过滤出的行数的百分比估计值。查询优化器预测有多少条记录满足其余的搜索条件。\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no \u0026gt; \u0026#39;A\u0026#39; AND order_no = \u0026#39;DD00_9S\u0026#39;; +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ | 1 | SIMPLE | s1 | NULL | ref | idx_order_no | idx_order_no | 152 | const | 17 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------+ 从执行计划的 key 列中可以看出来，该查询使用 PRIMARY 索引来执行查询，从 rows 列可以看出满足 id \u0026gt; 5890 的记录有 5286 条。执行计划的 filtered 列就代表查询优化器预测在这 5286 条记录中，有多少条记录满足其余的搜索条件，也就是 order_note = 'a' 这个条件的百分比。此处 filtered 列的值是 10.0，说明查询优化器预测在 5286 条记录中有 10.00% 的记录满足 order_note = 'a'这个条件。\n对于单表查询来说，这个 filtered 列的值没什么意义，一般更关注在连接查询中驱动表对应的执行计划记录的 filtered 值。\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.order_no = s2.order_no WHERE s1.order_note \u0026gt; \u0026#39;你好\u0026#39;; +----+-------------+-------+------------+------+---------------+--------------+---------+--------------------+-------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+--------------+---------+--------------------+-------+----------+-------------+ | 1 | SIMPLE | s1 | NULL | ALL | idx_order_no | NULL | NULL | NULL | 10609 | 33.33 | Using where | | 1 | SIMPLE | s2 | NULL | ref | idx_order_no | idx_order_no | 152 | tempdb.s1.order_no | 1 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+--------------+---------+--------------------+-------+----------+-------------+ 从执行计划中可以看出来，查询优化器打算把 s1 当作驱动表，s2 当作被驱动表。其中驱动表 s1 表的执行计划的 rows 列为 10573，filtered 列为 33.33，这意味着驱动表 s1 的扇出值就是 10573 x 33.33 % = 3524.3，这说明还要对被驱动表执行大约 3524 次查询。\n4.15. Extra 列 Extra 列用于记录关于 MySQL 如何解析查询的额外信息，包含不适合在其他列中显示但十分重要的额外信息。通过这些额外信息来更准确的理解 MySQL 到底将如何执行给定的查询语句。\n例如可以看到 Using temporary 和 Using filesort 这些坏的例子，意思 MySQL 根本不能使用索引，结果是检索会很慢。\n4.15.1. No tables used 当查询语句的没有 FROM 子句时将会提示该额外信息。\n4.15.2. impossible where 查询语句的 WHERE 子句永远为 FALSE 时将会提示该额外信息。这个值强调了 where 语句会导致没有符合条件的行。\n4.15.3. No matching min/max row 当查询列表处有 MIN 或者 MAX 聚集函数，但是并没有符合 WHERE 子句中的搜索条件的记录时，将会提示该额外信息。\n4.15.4. Using index 当查询列表以及搜索条件中只包含属于某个索引的列。即 select 操作使用了索引覆盖，避免回表访问表的数据行，效率不错。同时还有以下两种情况：\n如果同时出现 Using where，表明索引被用来执行索引键值的查找。 如果没有同时出现 Using where 表明索引用来读取数据而非执行查找动作。 1 2 3 4 5 6 mysql\u0026gt; explain select order_number from tb_order group by order_number; +----+-------------+----------+-------+--------------------+--------------------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+-------+--------------------+--------------------+---------+------+------+-------------+ | 1 | SIMPLE | tb_order | index | index_order_number | index_order_number | 99 | NULL | 1 | Using index | +----+-------------+----------+-------+--------------------+--------------------+---------+------+------+-------------+ 4.15.5. Using index condition 搜索条件中虽然出现了索引列，但却不能使用到索引。\n1 SELECT * FROM s1 WHERE order_no \u0026gt; \u0026#39;z\u0026#39; AND order_no LIKE \u0026#39;%a\u0026#39;; 其中的order_no \u0026gt; 'z'可以使用到索引，但是order_no LIKE '%a'却无法使用到索引。这里出现了“索引条件下推”的概念。\n先根据order_no \u0026gt; 'z'这个条件，定位到二级索引 idx_order_no 中对应的二级索引记录。 对于指定的二级索引记录，先不着急回表，而是先检测一下该记录是否满足order_no LIKE '%a'这个条件，如果这个条件不满足，则该二级索引记录就没必要回表。 对于满足order_no LIKE '%a'这个条件的二级索引记录执行回表操作。 回表操作其实是一个随机 IO，比较耗时，所以上述修改可以省去很多回表操作的成本。这个改进称之为索引条件下推（英文名：ICP ，Index Condition Pushdown）。如果在查询语句的执行过程中将要使用索引条件下推这个特性，在 Extra 列中将会显示 Using index condition\n4.15.6. Using where 表示 mysql 服务器将在存储引擎检索行后再进行过滤。许多 where 条件里涉及索引中的列，当（并且如果）它读取索引时，就能被存储引擎检验，因此不是所有带 where 字句的查询都会显示\u0026quot;Using where\u0026quot;。有时\u0026quot;Using where\u0026quot;的出现就是一个暗示：查询可受益与不同的索引。\n当使用索引访问来执行对某个表的查询，并且该语句的 WHERE 子句中有除了该索引包含的列之外的其他搜索条件时，在 Extra 列中也会提示上述信息。\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE order_no = \u0026#39;a\u0026#39; AND order_note = \u0026#39;a\u0026#39;; +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------------+ | 1 | SIMPLE | s1 | NULL | ref | idx_order_no | idx_order_no | 152 | const | 1 | 10.00 | Using where | +----+-------------+-------+------------+------+---------------+--------------+---------+-------+------+----------+-------------+ 出现了 Using where，只是表示在 server 层根据 where 条件进行了过滤，和是否全表扫描或读取了索引文件没有关系，有认为把 Using where 和是否读取索引进行关联，也有认为把 Using where 和回表进行了关联，都是不正确。\nMySQL 官方的说明：https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain_extra\nExtra 列中出现了 Using where 代表 WHERE 子句用于限制要与下一个表匹配或发送给客户端的行。很明显，Using where 只是表示 MySQL 使用 where 子句中的条件对记录进行了过滤。\n4.15.7. Using join buffer (Block Nested Loop) 在连接查询执行过程中，当被驱动表不能有效的利用索引加快访问速度，MySQL 一般会为其分配一块名叫 join buffer 的内存块来加快查询速度。\n该值强调了在获取连接条件时没有使用索引，并且需要连接缓冲区来存储中间结果。如果出现了这个值，那应该注意，根据查询的具体情况可能需要添加索引来改进性能。\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM s1 INNER JOIN s2 ON s1.order_note = s2.order_note; +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+--------------------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+--------------------------------------------+ | 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10609 | 100.00 | NULL | | 1 | SIMPLE | s2 | NULL | ALL | NULL | NULL | NULL | NULL | 10621 | 10.00 | Using where; Using join buffer (hash join) | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+--------------------------------------------+ 上例的 Extra 信息解析：\nUsing where：可以看到查询语句中有一个s1.order_note = s2.order_note条件，因为 s2 是驱动表，s1 是被驱动表，所以在访问 s1 表时，s1.order_note 的值已经确定下来了，所以实际上查询 s1 表的条件就是s1.order_note = 一个常数，所以提示了 Using where 额外信息。 Using join buffer (Block Nested Loop)：这是因为对表 s1 的访问不能有效利用索引，只好退而求其次，使用 join buffer 来减少对 s1 表的访问次数，从而提高性能。 注：测试时本地是安装 MySQL 8.0 版本，此时 extra 的显示为 Using join buffer (hash join)\n4.15.8. Not exists 当使用左（外）连接时，如果 WHERE 子句中包含要求被驱动表的某个列等于 NULL 值的搜索条件，而且那个列又是不允许存储 NULL 值的，那么在该表的执行计划的 Extra 列就会提示 Not exists 额外信息。\n1 2 3 4 5 6 7 mysql\u0026gt; EXPLAIN SELECT * FROM s1 LEFT JOIN s2 ON s1.order_no = s2.order_no WHERE s2.id IS NULL; +----+-------------+-------+------------+------+---------------+--------------+---------+--------------------+-------+----------+-------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+--------------+---------+--------------------+-------+----------+-------------------------+ | 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10609 | 100.00 | NULL | | 1 | SIMPLE | s2 | NULL | ref | idx_order_no | idx_order_no | 152 | tempdb.s1.order_no | 1 | 10.00 | Using where; Not exists | +----+-------------+-------+------------+------+---------------+--------------+---------+--------------------+-------+----------+-------------------------+ 上述查询中 s1 表是驱动表，s2 表是被驱动表，s2.id 列是主键而且不允许存储 NULL 值的，而 WHERE 子句中又包含s2.id IS NULL的搜索条件。\n4.15.9. Using intersect(\u0026hellip;)、Using union(\u0026hellip;)和 Using sort_union(\u0026hellip;) 当 MySQL 决定要在一个给定的表上使用超过一个索引的时候，就会出现以下格式中的一个，详细说明使用的索引以及合并的类型\n如果执行计划的 Extra 列出现了Using intersect(...)提示，说明准备使用 Intersect 索引合并的方式执行查询，括号中的...表示需要进行索引合并的索引名称； 如果出现了Using union(...)提示，说明准备使用 Union 索引合并的方式执行查询； 如果出现了Using sort_union(...)提示，说明准备使用 Sort-Union 索引合并的方式执行查询。 4.15.10. Zero limit 当 LIMIT 子句的参数为 0 时，表示不打算从表中读出任何记录，将会提示该额外信息。\n4.15.11. Using filesort（文件排序） 很多情况下排序操作无法使用到索引，只能在内存中（记录较少的时候）或者磁盘中（记录较多的时候）进行排序，MySQL 把这种无法按照表内既定的索引顺序进行读取，并在内存中或者磁盘上进行排序的方式统称为『文件排序』。如果某个查询需要使用文件排序的方式执行查询，就会在执行计划的 Extra 列中显示 Using filesort。\n1 2 3 4 5 6 mysql\u0026gt; explain select order_number from tb_order order by order_money; +----+-------------+----------+------+---------------+------+---------+------+------+----------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+------+------+----------------+ | 1 | SIMPLE | tb_order | ALL | NULL | NULL | NULL | NULL | 1 | Using filesort | +----+-------------+----------+------+---------------+------+---------+------+------+----------------+ 说明：order_number 是表内的一个唯一索引列，但是 order by 没有使用该索引列排序，所以 MySQL 使用不得不另起一列进行排序。\n4.15.11.1. filesort 文件排序方式 单路排序：是一次性取出满足条件行的所有字段，然后在 sort buffer 中进行排序；用 trace 工具可以看到 sort_mode 信息里显示\u0026lt;sort_key, additional_fields\u0026gt;或者\u0026lt;sort_key,packed_additional_fields\u0026gt; 双路排序（又叫回表排序模式）：是首先根据相应的条件取出相应的排序字段和可以直接定位行数据的行 ID，然后在 sort buffer 中进行排序，排序完后需要再次回表去查询其它需要的字段；用 trace 工具可以看到 sort_mode 信息里显示\u0026lt;sort_key, rowid\u0026gt; 其实对比两个排序模式，单路排序会把所有需要查询的字段都放到 sort buffer 中，而双路排序只会把主键和需要排序的字段放到 sort buffer 中进行排序，然后再通过主键回到原表查询需要的字段。\n4.15.11.2. 文件排序模式的选择 MySQL 通过比较系统变量 max_length_for_sort_data(默认1024字节) 的大小和需要查询的字段总大小来判断使用哪种排序模式。\n如果字段的总长度小于 max_length_for_sort_data，那么使用单路排序模式 如果字段的总长度大于 max_length_for_sort_data，那么使用双路排序模式 如果内存配置比较小并且没有条件继续增加，可以适当把 max_length_for_sort_data 配置小点，让优化器选择使用双路排序算法，可以在 sort_buffer 中一次排序更多的行，但需要再根据主键回到原表取数据；如果内存充足，可以适当增大 max_length_for_sort_data 的值，让优化器优先选择全字段排序(单路排序)，把需要的字段放到 sort_buffer 中，这样排序后就会直接从内存里返回查询结果了。\n所以，MySQL 通过 max_length_for_sort_data 这个参数来控制排序，在不同场景使用不同的排序模式，从而提升排序效率。\nTips: 如果全部使用 sort_buffer 内存排序一般情况下效率会高于磁盘文件排序，但因此而随便增大 sort_buffer(默认1M)，mysql 很多参数设置都是经过优化的，不要轻易调整。\n4.15.12. Using temporary Mysql使用了临时表保存中间结果，常见于排序order by和分组查询group by。\n比如在执行许多包含 DISTINCT、GROUP BY、UNION 等子句的查询过程中，如果不能有效利用索引来完成查询，MySQL 很有可能寻求通过建立内部的临时表来执行查询。如果查询中使用到了内部的临时表，在执行计划的 Extra 列将会显示 Using temporary 提示\n1 2 3 4 5 6 mysql\u0026gt; explain select order_number from tb_order group by order_money; +----+-------------+----------+------+---------------+------+---------+------+------+---------------------------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+----------+------+---------------+------+---------+------+------+---------------------------------+ | 1 | SIMPLE | tb_order | ALL | NULL | NULL | NULL | NULL | 1 | Using temporary; Using filesort | +----+-------------+----------+------+---------------+------+---------+------+------+---------------------------------+ 上述执行计划的 Extra 列不仅仅包含 Using temporary 提示，还包含 Using filesort 提示。因为 MySQL 会在包含 GROUP BY 子句的查询中默认添加上 ORDER BY 子句，也就是说上述查询其实和下边这个查询等价：\n1 EXPLAIN SELECT order_note, COUNT(*) AS amount FROM s1 GROUP BY order_note order by order_note; 如果并不想为包含 GROUP BY 子句的查询进行排序，需要显式的写上 ORDER BY NULL。\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT order_note, COUNT(*) AS amount FROM s1 GROUP BY order_note order by NULL; +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-----------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-----------------+ | 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10609 | 100.00 | Using temporary | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-----------------+ 这样执行计划中就没有 Using filesort 的提示了，也就意味着执行查询时可以省去对记录进行文件排序的成本了。很明显，执行计划中出现 Using temporary 并不是一个好的征兆，因为建立与维护临时表要付出很大成本的，所以最好能使用索引来替代掉使用临时表。\n4.15.13. Start temporary, End temporary 有子查询时，查询优化器会优先尝试将 IN 子查询转换成 semi-join(半连接优化技术，本质上是把子查询上拉到父查询中，与父查询的表做 join 操作)，而 semi-join 又有好多种执行策略，当执行策略为 DuplicateWeedout 时，也就是通过建立临时表来实现为外层查询中的记录进行去重操作时，驱动表查询执行计划的 Extra 列将显示 Start temporary 提示，被驱动表查询执行计划的 Extra 列将显示 End temporary 提示。\n4.15.14. select tables optimized away 这个值意味着仅通过使用索引，优化器可能仅从聚合函数结果中返回一行。\n4.15.15. LooseScan 在将 In 子查询转为 semi-join 时，如果采用的是 LooseScan 执行策略，则在驱动表执行计划的 Extra 列就是显示 LooseScan 提示。\n4.15.16. FirstMatch(tbl_name) 在将 In 子查询转为 semi-join 时，如果采用的是 FirstMatch 执行策略，则在被驱动表执行计划的 Extra 列就是显示 FirstMatch(tbl_name)提示。\n4.16. explain 两个变种 4.16.1. explain extended（已过时） Notes: 在 5.7 版本以前可以使用，后续的版本已取消此关键字。\nexplain extended 会在 explain 的基础上额外提供一些查询优化的信息。\n1 mysql\u0026gt; explain extended select * from film where id = 1; 紧随其后通过 show warnings 命令可以得到优化后的查询语句，从而看出优化器做了什么优化。额外还有 filtered 列，是一个百分比的值，rows * filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数（前一个表指 explain 中的id值比当前表id值小的表）。\n1 mysql\u0026gt; show warnings; 4.16.2. explain partitions 相比 explain 多了个 partitions 字段，如果查询是基于分区表的话，会显示查询将访问的分区。\n5. 分析慢 sql 的其他方法 通过应用程序访问 MySQL 服务时，有时候性能不一定全部卡在语句的执行上。常用的手段是通过慢查询日志定位那些执行效率较低的SQL语句。\n慢查询日志在查询结束以后才记录，在应用反映执行效率出现问题的时候查询未必执行完成 有时候问题的产生不一定是语句的执行，有可能是其他原因导致的。慢查询日志并不能定位问题。 5.1. 通过 show processlist 分析 SQL 1 2 3 4 5 6 7 8 9 10 11 12 mysql\u0026gt; show processlist; +----+-----------------+----------------+--------+---------+------+------------------------+------------------+ | Id | User | Host | db | Command | Time | State | Info | +----+-----------------+----------------+--------+---------+------+------------------------+------------------+ | 5 | event_scheduler | localhost | NULL | Daemon | 2784 | Waiting on empty queue | NULL | | 8 | root | localhost:2772 | tempdb | Query | 0 | init | show processlist | | 9 | root | localhost:2779 | tempdb | Sleep | 1533 | | NULL | | 10 | root | localhost:2780 | tempdb | Sleep | 1957 | | NULL | | 11 | root | localhost:2781 | tempdb | Sleep | 2128 | | NULL | | 12 | root | localhost:2782 | tempdb | Sleep | 2128 | | NULL | | 13 | root | localhost:2783 | tempdb | Sleep | 2128 | | NULL | +----+-----------------+----------------+--------+---------+------+------------------------+------------------+ id 列：线程 id，用户登录 mysql 时，系统分配的\u0026quot;connection_id\u0026quot;，可以使用函数connection_id()查看。此 id 可用于 kill id 杀死某个线程 user 列：显示当前用户。如果不是root，这个命令就只显示用户权限范围的sql语句 host 列：数据库实例的 IP，显示这个语句是从哪个ip的哪个端口上发的，可以用来跟踪出现问题语句的用户 db 列：显示这个进程目前连接的是哪个数据库 command 列：显示当前连接执行的命令，一般取值为休眠（sleep），查询（query），连接（connect）等 time 列：显示这个状态持续的时间，单位是秒 state 列(重要)：显示使用当前连接的 sql 语句的状态，描述的是语句执行中的某一个状态。以查询语句为例，可能需要经过 copying to tmp table、sorting result、sending data 等状态才可以完成。此列主要有以下常见状态： Sleep，线程正在等待客户端发送新的请求 Locked，线程正在等待锁 Sending data，正在处理 SELECT 查询的记录，同时把结果发送给客户端 Kill，正在执行 kill 语句，杀死指定线程 Connect，一个从节点连上了主节点 Quit，线程正在退出 Sorting for group，正在为 GROUP BY 做排序 Sorting for order，正在为 ORDER BY 做排序 info 列：显示这个 sql 语句，是判断问题语句的一个重要依据 通过上面的命令可以查看线程状态。可以了解当前 MySQL 在进行的线程，包括线程的状态、是否锁表等，可以实时地查看SQL的执行情况，同时对一些锁表操作进行优化。在一个繁忙的服务器上，可能会看到大量的不正常的状态，例如 statistics 正占用大量的时间。这通常表示，某个地方有异常了。如：\nstatistics The server is calculating statistics to develop a query execution plan. If a thread is in this state for a long time, the server is probably disk-bound performing other work.\n服务器正在计算统计信息以研究一个查询执行计划。如果线程长时间处于此状态，则服务器可能是磁盘绑定执行其他工作。\nCreating tmp table The thread is creating a temporary table in memory or on disk. If the table is created in memory but later is converted to an on-disk table, the state during that operation is Copying to tmp table on disk.\n该线程正在内存或磁盘上创建临时表。如果表在内存中创建但稍后转换为磁盘表，则该操作期间的状态将为 Copying to tmp table on disk\nSending data The thread is reading and processing rows for a SELECT statement, and sending data to the client. Because operations occurring during this state tend to perform large amounts of disk access (reads), it is often the longest-running state over the lifetime of a given query.\n线程正在读取和处理 SELECT 语句的行 ，并将数据发送到客户端。由于在此状态期间发生的操作往往会执行大量磁盘访问（读取），因此它通常是给定查询生命周期中运行时间最长的状态。\n具体状态参数解释参考官网：https://dev.mysql.com/doc/refman/5.7/en/general-thread-states.html\n5.2. 通过 show profile 分析 SQL 对于每个线程到底时间耗费在哪里，可以通过 show profile 来分析。\n首先检查当前 MySQL 是否支持 profile 1 2 3 4 5 6 mysql\u0026gt; SELECT @@have_profiling; +------------------+ | @@have_profiling | +------------------+ | YES | +------------------+ 默认 profiling 是关闭的，执行如下命令可以查看是否开启 profiling。（0-关闭；1-开启） 1 2 3 4 5 6 mysql\u0026gt; select @@profiling; +-------------+ | @@profiling | +-------------+ | 0 | +-------------+ 可以通过 set 语句在 Session 级别开启 profiling： 1 set profiling=1; 执行一个 SQL 查询。（如：select count(*) from order_exp;） 执行以下系列的语句，可以以不同的方式来查看执行的耗时 查看当前 SQL 的 Query ID 和 SQL 语句执行的耗时： 1 2 3 4 5 6 7 8 9 mysql\u0026gt; show profiles; +----------+------------+--------------------------------+ | Query_ID | Duration | Query | +----------+------------+--------------------------------+ | 1 | 0.01082900 | select count(*) from order_exp | | 2 | 0.00130800 | select count(*) from order_exp | | 3 | 0.00159725 | select count(*) from order_exp | | 4 | 0.00131825 | select count(*) from order_exp | +----------+------------+--------------------------------+ 通过 show profile for query query_id 语句查看指定 query_id 的 SQL 执行过程中每个阶段的状态和消耗的时间 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 mysql\u0026gt; show profile for query 1; +----------------------+----------+ | Status | Duration | +----------------------+----------+ | starting | 0.000035 | | checking permissions | 0.000003 | | Opening tables | 0.000010 | | init | 0.000007 | | System lock | 0.000005 | | optimizing | 0.000002 | | statistics | 0.000007 | | preparing | 0.000006 | | executing | 0.000001 | | Sending data | 0.010714 | | end | 0.000003 | | query end | 0.000004 | | closing tables | 0.000004 | | freeing items | 0.000023 | | cleaning up | 0.000006 | +----------------------+----------+ 通过仔细检查输出，能够发现在执行COUNT(*)的过程中，时间主要消耗在Sending data这个状态上。\n通过 show profile cpu for query query_id; 请求，查看指定 query_id 的 SQL 语句 CPU 的使用情况 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 mysql\u0026gt; show profile cpu for query 1; +----------------------+----------+----------+------------+ | Status | Duration | CPU_user | CPU_system | +----------------------+----------+----------+------------+ | starting | 0.000035 | 0.000000 | 0.000000 | | checking permissions | 0.000003 | 0.000000 | 0.000000 | | Opening tables | 0.000010 | 0.000000 | 0.000000 | | init | 0.000007 | 0.000000 | 0.000000 | | System lock | 0.000005 | 0.000000 | 0.000000 | | optimizing | 0.000002 | 0.000000 | 0.000000 | | statistics | 0.000007 | 0.000000 | 0.000000 | | preparing | 0.000006 | 0.000000 | 0.000000 | | executing | 0.000001 | 0.000000 | 0.000000 | | Sending data | 0.010714 | 0.000000 | 0.000000 | | end | 0.000003 | 0.000000 | 0.000000 | | query end | 0.000004 | 0.000000 | 0.000000 | | closing tables | 0.000004 | 0.000000 | 0.000000 | | freeing items | 0.000023 | 0.000000 | 0.000000 | | cleaning up | 0.000006 | 0.000000 | 0.000000 | +----------------------+----------+----------+------------+ 在获取到最消耗时间的线程状态后，MySQL 支持进一步选择 all、cpu、block io、contextswitch、page faults 等明细类型来查看 MySQL 在使用什么资源上耗费了过高的时间: 1 show profile all for query 1\\G; 能够发现 Sending data 状态下，时间主要消耗在 CPU 上了。所以show profile能够在做SQL优化时帮助了解时间都耗费到哪里去了，同时如果 MySQL 源码感兴趣，还可以通过 show profile source for query 查看 SQL 解析执行过程中每个步骤对应的源码的文件、函数名以及具体的源文件行数。\n字段 含义 Status sql语句执行的状态 Duration sql执行过程中第一个步骤的耗时 CPU_user 当前用户占有的CPU CPU_system 系统占有的CPU 6. SQL 优化最佳实践 6.1. SQL 编写规范 SQL 语句中优先使用 in 代替 or。in 是范围查找，MySQL 内部会对 in 的列表值进行排序后查找，比 or 效率更高。 如果查询结果集不需要去重、排序，应使用 UNION ALL 代替 UNION。 将查询条件中的 or 关键字转换为 UNION ALL，从应用层面处理 UNION ALL 中的重复数据。 需要使用随机数查询时，不应使用 order by rand()。因为此操作会为表增加一个伪列，然后用 rand() 函数为每一行数据计算出随机值，然后再基于该值排序，这通常都会生成磁盘上的临时表，因此效率非常低。建议先使用 rand() 函数获得随机的主键值，然后通过主键获取数据。 使用批量插入的 SQL 语句。 insert 和 update 等语句中以及 select 嵌套语句的最里面层，应使用明确的字段名，避免使用 * 获取全部。这样可减少网络带宽消耗，有效利用覆盖索引（如有），表结构变更对程序基本无影响。 使用 group by 的时候，如果确认不需要排序，语句应加上 order by null，避免多余的排序。因为 group by 默认会进行排序。 进行数据比较时，如果两边的数据类型不一致，应在一方加上类型转换的函数，避免隐式类型转换。日期类型是特例，包换 DATE、TIME、DATETIME。例如： 1 select e.username from employee e where e.birthday \u0026gt;= \u0026#39;1999-12-12 11:11:11\u0026#39; 限制表连接操作所涉及的表的个数。参与表连接的表数量不宜超过3个，若有超过3个表连接的需求，建议从应用层面进行优化 限制嵌套查询的层数。过多的嵌套层数，会使用查询语句的复杂度大幅增加而影响执行效率，查询语句的嵌套层数不应该多于3层 6.2. 插入数据优化 如果需要一次性往数据库表中插入多条记录，可以从以下3个方面进行优化。\n6.2.1. 批量插入 批量插入数据 SQL 语句，减少与数据库交互次数。如：\n1 2 3 4 5 6 7 insert into tb_test values(1,\u0026#39;Tom\u0026#39;),(2,\u0026#39;Cat\u0026#39;),(3,\u0026#39;Jerry\u0026#39;); -- 其他 insert ... on duplicate key update replace into insert ignore insert into values(), (), ... 6.2.2. 手动控制事务 1 2 3 4 5 start transaction; insert into tb_test values(1,\u0026#39;Tom\u0026#39;),(2,\u0026#39;Cat\u0026#39;),(3,\u0026#39;Jerry\u0026#39;); insert into tb_test values(4,\u0026#39;Tom\u0026#39;),(5,\u0026#39;Cat\u0026#39;),(6,\u0026#39;Jerry\u0026#39;); insert into tb_test values(7,\u0026#39;Tom\u0026#39;),(8,\u0026#39;Cat\u0026#39;),(9,\u0026#39;Jerry\u0026#39;); commit; 6.2.3. 主键顺序插入 尽可能让主键顺序插入行。主键顺序插入，性能要高于乱序插入。\n主键乱序插入 : 8 1 9 21 88 2 4 15 89 5 7 3 主键顺序插入 : 1 2 3 4 5 7 8 9 15 21 88 89 最好避免随机的（不连续且值的分布范围非常大）聚簇索引，特别是对于I/O密集型的应用。如果聚簇索引的插入变得完全随机，会存在以下的问题：\n写入的目标页可能已经刷到磁盘上并从缓存中移除，或者是还没有被加载到缓存中，InnoDB 在插入之前不得不先找到并从磁盘读取目标页到内存中。这将导致大量的随机 IO。 因为写入是乱序的，InnoDB 不得不频繁地做页分裂操作，以便为新的行分配空间。页分裂会导致移动大量数据，一次插入最少需要修改三个页而不是一个页。 所以使用 InnoDB 时应该尽可能地按主键顺序插入数据，并且尽可能地使用单调增加的聚簇键的值来插入新行。\n6.3. MySQL 大批量数据导入性能优化 6.3.1. 传统的 insert 优化 如果使用传统的 insert 插入大批量数据，提高导入性能一般有以下几点：\n对于有主键的表，导入前将数据按照主键的顺序排序，可以有效提高导入数据的效率 导入数据前通过set unique_checks=0，关闭唯一性校验，可以提高导入效率，导入完成后再用set unique_checks=1打开 通过set autocommit=0，关闭自动提交，可以提高导入效率 insert 语句采用INSERT INTO VALUES(), (), ...的方式，一条件语句插入多条数据（但需要注意 SQL 语句长度限制，max_allowed_packet 参数，大基线默认32M），可以幅度提高导入的效率。 使用工具导入（如：load data），比通过 sql 语句方式提速20倍。需要show global variables like 'local_infile';查看是否开启，通过set global local_infile=1;开启。 使用insert delayed ...异步插入的方式（先写入内存），mysql 可以进行合并写入，提高性能（值得注意的是，如果 mysql 数据库设备宕机，会丢失数据）。 对于要先查询是否有记录，有记录就 update，没有记录则 insert，最好采用下面的语法执行，减少 sql 交互 1 2 3 insert ... on duplicate key update replace into insert ignore 6.3.2. 存储过程+开启事务 通过存储过程可以实现循环批量插入大量数据，但正常情况与逐条插入的效率差不多，只是存储过程允许编写循环插入。可以开启事务来提高数据导入的速度。示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 DELIMITER $$ -- 定义结束符（为了不跟储存过程的“;”冲突，这里重新定义） drop procedure if exists `insert_batch_index` $$ CREATE procedure `insert_batch_index` (in n int) begin declare i int default 1; declare resource_id int default 0; declare test_name varchar(255) default \u0026#39;\u0026#39;; declare cate_id int default 0; declare input_time int default 0; while i \u0026lt; n do set resource_id = floor(1 + rand() * 3000); set test_name = concat(\u0026#39;name_\u0026#39;, resource_id); set cate_id = floor(1 + rand() * 20); set input_time = floor(1609430400 + rand() * 32227200); insert into batch_index values (null, resource_id, test_name, cate_id, input_time); set i = i + 1; end while; end $$ delimiter ; --把结束符再设置回“;” 调用：\n1 2 3 start transaction; call insert_batch_index(10000); commit; 6.3.3. 使用 load 指令 如果一次性需要插入大批量数据(几百万的记录)，使用 insert 语句插入性能较低，此时可以使用 MySQL 数据库提供的 load 指令进行插入。操作如下：\n首先，检查一个全局系统变量 'local_infile' 的状态， 如果得到如下显示 Value=OFF，则说明这是不可用的 1 2 3 4 5 6 mysql\u0026gt; show global variables like \u0026#39;local_infile\u0026#39;; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | local_infile | ON | +---------------+-------+ 修改 local_infile 值为 on，开启 local_infile 1 set global local_infile=1; 使用 load 命令加载数据 1 load data local infile \u0026#39;D:\\\\sql_data\\\\sql.log\u0026#39; into table tb_user fields terminated by \u0026#39;,\u0026#39; lines terminated by \u0026#39;\\n\u0026#39;; 命令参数说明：\n'D:\\\\sql_data\\\\sql.log'：待导入的数据文件 tb_user：待导入的表名称 fields terminated by ','：每行数据的字段值的分隔符是“,” lines terminated by '\\n'：每行数据的分隔符是换行符“\\n” 6.3.4. 主键顺序导入 因为 InnoDB 类型的表是按照主键的顺序保存的，所以将导入的数据按照主键的顺序排列，可以有效的提高导入数据的效率。如果 InnoDB 表没有主键，那么系统会自动默认创建一个内部列作为主键，所以如果可以给表创建一个主键，将可以利用这点，来提高导入数据的效率。\n6.3.5. 关闭唯一性校验 在导入数据前执行 SET UNIQUE_CHECKS=0，关闭唯一性校验，在导入结束后执行 SET UNIQUE_CHECKS=1，恢复唯一性校验，可以提高导入的效率。\n1 2 3 4 5 6 -- 导入前，关闭唯一性校验 SET UNIQUE_CHECKS=0; -- 导入数据 load data local infile \u0026#39;D:\\\\sql_data\\\\sql.log\u0026#39; into table tb_user fields terminated by \u0026#39;,\u0026#39; lines terminated by \u0026#39;\\n\u0026#39;; -- 导入后，开启唯一性校验 SET UNIQUE_CHECKS=1; 6.4. 优化 order by 语句 MySQL 一般有两种排序方式：\nUsing filesort：通过表的索引或全表扫描，读取满足条件的数据行，然后在排序缓冲区 sort buffer 中完成排序操作，所有不是通过索引直接返回排序结果的排序都叫 FileSort 排序。 Using index：通过有序索引顺序扫描直接返回有序数据，这种情况即为 using index，不需要额外排序，操作效率高 对于以上的两种排序方式，Using index 的性能高，而 Using filesort 的性能低，在优化排序操作时，尽量要优化为 Using index。\n6.4.1. Filesort 的优化 通过创建合适的索引，能够减少 Filesort 的出现，但是在某些情况下，条件限制不能让 Filesort 消失，那就需要加快 Filesort 的排序操作。MySQL 对于 Filesort 有两种排序算法：\n两次扫描算法(回表扫描)：MySQL 4.1 之前，使用该方式排序。首先根据条件取出排序字段和行指针信息，然后在排序区 sort buffer 中排序，如果 sort buffer 不够，则在临时表 temporary table 中存储排序结果。完成排序之后，再根据行指针回表读取记录，该操作可能会导致大量随机I/O操作。 一次扫描算法：一次性取出满足条件的所有字段，然后在排序区 sort buffer 中排序后直接输出结果集。排序时内存开销较大，但是排序效率比两次扫描算法要高。 MySQL 通过比较系统变量 max_length_for_sort_data 的大小和 Query 语句取出的字段总大小，来判定是否那种排序算法，如果 max_length_for_sort_data 更大，那么使用第二种优化之后的算法；否则使用第一种。\n可以适当提高 sort_buffer_size 和 max_length_for_sort_data 系统变量，来增大排序区的大小，提高排序的效率。\n6.4.2. 通过索引优化 假设 tb_user 表的 age, phone 都没有索引，那么查询排序时就会出现 Using filesort，排序性能较低。\n1 2 3 4 5 6 mysql\u0026gt; explain select id,age,phone from tb_user order by age, phone; +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+----------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+----------------+ | 1 | SIMPLE | tb_user | NULL | ALL | NULL | NULL | NULL | NULL | 24 | 100.00 | Using filesort | +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+----------------+ 给 age, phone 创建联合索引\n1 CREATE INDEX idx_user_age_phone_aa ON tb_user ( age, phone ); 创建索引后，根据 age, phone 进行升序排序。由原来的 Using filesort 变为了 Using index，性能就是比较高的了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 mysql\u0026gt; explain select id,age,phone from tb_user order by age; +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ | 1 | SIMPLE | tb_user | NULL | index | NULL | idx_user_age_phone_aa | 37 | NULL | 24 | 100.00 | Using index | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ mysql\u0026gt; explain select id,age,phone from tb_user order by age, phone; +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ | 1 | SIMPLE | tb_user | NULL | index | NULL | idx_user_age_phone_aa | 37 | NULL | 24 | 100.00 | Using index | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ 根据 age, phone 进行降序排序。此时也会出现 Using index，但是此时 Extra 中出现了 Backward index scan，这个代表反向扫描索引，因为在 MySQL 中创建的索引，默认索引的叶子节点是从小到大排序的，而查询排序时是从大到小，所以在扫描时就是反向扫描，就会出现 Backward index scan。在 MySQL 8 版本中，支持降序索引，也可以创建降序索引。\n1 2 3 4 5 6 mysql\u0026gt; explain select id,age,phone from tb_user order by age desc, phone desc; +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+----------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+----------------------------------+ | 1 | SIMPLE | tb_user | NULL | index | NULL | idx_user_age_phone_aa | 37 | NULL | 24 | 100.00 | Backward index scan; Using index | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+----------------------------------+ 根据 phone，age 进行升序排序，phone 在前，age 在后。排序时不满足最左前缀法则，因此出现了 filesort 排序。在创建索引的时候，age 是第一个字段，phone 是第二个字段，所以排序时也该按照这个顺序来，否则就会出现 Using filesort。\n1 2 3 4 5 6 mysql\u0026gt; explain select id,age,phone from tb_user order by phone, age; +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-----------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-----------------------------+ | 1 | SIMPLE | tb_user | NULL | index | NULL | idx_user_age_phone_aa | 37 | NULL | 24 | 100.00 | Using index; Using filesort | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-----------------------------+ 根据 age, phone 进行降序一个升序，一个降序。\n1 2 3 4 5 6 mysql\u0026gt; explain select id,age,phone from tb_user order by age asc, phone desc ; +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-----------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-----------------------------+ | 1 | SIMPLE | tb_user | NULL | index | NULL | idx_user_age_phone_aa | 37 | NULL | 24 | 100.00 | Using index; Using filesort | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-----------------------------+ 因为创建索引时，如果未指定顺序，默认都是按照升序排序的，而查询时，一个升序，一个降序，此时就会出现 Using filesort。\n创建联合索引(age 升序排序，phone 倒序排序)。注：需要 MySQL 8.0 以上版本\n1 2 3 4 5 6 7 8 9 10 11 12 mysql\u0026gt; CREATE INDEX idx_user_age_phone_ad ON tb_user ( age ASC, phone DESC ); mysql\u0026gt; SHOW INDEX FROM tb_user; +---------+------------+-----------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | +---------+------------+-----------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ | tb_user | 0 | PRIMARY | 1 | id | A | 24 | NULL | NULL | | BTREE | | | | tb_user | 1 | idx_user_age_phone_aa | 1 | age | A | 19 | NULL | NULL | YES | BTREE | | | | tb_user | 1 | idx_user_age_phone_aa | 2 | phone | A | 24 | NULL | NULL | | BTREE | | | | tb_user | 1 | idx_user_age_phone_ad | 1 | age | A | 19 | NULL | NULL | YES | BTREE | | | | tb_user | 1 | idx_user_age_phone_ad | 2 | phone | D | 24 | NULL | NULL | | BTREE | | | +---------+------------+-----------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ 再次查询，成功优化为 Using index\n1 2 3 4 5 6 mysql\u0026gt; explain select id,age,phone from tb_user order by age asc, phone desc; +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ | 1 | SIMPLE | tb_user | NULL | index | NULL | idx_user_age_phone_ad | 37 | NULL | 24 | 100.00 | Using index | +----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+-------------+ Notes: 以下测试sql都使用了覆盖索引，没有进行回表查询。\n6.4.3. order by 优化原则小结 根据排序字段建立合适的索引，多字段排序时，也遵循最左前缀法则。 尽量使用覆盖索引。 多字段排序，一个升序一个降序，此时需要注意联合索引在创建时的规则（ASC/DESC）。 如果不可避免的出现 filesort，大数据量排序时，可以适当增大排序缓冲区大小 sort_buffer_size(默认256k)。 6.5. 优化 group by 语句 GROUP BY 实际上也同样会进行排序操作，而且与 ORDER BY 相比，GROUP BY 主要只是多了排序之后的分组操作。当然，如果在分组的时候还使用了其他的一些聚合函数，那么还需要一些聚合函数的计算。所以，在 GROUP BY 的实现过程中，与 ORDER BY 一样也可以利用到索引。\n如果查询包含 group by 但是用户想要避免排序结果的消耗，则可以执行 order by null 禁止排序。(其实通过索引来优化后，本来就已经排序了，这么做有必须吗？)\n1 2 3 explain select age,count(*) from emp group by age; explain select age,count(*) from emp group by age order by null; 6.5.1. 通过索引优化 无索引的情况进行分组查询\n1 2 3 4 5 6 mysql\u0026gt; explain select profession, count(*) from tb_user group by profession; +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------+ | 1 | SIMPLE | tb_user | NULL | ALL | NULL | NULL | NULL | NULL | 24 | 100.00 | Using temporary | +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-----------------+ 对 profession、age、status 字符创建一个联合索引\n1 CREATE INDEX idx_user_pro_age_sta ON tb_user ( profession, age, `status` ); 再执行上面的分组查询\n1 2 3 4 5 6 mysql\u0026gt; explain select profession, count(*) from tb_user group by profession; +----+-------------+---------+------------+-------+----------------------+----------------------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+-------+----------------------+----------------------+---------+------+------+----------+-------------+ | 1 | SIMPLE | tb_user | NULL | index | idx_user_pro_age_sta | idx_user_pro_age_sta | 42 | NULL | 24 | 100.00 | Using index | +----+-------------+---------+------------+-------+----------------------+----------------------+---------+------+------+----------+-------------+ 如果仅仅根据 age 分组，就会出现 Using temporary ；而如果是根据 profession,age 两个字段同时分组，则不会出现 Using temporary。因此可以得到结论：对于有联合索引的字段进行分组操作，也是符合最左前缀法则的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 mysql\u0026gt; explain select profession, count(*) from tb_user group by age; +----+-------------+---------+------------+-------+----------------------+----------------------+---------+------+------+----------+------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+-------+----------------------+----------------------+---------+------+------+----------+------------------------------+ | 1 | SIMPLE | tb_user | NULL | index | idx_user_pro_age_sta | idx_user_pro_age_sta | 42 | NULL | 24 | 100.00 | Using index; Using temporary | +----+-------------+---------+------------+-------+----------------------+----------------------+---------+------+------+----------+------------------------------+ mysql\u0026gt; explain select profession, count(*) from tb_user group by profession,age; +----+-------------+---------+------------+-------+----------------------+----------------------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+-------+----------------------+----------------------+---------+------+------+----------+-------------+ | 1 | SIMPLE | tb_user | NULL | index | idx_user_pro_age_sta | idx_user_pro_age_sta | 42 | NULL | 24 | 100.00 | Using index | +----+-------------+---------+------------+-------+----------------------+----------------------+---------+------+------+----------+-------------+ mysql\u0026gt; explain select profession, count(*) from tb_user where profession = \u0026#39;软件工程\u0026#39; group by age; +----+-------------+---------+------------+------+----------------------+----------------------+---------+-------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+------+----------------------+----------------------+---------+-------+------+----------+-------------+ | 1 | SIMPLE | tb_user | NULL | ref | idx_user_pro_age_sta | idx_user_pro_age_sta | 36 | const | 4 | 100.00 | Using index | +----+-------------+---------+------------+------+----------------------+----------------------+---------+-------+------+----------+-------------+ 6.5.2. 分组操作原则 在分组操作时，可以通过索引来提高效率 分组操作时，索引的使用也是满足最左前缀法则的 6.6. 优化 limit 分页查询（超大分页） 在系统中需要进行分页操作的时候，通常会使用 LIMIT 加上偏移量的办法实现，同时加上合适的 ORDER BY 子句。\n一般分页查询时，通过创建覆盖索引能够比较好地提高性能。但有些情况，如 limit 9000000,10 偏移量非常大的查询，此时需要 MySQL 排序前 9000010 记录，仅仅返回 9000000 - 9000010 的记录，前面9百万记录将被丢弃，查询排序的代价非常大。\n1 2 3 4 5 6 7 8 9 mysql\u0026gt; select count(*) from tb_sku; +----------+ | count(*) | +----------+ | 10000000 | +----------+ mysql\u0026gt; select * from tb_sku limit 9000000,10; 10 rows in set (11.92 sec) 6.6.1. 优化思路一：子查询（连接查询）覆盖索引 优化此类分页查询的一个最简单的办法是：在索引上完成排序分页操作，通过创建『覆盖索引』+『子查询』的方式进行优化，最后根据主键关联回表查询所需要的其他列内容。\n1 SELECT * FROM tb_sku a WHERE id \u0026gt;= (SELECT id FROM tb_sku ORDER BY id LIMIT 9000000, 1) ORDER BY id LIMIT 10; 这种优化方式提升查询速度主要利用了索引覆盖的如下好处：\n索引文件不包含行数据的所有信息，故其大小远小于数据文件，因此可以减少大量的 IO 操作。 索引覆盖只需要扫描一次索引树，不需要回表扫描行数据，所以性能比回表查询要高。 同理，可以通过创建『覆盖索引』+『连接查询』的方式进行优化。例如：通过子查询先在索引上进行查询翻页中需要的 N 条数据的主键值，然后根据主键值回表查询相应的 N 条数据，在此过程中查询 N 条数据的主键 id 在索引中完成，所以效率会高一些。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 mysql\u0026gt; SELECT * FROM tb_sku a, ( SELECT id FROM tb_sku ORDER BY id LIMIT 9000000, 10 ) b WHERE a.id = b.id; 10 rows in set (6.74 sec) mysql\u0026gt; EXPLAIN SELECT * FROM s1 LIMIT 10000, 10; +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+ | 1 | SIMPLE | s1 | NULL | ALL | NULL | NULL | NULL | NULL | 10609 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+ mysql\u0026gt; EXPLAIN SELECT * FROM (SELECT id FROM s1 LIMIT 10000, 10) b, s1 WHERE s1.id = b.id; +----+-------------+------------+------------+--------+---------------+-----------------+---------+------+-------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+------------+------------+--------+---------------+-----------------+---------+------+-------+----------+-------------+ | 1 | PRIMARY | \u0026lt;derived2\u0026gt; | NULL | ALL | NULL | NULL | NULL | NULL | 10010 | 100.00 | NULL | | 1 | PRIMARY | s1 | NULL | eq_ref | PRIMARY | PRIMARY | 8 | b.id | 1 | 100.00 | NULL | | 2 | DERIVED | s1 | NULL | index | NULL | idx_insert_time | 5 | NULL | 10609 | 100.00 | Using index | +----+-------------+------------+------------+--------+---------------+-----------------+---------+------+-------+----------+-------------+ 6.6.2. 优化思路二：主键自增 最佳的方式是在业务上进行配合修改：假如是主键自增的表，可以把 limit 查询转换成某个位置的查询。但需要保证 id 必须连续\n1 2 3 4 5 6 7 8 9 mysql\u0026gt; SELECT * FROM tb_sku WHERE id \u0026gt; 9000000 LIMIT 10; 10 rows in set (0.16 sec) mysql\u0026gt; EXPLAIN SELECT * FROM s1 WHERE id \u0026gt; 9000 ORDER BY id LIMIT 10; +----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+ | 1 | SIMPLE | s1 | NULL | range | PRIMARY | PRIMARY | 8 | NULL | 3398 | 100.00 | Using where | +----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+ 采用这种写法，需要前端通过点击 More 来获得更多数据，而不是纯粹的翻页，因此，每次查询只需要使用上次查询出的数据中的 id 来获取接下来的数据即可，但这种写法需要业务配合。\n6.6.3. 优化思路三：计算边界值，转换为已知位置的查询 如果 id 连续不中断，就可以计算出每一页的边界值，让 MySQL 根据边界值进行范围扫描，查出数据。例如：\n1 2 3 4 5 select * from t_order where id between 0 and 10; select * from t_order where id between 10000 and 10010; select * from t_order where id between 100000 and 100010; select * from t_order where id between 1000000 and 1000010; select * from t_order where id between 10000000 and 10000010; 6.6.4. 其他优化思路 使用缓存，可预测性的提前查到内容，缓存至 redis 等 K-V 数据库中，查询时直接返回即可。 从需求的角度减少这种请求，不做类似的需求（直接跳转到几百万页之后的具体某一页，只允许逐页查看或者按照给定的路线走，这样可预测，可缓存）以及防止ID泄漏且连续被人恶意攻击。 Tips: 优化方式有多种，但其核心思想都一样，就是减少 load 的数据。\n6.7. 优化 count 查询 6.7.1. 优化思路 COUNT()是一个特殊的函数，有两种非常不同的作用：它可以统计某个列值的数量，也可以统计行数。\n统计列值，要求列值是非空的（不统计 NULL)。 统计结果集的行数，常用的就是 COUNT(*)。实际上，它会忽略所有的列而直接统计所有的行数。 在大数据量的表中执行 select count(*) from 表; 的操作时，是非常耗时。不同的存储引擎会有不同的处理：\nMyISAM 引擎把一个表的总行数存在了磁盘上，因此执行 count(*) 的时候会直接返回这个数，效率很高；但是如果是带条件的 count 操作，MyISAM 也是非常慢。 InnoDB 引擎执行 count(*) 的时候，需要把数据一行一行地从引擎里面读出来，然后累积计数，因此效率非常底。 通常来说，COUNT()都需要扫描大量的行（意味着要访问大量数据）才能获得精确的结果，因此是很难优化的。在 MySQL 层面能做的基本只有索引覆盖扫描了。如果说要大幅度提升 InnoDB 表的 count 效率，就需要考虑修改应用的架构，可以用估算值取代精确值，可以增加汇总表，或者增加类似 Redis 这样的外部缓存系统。\n主要的优化思路：自己进行计数(可以借助于 redis 这样的数据库进行，但是如果是带条件的 count 操作还是比较麻烦)，新增/删除由自己来维护。或者在数据库增加一张专门维护表计数的表，每次新增/修改都在同一个事务中，可以保证数据的一致性，但同时增加维护的成本，并且也无法实现条件统计。\n6.7.2. count 的各种写法比较 count() 是一个聚合函数，对于返回的结果集，一行行地判断，如果 count 函数的参数不是 NULL，累计值就加 1，否则不加，最后返回累计值。主要用法有以下几种：\ncount(主键)：InnoDB 引擎会遍历整张表，把每一行的主键 id 值都取出来，返回给服务层。服务层拿到主键后，直接按行进行累加(主键不可能为null) count(字段)：分以下两种情况 没有 not null 约束的字段：InnoDB 引擎会遍历整张表把每一行的字段值都取出来，返回给服务层，服务层判断是否为 null，不为 null 则计数累加。 有 not null 约束的字段：InnoDB 引擎会遍历整张表把每一行的字段值都取出来，返回给服务层，直接按行进行累加。 count(数字)：InnoDB 引擎遍历整张表，但不取值。服务层对于返回的每一行，都赋指定的“数字”的值，直接按行进行累加。 count(*)：InnoDB 引擎并不会把全部字段取出来，而是专门做了优化，不取值，服务层直接按行进行累加。 Notes: 综上所述，按照效率的排序是，count(字段) \u0026lt; count(主键 id) \u0026lt; count(1) ≈ count(*)，所以尽量使用 count(*)。\n6.8. 优化 update 语句 update 语句执行时的注意事项。当执行根据主键 id 更新的 SQL 语句时，会锁定 id 所在的一行的数据，然后事务提交之后，行锁释放。\n1 update course set name = \u0026#39;javaEE\u0026#39; where id = 1 ; 但是当执行根据没有索引的字段更新的 SQL 时，行锁会升级为了表锁，导致该 update 语句的性能大大降低。\n1 2 -- name 字段没有建索引 update course set name = \u0026#39;SpringBoot\u0026#39; where name = \u0026#39;PHP\u0026#39;; Notes: InnoDB 的行锁是针对索引加的锁，不是针对记录加的锁，并且该索引不能失效，否则会从行锁升级为表锁。因此更新数据时最好以索引列做为条件。\n6.9. 优化子查询 子查询的执行效率不高。子查询时，MySQL 需要为内层查询语句的查询结果建立一个临时表。然后外层查询语句再临时表中查询记录。查询完毕后，MySQL 需要撤销这些临时表。所以子查询的速度会受到一定的影响。如果查询的数据量比较大，影响速度就会随之增大。在 MySQL 中可以使用连接查询来代替子查询，连接查询不需要建立临时表，其速度比子查询要快。\n有些情况下，子查询是可以被更高效的连接（JOIN）替代。\n1 2 3 4 explain select * from user where uid in (select uid from user_role); -- 优化 explain select * from user u,user_role ur where u.uid = ur.uid; 连接(join)查询之所以更有效率一些 ，是因为 MySQL 不需要在内存中创建临时表来完成这个逻辑上需要两个步骤的查询工作。\n6.10. 关联查询的优化 关联字段加索引，让 mysql 做 join 操作时尽量选择 NLJ 算法（详见后面的章节） 小表驱动大表，写多表连接 sql 时如果明确知道哪张表是小表可以用 straight_join 写法固定连接驱动方式，省去 mysql 优化器判断的时间 尽量 inner join 而不用 left join 或者 right join（如必须使用，一定要以小表为驱动）。因为内连接会对两个表进行优化，优先把小表放到外边，把大表放到里边。而 left join 或 right join，不会重新调整顺序。 Tips: 使用 straight_join 一定要慎重，因为大部分情况下人为指定的执行顺序并不一定会比优化引擎选择的要更优，建议尽可能让优化器去判断。\n6.11. in 和 exsits 优化 in 和 exsits 语句的优化原则是，小表驱动大表，即小的数据集驱动大的数据集。例如：\n当in关键字后面是子查询时，该查询是优先执行，因此下例当B表的数据集小于A表的数据集时，使用 in 优于 exists 1 select * from A where id in (select id from B) 当exists关键字后面是子查询时，外层的查询是优先于exists后面的子查询执行，因为下例中当A表的数据集小于B表的数据集时，使用 exists 优于 in 1 select * from A where exists (select 1 from B where B.id = A.id) Tips: EXISTS 子查询也可以用 JOIN 来代替，但需要具体问题具体分析才能决定哪种方式最优\n6.12. SQL 语法优化的总结 使用EXPLAIN关键字去查看执行计划 type列，连接类型。一个好的 SQL 语句至少要达到 range 级别。杜绝出现 all 级别。 key列，使用到的索引名。如果没有选择索引，值是 NULL。可以采取强制索引方式。 key_len列，索引长度。 rows列，扫描行数。该值是个预估值。 extra列，详细说明。注意，常见的不太友好的值，如下：Using filesort，Using temporary。 比较运算符能用=就不用\u0026lt;\u0026gt;，因为=增加了索引的使用几率。 如果确定只有一条查询结果，则使用LIMIT 1。可以避免全表扫描，找到对应结果后就不会再继续扫描了。使 EXPLAIN 中 type 列达到 const 类型 为列选择合适的数据类型。能用 TINYINT 就不用 SMALLINT，能用 SMALLINT 就不用 INT 将大的DELETE、UPDATE、INSERT查询变成多个小查询。一个执行几十行、几百行数据的 sql 尽量优化成多个小的 sql 如果结果集允许重复或者保证两个结果集不出现重复时，使用UNION ALL代替UNION，因为UNION ALL不去重，效率高于UNION 尽量避免使用SELECT *，因为它会进行全表扫描，不能有效利用索引，增大了数据库服务器的负担，以及它与应用程序客户端之间的网络 IO 开销 WHERE子句、JOIN子句、ORDER BY的列里面的列尽量使用索引。根据实际情况进行调整，因为有时索引太多也会降低性能 7. 高性能的索引策略 索引可以快速的定位表中的某条记录。如果查询时不使用索引，查询语句将查询表中的所有字段，查询速度会很慢；如果使用索引进行查询，查询语句只查询索引字段，可以减少查询的记录数，提高查询速度。\n7.1. 索引创建策略 7.1.1. 索引列的类型尽量小 类型大小指的就是该类型表示的数据范围的大小。原因如下：\n数据类型越小，在查询时进行的比较操作越快（CPU 层次) 数据类型越小，索引占用的存储空间就越少，在一个数据页内就可以放下更多的记录，从而减少磁盘 I/O 带来的性能损耗，也就意味着可以把更多的数据页缓存在内存中，从而加快读写效率。 此建议对于表的主键来说更加适用，因为不仅是聚簇索引中会存储主键值，其他所有的二级索引的节点处都会存储一份记录的主键值，如果主键适用更小的数据类型，也就意味着节省更多的存储空间和更高效的I/O。\n7.1.2. 索引的选择性/离散性要高 创建索引应该选择选择性/离散性高的列。索引的选择性/离散性是指，不重复的索引值（也称为基数，cardinality）和数据表的记录总数(N)的比值，范围从 1/N 到 1 之间。索引的选择性越高则查询效率越高，因为选择性高的索引可以让 MySQL 在查找时过滤掉更多的行。唯一索引的选择性是 1，这是最好的索引选择性，性能也是最好的。\n很差的索引选择性就是列中的数据重复度很高，如：性别、是/否字典类字段等。计算索引的选择性/离散性如下：\n1 2 3 4 5 6 mysql\u0026gt; SELECT COUNT(DISTINCT order_no)/COUNT(*) cnt FROM order_exp; +--------+ | cnt | +--------+ | 0.9676 | +--------+ 1 2 3 4 5 6 mysql\u0026gt; SELECT COUNT(DISTINCT order_status)/COUNT(*) cnt FROM order_exp; +--------+ | cnt | +--------+ | 0.0001 | +--------+ 显然，order_no 列上的索引就比 order_status 列上的索引的选择性就要好，离散性更高。\n7.1.3. 前缀索引 当字段类型为字符串（varchar，text，longtext 等）时，有时候需要索引很长的字符串，这会让索引变得很大，查询时，浪费大量的磁盘 IO，影响查询效率。一种解决方案是模拟哈希索引。模拟哈希索引的做法：一个表中 a 字段很长，想把它作为一个索引，可以增加一个 a_hash 字段来存储 a 的哈希值，然后在 a_hash 上建立索引，相对于之前的索引速度会有明显提升，一个是对完整的 a 做索引，而后者则是用整数哈希值做索引，显然数字的比较比字符串的匹配要高效得多。\n模拟哈希索引的缺点：\n需要额外维护 order_not_hash 字段； 哈希算法的选择决定了哈希冲突的概率，不良的哈希算法会导致重复值很多 不支持范围查找 于是有改进的方案 - 前缀索引：就是以开始的一部分字符前缀，建立索引，这样可以大大节约索引空间，从而提高索引效率。一般情况下需要保证某个列前缀的选择性也是足够高的，以满足查询性能。（尤其对于 BLOB、TEXT 或者很长的 VARCHAR 类型的列，应该使用前缀索引，因为 MySQL 不允许索引这些列的完整长度）。创建语法如下：\n1 2 3 4 -- 方式一： CREATE INDEX 索引名称 ON 表名(字段名(长度)); -- 方式二： ALTER TABLE 表名 ADD KEY (字段名(长度)); 那么前缀索引的的长度选择多少最合适？诀窍在于要选择足够长的前缀以保证较高的选择性，同时又不能太长（以便节约空间)。所谓的选择性是指不重复的索引值（基数）和数据表的记录总数的比值，索引选择性越高则查询效率越高，唯一索引的选择性是 1，这是最好的索引选择性，性能也是最好的。\n前缀应该足够长，以使得前缀索引的选择性接近于索引整个列。即前缀的“基数”应该接近于完整列的“基数”。为了决定前缀的合适长度，可以找到最常见的值的列表，然后和最常见的前缀列表进行比较\n可以看见，从第 10 个开始选择性的增加值很高，随着前缀字符的越来越多，选择度也在不断上升，但是增长到第 15 时，已经和第 14 没太大差别了，选择性提升的幅度已经很小了，都非常接近整个列的选择性了。那么针对这个字段做前缀索引的话，从第 13 到第 15 都是不错的选择，甚至第 12 也不是不能考虑。然后就是创建索引：\n1 ALTER TABLE order_exp ADD KEY (order_note(14)); 前缀索引是一种能使索引更小、更快的有效办法，但另一方面也有其缺点 MySQL 无法使用前缀索引做 ORDER BY 和 GROUP BY，也无法使用前缀索引做覆盖扫描。有时候后缀索引 (suffix index)也有用途（例如，找到某个域名的所有电子邮件地址)。MySQL 原生并不支持反向索引，但是可以把字符串反转后存储，并基于此建立前缀索引。可以通过触发器或者应用程序自行处理来维护索引。\n前缀索引的查询流程图示：\n7.1.4. 只为用于搜索、排序或分组的列创建索引 只为出现在WHERE子句中的列、连接子句中的连接列创建索引，而出现在查询列表中的列一般就没必要建立索引了，除非是需要使用覆盖索引；又或者为出现在ORDER BY或GROUP BY子句中的列创建索引\n7.1.5. 多列索引列的顺序的选择 建立索引的目的是：希望通过索引进行数据查找，减少随机IO，增加查询性能 ，索引能过滤出越少的数据，则从磁盘中读入的数据也就越少。\n正确的顺序依赖于使用该索引的查询，并且同时需要考虑如何更好地满足排序和分组的需要。在一个多列 B-Tree 索引中，索引列的顺序意味着索引首先按照最左列进行排序，其次是第二列，等等。所以，索引可以按照升序或者降序进行扫描，以满足精确符合列顺序的 ORDER BY、GROUP BY 和 DISTINCT 等子句的查询需求。\n如何选择索引的列顺序有一个经验法则：将选择性最高的列放到索引最前列。当不需要考虑排序和分组时，将选择性最高的列放在前面通常是很好的。这时候索引的作用只是用于优化 WHERE 条件的查找。在这种情况下，这样设计的索引确实能够最快地过滤出需要的行，对于在 WHERE 子句中只使用了索引部分前缀列的查询来说选择性也更高。\n性能不只是依赖于索引列的选择性，也和查询条件的有关。可能需要根据那些运行频率最高的查询来调整索引列的顺序，比如排序和分组，让这种情况下索引的选择性最高。同时在优化性能的时候，可能需要使用相同的列但顺序不同的索引来满足不同类型的查询需求。\n索引列顺序的选择总结如下：\n区分度最高的放在联合索引的最左侧（区分度=列中不同值的数量/列的总行数）； 尽量把字段长度小的列放在联合索引的最左侧（因为字段长度越小，一页能存储的数据量越大，IO性能也就越好）； 使用最频繁的列放到联合索引的左侧（这样可以比较少的建立一些索引）。 7.1.6. 设计三星索引 7.1.6.1. 概念 对于一个查询而言，一个三星索引，可能是其最好的索引。如果查询使用三星索引，一次查询通常只需要进行一次磁盘随机读以及一次窄索引片的扫描，因此其相应时间通常比使用一个普通索引的响应时间少几个数量级。\n三星索引概念是在《Rrelational Database Index Design and the optimizers》 一书中提出来的。原文如下：\nThe index earns one star if it places relevant rows adjacent to each other, a second star if its rows are sorted in the order the query needs, and a final star if it contains all the columns needed for the query.\n索引将相关的记录放到一起则获得一星；如果索引中的数据顺序和查找中的排列顺序一致则获得二星；如果索引中的列包含了查询中需要的全部列则获得三星。\n一星：\n如果一个查询相关的索引行是相邻的或者至少相距足够靠近的话，必须扫描的索引片宽度就会缩至最短，也就是说，让索引片尽量变窄，也就是我们所说的索引的扫描范围越小越好。\n二星（排序星）：\n在满足一星的情况下，当查询需要排序，group by、 order by，如果查询所需的顺序与索引是一致的（索引本身是有序的），是不是就可以不用再另外排序了，一般来说排序可是影响性能的关键因素。\n三星（宽索引星）：\n在满足了二星的情况下，如果索引中所包含了这个查询所需的所有列（包括 where 子句 和 select 子句中所需的列，也就是覆盖索引），这样一来，查询就不再需要回表了，减少了查询的步骤和 IO 请求次数，性能几乎可以提升一倍。\n第三颗星是最重要。因为将一个列排除在索引之外可能会导致很多磁盘随机读（回表操作）。第一和第二颗星重要性差不多，可以理解为第三颗星比重是 50%，第一颗星为 27%，第二颗星为 23%，所以在大部分的情况下，会先考虑第一颗星，但会根据业务情况调整这两颗星的优先度。\n7.1.6.2. 达成三星索引 1 2 3 4 5 6 7 8 9 10 11 -- 创建表 create table customer( cno int, lname varchar(10), fname varchar(10), sex int, weight int, city varchar(10) ); -- 创建索引 create index idx_cust on customer(city, lname, fname, cno); 符合三星索引\n1 select cno,fname from customer where lname =’xx’ and city =’yy’ order by fname; 第一颗星：所有等值谓词的列，是组合索引的开头的列，可以把索引片缩得很窄，符合。 第二颗星：order by 的 fname 字段在组合索引中且是索引自动排序好的，符合。 第三颗星：select 中的 cno 字段、fname 字段在组合索引中存在，符合。 7.1.6.3. 达不成三星索引 1 2 3 4 5 6 7 8 9 -- 创建表 CREATE TABLE `test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_name` varchar(100) DEFAULT NULL, `sex` int(11) DEFAULT NULL, `age` int(11) DEFAULT NULL, `c_date` datetime DEFAULT NULL, PRIMARY KEY (`id`), ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8; 达不到三星索引的sql分析\n1 select user_name,sex,age from test where user_name like \u0026#39;test%\u0026#39; and sex = 1 ORDER BY age; 如果建立索引(user_name,sex,age)：\n第三颗星，满足 第一颗星，满足 第二颗星，不满足，user_name 采用了范围匹配，sex 是过滤列，此时 age 列无法保证有序的。 如果建立索引(sex, age，user_name)：\n第一颗星，不满足，只可以匹配到 sex，sex 选择性很差，意味着是一个宽索引片 第二颗星，满足，等值 sex 的情况下，age 是有序的 第三颗星，满足，select 查询的列都在索引列中 以上2个索引，都是无法同时满足三星索引设计中的三个需求的，只能尽力满足2个。而在多数情况下，能够满足2颗星，已经能缩小很大的查询范围了，具体最终要保留那一颗星（排序星 or 窄索引片星）\n7.1.7. 主键索引设计原则 选择很少修改的列做为主键。因为行是按照聚集索引物理排序的，如果主键频繁改变(update)，物理顺序会改变，MySQL 要不断调整 B+树，并且中间可能会产生页面的分裂和合并等等，会导致性能会急剧降低。 满足业务需求的情况下，尽量降低主键的长度。 插入数据时，尽量选择顺序插入，选择使用 AUTO_INCREMENT 自增主键。 尽量不要使用 UUID 做主键或者是其他自然主键，如身份证号。因为其数值既是乱序，长度又过长 业务操作时，避免对主键的修改。 7.1.8. 避免建立冗余索引和重复索引 建立冗余索引和重复索引，这样会增加查询优化器生成执行计划的时间。需要单独维护重复的索引，并且优化器在优化查询的时候也需要逐个地进行考虑，这会影响性能。重复索引是指在相同的列上按照相同的顺序创建的相同类型的索引。\n重复索引示例：primary key(id)、index(id)、unique index(id) 冗余索引示例：index(a,b,c)、index(a,b)、index(a) 冗余索引和重复索引有一些不同。如果创建了索引(A,B)，再创建索引(A)就是冗余索引，因为这只是前一个索引的前缀索引。因此索引(A,B)也可以当作索引(A)来使用（这种冗余只是对 B-Tree 索引来说的)。但是如果再创建索引 (B,A)，则不是冗余索引，索引(B)也不是，因为 B 不是索引(A,B)的最左前缀列。已有的索引(A)，扩展为(A，ID)，其中 ID 是主键，对于 InnoDB 来说主键列已经包含在二级索引中了，所以这也是冗余的。\n删除冗余索引和重复索引，但首先要做的是找出这样的索引。可以通过写一些复杂的访问INFORMATION_SCHEMA表的查询来找\n7.1.9. 删除未使用的索引 有一些表永远不用的索引，建议考虑删除。\n7.1.10. 不建议使用索引的情况 where 条件中用不到的字段不适合建立索引 表记录较少 需要经常增删改的表或者字段 参与列计算的列不适合建索引 7.2. 索引使用策略 Tips: 若不按以下策略使用索引，可能会导致索引失效。\n7.2.1. 不在索引列上做任何操作 如果查询中的列不是独立的，则 MySQL 就不会使用索引。“独立的列”是指索引列不能是表达式的一部分，也不能是函数的参数。即不在索引列上做任何的逻辑处理\n1 2 SELECT * FROM order_exp WHERE order_status + 1 = 1; SELECT * FROM order_exp WHERE TO_DAYS(insert_time) - TO_DAYS(expire_time) \u0026lt;= 10; WHERE 中的表达式其实等价于 order_status = 0，但是 MySQL 无法自动解析这个方程式；在索引列上使用函数，也是无法利用索引的。\n7.2.2. 尽量全值匹配 建立了联合索引列后，如果搜索条件中的列和索引列一致的话，这种情况就称为全值匹配\n1 select * from order_exp where insert_time=\u0026#39;2021-03-22 18:34:55\u0026#39; and order_status=0 and expire_time=\u0026#39;2021-03-22 18:35:14\u0026#39;; 上述语句的联合索引中的三个列都用到。值得注意是，WHERE 子句中的几个and连接的搜索条件的顺序对查询结果是否使用索引，是没有任何影响。查询优化器会分析这些搜索条件并且按照可以使用的索引中列的顺序来决定先使用哪个搜索条件，后使用哪个搜索条件。\n7.2.3. 最佳左前缀法则 建立了联合索引列，尽管在查询语句中无法包含全部联合索引中的列，但也要遵守最左前缀法则。『最左前缀法则』指的是查询从索引的最左前列开始并且不跳过索引中的列，并且遇到范围查询(\u0026gt;、\u0026lt;、between、like)就停止匹配\n1 2 3 4 5 6 7 8 9 10 -- 联合索引 index(a,b,c) -- 能使用索引 select * from t where a = \u0026#39;xxx\u0026#39; and b = 1; select * from t where a = \u0026#39;xxx\u0026#39;; -- 无法使用索引 select * from t where b = 1; select * from t where b = 1 and c = \u0026#39;3\u0026#39;; 根据联合索引的数据结构可以分析，索引是先按a，再按b，最后按c来进行排序，如果跳过a直接使用b或者c去匹配，因为b与c可能是乱序，所以查询优化器可能就会直接选择全表扫描。如下图，对(a, b) 建立索引，a 在索引树中是全局有序的，而 b 是全局无序，局部有序（当a相等时，会根据b进行排序）。直接执行 b = 2 这种查询条件无法使用索引。\n如果想使用联合索引中尽可能多的列，搜索条件中的各个列必须是联合索引中从最左边连续的列。\nTips: 最左前缀法则中指的最左边的列，是指在查询时，联合索引的最左边的字段(即是第一个字段)必须存在，与编写SQL时条件的先后顺序无关。mysql的查询优化器会优化成索引可以识别的形式。\n7.2.4. 范围条件放最后 这一点，也是针对联合索引而言。所有记录都是按照索引列的值从小到大的顺序排好序的，而联合索引则是按创建索引时的顺序进行分组排序。\n对于一个联合索引来说，虽然对多个列都进行范围查找时只能用到最左边那个索引列，但是如果左边的列是精确查找，则右边的列可以进行范围查找：\n1 2 3 4 5 6 mysql\u0026gt; explain select * from order_exp_cut where insert_time=\u0026#39;2021-03-22 18:34:55\u0026#39; and order_status=0 and expire_time\u0026gt;\u0026#39;2021-03-22 18:23:57\u0026#39; and expire_time\u0026lt;\u0026#39;2021-03-22 18:35:00\u0026#39;; +----+-------------+---------------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+ | 1 | SIMPLE | order_exp_cut | NULL | range | u_idx_day_status | u_idx_day_status | 13 | NULL | 1 | 100.00 | Using index condition | +----+-------------+---------------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+ 中间有范围查询会导致后面的列全部失效，无法充分利用这个联合索引\n1 2 3 4 5 6 mysql\u0026gt; explain select * from order_exp_cut where insert_time=\u0026#39;2021-03-22 18:23:42\u0026#39; and order_status\u0026gt;-1 and expire_time\u0026gt;\u0026#39;2021-03-22 18:35:14\u0026#39;; +----+-------------+---------------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+ | 1 | SIMPLE | order_exp_cut | NULL | range | u_idx_day_status | u_idx_day_status | 8 | NULL | 1 | 50.00 | Using index condition | +----+-------------+---------------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+ Notes: 当范围查询使用 \u0026gt;= 或 \u0026lt;= 时，是可以使用联合索引（待验证）\n7.2.5. 优先使用覆盖索引 覆盖索引：是指查询使用了索引，并且需要返回索引所包含的列，即包含了所有查询字段(where, select, ordery by, group by 包含的字段)的索引。\n对于频繁的查询优先考虑使用覆盖索引。覆盖索引是非常有用的工具，能够极大地提高性能，三星索引里最重要的那颗星就是宽索引星。查询只需要扫描索引而无须回表的有以下好处：\n索引条目通常远小于数据行大小，所以如果只需要读取索引，那 MySQL 就会极大地减少数据访问量。这对缓存的负载非常重要，因为这种情况下响应时间大部分花费在数据拷贝上。覆盖索引对于 I/O 密集型的应用也有帮助，因为索引比数据更小，更容易全部放入内存中。 可以把随机IO变成顺序IO加快查询效率。由于覆盖索引是按键值的顺序存储的，对于IO密集型的范围查找来说，对比随机从磁盘读取每一行的数据IO要少的多，因此利用覆盖索引在访问时也可以把磁盘的随机读取的IO转变成索引查找的顺序IO。。 由于 InnoDB 的聚簇索引，覆盖索引对 InnoDB 表特别有用。InnoDB 的二级索引在叶子节点中保存了行的主键值，所以如果二级主键能够覆盖查询，则可以避免对主键索引的二次查询。 所以尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致))，不是必要的情况下减少select *，除非是需要将表中的全部列检索后，进行缓存。\n1 2 3 4 5 6 7 8 9 10 11 12 13 mysql\u0026gt; EXPLAIN SELECT * FROM order_exp_cut WHERE insert_time = \u0026#39;2022-08-04 10:39:11\u0026#39; AND order_status = 0 AND expire_time = \u0026#39;2022-08-04 10:39:16\u0026#39;; +----+-------------+---------------+------------+-------+------------------+------------------+---------+-------------------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------------+------------+-------+------------------+------------------+---------+-------------------+------+----------+-------+ | 1 | SIMPLE | order_exp_cut | NULL | const | u_idx_day_status | u_idx_day_status | 13 | const,const,const | 1 | 100.00 | NULL | +----+-------------+---------------+------------+-------+------------------+------------------+---------+-------------------+------+----------+-------+ mysql\u0026gt; EXPLAIN SELECT expire_time,id FROM order_exp_cut WHERE insert_time = \u0026#39;2022-08-04 10:39:11\u0026#39; AND order_status = 0 AND expire_time = \u0026#39;2022-08-04 10:39:16\u0026#39;; +----+-------------+---------------+------------+-------+------------------+------------------+---------+-------------------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------------+------------+-------+------------------+------------------+---------+-------------------+------+----------+-------------+ | 1 | SIMPLE | order_exp_cut | NULL | const | u_idx_day_status | u_idx_day_status | 13 | const,const,const | 1 | 100.00 | Using index | +----+-------------+---------------+------------+-------+------------------+------------------+---------+-------------------+------+----------+-------------+ 7.2.6. 不等于要慎用 mysql 在使用不等于(!=或者\u0026lt;\u0026gt;)的时候无法使用索引会导致全表扫描\n1 2 3 4 5 6 mysql\u0026gt; EXPLAIN SELECT * FROM order_exp WHERE order_no \u0026lt;\u0026gt; \u0026#39;DD00_6S\u0026#39;; +----+-------------+-----------+------------+------+---------------+------+---------+------+-------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-----------+------------+------+---------------+------+---------+------+-------+----------+-------------+ | 1 | SIMPLE | order_exp | NULL | ALL | idx_order_no | NULL | NULL | NULL | 10311 | 55.93 | Using where | +----+-------------+-----------+------------+------+---------------+------+---------+------+-------+----------+-------------+ 7.2.7. null / not Null 对索引的影响 需要注意搜索条件是 null/not null 对索引的可能影响\nis not null 容易导致索引失效。 is null 则会区分被检索的列是否允许为 null，如果可以为 null 则会走 ref 类型的索引访问；如果不能为 null，也是全表扫描。 注意：如果联合索引上使用时覆盖索引时，情况又不同，可以使用上索引。\n7.2.8. 注意使用 LIKE 关键字 在查询语句中使用LIKE关键字进行查询时，如果匹配字符串的第一个字符为 % 时，索引将不会被使用。如果 % 不是在第一个位置，索引就会被使用。\n如果使用覆盖索引可以改善这个索引失效的问题。即在select语句中只查询索引列，则即使在开始使用 % 进行匹配，索引也会被使用。\n1 SELECT 索引列1,索引列2,... FROM table_name WHERE 索引列 like \u0026#39;%xxx\u0026#39;; 7.2.8.1. 索引下推 索引下推（Index Condition Pushdown，ICP），对于使用辅助的联合索引，正常情况按照最左前缀原则，如：\n1 SELECT * FROM test_table WHERE `name` like \u0026#39;moon%\u0026#39; AND age = 22 AND position =\u0026#39;manager\u0026#39; 如上情况只会走 name 字段索引，因为根据 name 字段过滤完，得到的索引行里的 age 和 position 是无序的，无法很好的利用索引。\n在 MySQL 5.6 之前的版本，此查询只能在联合索引里匹配到名字是'moon'开头的索引，然后拿这些索引对应的主键逐个回表，到主键索引上找出相应的记录，再比对 age 和 position 这两个字段的值是否符合。 MySQL 5.6 以后版本引入了索引下推优化，可以在索引遍历过程中，对索引中包含的所有字段先做判断，过滤掉不符合条件的记录之后再回表，可以有效的减少回表次数。使用了索引下推优化后，上面那个查询在联合索引里匹配到名字是'moon'开头的索引之后，同时还会在索引里过滤 age 和 position 这两个字段，将过滤完后剩下的索引对应的主键再回表查整行数据。 索引下推会减少回表次数，对于 innodb 引擎的表索引下推只能用于二级索引(辅助索引)或联合索引(复合索引)，innodb 的主键索引（聚簇索引）树叶子节点上保存的是全行数据，所以这个时候索引下推并不会起到减少查询全行数据的效果。\nTips: 在某些范围查询中，如果根据首个字段范围查找过滤的结果集过大（已接近全表扫描），然后每条记录还进行索引下推比对，此时 MySQL 会认为使用索引下推的方式反而会加大查询成本，因此就会选择不使用索引下推。\n7.2.9. 字符类型加引号 搜索条件列为字符串时，如果不加单引号，会导致索引失效。因为 MySQL 的查询优化器，会自动的进行类型转换（隐式转换），然后再进行比较，自然造成索引失效。\n7.2.10. 查询语句中使用 OR 关键字 查询语句只有OR关键字时，如果OR前后的两个条件的列都是索引时，查询中将使用索引；如果OR前后有其中一个条件的列不是索引，查询中将不使用索引。\n7.2.11. 使用索引扫描来做排序和分组 只有当索引的列顺序和 ORDER BY 子句的顺序完全一致，并且所有列的排序方向（倒序或正序）都一样时，MySQL 才能够使用索引来对结果做排序。如果查询需要关联多张表，则只有当 ORDER BY 子句引用的字段全部为第一个表时，才能使用索引做排序。\n7.2.12. 排序要当心 ASC、DESC 两种排序别混用。对于使用联合索引进行排序的场景，各个排序列的排序顺序是一致的，也就是要么各个列都是 ASC 规则排序，要么都是 DESC 规则排序。 排序列包含非同一个索引的列。这种情况也不能使用索引进行排序 7.2.13. 索引 SET 规范 尽量避免使用外键约束\n不建议使用外键约束（foreign key），但一定要在表与表之间的关联键上建立索引。 外键可用于保证数据的参照完整性，但建议在业务端实现。 外键会影响父表和子表的写操作从而降低性能。 7.2.14. 查询语句中使用联合（多列）索引 在业务场景中，如果存在多个查询条件，考虑针对于查询字段建立索引时，建议建立联合索引，而非单列索引。联合（多列）索引是在表的多个字段上创建一个索引。\n如果查询使用的是联合索引，具体的结构示意图如下：\n只有查询条件中使用了联合索引中第一个字段时，索引才会被使用；如果查询条件中只使用多列索引中非首个字段的其他字段时，索引将会失效。这就是“最佳左前缀法则”原理。\n7.2.15. 少创建多个单列索引 如果一个表创建了多个单列索引，即使where查询条件中都使用这些索引列，但也只会匹配一个最优索引生效。所以一般建议使用组合索引。\n7.2.16. in / not in 对索引的影响 在索引列使用 in 查询条件，是可以使用索引；但使用 not in 则索引失效。\n如果是主键索引，则 in 和 not in，均可使用索引。\n7.2.17. 主键顺序插入优化 以下分析主键顺序插入的性能是要高于乱序插入的原因\n7.2.17.1. 数据组织方式 在 InnoDB 存储引擎中，表数据都是根据主键顺序组织存放的，这种存储方式的表称为索引组织表(index organized table IOT)。\n行数据，都是存储在聚集索引的叶子节点上的。InnoDB 的逻辑结构图（引用于《MySQL数据库01-体系架构》笔记）：\n在 InnoDB 引擎中，数据行是记录在逻辑结构 page 页中的，而每一个页的大小是固定的，默认16K。那也就意味着，一个页中所存储的行也是有限的，如果插入的数据行row在该页存储不小，将会存储到下一个页中，页与页之间会通过指针连接。\n7.2.17.2. 页分裂 页可以为空，也可以填充一半，也可以填充100%。每个页包含了2-N行数据(如果一行数据过大，会行溢出)，根据主键排列。\n7.2.17.3. 主键顺序插入效果 从磁盘中申请页，主键顺序插入 第一个页没有满，继续往第一页插入 当第一个也写满之后，再写入第二个页，页与页之间会通过指针连接 当第二页写满了，再往第三页写入 7.2.17.4. 主键乱序插入效果 假设加入1#,2#页都已经写满了，存放了如图所示的数据\n此时再插入id为50的记录，是不会开启新一个页\n因为，索引结构的叶子节点是有顺序的。按照顺序，应该存储在47之后。\n但是47所在的1#页，已经写满了，存储不了50对应的数据了。那么此时会开辟一个新的页3#。\n但是并不会直接将50存入3#页，而是会将1#页后一半的数据，移动到3#页，然后在3#页，插入50。\n移动数据，并插入id为50的数据之后，那么此时，这三个页之间的数据顺序是有问题的。1#的下一个页，应该是3#，3#的下一个页是2#。所以，此时，需要重新设置链表指针。\n上述的这种现象，称之为\u0026quot;页分裂\u0026quot;，是比较耗费性能的操作。\n7.2.17.5. 页合并 目前表中已有数据的索引结构(叶子节点)如下：\n当对已有数据进行删除时，具体的效果如下：\n当删除一行记录时，实际上记录并没有被物理删除，只是记录被标记（flaged）为删除并且它的空间变得允许被其他记录声明使用。\n当继续删除2#的数据记录\n当页中删除的记录达到 MERGE_THRESHOLD（默认为页的50%），InnoDB 会开始寻找最靠近的页（前或后）看看是否可以将两个页合并以优化空间使用。\n删除数据，并将页合并之后，再次插入新的数据21，则直接插入3#页\n这个里面所发生的合并页的这个现象，就称之为\u0026quot;页合并\u0026quot;。\nTips: MERGE_THRESHOLD 合并页的阈值，可以自行设置，在创建表或者创建索引时指定。\n7.2.18. SQL 提示 SQL 提示，是优化数据库的一个重要手段，即是在 SQL 语句中加入一些人为的提示来达到优化操作的目的。\nuse index：建议 MySQL 使用哪一个索引完成此次查询（仅仅是建议，mysql 内部还会再次进行评估是否使用索引） 1 2 3 4 5 6 mysql\u0026gt; explain select * from tb_user use index(idx_user_pro) where profession = \u0026#39;软件工程\u0026#39;; +----+-------------+---------+------------+------+----------------------+----------------------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+------+----------------------+----------------------+---------+-------+------+----------+-------+ | 1 | SIMPLE | tb_user | NULL | ref | idx_user_pro_age_sta | idx_user_pro_age_sta | 36 | const | 4 | 100.00 | NULL | +----+-------------+---------+------------+------+----------------------+----------------------+---------+-------+------+----------+-------+ ignore index：忽略指定的索引 1 2 3 4 5 6 mysql\u0026gt; explain select * from tb_user ignore index(idx_user_pro) where profession = \u0026#39;软件工程\u0026#39;; +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | tb_user | NULL | ALL | NULL | NULL | NULL | NULL | 24 | 10.00 | Using where | +----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+ force index：强制使用索引 1 2 3 4 5 6 mysql\u0026gt; explain select * from tb_user force index(idx_user_pro) where profession = \u0026#39;软件工程\u0026#39;; +----+-------------+---------+------------+------+----------------------+----------------------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+---------+------------+------+----------------------+----------------------+---------+-------+------+----------+-------+ | 1 | SIMPLE | tb_user | NULL | ref | idx_user_pro_age_sta | idx_user_pro_age_sta | 36 | const | 4 | 100.00 | NULL | +----+-------------+---------+------------+------+----------------------+----------------------+---------+-------+------+----------+-------+ 7.3. 索引策略总结 针对于数据量较大，且查询比较频繁的表建立索引。 出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列，或者包含在 ORDER BY、GROUP BY、DISTINCT 中的字段。但并不建议将符合 where 和 ORDER BY、GROUP BY 中的字段的列都各自建立一个索引，通常是建立联合索引效果更好 尽量选择区分度高的列作为索引，尽量建立唯一索引，区分度越高，使用索引的效率越高。 如果是字符串类型的字段，字段的长度较长，可以针对于字段的特点，建立前缀索引。 尽量使用联合索引，减少单列索引，查询时，联合索引很多时候可以覆盖索引，节省存储空间，避免回表，提高查询效率。 要控制索引的数量，索引并不是越多越好。索引越多，维护索引结构的代价也就越大，会影响增删改的效率。 如果索引列不能存储 NULL 值，请在创建表时使用 NOT NULL 约束它。当优化器知道每列是否包含 NULL 值时，它可以更好地确定哪个索引最有效地用于查询。 多表join的关联列 联合索引是否被使用判断总结（下图引用自网络）： Tips: like KK% 相当于=常量，%KK和%KK%相当于范围查询\n8. MySQL 的查询成本 8.1. 成本的概念 MySQL 执行一个查询可以有不同的执行方案，它会选择其中成本最低，或者说代价最低的那种方案去真正的执行查询。在 MySQL 中一条查询语句的执行成本是由下边这两个方面组成的：\nI/O 成本：数据库的表经常使用的 MyISAM、InnoDB 存储引擎都是将数据和索引都存储到磁盘上的，当查询表中的记录时，需要先把数据或者索引加载到内存中然后再操作。这个从磁盘到内存这个加载的过程损耗的时间称之为 I/O 成本。 CPU 成本：读取以及检测记录是否满足对应的搜索条件、对结果集进行排序等这些操作损耗的时间称之为 CPU 成本 对于 InnoDB 存储引擎来说，页是磁盘和内存之间交互的基本单位，MySQL 规定读取一个页面花费的成本默认是1.0，读取以及检测一条记录是否符合搜索条件的成本默认是0.2。1.0、0.2 这些数字称之为成本常数，这两个成本常数最常用到，当然还有其他的成本常数。\n注意，不管读取记录时需不需要检测是否满足搜索条件，其成本都算是0.2。\n8.2. 单表查询的成本 - 基于成本的优化步骤（TODO mark: 待补充） 在一条单表查询语句真正执行之前，MySQL 的查询优化器会找出执行该语句所有可能使用的方案，对比之后找出成本最低的方案，这个成本最低的方案就是所谓的执行计划，之后才会调用存储引擎提供的接口真正的执行查询，这个过程主要是：\n根据搜索条件，找出所有可能使用的索引 计算全表扫描的代价 计算使用不同索引执行查询的代价 对比各种执行方案的代价，找出成本最低的那一个 示例成本分析sql\n1 2 3 4 5 6 7 8 9 10 SELECT * FROM order_exp WHERE order_no IN ( \u0026#39;DD00_6S\u0026#39;, \u0026#39;DD00_9S\u0026#39;, \u0026#39;DD00_10S\u0026#39; ) AND expire_time \u0026gt; \u0026#39;2021-03-22 18:28:28\u0026#39; AND expire_time \u0026lt;= \u0026#39;2021-03-22 18:35:09\u0026#39; AND insert_time \u0026gt; expire_time AND order_note LIKE \u0026#39;%7 排 1%\u0026#39; AND order_status = 0; 8.2.1. 根据搜索条件，找出所有可能使用的索引 对于 B+树索引来说，只要索引列和常数使用=、\u0026lt;=\u0026gt;、IN、NOT IN、IS NULL、IS NOT NULL、\u0026gt;、\u0026lt;、\u0026gt;=、\u0026lt;=、BETWEEN、!=（不等于也可以写成\u0026lt;\u0026gt;）或者 LIKE 操作符连接起来，就可以产生一个所谓的范围区间（LIKE匹配字符串前缀也行），MySQL 把一个查询中可能使用到的索引称之为 possible keys。\n8.3. (！待整理)单表查询的成本 - 基于索引统计数据的成本计算 TODO: 整理中\n8.4. (！待整理)EXPLAIN 输出成本 TODO: 整理中\n8.5. Optimizer Trace 分析优化器执行计划 对于 MySQL5.6 之前的版本来说，只能通过 EXPLAIN 语句查看到最后优化器决定使用的执行计划，却无法知道它为什么做这个决策。\n在 MySQL 5.6 以及之后的版本中，MySQL 提出了一个 optimizer trace 的功能，这个功能可以让我们方便的查看优化器生成执行计划的整个过程，这个功能的开启与关闭由系统变量 optimizer_trace 决定\n8.5.1. 检查是否开启 Trace 功能 语法：\n1 SHOW VARIABLES LIKE \u0026#39;optimizer_trace\u0026#39;; 结果：\n1 2 3 4 5 6 +-----------------+--------------------------+ | Variable_name | Value | +-----------------+--------------------------+ | optimizer_trace | enabled=off,one_line=off | +-----------------+--------------------------+ 1 row in set (0.01 sec) 从输出结果可知。enabled 值为 off，表明这个功能默认是关闭的。one_line 的值是控制输出格式是否在一行显示，如果为 on 那么所有输出都将在一行中展示。\n8.5.2. 使用 Trace 功能 开启 Trace 功能，必须首先把 enabled 的值改为 on。同时可设置格式为JSON，并设置 trace 最大能够使用的内存大小，避免解析过程中因为默认内存过小而不能够完整展示。\n1 2 SET optimizer_trace=\u0026#34;enabled=on\u0026#34;,end_markers_in_json=on; set optimizer_trace_max_mem_size=1000000; 输入想要查看优化过程的查询语句，当该查询语句执行完成后，就可以到 information_schema 数据库下的 OPTIMIZER_TRACE 表中查看完整的优化过程。 OPTIMIZER_TRACE 表有 4 个列，分别是：\nQUERY：表示执行的查询语句。 TRACE：表示优化过程的 JSON 格式文本。 MISSING_BYTES_BEYOND_MAX_MEM_SIZE：由于优化过程可能会输出很多，如果超过某个限制时，多余的文本将不会被显示，这个字段展示了被忽略的文本字节数。 INSUFFICIENT_PRIVILEGES：表示是否没有权限查看优化过程，默认值是 0，只有某些特殊情况下才会是 1，暂时不关心这个字段的值。 最后，输入以下查询语句，检查 information_schema.optimizer_trace 就可以知道 MySQL 是如何执行 SQL 的\n1 SELECT * FROM information_schema.OPTIMIZER_TRACE \\G; 注：展示的内容极多，日后需要时再慢慢了解\n最后当停止查看语句的优化过程时，把 optimizer trace 功能关闭\n1 SET optimizer_trace=\u0026#34;enabled=off\u0026#34;; 8.5.3. 注意事项 开启 trace 会影响 mysql 性能，所以只能临时分析 sql 使用，用完之后立即关闭\n8.6. 连接查询的成本 连接查询总成本 = 单次访问驱动表的成本 + 驱动表扇出数(即从驱动表查出的记录数) x 单次访问被驱动表的成本\n8.7. (！待整理)调节成本常数 TODO: 整理中\n9. 全局考虑性能优化 9.1. 为什么查询速度会慢 快速的查询，真正重要是响应时间。如果把查询看作是一个任务，那么它由一系列子任务组成，每个子任务都会消耗一定的时间。如果要优化查询，实际上要优化其子任务，要么消除其中一些子任务，要么减少子任务的执行次数，要么让子任务运行得更快。\nMySQL 查询的生命周期大致可以按照顺序来看：从客户端，到服务器，然后在服务器上进行解析，生成执行计划，执行，并返回结果给客户端。其中“执行”可以认为是整个生命周期中最重要的阶段，这其中包括了大量为了检索数据到存储引擎的调用以及调用后的数据处理，包括排序、分组等。\n在完成这些任务的时候，查询需要在不同的地方花费时间，包括网络，CPU计算，生成统计信息和执行计划、锁等待（互斥等待）等操作，尤其是向底层存储引擎检索数据的调用操作，这些调用需要在内存操作，CPU 操作和内存不足时导致的IO操作上消耗时间。根据存储引擎不同，可能还会产生大量的上下文切换以及系统调用。优化查询的目的就是减少和消除这些操作所花费的时间。\n9.2. 查询执行的流程 MySQL 执行一个查询的过程。具体流程如下图：\n客户端发送一条查询给服务器。 服务器先检查查询缓存，如果命中了缓存，则立刻返回存储在缓存中的结果。否则进入下一阶段。 服务器端进行 SQL 解析、预处理，再由优化器生成对应的执行计划。 MySQL 根据优化器生成的执行计划，调用存储引擎的 API 来执行查询。 将结果返回给客户端。 很多查询优化工作实际上就是遵循一些原则让优化器能够按照预想的合理的方式运行。但除了查询优化器之外，其他部分也是对查询的性能有一定程度的影响。\n9.3. MySQL 客户端/服务器通信协议 大致理MySQL解通信协议。MySQL 客户端和服务器之间的通信协议是“半双工”的，这意味着，在任何一个时刻，要么是由服务器向客户端发送数据，要么是由客户端向服务器发送数据，这两个动作不能同时发生。所以，无法也无须将一个消息切成小块独立来发送。\n这种协议让 MySQL 通信简单快速，但会没法进行流量控制。一旦一端开始发生消息，另一端要接收完整个消息才能响应它。\n客户端用一个单独的数据包将查询传给服务器。当查询的语句很长的时，参数max_allowed_packet就相当重要。一般服务器响应给用户的数据通常很多，由多个数据包组成。当服务器开始响应客户端请求时，客户端必须完整地接收整个返回结果，而不能简单地只取前面几条结果，然后让服务器停止发送数据。这种情况下，客户端若接收完整的结果，然后取前面几条需要的结果，或者接收完几条结果后就直接地断开连接，都不是好方法。这也是在必要的时候一定要在查询中加上LIMIT限制的原因。\n当客户端从服务器取数据时，看起来是一个拉数据的过程，但实际上是 MySQL 在向客户端推送数据的过程。客户端不断地接收从服务器推送的数据，客户端也没法让服务器停下来。\n多数连接 MySQL 的库函数都可以获得全部结果集并缓存到内存里，还可以逐行获取需要的数据。默认一般是获得全部结果集并缓存到内存中。MySQL 通常需要等所有的数据都已经发送给客户端才能释放这条查询所占用的资源，所以接收全部结果并缓存通常可以减少服务器的压力，让查询能够早点结束、早点释放相应的资源。\n当使用库函数从 MySQL 获取数据时，其结果看起来都像是从 MySQL 服务器获取数据，而实际上都是从这个库函数的缓存获取数据。多数情况下这没什么问题，但是如果需要返回一个很大的结果集的时候，这种做法就可能有问题，因为库函数会花很多时间和内存来存储所有的结果集。对于 Java 程序来说，很有可能发生 OOM，所以 MySQL 的 JDBC 里提供了setFetchSize()之类的功能，来解决这个问题：\n当statement设置以下属性时，采用的是流数据接收方式，每次只从服务器接收部份数据，直到所有数据处理完毕，不会发生JVM OOM。 1 2 setResultSetType(ResultSet.TYPE_FORWARD_ONLY); setFetchSize(Integer.MIN_VALUE); 调用statement的enableStreamingResults方法，实际上enableStreamingResults方法内部封装的就是第 1 种方式。 设置连接属性useCursorFetch=true(5.0 版驱动开始支持)，statement以TYPE_FORWARD_ONLY打开，再设置fetch size参数，表示采用服务器端游标，每次从服务器取fetch_size条数据。 1 2 3 4 5 6 7 8 con = DriverManager.getConnection(url); ps = (PreparedStatement) con.prepareStatement(sql,ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); ps.setFetchSize(Integer.MIN_VALUE); ps.setFetchDirection(ResultSet.FETCH_REVERSE); rs = ps.executeQuery(); while (rs.next()) { // ...实际的业务处理 } 9.4. 查询状态 对于一个 MySQL 连接，或者说一个线程，任何时刻都有一个状态，该状态表示了 MySQL 当前正在做什么。在一个查询的生命周期中，状态会变化很多次。\n9.5. 查询优化处理 查询的生命周期的下一步是将一个 SQL 转换成一个执行计划，MySQL 再依照这个执行计划和存储引擎进行交互。这包括多个子阶段：解析 SQL、预处理、优化 SQL 执行计划。这个过程中任何错误（例如语法错误）都可能终止查询。在实际执行中，这几部分可能一起执行也可能单独执行。\nMySQL 的查询优化器是一个非常复杂的部件，它使用了很多优化策略来生成一个最优的执行计划。优化策略可以简单地分为两种，一种是静态优化，一种是动态优化。\n静态优化可以直接对解析树进行分析，并完成优化。例如，优化器可以通过一些简单的代数变换将WHERE条件转换成另一种等价形式。静态优化不依赖于特别的数值，如WHERE条件中带入的一些常数等。静态优化在第一次完成后就一直有效，即使使用不同的参数重复执行查询也不会发生变化。可以认为这是一种“编译时优化”。\n动态优化则和查询的上下文有关，也可能和很多其他因素有关，例如WHERE条件中的取值、索引中条目对应的数据行数等。这需要在每次查询的时候都重新评估，可以认为这是“运行时优化”。\n优化器是相当复杂性和智能的。如果没有必要，不要去干扰优化器的工作，让优化器按照它的方式工作。尽量按照优化器的提示去优化我们的表、索引和 SQL 语句，比如写查询，或者重新设计更优的库表结构，或者添加更合适的索引。但是请尽可能的保持 SQL 语句的简洁。当然，虽然优化器已经很智能了，但是有时候也无法给出最优的结果。如果能够确认优化器给出的不是最佳选择，并且清楚优化背后的原理，那么也可以帮助优化器做进一步的优化。\nMySQL 架构由多个层次组成。在服务器层有查询优化器，却没有保存数据和索引的统计信息。统计信息申存储引擎实现，不同的存储引擎可能会存储不同的统计信息（也可以按照不同的格式存储统计信息)。某些引擎，例如 Archive 引擎，则根本就没有存储任何统计信息!\n因为服务器层没有任何统计信息，所以 MySQL 查询优化器在生成查询的执行计划时，需要向存储引擎获取相应的统计信息。存储引擎则提供给优化器对应的统计信息，包括:每个表或者索引有多少个页面、每个表的每个索引的基数是多少、数据行和索引长度、索引的分布信息等。优化器根据这些信息来选择一个最优的执行计划。\n9.6. 查询执行引擎 在解析和优化阶段，MySQL 将生成查询对应的执行计划，MySQL 的查询执行引擎则根据这个执行计划来完成整个查询。相对于查询优化阶段，查询执行阶段不是那么复杂：MySQL 只是简单地根据执行计划给出的指令逐步执行。\n9.7. 返回结果给客户端 查询执行的最后一个阶段是将结果返回给客户端。即使查询不需要返回结果集给客户端，MySQL 仍然会返回这个查询的一些信息，如该查询影响到的行数。如果查询可以被缓存，那么 MySQL 在这个阶段也会将结果存放到查询缓存中。\nMySQL 将结果集返回客户端是一个增量、逐步返回的过程。一旦服务器开始生成第一条结果时，MySQL 就可以开始向客户端逐步返回结果集了。\n这样处理有两个好处﹔服务器端无须存储太多的结果，也就不会因为要返回太多结果而消耗太多内存。另外，这样的处理也让 MySQL 客户端第一时间获得返回的结果。结果集中的每一行都会以一个满足 MySQL 客户端/服务器通信协议的封包发送，再通过 TCP 协议进行传输，在 TCP 传输的过程中，可能对 MySQL 的封包进行缓存然后批量传输。\n10. InnoDB 引擎底层解析 10.1. InnoDB 中的统计数据 查询成本的时候经常用到一些统计数据，比如通过 SHOW TABLE STATUS 可以看到关于表的统计数据，通过 SHOW INDEX 可以看到关于索引的统计数据。这些统计都是相应的存储引擎来实现。\n10.1.1. 统计数据存储方式 InnoDB 提供了两种存储统计数据的方式：\n永久性的统计数据，这种统计数据存储在磁盘上，也就是服务器重启之后这些统计数据还在。 非永久性的统计数据，这种统计数据存储在内存中，当服务器关闭时这些这些统计数据就都被清除掉了，等到服务器重启之后，在某些适当的场景下才会重新收集这些统计数据。 MySQL提供了系统变量innodb_stats_persistent来控制到底采用哪种方式去存储统计数据。在 MySQL 5.6.6 之前，innodb_stats_persistent的值默认是OFF，也就是说 InnoDB 的统计数据默认是存储到内存的，之后的版本中innodb_stats_persistent的值默认是ON，也就是统计数据默认被存储到磁盘中。\n查询当前存储统计数据的方式\n1 2 3 4 5 6 mysql\u0026gt; SHOW VARIABLES LIKE \u0026#39;innodb_stats_persistent\u0026#39;; +-------------------------+-------+ | Variable_name | Value | +-------------------------+-------+ | innodb_stats_persistent | ON | +-------------------------+-------+ InnoDB 默认是以表为单位来收集和存储统计数据的，也就是说可以把某些表的统计数据（以及该表的索引统计数据）存储在磁盘上，把另一些表的统计数据存储在内存中。\n在创建和修改表的时候通过指定 STATS_PERSISTENT 属性来指明该表的统计数据存储方式：\n1 2 CREATE TABLE 表名 (...) Engine=InnoDB, STATS_PERSISTENT = (1|0); ALTER TABLE 表名 Engine=InnoDB, STATS_PERSISTENT = (1|0); 当STATS_PERSISTENT=1时，表明我们想把该表的统计数据永久的存储到磁盘上 当STATS_PERSISTENT=0时，表明我们想把该表的统计数据临时的存储到内存中 如果在创建表时未指定STATS_PERSISTENT属性，那默认采用系统变量innodb_stats_persistent的值作为该属性的值 10.1.2. 基于磁盘的永久性统计数据 当选择把某个表以及该表索引的统计数据存放到磁盘上时，实际上是把这些统计数据存储到了两个表里：\n1 2 3 4 5 6 7 8 mysql\u0026gt; SHOW TABLES FROM mysql LIKE \u0026#39;innodb%\u0026#39;; +---------------------------+ | Tables_in_mysql (innodb%) | +---------------------------+ | innodb_index_stats | | innodb_table_stats | +---------------------------+ 2 rows in set (0.01 sec) 这两个表都位于 mysql 系统数据库下。\ninnodb_table_stats 存储了关于表的统计数据，每一条记录对应着一个表的统计数据。 innodb_index_stats 存储了关于索引的统计数据，每一条记录对应着一个索引的一个统计项的统计数据。 10.1.2.1. innodb_table_stats innodb_table_stats 表结构与字段的作用\n1 2 3 4 5 6 7 8 9 10 11 mysql\u0026gt; desc mysql.innodb_table_stats; +--------------------------+-----------------+------+-----+-------------------+-----------------------------------------------+ | Field | Type | Null | Key | Default | Extra | +--------------------------+-----------------+------+-----+-------------------+-----------------------------------------------+ | database_name | varchar(64) | NO | PRI | NULL | | | table_name | varchar(199) | NO | PRI | NULL | | | last_update | timestamp | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED on update CURRENT_TIMESTAMP | | n_rows | bigint unsigned | NO | | NULL | | | clustered_index_size | bigint unsigned | NO | | NULL | | | sum_of_other_index_sizes | bigint unsigned | NO | | NULL | | +--------------------------+-----------------+------+-----+-------------------+-----------------------------------------------+ database_name 数据库名 table_name 表名 last_update 本条记录最后更新时间 n_rows 表中记录的条数 clustered_index_size 表的聚簇索引占用的页面数量 sum_of_other_index_sizes 表的其他索引占用的页面数量 innodb_table_stats 表内容分析，几个重要统计信息项的值如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 mysql\u0026gt; SELECT * FROM mysql.innodb_table_stats WHERE database_name = \u0026#39;tempdb\u0026#39;; +---------------+---------------------+---------------------+---------+----------------------+--------------------------+ | database_name | table_name | last_update | n_rows | clustered_index_size | sum_of_other_index_sizes | +---------------+---------------------+---------------------+---------+----------------------+--------------------------+ | tempdb | account | 2022-08-04 22:30:34 | 7 | 1 | 0 | | tempdb | article | 2022-08-04 22:29:42 | 17 | 1 | 0 | | tempdb | article_data | 2022-08-04 22:29:42 | 0 | 1 | 1 | | tempdb | article_type | 2022-08-04 22:29:42 | 4 | 1 | 1 | | tempdb | comment | 2022-08-04 22:29:42 | 2 | 1 | 1 | | tempdb | course | 2022-08-05 14:50:03 | 4 | 1 | 0 | | tempdb | dept | 2022-08-05 14:49:53 | 6 | 1 | 0 | | tempdb | emp | 2022-08-05 14:50:13 | 17 | 1 | 0 | | tempdb | employee | 2022-08-04 22:29:43 | 6 | 1 | 0 | | tempdb | order_exp | 2022-08-04 22:29:43 | 10625 | 97 | 74 | | tempdb | order_exp_cut | 2022-08-04 22:29:43 | 2 | 1 | 2 | | tempdb | s1 | 2022-08-04 22:29:43 | 3 | 1 | 3 | | tempdb | s2 | 2022-08-04 22:29:43 | 3 | 1 | 3 | | tempdb | salgrade | 2022-08-05 14:49:53 | 8 | 1 | 0 | | tempdb | score | 2022-08-04 22:29:43 | 5 | 1 | 0 | | tempdb | student | 2022-08-05 14:49:53 | 4 | 1 | 0 | | tempdb | student_course | 2022-08-05 14:50:23 | 6 | 1 | 2 | | tempdb | tb_sku | 2022-08-05 11:08:22 | 9214983 | 234496 | 0 | | tempdb | tb_user | 2022-08-05 10:39:47 | 24 | 1 | 2 | | tempdb | teacher | 2022-08-04 22:29:54 | 5 | 1 | 1 | | tempdb | type | 2022-08-04 22:30:04 | 2 | 1 | 0 | | tempdb | user | 2022-08-04 22:30:14 | 3 | 1 | 0 | +---------------+---------------------+---------------------+---------+----------------------+--------------------------+ n_rows 的值是10625，表明order_exp表中大约有10625条记录，注意这个数据是估计值。 clustered_index_size 的值是97，表明order_exp表的聚簇索引占用97个页面，这个值是也是一个估计值。 sum_of_other_index_sizes 的值是74，表明order_exp表的其他索引一共占用81个页面，这个值是也是一个估计值。 n_rows 统计项的收集，InnoDB 统计一个表中有多少行记录的执行流程如下：\n按照一定算法（并不是纯粹随机的）选取几个叶子节点页面，计算每个页面中主键值记录数量，然后计算平均一个页面中主键值的记录数量乘以全部叶子节点的数量就算是该表的 n_rows 值\n这个 n_rows 值精确与否取决于统计时采样的页面数量，MySQL 用名为 innodb_stats_persistent_sample_pages 的系统变量来控制使用永久性的统计数据时，计算统计数据时采样的页面数量。该值设置的越大，统计出的 n_rows值越精确，但是统计耗时也就最久；该值设置的越小，统计出的 n_rows 值越不精确，但是统计耗时特别少。所以在实际使用是需要我们去权衡利弊，该系统变量的默认值是 20。\nInnoDB 默认是以表为单位来收集和存储统计数据的，也可以单独设置某个表的采样页面的数量，设置方式就是在创建或修改表的时候通过指定STATS_SAMPLE_PAGES属性来指明该表的统计数据存储方式：\n1 2 CREATE TABLE 表名 (...) Engine=InnoDB, STATS_SAMPLE_PAGES = 具体的采样页面数量; ALTER TABLE 表名 Engine=InnoDB, STATS_SAMPLE_PAGES = 具体的采样页面数量; 如果在创建表的语句中并没有指定STATS_SAMPLE_PAGES属性的话，将默认使用系统变量innodb_stats_persistent_sample_pages的值作为该属性的值。clustered_index_size和sum_of_other_index_sizes 统计项的收集牵涉到很具体的 InnoDB 表空间的知识和存储页面数据的细节\n10.1.2.2. innodb_index_stats innodb_index_stats 表结构与字段的作用\n1 2 3 4 5 6 7 8 9 10 11 mysql\u0026gt; desc mysql.innodb_table_stats; +--------------------------+-----------------+------+-----+-------------------+-----------------------------------------------+ | Field | Type | Null | Key | Default | Extra | +--------------------------+-----------------+------+-----+-------------------+-----------------------------------------------+ | database_name | varchar(64) | NO | PRI | NULL | | | table_name | varchar(199) | NO | PRI | NULL | | | last_update | timestamp | NO | | CURRENT_TIMESTAMP | DEFAULT_GENERATED on update CURRENT_TIMESTAMP | | n_rows | bigint unsigned | NO | | NULL | | | clustered_index_size | bigint unsigned | NO | | NULL | | | sum_of_other_index_sizes | bigint unsigned | NO | | NULL | | +--------------------------+-----------------+------+-----+-------------------+-----------------------------------------------+ database_name 数据库名 table_name 表名 index_name 索引名 last_update 本条记录最后更新时间 stat_name 统计项的名称 stat_value 对应的统计项的值 sample_size 为生成统计数据而采样的页面数量 stat_description 对应的统计项的描述 innodb_index_stats 表的每条记录代表着一个索引的一个统计项，几个重要统计信息项的值如下：\nindex_name：说明该记录是哪个索引的统计信息 从示例结果中可以看出来，PRIMARY 索引（也就是主键）占了3条记录，idx_expire_time 索引占了6条记录。 stat_name：表示针对该索引的统计项名称 n_leaf_pages：表示该索引的叶子节点占用多少页面。 size：表示该索引共占用多少页面。 n_diff_pfxNN：表示对应的索引列不重复的值有多少。（其中“NN”可以被替换为01、02、03\u0026hellip;的数字） stat_value：该索引在该统计项上的值 sample_size：表明了采样的页面数量是多少 对于有多个列的联合索引来说，采样的页面数量是：innodb_stats_persistent_sample_pages × 索引列的个数。 当需要采样的页面数量大于该索引的叶子节点数量的话，就直接采用全表扫描来统计索引列的不重复值数量了。所以可以在查询结果中看到不同索引对应的size列的值可能是不同的。 stat_description：指的是来描述该统计项的含义的 以u_idx_day_status索引为例。\nn_diff_pfx01 表示的是统计 insert_time 这单单一个列不重复的值有多少。 n_diff_pfx02 表示的是统计 insert_time,order_status 这两个列组合起来不重复的值有多少。 n_diff_pfx03 表示的是统计 insert_time,order_status,expire_time 这三个列组合起来不重复的值有多少。 n_diff_pfx04 表示的是统计 key_pare1、key_pare2、expire_time、id 这四个列组合起来不重复的值有多少。 查询innodb_stats_persistent_sample_pages值\n1 2 3 4 5 6 mysql\u0026gt; show variables like \u0026#39;%innodb_stats_persistent_sample_pages%\u0026#39;; +--------------------------------------+-------+ | Variable_name | Value | +--------------------------------------+-------+ | innodb_stats_persistent_sample_pages | 20 | +--------------------------------------+-------+ 10.1.2.3. 定期更新统计数据 随着不断的对表进行增删改操作，表中的数据也一直在变化，innodb_table_stats 和 innodb_index_stats 表里的统计数据也在变化。MySQL 提供了如下两种更新统计数据的方式：\n开启 innodb_stats_auto_recalc\n系统变量 innodb_stats_auto_recalc 决定着服务器是否自动重新计算统计数据，它的默认值是 ON，也就是该功能默认是开启的。每个表都维护了一个变量，该变量记录着对该表进行增删改的记录条数，如果发生变动的记录数量超过了表大小的 10%，并且自动重新计算统计数据的功能是打开的，那么服务器会重新进行一次统计数据的计算，并且更新 innodb_table_stats 和 innodb_index_stats 表。\n自动重新计算统计数据的过程是异步发生的，也就是即使表中变动的记录数超过了 10%，自动重新计算统计数据也不会立即发生，可能会延迟几秒才会进行计算。\nInnoDB 默认是以表为单位来收集和存储统计数据的，所以也可以单独为某个表设置是否自动重新计算统计数的属性，设置方式就是在创建或修改表的时候通过指定STATS_AUTO_RECALC属性来指明该表的统计数据存储方式：\n1 2 CREATE TABLE 表名 (...) Engine=InnoDB, STATS_AUTO_RECALC = (1|0); ALTER TABLE 表名 Engine=InnoDB, STATS_AUTO_RECALC = (1|0) 当STATS_AUTO_RECALC=1时，表明想让该表自动重新计算统计数据 当STATS_AUTO_RECALC=0时，表明不想让该表自动重新计算统计数据 如果在创建表时未指定STATS_AUTO_RECALC属性，那默认采用系统变量innodb_stats_auto_recalc的值作为该属性的值。 手动调用 ANALYZE TABLE 语句来更新统计信息\n如果 innodb_stats_auto_recalc 系统变量的值为 OFF 的话。也可以手动调用 ANALYZE TABLE 语句来重新计算统计数据\n1 ANALYZE TABLE 表名; ANALYZE TABLE 语句会立即重新计算统计数据，也就是这个过程是同步的，在表中索引多或者采样页面特别多时这个过程可能会特别慢最好在业务不是很繁忙的时候再运行。\n10.1.3. 手动更新 innodb_table_stats 和 innodb_index_stats 表 innodb_table_stats 和 innodb_index_stats 表就相当于一个普通的表一样，能对它们做增删改查操作。这也就意味着可以手动更新某个表或者索引的统计数据。\n步骤一：更新 innodb_table_stats 表 步骤二：让 MySQL 查询优化器重新加载更改过的数据 更新完 innodb_table_stats 只是单纯的修改了一个表的数据，需要让 MySQL 查询优化器重新加载我们更改过的数据，运行以下的命令即可：\n1 FLUSH TABLE 表名; 10.2. InnoDB 记录存储结构和索引页结构(TODO mark: 整理中) InnoDB 是一个将表中的数据存储到磁盘上的存储引擎，所以即使关机后重启，数据还是存在的。而真正处理数据的过程是发生在内存中的，所以需要把磁盘中的数据加载到内存中，如果是处理写入或修改请求的话，还需要把内存中的内容刷新到磁盘上。\n当从表中获取某些记录时，InnoDB 采取的方式是：将数据划分为若干个页，以页作为磁盘和内存之间交互的基本单位，InnoDB 中页的大小一般为 16 KB。也就是在一般情况下，一次最少从磁盘中读取 16KB 的内容到内存中，一次最少把内存中的 16KB 内容刷新到磁盘中。\n以记录为单位来向表中插入数据的，这些记录在磁盘上的存放方式也被称为行格式或者记录格式。InnoDB 存储引擎设计了4种不同类型的行格式，分别是 Compact、Redundant、Dynamic 和 Compressed 行格式。\n10.2.1. 指定行格式的语法 创建或修改表的语句中指定行格式：\n1 CREATE TABLE 表名 (列的信息) ROW_FORMAT=行格式名称; 10.2.1.1. COMPACT 类型行格式 MySQL对于变长列，如 VARCHAR(M)、VARBINARY(M)、各种 TEXT 类型，各种 BLOB 类型。存储真实数据的同时，也会保存这些数据占用的字节数。\nCompact 行格式会把可能为 NULL 的列统一管理起来，存储到 NULL 值列表。每个允许存储 NULL 的列对应一个二进制位，二进制位的值为 1 时，代表该列的值为 NULL。二进制位的值为 0 时，代表该列的值不为 NULL。\n还有用于描述记录的记录头信息，它是由固定的 5 个字节组成。5 个字节也就是 40 个二进制位，不同的位代表不同的意思。\n列名 位数 说明 预留位1 1 没有使用 预留位2 1 没有使用 delete_mask 1 标记该记录是否被删除 min_rec_mask 1 B+树的每层非叶子节点中的最小记录都会添加该标记 n_owned 4 表示当前记录拥有的记录数 heap_no 13 表示当前记录在页的位置信息 record_type 3 表示当前记录的类型，0 表示普通记录，1 表示 B+树非叶子节点记录，2 表示最小记录，3 表示最大记录 next_record 16 表示下一条记录的相对位置 记录的真实数据除了自定义的列的数据以外，MySQL 会为每个记录默认的添加一些列（也称为隐藏列），包括：\nDB_ROW_ID(row_id)：非必须，6 字节，表示行 ID，唯一标识一条记录 DB_TRX_ID：必须，6 字节，表示事务 ID DB_ROLL_PTR：必须，7 字节，表示回滚指针 10.2.1.2. Redundant 行格式（待整理） Redundant 行格式是 MySQL5.0 之前用的一种行格式\n10.2.2. 索引页格式（待整理） TODO: 待整理\n10.3. InnoDB 的内存结构总结 10.3.1. InnoDB 的内存结构和磁盘存储结构图总结 其中的 Insert/Change Buffer 主要是用于对二级索引的写入优化，Undo 空间则是 undo 日志一般放在系统表空间，但是通过参数配置后，也可以用独立表空间存放，所以用虚线表示。\n10.3.2. 扩展知识 计算机基础原理，但凡是对硬盘的读写操作，都是先从硬盘上数据读取到内存，然后cpu再从内存中读取数据。修改数据数据的操作也是一样，从先内存中的数据进行操作后，然后再将内存中的数据持久化到磁盘中。\n所以之前在mysql的成本计算中提到的I/O成本与CPU成本。就是分别指从磁盘到内存，然后cpu读取内存的时间成本。\n内存的扩容成本是很昂贵的，一般都是尽可能一次申请合适的内存。如JVM，一般会将堆的最小值与最大值都设置为一样，为了防止在运行过程中出现向cpu重新申请内存的操作。因为一般内存区域都是连续的。所以一般内存的扩容是先向cpu申请新的一块内存区域，然后将原本的内存中的数据再复制到新的内存区域中。这个扩容成本是巨大的。\n11. MySQL 的执行原理 11.1. 单表访问之索引合并 MySQL 在一般情况下执行一个查询时最多只会用到单个二级索引，但存在有特殊情况，在这些特殊情况下也可能在一个查询中使用到多个二级索引，MySQL 中这种使用到多个索引来完成一次查询的执行方法称之为：索引合并（index merge），具体的索引合并算法有以下的3种：\n11.1.1. 并集（Intersection）合并 某个查询可以使用多个二级索引，将从多个二级索引中查询到的结果取交集。\n1 SELECT * FROM table WHERE index1 = \u0026#39;a\u0026#39; AND index2 = \u0026#39;b\u0026#39;; 假设这个查询使用 Intersection 合并的方式执行的话，那执行过程是：\n从index1相应的二级索引对应的B+树中取出index1 = 'a'的相关记录。 从index2相应的二级索引对应的B+树中取出index2 = 'b'的相关记录。 因为二级索引的组成结构都是由索引列+主键构成，所以可以计算出这两个结果集中主键的交集。然后再根据这个主键的交集去进行回表操作，也就是从聚簇索引中把指定的主键的完整行记录返回。\n【问题】：不同的查询方式的执行成本比较\n只读取一个二级索引的成本：按照某个搜索条件读取一个二级索引，根据从该二级索引得到的主键值进行回表操作，然后再过滤其他的搜索条件 读取多个二级索引之后取交集成本：按照不同的搜索条件分别读取不同的二级索引，将从多个二级索引得到的主键值取交集，然后进行回表操作 【解释】：虽然读取多个二级索引比读取一个二级索引消耗性能，但是大部分情况下读取二级索引的操作是顺序I/O，而回表操作是随机I/O，所以如果只读取一个二级索引时需要回表的记录数特别多，而读取多个二级索引之后取交集的记录数非常少，当节省的因为回表而造成的性能损耗比访问多个二级索引带来的性能损耗更高时，读取多个二级索引后取交集比只读取一个二级索引的成本更低。\nMySQL 在以下特定的情况下才可能会使用到 Intersection 索引合并\n11.1.1.1. 情况一：等值匹配 二级索引列是等值匹配的情况 对于联合索引来说，在联合索引中的每个列都必须等值匹配，不能出现只匹配部分列的情况。 满足以上的情况，才可能会使用到索引合并\n11.1.1.2. 情况二：主键列可以是范围匹配 1 SELECT * FROM 表 WHERE 主键列 \u0026gt; 100 AND 索引列 = \u0026#39;a\u0026#39;; 对于 InnoDB 的二级索引来说，记录先是按照索引列进行排序，如果该二级索引是一个联合索引，那么会按照联合索引中的各个列依次排序。而二级索引的用户记录是由索引列 + 主键构成的，二级索引列的值相同的记录可能会有好多条，这些索引列的值相同的记录又是按照主键的值进行排序的\n之所以在二级索引列都是等值匹配的情况下才可能使用Intersection 索引合并，是因为在这种情况下根据二级索引查询出的结果集是按照主键值排序的。因为各个二级索引中查询的到的结果集按主键排好序，取交集的过程比较容易。按照有序的主键值去回表取记录有个专有名词，叫：Rowid Ordered Retrieval，简称 ROR。\n如果从各个二级索引中查询出的结果集并不是按照主键排序的话，那就要先把结果集中的主键值排序完再来做上边的那个过程，就比较耗时了。\n****不仅是多个二级索引之间可以采用 Intersection 索引合并，索引合并也可以有聚簇索引。在搜索条件中有主键的范围匹配的情况下也可以使用 Intersection 索引合并索引合并。如上例，通过二级索引查询到相应的主键值集合，因为主键已经排序，所以很容易就匹配主键范围条件，取最终的结果集\n11.1.1.3. 索引并集合并小结 上边的情况一和情况二只是发生 Intersection 索引合并的必要条件，不是充分条件。也就是说即使情况一、情况二成立，也不一定发生 Intersection索引合并，这得看优化器的具体分析。优化器只有在单独根据搜索条件从某个二级索引中获取的记录数太多，导致回表开销太大，而通过 Intersection 索引合并后需要回表的记录数大大减少时才会使用 Intersection 索引合并。\n11.1.2. Union 合并 查询时经常会把既符合某个搜索条件的记录取出来，也把符合另外的某个搜索条件的记录取出来，然后这些不同的搜索条件之间是OR关系。有时候OR关系的不同搜索条件会使用到不同的索引，如：\n1 SELECT * FROM 表 WHERE 索引列1 = \u0026#39;a\u0026#39; OR 索引列2 = \u0026#39;b\u0026#39;; Union 是并集的意思，适用于使用不同索引的搜索条件之间使用OR连接起来的情况。与 Intersection 索引合并类似，MySQL 在某些特定的情况下才可能会使用到 Union 索引合并：\n11.1.2.1. 情况一：等值匹配 分析与Intersection 合并同理\n11.1.2.2. 情况二：主键列可以是范围匹配 分析与 Intersection 合并同理\n11.1.2.3. 情况三：使用 Intersection 索引合并的搜索条件 此情况是，搜索条件的某些部分使用 Intersection 索引合并的方式得到的主键集合和其他方式得到的主键集合取交集。比如：\n1 SELECT * FROM order_exp WHERE insert_time = \u0026#39;a\u0026#39; AND order_status = \u0026#39;b\u0026#39; AND expire_time = \u0026#39;c\u0026#39; OR (order_no = \u0026#39;a\u0026#39; AND expire_time = \u0026#39;b\u0026#39;); 优化器可能采用这样的方式来执行这个查询：\n先按照搜索条件 order_no = \u0026lsquo;a\u0026rsquo; AND expire_time = \u0026lsquo;b\u0026rsquo;从索引 idx_order_no 和 idx_expire_time 中使用 Intersection 索引合并的方式得到一个主键集合。 再按照搜索条件 insert_time = \u0026lsquo;a\u0026rsquo; AND order_status = \u0026lsquo;b\u0026rsquo; AND expire_time = \u0026lsquo;c\u0026rsquo; 从联合索引 u_idx_day_status 中得到另一个主键集合。 采用 Union 索引合并的方式把上述两个主键集合取并集，然后进行回表操作，将结果返回给客户端。 11.1.2.4. 索引并集合并小结 查询条件符合了以上情况也不一定就会采用 Union 索引合并，也得看优化器的具体分析。优化器只有在单独根据搜索条件从某个二级索引中获取的记录数比较少，通过 Union 索引合并后进行访问的代价比全表扫描更小时才会使用Union 索引合并。\n11.1.3. Sort-Union 合并 Union 索引合并的使用条件必须保证各个二级索引列在进行等值匹配的条件下才可能被用到。有一些情况：\n1 SELECT * FROM 表 WHERE 索引列1 \u0026lt; \u0026#39;a\u0026#39; OR 索引列2 \u0026gt; \u0026#39;z\u0026#39;; 先根据索引列1从二级索引中获取到记录，并将记录上的主键进行排序 同样操作索引列2 两个二级索引主键值都是排好序后，后续的操作和 Union 索引合并方式就一样了 上述这种先按照二级索引记录的主键值进行排序，之后按照 Union 索引合并方式执行的方式称之为 Sort-Union 索引合并，很显然，这种 Sort-Union 索引合并比单纯的 Union 索引合并多了一步对二级索引记录的主键值排序的过程。\n11.1.4. 联合索引替代 Intersection 索引合并 在使用 Intersection 索引合并的方式来处理的查询语句，是因为查询条件的列分别是索引，并且是各个单独的B+树。可以直接将其各个二级索引合并成一个联合索引\n11.2. 连接查询的实现原理 连接查询的基础知识详见前面《连接查询》的章节\n11.2.1. 嵌套循环连接 Nested-Loop Join (NLJ) 算法 两表连接查询：驱动表只会被访问一次，但被驱动表具体访问次数取决于对驱动表执行单表查询后的结果集中的记录条数。 内连接查询：选取哪个表为驱动表都没关系 外连接查询：驱动表是固定的，也就是说左（外）连接的驱动表就是左边的那个表，右（外）连接的驱动表就是右边的那个表。 3个表连接查询：那么首先两表连接得到的结果集作为新的驱动表，然后第三个表作为被驱动表 从上面可以看出，连接查询这个过程就像是一个嵌套的循环，所以这种驱动表只访问一次，但被驱动表却可能被多次访问，访问次数取决于对驱动表执行单表查询后的结果集中的记录条数的连接执行方式称之为嵌套循环连接（Nested-Loop Join），这是最简单，也是最笨拙的一种连接查询算法，时间复杂度是O(N*M*L)。\n11.2.2. 使用索引加快连接速度 被驱动表其实就相当于一次单表查询，假设查询驱动表后的结果集中有N条记录，根据嵌套循环连接算法需要对被驱动表查询N次。\n1 SELECT * FROM e1, e2 WHERE e1.m1 \u0026gt; 1 AND e1.m1 = e2.m2 AND e2.n2 \u0026lt; \u0026#39;d\u0026#39;; 如上示例，如果给被驱动表的连接列（即上例的m2列）建立索引，因为m2列的条件是等值查找，所以可能使用到ref类型的访问方法； 如果m2列是e2表的主键或者唯一二级索引列，那么使用e2.m2 = 常数值这样的条件从 e2 表中查找记录的过程的代价就是常数级别的。在单表中使用主键值或者唯一二级索引列的值进行等值查找的方式称之为const，而 MySQL 把在连接查询中对被驱动表使用主键值或者唯一二级索引列的值进行等值查找的查询执行方式称之为：eq_ref。\n如果连接查询条件列与其他条件列都存在索引，需要从所有索引中选一个代价更低的去执行对被驱动表的查询。当然，建立了索引不一定使用索引，只有在二级索引+回表的代价比全表扫描的代价更低时才会使用索引。\n有时候连接查询的查询列表和过滤条件中可能只涉及被驱动表的部分列，而这些列都是某个索引的一部分，这种情况下即使不能使用eq_ref、ref、ref_or_null或者range这些访问方法执行对被驱动表的查询的话，也可以使用索引扫描，也就是index(索引覆盖)的访问方法来查询被驱动表。\n11.2.3. 基于块的嵌套循环连接 Block Nested-Loop Join (BNL)算法 扫描一个表的过程其实是先把这个表从磁盘上加载到内存中，然后从内存中比较匹配条件是否满足。当表数据量很大的时候，每次访问被驱动表，被驱动表的记录会被加载到内存中，在内存中的每一条记录只会和驱动表结果集的一条记录做匹配，之后就会被从内存中清除掉。然后再从驱动表结果集中拿出另一条记录，再一次把被驱动表的记录加载到内存中一遍，驱动表结果集中有多少条记录，就得把被驱动表从磁盘上加载到内存中多少次。这个 I/O 代价就非常大了，所以需想办法：尽量减少访问被驱动表的次数。\nMySQL 提出了一个 join buffer 的概念，join buffer 就是执行连接查询前申请的一块固定大小的内存，先把若干条驱动表结果集中的记录装在这个 join buffer 中，然后**开始扫描被驱动表，每一条被驱动表的记录一次性和 join buffer 中的多条驱动表记录做匹配，**因为匹配的过程都是在内存中完成的，所以这样可以显著减少被驱动表的 I/O 代价。使用 join buffer 的过程如下图所示：\n其中最好的情况是 join buffer 足够大，能容纳驱动表结果集中的所有记录。这种加入了 join buffer 的嵌套循环连接算法称之为基于块的嵌套连接（Block Nested-Loop Join）算法。\n这个 join buffer 的大小是可以通过启动参数或者系统变量join_buffer_size进行配置，默认大小为 262144 字节（也就是 256KB），最小可以设置为 128 字节。\n1 2 3 4 5 6 mysql\u0026gt; show variables like \u0026#39;join_buffer_size\u0026#39;; +------------------+--------+ | Variable_name | Value | +------------------+--------+ | join_buffer_size | 262144 | +------------------+--------+ 对于优化被驱动表的查询来说，最好是为被驱动表加上效率高的索引，如果实在不能使用索引，并且自己的机器的内存也比较大可以尝试调大join_buffer_size的值来对连接查询进行优化。\n需要注意的是，驱动表的记录并不是所有列都会被放到 join buffer 中，只有查询列表中的列和过滤条件中的列才会被放到 join buffer 中，所以最好不要把*作为查询列表，只将需要的列放到查询列表就好了，这样还可以在 join buffer 中放置更多的记录。\n12. MySQL 的查询重写规则（了解） 12.1. 条件化简 查询语句的搜索条件本质上是一个表达式，MySQL 的查询优化器会简化这些表达式。\n12.1.1. 移除不必要的括号 表达式里有许多无用的括号，如：\n1 ((a = 5 AND b = c) OR ((a \u0026gt; c) AND (c \u0026lt; 5))) 优化器会把那些用不到的括号移除\n1 (a = 5 and b = c) OR (a \u0026gt; c AND c \u0026lt; 5) 12.1.2. 常量传递（constant_propagation） 当表达式是某个列和某个常量做等值匹配（a=5），当这个表达式和其他涉及列a的表达式使用AND连接起来时，可以将其他表达式中的a的值替换为5\n1 2 3 4 5 6 7 8 a = 5 AND b \u0026gt; a -- 替换成 a = 5 AND b \u0026gt; 5 -- 多个列之间存在等值匹配的关系 a = b and b = c and c = 5 -- 替换成 a = 5 and b = 5 and c = 5 12.1.3. 移除没用的条件（trivial_condition_removal） 对于一些永远为TRUE或者FALSE的表达式，优化器会将其移除。\n1 2 3 4 5 (a \u0026lt; 1 and b = b) OR (a = 6 OR 5 != 5) -- 简化后 (a \u0026lt; 1 and TRUE) OR (a = 6 OR FALSE) -- 最终的表达式 a \u0026lt; 1 OR a = 6 12.1.4. 表达式计算 在查询开始执行之前，如果表达式中只包含常量的话，它的值会被先计算出来。\n1 2 3 a = 5 + 1 -- 简化 a = 6 需要注意的是，如果某个列并不是以单独的形式作为表达式的操作数时，比如出现在函数中，出现在某个更复杂表达式中，优化器是不会尝试对这些表达式进行化简的。如：\n1 2 ABS(a) \u0026gt; 5 -a \u0026lt; -8 所以只有搜索条件中索引列和常数使用某些运算符连接起来才可能使用到索引，以上会示例可能会使索引失效\n12.1.5. 常量表检测 使用主键等值匹配或者唯一二级索引列等值匹配作为搜索条件来查询某个表。MySQL把通过这两种方式查询的表称之为常量表（英文名：constant tables）。\n1 2 3 SELECT * FROM table1 INNER JOIN table2 ON table1.column1 = table2.column2 WHERE table1.primary_key = 1; 优化器在分析以上查询语句时，先首先执行常量表查询，然后把查询中涉及到该表的条件全部替换成常数，最后再分析其余表的查询成本。所以这个查询可以使用主键和常量值的等值匹配来查询 table1 表，也就是在这个查询中 table1 表相当于常量表，在分析对 table2 表的查询成本之前，就会执行对 table1 表的查询，并把查询中涉及 table1 表的条件都替换掉，也就是上边的语句会被转换成：\n1 2 SELECT table1 表记录的各个字段的常量值, table2.* FROM table1 INNER JOIN table2 ON table1 表 column1 列的常量值 = table2.column2; 12.2. 外连接消除 外连接和内连接的本质区别就是：对于外连接的驱动表的记录来说，如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录，那么该记录仍然会被加入到结果集中，对应的被驱动表记录的各个字段使用NULL值填充；而内连接的驱动表的记录如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录，那么该记录会被舍弃。\n只要在搜索条件中指定关于被驱动表相关列的值不为NULL，那么外连接中在被驱动表中找不到符合ON子句条件的驱动表记录也就被排除出最后的结果集了，也就是说：在这种情况下：外连接和内连接也就没有什么区别了！\n1 2 3 SELECT * FROM e1 LEFT JOIN e2 ON e1.m1 = e2.m2 WHERE e2.n2 IS NOT NULL; -- 或者 SELECT * FROM e1 LEFT JOIN e2 ON e1.m1 = e2.m2 WHERE e2.m2 = 2; 这种在外连接查询中，指定的WHERE子句中包含被驱动表中的列不为 NULL 值的条件称之为空值拒绝（英文名：reject-NULL）。在被驱动表的WHERE子句符合空值拒绝的条件后，外连接和内连接可以相互转换。这种转换带来的好处就是查询优化器可以通过评估表的不同连接顺序的成本，选出成本最低的那种连接顺序来执行查询。\n12.3. 子查询优化（待补充） TODO: 待补充内容\n子查询的执行方式 标量子查询、行子查询的执行方式 MySQL 对 IN 子查询的优化 物化表转连接 ","permalink":"https://ktzxy.top/posts/v2jc614g72/","summary":"MySQL 性能优化","title":"MySQL 性能优化"},{"content":"﻿\nDay-06-java面向对象 什么是面向对象 面向对象编程(Object-Oriented Programming, OOP)\n面向对象编程的本质就是:==以类的方式组织代码，以对象的组织(封装)数据。==\n抽象\n三大特性:\n​\t封装\n​\t继承\n​\t多态 从认识论角度考虑是先有对象后有类。对象，是具体的事物。类，是抽象的，是对对象的抽象\n从代码运行角度考虑是先有类后有对象。类是对象的模板。\n回顾方法及加深 方法的定义\n修饰符\n返回类型\n==break: 跳出switch，结束循环和return的区别方法名==\n参数列表\n异常抛出\n方法的调用 静态方法\n​\t非静态方法\n​\t形参和实参\n​\t值传递和引用传递\n​\tthis关键字\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 //demo1 类 public class demo1 { //main方法 public static void main(String[] args) { } /* 修饰符 返回值类型 方法名（...）{ //方法体 return 返回值； } */ public String sayHello(){ return \u0026#34;Helloworld!\u0026#34;; } public void printhell(){ return; } public int max(int a,int b){ return a\u0026gt;b ? a :b; //三元运算符 } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.oop.Demo01; public class demo3 { public static void say(){ System.out.println(\u0026#34;HelloWorld!\u0026#34;); } } =================================================== package com.oop.Demo01; public class demo2 { //调用 public static void main(String[] args) { //静态方法 static demo3.say(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.oop.Demo01; public class demo3 { public void say(){ System.out.println(\u0026#34;HelloWorld!\u0026#34;); } } ================================================= package com.oop.Demo01; public class demo2 { public static void main(String[] args) { //非静态方法 //实例化这个类 new //对象类型 对象名 = 对象值； demo3 a = new demo3(); //调用 a.say(); } } 1 2 3 4 5 6 7 8 9 10 public class demo4 { public static void main(String[] args) { //实际参数和形式参数的类型要对应！ int c = demo4.add(1,5); System.out.println(c); } public static int add(int a,int b){ return a+b; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //值传递 public class demo5 { public static void main(String[] args) { int a =1; System.out.println(a); //1 demo5.change(a); System.out.println(a); //1 } //返回值为空 public static void change(int a){ a =10; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 //引用传递：对象，本质还是值传递 public class demo6 { public static void main(String[] args) { Person person = new Person(); System.out.println(person.name); //null demo6.change(person); System.out.println(person.name); } public static void change(Person person){ //person 是一个对象：指向的 -----\u0026gt; Person person = new Person(); 这是一个具体的人，可以改变属性 person.name = \u0026#34;张三\u0026#34;; } } //定义一个Person类，有一个属性：name class Person{ String name; //null } 类与对象的关系 万事万物皆对象\n类是一种抽象的数据类型,它是对某一类事物整体描述/定义,但是并不能代表某一个具体的事物.\n对象是抽象概念的具体实例\n​\t张三就是人的一个具体实例,张三家里的旺财就是狗的一个具体实例。 ​\t能够体现出特点,展现出功能的是具体的实例,而不是一个抽象的概念。\n面向对象的三个阶段 【1】 面向对象分析OOA \u0026ndash; Object Oriented Analysis\n对象：张三，王五，。。\n抽取一个类：人类\n类里面有什么:\n动词——》动态特性——》方法\n名词——》静态特性——》属性\n【2】面向对象设计OOD \u0026ndash; Object Oriented Design\n先有类，再有对象：\n类：人类：Person\n对象：张三，李四\n【3】面向对象编程OOP \u0026ndash; Object Oriented Programming\n局部变量和成员变量的关系 区别1：代码中位置不同\n​\t成员变量：类中方法外定义的变量\n​\t局部变量：方法中定义的变量 代码块中定义的变量\n区别2：代码的作用范围\n​\t成员变量：当前类的很多方法\n​\t局部变量：当前一个方法（当前代码块）\n区别3：是否有默认值\n​\t成员变量：有\n​\t局部变量：没有\n1 2 3 4 5 6 7 8 9 基本类型\t默认值 boolean\tFlase char\t\u0026#39;\\u0000\u0026#39;(null) byte\t(byte)0 short\t(short)0 int\t0 long\t0L\tfloat\t0.0f double\t0.0d 区别4：是否要初始化\n​\t成员变量：不需要\n​\t局部变量：一定需要\n区别5：内存中位置不同\n​\t成员变量：堆内存\n​\t局部变量：栈内存\n区别6：作用时间不同\n​\t成员变量：当前对象从创建到销毁\n​\t局部变量：当前方法从开始执行到执行完毕\n创建与初始化对象 使用new关键字创建对象\n使用new关键字创建的时候，除了分配内存空间之外,还会给创建好的对象进行默认的初始化 以及对类中构造器的调用。\n类中的构造器也称为构造方法, 是在进行创建对象的时候必须要调用的。并且构造器有以下俩个特点:\n​\t1.必须和类的名字相同\n​\t2.必须没有返回类型,也不能写void\n构造器必须要掌握\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 //学生类 public class Person { //属性：字段 String name; int age; double height; double weight; } public void eat(){ int num = 10; //局部变量 System.out.println(\u0026#34;我喜欢吃饭\u0026#34;); } public void sleep(String address){ System.out.println(\u0026#34;我在\u0026#34;+address+\u0026#34;睡觉\u0026#34;); } public String introduce(){ return \u0026#34;我的名字是\u0026#34;+name+\u0026#34;,我的年龄是\u0026#34;+age+\u0026#34;,我的身高是\u0026#34;+height+\u0026#34;,我的体重是\u0026#34;+weight; } 构造器详解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 //java --\u0026gt;class public class Person { //一个类即使什么都不写，它也会存在一个方法 //显示的定义一个构造器 String name; //实例化初始值 //1.使用new关键字 ，本质是在调用构造器 //2.用来初始化值 public Person(){ //this.name = \u0026#34;张三\u0026#34;; } //有参构造:一旦定义了有参构造，就必须显示定义 public Person(String name){ this.name = name; } } ========================================= public class Application { public static void main(String[] args) { //new 实例化了一个对象 Person person = new Person(\u0026#34;zhangsan\u0026#34;); System.out.println(person.name); } } alt+insert 快捷键生成有参和无参 /* 创建对象的过程： 1.第一次遇到一个类的时候，进行类的加载（只加载一次） 2.创建对象，为这个对象在堆中开辟空间 3.为对象进行属性的初始化动作，属性赋值都是默认的初始值 4.new关键字调用构造器，执行构造方法，在构造器中对属性重新进行赋值 new关键字实际上是在调用一个方法，这个方法叫做构造方法（构造器） 调用构造器的时候，如果该类中没有写构造器，那么系统会默认给你分配一个构造器，只是我们看不到罢了 可以自己显示 的将构造器编写出来： 构造器的格式： [修饰符] 构造器的名字(){ } 构造器和方法的区别： 1.没有方法的返回值类型 2.方法体内部不能有return语句 3.构造器的名字很特殊，必须跟类名一样 构造器的作用：不是为了创建对象，因为在调用构造器之前，这个对象就已经创建好了，并且属性有默认的初始化的值。 调用构造器的目的是给属性进行赋值操作的。 注意：我们一般不会在空构造器中进行初始化操作，因为那样的话，每个对象的属性就一样了。 实际上，我们只要保证空构造器的存在就可以了，里面的东西不用写 */ 构造器重载 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class Person { //属性 String name; int age; double height; //空构造器 public Person(){ } public Person(String name,int age,double height){ //构造器的重载 //当形参名字和属性名字重名的时候，会出现就近原则 //在要表示对象的属性前加上this.来修饰，因为this代表的就是你创建的那个对象 this.name = name; this.age = age; this.height = height; } //方法： public void eat(){ System.out.println(\u0026#34;我喜欢吃饭\u0026#34;); } } ===================================================== public class test { public static void main(String[] args) { /* 1.一般保证空构造器的存在，空构造器中一般不会进行属性的赋值操作 2.一般我们会重载构造器，在重载的构造器中进行属性赋值操作 3.在重载构造器以后，假如空构造器忘写了，系统也不会给你分配默认的空构造器了，那么你要调用的话就会出错了。 4.当形参名字和属性名字重名的时候，会出现就近原则 在要表示对象的属性前加上this.来修饰，因为this代表的就是你创建的那个对象 */ Person p = new Person(\u0026#34;张三\u0026#34;,10,175.5); System.out.println(p.name); System.out.println(p.age); System.out.println(p.height); } } 内存分析 this的使用 this指代的就是当前对象\nthis关键字 用法：\n（1）this可以修饰属性：\n当属性名字和形参发生重名的时候，或者 属性名字和局部变量重名的时候，都会发生就近原则，所以如果我要是直接使用变量名字的话就指的是离的近的那个形参或者局部变量，这时候如果我想要表示属性的话，在前面要加上：this.修饰\n如果不发生重名问题的话，实际上你要访问属性也可以省略this.\n（2）this修饰方法\n在同一个类中，方法可以互相调用，this.可以省略不写\n(3)this可以修饰构造器：\n同一个类中的构造器可以相互用this调用，注意：this修饰构造器必须放在第一行\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 String name; int age; double height; public Person(String name,int age,double height){ this(name,age); this.height = height; } public Person(String name,int age){ this(name); this.age = age; } public Person(String name){ this.age = age; } static关键字 static可以修饰：属性，方法，代码块，内部类\n（一）\nstatic修饰属性：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class demo1 { //属性： int id; static int sid; public static void main(String[] args) { //创建具体对象 demo1 t1 = new demo1(); t1.id = 10; t1.sid = 10; demo1 t2 = new demo1(); t2.id = 20; t2.sid = 20; demo1 t3 = new demo1(); t3.id = 30; t3.sid = 30; //读取属性的值： System.out.println(t1.id); //10 System.out.println(t2.id); //20 System.out.println(t3.id); //30 System.out.println(t1.sid); //30 System.out.println(t2.sid); //30 System.out.println(t3.sid); //30 } } 一般官方的推荐访问方式：可以通过类名.属性名的方式去访问：\n总结：\n（1）在类加载的时候一起加载入方法区中的静态域中\n（2）先于对象存在\n（3）访问方式：对象名.属性名 类名.属性名（推荐）\n（4）应用场景：某些特定的数据想要在内存中共享，只有一块 ，这个情况下，就可以用static修饰的属性\n属性：\n静态属性：（类变量）\n非静态属性：（实例变量）\nstatic修饰方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class Demo2 { int id; static int sid; public void a(){ System.out.println(id); System.out.println(sid); System.out.println(\u0026#34;--------a\u0026#34;); } //1.static和public都是修饰符，并列，没有先后顺序， public static void b(){ // System.out.println(this.id); 4.在静态方法中不能使用this关键字 // a(); 3.在静态方法中不能访问非静态的方法 // System.out.println(id); 2.在静态方法中不能访问非静态的属性 System.out.println(sid); System.out.println(\u0026#34;-------b\u0026#34;); } public static void main(String[] args) { //5.非静态的方法可以用对象名.方法名去调用 Demo2 d = new Demo2(); d.a(); //6.静态的方法可以用 对象名.方法名去调用 Demo2.b(); d.b(); //在同一个类中可以直接调用 b(); } } （二）\n1 2 3 4 5 6 7 8 9 10 11 12 13 //static public class Student { private static int age; //静态的变量 多线程 private double score; //非静态的变量 public static void main(String[] args) { Student s1 = new Student(); System.out.println(Student.age); System.out.println(s1.age); System.out.println(s1.score); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class Person { //第二个执行 赋初始值 { System.out.println(\u0026#34;匿名代码块\u0026#34;); } //第一个执行 只执行一次 static { System.out.println(\u0026#34;静态代码块\u0026#34;); } //第三个执行 public Person(){ System.out.println(\u0026#34;构造方法\u0026#34;); } public static void main(String[] args) { Person person1 = new Person(); System.out.println(\u0026#34;===============\u0026#34;); Person person2 = new Person(); } } 1 2 3 4 5 6 7 8 //静态导入包 import static java.lang.Math.random; public class Student{ public void test(){ System.out.println(random()); } } 代码块 【1】类的组成：属性，方法，构造器，代码块，内部类\n【2】代码块分类：普通块，构造块，静态块，同步块（多线程）\n【3】代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public class Demo3 { //属性 int a; static int b; public Demo3() { //空构造器 System.out.println(\u0026#34;这是空构造器\u0026#34;); } //方法 public void a(){ System.out.println(\u0026#34;-----a\u0026#34;); { System.out.println(\u0026#34;这是普通块\u0026#34;); //普通块限制了局部变量的作用范围 System.out.println(\u0026#34;你好\u0026#34;); int num = 10; System.out.println(num); } } public static void b(){ System.out.println(\u0026#34;-----b\u0026#34;); } //构造块 { System.out.println(\u0026#34;这是构造块\u0026#34;); } //静态块 static { System.out.println(\u0026#34;这是静态块\u0026#34;); //在静态块中只能访问：静态属性，静态方法 System.out.println(b); b(); } //构造器 public Demo3(int a){ this.a = a; } public static void main(String[] args) { Demo3 d = new Demo3(); d.a(); } } 总结：\n代码块执行顺序：\n最先执行静态块，只在类加载的时候执行一次，所以一般以后实战写项目：创建工厂，数据库的初始化信息都放入静态块。\n一般用于执行一些全局性的初始化操作。\n再执行构造块，（不常用）\n再执行构造器\n再执行方法中的普通块。\n包 包名定义：\n（1）名字全部小写\n（2）中间用.隔开\n（3）一般都是公司域名倒着写\n（4）加上模块名字\n（5）不能使用系统中的关键字\n（6）包声明的位置一般都在非注释性代码的第一行：\n（7）使用不同包下的类需要导包\n（8）在java.lang包下的类，可以直接使用无需导包。\n封装 我们程序设计要追求“高内聚，低耦合”。高内聚就是类的内部数据细节自己完成，不允许外部干涉；低耦合：仅暴露少量的方法给外部使用。\n封装(数据的隐藏) 通常，应禁止直接访问一个对象中数据的实际表示，而应通过操作接口来访问，这称为信息隐藏。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 /* 封装的好处 1.提高程序的安全性,保护数据 2.隐藏代码的实现细节 3.统一接口 4.系统可维护增加了 */ public class Application { public static void main(String[] args) { Student s1 = new Student(); s1.setName(\u0026#34;张三\u0026#34;); System.out.println(s1.getName()); s1.setId(1535453); System.out.println(s1.getId()); } } ====================================== public class Student { //属性私有 private String name; //名字 private int id; //学号 private char sex; // 性别 //提供一些可以操作这个属性的方法!1/提供一些public 的get、 set方法 //get 获得这个数据 public String getName(){ return this.name; } //set 给这个数据设置值 public void setName(String name){ this.name = name; } //alt + insert 直接获取get和set方法 public int getId() { return id; } public void setId(int id) { this.id = id; } public char getSex() { return sex; } public void setSex(char sex) { this.sex = sex; } } 进行封装：\n（1）将属性私有化，被private修饰\u0026mdash;加人权限修饰符\n一旦加入了权限修饰符，其他人就不可以随意地获取这个属性\n（2）提供public修饰的方法让别人来访问/使用\n（3）即使外界可以通过方法来访问属性了，但是也不能随意访问，因为咱们在方法中可以加入限制条件。\n继承 类是对对象的抽象\n继承是对类的抽象\n继承的本质是对某一批类的抽象，从而实现对现实世界更好的建模。\nextends的意思是“扩展”。子类是父类的扩展。\nJAVA中类只有单继承，没有多继承!\n继承是类和类之间的一种关系。除此之外,类和类之间的关系还有依赖、组合、聚合等。\n继承关系的俩个类，一个为子类(派生类).一个为父类(基类)。子类继承父类,使用关键字extends来表示。\n子类和父类之间,从意义上讲应该具有\u0026quot;is a\u0026quot;的关系.\nobject类\nsuper\n方法重写\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 //在Java中，所有的类。都默认直接或者间接继承Object //父类 public class Person { public void say(){ System.out.println(\u0026#34;你好\u0026#34;); } } =========================================== //派生类 子类 //子类继承了父类，就会拥有父类的全部方法! public class Student extends Person { } =========================================== //派生类 子类 public class Teacher extends Person { } ============================================ //测试类 public class Application { public static void main(String[] args) { Student student = new Student(); student.say(); #输出你好 } } 继承的好处：提供代码的复用性\n父类定义的内容，子类可以直接拿过来用就可以了，不用代码上反复定义了\n需要注意的点：\n父类private修饰的内容，子类实际上也继承，只是因为封装的特性阻碍了直接调用，但是提供了间接调用的方式，可以间接调用。\n总结：\n(1)继承关系：\n父类/基类/超类\n子类/派生类\n子类继承父类一定在合理的范围进行继承的 子类extends父类\n（2）继承的好处：\n1.提高了代码的复用性，父类定义的内容，子类可以直接拿过来用就可以了，不用代码上反复定义了\n2.便于代码的扩展\n3.为了以后多态的使用。是多态的前提。\n（3）父类private修饰的内容，子类也继承过来了。\n（4）一个父类可以有多个子类\n（5）一个子类只能由一个直接父类。\n但是可以间接的继承自其它类\n（6）继承具有传递性：\nObject类是所有类的根基父类。\n所有的类都是直接或者间接的继承自Object\nsuper 指的是父类的\nsuper可以修饰属性，可以修饰方法；\n在子类的方法中，可以通过super.属性 super.方法 的方式，显示的去调用父类提供的属性，方法。在通常情况下，super.可以省略不写：\n在特殊情况下，当子类和父类的属性重名时，你要想使用父类的属性，必须加上修饰符super.,只能通过super.属性来调用。\n在特殊情况下，当子类和父类的方法重名时，你要想使用父类的方法，必须加上修饰符super.,只能通过super.方法来调用。\n在这种情况下，super.就不可以省略。\nsuper修饰构造器：\n其实我们平时写的构造器的第一行都有：super（）\u0026mdash;\u0026ndash;\u0026gt;作用：调用父类的空构造器，只是我们一般都省略不写。（所有构造器的第一行默认情况下都有super（），但是一旦你的构造器中显示的使用super调用了父类构造器，那么这个super（）就不会给你默认分配了。如果构造器中没有显示的调用父类构造器的话，那么第一行都有super（），可以省略不写）\n如果构造器中已经显示的调用super父类构造器，那么它的第一行就没有默认分配的super（）；\n在构造器中，super调用父类构造器和this调用子类构造器只能存在一个，两者不能共存：因为super修饰构造器要放在第一行，this修饰构造器也要放在第一行；\n以后写代码构造器的生成可以直接使用IDEA提供的快捷键：alt+insert\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //父类 public class Person { protected String name = \u0026#34;张三\u0026#34;; } ================================== //子类 public class Student extends Person { public String name = \u0026#34;李四\u0026#34;; public void test(String name){ System.out.println(name); //王五 System.out.println(this.name); //李四 System.out.println(super.name); //张三 } } =================================== //测试类 public class Application { public static void main(String[] args) { Student student = new Student(); student.test(\u0026#34;王五\u0026#34;); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 //父类 public class Person { public void print(){ System.out.println(\u0026#34;张三\u0026#34;); } } ================================== //子类 public class Student extends Person { public void print(){ System.out.println(\u0026#34;李四\u0026#34;); } public void test1(){ print(); //李四 this.print(); //李四 super.print(); //张三 } } =================================== //测试类 public class Application { public static void main(String[] args) { Student student = new Student(); student.test1(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 //父类 public class Person { public Person(){ System.out.println(\u0026#34;Person无参执行了\u0026#34;); } } //子类 public class Student extends Person { public Student() { //隐藏代码：调用了父类的无参构造 super(); //调用父类的构造器，必须要在子类构造器的第一行 System.out.println(\u0026#34;studnet无参实行了\u0026#34;); } } //测试类 public class Application { public static void main(String[] args) { Student student = new Student(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 super注意点: 1. super调用父类的构造方法，必须在构造方法的第一个 2. super 必须只能出现在子类的方法或者构造方法中! 3. super和 this不能同时调用构造方法! vs this: 代表的对象不同: this:本身调用者这个对象 super:代表父类对象的应用 前提 this:没有继承也可以使用 super:只能在继承条件才可以使用 构造方法 this();本类的构造 super():父类的构造! 权限修饰符 1 2 3 4 5 同一个类\t同一个包\t子类\t所有类 private\t* default\t*\t* protected\t*\t*\t* public\t*\t*\t*\t* private\ndefault：缺省修饰符\n**protected：**在不同改包下的子类中依然可以访问\n方法重写 发生在子类和父类中，当子类对父类提供的方法不满意的时候，要对父类的方法进行重写。\n重写有严格的格式要求：\n子类的方法名字和父类必须一致，参数列表（个数，类型，顺序）也要和父类一致。\n重载和重写的区别：\n重载：在同一个类中，当方法名相同，形参列表不同的时候，多个方法构成了重载\n重写：在不同的类中。子类对父类提供的方法不满意的时候，要对父类的方法进行重写\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 //父类 //重写都是方法的重写,和属性无关 public class B { public static void test(){ System.out.println(\u0026#34;B--\u0026gt;test()\u0026#34;); } } ====================================== //子类 //继承 public class A extends B { public static void test(){ System.out.println(\u0026#34;A--\u0026gt;test()\u0026#34;); } } ======================================== //测试类 public class Application { public static void main(String[] args) { //静态方法： 方法的调用只和定义的数据类型有关 A a = new A(); a.test(); //A //父类的引用指向了子类 B b = new A(); b.test(); //B } } //输出： A--\u0026gt;test() B--\u0026gt;test() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 //父类 //重写都是方法的重写,和属性无关 public class B { public static void test(){ System.out.println(\u0026#34;B--\u0026gt;test()\u0026#34;); } } ====================================== //子类 //继承 public class A extends B { //Override 重写 @Override //注解：有功能的注释！ public void test() { System.out.println(\u0026#34;A--\u0026gt;test()\u0026#34;); } } ======================================== //测试类 public class Application { //静态的方法和非静态的方法区别很大！ //静态方法： //方法的调用只和定义的数据类型有关 //非静态：重写 public static void main(String[] args) { A a = new A(); a.test(); //A //父类的引用指向了子类 B b = new A(); b.test(); //B } } //输出： A--\u0026gt;test() A--\u0026gt;test() 1 2 3 4 5 6 7 8 9 10 重写:需要有继承关系，子类重写父类的方法! 1．方法名必须相同 2．参数列表必须相同 3. 修饰符:范围可以扩大但不能缩小: public\u0026gt;Protected\u0026gt;Default\u0026gt;private 4．抛出的异常:范围，可以被缩小，但不能扩大; ClassNotFoundException --\u0026gt; Exception(大) 重写，子类的方法和父类必要一致;方法体不同! 为什么需要重写: 1．父类的功能,子类不一定需要，或者不一定满足! Alt + Insert ; override; Object类 所有类都直接或间接的继承自Object类，Object类是所有Java类的根基类。\n也就意味着所有的Java对象都拥有Object类的属性和方法。\n如果在类的声明中未使用extends关键字指明其父类，则默认继承Object类。\nequals作用：这个方法提供了对对象的内容是否相等的一个比较方式，对象的内容指的就是属性。\n父类Object提供的equals就是在比较==地址，没有实际意义，我们一般不会直接使用父类提供的方法，而是在子类中对这个方法进行重写。\nequals可以使用快捷键快速生成\n类和类之间的关系 （1）将一个类作为另一个类中的方法的形参\n（2）将一个类作为另一个类的属性\n总结：\n一、继承关系 继承指的是一个类（称为子类、子接口）继承另外的一个类（称为父类、父接口）的功能，并可以增加它自己的新功能的能力。在Java中继承关系通过关键字extends明确标识，在设计时一般没有争议性。在UML类图设计中，继承用一条带空心三角箭头的实线表示，从子类指向父类，或者子接口指向父接口。\n二、实现关系 实现指的是一个class类实现interface接口（可以是多个)的功能，实现是类与接口之间最常见的关系。在Java中此类关系通过关键字implements明确标识，在设计时一般没有争议性。在UML类图设计中，实现用一条带空心三角箭头的虚线表示，从类指向实现的接口。\n三、依赖关系 简单的理解，依赖就是一个类A使用到了另一个类B，而这种使用关系是具有偶然性的、临时性的、非常弱的，但是类B的变化会影响到类A。比如某人要过河，需要借用一条船，此时人与船之间的关系就是依赖。表现在代码层面，为类B作为参数被类A在某个method方法中使用。在UML类图设计中，依赖关系用由类A指向类B的带箭头虚线表示。\n四、关联关系 关联体现的是两个类之间语义级别的一种强依赖关系，比如我和我的朋友，这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的，一般是长期性的，而且双方的关系一般是平等的。关联可以是单向、双向的。表现在代码层面，为被关联类B以类的属性形式出现在关联类A中，也可能是关联类A引用了一个类型为被关联类B的全局变量。在UML类图设计中，关联关系用由关联类A指向被关联类B的带箭头实线表示，在关联的两端可以标注关联双方的角色和多重性标记。\n五、聚合关系 聚合是关联关系的一种特例，它体现的是整体与部分的关系，即has-a的关系。此时整体与部分之间是可分离的，它们可以具有各自的生命周期，部分可以属于多个整体对象，也可以为多个整体对象共享。比如计算机与CPU、公司与员工的关系等，比如一个航母编队包括海空母舰、驱护舰艇、舰载飞机及核动力攻击潜艇等。表现在代码层面，和关联关系是一致的，只能从语义级别来区分。在UML类图设计中，聚合关系以空心菱形加实线箭头表示。\n六、组合关系 组合也是关联关系的一种特例，它体现的是一种contains-a的关系，这种关系比聚合更强，也称为强聚合。它同样体现整体与部分间的关系，但此时整体与部分是不可分的，整体的生命周期结束也就意味着部分的生命周期结束，比如人和人的大脑。表现在代码层面，和关联关系是一致的，只能从语义级别来区分。在UML类图设计中，组合关系以实心菱形加实线箭头表示。\n七、总结 对于继承、实现这两种关系没多少疑问，它们体现的是一种类和类、或者类与接口间的纵向关系。其他的四种关系体现的是类和类、或者类与接口间的引用、横向关系，是比较难区分的，有很多事物间的关系要想准确定位是很难的。前面也提到，这四种关系都是语义级别的，所以从代码层面并不能完全区分各种关系，但总的来说，后几种关系所表现的强弱程度依次为：组合\u0026gt;聚合\u0026gt;关联\u0026gt;依赖。\n多态 即同一方法可以根据发送对象的不同而采用多种不同的行为方式。\n一个对象的实际类型是确定的，但可以指向对象的引用的类型有很多\n多态存在的条件\n​\t有继承关系\n​\t子类重写父类方法\n​\t父类引用指向子类对象\n注意: 多态是方法的多态，属性没有多态性。\ninstanceof (类型转换) 引用类型\n（1）先有父类，再有子类：\u0026ndash;》继承 先有子类，再抽取父类\u0026mdash;-》泛化\n（2）什么是多态：\n​\t多态就是多种状态：同一个行为，不同的子类表现出来不同的形态。\n​\t多态指的就是同一个方法调用，然后由于对象不同会产生不同的行为。\n（3）多态的好处：\n​\t为了提高代码的扩展性，符合面向对象的设计原则：开闭原则。\n​\t开闭原则：指的就是扩展是 开放的，修改是关闭的。\n​\t注意：多态可以提高扩展性，但是扩展性没有达到最好，以后会学习反射\n（4）多态的要素：\n一：继承\n二：重写\n三：父类引用指向子类对象。\n应用场合：父类当方法的形参，然后传入的是具体的子类的对象，然后调用同一个方法，根据传入的子类的不同展现出来的效果也不同，构成了多态。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 //父类 public class Person { public void run(){ System.out.println(\u0026#34;run\u0026#34;); } } ================================= //子类 public class Student extends Person { @Override public void run() { System.out.println(\u0026#34;run\u0026#34;); } public void eat() { System.out.println(\u0026#34;eat\u0026#34;); } } =================================== //测试类 public class Application { public static void main(String[] args) { //可以指向的引用类型就不确定了：父类的引用指向子类 //Student 能调用的方法都是自己的或者继承父类的！ Student s1 = new Student(); //Person 父类型，可以指向子类，但是不能调用子类独有的方法 Person s2 = new Student(); Object s3 = new Student(); s2.run(); //子类重写了父类的方法，执行子类的方法 s1.run(); //对象能执行哪些方法，主要看对象左边的类型，和右边关系不大！ ((Student) s2).eat(); //子类重写了父类的方法，执行子类的方法 s1.eat(); } } 多态注意事项: 1.多态是方法的多态，属性没有多态 2．父类和子类，有联系 类型转换异常! ClassCastException ! 3．存在条件:继承关系，方法需要重写，父类引用指向子类对象! Father f1 = new Son( ); 1. static方法,属于类，它不属于实例 2.final常量; 3. private方法; instanceof 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 //Object \u0026gt;Person \u0026gt;Student //Object \u0026gt; person \u0026gt;Teacher //Object \u0026gt; String //System.out.println(X instanceof Y); //能不能编译通过 Object object = new Student(); System.out.println(object instanceof Student); //true System.out.println(object instanceof Person); //true System.out.println(object instanceof Teacher); //false System.out.println(object instanceof Object); //true System.out.println(object instanceof String); //false System.out.println(\u0026#34;========================\u0026#34;); Person person = new Student(); System.out.println(person instanceof Student); //true System.out.println(person instanceof Person); //true System.out.println(person instanceof Teacher); //false System.out.println(person instanceof Object); //true //System.out.println(person instanceof String); //编译报错 System.out.println(\u0026#34;========================\u0026#34;); Student student = new Student(); System.out.println(student instanceof Student); //true System.out.println(student instanceof Person); //true //System.out.println(student instanceof Teacher); //编译报错 System.out.println(student instanceof Object); //true //System.out.println(student instanceof String); //编译报错 final修饰符 【1】修饰变量\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static void main(String[] args) { //第一种情况： //final修饰一个变量，变量的值不可以改变，这个变量也变成了一个字符常量，约定俗称的规定：名字大写 final int A = 10; //final 修饰基本数据类型 // A = 20; //报错：不可以修改值 //第二种情况: final Dog d = new Dog(); //final修饰引用数据类型，难么地址值就不可以改变 // d = new Dog(); 地址值不可以更改 //d对象的属性依然可以改变： d.age = 10; d.weight = 12.5; //第三种情况： final Dog d2 = new Dog(); a(d2); //第四种情况： b(d2); } public static void a(Dog d){ d = new Dog(); } public static void b(final Dog d){ //b被final修饰，指向不可以改变 // d = new Dog(); } 【2】修饰方法\nfinal修饰方法，那么这个方法不可以被该类的子类重写：\n【3】修饰类\nfinal修饰类，代表没有子类，该类不可以被继承：\n一旦一个类被final修饰，那么里面的方法也没有必要用final修饰了（final省略不写）\n类型转化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Application { public static void main(String[] args) { //类型之间的转化：父 子 //高 低 Person a = new Student(); ((Student) a).go(); } } /* 1.父类引用指向子类的对象 2.把子类转换为父类，向上转型； 3.把父类转换为子类，向下转型； 强制转换 4.方便方法的调用，减少重复的代码！简洁 */ 抽象类 abstract修饰符可以用来修饰方法也可以修饰类,如果修饰方法,那么该方法就是抽象方法;如果修饰类,那么该类就是抽象类。\n抽象类中可以没有抽象方法,但是有抽象方法的类一定要声明为抽象类。\n抽象类,不能使用new关键字来创建对象,它是用来让子类继承的。\n抽象方法,只有方法的声明,没有方法的实现,它是用来让子类实现的。\n子类继承抽象类,那么就必须要实现抽象类没有实现的抽象方法,否则该子类也要声明为抽象类。\n抽象类作用：\n​\t在抽象类中定义抽象方法，目的是为了对子类提供一个通用的模板，子类可以在模板的基础上进行开发，先重写父类的抽象方法，然后可以扩展子类自己的内容。抽象类涉及避免了子类设计的随意性，通过抽象类，子类的设计变得更加严格，进行某些程度上的限制。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 //abstract 抽象类 :类 extends：单继承 （接口可以多继承） public abstract class Action { //abstract ，抽象方法，只有方法名字，没有方法的实现 public abstract void doSomething(); //1.不能new这个抽象类，只能靠子类去实现它； 约束！ //2.抽象类中可以写普通的方法 //3.抽象方法必须在抽象类中 } /* 1.在一个类中，会有一类方法，子类对这个方法非常满意，无需重写，直接使用 2.在一个类中，会有一类方法，子类对这个方法非常不满意，会对这个方法进行重写 3.一个方法的方法体去掉，然后被abstract修饰，那么这个方法就变成了一个抽象方法 4.一个类中如果有方法是抽象方法，那么这个类也要变成一个抽象类。 5.一个抽象类中可以有0——n个抽象方法 6.抽象类可以被其它类继承 7.一个类继承一个抽象类，那么这个类可以变成抽象类 8.一般子类不会加abstract修饰，一般会让子类重写父类中的抽象方法 9.子类继承抽象类，就必须重写全部的抽象方法 10.子类如果没有重写父类全部的抽象方法，那么子类也可以变成一个抽象类 11.创建抽象类的对象：---\u0026gt;抽象类不可以创建对象 12.创建子类对象 13.多态的写法：父类只想引用子类对象: */ （1）抽象类不能创建对象，那么抽象类中是否有构造器？\n抽象类中一定有构造器。构造器的作用 给子类初始化对象的时候要先super调用父类的构造器。\n（2）抽象类是否可以被final修饰？\n不能被final修饰，因为抽象类设计的初衷就是给子类继承用的。要是被final修饰了这个抽象类了，就不存在继承了，就没有子类。\n接口 普通类:只有具体实现\n抽象类:具体实现和规范(抽象方法)都有!\n接口:只有规范! 约束和实现分离：面对接口编程\n接口就是规范，定义的是一组规则\n声明类的关键字是class,声明接口的关键字是interface\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 /* 1.类是类，接口是接口，他们是同一层次的概念。 2.接口中没有构造器 3.接口如何声明：interface 4.在JDK1.8之前，接口中只有两部分内容： 常量：固定修饰符：public static final 抽象方法：固定修饰符：public abstract 注意：修饰符可以省略不写，IDE会自动补全 5.类和接口的关系是什么？实现关系 类实现接口 6.一旦实现一个接口，那么实现类要重写接口中的全部抽象方法 7.如果没有全部重写抽象方法，那么这个类可以变成一个抽象类 8.java只有单继承，java还有多实现 一个类继承其它类，只能直接继承一个父类 但是实现类实现接口的话，可以实现多个接口 9.先继承，后实现 10.接口不能创建对象 11.接口如何访问？ 接口.常量名 实现类.常量名 创建实现类的对象 12.在JDK1.8之后，新增非抽象方法： 被public default修饰的非抽象方法： 注意：default修饰符必须要写 实现类中要是想重写接口中的非抽象方法，那么default修饰符必须不能加，否则出错。 静态方法： 注意：static不可以省略不写 静态方法不能重写 疑问:为什么要在接口中加入非抽象方法??? 如果接口中只能定义抽象方法的话，那么我要是修改接口中的内容，那么对实现类的影响太大了，所有实现类都会受到影响现在在接口中加入非抽象方法，对实现类没有影响，想调用就去调用即可。\t*/ //interface 定义的关键字，接口都需要有实现类 public interface uerService { //接口中的所有定义其实都是抽象的 public abstract void add(String name); void delete(String name); } ======================================== public interface demo { } ======================================== //抽象类 ：extends //类 可以实现接口 implements 接口 //实现了接口的类，就需要重写接口中的方法 //多继承 利用接口可以实现多继承 public class userServicempl implements uerService,demo{ @Override public void add(String name) { } @Override public void delete(String name) { } } /* 作用: 定义规则，只是跟抽象类不同地方在哪？他是接口不是类。 接口定义好规则之后，实现类负责实现即可。 继承：子类对父类的继承 实现：实现类对接口的实现 继承：手机 extends 照相机 “is a” 的关系，手机是一个照相机 实现：手机 implements 拍照功能 “has-a”的关系，手机具备照相的能力 多态的应用场合: (1)父类当做方法的形参，传入具体的子类的对象 (2)父类当做方法的返回值，返回的是具体的子类的对象 (3)接口当做方法的形参，传入具体的实现类的对象 (4)接口当做方法的返回值，返回的是具体的实现类的对象 接口和抽象类的区别： 1.默认方法：抽象类可以有默认的方法实现，接口中不存在方法的实现，接口是完全抽象的。 2.实现方式：子类使用extends关键字来继承抽象类，如果子类不是抽象类，子类需要提供抽象类中所声明的所有方法的实现。而接口的子类使用implements来实现接口，需要提供接口中声明的所有方法实现。 3.构造函数：抽象类中可以有构造函数，接口中不能。 4.和正常类区别：抽象类不能被实例化，接口则是完全不同的类型。 5.访问修饰符：抽象方法可以有public,protected和default等修饰，接口默认是public，不能使用其他修饰符。 6.多继承：一个子类只能继承在一个抽象类，而一个子类可以实现多个接口。 7.添加新方法：想在抽象类中添加新方法，可以提供默认的实现，因此可以不修改子类现有的代码。如果往接口中添加新方法，则子类中需要实现该方法。 */ 内部类 内部类就是在一个类的内部在定义一个类，比如，A类中定义一个B类，那么B类相对A类来说就称为内部类，而A类相对B类来说就是外部类了。\n1.类的组成:属性，方法，构造器，代码块（普通块，静态块，构造块，同步块），内部类\n2.一个类的内部的类叫内部类，内部类:Inner外部类:Outer\n3.内部类:成员内部类（静态的，非静态的)和﹑局部内部类（位置:方法内，块内，构造器内）\n4.成员内部类: 里面属性，方法，构造器等 修饰符: private，default，protect，public，final, abstract\n5.内部类可以访问外部类的内容\n6.静态内部类中只能访问外部类中被static修饰的内容\n7.外部类想要访问内部类的东西，需要创建内部类的对象然后进行调用\n8.在局部内部类中访问到的变量必须是被 final修饰的\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class Outer { private int id=10; public void out(){ System.out.println(\u0026#34;这是外部类的方法\u0026#34;); } public class Inner{ public void in(){ System.out.println(\u0026#34;这是内部类的方法\u0026#34;); } //获得外部类的私有属性和私有方法 public void getID(){ System.out.println(id); } } } ================================= //测试 public class Application { public static void main(String[] args) { Outer outer = new Outer(); //通过这个外部类来实例化内部类 Outer.Inner inner = outer.new Inner(); inner.in(); inner.getID(); } } 局部内部类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public void method(){ final int num = 10; class A{ public void a(){ System.out.println(num); } } } //2.如果类B在整个项目中只使用一次，那么就没有必要单独创建一个B类，使用内部类就可以了 public Comparable method2(){ class B implements Comparable{ @Override public int compareTo(Object o) { return 100; } } return new B(); } public Comparable method3(){ //匿名内部类 return new Comparable() { @Override public int compareTo(Object o) { return 200; } }; } public void eat(){ Comparable com = new Comparable(){ @Override public int compareTo(Object o) { return 300; } }; System.out.println(com.compareTo(\u0026#34;abc\u0026#34;)); } 项目 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.zy.test1; /** * @Auther: 赵羽 * @Description: com.zy.test1 * @version: 1.0 * 父类：Pissa类 */ public class Pizza { //属性 private String name; private int size; private int price; //方法 public String getName() { return name; } public void setName(String name) { this.name = name; } public int getSize() { return size; } public void setSize(int size) { this.size = size; } public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } //展示披萨信息： public String showPizza(){ return \u0026#34;披萨的名字是：\u0026#34;+name+\u0026#34;\\n披萨的大小是：\u0026#34;+size+\u0026#34;寸\\n披萨的价格是：\u0026#34;+price+\u0026#34;元\u0026#34;; } //构造器 public Pizza() { } public Pizza(String name, int size, int price) { this.name = name; this.size = size; this.price = price; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.zy.test1; /** * @Auther: 赵羽 * @Description: com.zy.test1 * @version: 1.0 */ public class FruitsPizza extends Pizza { //属性 private String burdening; public String getBurdening() { return burdening; } public void setBurdening(String burdening) { this.burdening = burdening; } //构造器 public FruitsPizza() { } public FruitsPizza(String name, int size, int price, String burdening) { super(name, size, price); this.burdening = burdening; } //重写父类showPizza方法： @Override public String showPizza() { return super.showPizza()+\u0026#34;你要加入的水果是：\u0026#34;+burdening; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.zy.test1; import java.util.Scanner; /** * @Auther: 赵羽 * @Description: com.zy.test1 * @version: 1.0 * 工厂类的提取 */ public class PizzaStore { public static Pizza getPizza(int choice){ Scanner sc = new Scanner(System.in); Pizza p = null; switch (choice) { case 1: { System.out.println(\u0026#34;请录入培根的克数：\u0026#34;); int weight = sc.nextInt(); System.out.println(\u0026#34;请录入培根的大小：\u0026#34;); int size = sc.nextInt(); System.out.println(\u0026#34;请录入培根的价格：\u0026#34;); int price = sc.nextInt(); //将录入的信息封装为培根披萨的对象 BaconPizza bp = new BaconPizza(\u0026#34;培根披萨\u0026#34;, size, price, weight); p = bp; } break; case 2: { System.out.println(\u0026#34;请录入你想要加入的水果：\u0026#34;); String burdening = sc.next(); System.out.println(\u0026#34;请录入培根的大小：\u0026#34;); int size = sc.nextInt(); System.out.println(\u0026#34;请录入培根的价格：\u0026#34;); int price = sc.nextInt(); //将录入的信息封装为水果披萨的对象 FruitsPizza fp = new FruitsPizza(\u0026#34;水果披萨\u0026#34;, size, price, burdening); p = fp; } break; } return p; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.zy.test1; /** * @Auther: 赵羽 * @Description: com.zy.test1 * @version: 1.0 */ public class BaconPizza extends Pizza { //属性 private int weight; public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } //构造器 public BaconPizza() { } public BaconPizza(String name, int size, int price, int weight) { super(name, size, price); this.weight = weight; } //重写父类showPizza方法： @Override public String showPizza() { return super.showPizza()+\u0026#34;\\n培根的克数：\u0026#34;+weight+\u0026#34;克\u0026#34;; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.zy.test1; import java.util.Scanner; /** * @Auther: 赵羽 * @Description: com.zy.test1 * @version: 1.0 */ public class Test { public static void main(String[] args) { //选择购买披萨 Scanner sc = new Scanner(System.in); System.out.println(\u0026#34;请选择你想要购买的披萨（1.培根披萨 2.水果披萨）：\u0026#34;); int choice = sc.nextInt(); //通过工厂获取披萨： Pizza pizza = PizzaStore.getPizza(choice); System.out.println(pizza.showPizza()); } } ","permalink":"https://ktzxy.top/posts/4qvv7e4syw/","summary":"Day 06 java面向对象","title":"Day 06 java面向对象"},{"content":"1、Mybatis简介 1.1、MyBatis历史 MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation迁移到了Google Code。随着开发团队转投Google Code旗下，iBatis3.x正式更名为MyBatis。代码于2013年11月迁移到Github。\niBatis一词来源于“internet”和“abatis”的组合，是一个基于Java的持久层框架。iBatis提供的持久层框架包括SQL Maps和Data Access Objects（DAO）\n1.2、MyBatis特性 MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架 MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集 MyBatis可以使用简单的XML或注解用于配置和原始映射，将接口和Java的POJO（Plain Old Java Objects，普通的Java对象）映射成数据库中的记录 MyBatis 是一个 半自动的ORM（Object Relation Mapping）框架 1.3、MyBatis下载 MyBatis下载地址 1.4、和其它持久化层技术对比 JDBC SQL 夹杂在Java代码中耦合度高，导致硬编码内伤 维护不易且实际开发需求中 SQL 有变化，频繁修改的情况多见 代码冗长，开发效率低 Hibernate 和 JPA 操作简便，开发效率高 程序中的长难复杂 SQL 需要绕过框架 内部自动生产的 SQL，不容易做特殊优化 基于全映射的全自动框架，大量字段的 POJO 进行部分映射时比较困难。 反射操作太多，导致数据库性能下降 MyBatis 轻量级，性能出色 SQL 和 Java 编码分开，功能边界清晰。Java代码专注业务、SQL语句专注数据 开发效率稍逊于HIbernate，但是完全能够接受 2、搭建MyBatis 2.1、开发环境 IDE：idea 2019.3\n构建工具：maven 3.5.4\nMySQL版本：MySQL 8.0\nMyBatis版本：MyBatis 3.5.13\nMySQL不同版本的注意事项\n1、驱动类driver-class-name MySQL 5版本使用jdbc5驱动，驱动类使用: com.mysql.jdbc.Driver\nMySQL8版本使用jdbc8驱动，驱动类使用: com.mysql.cj.jdbc.Driver\n2、连接地址url MySQL 5版本的url: jdbc:mysql://localhost:3306/ssm\nMySQL 8版本的url: jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC\n否则运行测试用例报告如下错误: java.sql.SQLException: The server time zone value \u0026lsquo;OD1ité×4E士14a\u0026rsquo; is unrecognized or representsmore\n2.2、创建maven工程 打包方式：jar\n引入依赖\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 \u0026lt;dependencies\u0026gt; \u0026lt;!-- Mybatis核心 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.13\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- junit测试 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.12\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- MySQL驱动 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;8.0.16\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 2.3、创建MyBatis的核心配置文件 习惯上命名为mybatis-config.xml，这个文件名仅仅只是建议，并非强制要求。将来整合Spring之后，这个配置文件可以省略，所以大家操作时可以直接复制、粘贴。 核心配置文件主要用于配置连接数据库的环境以及MyBatis的全局配置信息 核心配置文件存放的位置是src/main/resources目录下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE configuration PUBLIC \u0026#34;-//mybatis.org//DTD Config 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-config.dtd\u0026#34;\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!--设置连接数据库的环境--\u0026gt; \u0026lt;environments default=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;environment id=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;transactionManager type=\u0026#34;JDBC\u0026#34;/\u0026gt; \u0026lt;dataSource type=\u0026#34;POOLED\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driver\u0026#34; value=\u0026#34;com.mysql.cj.jdbc.Driver\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;jdbc:mysql://localhost:3306/数据库名?serverTimezone=UTC\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;root\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;123456\u0026#34;/\u0026gt; \u0026lt;/dataSource\u0026gt; \u0026lt;/environment\u0026gt; \u0026lt;/environments\u0026gt; \u0026lt;!--引入映射文件--\u0026gt; \u0026lt;mappers\u0026gt; \u0026lt;mapper resource=\u0026#34;mappers/UserMapper.xml\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; \u0026lt;/configuration\u0026gt; 2.4、创建mapper接口 MyBatis中的mapper接口相当于以前的dao。但是区别在于，mapper仅仅是接口，我们不需要提供实现类\n1 2 3 4 5 6 7 8 package com.hbnu.mybatis.mapper; public interface UserMapper { /** * 添加用户信息 */ int insertUser(); } 2.5、创建MyBatis的映射文件 相关概念：ORM（Object Relationship Mapping）对象关系映射。 对象：Java的实体类对象 关系：关系型数据库 映射：二者之间的对应关系 Java概念 数据库概念 类 表 属性 字段/列 对象 记录/行 映射文件的命名规则 表所对应的实体类的类名+Mapper.xml 例如：表t_user，映射的实体类为User，所对应的映射文件为UserMapper.xml 因此一个映射文件对应一个实体类，对应一张表的操作 MyBatis映射文件用于编写SQL，访问以及操作表中的数据 MyBatis映射文件存放的位置是src/main/resources/mappers目录下 MyBatis中可以面向接口操作数据，要保证两个一致 mapper接口的全类名和映射文件的命名空间（namespace）保持一致 mapper接口中方法的方法名和映射文件中编写SQL的标签的id属性保持一致 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;https://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;!-- 表示当前配置文件为映射配置文件，其中namespace属性不可省略，MyBatis执行SQL时，是通过namespace + id定位到具体的SQL 多个映射配置文件的namespace属性值不能重复 --\u0026gt; \u0026lt;mapper namespace=\u0026#34;com.hbnu.mybatis.mapper.UserMapper\u0026#34;\u0026gt; \u0026lt;!-- 对于数据库的CRUD操作，都有对应的标签，比如select标签、insert标签、update标签、delete标签 对于查询操作，select标签中有resultType属性，该属性表示查询结果类型，常见类型（Integer、String） 如果查询结果为List集合类型，则resultType属性值为List集合中的泛型类型 mapper接口和映射文件要保证两个一致： 1.mapper接口的全类名和映射文件的namespace一致 2.mapper接口中的方法的方法名要和映射文件中的sql的id保持一致 --\u0026gt; \u0026lt;!--int insertUser();--\u0026gt; \u0026lt;insert id=\u0026#34;insertUser\u0026#34;\u0026gt; insert into tb_user values(null,\u0026#39;admin\u0026#39;,\u0026#39;123456\u0026#39;,22,\u0026#39;男\u0026#39;,\u0026#39;123456@qq.com\u0026#39;) \u0026lt;/insert\u0026gt; \u0026lt;/mapper\u0026gt; 2.6、通过junit测试功能 SqlSession：代表Java程序和数据库之间的会话。（HttpSession是Java程序和浏览器之间的会话） SqlSessionFactory：是“生产”SqlSession的“工厂” 工厂模式：如果创建某一个对象，使用的过程基本固定，那么我们就可以把创建这个对象的相关代码封装到一个“工厂类”中，以后都使用这个工厂类来“生产”我们需要的对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class MybatisTest { @Test public void InsertTest() throws IOException { //获取核心配置文件的输入流 InputStream is = Resources.getResourceAsStream(\u0026#34;mybatis-config.xml\u0026#34;); //获取SqlSessionFactoryBuilder对象 SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder(); //通过核心配置文件所对应的字节输入流创建工厂类SqlSessionFactory，生产SqlSession对象 SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(is); //获取sql的会话对象SqlSession(不会提交事务)，是Mybatis提供的操作数据库的对象 // SqlSession sqlSession = sqlSessionFactory.openSession(); //获取sql的会话对象SqlSession(true)，是Mybatis提供的操作数据库的对象 （会自动提交事务） SqlSession sqlSession = sqlSessionFactory.openSession(true); //UserMapper的代理实现类对象 UserMapper mapper = sqlSession.getMapper(UserMapper.class); //调用UserMapper接口中的方法，就可以根据UserMapper的全类名匹配元素文件，通过调用的方法名匹配映射文件中的SQL标签，并执行标签中的SQL语句 int result = mapper.insertUser(); // int result = sqlSession.insert(\u0026#34;com.hbnu.mybatis.mapper.UserMapper.insertUser\u0026#34;); System.out.println(\u0026#34;结果\u0026#34; + result); //提交事务 // sqlSession.commit(); //关闭sqlSession对象 sqlSession.close(); } } 此时需要手动提交事务，如果要自动提交事务，则在获取sqlSession对象时，使用SqlSession sqlSession = sqlSessionFactory.openSession(true);，传入一个Boolean类型的参数，值为true，这样就可以自动提交 2.7、加入log4j日志功能 加入依赖 1 2 3 4 5 6 \u0026lt;!-- log4j日志 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;log4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;log4j\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.17\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 加入log4j的配置文件 log4j的配置文件名为log4j.xml，存放的位置是src/main/resources目录下 日志的级别：FATAL(致命)\u0026gt;ERROR(错误)\u0026gt;WARN(警告)\u0026gt;INFO(信息)\u0026gt;DEBUG(调试) 从左到右打印的内容越来越详细 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE log4j:configuration SYSTEM \u0026#34;log4j.dtd\u0026#34;\u0026gt; \u0026lt;log4j:configuration xmlns:log4j=\u0026#34;http://jakarta.apache.org/log4j/\u0026#34;\u0026gt; \u0026lt;appender name=\u0026#34;STDOUT\u0026#34; class=\u0026#34;org.apache.log4j.ConsoleAppender\u0026#34;\u0026gt; \u0026lt;param name=\u0026#34;Encoding\u0026#34; value=\u0026#34;UTF-8\u0026#34; /\u0026gt; \u0026lt;layout class=\u0026#34;org.apache.log4j.PatternLayout\u0026#34;\u0026gt; \u0026lt;param name=\u0026#34;ConversionPattern\u0026#34; value=\u0026#34;%-5p %d{MM-dd HH:mm:ss,SSS} %m (%F:%L) \\n\u0026#34; /\u0026gt; \u0026lt;/layout\u0026gt; \u0026lt;/appender\u0026gt; \u0026lt;logger name=\u0026#34;java.sql\u0026#34;\u0026gt; \u0026lt;level value=\u0026#34;debug\u0026#34; /\u0026gt; \u0026lt;/logger\u0026gt; \u0026lt;logger name=\u0026#34;org.apache.ibatis\u0026#34;\u0026gt; \u0026lt;level value=\u0026#34;info\u0026#34; /\u0026gt; \u0026lt;/logger\u0026gt; \u0026lt;root\u0026gt; \u0026lt;level value=\u0026#34;debug\u0026#34; /\u0026gt; \u0026lt;appender-ref ref=\u0026#34;STDOUT\u0026#34; /\u0026gt; \u0026lt;/root\u0026gt; \u0026lt;/log4j:configuration\u0026gt; 3、核心配置文件详解 核心配置文件中的标签必须按照固定的顺序(有的标签可以不写，但顺序一定不能乱)： properties、settings、typeAliases、typeHandlers、objectFactory、objectWrapperFactory、reflectorFactory、plugins、environments、databaseIdProvider、mappers\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE configuration PUBLIC \u0026#34;-//MyBatis.org//DTD Config 3.0//EN\u0026#34; \u0026#34;http://MyBatis.org/dtd/MyBatis-3-config.dtd\u0026#34;\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- MyBatis核心配置文件中的标签必须要按照指定的顺序配置： properties?,settings?,typeAliases?,typeHandlers?, objectFactory?,objectWrapperFactory?,reflectorFactory?, plugins?,environments?,databaseIdProvider?,mappers? --\u0026gt; \u0026lt;!--引入properties文件，此时就可以${属性名}的方式访问属性值--\u0026gt; \u0026lt;properties resource=\u0026#34;jdbc.properties\u0026#34;\u0026gt;\u0026lt;/properties\u0026gt; \u0026lt;settings\u0026gt; \u0026lt;!--将表中字段的下划线自动转换为驼峰--\u0026gt; \u0026lt;setting name=\u0026#34;mapUnderscoreToCamelCase\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;!--开启延迟加载--\u0026gt; \u0026lt;setting name=\u0026#34;lazyLoadingEnabled\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;/settings\u0026gt; \u0026lt;typeAliases\u0026gt; \u0026lt;!-- typeAlias：设置某个具体的类型的别名 在MyBatis的范围中，就可以使用别名表示一个具体的类型 属性： type：需要设置别名的类型的全类名 alias：设置此类型的别名，且别名不区分大小写。若不设置此属性，该类型拥有默认的别名，即类名 --\u0026gt; \u0026lt;!--\u0026lt;typeAlias type=\u0026#34;com.hbnu.mybatis.pojo.User\u0026#34;\u0026gt;\u0026lt;/typeAlias\u0026gt;--\u0026gt; \u0026lt;!--\u0026lt;typeAlias type=\u0026#34;com.hbnu.mybatis.pojo.User\u0026#34; alias=\u0026#34;user\u0026#34;\u0026gt; \u0026lt;/typeAlias\u0026gt;--\u0026gt; \u0026lt;!--以包为单位，设置改包下所有的类型都拥有默认的别名，即类名且不区分大小写--\u0026gt; \u0026lt;package name=\u0026#34;com.hbnu.mybatis.pojo\u0026#34;/\u0026gt; \u0026lt;/typeAliases\u0026gt; \u0026lt;!-- environments：设置多个连接数据库的环境 属性： default：设置默认使用的环境的id --\u0026gt; \u0026lt;environments default=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;!-- environment：设置具体的连接数据库的环境信息 属性： id：设置环境的唯一标识，可通过environments标签中的default设置某一个环境的id，表示默认使用的环境 --\u0026gt; \u0026lt;environment id=\u0026#34;test\u0026#34;\u0026gt; \u0026lt;!-- transactionManager：设置事务管理方式 属性： type：设置事务管理方式，type=\u0026#34;JDBC|MANAGED\u0026#34; type=\u0026#34;JDBC\u0026#34;：设置当前环境的事务管理都必须手动处理 type=\u0026#34;MANAGED\u0026#34;：设置事务被管理，例如spring中的AOP --\u0026gt; \u0026lt;transactionManager type=\u0026#34;JDBC\u0026#34;/\u0026gt; \u0026lt;!-- dataSource：设置数据源 属性： type：设置数据源的类型，type=\u0026#34;POOLED|UNPOOLED|JNDI\u0026#34; type=\u0026#34;POOLED\u0026#34;：使用数据库连接池，即会将创建的连接进行缓存，下次使用可以从缓存中直接获取，不需要重新创建 type=\u0026#34;UNPOOLED\u0026#34;：不使用数据库连接池，即每次使用连接都需要重新创建 type=\u0026#34;JNDI\u0026#34;：调用上下文中的数据源 --\u0026gt; \u0026lt;dataSource type=\u0026#34;POOLED\u0026#34;\u0026gt; \u0026lt;!--设置驱动类的全类名--\u0026gt; \u0026lt;property name=\u0026#34;driver\u0026#34; value=\u0026#34;${jdbc.driver}\u0026#34;/\u0026gt; \u0026lt;!--设置连接数据库的连接地址--\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;${jdbc.url}\u0026#34;/\u0026gt; \u0026lt;!--设置连接数据库的用户名--\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;${jdbc.username}\u0026#34;/\u0026gt; \u0026lt;!--设置连接数据库的密码--\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;${jdbc.password}\u0026#34;/\u0026gt; \u0026lt;/dataSource\u0026gt; \u0026lt;/environment\u0026gt; \u0026lt;/environments\u0026gt; \u0026lt;!--引入映射文件--\u0026gt; \u0026lt;mappers\u0026gt; \u0026lt;!-- \u0026lt;mapper resource=\u0026#34;UserMapper.xml\u0026#34;/\u0026gt; --\u0026gt; \u0026lt;!-- 以包为单位，将包下所有的映射文件引入核心配置文件 注意： 1. 此方式必须保证mapper接口和mapper映射文件必须在相同的包下 2. mapper接口要和mapper映射文件的名字一致 --\u0026gt; \u0026lt;package name=\u0026#34;com.hbnu.mybatis.mapper\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; \u0026lt;/configuration\u0026gt; 4、默认的类型别名 5、MyBatis的增删改查 添加\n1 2 3 4 \u0026lt;!--int insertUser();--\u0026gt; \u0026lt;insert id=\u0026#34;insertUser\u0026#34;\u0026gt; insert into tb_user values(null,\u0026#39;admin\u0026#39;,\u0026#39;123456\u0026#39;,23,\u0026#39;男\u0026#39;,\u0026#39;12345@qq.com\u0026#39;) \u0026lt;/insert\u0026gt; 删除\n1 2 3 4 \u0026lt;!--int deleteUser();--\u0026gt; \u0026lt;delete id=\u0026#34;deleteUser\u0026#34;\u0026gt; delete from tb_user where id = 6 \u0026lt;/delete\u0026gt; 修改\n1 2 3 4 \u0026lt;!--int updateUser();--\u0026gt; \u0026lt;update id=\u0026#34;updateUser\u0026#34;\u0026gt; update tb_user set username = \u0026#39;张三\u0026#39; where id = 5 \u0026lt;/update\u0026gt; 查询一个实体类对象\n1 2 3 4 \u0026lt;!--User getUserById();--\u0026gt; \u0026lt;select id=\u0026#34;getUserById\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; select * from tb_user where id = 2 \u0026lt;/select\u0026gt; 查询集合\n1 2 3 4 \u0026lt;!--List\u0026lt;User\u0026gt; getUserList();--\u0026gt; \u0026lt;select id=\u0026#34;getUserList\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; select * from tb_user \u0026lt;/select\u0026gt; 1 2 3 4 5 6 7 @Test public void GetAllUsersTest() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); List\u0026lt;User\u0026gt; list = mapper.getAllUsers(); list.forEach(System.out::println); } 注意：\n查询的标签select必须设置属性resultType或resultMap，用于设置实体类和数据库表的映射关系 resultType：自动映射，用于属性名和表中字段名一致的情况 resultMap：自定义映射，用于一对多或多对一或字段名和属性名不一致的情况 当查询的数据为多条时，不能使用实体类作为返回值，只能使用集合，否则会抛出异常TooManyResultsException；但是若查询的数据只有一条，可以使用实体类或集合作为返回值 6、MyBatis获取参数值的两种方式（重点） MyBatis获取参数值的两种方式：${}和#{} ${}的本质就是字符串拼接，#{}的本质就是占位符赋值 ${}使用字符串拼接的方式拼接sql，若为字符串类型或日期类型的字段进行赋值时，需要手动加单引号；但是#{}使用占位符赋值的方式拼接sql，此时为字符串类型或日期类型的字段进行赋值时，可以自动添加单引号 6.1、单个字面量类型的参数 若mapper接口中的方法参数为单个的字面量类型，此时可以使用${}和#{}以任意的名称（最好见名识意）获取参数的值，注意${}需要手动加单引号 1 2 3 4 \u0026lt;!--User getUserByUsername(String username);--\u0026gt; \u0026lt;select id=\u0026#34;getUserByUsername\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; select * from tb_user where username = #{username} \u0026lt;/select\u0026gt; 1 2 3 4 \u0026lt;!--User getUserByUsername(String username);--\u0026gt; \u0026lt;select id=\u0026#34;getUserByUsername\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; select * from tb_user where username = \u0026#39;${username}\u0026#39; \u0026lt;/select\u0026gt; 6.2、多个字面量类型的参数 若mapper接口中的方法参数为多个时，此时MyBatis会自动将这些参数放在一个map集合中\n以arg0,arg1\u0026hellip;为键，以参数为值； 以param1,param2\u0026hellip;为键，以参数为值； 因此只需要通过${}和#{}访问map集合的键就可以获取相对应的值，注意${}需要手动加单引号。\n使用arg或者param都行，要注意的是，arg是从arg0开始的，param是从param1开始的\n1 2 3 4 \u0026lt;!--User checkLogin(String username,String password);--\u0026gt; \u0026lt;select id=\u0026#34;checkLogin\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; select * from tb_user where username = #{arg0} and password = #{arg1} \u0026lt;/select\u0026gt; 1 2 3 4 \u0026lt;!--User checkLogin(String username,String password);--\u0026gt; \u0026lt;select id=\u0026#34;checkLogin\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; select * from tb_user where username = \u0026#39;${param1}\u0026#39; and password = \u0026#39;${param2}\u0026#39; \u0026lt;/select\u0026gt; 6.3、map集合类型的参数 若mapper接口中的方法需要的参数为多个时，此时可以手动创建map集合，将这些数据放在map中只需要通过${}和#{}访问map集合的键就可以获取相对应的值，注意${}需要手动加单引号 1 2 3 4 \u0026lt;!--User checkLoginByMap(Map\u0026lt;String,Object\u0026gt; map);--\u0026gt; \u0026lt;select id=\u0026#34;checkLoginByMap\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; select * from tb_user where username = #{username} and password = #{password} \u0026lt;/select\u0026gt; 1 2 3 4 5 6 7 8 9 10 @Test public void checkLoginByMap() { SqlSession sqlSession = SqlSessionUtils.getSqlSession(); ParameterMapper mapper = sqlSession.getMapper(ParameterMapper.class); Map\u0026lt;String,Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;usermane\u0026#34;,\u0026#34;admin\u0026#34;); map.put(\u0026#34;password\u0026#34;,\u0026#34;123456\u0026#34;); User user = mapper.checkLoginByMap(map); System.out.println(user); } 6.4、实体类类型的参数 若mapper接口中的方法参数为实体类对象时此时可以使用${}和#{}，通过访问实体类对象中的属性名获取属性值，注意${}需要手动加单引号 1 2 3 4 \u0026lt;!--void insertUser(User user);--\u0026gt; \u0026lt;insert id=\u0026#34;insertUser\u0026#34;\u0026gt; insert into t_user values(null,#{username},#{password},#{age},#{sex},#{email}) \u0026lt;/insert\u0026gt; 1 2 3 4 5 6 7 @Test public void insertUserTest() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); User user = new User(null,\u0026#34;王五\u0026#34;,\u0026#34;123456\u0026#34;,23,\u0026#34;男\u0026#34;,\u0026#34;145313@qq.com\u0026#34;); mapper.inseretUser(user); } 6.5、使用@Param标识参数 可以通过@Param注解标识mapper接口中的方法参数，此时，会将这些参数放在map集合中\n以@Param注解的value属性值为键，以参数为值； 以param1,param2\u0026hellip;为键，以参数为值； 只需要通过${}和#{}访问map集合的键就可以获取相对应的值，注意${}需要手动加单引号\n1 2 3 4 \u0026lt;!--User CheckLoginByParam(@Param(\u0026#34;username\u0026#34;) String username, @Param(\u0026#34;password\u0026#34;) String password);--\u0026gt; \u0026lt;select id=\u0026#34;CheckLoginByParam\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; select * from t_user where username = #{username} and password = #{password} \u0026lt;/select\u0026gt; 1 2 3 4 5 6 @Test public void checkLoginByParam() { SqlSession sqlSession = SqlSessionUtils.getSqlSession(); ParameterMapper mapper = sqlSession.getMapper(ParameterMapper.class); mapper.CheckLoginByParam(\u0026#34;admin\u0026#34;,\u0026#34;123456\u0026#34;); } 总结 建议分成两种情况进行处理\n实体类类型的参数 使用@Param标识参数 7、MyBatis的各种查询功能 如果查询出的数据只有一条，可以通过 实体类对象接收 List集合接收 Map集合接收，结果{password=123456, sex=男, id=1, age=23, username=admin} 如果查询出的数据有多条，一定不能用实体类对象接收，会抛异常TooManyResultsException，可以通过 实体类类型的LIst集合接收 Map类型的LIst集合接收 在mapper接口的方法上添加@MapKey注解 7.1、查询一个实体类对象 1 2 3 4 5 6 /** * 根据用户id查询用户信息 * @param id * @return */ User getUserById(Integer id); 1 2 3 4 \u0026lt;!--User getUserById(Integer id);--\u0026gt; \u0026lt;select id=\u0026#34;getUserById\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; select * from tb_user where id = #{id} \u0026lt;/select\u0026gt; 7.2、查询一个List集合 1 2 3 4 5 /** * 查询所有用户信息 * @return */ List\u0026lt;User\u0026gt; getAllUsers(); 1 2 3 4 \u0026lt;!--List\u0026lt;User\u0026gt; getAllUsers();--\u0026gt; \u0026lt;select id=\u0026#34;getAllUsers\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; select * from tb_user \u0026lt;/select\u0026gt; 7.3、查询单个数据 1 2 3 4 5 6 7 8 9 /** * 查询用户的总记录数 * @return * 在MyBatis中，对于Java中常用的类型都设置了类型别名 * 例如：java.lang.Integer--\u0026gt;int|integer * 例如：int--\u0026gt;_int|_integer * 例如：Map--\u0026gt;map,List--\u0026gt;list */ int getCount(); 1 2 3 4 \u0026lt;!--int getCount();--\u0026gt; \u0026lt;select id=\u0026#34;getCount\u0026#34; resultType=\u0026#34;_integer\u0026#34;\u0026gt; select count(id) from tb_user \u0026lt;/select\u0026gt; 7.4、查询一条数据为map集合 1 2 3 4 5 6 /** * 根据用户id查询用户信息为map集合 * @param id * @return */ Map\u0026lt;String, Object\u0026gt; getUserToMap(@Param(\u0026#34;id\u0026#34;) int id); 1 2 3 4 5 \u0026lt;!--Map\u0026lt;String, Object\u0026gt; getUserToMap(@Param(\u0026#34;id\u0026#34;) int id);--\u0026gt; \u0026lt;select id=\u0026#34;getUserToMap\u0026#34; resultType=\u0026#34;map\u0026#34;\u0026gt; select * from tb_user where id = #{id} \u0026lt;/select\u0026gt; \u0026lt;!--结果：{password=123456, sex=男, id=1, age=23, username=admin}--\u0026gt; 7.5、查询多条数据为map集合 7.5.1、方法一 1 2 3 4 5 6 /** * 查询所有用户信息为map集合 * @return * 将表中的数据以map集合的方式查询，一条数据对应一个map；若有多条数据，就会产生多个map集合，此时可以将这些map放在一个list集合中获取 */ List\u0026lt;Map\u0026lt;String, Object\u0026gt;\u0026gt; getAllUserToMap(); 1 2 3 4 5 6 7 8 9 10 \u0026lt;!--Map\u0026lt;String, Object\u0026gt; getAllUserToMap();--\u0026gt; \u0026lt;select id=\u0026#34;getAllUserToMap\u0026#34; resultType=\u0026#34;map\u0026#34;\u0026gt; select * from tb_user \u0026lt;/select\u0026gt; \u0026lt;!-- 结果： [{password=123456, sex=男, id=1, age=23, username=admin}, {password=123456, sex=男, id=2, age=23, username=张三}, {password=123456, sex=男, id=3, age=23, username=张三}] --\u0026gt; 7.5.2、方法二 1 2 3 4 5 6 7 /** * 查询所有用户信息为map集合 * @return * 将表中的数据以map集合的方式查询，一条数据对应一个map；若有多条数据，就会产生多个map集合，并且最终要以一个map的方式返回数据，此时需要通过@MapKey注解设置map集合的键，值是每条数据所对应的map集合 */ @MapKey(\u0026#34;id\u0026#34;) Map\u0026lt;String, Object\u0026gt; getAllUserToMap(); 1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;!--Map\u0026lt;String, Object\u0026gt; getAllUserToMap();--\u0026gt; \u0026lt;select id=\u0026#34;getAllUserToMap\u0026#34; resultType=\u0026#34;map\u0026#34;\u0026gt; select * from tb_user \u0026lt;/select\u0026gt; \u0026lt;!-- 结果： { 1={password=123456, sex=男, id=1, age=23, username=admin}, 2={password=123456, sex=男, id=2, age=23, username=张三}, 3={password=123456, sex=男, id=3, age=23, username=张三} } --\u0026gt; 8、特殊SQL的执行 8.1、模糊查询 1 2 3 4 5 6 /** * 根据用户名进行模糊查询 * @param keywords * @return */ List\u0026lt;User\u0026gt; getUserByLike(@Param(\u0026#34;keyword\u0026#34;) String keywords); 1 2 3 4 5 6 \u0026lt;!--List\u0026lt;User\u0026gt; getUserByLike(@Param(\u0026#34;keyword\u0026#34;) String keywords);--\u0026gt; \u0026lt;select id=\u0026#34;getUserByLike\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; \u0026lt;!--select * from tb_user where username like \u0026#39;%${keyword}%\u0026#39;--\u0026gt; \u0026lt;!--select * from tb_user where username like concat(\u0026#39;%\u0026#39;,#{keyword},\u0026#39;%\u0026#39;)--\u0026gt; select * from tb_user where username like \u0026#34;%\u0026#34;#{keyword}\u0026#34;%\u0026#34; \u0026lt;/select\u0026gt; 其中select * from tb_user where username like \u0026quot;%\u0026quot;#{keyword}\u0026quot;%\u0026quot;是最常用的 8.2、批量删除 只能使用${}，如果使用#{}，则解析后的sql语句为delete from t_user where id in ('1,2,3')，这样是将1,2,3看做是一个整体，只有id为1,2,3的数据会被删除。正确的语句应该是delete from t_user where id in (1,2,3)，或者delete from t_user where id in ('1','2','3') 1 2 3 4 5 /** * 批量删除 * @param ids */ void deleteMoreUsers(@Param(\u0026#34;ids\u0026#34;) String ids); 1 2 3 4 \u0026lt;!--void deleteMoreUsers(@Param(\u0026#34;ids\u0026#34;) String ids);--\u0026gt; \u0026lt;select id=\u0026#34;deleteMoreUsers\u0026#34;\u0026gt; delete from tb_user where id in (${ids}) \u0026lt;/select\u0026gt; 1 2 3 4 5 6 @Test public void deleteMoreUsers() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); SpecilSqlMapper mapper = sqlSession.getMapper(SpecilSqlMapper.class); mapper.deleteMoreUsers(\u0026#34;8,9\u0026#34;); } 8.3、动态设置表名 只能使用${}，因为表名不能加单引号 1 2 3 4 5 6 /** * 动态设置表名 * @param tableName * @return */ List\u0026lt;User\u0026gt; getUserList(@Param(\u0026#34;tableName\u0026#34;) String tableName); 1 2 3 4 \u0026lt;!--List\u0026lt;User\u0026gt; getUserList(@Param(\u0026#34;tableName\u0026#34;) String tableName);--\u0026gt; \u0026lt;select id=\u0026#34;getUserList\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; select * from ${tableName} \u0026lt;/select\u0026gt; 8.4、添加功能获取自增的主键 使用场景 t_clazz(clazz_id,clazz_name) t_student(student_id,student_name,clazz_id) 添加班级信息 获取新添加的班级的id 为班级分配学生，即将某学的班级id修改为新添加的班级的id 在mapper.xml中设置两个属性 useGeneratedKeys：设置使用自增的主键 keyProperty：因为增删改有统一的返回值是受影响的行数，因此只能将获取的自增的主键放在传输的参数user对象的某个属性中 1 2 3 4 5 /** * 添加用户信息并获取自增主键 * @param user */ void insertUser(User user); 1 2 3 4 \u0026lt;!--void insertUser(User user);--\u0026gt; \u0026lt;insert id=\u0026#34;insertUser\u0026#34; useGeneratedKeys=\u0026#34;true\u0026#34; keyProperty=\u0026#34;id\u0026#34;\u0026gt; insert into tb_user values (null,#{username},#{password},#{age},#{sex},#{email}) \u0026lt;/insert\u0026gt; 1 2 3 4 5 6 7 8 9 10 //测试类 @Test public void insertUser() { SqlSession sqlSession = SqlSessionUtils.getSqlSession(); SQLMapper mapper = sqlSession.getMapper(SQLMapper.class); User user = new User(null,\u0026#34;xiaoming\u0026#34;,\u0026#34;456123\u0026#34;,23,\u0026#34;男\u0026#34;,\u0026#34;131313@qq.com\u0026#34;); mapper.insertUser(user); System.out.println(user); //输出：User{id=10, username=\u0026#39;xiaoming\u0026#39;, password=\u0026#39;456123\u0026#39;, age=23, gender=\u0026#39;男\u0026#39;, email=\u0026#39;131313@qq.com\u0026#39;}，自增主键存放到了user的id属性中 } 9、自定义映射resultMap 9.1、resultMap处理字段和属性的映射关系 resultMap：设置自定义映射 属性： id：表示自定义映射的唯一标识，不能重复 type：查询的数据要映射的实体类的类型 子标签： id：设置主键的映射关系 result：设置普通字段的映射关系 子标签属性： property：设置映射关系中实体类中的属性名 column：设置映射关系中表中的字段名 若字段名和实体类中的属性名不一致，则可以通过resultMap设置自定义映射，即使字段名和属性名一致的属性也要映射，也就是全部属性都要列出来 1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;resultMap id=\u0026#34;empResultMap\u0026#34; type=\u0026#34;Emp\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;emp_id\u0026#34; property=\u0026#34;empId\u0026#34;\u0026gt;\u0026lt;/id\u0026gt; \u0026lt;result column=\u0026#34;emp_name\u0026#34; property=\u0026#34;empName\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;age\u0026#34; property=\u0026#34;age\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;gender\u0026#34; property=\u0026#34;gender\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;!--Empt getEmpByEmpId(@Param(\u0026#34;empId\u0026#34;) Integer empId);--\u0026gt; \u0026lt;!--注意：resultMap这个不要写成resultType，否则报错--\u0026gt; \u0026lt;select id=\u0026#34;getEmpByEmpId\u0026#34; resultMap=\u0026#34;empResultMap\u0026#34;\u0026gt; select * from tb_emp where emp_id = #{empId} \u0026lt;/select\u0026gt; 若字段名和实体类中的属性名不一致，但是字段名符合数据库的规则（使用_），实体类中的属性名符合Java的规则（使用驼峰）。此时也可通过以下两种方式处理字段名和实体类中的属性的映射关系\n可以通过为字段起别名的方式，保证和实体类中的属性名保持一致 1 2 3 4 5 \u0026lt;!--Empt getEmpByEmpId(@Param(\u0026#34;empId\u0026#34;) Integer empId);--\u0026gt; \u0026lt;select id=\u0026#34;getEmpByEmpId\u0026#34; resultType=\u0026#34;Emp\u0026#34;\u0026gt; select * from tb_emp where emp_id = #{empId} \u0026lt;!--select emp_id empId,emp_name empName,age,gender from tb_emp where emp_id = #{empId}--\u0026gt; \u0026lt;/select\u0026gt; 可以在MyBatis的核心配置文件中的setting标签中，设置一个全局配置信息mapUnderscoreToCamelCase，可以在查询表中数据时，自动将_类型的字段名转换为驼峰，例如：字段名user_name，设置了mapUnderscoreToCamelCase，此时字段名就会转换为userName。 1 2 3 4 \u0026lt;!--将下划线映射为驼峰--\u0026gt; \u0026lt;settings\u0026gt; \u0026lt;setting name=\u0026#34;mapUnderscoreToCamelCase\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;/settings\u0026gt; 9.2、多对一映射处理 查询员工信息以及员工所对应的部门信息\n1 2 3 4 5 6 7 8 9 public class Emp { private Integer eid; private String empName; private Integer age; private String sex; private String email; private Dept dept; //...构造器、get、set方法等 } 9.2.1、级联方式处理映射关系 1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;resultMap id=\u0026#34;empAndDeptResultMap\u0026#34; type=\u0026#34;Emp\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;emp_id\u0026#34; property=\u0026#34;empId\u0026#34;\u0026gt;\u0026lt;/id\u0026gt; \u0026lt;result column=\u0026#34;emp_name\u0026#34; property=\u0026#34;empName\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;age\u0026#34; property=\u0026#34;age\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;gender\u0026#34; property=\u0026#34;gender\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;dept_id\u0026#34; property=\u0026#34;dept.deptId\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;dept_name\u0026#34; property=\u0026#34;dept.deptName\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;!--Emp getEmpAndDeptByEmpId(@Param(\u0026#34;empId\u0026#34;)Integer empId);--\u0026gt; \u0026lt;select id=\u0026#34;getEmpAndDeptByEmpId\u0026#34; resultMap=\u0026#34;empAndDeptResultMap\u0026#34;\u0026gt; select tb_emp.*,dept_name from tb_emp left join tb_dept on tb_emp.dept_id = tb_dept.dept_id where tb_emp.emp_id = #{empId} \u0026lt;/select\u0026gt; 9.2.2、使用association处理映射关系 association：处理多对一的映射关系（处理实体类类型的属性） property：设置需要处理映射关系的属性的属性名 javaType：设置要处理的属性的类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;resultMap id=\u0026#34;empAndDeptResultMap\u0026#34; type=\u0026#34;Emp\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;emp_id\u0026#34; property=\u0026#34;empId\u0026#34;\u0026gt;\u0026lt;/id\u0026gt; \u0026lt;result column=\u0026#34;emp_name\u0026#34; property=\u0026#34;empName\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;age\u0026#34; property=\u0026#34;age\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;gender\u0026#34; property=\u0026#34;gender\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;association property=\u0026#34;dept\u0026#34; javaType=\u0026#34;Dept\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;dept_id\u0026#34; property=\u0026#34;deptId\u0026#34;\u0026gt;\u0026lt;/id\u0026gt; \u0026lt;result column=\u0026#34;dept_name\u0026#34; property=\u0026#34;deptName\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;/association\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;!--Emp getEmpAndDeptByEmpId(@Param(\u0026#34;empId\u0026#34;)Integer empId);--\u0026gt; \u0026lt;select id=\u0026#34;getEmpAndDeptByEmpId\u0026#34; resultMap=\u0026#34;empAndDeptResultMap\u0026#34;\u0026gt; select tb_emp.*,dept_name from tb_emp left join tb_dept on tb_emp.dept_id = tb_dept.dept_id where tb_emp.emp_id = #{empId} \u0026lt;/select\u0026gt; 9.2.3、分步查询 1. 查询员工信息 property：设置需要处理映射关系的属性的属性名\nselect：设置分布查询的sql的唯一标识（namespace.SQLId或mapper接口的全类名.方法名）\ncolumn：将查询出的某个字段作为分步查询的sql条件\n1 2 3 4 5 6 7 //EmpMapper里的方法 /** * 通过分步查询查询员工及所对应的部门信息 第一步 * @param empId * @return */ Emp getEmpAndDeptByStepOne(@Param(\u0026#34;empId\u0026#34;)Integer empId); 1 2 3 4 5 6 7 8 9 10 11 \u0026lt;resultMap id=\u0026#34;empAndDeptByStepResultMap\u0026#34; type=\u0026#34;Emp\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;emp_id\u0026#34; property=\u0026#34;empId\u0026#34;\u0026gt;\u0026lt;/id\u0026gt; \u0026lt;result column=\u0026#34;emp_name\u0026#34; property=\u0026#34;empName\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;age\u0026#34; property=\u0026#34;age\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;gender\u0026#34; property=\u0026#34;gender\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;association property=\u0026#34;dept\u0026#34; select=\u0026#34;com.hbnu.mybatis.mapper.DeptMapper.getEmpAndDeptByStepTwo\u0026#34; column=\u0026#34;dept_id\u0026#34;\u0026gt;\u0026lt;/association\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;!--Emp getEmpAndDeptByStepOne(@Param(\u0026#34;empId\u0026#34;)Integer empId);--\u0026gt; \u0026lt;select id=\u0026#34;getEmpAndDeptByStepOne\u0026#34; resultMap=\u0026#34;empAndDeptByStepResultMap\u0026#34;\u0026gt; select * from tb_emp where emp_id = #{empId} \u0026lt;/select\u0026gt; 2. 查询部门信息 1 2 3 4 5 6 7 //DeptMapper里的方法 /** * 通过分步查询查询员工及所对应的部门信息 第二步 * @param deptId * @return */ Dept getEmpAndDeptByStepTwo(@Param(\u0026#34;deptId\u0026#34;) Integer deptId); 1 2 3 4 \u0026lt;!--Dept getEmpAndDeptByStepTwo(@Param(\u0026#34;deptId\u0026#34;) Integer deptId);--\u0026gt; \u0026lt;select id=\u0026#34;getEmpAndDeptByStepTwo\u0026#34; resultType=\u0026#34;Dept\u0026#34;\u0026gt; select * from tb_dept where dept_id = #{deptId} \u0026lt;/select\u0026gt; 结果为：Emp{empId=1, empName=\u0026lsquo;张三\u0026rsquo;, age=20, gender=\u0026lsquo;男\u0026rsquo;, dept=null}\n则表示：mybatis-config.xml中没有添加下列代码或者可以使用resultMap来解决部门Dept表中字段名和类中属性名不一致问题\n1 2 3 \u0026lt;settings\u0026gt; \u0026lt;setting name=\u0026#34;mapUnderscoreToCamelCase\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;/settings\u0026gt; 9.3、一对多映射处理 1 2 3 4 5 6 public class Dept { private Integer deptId; private String deptName; private List\u0026lt;Emp\u0026gt; emps; //...构造器、get、set方法等 } 9.3.1、collection collection：用来处理一对多的映射关系 ofType：设置集合类型的属性中存储的数据的类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;resultMap id=\u0026#34;empAndDeptResultMap\u0026#34; type=\u0026#34;Dept\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;dept_id\u0026#34; property=\u0026#34;deptId\u0026#34;\u0026gt;\u0026lt;/id\u0026gt; \u0026lt;result column=\u0026#34;dept_name\u0026#34; property=\u0026#34;deptName\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;collection property=\u0026#34;emps\u0026#34; ofType=\u0026#34;Emp\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;emp_id\u0026#34; property=\u0026#34;empId\u0026#34;\u0026gt;\u0026lt;/id\u0026gt; \u0026lt;result column=\u0026#34;emp_name\u0026#34; property=\u0026#34;empName\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;age\u0026#34; property=\u0026#34;age\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;gender\u0026#34; property=\u0026#34;gender\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;/collection\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;!--Dept getEmpAndDeptByDeptId(@Param(\u0026#34;deptId\u0026#34;) Integer deptId);--\u0026gt; \u0026lt;select id=\u0026#34;getEmpAndDeptByDeptId\u0026#34; resultMap=\u0026#34;empAndDeptResultMap\u0026#34;\u0026gt; select tb_emp.*,tb_dept.dept_name from tb_emp right join tb_dept on tb_emp.dept_id = tb_dept.dept_id where tb_dept.dept_id = #{deptId} \u0026lt;/select\u0026gt; 9.3.2、分步查询 1. 查询部门信息 1 2 3 4 5 6 7 //DeptMapper /** * 通过分步查询查询部门及部门中的员工信息的第一步 * @param deptId * @return */ Dept getDeptAndEmpByStepOne(@Param(\u0026#34;deptId\u0026#34;) Integer deptId); 1 2 3 4 5 6 7 8 9 10 11 \u0026lt;resultMap id=\u0026#34;DeptAndEmpByStepOneResultMap\u0026#34; type=\u0026#34;Dept\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;dept_id\u0026#34; property=\u0026#34;deptId\u0026#34;\u0026gt;\u0026lt;/id\u0026gt; \u0026lt;result column=\u0026#34;dept_name\u0026#34; property=\u0026#34;deptName\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;collection property=\u0026#34;emps\u0026#34; select=\u0026#34;com.hbnu.mybatis.mapper.EmpMapper.getDeptAndEmpByStepTwo\u0026#34; column=\u0026#34;dept_id\u0026#34;\u0026gt;\u0026lt;/collection\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;!--Dept getDeptAndEmpByStepOne(@Param(\u0026#34;deptId\u0026#34;) Integer deptId);--\u0026gt; \u0026lt;select id=\u0026#34;getDeptAndEmpByStepOne\u0026#34; resultMap=\u0026#34;DeptAndEmpByStepOneResultMap\u0026#34;\u0026gt; select * from tb_dept where dept_id = #{deptId} \u0026lt;/select\u0026gt; 2. 根据部门id查询部门中的所有员工 1 2 3 4 5 6 /** * 通过分步查询查询部门及部门中的员工信息的第二步 * @param deptID * @return */ List\u0026lt;Emp\u0026gt; getDeptAndEmpByStepTwo(@Param(\u0026#34;deptID\u0026#34;)Integer deptID); 1 2 3 4 \u0026lt;!--List\u0026lt;Emp\u0026gt; getDeptAndEmpByStepTwo(@Param(\u0026#34;deptID\u0026#34;)Integer deptID);--\u0026gt; \u0026lt;select id=\u0026#34;getDeptAndEmpByStepTwo\u0026#34; resultType=\u0026#34;Emp\u0026#34;\u0026gt; select * from tb_emp where dept_id = #{deptId} \u0026lt;/select\u0026gt; 9.4、多对多映射处理 数据库中的多对多关系往往可以看做两个一对多的关系，在mybatis中可以通过嵌套实现多对多的关系，collection嵌套association或者 association嵌套collection 使用\n9.5、延迟加载 分步查询的优点：可以实现延迟加载，但是必须在核心配置文件中设置全局配置信息： lazyLoadingEnabled：延迟加载的全局开关。当开启时，所有关联对象都会延迟加载 aggressiveLazyLoading：当开启时，任何方法的调用都会加载该对象的所有属性。 否则，每个属性会按需加载 此时就可以实现按需加载，获取的数据是什么，就只会执行相应的sql。此时可通过association和collection中的fetchType属性设置当前的分步查询是否使用延迟加载，fetchType=\u0026ldquo;lazy(延迟加载)|eager(立即加载)\u0026rdquo; 1 2 3 4 5 6 7 \u0026lt;settings\u0026gt; \u0026lt;setting name=\u0026#34;mapUnderscoreToCamelCase\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;!--开启延迟加载--\u0026gt; \u0026lt;setting name=\u0026#34;lazyLoadingEnabled\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;!--按需加载--\u0026gt; \u0026lt;setting name=\u0026#34;aggressiveLazyLoading\u0026#34; value=\u0026#34;false\u0026#34;/\u0026gt; \u0026lt;/settings\u0026gt; 1 2 3 4 5 6 7 @Test public void getEmpAndDeptByStep() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); Emp emp = mapper.getEmpAndDeptByStepOne(1); System.out.println(emp); } 关闭延迟加载\n开启延迟加载，只运行获取emp的SQL语句 fetchType：当开启了全局的延迟加载之后，可以通过该属性手动控制延迟加载的效果，fetchType=\u0026ldquo;lazy(延迟加载)|eager(立即加载)\u0026rdquo;\n1 2 3 4 5 6 7 8 9 10 \u0026lt;resultMap id=\u0026#34;empAndDeptByStepResultMap\u0026#34; type=\u0026#34;Emp\u0026#34;\u0026gt; \u0026lt;id column=\u0026#34;emp_id\u0026#34; property=\u0026#34;empId\u0026#34;\u0026gt;\u0026lt;/id\u0026gt; \u0026lt;result column=\u0026#34;emp_name\u0026#34; property=\u0026#34;empName\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;age\u0026#34; property=\u0026#34;age\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;result column=\u0026#34;gender\u0026#34; property=\u0026#34;gender\u0026#34;\u0026gt;\u0026lt;/result\u0026gt; \u0026lt;association property=\u0026#34;dept\u0026#34; select=\u0026#34;com.hbnu.mybatis.mapper.DeptMapper.getEmpAndDeptByStepTwo\u0026#34; column=\u0026#34;dept_id\u0026#34; fetchType=\u0026#34;lazy\u0026#34;\u0026gt;\u0026lt;/association\u0026gt; \u0026lt;/resultMap\u0026gt; 10、动态SQL Mybatis框架的动态SQL技术是一种根据特定条件动态拼装SQL语句的功能，它存在的意义是为了解决拼接SQL语句字符串时的痛点问题 10.1、if if标签可通过test属性（即传递过来的数据）的表达式进行判断，若表达式的结果为true，则标签中的内容会执行；反之标签中的内容不会执行 在where后面添加一个恒成立条件1=1 这个恒成立条件并不会影响查询的结果 这个1=1可以用来拼接and语句，例如：当empName为null时 如果不加上恒成立条件，则SQL语句为select * from tb_emp where and age = ? and sex = ? and email = ?，此时where会与and连用，SQL语句会报错 如果加上一个恒成立条件，则SQL语句为select * from t_emp where 1= 1 and age = ? and sex = ? and email = ?，此时不报错 1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;!--List\u0026lt;Emp\u0026gt; getEmpByCondition(Emp emp);--\u0026gt; \u0026lt;select id=\u0026#34;getEmpByConditionOne\u0026#34; resultType=\u0026#34;Emp\u0026#34;\u0026gt; select * from tb_emp where 1 = 1 \u0026lt;if test=\u0026#34;empName != null and empName != \u0026#39;\u0026#39; \u0026#34;\u0026gt; emp_name = #{empName} \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;age != null and age != \u0026#39;\u0026#39; \u0026#34;\u0026gt; and age = #{age} \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;gender != null and gender != \u0026#39;\u0026#39; \u0026#34;\u0026gt; and gender = #{gender} \u0026lt;/if\u0026gt; \u0026lt;/select\u0026gt; 1 2 3 4 5 6 7 8 9 @Test public void testDynamicSQLMapper() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class); Emp emp = new Emp(null,\u0026#34;\u0026#34;,20,\u0026#34;男\u0026#34;); List\u0026lt;Emp\u0026gt; list = mapper.getEmpByCondition(emp); list.forEach(System.out::println); } 10.2、where where和if一般结合使用： 若where标签中的if条件都不满足，则where标签没有任何功能，即不会添加where关键字 若where标签中的if条件满足，则where标签会自动添加where关键字，并将条件最前方多余的and/or去掉 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;!--List\u0026lt;Emp\u0026gt; getEmpByCondition(Emp emp);--\u0026gt; \u0026lt;select id=\u0026#34;getEmpByConditionOne\u0026#34; resultType=\u0026#34;Emp\u0026#34;\u0026gt; select * from tb_emp \u0026lt;where\u0026gt; \u0026lt;if test=\u0026#34;empName != null and empName != \u0026#39;\u0026#39; \u0026#34;\u0026gt; emp_name = #{empName} \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;age != null and age != \u0026#39;\u0026#39; \u0026#34;\u0026gt; and age = #{age} \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;gender != null and gender != \u0026#39;\u0026#39; \u0026#34;\u0026gt; and gender = #{gender} \u0026lt;/if\u0026gt; \u0026lt;/where\u0026gt; \u0026lt;/select\u0026gt; 注意：where标签不能去掉条件后多余的and/or\n1 2 3 4 5 6 7 \u0026lt;!--这种用法是错误的，只能去掉条件前面的and/or，条件后面的不行--\u0026gt; \u0026lt;if test=\u0026#34;empName != null and empName !=\u0026#39;\u0026#39;\u0026#34;\u0026gt; emp_name = #{empName} and \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;age != null and age !=\u0026#39;\u0026#39;\u0026#34;\u0026gt; age = #{age} \u0026lt;/if\u0026gt; 10.3、trim trim用于去掉或添加标签中的内容 常用属性 prefix：在trim标签中的内容的前面添加某些内容 suffix：在trim标签中的内容的后面添加某些内容 prefixOverrides：在trim标签中的内容的前面去掉某些内容 suffixOverrides：在trim标签中的内容的后面去掉某些内容 若trim中的标签都不满足条件，则trim标签没有任何效果，也就是只剩下select * from t_emp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;!--List\u0026lt;Emp\u0026gt; getEmpByCondition(Emp emp);--\u0026gt; \u0026lt;select id=\u0026#34;getEmpByCondition\u0026#34; resultType=\u0026#34;Emp\u0026#34;\u0026gt; select * from tb_emp \u0026lt;trim prefix=\u0026#34;where\u0026#34; suffixOverrides=\u0026#34;and | or\u0026#34;\u0026gt; \u0026lt;if test=\u0026#34;empName != null and empName != \u0026#39;\u0026#39; \u0026#34;\u0026gt; emp_name = #{empName} and \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;age != null and age != \u0026#39;\u0026#39; \u0026#34;\u0026gt; age = #{age} and \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;gender != null and gender != \u0026#39;\u0026#39; \u0026#34;\u0026gt; gender = #{gender} \u0026lt;/if\u0026gt; \u0026lt;/trim\u0026gt; \u0026lt;/select\u0026gt; 1 2 3 4 5 6 7 8 9 10 //测试类 @Test public void testDynamicSQLMapper() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class); Emp emp = new Emp(null,\u0026#34;张三\u0026#34;,20,\u0026#34;\u0026#34;); List\u0026lt;Emp\u0026gt; list = mapper.getEmpByCondition(emp); list.forEach(System.out::println); } 10.4、choose、when、otherwise choose、when、otherwise相当于if...else if..else when至少要有一个，otherwise至多只有一个 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;!--List\u0026lt;Emp\u0026gt; getEmpByChoose(Emp emp);--\u0026gt; \u0026lt;select id=\u0026#34;getEmpByChoose\u0026#34; resultType=\u0026#34;Emp\u0026#34;\u0026gt; select * from tb_emp \u0026lt;where\u0026gt; \u0026lt;choose\u0026gt; \u0026lt;when test=\u0026#34;empName != null and empName != \u0026#39;\u0026#39; \u0026#34;\u0026gt; emp_name = #{empName} \u0026lt;/when\u0026gt; \u0026lt;when test=\u0026#34;age != null and age != \u0026#39;\u0026#39; \u0026#34;\u0026gt; age = #{age} \u0026lt;/when\u0026gt; \u0026lt;when test=\u0026#34;gender != null and gender != \u0026#39;\u0026#39; \u0026#34;\u0026gt; gender = #{gender} \u0026lt;/when\u0026gt; \u0026lt;otherwise\u0026gt; emp_id = 1 \u0026lt;/otherwise\u0026gt; \u0026lt;/choose\u0026gt; \u0026lt;/where\u0026gt; \u0026lt;/select\u0026gt; 1 2 3 4 5 6 7 8 9 @Test public void testDynamicSQLMapperByChoose() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class); Emp emp = new Emp(null,\u0026#34;张三\u0026#34;,30,\u0026#34;男\u0026#34;); List\u0026lt;Emp\u0026gt; list = mapper.getEmpByChoose(emp); list.forEach(System.out::println); } 相当于if a else if b else if c else d，只会执行其中一个 10.5、foreach 属性：\ncollection：设置要循环的数组或集合 item：表示集合或数组中的每一个数据 separator：设置循环体之间的分隔符，分隔符前后默认有一个空格，如, open：设置foreach标签中的内容的开始符 close：设置foreach标签中的内容的结束符 批量删除\n1 2 3 4 5 6 7 8 \u0026lt;!--void deleteMoreEmp(@Param(\u0026#34;empIds\u0026#34;) Integer[] empIds);--\u0026gt; \u0026lt;delete id=\u0026#34;deleteMoreEmp\u0026#34;\u0026gt; delete from tb_emp where emp_id in ( \u0026lt;foreach collection=\u0026#34;empIds\u0026#34; item=\u0026#34;empId\u0026#34; separator=\u0026#34;,\u0026#34;\u0026gt; (#{empId}) \u0026lt;/foreach\u0026gt; ) \u0026lt;/delete\u0026gt; 1 2 3 4 5 6 7 \u0026lt;!--void deleteMoreEmp(@Param(\u0026#34;empIds\u0026#34;) Integer[] empIds);--\u0026gt; \u0026lt;delete id=\u0026#34;deleteMoreEmp\u0026#34;\u0026gt; delete from tb_emp where emp_id in \u0026lt;foreach collection=\u0026#34;empIds\u0026#34; item=\u0026#34;empId\u0026#34; separator=\u0026#34;,\u0026#34; open=\u0026#34;(\u0026#34; close=\u0026#34;)\u0026#34;\u0026gt; (#{empId}) \u0026lt;/foreach\u0026gt; \u0026lt;/delete\u0026gt; 1 2 3 4 5 6 7 @Test public void testDeleteMoreByList() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class); Integer[] emps = new Integer[]{8,9}; mapper.deleteMoreEmp(emps); } 批量添加\n1 2 3 4 5 6 7 \u0026lt;!--void insertMoreEmp(@Param(\u0026#34;emps\u0026#34;) List\u0026lt;Emp\u0026gt; emps)--\u0026gt; \u0026lt;insert id=\u0026#34;insertMoreEmp\u0026#34;\u0026gt; insert into tb_emp values \u0026lt;foreach collection=\u0026#34;emps\u0026#34; item=\u0026#34;emp\u0026#34; separator=\u0026#34;,\u0026#34;\u0026gt; (null,#{emp.empName},#{emp.age},#{emp.gender},null) \u0026lt;/foreach\u0026gt; \u0026lt;/insert\u0026gt; 1 2 3 4 5 6 7 8 9 10 @Test public void testInsertMoreByList() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); DynamicSQLMapper mapper = sqlSession.getMapper(DynamicSQLMapper.class); Emp emp1 = new Emp(null,\u0026#34;张三1\u0026#34;,30,\u0026#34;男\u0026#34;); Emp emp2 = new Emp(null,\u0026#34;张三2\u0026#34;,30,\u0026#34;男\u0026#34;); Emp emp3 = new Emp(null,\u0026#34;张三3\u0026#34;,30,\u0026#34;男\u0026#34;); List\u0026lt;Emp\u0026gt; emps = Arrays.asList(emp1, emp2, emp3); mapper.insertMoreEmp(emps); } 10.6、SQL片段 sql片段，可以记录一段公共sql片段，在使用的地方通过include标签进行引入 声明sql片段：\u0026lt;sql\u0026gt;标签 1 \u0026lt;sql id=\u0026#34;empColumns\u0026#34;\u0026gt;emp_id,emp_name,age,gender\u0026lt;/sql\u0026gt; 引用sql片段：\u0026lt;include\u0026gt;标签 1 2 3 4 \u0026lt;!--List\u0026lt;Emp\u0026gt; getEmpByCondition(Emp emp);--\u0026gt; \u0026lt;select id=\u0026#34;getEmpByCondition\u0026#34; resultType=\u0026#34;Emp\u0026#34;\u0026gt; select \u0026lt;include refid=\u0026#34;empColumns\u0026#34;\u0026gt;\u0026lt;/include\u0026gt; from tb_emp \u0026lt;/select\u0026gt; 11、MyBatis的缓存 11.1、MyBatis的一级缓存 一级缓存是SqlSession级别的，通过同一个SqlSession查询的数据会被缓存，下次查询相同的数据，就会从缓存中直接获取，不会从数据库重新访问\n使一级缓存失效的四种情况：\n不同的SqlSession对应不同的一级缓存 同一个SqlSession但是查询条件不同 同一个SqlSession两次查询期间执行了任何一次增删改操作 同一个SqlSession两次查询期间手动清空了缓存 11.2、MyBatis的二级缓存 二级缓存是SqlSessionFactory级别，通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存；此后若再次执行相同的查询语句，结果就会从缓存中获取\n二级缓存开启的条件\n在核心配置文件中，设置全局配置属性cacheEnabled=\u0026ldquo;true\u0026rdquo;，默认为true，不需要设置 在映射文件中设置标签 二级缓存必须在SqlSession关闭或提交之后有效 查询的数据所转换的实体类类型必须实现序列化的接口 使二级缓存失效的情况：两次查询之间执行了任意的增删改，会使一级和二级缓存同时失效\n11.3、二级缓存的相关配置 在mapper配置文件中添加的cache标签可以设置一些属性 eviction属性：缓存回收策略 LRU（Least Recently Used） – 最近最少使用的：移除最长时间不被使用的对象。 FIFO（First in First out） – 先进先出：按对象进入缓存的顺序来移除它们。 SOFT – 软引用：移除基于垃圾回收器状态和软引用规则的对象。 WEAK – 弱引用：更积极地移除基于垃圾收集器状态和弱引用规则的对象。 默认的是 LRU flushInterval属性：刷新间隔，单位毫秒 默认情况是不设置，也就是没有刷新间隔，缓存仅仅调用语句（增删改）时刷新 size属性：引用数目，正整数 代表缓存最多可以存储多少个对象，太大容易导致内存溢出 readOnly属性：只读，true/false true：只读缓存；会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。 false：读写缓存；会返回缓存对象的拷贝（通过序列化）。这会慢一些，但是安全，因此默认是false 11.4、MyBatis缓存查询的顺序 先查询二级缓存，因为二级缓存中可能会有其他程序已经查出来的数据，可以拿来直接使用 如果二级缓存没有命中，再查询一级缓存 如果一级缓存也没有命中，则查询数据库 SqlSession关闭之后，一级缓存中的数据会写入二级缓存 11.5、整合第三方缓存EHCache（了解） 11.5.1、添加依赖 1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;!-- Mybatis EHCache整合包 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis.caches\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-ehcache\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- slf4j日志门面的一个具体实现 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;ch.qos.logback\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;logback-classic\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 11.5.2、各个jar包的功能 jar包名称 作用 mybatis-ehcache Mybatis和EHCache的整合包 ehcache EHCache核心包 slf4j-api SLF4J日志门面包 logback-classic 支持SLF4J门面接口的一个具体实现 11.5.3、创建EHCache的配置文件ehcache.xml 名字必须叫ehcache.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34; ?\u0026gt; \u0026lt;ehcache xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:noNamespaceSchemaLocation=\u0026#34;../config/ehcache.xsd\u0026#34;\u0026gt; \u0026lt;!-- 磁盘保存路径 --\u0026gt; \u0026lt;diskStore path=\u0026#34;D:\\atguigu\\ehcache\u0026#34;/\u0026gt; \u0026lt;defaultCache maxElementsInMemory=\u0026#34;1000\u0026#34; maxElementsOnDisk=\u0026#34;10000000\u0026#34; eternal=\u0026#34;false\u0026#34; overflowToDisk=\u0026#34;true\u0026#34; timeToIdleSeconds=\u0026#34;120\u0026#34; timeToLiveSeconds=\u0026#34;120\u0026#34; diskExpiryThreadIntervalSeconds=\u0026#34;120\u0026#34; memoryStoreEvictionPolicy=\u0026#34;LRU\u0026#34;\u0026gt; \u0026lt;/defaultCache\u0026gt; \u0026lt;/ehcache\u0026gt; 11.5.4、设置二级缓存的类型 在xxxMapper.xml文件中设置二级缓存类型 1 \u0026lt;cache type=\u0026#34;org.mybatis.caches.ehcache.EhcacheCache\u0026#34;/\u0026gt; 11.5.5、加入logback日志 存在SLF4J时，作为简易日志的log4j将失效，此时我们需要借助SLF4J的具体实现logback来打印日志。创建logback的配置文件logback.xml，名字固定，不可改变 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;configuration debug=\u0026#34;true\u0026#34;\u0026gt; \u0026lt;!-- 指定日志输出的位置 --\u0026gt; \u0026lt;appender name=\u0026#34;STDOUT\u0026#34; class=\u0026#34;ch.qos.logback.core.ConsoleAppender\u0026#34;\u0026gt; \u0026lt;encoder\u0026gt; \u0026lt;!-- 日志输出的格式 --\u0026gt; \u0026lt;!-- 按照顺序分别是：时间、日志级别、线程名称、打印日志的类、日志主体内容、换行 --\u0026gt; \u0026lt;pattern\u0026gt;[%d{HH:mm:ss.SSS}] [%-5level] [%thread] [%logger] [%msg]%n\u0026lt;/pattern\u0026gt; \u0026lt;/encoder\u0026gt; \u0026lt;/appender\u0026gt; \u0026lt;!-- 设置全局日志级别。日志级别按顺序分别是：DEBUG、INFO、WARN、ERROR --\u0026gt; \u0026lt;!-- 指定任何一个日志级别都只打印当前级别和后面级别的日志。 --\u0026gt; \u0026lt;root level=\u0026#34;DEBUG\u0026#34;\u0026gt; \u0026lt;!-- 指定打印日志的appender，这里通过“STDOUT”引用了前面配置的appender --\u0026gt; \u0026lt;appender-ref ref=\u0026#34;STDOUT\u0026#34; /\u0026gt; \u0026lt;/root\u0026gt; \u0026lt;!-- 根据特殊需求指定局部日志级别 --\u0026gt; \u0026lt;logger name=\u0026#34;com.hbnu.mybatis.mapper\u0026#34; level=\u0026#34;DEBUG\u0026#34;/\u0026gt; \u0026lt;/configuration\u0026gt; 11.5.6、EHCache配置文件说明 属性名 是否必须 作用 maxElementsInMemory 是 在内存中缓存的element的最大数目 maxElementsOnDisk 是 在磁盘上缓存的element的最大数目，若是0表示无穷大 eternal 是 设定缓存的elements是否永远不过期。 如果为true，则缓存的数据始终有效， 如果为false那么还要根据timeToIdleSeconds、timeToLiveSeconds判断 overflowToDisk 是 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上 timeToIdleSeconds 否 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时， 这些数据便会删除，默认值是0,也就是可闲置时间无穷大 timeToLiveSeconds 否 缓存element的有效生命期，默认是0.,也就是element存活时间无穷大 diskSpoolBufferSizeMB 否 DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区 diskPersistent 否 在VM重启的时候是否启用磁盘保存EhCache中的数据，默认是false diskExpiryThreadIntervalSeconds 否 磁盘缓存的清理线程运行间隔，默认是120秒。每个120s， 相应的线程会进行一次EhCache中数据的清理工作 memoryStoreEvictionPolicy 否 当内存缓存达到最大，有新的element加入的时候， 移除缓存中element的策略。 默认是LRU（最近最少使用），可选的有LFU（最不常使用）和FIFO（先进先出 12、MyBatis的逆向工程 正向工程：先创建Java实体类，由框架负责根据实体类生成数据库表。Hibernate是支持正向工程的 逆向工程：先创建数据库表，由框架负责根据数据库表，反向生成如下资源： Java实体类 Mapper接口 Mapper映射文件 12.1、创建逆向工程的步骤 12.1.1、添加依赖和插件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 \u0026lt;dependencies\u0026gt; \u0026lt;!-- MyBatis核心依赖包 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.9\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- junit测试 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.13.2\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- MySQL驱动 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;8.0.27\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- log4j日志 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;log4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;log4j\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.17\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;!-- 控制Maven在构建过程中相关配置 --\u0026gt; \u0026lt;build\u0026gt; \u0026lt;!-- 构建过程中用到的插件 --\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;!-- 具体插件，逆向工程的操作是以构建过程中插件形式出现的 --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis.generator\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-generator-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.3.0\u0026lt;/version\u0026gt; \u0026lt;!-- 插件的依赖 --\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- 逆向工程的核心依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis.generator\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-generator-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.3.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 数据库连接池 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.mchange\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;c3p0\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;0.9.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- MySQL驱动 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;8.0.27\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; 12.1.2、创建MyBatis的核心配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE configuration PUBLIC \u0026#34;-//mybatis.org//DTD Config 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-config.dtd\u0026#34;\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;properties resource=\u0026#34;jdbc.properties\u0026#34;/\u0026gt; \u0026lt;typeAliases\u0026gt; \u0026lt;package name=\u0026#34;\u0026#34;/\u0026gt; \u0026lt;/typeAliases\u0026gt; \u0026lt;environments default=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;environment id=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;transactionManager type=\u0026#34;JDBC\u0026#34;/\u0026gt; \u0026lt;dataSource type=\u0026#34;POOLED\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driver\u0026#34; value=\u0026#34;${jdbc.driver}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;${jdbc.url}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;${jdbc.username}\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;${jdbc.password}\u0026#34;/\u0026gt; \u0026lt;/dataSource\u0026gt; \u0026lt;/environment\u0026gt; \u0026lt;/environments\u0026gt; \u0026lt;mappers\u0026gt; \u0026lt;package name=\u0026#34;\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; \u0026lt;/configuration\u0026gt; 12.1.3、创建逆向工程的配置文件 文件名必须是：generatorConfig.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;!DOCTYPE generatorConfiguration PUBLIC \u0026#34;-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd\u0026#34;\u0026gt; \u0026lt;generatorConfiguration\u0026gt; \u0026lt;!-- targetRuntime: 执行生成的逆向工程的版本 MyBatis3Simple: 生成基本的CRUD（清新简洁版） MyBatis3: 生成带条件的CRUD（奢华尊享版） --\u0026gt; \u0026lt;context id=\u0026#34;DB2Tables\u0026#34; targetRuntime=\u0026#34;MyBatis3Simple\u0026#34;\u0026gt; \u0026lt;!-- 数据库的连接信息 --\u0026gt; \u0026lt;jdbcConnection driverClass=\u0026#34;com.mysql.cj.jdbc.Driver\u0026#34; connectionURL=\u0026#34;jdbc:mysql://localhost:3306/mybatis\u0026#34; userId=\u0026#34;root\u0026#34; password=\u0026#34;123456\u0026#34;\u0026gt; \u0026lt;/jdbcConnection\u0026gt; \u0026lt;!-- javaBean的生成策略--\u0026gt; \u0026lt;javaModelGenerator targetPackage=\u0026#34;com.atguigu.mybatis.pojo\u0026#34; targetProject=\u0026#34;.\\src\\main\\java\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;enableSubPackages\u0026#34; value=\u0026#34;true\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;trimStrings\u0026#34; value=\u0026#34;true\u0026#34; /\u0026gt; \u0026lt;/javaModelGenerator\u0026gt; \u0026lt;!-- SQL映射文件的生成策略 --\u0026gt; \u0026lt;sqlMapGenerator targetPackage=\u0026#34;com.atguigu.mybatis.mapper\u0026#34; targetProject=\u0026#34;.\\src\\main\\resources\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;enableSubPackages\u0026#34; value=\u0026#34;true\u0026#34; /\u0026gt; \u0026lt;/sqlMapGenerator\u0026gt; \u0026lt;!-- Mapper接口的生成策略 --\u0026gt; \u0026lt;javaClientGenerator type=\u0026#34;XMLMAPPER\u0026#34; targetPackage=\u0026#34;com.atguigu.mybatis.mapper\u0026#34; targetProject=\u0026#34;.\\src\\main\\java\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;enableSubPackages\u0026#34; value=\u0026#34;true\u0026#34; /\u0026gt; \u0026lt;/javaClientGenerator\u0026gt; \u0026lt;!-- 逆向分析的表 --\u0026gt; \u0026lt;!-- tableName设置为*号，可以对应所有表，此时不写domainObjectName --\u0026gt; \u0026lt;!-- domainObjectName属性指定生成出来的实体类的类名 --\u0026gt; \u0026lt;table tableName=\u0026#34;t_emp\u0026#34; domainObjectName=\u0026#34;Emp\u0026#34;/\u0026gt; \u0026lt;table tableName=\u0026#34;t_dept\u0026#34; domainObjectName=\u0026#34;Dept\u0026#34;/\u0026gt; \u0026lt;/context\u0026gt; \u0026lt;/generatorConfiguration\u0026gt; 12.1.4、执行MBG插件的generate目标 如果出现报错：Exception getting JDBC Driver，可能是pom.xml中，数据库驱动配置错误\n执行结果\n12.2、QBC 查询 selectByExample：按条件查询，需要传入一个example对象或者null；如果传入一个null，则表示没有条件，也就是查询所有数据 example.createCriteria().xxx：创建条件对象，通过andXXX方法为SQL添加查询添加，每个条件之间是and关系 example.or().xxx：将之前添加的条件通过or拼接其他条件 1 2 3 4 5 6 7 8 @Test public void testMBG() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); //根据id查询数据 Emp emp = mapper.selectByPrimaryKey(1); System.out.println(emp); } 1 2 3 4 5 6 7 8 @Test public void testMBG() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); //查询所有数据 List\u0026lt;Emp\u0026gt; emps = mapper.selectByExample(null); emps.forEach(System.out::println); } 1 2 3 4 5 6 7 8 9 10 11 @Test public void testMBG() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); //根据员工姓名查询数据 EmpExample empExample = new EmpExample(); empExample.createCriteria().andEmpNameEqualTo(\u0026#34;李四\u0026#34;).andAgeGreaterThanOrEqualTo(20); empExample.or().andGenderEqualTo(\u0026#34;男\u0026#34;); List\u0026lt;Emp\u0026gt; list = mapper.selectByExample(empExample); list.forEach(System.out::println); } 增改 updateByPrimaryKey：通过主键进行数据修改，如果某一个值为null，也会将对应的字段改为null\n1 2 3 4 5 6 7 8 @Test public void testMBG() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); //测试普通修改功能 Emp emp = new Emp(1,\u0026#34;小红\u0026#34;,null,\u0026#34;女\u0026#34;); mapper.updateByPrimaryKey(emp); } updateByPrimaryKeySelective()：通过主键进行选择性数据修改，如果某个值为null，则不修改这个字段\n1 2 3 4 5 6 7 8 @Test public void testMBG() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); //选择性修改 Emp emp = new Emp(1,\u0026#34;小黑\u0026#34;,null,\u0026#34;女\u0026#34;); mapper.updateByPrimaryKey(emp); } 13、分页插件 limit index,pageSize\npageSize：每页显示的条数\npageNum：当前页的页码\nindex：当前页的起始索引，index = (pageNum - 1) * pageSize\ncount：总记录数\ntotalPage：总页数\ntotalPage = count/pageSize\nif(count % pageSize != 0){\n​\ttotalPage += 1;\n}\npageSize = 4, pageNum = 1, index = 0 limit 0,4\npageSize = 4, pageNum =3, index = 8 limit 8,4\npageSize = 4, pageNum = 6, index = 20 limit 8,4\n13.1、分页插件使用步骤 添加依赖 1 2 3 4 5 6 \u0026lt;!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.github.pagehelper\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;pagehelper\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.2.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 配置分页插件 在MyBatis的核心配置文件（mybatis-config.xml）中配置插件 1 2 3 4 \u0026lt;plugins\u0026gt; \u0026lt;!--设置分页插件--\u0026gt; \u0026lt;plugin interceptor=\u0026#34;com.github.pagehelper.PageInterceptor\u0026#34;\u0026gt;\u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; 13.2、分页插件的使用 13.2.1、开启分页功能 在查询功能之前使用PageHelper.startPage(int pageNum, int pageSize)开启分页功能 pageNum：当前页的页码 pageSize：每页显示的条数 1 2 3 4 5 6 7 8 9 @Test public void testPage() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); //查询功能之前开启分页功能 PageHelper.startPage(1,4); List\u0026lt;Emp\u0026gt; list = mapper.selectByExample(null); list.forEach(System.out::println); } 分页相关数据 方法一：直接输出 1 2 3 4 5 6 7 8 9 10 @Test public void testPage() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); Page\u0026lt;Object\u0026gt; page = PageHelper.startPage(1, 4); List\u0026lt;Emp\u0026gt; emps = mapper.selectByExample(null); //在查询到List集合后，打印分页数据 System.out.println(page); } 分页相关数据：\n1 Page{count=true, pageNum=1, pageSize=4, startRow=0, endRow=4, total=32, pages=8, reasonable=false, pageSizeZero=false}[Emp{empId=1, empName=\u0026#39;小黑\u0026#39;, age=20, gender=\u0026#39;女\u0026#39;, deptId=1}, Emp{empId=2, empName=\u0026#39;李四\u0026#39;, age=21, gender=\u0026#39;男\u0026#39;, deptId=2}, Emp{empId=3, empName=\u0026#39;王五\u0026#39;, age=24, gender=\u0026#39;男\u0026#39;, deptId=3}, Emp{empId=4, empName=\u0026#39;赵六\u0026#39;, age=20, gender=\u0026#39;男\u0026#39;, deptId=2}] 方法二使用PageInfo 在查询获取list集合之后，使用PageInfo\u0026lt;T\u0026gt; pageInfo = new PageInfo\u0026lt;\u0026gt;(List\u0026lt;T\u0026gt; list, intnavigatePages)获取分页相关数据 list：分页之后的数据 navigatePages：导航分页的页码数 1 2 3 4 5 6 7 8 9 10 11 @Test public void testPage() { SqlSession sqlSession = SqlSessionUtil.getSqlSession(); EmpMapper mapper = sqlSession.getMapper(EmpMapper.class); //查询功能之前开启分页功能 PageHelper.startPage(1,4); List\u0026lt;Emp\u0026gt; list = mapper.selectByExample(null); //查询功能之后可以获取分页相关的所有数据 PageInfo\u0026lt;Emp\u0026gt; page = new PageInfo\u0026lt;\u0026gt;(list,5); System.out.println(page); } 分页相关数据：\n1 PageInfo{pageNum=1, pageSize=4, size=4, startRow=1, endRow=4, total=32, pages=8, list=Page{count=true, pageNum=1, pageSize=4, startRow=0, endRow=4, total=32, pages=8, reasonable=false, pageSizeZero=false}[Emp{empId=1, empName=\u0026#39;小黑\u0026#39;, age=20, gender=\u0026#39;女\u0026#39;, deptId=1}, Emp{empId=2, empName=\u0026#39;李四\u0026#39;, age=21, gender=\u0026#39;男\u0026#39;, deptId=2}, Emp{empId=3, empName=\u0026#39;王五\u0026#39;, age=24, gender=\u0026#39;男\u0026#39;, deptId=3}, Emp{empId=4, empName=\u0026#39;赵六\u0026#39;, age=20, gender=\u0026#39;男\u0026#39;, deptId=2}], prePage=0, nextPage=2, isFirstPage=true, isLastPage=false, hasPreviousPage=false, hasNextPage=true, navigatePages=5, navigateFirstPage=1, navigateLastPage=5, navigatepageNums=[1, 2, 3, 4, 5]} 其中list中的数据等同于方法一中直接输出的page数据\n常用数据： pageNum：当前页的页码 pageSize：每页显示的条数 size：当前页显示的真实条数 total：总记录数 pages：总页数 prePage：上一页的页码 nextPage：下一页的页码 isFirstPage/isLastPage：是否为第一页/最后一页 hasPreviousPage/hasNextPage：是否存在上一页/下一页 navigatePages：导航分页的页码数 navigatepageNums：导航分页的页码，[1,2,3,4,5] ","permalink":"https://ktzxy.top/posts/4pj0c5483r/","summary":"MyBatis笔记","title":"MyBatis笔记"},{"content":"TCP中的流量控制和拥塞控制 流量控制 什么是流量控制 如果发送者发送数据过快，接收者来不及接收，那么就会出现分组丢失，为了避免分组丢失，控制发送者的发送速度，使得接收者来得及接收，这就是流量控制。\n流量控制的目的是：防止分组丢失，是构成TCP可靠性的一方面。\n如何实现流量控制 由滑动窗口协议（连续ARQ协议）实现，滑动窗口协议即保证了分组无差错，有序接收，也实现了流量控制。主要的方式就是接收方返回的ACK会包含自己的接受窗口大小，并利用大小来控制发送方的数据发送。\n拥塞控制 什么是拥塞控制 拥塞控制是作用于网络的，它是防止过多的数据注入网络，避免出现网络负载过大的情况，常见的方法就是\n慢开始，避免拥塞 快重传、快恢复 拥塞控制算法 我们首先添加几个限定条件\n数据是单方向传递，另一个窗口只发送确认 接收方的缓存足够大，因此发送方的大小由网络的拥塞程度来决定 慢开始算法 发送方维持一个叫做拥塞窗口cwnd（congestion window）的状态变量，拥塞窗口的大小取决于网络的拥塞程度，并且动态地在变化，发送方让自己的发送窗口等于拥塞窗口，另外考虑到接收方的接受能力，发送窗口可能小于拥塞窗口。\n慢开始算法的思路就是：不要一开始就发送大量的数据，先测探一下网络的拥塞程度，也就是说从小到大主键增加拥塞窗口的大小。\n这里用报文段的个数作为拥塞窗口的大小举例说明慢开始算法，实际的拥塞窗口大小是以字节为单位的。如下图所示：\n发送方没收到一个确认窗口，就把窗口cwnd加1\n从上图可以看到，一个传输轮次所经历的时间其实就是往返时间RTT，而且每经过一个传输轮次，拥塞窗口cwnd就加倍\n为了防止cwnd增长过大引起网络拥塞，还需设置一个慢开始门限ssthresh状态变量，ssthresh的用法如下：\n当 cwnd \u0026lt; ssthresh时：使用慢开始算法 当cwnd = ssthresh时：采用 慢开始或拥塞避免中的任意一种 当 cwnd \u0026gt; ssthresh时：采用拥塞避免算法 拥塞避免算法 拥塞避免算法让拥塞窗口缓慢增长，即没经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1，而不是加倍，这样能够让拥塞窗口按线性规律增长。\n无论是在慢开始阶段，还是在拥塞控制阶段，只要发送方判断网络出现拥塞，就把慢开始门限 ssthressh设置为当前出现拥塞时发送窗口大小的一半（不能小于2），然后将拥塞窗口cwnd设置为1，执行慢开始算法。\n这样做的目的是迅速减少主机发送到网络中的分组数，使得发送拥塞的路由器有足够时间把队列中积压的分组处理完毕。\n整个拥塞控制的流程图如下图所示：\n拥塞窗口cwnd初始化为1个报文段，慢开始门限初始值为16 执行慢开始算法，指数规律增长到第4轮，即cwnd=16=ssthresh，改为执行拥塞避免算法，拥塞窗口按线性规律增长 假定cwnd=24时，网络出现超时（拥塞），则更新后的ssthresh=12，cwnd重新设置为1，并执行慢开始算法。当cwnd=12=ssthresh时，改为执行拥塞避免算法 乘法减小和加法增大\n乘法减小”指的是无论是在慢开始阶段还是在拥塞避免阶段，只要发送方判断网络出现拥塞，就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半，并执行慢开始算法，所以当网络频繁出现拥塞时，ssthresh下降的很快，以大大减少注入到网络中的分组数。 加法增大”是指执行拥塞避免算法后，使拥塞窗口缓慢增大，以防止过早出现拥塞。常合起来成为AIMD算法。 快重传算法 快重传要求接收方在收到一个失序的报文段后，就立即发出重复确定（为的是使发送方及早知道有报文段没有达到对方，可提高网络吞吐量约20%）而不要等到自己发送数据时捎带确定。快重传算法规定，发送方只要一连收到三个重复确定就应当立即重传对方尚为收到的报文段，而不必继续等待设置的重传计时器时间到期，如下所示\n快恢复 快重传配合使用的还有快恢复算法，有以下两点要求\n当发送方连续收到三个重复确认时，就执行乘法减小算法，把ssthresh门限减半（为了预防发送拥塞），但是接下来并不执行慢开始算法 考虑到如果网络出现拥塞的话，就不会收到好几个重复的确认，所以发送方现在认为网络可能没有出现拥塞，所以此时不执行慢开始算法，而是将cwnd设置为ssthresh减半后的值，然后执行拥塞避免算法，使cwnd缓慢增大，如下图所示：TCP Reno版本是目前使用最广泛的版本。 在采用快恢复算法时，慢开始算法只是在TCP连接建立时和网络出现超时时才使用\n来源 https://zhuanlan.zhihu.com/p/37379780\n","permalink":"https://ktzxy.top/posts/50o5sxm5mx/","summary":"TCP中的拥塞控制和流量控制","title":"TCP中的拥塞控制和流量控制"},{"content":"﻿# 1. MySQL 的版本\n1.1. 版本分类 MySQL 服务端提供了以下版本：\nMySQL Community Server：社区版本，免费，但是MySQL不提供官方技术支持。 MySQL Enterprise Edition：商业版，该版本是收费版本，可以试用30天，官方提供技术支持 MySQL Cluster：集群版，开源免费，可将几个MySQL Server封装成一个Server。 MySQL Cluster CGE：高级集群版，需付费。 MySQL 的图形化客户端 MySQL Workbench（GUI TOOL）：一款专为MySQL设计的ER/数据库建模工具。又分为两个版本：\n社区版（MySQL Workbench OSS） 商用版（MySQL Workbench SE） 1.2. 版本号 MySQL的命名机制使用由3个数字和一个后缀组成的版本号。例如，mysql-8.0.26的版本号的含义如下：\n第1个数字(8)是主版本号，描述了文件格式。所有版本5的发行都有相同的文件格式。 第2个数字(0)是发行级别。主版本号和发行级别组合到一起便构成了发行序列号。 第3个数字(26)是在此发行系列的版本号，随每个新分发版递增。 1.3. 查询当前 MySQL 的版本 使用 MySQL 客户端登陆后，输入 select version(); 命令，即可查询当前 mysql 的版本号\n1 2 3 4 5 6 7 mysql\u0026gt; select version(); +-----------+ | version() | +-----------+ | 8.0.30 | +-----------+ 1 row in set (0.02 sec) 2.常见数据库及其概念 Oracle（神谕）：甲骨文 DB2：IBM SQL Server：微软 Sybase：塞尔斯 MySQL：甲骨文 数据库系统：DBS\n数据库系统DBS (DataBase System,简称DBS)通常由软件、数据库和数据管理员组成。其软件主要包括操作系统、各种宿主语言、实用程序以及数据库管理系统。\n数据库由数据库管理系统统-管理，数据的插入、修改和检索均要通过数据库管理系统进行。\n数据管理员负责创建、监控和维护整个数据库,使数据能被任何有权使用的人有效使用。数据库管理员一般是由业务水平较高、资历较深的人员担任。\n关系型数据库: SQL和DBMS\n●结构化查询语言（Structured Query Language，SQL）允许用户在高层数据结构上工作，用于存放数据以及查询、更新和管理关系型数据库系统。\n●由于SQL语言结构简洁、功能强大、简单易学，因此是大型数据库系统的标准语言。\n●DBMS( DataBase Management System)是一种创建和管理数据库的系统软件,能够给用户和程序员提供一种系统地创建、回收、更新、管理数据的方式,本质上就是一种服务于数据库和终端用户或者应用程序的接口。\n非关系型数据库: NoSQL\n●非关系型数据库常常用于超大规模数据的存储,因为这些大规模的数据没有固定的模式，因此可以相对容易地进行横向扩展。\n●在云数据库中，NoSQL所具有的容易拓展、结构简单的特点使得大规模分布式开发变得更加方便,因此成为云数据库的宠儿。\n分布式数据库/内存数据库\n●分布式数据库(Distributed DataBase)通常使用多个存储节点构建一个完整的、全局的逻辑上集中、物理上分布的大型数据库，每个节点都有其独立的数据库或全局数据库的部分副本。\n●分布式数据库管理系统是一种管理分布式数据库系统的应用，它能够周期性地同步数据,从而保证不同的用户能够访问同样的数据，以及对于数据的操作能够同步到分布式数据库系统的其他部分中。\n●内存数据库(Main Memory DataBasee)就是将数据库放在内存中直接操作的数据库。采用内存数据库主要有两个方面的原因:\n➢内存比磁盘读写速度更快，能够极大提高数据库性能\n➢内存数据库抛弃了磁盘数据管理的传统方式，基于全部数据在内存中重新设计了体系结构\n3.数据完整性 作用：保证用户输入的数据保存到数据库中是正确的。\n确保数据的完整性 = 在创建表时给表中添加约束\n完整性的分类：\n实体完整性: 域完整性: 引用完整性: 3.1 实体完整性 实体：即表中的一行(一条记录)代表一个实体（entity）\n实体完整性的作用：标识每一行数据不重复。\n约束类型：\n主键约束（primary key）\n唯一约束(unique)\n自动增长列(auto_increment)\n3.2 域完整性 域完整性的作用：限制此单元格的数据正确，不对照此列的其它单元格比较\n域代表当前单元格\n约束类型：\n数据类型\n非空约束（not null）\n默认值约束(default)\n3.3 引用完整性（参照完整性） 外键约束：FOREIGN KEY\n4.SQL语句分类： DDL（Data Definition Language）：数据定义语言，用来定义数据库对象：库、表、列等；创建、删除、修改：库、表结构。\nDML（Data Manipulation Language）：数据操作语言，用来定义数据库记录（数据）：增添，删除，修改:表记录。\nDCL（Data Control Language）：数据控制语言，用来定义访问权限和安全级别；\nDQL（Data Query Language）：数据查询语言，用来查询记录（数据）。\nTCL (Transaction Control Language) : 事务控制语言\n5.数据类型 1. 数值类型 类型 大小(byte) 说明 TINYINT 1 很小的整数型，默认长度4 SMALLINT 2 小的整型，默认长度6 MEDIUMINT 3 中等大小的整数，默认长度9 INT或INTEGER 4 普通大小的整数（占4字节），默认长度11 BIGINT 8 占用的8个字节，默认长度20 FLOAT(m,d) 4 单精度浮点型小数 DOUBLE(m,d) 8 双精度浮点型小数 d代表小数位数，m代表总位数 (整数位=m-d);比如：DOUBLE(5.2)， 数值共5位，其中小数为2位。 DECIMAL(m,d) 压缩严格的定点数，取值范围与double相同，但有效取值范围由M(精度)与D(标度)决定 Tips: 当字段用记录年龄时，因为不会存在负数与数值的范围不会太大，可以设置为 age tinyint unsigned\n1.1. 整型类型的长度设置 MySQL 中 int(10) 中的 10 表示的是显示数据的长度，不足 10 位以 0 填充。也就是说，int(1) 和 int(10) 所能存储的数字大小以及占用的空间都是相同的。\n关联知识：字符串类型 char(10) 的 10 表示的是存储数据的长度。\n1.2. Decimal 类型和 Float、Double 等区别 MySQL 中存在 float, double 等非标准数据类型，可以存浮点数（即小数类型），但是 float 有个坏处，当给定的数据是整数的时候，那么它就以整数处理。这样在存取货币值的时候自然遇到问题，default 值为 0.00 而实际存储是 0，同样存取货币为 12.00，实际存储是 12。\n为了解决上面的问题，MySQL 提供了1种标准数据类型：decimal。decimal 类型被 MySQL 以同样的类型实现，这在 SQL92 标准中是允许的。它们用于保存对准确精度有重要要求的值，例如与金钱有关的数据。\n其中 Decimal 类型和 Float、Double 主要区别在于：float，double 等非标准类型，在 DB 中保存的是近似值；而 Decimal 则以字符串的形式保存数值。\n2.字符串类型 类型 大小(byte) 说明 CHAR(M) 0-255 CHAR(x)，定长的字符串。性能较好 VARCHAR(M) 0-65535 可变长的字符串，注意数据不能超过X位数性能较差 TINYBLOB 0-255 不超过 255 个字符的二进制字符串 BLOB 0-65535 二进制形式的长文本数据。（图片、视频、音频） MEDIUMBLOB 0-16M 二进制形式的中等长度文本数据 LONGBLOB 0-4G 二进制形式的长文本数据 TINYTEXT 0-255 短文本字符串 TEXT 0-65535 长文本数据 MEDIUMTEXT 0-16M 中等长度文本数据 LONGTEXT 0-4G 极大文本数据 VARBINARY(M) 允许长度0~M个字节的变长字节字符串 BINARY(M) 允许长度0~M个字节的定长字节字符串 Notes: 其中 TINYTEXT、TEXT、MEDIUMTEXT、LONGTEXT 是非标准字符串类型\n2.1. char 和 vachar 的区别 char 与 varchar 都可以描述字符串，两者主要有以下区别：\nchar 最大长度是 255 字符；varchar 最大长度是 65535 个字节。 char 是定长字符串，指定长度多长，就占用多少个字符，和字段值的长度无关，不足的部分用隐藏空格填充；varchar 是变长字符串，指定的长度为最大占用长度。 相对而言，char 会浪费空间；varchar 会更加节省空间。 对于查找效率而言，char 的查找效率会更高些；varchar 查找效率会更低。因此 varchar 需要计算内容占用的长度，而 char 不需要，所以char 的效率稍高一些。 总结：char 的性能会更高些。当存储的字符串长度是固定时，优先选择 char 类型。\n2.2字符中的分隔字符处理 char、varchar 和 text 等字符串类型都可以存储路径，但使用 \\ 会被过滤，所以路径中用 / 或 \\\\ 来代替，MySQL 就不会自动过滤路径的分隔字符，完整的表示路径。\n2.3. BLOB 和 TEXT 的区别 一般情况下，数据库中不直接存储图片和音频文件，而是存储图片与文件的路径。如果存储文件，则选择 blob 类型。\nBLOB 类型是一个二进制对象，可以容纳可变数量的数据； TEXT 类型是一个不区分大小写的 BLOB 类型。 两种类型之间的主要的区别是：BLOB 值进行排序和比较时区分大小写；对 TEXT 值不区分大小写。\n3.日期时间类型 类型 大小(byte) 范围 格式 说明 YEAR 1 1901~2155 YYYY 年份值 TIME 3 -838:59:59~838:59:59 HH:MM:SS 时间值或持续时间 DATE 3 1000-01-01~9999-12-31 YYYY-MM-DD 日期值(只有年月日，没有时分秒) DATETIME 8 1000-01-01 00:00:00~ 9999-12-31 23:59:59 YYYY-MM-DD HH:MM:SS 混合日期和时间值 TIMESTAMP 4 19700101 00:00:01 UTC~2038-01-19 03:14:07UTC YYYYMMDD HHMMSS 混合日期和时间值，时间戳 Notes: 尽量使用 timestamp，空间效率高于 datetime，用整数保存时间戳通常不方便处理。若需要存储微秒，可以使用 bigint 存储。其中 DATETIME 类型与时区无关；TIMESTAMP 显示依赖于所指定得时区，默认在第一个列行的数据修改时可以自动得修改\n4.字段类型优先级选择 优先考虑数字类型，其次是日期或者二进制类型，最后是字符串类型，同级别得数据类型，应该优先选择占用空间小的数据类型\n1 整形 \u0026gt; date,time \u0026gt; enum,char \u0026gt; varchar \u0026gt; blob,text 5. 关于 Null 类型的特别说明 MySQL 对 null 值的处理，有以下三种：\nNULL 值代表一个未确定的值，每个 null 都是独一无二。MySQL 认为任何和 NULL 值做比较的表达式的值都为 NULL，包括 select null = null; 和 select null != null; 1 2 3 4 5 6 7 8 9 10 11 12 13 mysql\u0026gt; select null = null; +-------------+ | null = null | +-------------+ | NULL | +-------------+ mysql\u0026gt; select null != null; +--------------+ | null != null | +--------------+ | NULL | +--------------+ NULL 值在业务上就是代表没有，所有的 NULL 值和起来算一份 NULL 完全没有意义，所以在统计数量不会将其算进去 假设一个表中某个列 c1 的记录为(2, 1000, null, null)，在第一种情况下，表中 c1 的记录数为4，第二种表中 c1 的记录数为3，第三种表中 c1 的记录数为2。\nMySQL 专门提供了一个 innodb_stats_method 的系统变量，专门针对统计索引列不重复值的数量时如何对待 NULL 值。此系统变量有三个候选值：\nnulls_equal：认为所有 NULL 值都是相等的。这个值也是 innodb_stats_method 的默认值。如果某个索引列中 NULL 值特别多的话，这种统计方式会让优化器认为某个列中平均一个值重复次数特别多，所以倾向于不使用索引进行访问。 nulls_unequal：认为所有 NULL 值都是不相等的。如果某个索引列中 NULL 值特别多的话，这种统计方式会让优化器认为某个列中平均一个值重复次数特别少，所以倾向于使用索引进行访问。 nulls_ignored：直接把 NULL 值忽略掉。 详见官网：https://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_stats_method\n有迹象表明，在 MySQL 5.7.22 以后的版本，对这个innodb_stats_method的修改不起作用，MySQL 把这个值在代码里写死为nulls_equal。也就是说 MySQL在进行索引列的数据统计行为又把 null 视为第二种情况（NULL 值在业务上就是代表没有，所有的 NULL 值和起来算一份），MySQL 对 Null 值的处理比较飘忽。所以总的来说，对于列的声明尽可能的不要允许为null。\n6.枚举和集合 1 2 3 4 5 6 7 8 9 10 11 12 13 14 a.枚举(enum) enum(val1, val2, val3...) 在已知的值中进行单选。最大数量为65535. 枚举值在保存时，以2个字节的整型(smallint)保存。每个枚举值，按保存的位置顺序，从1开始逐一递增。 表现为字符串类型，存储却是整型。 NULL值的索引是NULL。 空字符串错误值的索引值是0。 b.集合（set） set(val1, val2, val3...) create table tab ( gender set(\u0026#39;男\u0026#39;, \u0026#39;女\u0026#39;, \u0026#39;无\u0026#39;) ); insert into tab values (\u0026#39;男, 女\u0026#39;); 最多可以有64个不同的成员。以bigint存储，共8个字节。采取位运算的形式。 当创建表时，SET成员值的尾部空格将自动被删除。 6.SQL语句 6.1 连接到服务器并断开与服务器的连接 1 2 3 4 5 6 7 8 9 10 1.连接到服务器 mysql -h 地址 -P 端口 -u 用户名 -p 密码 2.断开服务器 quit 3.启动MySQL net start mysql 4.关闭MySQL net stop mysql 5.跳过权限验证登录MySQL mysqld --skip-grant-tables 6.2 DDL:数据定义语言 库操作： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1.查看当前数据库 SELECT DATABASE(); 2.显示当前时间、用户名、数据库版本 SELECT now(), user(), version(); 3.创建库 CREATE DATABASE[ IF NOT EXISTS] 数据库名 数据库选项 数据库选项： CHARACTER SET charset_name COLLATE collation_name 4.查看已有库 SHOW DATABASES; 5.查看当前库信息 SHOW CREATE DATABASE 数据库名 6.修改库的选项信息 ALTER DATABASE 库名 选项信息 7.删除库 DROP DATABASE[ IF EXISTS] 数据库名 同时删除该数据库相关的目录及其目录内容 表操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 1.创建表 CREATE [TEMPORARY] TABLE[ IF NOT EXISTS] [库名.]表名 ( 表的结构定义 )[ 表选项] 每个字段必须有数据类型 最后一个字段后不能有逗号 TEMPORARY 临时表，会话结束时表自动消失 对于字段的定义： 字段名 数据类型 [NOT NULL | NULL] 非空 |空 [DEFAULT default_value] 默认值 [AUTO_INCREMENT] 自动增长 [UNIQUE [KEY] | [PRIMARY] KEY] 唯一键| 主键 [COMMENT \u0026#39;string\u0026#39;] 表注释 -- 表选项 -- 字符集 CHARSET = charset_name 如果表没有设定，则使用数据库字符集 -- 存储引擎 ENGINE = engine_name 表在管理数据时采用的不同的数据结构，结构不同会导致处理方式、提供的特性操作等不同 常见的引擎：InnoDB MyISAM Memory/Heap BDB Merge Example CSV MaxDB Archive 不同的引擎在保存表的结构和数据时采用不同的方式 MyISAM表文件含义：.frm表定义，.MYD表数据，.MYI表索引 InnoDB表文件含义：.frm表定义，表空间数据和日志文件 SHOW ENGINES -- 显示存储引擎的状态信息 SHOW ENGINE 引擎名 {LOGS|STATUS} -- 显示存储引擎的日志或状态信息 -- 自增起始数 AUTO_INCREMENT = 行数 -- 数据文件目录 DATA DIRECTORY = \u0026#39;目录\u0026#39; -- 索引文件目录 INDEX DIRECTORY = \u0026#39;目录\u0026#39; -- 表注释 COMMENT = \u0026#39;string\u0026#39; -- 分区选项 PARTITION BY ... (详细见手册) 2.查看所有表 SHOW TABLES FROM 表名 3.查看表结构 SHOW CREATE TABLE 表名 （信息更详细） DESC 表名 / DESCRIBE 表名 / EXPLAIN 表名 / SHOW COLUMNS FROM 表名 4.修改表 a.修改表本身的选项 ALTER TABLE 表名 表的选项 eg: ALTER TABLE 表名 ENGINE=MYISAM; b.对表进行重命名 RENAME TABLE 原表名 TO 新表名 RENAME TABLE 原表名 TO 库名.表名 （可将表移动到另一个数据库） c.修改表的字段机构（13.1.2. ALTER TABLE语法） ALTER TABLE 表名 操作名 -- 操作名 ADD[ COLUMN] 字段定义 -- 增加字段 AFTER 字段名 -- 表示增加在该字段名后面 FIRST -- 表示增加在第一个 ADD PRIMARY KEY(字段名) -- 创建主键 ADD UNIQUE [索引名](字段名)-- 创建唯一索引 ADD INDEX [索引名](字段名) -- 创建普通索引 DROP[ COLUMN] 字段名 -- 删除字段 MODIFY[ COLUMN] 字段名 字段属性 -- 支持对字段属性进行修改，不能修改字段名(所有原有属性也需写上) CHANGE[ COLUMN] 原字段名 新字段名 字段属性 -- 支持对字段名修改 DROP PRIMARY KEY -- 删除主键(删除主键前需删除其AUTO_INCREMENT属性) DROP INDEX 索引名 -- 删除索引 DROP FOREIGN KEY 外键 -- 删除外键 5.删除表 DROP TABLE[ IF EXISTS] 表名 6.清空表数据 TRUNCATE [TABLE] 表名 7.复制表结构 CREATE TABLE 表名 LIKE 要复制的表名 8.复制表结构和数据 CREATE TABLE 表名 [AS] SELECT * FROM 要复制的表名 6.3 DML:数据操作语言 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 1.增添数据 a.INSERT [INTO] 表名 [(字段列表)] VALUES (值列表)[, (值列表), ...] -- 如果要插入的值列表包含所有字段并且顺序一致，则可以省略字段列表。 -- 可同时插入多条数据记录！ REPLACE 与 INSERT 完全一样，可互换。 b.INSERT [INTO] 表名 SET 字段名=值[, 字段名=值, ...] 2.查询数据 SELECT 字段列表 FROM 表名[ 其他子句] -- 可来自多个表的多个字段 -- 字段列表可以用*代替，表示所有字段 3.删除数据 DELETE FROM 表名[WHERE ...] [ORDER BY ...] [LIMIT ...] 没有条件子句，则会删除全部 按照条件删除。where 指定删除的最多记录数。limit 可以通过排序条件删除。order by + limit TRUNCATE TABLE 表名：TRUNCATE是DDL语句，它是先删除drop该表，再create该表，而且无法回滚。 区别： 1，truncate 是删除表再创建，delete 是逐条删除 2，truncate 重置auto_increment的值。而delete不会 3，truncate 不知道删除了几条，而delete知道。 4，当被用于带分区的表时，truncate 会保留分区 4.修改数据 UPDATE 表名 SET 字段名=新值[, 字段名=新值] [更新条件] 6.4 DCL:数据控制语言 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 1.root密码重置 a. 停止MySQL服务 b. [Linux] /usr/local/mysql/bin/safe_mysqld --skip-grant-tables \u0026amp; [Windows] mysqld --skip-grant-tables c. use mysql; d. UPDATE `user` SET PASSWORD=PASSWORD(\u0026#34;密码\u0026#34;) WHERE `user` = \u0026#34;root\u0026#34;; e. FLUSH PRIVILEGES; 用户信息表：mysql.user 2.刷新权限 FLUSH PRIVILEGES; 3.创建用户 *一个项目创建一个用户！一个项目对应的数据库只有一个！ *这个用户只能对这个数据库有权限，其它数据库就操作不了了！ CREATE USER 用户名@IP地址 IDENTIFIED BY [PASSWORD] 密码(字符串) - 必须拥有mysql数据库的全局CREATE USER权限，或拥有INSERT权限。 - 只能创建用户，不能赋予权限。 - 用户名，注意引号：如 \u0026#39;user_name\u0026#39;@\u0026#39;192.168.1.1\u0026#39; - 密码也需引号，纯数字密码也要加引号 - 要在纯文本中指定密码，需忽略PASSWORD关键词。要把密码指定为由PASSWORD()函数返回的混编值，需包含关键字PASSWORD 4.重命名用户 RENAME USER old_user TO new_user 5.设置密码 SET PASSWORD = PASSWORD(\u0026#39;密码\u0026#39;) -- 为当前用户设置密码 SET PASSWORD FOR 用户名 = PASSWORD(\u0026#39;密码\u0026#39;) -- 为指定用户设置密码 6.删除用户 DROP USER 用户名 7.分配权限/添加用户 GRANT 权限列表 ON 库名.表名 TO 用户名@IP地址 [IDENTIFIED BY [PASSWORD] \u0026#39;password\u0026#39;] - all privileges 表示所有权限 - *.* 表示所有库的所有表 - 库名.表名 表示某库下面的某表 eg:GRANT ALL PRIVILEGES ON `pms`.* TO \u0026#39;pms\u0026#39;@\u0026#39;%\u0026#39; IDENTIFIED BY \u0026#39;pms0817\u0026#39;; 8.查看权限 SHOW GRANTS FOR 用户名 -- 查看当前用户权限 SHOW GRANTS; 或 SHOW GRANTS FOR CURRENT_USER; 或 SHOW GRANTS FOR CURRENT_USER(); 9.撤消权限 REVOKE 权限列表 ON 表名 FROM 用户名@IP地址 REVOKE ALL PRIVILEGES, GRANT OPTION FROM 用户名 -- 撤销所有权限 #权限层级 #要使用GRANT或REVOKE，您必须拥有GRANT OPTION权限，并且您必须用于您正在授予或撤销的权限。 #全局层级：全局权限适用于一个给定服务器中的所有数据库，mysql.user GRANT ALL ON *.*和 REVOKE ALL ON *.*只授予和撤销全局权限。 #数据库层级：数据库权限适用于一个给定数据库中的所有目标，mysql.db, mysql.host GRANT ALL ON db_name.*和REVOKE ALL ON db_name.*只授予和撤销数据库权限。 #表层级：表权限适用于一个给定表中的所有列，mysql.talbes_priv GRANT ALL ON db_name.tbl_name和REVOKE ALL ON db_name.tbl_name只授予和撤销表权限。 #列层级：列权限适用于一个给定表中的单一列，mysql.columns_priv 当使用REVOKE时，您必须指定与被授权列相同的列。 #权限列表 ALL [PRIVILEGES] -- 设置除GRANT OPTION之外的所有简单权限 ALTER -- 允许使用ALTER TABLE ALTER ROUTINE -- 更改或取消已存储的子程序 CREATE -- 允许使用CREATE TABLE CREATE ROUTINE -- 创建已存储的子程序 CREATE TEMPORARY TABLES -- 允许使用CREATE TEMPORARY TABLE CREATE USER -- 允许使用CREATE USER, DROP USER, RENAME USER和REVOKE ALL PRIVILEGES。 CREATE VIEW -- 允许使用CREATE VIEW DELETE -- 允许使用DELETE DROP -- 允许使用DROP TABLE EXECUTE -- 允许用户运行已存储的子程序 FILE -- 允许使用SELECT...INTO OUTFILE和LOAD DATA INFILE INDEX -- 允许使用CREATE INDEX和DROP INDEX INSERT -- 允许使用INSERT LOCK TABLES -- 允许对您拥有SELECT权限的表使用LOCK TABLES PROCESS -- 允许使用SHOW FULL PROCESSLIST REFERENCES -- 未被实施 RELOAD -- 允许使用FLUSH REPLICATION CLIENT -- 允许用户询问从属服务器或主服务器的地址 REPLICATION SLAVE -- 用于复制型从属服务器（从主服务器中读取二进制日志事件） SELECT -- 允许使用SELECT SHOW DATABASES -- 显示所有数据库 SHOW VIEW -- 允许使用SHOW CREATE VIEW SHUTDOWN -- 允许使用mysqladmin shutdown SUPER -- 允许使用CHANGE MASTER, KILL, PURGE MASTER LOGS和SET GLOBAL语句，mysqladmin debug命令；允许您连接（一次），即使已达到max_connections。 UPDATE -- 允许使用UPDATE USAGE -- “无权限”的同义词 GRANT OPTION -- 允许授予权限 6.5 DQL:数据查询语言 查询语法格式 mysql 查询数据有两种方式\nSELECT 语法 语法格式如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 SELECT [all|distinct] |top 数字[percent] [ALL | DISTINCT | DISTINCTROW ] 字段 as 常量 | 包含字段表达式 | 函数(如：sum, max) | 常量 FROM 表或结果集 WHERE 条件： 逻辑|空值|多条件|模糊|范围 GROUP BY 字段 HAVING 筛选条件 ORDER BY 字段 ASC | DESC LIMIT 开始索引, 每页大小 sql的执行顺序：from -\u0026gt; where -\u0026gt; group by -\u0026gt; having -\u0026gt; select -\u0026gt; order by\nSELECT INTO 语法 select into 通常用来把旧表数据插入到新表中，请求格式如下:\n1 2 3 4 5 6 7 SELECT 字段等 INTO 表名 FROM 数据源 其他子句 查询数据 select 查询所有列 1 SELECT * FROM 表名; Tips: * 号代表查询所有字段，在实际开发中尽量少用（不直观、影响效率）。\n示例：\n1 2 -- 查询 student 表所有数据 SELECT * FROM student; 查询指定列 1 SELECT 列名1, 列名2, ……… FROM 表名; 示例：\n1 2 -- 查询 student 表的 NAME、gender 列 SELECT NAME,gender FROM student; 查询时指定列的别名 1 SELECT 列名1 AS 别名1, 列名2 AS 别名2,…… FROM 表名; Tips: AS 关键字可以省略\n示例：\n1 2 3 4 SELECT NAME AS \u0026#39;姓名\u0026#39;,gender AS \u0026#39;性别\u0026#39; FROM student; -- AS 可以省略 SELECT NAME \u0026#39;姓名\u0026#39;,gender \u0026#39;性别\u0026#39; FROM student; 合并列查询 查询时可以对数值类型的列进行合并运算，并将结果新增一列。\n1 select *,(数值类型的列名1+数值类型的列名2+……) as 别名 from 表名; 注意事项：\n合并列必须是数值类型 合并非数值类型是没有意义，合并后也是输出数据类型的值 1 2 3 4 5 6 SELECT *, (math+english) \u0026#39;总成绩\u0026#39; FROM student; -- 合并非数值类型 SELECT *, (math+NAME) \u0026#39;总成绩\u0026#39; FROM student; -- 查询所有员工的薪资,年薪,以及姓名，也可以选择列后直接进行算术运算 SELECT uname,salary,(salary*12) AS \u0026#39;年薪\u0026#39; FROM users; 查询时添加常量列 使用''单引号包裹的内容会当成常量列，在查询时新增一列到原表。语法如下：\n1 select *,\u0026#39;添加的内容\u0026#39; as 别名 from 表名; 示例：\n1 SELECT *, \u0026#39;JavaEE 就业班\u0026#39; AS \u0026#39;班级\u0026#39; FROM student; 1 2 3 4 5 6 7 8 9 +----+------+------+------+---------+--------+ | ID | NAME | AGE | MATH | ENGLISH | 班级 | +----+------+------+------+---------+--------+ | 1 | 张三 | 17 | 88 | 98 | JavaEE | +----+------+------+------+---------+--------+ | 2 | 李四 | 19 | 99 | 86 | JavaEE | +----+------+------+------+---------+--------+ | 2 | jack | 30 | 78 | 83 | JavaEE | +----+------+------+------+---------+--------+ 去除重复数据 查询时使用 DISTINCT 关键字可以去除重复数据。\n根据某一列的内容去掉重复的值，只保留其中一个内容 1 select distinct 列名 from 表名; 示例：\n1 SELECT DISTINCT address FROM student; 根据多列的内容去掉重复的值，要多个列的内容同时一致才去掉。 1 select distinct 列名1,列名2,…… from 表名; 示例：\n1 SELECT DISTINCT(address) FROM student; 条件查询 where 基础语法 where语句表条件过滤。满足条件操作，不满足不操作，多用于数据的查询与修改。\n1 SELECT 字段列表 FROM 表名 WHERE 条件; MySQL支持4种运算符 算术运算符 比较运算符 逻辑运算符 位运算符 算术运算符 算术运算符 说明 + 加法运算 - 减法运算 * 乘法运算 / 或 DIV 除法运算，返回商 % 或 MOD 求余运算，返回余数 1 2 3 4 -- 将每件商品的价格加10 select name,price + 10 as new_price from product; -- 将所有商品的价格上调10% select pname,price * 1.1 as new_price from product; 比较运算符 比较运算符 说明 = 等于 \u0026lt; 和 \u0026lt;= 小于和小于等于 \u0026gt; 和 \u0026gt;= 大于和大于等于 \u0026lt;=\u0026gt; 安全的等于，两个操作码均为 NULL 时，其所得值为1；而当一个操作码为NULL时，其所得值为0 \u0026lt;\u0026gt; 或!= 不等于 IS NULL 或 ISNULL 判断一个值是否为 NULL IS NOT NULL 判断一个值是否不为 NULL LEAST 当有两个或多个参数时，返回最小值 GREATEST 当有两个或多个参数时，返回最大值；若比较值中有一个null值，则返回null BETWEEN .. AND .. 判断一个值是否落在两个值之间，显示在某一区间的值(包头包尾) IN 判断一个值是 IN 列表中的任意一个值 NOT IN 判断一个值不是 IN 列表中的任意一个值 LIKE 通配符匹配。模糊查询，Like语句中有两个通配符：%：用来匹配多个字符 _：用来匹配一个字符 REGEXP 正则表达式匹配 注：mysql中用\u0026lt;\u0026gt;与!=都是可以的，但sqlserver中不识别!=，所以建议用\u0026lt;\u0026gt;；但是!=在sql2000中用到，则是语法错误，不兼容的\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 -- 查询 math 字段值在 [80, 88] 区间的记录 SELECT * FROM student WHERE math\u0026gt;=80 AND math\u0026lt;=88; -- 等价于上面 SELECT * FROM student WHERE math BETWEEN 80 AND 88; -- (包前包后) -- 使用least求最小值 select least(10, 20, 30); -- 10 select least(10, null , 30); -- 当比较值中有一个null值，则直接返回null -- 使用greatest求最大值 select greatest(10, 20, 30); select greatest(10, null, 30); -- 当比较值中有一个null值，则直接返回null null 和空字符串的比较运算 null 和 空字符串的区别：\nnull：没有数据。 空字符：有数据，数据就是空字符串。 判断是否为空串：\n= '': 是空串。（注意：这里不是用==） \u0026lt;\u0026gt; '': 不是空串 判断是否为空(null)：\nis null：判断是null is not null：判断不是null 1 2 3 4 SELECT * FROM student WHERE address IS NULL; SELECT * FROM student WHERE address=\u0026#39;\u0026#39;; SELECT * FROM student WHERE address IS NULL OR address=\u0026#39;\u0026#39;; SELECT * FROM student WHERE address IS NOT NULL AND address\u0026lt;\u0026gt;\u0026#39;\u0026#39;; 模糊查询：like 相关字符的含义：\n%：表示匹配多个任意字符(0到多个) _：表示匹配一个任意字符 语法：\n1 select * from 表名 where 列名 like 条件; 示例：\n1 2 3 4 5 6 -- 查询姓张的学生 SELECT * FROM student WHERE NAME LIKE \u0026#39;张%\u0026#39;; -- 查询姓名中包含\u0026#39;张\u0026#39;字的学生 SELECT * FROM student WHERE NAME LIKE \u0026#39;%张%\u0026#39;; -- 查询姓张，且姓名只有两个字的学生 SELECT * FROM student WHERE NAME LIKE \u0026#39;张__\u0026#39;; 模糊查询：in 语法：\n1 select * from 表名 where 列名 in (条件1,条件2,……); 示例：\n1 2 SELECT * FROM student WHERE id IN (1,3); -- 这种效率更高 SELECT * FROM student WHERE id=1 OR id=3; -- 等价于上面的sql 扩展 - 行行比较（SQL-92） SQL-92 中加入了行与行比较的功能。如 =、\u0026lt;、\u0026gt; 和 IN 等比较运算符就不再只是标量值了，还可以是值列表。\n此类型 SQL 同样能走索引\n总结：行行比较是 SQL 规范，不是某个关系型数据库的规范，即关系型数据库都应该支持这种写法。行行比较是 SQL-92 中引入的，SQL-92 是 1992 年制定的规范，即该写法不是新特性，而是很早就存在的基础功能！\n逻辑运算符 逻辑运算符 说明 NOT 或者 ! 逻辑非，条件不成立 AND 或者 \u0026amp;\u0026amp; 逻辑与，多个条件同时成立 OR 或者 ` XOR 逻辑异或 示例：\n1 2 SELECT * FROM student WHERE id=3 AND gender=\u0026#39;男\u0026#39;; SELECT * FROM student WHERE id=3 OR gender=\u0026#39;男\u0026#39;; 位运算符(了解) 位运算符 说明 ` ` \u0026amp; 按位与 ^ 按位异或 \u0026lt;\u0026lt; 按位左移 \u0026gt;\u0026gt; 按位右移 ~ 按位取反，反转所有比特 位运算符是在二进制数上进行计算的运算符。位运算会先将操作数变成二进制数，进行位运算。然后再将计算结果从二进制数变回十进制数。\n1 2 3 4 5 6 select 3\u0026amp;5; -- 位与 select 3|5; -- 位或 select 3^5; -- 位异或 select 3\u0026gt;\u0026gt;1; -- 位左移 select 3\u0026lt;\u0026lt;1; -- 位右移 select ~3; -- 位取反 排序查询 order by order by 语句的作用是根据指定的列内容排序，排序的列可以是表中的列名，也可以是 select 语句后指定的列名。\n1 2 3 4 5 6 select 字段名1，字段名2，…… from 表名 order by 字段名1 [asc|desc]，字段名2[asc|desc]…… 关键字解释：\nasc：顺序（正序：数值：从小到大，字符串：字符 a-z）。不指定时默认asc desc：倒序（正序：数值：从大到小，字符串：字符 z-a） 排序查询注意事项：\norder by 子句应位于 select 语句的结尾。LIMIT 子句除外 order by用于子句中可以支持单个字段，多个字段，表达式，函数，别名 order by 后面指定的列名或别名必须存在，否则查询出错。 示例：\n1 2 3 4 5 6 -- 1.使用价格排序(降序) select * from product order by price desc; -- 2.在价格排序(降序)的基础上，以分类排序(降序) select * from product order by price desc,category_id asc; -- 3.显示商品的价格(去重复)，并排序(降序) select distinct price from product order by price desc; 以表中的列名排序 按表中的列名排序，如果不写(asc/desc)则默认是顺序(asc)\n1 select * from 表名 order by 列名(别名) asc/desc; 示例：\n1 2 3 4 -- 1) 对数学成绩从小到大排序后输出。 SELECT * FROM student ORDER BY math; -- 2) 对总分按从高到低的顺序输出 SELECT *, (math+english) AS 总分 FROM student ORDER BY 总分 DESC; 以 select 语句后指定的列名排序 按新的列名排序，如果出现 where 条件查询，则 ORDER BY 子句应位于 SELECT 语句的结尾。\n1 select *,(列名1+列名2+……) as 别名 from 表名 order by 别名 asc/desc; 示例：\n1 2 -- 3) 姓张的学生成绩从小到大排序输出 SELECT *, (math+english) AS 总分 FROM student WHERE NAME LIKE \u0026#39;张%\u0026#39; ORDER BY 总分; order by 排序内部原理 MySQL 会为每个线程分配一个内存（sort-buffer）用于排序，该内存大小为参数 sort_buffer_size 的值。\n如果排序的数据量小于 sort_buffer_size，排序就会在内存中完成。内部排序分为两种： 全字段排序：到索引树上找到满足条件的主键 ID，根据主键 ID 去取出数据放到 sort_buffer 然后进行快速排序。 rowid 排序：通过控制排序的行数据的长度来让 sort_buffer 中尽可能多的存放数据。 如果数据量很大，内存中无法存下这么多，就会使用磁盘临时文件来辅助排序，称为外部排序。MySQL 会分成多份单独的临时文件来存放排序后的数据，一般是在磁盘文件中进行归并，然后将这些文件合并成一个大文件。 聚合查询 定义与语法 聚合函数查询是纵向查询，它是对一列的值进行计算，然后返回一个单一的值。语法：\n1 select 聚合函数名称(数值列名) from 表名; Notes:\n聚合函数会排除空值(null)的数据。 按聚合函数的结果来查询，列必须是数值列（COUNT函数除外），如果不是数值列，则结果为0 常用的聚合函数 聚合函数 作用 count() 统计指定列不为NULL的记录行数； sum() 计算指定列的数值和，如果指定列类型不是数值类型，那么计算结果为0 max() 计算指定列的最大值，如果指定列是字符串类型，那么使用字符串排序运算； min() 计算指定列的最小值，如果指定列是字符串类型，那么使用字符串排序运算； avg() 计算指定列的平均值，如果指定列类型不是数值类型，那么计算结果为0 聚合查询示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 -- 1. 需求： 查询所有学生 english 的总分 SELECT SUM(english) FROM student; -- 2. 需求： 查询所有学生 english 的平均分 SELECT AVG(english) FROM student; -- 3. 需求：查询最高的 english 分数 SELECT MAX(english) FROM student; -- 4. 需求：查询最低的 english 分数 SELECT MIN(english) FROM student; -- 5. 需求： 一共几个学生 SELECT COUNT(*) FROM student; -- 推荐使用 SELECT COUNT(id) FROM student; -- 效率会比 count(*)效率稍高 -- 注意： 聚合函数，如果列的值 为null，会排除 null 值的数据 SELECT COUNT(address) FROM student; -- 1 查询商品的总条数 select count(*) from product; -- 2 查询价格大于200商品的总条数 select count(*) from product where price \u0026gt; 200; -- 3 查询分类为\u0026#39;c001\u0026#39;的所有商品的总和 select sum(price) from product where category_id = \u0026#39;c001\u0026#39;; -- 4 查询商品的最大价格 select max(price) from product; -- 5 查询商品的最小价格 select min(price) from product; -- 6 查询分类为\u0026#39;c002\u0026#39;所有商品的平均价格 select avg(price) from product where category_id = \u0026#39;c002\u0026#39;; 聚合查询对 NULL 值的处理 count 函数对 null 值的处理逻辑是，如果count函数的参数为星号（*），则统计所有记录的个数。而如果参数为某字段，则统计不为null值的记录个数。 sum 和 avg 函数是忽略 null 值的存在，就好象该条记录不存在一样。 max 和 min 函数也同样忽略 null 值的存在。 分页查询 limit 语法 limit 关键字，用于分页查询数据。语法：\n1 2 3 4 5 -- 方式1: 显示前面指定的n条记录 select * from 表名 limit n; -- 方式2: 分页显示，从起始索引m开始，查询指定n条的记录 select * from 表名 limit m, n; 参数说明：\nm：整数，表示从第几条索引开始， n：整数，表示查询多少条数据 Notes：\n起始行数是从 0 开始，计算公式：起始索引 = (当前页-1) * 每页显示记录数 如果分页同时要进行排序，limit 语句要放在 order by 的后面。 分页查询是数据库的方言，不同的数据库有不同的实现，MySQL 中是 LIMIT 分页查询示例 1 2 3 4 5 6 7 SELECT * FROM student; -- 需求： 查询第 1,2 条数据（第 1 页数据） SELECT * FROM student LIMIT 0,2; -- 需求： 查询第 3,4 条数据（第 2 页数据） SELECT * FROM student LIMIT 2,2; -- 需求： 查询第 5,6 条数据（第 3 页数据） SELECT * FROM student LIMIT 4,2; 分页计算规律总结 观察分页的开始索引，其实是等差数列。如：0,2,4。因此开始索引的计算公式如下：\n1 startIndex = (curPage-1)*pageSize 参数说明：\npageSize：每页多少条 curPage：当前页 startIndex：查询的起始号 分组查询 group by 语法规则 group by 关键字可以将查询结果按某个字段或多个字段进行分组。字段中值相等的为一组。语法：\n1 SELECT 字段1,字段2… FROM 表名 [ WHERE 条件 ] GROUP BY 分组字段名 [HAVING 条件表达式][with rollup]; 参数说明：\nGROUP BY 分组字段名：是指按照该字段的值进行分组。支持多字段分组，如：group by columnA,columnB having 条件表达式：分组后过滤条件，用来限制分组后的显示内容，满足条件表达式的结果将显示 with rollup：关键字将会在所有记录的最后加上一条记录。该记录是上面所有记录的总和 示例：\n1 2 -- 在分组查询的同时，统计人数。 SELECT gender,COUNT(*) \u0026#39;人数\u0026#39; FROM student GROUP BY gender; 注：如果两个表关联，使用分组的话。group by 后面需要写上两个表的分组的列名，要以理解为，这样操作可以保持两个表的行数一致。\n1 2 3 4 5 6 7 8 9 10 SELECT m.courseid, c.`NAME`, round(avg(m.score)) avgscore FROM t_mark m, t_course c WHERE m.courseid = c.id GROUP BY m.courseid, c.`NAME` 分组条件筛选 (having) having 关键字作用：用来对分组信息进行过滤，用法与where一样。但分组之后对统计结果进行筛选的话必须使用 having，不能使用 where。\nwhere 子句用来筛选 FROM 子句中指定的操作所产生的行；group by 子句用来分组 WHERE 子句的输出；having 子句用来从分组的结果中筛选行。\n1 select * from 表名 group by 列名 having 筛选条件; 示例：\n1 2 3 4 5 SELECT address,COUNT(address) \u0026#39;人数\u0026#39; FROM student GROUP BY address; SELECT address,COUNT(address) \u0026#39;人数\u0026#39; FROM student GROUP BY address HAVING COUNT(address)\u0026gt;2; -- 2.统计各个分类商品的个数,且只显示个数大于4的信息 select category_id ,count(*) from product group by category_id having count(*) \u0026gt; 1; where 和 having 的区别 执行时机不同：where 是分组之前进行过滤，不满足 where 条件，不参与分组；而 having 是分组之后对结果进行过滤。 判断条件不同：where 是对行记录进行筛选过滤，where 后不能跟聚合函数的(如:count(*))；而 having 是对组信息进行筛选过滤，having 后可以跟聚合函数的。(如:count(*)) Notes: 执行顺序是 where -\u0026gt; 聚合函数 -\u0026gt; having\nwith rollup 关键字 with rollup 关键字的作用是，在查询结果最后加上一条记录，并且该记录是上面所有分组记录的汇总。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 mysql\u0026gt; SELECT sex, COUNT(sex) FROM employee GROUP BY sex WITH ROLLUP; +------+------------+ | sex | COUNT(sex) | +------+------------+ | 女 | 1 | | 男 | 3 | | NULL | 4 | +------+----------- + mysql\u0026gt; SELECT sex, GROUP_CONCAT(name) FROM employee GROUP BY sex WITH ROLLUP; +------+--------------------+ | sex | GROUP_CONCAT(name) | +------+--------------------+ | 女 | 李四 | | 男 | 张三,王五,Aric | | NULL | 李四,张三,王五,Aric | +------+--------------------+ 上面示例中，最后一条记录 NULL 就是上面各个分组记录的汇总。\nGROUP BY 使用规定 GROUP BY 子句必须出现在 WHERE 子句之后，ORDER BY 子句之前。 使用 GROUP BY 子句进行分组，则 SELECT 子句之后，只能出现分组的字段和统计函数，其他的字段不能出现。 按照指定的列对象数据进行分组，查询的字段一般为分组字段与聚合函数（COUNT()、SUM()、AVG()、MAX()、MIN()）配合使用。如果 group by 不与上述函数一起使用，那么查询结果就是字段聚合的分组情况，字段中取值相同记录为一组，但只显示该组的第一条记录（这种使用意义不大）。 如果分组列中具有 NULL 值，则 NULL 将作为一个分组返回。如果列中有多行 NULL 值，它们将分为一组。 GROUP BY 子句可以包含任意数目的列。这使得能对分组进行嵌套，为数据分组提供更细致的控制。 正则表达式 正则表达式(regular expression)描述了一种字符串匹配的规则，正则表达式本身就是一个字符串，使用这个字符串来描述、用来定义匹配规则，匹配一系列符合某个句法规则的字符串。在开发中，正则表达式通常被用来检索、替换那些符合某个规则的文本。\nMySQL通过 REGEXP 关键字支持正则表达式进行字符串匹配。\n语法规则 模式 描述 ^ 匹配输入字符串的开始位置。 $ 匹配输入字符串的结束位置。 . 匹配除 \u0026ldquo;\\n\u0026rdquo; 之外的任何单个字符。 [...] 字符集合。匹配所包含的任意一个字符。例如，\u0026rsquo;[abc]\u0026rsquo; 可以匹配 \u0026ldquo;plain\u0026rdquo; 中的 \u0026lsquo;a\u0026rsquo;。 [^...] 负值字符集合。匹配未包含的任意字符。例如，\u0026rsquo;[^abc]\u0026rsquo; 可以匹配 \u0026ldquo;plain\u0026rdquo; 中的 \u0026lsquo;p\u0026rsquo;。 `p1 p2 * 匹配前面的子表达式零次或多次。例如，zo* 能匹配 \u0026ldquo;z\u0026rdquo; 以及 \u0026ldquo;zoo\u0026rdquo;。* 等价于{0,}。 + 匹配前面的子表达式一次或多次。例如，\u0026rsquo;zo+\u0026rsquo; 能匹配 \u0026ldquo;zo\u0026rdquo; 以及 \u0026ldquo;zoo\u0026rdquo;，但不能匹配 \u0026ldquo;z\u0026rdquo;。+ 等价于 {1,}。 {n} n 是一个非负整数。匹配确定的 n 次。例如，\u0026rsquo;o{2}\u0026rsquo; 不能匹配 \u0026ldquo;Bob\u0026rdquo; 中的 \u0026lsquo;o\u0026rsquo;，但是能匹配 \u0026ldquo;food\u0026rdquo; 中的两个 o。 {n,m} m 和 n 均为非负整数，其中n \u0026lt;= m。最少匹配 n 次且最多匹配 m 次。 示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 -- ^ 在字符串开始处进行匹配 SELECT \u0026#39;abc\u0026#39; REGEXP \u0026#39;^a\u0026#39;; -- $ 在字符串末尾开始匹配 SELECT \u0026#39;abc\u0026#39; REGEXP \u0026#39;a$\u0026#39;; SELECT \u0026#39;abc\u0026#39; REGEXP \u0026#39;c$’; -- . 匹配任意字符 SELECT \u0026#39;abc\u0026#39; REGEXP \u0026#39;.b\u0026#39;; SELECT \u0026#39;abc\u0026#39; REGEXP \u0026#39;.c\u0026#39;; SELECT \u0026#39;abc\u0026#39; REGEXP \u0026#39;a.\u0026#39;; -- [...] 匹配括号内的任意单个字符 SELECT \u0026#39;abc\u0026#39; REGEXP \u0026#39;[xyz]\u0026#39;; SELECT \u0026#39;abc\u0026#39; REGEXP \u0026#39;[xaz]\u0026#39;; -- [^...] 注意^符合只有在[]内才是取反的意思，在别的地方都是表示开始处匹配 SELECT \u0026#39;a\u0026#39; REGEXP \u0026#39;[^abc]\u0026#39;; SELECT \u0026#39;x\u0026#39; REGEXP \u0026#39;[^abc]\u0026#39;; SELECT \u0026#39;abc\u0026#39; REGEXP \u0026#39;[^a]\u0026#39;; -- a* 匹配0个或多个a,包括空字符串。 可以作为占位符使用.有没有指定字符都可以匹配到数据 SELECT \u0026#39;stab\u0026#39; REGEXP \u0026#39;.ta*b\u0026#39;; SELECT \u0026#39;stb\u0026#39; REGEXP \u0026#39;.ta*b\u0026#39;; SELECT \u0026#39;\u0026#39; REGEXP \u0026#39;a*\u0026#39;; -- a+ 匹配1个或者多个a,但是不包括空字符 SELECT \u0026#39;stab\u0026#39; REGEXP \u0026#39;.ta+b\u0026#39;; SELECT \u0026#39;stb\u0026#39; REGEXP \u0026#39;.ta+b\u0026#39;; -- a? 匹配0个或者1个a SELECT \u0026#39;stb\u0026#39; REGEXP \u0026#39;.ta?b\u0026#39;; SELECT \u0026#39;stab\u0026#39; REGEXP \u0026#39;.ta?b\u0026#39;; SELECT \u0026#39;staab\u0026#39; REGEXP \u0026#39;.ta?b\u0026#39;; -- a1|a2 匹配a1或者a2， SELECT \u0026#39;a\u0026#39; REGEXP \u0026#39;a|b\u0026#39;; SELECT \u0026#39;b\u0026#39; REGEXP \u0026#39;a|b\u0026#39;; SELECT \u0026#39;b\u0026#39; REGEXP \u0026#39;^(a|b)\u0026#39;; SELECT \u0026#39;a\u0026#39; REGEXP \u0026#39;^(a|b)\u0026#39;; SELECT \u0026#39;c\u0026#39; REGEXP \u0026#39;^(a|b)\u0026#39;; -- a{m} 匹配m个a SELECT \u0026#39;auuuuc\u0026#39; REGEXP \u0026#39;au{4}c\u0026#39;; SELECT \u0026#39;auuuuc\u0026#39; REGEXP \u0026#39;au{3}c\u0026#39;; -- a{m,n} 匹配m到n个a,包含m和n SELECT \u0026#39;auuuuc\u0026#39; REGEXP \u0026#39;au{3,5}c\u0026#39;; SELECT \u0026#39;auuuuc\u0026#39; REGEXP \u0026#39;au{4,5}c\u0026#39;; SELECT \u0026#39;auuuuc\u0026#39; REGEXP \u0026#39;au{5,10}c\u0026#39;; -- (abc) abc作为一个序列匹配，不用括号括起来都是用单个字符去匹配，如果要把多个字符作为一个整体去匹配就需要用到括号，所以括号适合上面的所有情况。 SELECT \u0026#39;xababy\u0026#39; REGEXP \u0026#39;x(abab)y\u0026#39;; SELECT \u0026#39;xababy\u0026#39; REGEXP \u0026#39;x(ab)*y\u0026#39;; SELECT \u0026#39;xababy\u0026#39; REGEXP \u0026#39;x(ab){1,2}y\u0026#39;; 查询（select）语句执行顺序分析 执行顺序说明 DQL 语句在执行的顺序如下：\n执行顺序文字说明：\nfrom 子句组装来自不同数据源的数据。 where 子句基于指定的条件对记录行进行筛选。 group by 子句将数据划分为多个分组。 对聚集函数进行计算。 having 子句对分组后筛选分组。 计算所有的表达式。 select 的字段列表的处理。 使用 order by 对结果集进行排序。 根据 limit 参数对结果集进行分页处理。 综上所述，DQL 语句的执行顺序为：from -\u0026gt; where -\u0026gt; group by -\u0026gt; having -\u0026gt; select -\u0026gt; order by -\u0026gt; limit\n验证过程 1 2 -- 查询年龄大于15的员工姓名、年龄，并根据年龄进行升序排序。 select name, age from emp where age \u0026gt; 15 order by age asc; 在查询时，给 emp 表起一个别名 e，然后在 select 及 where 中使用该别名。\n1 select e.name, e.age from emp e where e.age \u0026gt; 15 order by age asc; 执行上述SQL语句后，可以正常的查询到结果，此时就说明：from 先执行生成了别名，然后 where 和 select 可以使用别名并执行。\n若给 select 后面的字段起别名，然后在 where 中使用这个别名，然后看看是否可以执行成功。\n1 select e.name ename, e.age eage from emp e where eage \u0026gt; 15 order by age asc; 执行上述SQL报错了：\n由此可以得出结论：from 先执行，然后执行 where，再执行 select。\n接下来，再执行如下SQL语句，查看执行效果：\n1 select e.name ename, e.age eage from emp e where e.age \u0026gt; 15 order by eage asc; 结果执行成功。那么也就验证了：order by 是在 select 语句之后执行的。\n6.6 备份与恢复 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 1.数据库导出sQr脚本 mysqldump -u用户名 -p密码 数据库名\u0026gt;生成的脚本文件路径 eg:mysqldump -uroot -p123 mydb1\u0026gt;C:\\mydb1.sql(与mysq1.exe和myqld.exe一样，都在bin目录下) #注意,不要打分号，不要登录mysql，直接在cmd下运行 #注意,生成的脚本文件中不包含create database语句 2.执行sql脚本 第一种方式 mysgl -u用户名 -p密码 数据库\u0026lt;脚本文件路径 eg:*先删除mydb1库,再重新创建mydb1库 *mysgl -uroot -p123 mydb1\u0026lt;C:\\mydb1.sql 第二种方式 a.登录mysql b.source sQL脚本路径 eg:先册除mydb1库，再重新创建myab1库 切换到mydb1库 source c:\\mydb1.sql 3.其它用法 a. 导出一张表 mysqldump -u用户名 -p密码 库名 表名 \u0026gt; 文件名(D:/a.sql) b. 导出多张表 mysqldump -u用户名 -p密码 库名 表1 表2 表3 \u0026gt; 文件名(D:/a.sql) c. 导出所有表 mysqldump -u用户名 -p密码 库名 \u0026gt; 文件名(D:/a.sql) d. 导出一个库 mysqldump -u用户名 -p密码 --lock-all-tables --database 库名 \u0026gt; 文件名(D:/a.sql) 7.字符集编码 7.1. 字符集概述 MySQL 服务器支持多种字符集，常用的字符集有 UTF8、UTF8MB4、UTF16、UTF32 等。在同一台服务器、同一个数据库、甚至同一个表的不同字段都可以指定使用不同的字符集。\nMySQL 的字符集包括字符集(CHARACTER)校对规则(COLLATION)等概念。\n7.1.1. 查看支持的字符集 可以使用 show character set 命令查看所有 MySQL 支持的字符集：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 mysql\u0026gt; show character set; +----------+---------------------------------+---------------------+--------+ | Charset | Description | Default collation | Maxlen | +----------+---------------------------------+---------------------+--------+ | armscii8 | ARMSCII-8 Armenian | armscii8_general_ci | 1 | | ascii | US ASCII | ascii_general_ci | 1 | | big5 | Big5 Traditional Chinese | big5_chinese_ci | 2 | | binary | Binary pseudo charset | binary | 1 | | cp1250 | Windows Central European | cp1250_general_ci | 1 | | cp1251 | Windows Cyrillic | cp1251_general_ci | 1 | | cp1256 | Windows Arabic | cp1256_general_ci | 1 | | cp1257 | Windows Baltic | cp1257_general_ci | 1 | | cp850 | DOS West European | cp850_general_ci | 1 | | cp852 | DOS Central European | cp852_general_ci | 1 | | cp866 | DOS Russian | cp866_general_ci | 1 | | cp932 | SJIS for Windows Japanese | cp932_japanese_ci | 2 | | dec8 | DEC West European | dec8_swedish_ci | 1 | | eucjpms | UJIS for Windows Japanese | eucjpms_japanese_ci | 3 | | euckr | EUC-KR Korean | euckr_korean_ci | 2 | | gb18030 | China National Standard GB18030 | gb18030_chinese_ci | 4 | | gb2312 | GB2312 Simplified Chinese | gb2312_chinese_ci | 2 | | gbk | GBK Simplified Chinese | gbk_chinese_ci | 2 | | geostd8 | GEOSTD8 Georgian | geostd8_general_ci | 1 | | greek | ISO 8859-7 Greek | greek_general_ci | 1 | | hebrew | ISO 8859-8 Hebrew | hebrew_general_ci | 1 | | hp8 | HP West European | hp8_english_ci | 1 | | keybcs2 | DOS Kamenicky Czech-Slovak | keybcs2_general_ci | 1 | | koi8r | KOI8-R Relcom Russian | koi8r_general_ci | 1 | | koi8u | KOI8-U Ukrainian | koi8u_general_ci | 1 | | latin1 | cp1252 West European | latin1_swedish_ci | 1 | | latin2 | ISO 8859-2 Central European | latin2_general_ci | 1 | | latin5 | ISO 8859-9 Turkish | latin5_turkish_ci | 1 | | latin7 | ISO 8859-13 Baltic | latin7_general_ci | 1 | | macce | Mac Central European | macce_general_ci | 1 | | macroman | Mac West European | macroman_general_ci | 1 | | sjis | Shift-JIS Japanese | sjis_japanese_ci | 2 | | swe7 | 7bit Swedish | swe7_swedish_ci | 1 | | tis620 | TIS620 Thai | tis620_thai_ci | 1 | | ucs2 | UCS-2 Unicode | ucs2_general_ci | 2 | | ujis | EUC-JP Japanese | ujis_japanese_ci | 3 | | utf16 | UTF-16 Unicode | utf16_general_ci | 4 | | utf16le | UTF-16LE Unicode | utf16le_general_ci | 4 | | utf32 | UTF-32 Unicode | utf32_general_ci | 4 | | utf8mb3 | UTF-8 Unicode | utf8mb3_general_ci | 3 | | utf8mb4 | UTF-8 Unicode | utf8mb4_0900_ai_ci | 4 | +----------+---------------------------------+---------------------+--------+ 7.1.2. Unicode、UTF8 和 UTF8MB4 Unicode（统一码、万国码、单一码）是计算机科学领域里的一项业界标准，包括字符集、编码方案等。Unicode 是为了解决传统的字符编码方案的局限而产生的，它为每种语言中的每个字符设定了统一并且唯一的二进制编码，以满足跨语言、跨平台进行文本转换、处理的要求。UTF8、UTF16、UTF32 是 Unicode码 一种实现形式，都是属于 Unicode 编码。\nUTF8 和 UTF8MB4 是常用的两种字符集，根据业务情况而决定选那种类型。UTF8MB4 兼容 UTF8，比 UTF8 能表示更多的字符。一般情况下 UTF8 就满足需求，如果考虑到以后扩展，比如考虑到以后存储 emoji 表情，则选择 UTF8MB4，否则只是浪费空间。个人建议还是选择 UTF8MB4\n7.2. 校对规则(比较和排序规则)（COLLATE） 7.2.1. 概念 在 MySQL 中，character set 用于指定数据库默认的字符集；而 collate 是用于指定校对规则。\n校对规则，（COLLATE）又叫『比较和排序规则』。它是一组规则，负责决定某一字符集下的字符进行比较和排序的结果。如：a,B,c,D，如果使用 utf-8 的编码，按照普通的字母顺序，而且不区分大小写。如果想使用字母的二进制比较和排序，则可以修改它的校对规则。\nNotes:\nutf8_general_ci：按照普通的字母顺序，而且不区分大小写（比如：a B c D） utf8_bin：按照二进制排序（比如：A 排在 a 前面，B D a c） 7.2.2. 查看支持的校对规则 通过 show collation 命令查看校对规则。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 -- 显示所有 utf-8 mysql\u0026gt; show collation like \u0026#39;utf8%\u0026#39;; +-----------------------------+---------+-----+---------+----------+---------+---------------+ | Collation | Charset | Id | Default | Compiled | Sortlen | Pad_attribute | +-----------------------------+---------+-----+---------+----------+---------+---------------+ | utf8mb3_general_ci | utf8mb3 | 33 | Yes | Yes | 1 | PAD SPACE | | utf8mb3_unicode_ci | utf8mb3 | 192 | | Yes | 8 | PAD SPACE | | utf8mb4_general_ci | utf8mb4 | 45 | | Yes | 1 | PAD SPACE | | utf8mb4_unicode_ci | utf8mb4 | 224 | | Yes | 8 | PAD SPACE | | ...省略大部分内容 | +-----------------------------+---------+-----+---------+----------+---------+---------------+ -- 显示所有 GBK mysql\u0026gt; show collation like \u0026#39;gbk%\u0026#39;; +----------------+---------+----+---------+----------+---------+---------------+ | Collation | Charset | Id | Default | Compiled | Sortlen | Pad_attribute | +----------------+---------+----+---------+----------+---------+---------------+ | gbk_bin | gbk | 87 | | Yes | 1 | PAD SPACE | | gbk_chinese_ci | gbk | 28 | Yes | Yes | 1 | PAD SPACE | +----------------+---------+----+---------+----------+---------+---------------+ Tips: SQL 语句中的字符串一般都是使用单引号(')括起。\n7.2.3. UTF8MB4 常用的排序规则 UTF8MB4 常用的排序规则：utf8mb4_unicode_ci、utf8mb4_general_ci、utf8mb4_bin\n从准确性的角度比较\nutf8mb4_unicode_ci 是基于标准的 Unicode 来排序和比较，能够在各种语言之间精确排序，不区分大小写 utf8mb4_general_ci 没有实现 Unicode 排序规则，在遇到某些特殊语言或者字符集，排序结果可能不一致，不区分大小写 从性能的角度比较\nutf8mb4_general_ci 在比较和排序的时候更快 utf8mb4_unicode_ci 在特殊情况下，Unicode 排序规则为了能够处理特殊字符的情况，实现了略微复杂的排序算法。相比选择哪一种 collation，使用者更应该关心字符集与排序规则在db里需要统一。 utf8mb4_bin 将字符串每个字符用二进制数据编译存储，区分大小写，而且可以存二进制的内容。 Tips: 总而言之，utf8mb4_general_ci 和 utf8mb4_unicode_ci 是最常使用的排序规则。utf8mb4_general_ci 校对速度快，但准确度稍差；utf8_unicode_ci 准确度高，但校对速度稍慢。两者都不区分大小写，选择那种类型按具体情况而定\n7.3. 指定数据库的默认字符集与校对规则 例如：指定数据库的默认字符集为 gbk 和校对规则 gbk_chinese_ci。\n1 create database db4 default character set gbk collate gbk_chinese_ci; 7.4. 查看与修改当前字符集 7.4.1. 查看字符集 语法：\n1 show variables like \u0026#39;character%\u0026#39;; 参数解释：\nshow variables 显示所有的全局变量 % 代表通配符 1 2 3 4 5 6 7 8 9 10 11 12 13 mysql\u0026gt; show variables like \u0026#39;character%\u0026#39;; +--------------------------+-------------------------------------------------------+ | Variable_name | Value | +--------------------------+-------------------------------------------------------+ | character_set_client | utf8mb4 | | character_set_connection | utf8mb4 | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | utf8mb4 | | character_set_server | utf8mb3 | | character_set_system | utf8mb3 | | character_sets_dir | D:\\development\\MySQL\\MySQL Server 8.0\\share\\charsets\\ | +--------------------------+-------------------------------------------------------+ 7.4.2. 修改字符集 以解决 DOS 命令行下汉字乱码的问题为例。DOS 命令行默认的字符集是 GBK，而数据库的字符集是 UTF-8，要将数据库中下列三项的字符集也改成 GBK。在命令行插入数据之前输入: set names gbk; 则等同于\n1 2 3 set character_set_connection=gbk; -- 设置数据库连接使用的字符集 set character_set_results=gbk; -- 设置查询结果的字符集 set character_set_client=gbk; -- 设置客户端的字符集 本次连接中修改字符集\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 mysql\u0026gt; set character_set_connection=gbk; Query OK, 0 rows affected (0.00 sec) mysql\u0026gt; set character_set_results=gbk; Query OK, 0 rows affected (0.00 sec) mysql\u0026gt; set character_set_client=gbk; Query OK, 0 rows affected (0.00 sec) mysql\u0026gt; show variables like \u0026#39;character%\u0026#39;; +--------------------------+-------------------------------------------------------+ | Variable_name | Value | +--------------------------+-------------------------------------------------------+ | character_set_client | gbk | | character_set_connection | gbk | | character_set_database | utf8mb4 | | character_set_filesystem | binary | | character_set_results | gbk | | character_set_server | utf8mb3 | | character_set_system | utf8mb3 | | character_sets_dir | D:\\development\\MySQL\\MySQL Server 8.0\\share\\charsets\\ | +--------------------------+-------------------------------------------------------+ Notes: 上面只改变了本次运行时的数据库局部的字符集，重启后也会变回原来的模式。\n7.5. 字符集的选择原则 建议在能够完全满足应用的前提下，尽量使用小的字符集。因为更小的字符集意味着能够节省空间、减少网络传输字节数，同时由于存储空间的较小间接的提高了系统的性能。\n8.导入导出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 1.导出表数据 select * into outfile 文件地址 [控制格式] from 表名; 2.导入数据 load data [local] infile 文件地址 [replace|ignore] into table 表名 [控制格式]; 生成的数据默认的分隔符是制表符 local未指定，则数据文件必须在服务器上 replace 和 ignore 关键词控制对现有的唯一键记录的重复的处理 ---控制格式 *fields 控制字段格式 默认：fields terminated by \u0026#39;\\t\u0026#39; enclosed by \u0026#39;\u0026#39; escaped by \u0026#39;\\\\\u0026#39; terminated by \u0026#39;string\u0026#39; -- 终止 enclosed by \u0026#39;char\u0026#39; -- 包裹 escaped by \u0026#39;char\u0026#39; -- 转义 -- 示例： SELECT a,b,a+b INTO OUTFILE \u0026#39;/tmp/result.text\u0026#39; FIELDS TERMINATED BY \u0026#39;,\u0026#39; OPTIONALLY ENCLOSED BY \u0026#39;\u0026#34;\u0026#39; LINES TERMINATED BY \u0026#39;\\n\u0026#39; FROM test_table; *lines 控制行格式 默认：lines terminated by \u0026#39;\\n\u0026#39; terminated by \u0026#39;string\u0026#39; -- 终止 9. MySQL 的多表操作 实际开发中，一个项目通常需要很多张表才能完成，且这些表的数据之间存在一定的关系。\n9.1. 多表关系 MySQL多表之间的关系可以概括为：一对一、一对多/多对一关系，多对多\n9.1.1. 一对一(1:1) 在实际的开发中应用不多，因为一对一可以创建成一张表。有两种建表原则：\n外键唯一：主表的主键和从表的外键（唯一），形成主外键关系，外键唯一，这其实是一种特殊的多对一的关系。 注：如果是外键唯一这种方式，则需要外键的约束条件和主表的主键一致 外键是主键：主表的主键和从表的主键，形成主外键关系 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 -- 一对一关系： 外键唯一 -- 简历表 create table jl( id int primary key, content varchar(200) ); -- 学生表 create table s7( id int primary key, name varchar(20), jl_id int unique, -- 外键唯一 constraint foreign key(jl_id) references jl(id) ); -- 一对一关系：主键又是外键 -- 简历表：主表 create table jl( id int primary key, content varchar(200) ); -- 学生表：从表 create table s7( id int primary key, name varchar(20), constraint foreign key(id) references jl(id) ); Tips: 其中设置从表的外键为唯一的(UNIQUE)是关键\n9.1.2. 一对多(1:n)(重点) 常见实例：客户和订单，分类和商品，部门和员工。 一对多建表原则：在从表(多方)创建一个字段，字段作为外键指向主表(一方)的主键。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 -- 创建学科表格 主表 create table class( cid int, sub varchar(10) not null unique ) -- 创建表完成之后添加主键 alter table class modify cid int primary key; -- 创建学生表格 从表 create table student( sid int primary key auto_increment, sname varchar(10) not null, gender varchar(2) not null, class_id int, constraint foreign key(class_id) references class(cid) on update cascade ); -- 创建后查看表清单 show tables; desc class; desc student; -- 插入数据 insert into class values(001, \u0026#39;java\u0026#39;), (002, \u0026#39;iso\u0026#39;),(003, \u0026#39;php\u0026#39;); select * from class; insert into student(sname, gender, class_id) values (\u0026#39;敌法师\u0026#39;,\u0026#39;男\u0026#39;,2), (\u0026#39;主宰\u0026#39;,\u0026#39;男\u0026#39;,1), (\u0026#39;痛苦女王\u0026#39;,\u0026#39;女\u0026#39;,3), (\u0026#39;露娜\u0026#39;,\u0026#39;女\u0026#39;,1); select * from student; 1:n表关系图： 9.1.3. 多对多(n:n) 常见实例：学生和课程、用户和角色。 多对多关系建表原则 需要创建第三张表，中间表中至少两个字段，这两个字段分别作为外键指向各自一方的主键。 多对多设计的关系表的关键： 单独设置一张关系表(设置为联合主键) 语法例子 1 constraint primary key(s_id, c_id) 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 create table goods( gid int primary key auto_increment, gname varchar(20) not null unique ); -- 插入商品 insert into goods(gname) values (\u0026#39;椅子\u0026#39;),(\u0026#39;床\u0026#39;),(\u0026#39;桌子\u0026#39;),\t(\u0026#39;苹果\u0026#39;),(\u0026#39;香蕉\u0026#39;),(\u0026#39;汽水\u0026#39;),(\u0026#39;饼干\u0026#39;); -- 查看商品表 select * from goods; -- 创建购买人表 主表 create table person( pid int primary key auto_increment, pname varchar(10) not null, age int not null ); -- 插入购买人信息 insert into person(pname, age) values (\u0026#39;剑圣\u0026#39;,28),(\u0026#39;敌法师\u0026#39;,26),(\u0026#39;痛苦女王\u0026#39;,23),(\u0026#39;西门吹水\u0026#39;,34),(\u0026#39;潘银莲\u0026#39;,21),(\u0026#39;东施\u0026#39;,23); -- 查看购买人表 select * from person; -- 创建关系表 从表 create table person_goods( p_id int, g_id int, constraint primary key (p_id,g_id), constraint foreign key(p_id) references person(pid), constraint foreign key(g_id) references goods(gid) ); insert into person_goods values (1,2),(1,6),(2,4),(3,4),(3,5),(4,6),(5,7),(6,6),(5,6); -- 查看关系表 select * from person_goods; -- 修改关系表数据 delete from person_goods where p_id=2 and g_id=4; update person_goods set p_id=2 where p_id=5 and g_id=6; n:n表关系图： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 /* 关卡2训练案例2 1:完成学员 student 和 老师 teacher 表和课程表的设计 2:多对多设计原则,引入中间表. 操作步骤 1: 完成学员和老师,课程以及中间表设计 2: 使用 sql 脚本完成中间表设计以及联合主键,外键的引入. 3: 录入相关数据. */ -- 创建学生表 stu create table stu( sid int(4) zerofill primary key auto_increment, sname varchar(6) not null, age int ); -- 创建老师表 create table teacher( tid int(4) zerofill primary key auto_increment, tname varchar(6) not null, age int ); -- 创建课程表course create table course( cid int(2) zerofill primary key auto_increment, cname varchar(20) not null unique ); -- 创建关系表 create table stu_tea_cou( sid int(4) zerofill, tid int(4) zerofill, cid int(2) zerofill, -- 定义联合主键 constraint primary key(sid,tid,cid), -- 定义主键对应各个表的主键 constraint foreign key(sid) references stu(sid), constraint foreign key(tid) references teacher(tid), constraint foreign key(cid) references course(cid) ); -- 使用内连接查询全部学生内容 -- SELECT * FROM ((表1 INNER JOIN 表2 ON 表1.字段号=表2.字段号) -- INNER JOIN 表3 ON 表1.字段号=表3.字段号) INNER JOIN 表4 ON Member.字段号=表4.字段号; select stu.*,course.cname as \u0026#39;学科名\u0026#39;,stu_tea_cou.score as \u0026#39;得分\u0026#39;,teacher.tname as \u0026#39;老师\u0026#39; from ((stu inner join stu_tea_cou on stu.sid=stu_tea_cou.sid) inner join course on course.cid=stu_tea_cou.cid) inner join teacher on teacher.tid=stu_tea_cou.tid; n:n表关系图（三个） 9.2. 多表连接查询 9.2.1. 多表连接查询概述 单表查询是从一张表中查询数据，而多表查询就是从多张有关联的表中查询数据。\nNotes: N 张表的连接查询，至少要有 N-1 个连接条件\n9.2.2. 多表连接类型 交叉连接查询[产生笛卡尔积，了解] 内连接查询 左右(外)连接查询 子查询 表自关联 全表连接查询（MySql 不支持，Oracle 支持） 下图展示了 LEFT JOIN、RIGHT JOIN、INNER JOIN、OUTER JOIN 相关的 7 种用法\n9.2.3. 多表连接查询的步骤 首先确定要查询的数据有哪些 再确定要查询的数据来自哪些表 最后确定表之间的连接条件 Tips: 多表连接查询必须使用表名(或表别名).列名才进行查询，因为需要区分该列是属于哪个表的，一旦设置了别名后，就必须用别名.列名，用原来表名.列名会报错。\n9.3. 交叉连接查询（笛卡尔积） 9.3.1. 交叉查询概述 当查询记录数等于多个表的记录数乘积时，该查询则称为交叉查询。 交叉查询的结果称为笛卡尔积，即多张表记录的乘积 在实际开发中，笛卡尔积的结果一般没有任何意义，一般都会在笛卡尔积基础上加上过滤条件，得出的结果才会有意义。 9.3.2. 交叉查询格式 这种查询会产生笛卡尔积，就是两个表的所有记录的乘积。语法格式：\n1 select 表名1.*,表名2.*,…… from 表名1,表名2,…… where 控制条件; 示例：\n1 SELECT e.*, d.* FROM employee e, dept d; 图例：有 2 张表，1 张 R、1 张 S\nR 表有 ABC 三列，表中有三条记录。 A B C a1 b1 c1 a2 b2 c2 a3 b3 c3 S 表有 CD 两列，表中有三条记录。 C D c1 d1 c2 d2 c4 d3 交叉连接(笛卡尔积): select r.*,s.* from r,s; A B C C D a1 b1 c1 c1 d1 a2 b2 c2 c1 d1 a3 b3 c3 c1 d1 a1 b1 c1 c2 d2 a2 b2 c2 c2 d2 a3 b3 c3 c2 d2 a1 b1 c1 c4 d3 a2 b2 c2 c4 d3 a3 b3 c3 c4 d3 9.4. 内连接查询( inner join …… on ) 9.4.1. 内连接概述 只有满足连接条件的记录才会被查询出来，实际开发使用频率最高。连接条件：主表的主键与从表的外键值进行相等匹配查询\n1 2 3 4 5 SELECT \u0026lt; select_list \u0026gt; FROM Table_A A INNER JOIN Table_B B ON A. KEY = B. KEY 9.4.2. 内连接语法格式 1 SELECT * FROM 表1 [INNER | CROSS] JOIN 表2 [ON 连接条件] [WHERE 普通过滤条件]; 9.4.3. 内连接查询的分类 隐式内连接：使用where语句(在笛卡尔积的基础上使用) 显式内连接：使用语法格式 inner join …… on（inner 可以省略） 在 MySQL 中，下边这几种内连接的写法都是等价的：\n1 2 3 4 5 SELECT * FROM t1 JOIN t2; SELECT * FROM t1 INNER JOIN t2; SELECT * FROM t1 CROSS JOIN t2; -- 上边的这些写法和直接把需要连接的表名放到 FROM 语句之后，用逗号,分隔开的写法是等价的 SELECT * FROM t1, t2; 注：在内连接查询中，on子语句与where子语句的作用是一样的。\n9.4.4. 内连接的驱动表与被驱动表 对于内连接来说，由于凡是不符合ON子句或WHERE子句中的条件的记录都会被过滤掉，其实也就相当于从两表连接的笛卡尔积中过滤了不符合条件的记录，所以对于内连接来说，驱动表和被驱动表是可以互换的，并不会影响最后的查询结果。\n但是对于外连接来说，由于驱动表中的记录即使在被驱动表中找不到符合ON子句条件的记录时也要将其加入到结果集，所以此时驱动表和被驱动表的关系就很重要了，也就是说左外连接和右外连接的驱动表和被驱动表不能轻易互换。\n9.4.5. 显式内连接：使用 inner join \u0026hellip; on 显式内连接格式： 1 select 表名1.*,表名2.* from 表名1 inner join 表名2 on 表名1.列名=表名2.列名; 显式内连接，上面的列名分别是主从表的主键与从键，表名后面可以跟表别名，通常用表的首字母，后面使用表别名.列名 1 select s.sname,c.sub from student s inner join class c on s.class_id=c.cid; 图例：内连接：select r.*,s.* from r inner join s on r.c=s.c;\nA B C C D a1 b1 c1 c1 d1 a2 b2 c2 c2 d2 9.4.6. 隐式内连接：使用 where 子句（笛卡尔积再过滤） 隐式内连接格式：\n1 select 表名1.*,表名2.* from 表名1,表名2 where 表名1.列名=表名2.列名; 隐式内连接，上面的列名分别是主从表的主键与从键，表名后面可以跟表别名，通常用表的首字母，后面使用 表别名.列名\n1 select s.sname,c.sub from student s,class c where s.class_id=c.cid; 9.4.7. 扩展：内连接3个以上数据表 INNER JOIN 连接三个数据表的用法： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 -- 方式1： SELECT * FROM 表1 INNER JOIN 表2 INNER JOIN 表3 ON 表1.字段号 = 表2.字段号 AND 表1.字段号 = 表3.字段号; -- 方式2： SELECT * FROM (表1 INNER JOIN 表2 ON 表1.字段号 = 表2.字段号) INNER JOIN 表3 ON 表1.字段号 = 表3.字段号; -- 以上两种写法一样的效果。 INNER JOIN 连接四个数据表的用法： 1 2 3 4 5 6 7 8 9 SELECT * FROM 表1 INNER JOIN 表2 INNER JOIN 表3 INNER JOIN 表4 ON 表1.字段号 = 表2.字段号 AND 表1.字段号 = 表3.字段号 AND 表1.字段号 = 表4.字段号; INNER JOIN 连接五个数据表的用法： 1 2 3 4 5 6 7 8 9 10 11 SELECT * FROM 表1 INNER JOIN 表2 INNER JOIN 表3 INNER JOIN 表4 INNER JOIN 表5 ON 表1.字段号 = 表2.字段号 AND 表1.字段号 = 表3.字段号 AND 表1.字段号 = 表4.字段号 AND 表1.字段号 = 表5.字段号; 上面的表号根据实际情况确定，连接六个数据表的用法，根据上面类推\n注意事项：\n如果连接n张表，其连接条件就是n-1个。 使用内连接前，搞清楚需要输出那些字段，字段在那些表中，各自表的主外键的关系。 在建立数据表时，如果一个表与多个表联接，那么这一个表中的字段必须是“数字”数据类型，而多个表中的相同字段必须是主键，而且是“自动编号”数据类型。否则，很难联接成功。 9.5. 左(外)连接( left join …… on ) 9.5.1. 左外连接概述 左外连接定义：用左表的记录去匹配右表的记录，如果条件满足，则右边显示右表的记录；否则右表显示 null。（左表和右表取决于定义在实际语句的位置） 左外连接特点：左边的表的记录一定会全部显示完整 左外连接的驱动表：选取语句左侧的表 1 2 3 4 5 SELECT \u0026lt; select_list \u0026gt; FROM Table_A A LEFT JOIN Table_B B ON A. KEY = B. KEY 9.5.2. 左外连接语法格式 1 SELECT * FROM 表1 LEFT [OUTER] JOIN 表2 ON 连接条件 [WHERE 普通过滤条件]; Tips: 其中中括号里的OUTER关键字是可以省略的。\n示例 1 select s.sname,c.sub from student s left join class c on s.class_id=c.cid; 图例：左连接：select r.*,s.* from r left join s on r.c=s.c;\nA B C C D a1 b1 c1 c1 d1 a2 b2 c2 c2 d2 a3 b3 c3 null 9.6. 右(外)连接( right join …… on ) 9.6.1. 右外连接概述 右外连接定义：用右表的记录去匹配左表的记录，如果条件满足，则左边显示左表的记录；否则左边显示 null。（左表和右表取决于定义在实际语句的位置） 右外连接特点：如果右外连接，右边的表的记录一定会全部显示完整 右外连接驱动表：选取语句右侧的表 1 2 3 4 5 SELECT \u0026lt; select_list \u0026gt; FROM Table_A A RIGHT JOIN Table_B B ON A. KEY = B. KEY 9.6.2. 右外连接语法格式 1 SELECT * FROM 表1 RIGHT [OUTER] JOIN 表2 ON 连接条件 [WHERE 普通过滤条件]; 其中中括号里的OUTER关键字是可以省略的。\n示例： 1 select s.sname,c.sub from student s right join class c on s.class_id=c.cid; 图例：右外连接：select r.*,s.* from r right join s on r.c=s.c;\nA B C C D a1 b1 c1 c1 d1 a2 b2 c2 c2 d2 null null c4 d3 9.7. 连接查询的过滤条件 on 与 where 的区别总结 在连接查询中，过滤条件分为两种on与where，根据过滤条件使用的不同的关键字有不同的语义\nWHERE 子句中的过滤条件：不论是内连接还是外连接，凡是不符合 WHERE 子句中的过滤条件的记录都不会被加入最后的结果集。 ON 子句中的过滤条件： 对于外连接的驱动表的记录来说，如果无法在被驱动表中找到匹配ON子句中的过滤条件的记录，那么该记录仍然会被加入到结果集中，对应的被驱动表记录的各个字段使用NULL值填充。 对于内连接来说，MySQL 会把它和WHERE子句一样对待，也就是说：内连接中的WHERE子句和ON子句是等价的。 一般情况下，都把只涉及单表的过滤条件放到WHERE子句中，把涉及两表的过滤条件都放到ON子句中，也一般把放到ON子句中的过滤条件也称之为连接条件。\n9.8. 自连接查询 9.8.1. 自关联表概述 一张表，自关联一对多，数据表的外键列引用自身的主键列，自关联一般针对多级关系的使用\n省 \u0026ndash;\u0026gt; 市 \u0026ndash;\u0026gt; 区(县) \u0026ndash;\u0026gt; 镇(街道)\n老板 \u0026ndash;\u0026gt; 总经理 \u0026ndash;\u0026gt; 部门经理 \u0026ndash;\u0026gt; 主管 \u0026ndash;\u0026gt; 组长 \u0026ndash;\u0026gt; 员工\n9.8.2. 自关联表格式（创建外键） 创建表同时自关联主外键：\n1 2 3 4 5 6 create table 表名( 主键名 int primary key auto_increment, 其他列, 外键名(parent_主键名，一般这么写) int, constraint foreign key(parent_主键名) references 表名(主键名); ); 创建表后再关联主外键的格式：\n1 alter table 表名 add constraint foreign key(主键名) references 表名(parent_主键名); 注：最顶层的 parent_id 是 null\n1 2 3 4 5 6 7 8 9 10 -- Code Dome:一张表，自关联一对多 CREATE TABLE AREA( id int PRIMARY KEY auto_increment, NAME VARCHAR(50), description VARCHAR(100), parent_id int ); -- 自关联一对多 ALTER TABLE AREA ADD CONSTRAINT FOREIGN KEY (parent_id) REFERENCES AREA(id); 9.8.3. 自连接查询的概念 自连接查询：在数据查询时需要进行对表自身进行关联查询，即一张表自己和自己关联，一张表当成多张表来用。\n注意：\n自连接查询，本质还是使用到内连接或左连接或右连接。 自连接查询其实不需要依赖自关联表的外键约束的创建，无自关联外键约束也是可以进行自连接查询 注意自关联查询表时，必须给表起别名。 9.8.4. 自连接查询的格式 先创建自关联表 使用内连接(左连接、右连接) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 -- 自连接 -- 创建新员工表emp(员工工号,员工姓名,上级编号) CREATE TABLE emp ( id INT PRIMARY KEY, NAME VARCHAR (20), parent_id INT, -- 上级id CONSTRAINT FOREIGN KEY (parent_id) REFERENCES emp (id) ); -- 使用显式内连接 SELECT e.NAME 员工姓名, b.NAME 上司姓名 FROM emp e INNER JOIN emp b ON e.parent_id = b.id; -- 连接条件 -- 查询员工姓名和对应的上司姓名，没有上司的员工姓名也要显示出来。 -- 左外连接 SELECT e. NAME 员工姓名, b. NAME 上司姓名 FROM emp e LEFT JOIN emp b ON e.parent_id = b.id; -- 连接条件 9.9. 联合查询 UNION [ALL] 9.9.1. 概述 对于 union 查询，就是把多次查询的结果合并起来，形成一个新的查询结果集。语法格式如下：\n1 2 3 SELECT 字段列表 FROM 表A ... UNION [ ALL ] SELECT 字段列表 FROM 表B ....; 9.9.2. 基础使用示例 示例需求：将薪资低于 5000 的员工，和年龄大于 50 岁的员工全部查询出来\n对于此需求，可以直接使用多条件查询，使用逻辑运算符 or 连接即可。也可以通过 union/union all 来联合查询\n1 2 3 SELECT * FROM emp WHERE salary \u0026lt; 5000 UNION ALL SELECT * FROM emp WHERE age \u0026gt; 50; 由结果可知，union all 查询出来的结果仅仅进行简单的合并，并未去重。\n1 2 3 SELECT * FROM emp WHERE salary \u0026lt; 5000 UNION SELECT * FROM emp WHERE age \u0026gt; 50; 而 union 联合查询，会对查询出来的结果进行去重处理。\n9.9.3. 联合查询注意事项 对于联合查询的多张表的列数必须保持一致，字段类型也需要保持一致。否则在进行union/union all联合查询时，将会报错。如：\n9.9.4. union 与 union all 区别 union all：直接合并多个结果集，不会去除重复记录。 union：合并多个结果集，去除重复，所以执行效率会较差。 9.10. 全外连接查询（MySql 暂不支持，Oracle 支持、了解） 9.10.1. full join 语法定义 1 select r.*,s.* from r full join s on r.c=s.c A B C C D a1 b1 c1 c1 d1 a2 b2 c2 c2 d2 a3 b3 c3 null null null c4 d3 9.10.2. 使用 union 关键字实现全表连接查询 语法格式：\n1 2 3 select * from 表1 left outer join 表2 on 表1.字段名 = 表2.字段名 union select * from 表2 right outer join 表1 on 表2.字段名 = 表1.字段名; 注：union 关键字会去掉两个结果集的重复记录\n9.11. SQL 的其他 join 用法(网上资料) 9.11.1. OUTER JOIN（外连接） 1 2 3 4 5 SELECT \u0026lt; select_list \u0026gt; FROM Table_A A FULL OUTER JOIN Table_B B ON A. KEY = B. KEY 9.11.2. LEFT JOIN EXCLUDING INNER JOIN（左连接-内连接） 1 2 3 4 5 6 7 SELECT \u0026lt; select_list \u0026gt; FROM Table_A A LEFT JOIN Table_B B ON A. KEY = B. KEY WHERE B. KEY IS NULL 9.11.3. RIGHT JOIN EXCLUDING INNER JOIN（右连接-内连接） 1 2 3 4 5 6 7 SELECT \u0026lt; select_list \u0026gt; FROM Table_A A RIGHT JOIN Table_B B ON A. KEY = B. KEY WHERE A. KEY IS NULL 9.11.4. OUTER JOIN EXCLUDING INNER JOIN（外连接-内连接） 1 2 3 4 5 6 7 8 SELECT \u0026lt; select_list \u0026gt; FROM Table_A A FULL OUTER JOIN Table_B B ON A. KEY = B. KEY WHERE A. KEY IS NULL OR B. KEY IS NULL 9.12. straight_join 指定驱动表查询 9.12.1. 概述 straight_join 关键字功能与 join 关键字类似，区别在于，straight_join 可以指定左边的表来驱动右边的表，改变 MySQL 优化器对于联表查询的执行顺序。\n1 select * from t2 straight_join t1 on t2.a = t1.a; 以上语句代表指定 t2 表作为驱动表。\n9.12.2. 注意事项 straight_join 只适用于 inner join 的情况，并不适用于 left join，right join。（因为left join，right join已经指定了表的执行顺序，哪个表做为驱动表）\nTips: 建议少使用此关键字，尽可能使用优化器去选择的执行顺序，因为大部分情况下人为指定的执行顺序并不一定会比优化引擎选择的要更优。\n10. 子查询 10.1. 子查询概述 一条 SQL 语句(子查询)的查询结果做为另一条SQL语句(父语句)的条件或查询结果，这种操作则称为子查询。多条 SQL 语句嵌套使用，内部的 SQL 查询语句称为子查询。简单理解就是包含select嵌套的查询。\n例如：在一个查询语句 A 里的某个位置也可以有另一个查询语句 B，这个出现在 A 语句的某个位置中的查询 B 就被称为子查询，A 也被称之为外层查询。\n子查询外部的语句可以是INSERT/UPDATE/DELETE/SELECT的任何一个。\n10.2. 子查询语法使用位置 子查询可以在一个外层查询的各种位置出现。\n10.2.1. SELECT 子句 出现在select语句中\n1 SELECT (SELECT col FROM table LIMIT 1); 10.2.2. FROM 子句 出现在from子句中，可以把子查询的查询结果当作是一个表，但这种表与正常的创建的表不一样，MySQL 把这种由子查询结果集组成的表称之为派生表。\n1 SELECT m, n FROM (SELECT m2 + 1 AS m, n2 AS n FROM table2 WHERE m2 \u0026gt; 2) AS t; 10.2.3. WHERE 或 ON 子句 子查询可放在外层查询的WHERE子句或者ON子句中\n1 SELECT * FROM table1 WHERE m1 IN (SELECT m2 FROM table2); 示例查询表明想要将(SELECT m2 FROM table2)这个子查询的结果作为外层查询的IN语句参数，整个查询语句逻辑是找table1表中的某些记录，这些记录的 m1 列的值能在 table2 表的 m2 列找到匹配的值。\n10.2.4. ORDER BY 子句、GROUP BY 子句 子查询也可以出现ORDER BY 子句、GROUP BY 子句中。虽然语法支持，但没有意义。\n10.3. 按返回的结果集区分子查询类型 子查询本身也算是一个查询，所以可以按照它们返回的不同结果集类型，可以把这些子查询分为不同的类型：\n单行单列（标量子查询）：返回的是一个具体列的内容，可以理解为一个单值数据； 单行多列（行子查询）：返回一行数据中多个列的内容； 多行单列（列子查询）：返回多行记录之中同一列的内容，相当于给出了一个操作范围； 多行多列（表子查询）：查询返回的结果是一张临时表 10.3.1. 标量子查询 只返回一个单一值的子查询称之为标量子查询。这些标量子查询可以作为一个单一值或者表达式的一部分出现在查询语句的各个地方。父查询可以使用 =、 \u0026lt;、 \u0026gt; 等比较运算符\n1 2 SELECT (SELECT m1 FROM e1 LIMIT 1); SELECT * FROM e1 WHERE m1 = (SELECT MIN(m2) FROM e2); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- 先查询平均工资的值（单行子查询） SELECT AVG(salary) FROM employee; -- 再使用父查询判断小于平均值的员工 SELECT NAME 姓名, salary 工资 FROM employee WHERE salary \u0026lt; ( SELECT AVG(salary) FROM employee ); 10.3.2. 单行（多列）子查询 返回一条记录的子查询，不过这条记录需要包含多个列（只包含一个列就成了标量子查询了）\n1 SELECT * FROM e1 WHERE (m1, n1) = (SELECT m2, n2 FROM e2 LIMIT 1); sql语句的含义就是要从 e1 表中找一些记录，这些记录的 m1 和 n1 列分别等于子查询结果中的m2 和 n2 列。\n10.3.3. 单列（多行）子查询 多行子查询查询结果是多行单列的值，类似于一个数组（只包含一条记录就成了标量子查询了）。父查询使用 in 关键字的使用结果\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 -- 3) 查询大于 5000 的员工，来至于哪些部门，输出部门的名字 -- 先查询大于5000的员工名单（多行子查询） SELECT NAME 姓名 FROM employee WHERE salary \u0026gt; 5000; -- 查询大于5000的员工的部门名字 SELECT d. NAME 部门名称 FROM dept d WHERE d.id IN ( SELECT e.dept_id FROM employee e WHERE salary \u0026gt; 5000 ); -- 第2种方法使用内连接 SELECT e. NAME 员工姓名, e.salary 工资, d. NAME 部门名称 FROM employee e INNER JOIN dept d ON e.dept_id = d.id AND e.salary \u0026gt; 5000; -- 4) 查询开发部与财务部所有的员工信息，分别使用子查询和表连接实现 -- 使用多行子查询。查询开发部与财务部的部门ID SELECT d.id FROM dept d WHERE d. NAME IN (\u0026#39;开发部\u0026#39;, \u0026#39;财务部\u0026#39;); -- 使用多行子查询 SELECT * FROM employee e WHERE e.dept_id IN ( SELECT d.id FROM dept d WHERE d. NAME IN (\u0026#39;开发部\u0026#39;, \u0026#39;财务部\u0026#39;) ); -- 使用表连接查询 SELECT e.*, d. NAME FROM employee e INNER JOIN dept d ON e.dept_id = d.id WHERE d. NAME IN (\u0026#39;开发部\u0026#39;, \u0026#39;财务部\u0026#39;); 注：需要注意，如果在子查询定义过的别名，出了括号后，父查询就无法使用该别名，需要自己重新定义一个别名，如下例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 -- 先找到上过关羽课的学生id，当子查询结果，再查找不在结果内的学生 SELECT s.sid 学号, s.sname 姓名 FROM stu s WHERE s.sid NOT IN ( SELECT s.sid FROM stu s INNER JOIN stu_cou sc INNER JOIN course c INNER JOIN teacher t ON s.sid = sc.sid AND sc.cid = c.cid AND c.cid = t.cid WHERE t.tname = \u0026#39;关羽\u0026#39; ); 10.3.4. 表（多行多列）子查询 表子查询返回结果是一个多行多列的值，类似于一张虚拟表。不能用于 where 条件，用于 select 子句中做为子表。\n1 SELECT * FROM e1 WHERE (m1, n1) IN (SELECT m2, n2 FROM e2); 注意事项：如果子查询和表连接可以同时实现结果时，子查询的效率低于表连接查询，优先考虑使用表连接。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 -- 5) 查询 2011 年以后入职的员工信息和部门信息，分别使用子查询和表连接实现 -- 使用多列子查询。查询2011年后入职人员的全部信息 SELECT * FROM employee e WHERE e.join_date \u0026gt; \u0026#39;2011-01-01\u0026#39;; -- 使用多列子查询 SELECT e.*, d. NAME FROM dept d, ( SELECT * FROM employee e WHERE e.join_date \u0026gt; \u0026#39;2011-01-01\u0026#39; ) e WHERE d.id = e.dept_id; -- 使用内连接查询 SELECT e.*, d. NAME FROM employee e INNER JOIN dept d ON e.dept_id = d.id WHERE e.join_date \u0026gt; \u0026#39;2011-01-01\u0026#39;; 10.4. 按与外层查询关系来区分子查询类型 10.4.1. 不相关子查询 如果子查询可以单独运行出结果，而不依赖于外层查询的值，就可以把这个子查询称之为不相关子查询。上面示例基本上都是不相关子查询\n10.4.2. 相关子查询 如果子查询的执行需要依赖于外层查询的值，就可以把这个子查询称之为相关子查询。\n1 SELECT * FROM e1 WHERE m1 IN (SELECT m2 FROM e2 WHERE n1 = n2); 其中子查询(SELECT m2 FROM e2 WHERE n1 = n2)的查询条件n1是外层查询的列。也就是说子查询的执行需要依赖于外层查询的值，所以这个子查询就是一个相关子查询。\n10.5. [NOT] IN/ANY/SOME/ALL/EXISTS 子查询 对于列子查询和表子查询来说，它们的结果集中包含很多条记录，这些记录相当于是一个集合，所以就不能单纯的和另外一个操作数使用操作符来组成布尔表达式了，MySQL 通过下面的语法来支持某个操作数和一个集合组成一个布尔表达式：\nALL 关键字 ANY 关键字 SOME 关键字 IN/NOT IN 关键字 EXISTS 关键字 10.5.1. IN 或者 NOT IN 语法格式：\n1 select * from 表 where 字段名 [NOT] IN (子查询); 用来判断某个操作数在不在由子查询结果集组成的集合中\n1 SELECT * FROM e1 WHERE (m1, n1) IN (SELECT m2, n2 FROM e2); 10.5.2. ANY/SOME（ANY 和 SOME 是同义词） 语法格式：\n1 select * from 表 where 字段名 比较操作符 ANY/SOME(子查询); ANY/SOME 关键字的作用是，只要子查询结果集中存在某个值和给定的操作数做比较操作，比较结果为TRUE，那么整个表达式的结果就为TRUE，否则整个表达式的结果就为FALSE ANY/SOME 可以与=、\u0026gt;、\u0026gt;=、\u0026lt;、\u0026lt;=、\u0026lt;\u0026gt;结合是来使用，分别表示等于、大于、大于等于、小于、小于等于、不等于其中的其中的任何一个数据。 表示制定列中的值要大于子查询中的任意一个值，即必须要大于子查询集中的最小值。同理可以推出其它的比较运算符的情况。 SOME和ANY的作用一样，SOME可以理解为ANY的别名 示例：\n1 SELECT * FROM e1 WHERE m1 \u0026gt; ANY(SELECT m2 FROM e2); 查询示例的意思就是，对于 e1 表的某条记录的 m1 列的值来说，如果子查询(SELECT m2 FROM e2)的结果集中存在一个小于 m1 列的值，那么整个布尔表达式的值就是 TRUE，否则为 FALSE，也就是说只要 m1 列的值大于子查询结果集中最小的值，整个表达式的结果就是TRUE，所以上边的查询本质上等价于这个查询：\n1 SELECT * FROM e1 WHERE m1 \u0026gt; (SELECT MIN(m2) FROM e2); 另外，=ANY相当于判断子查询结果集中是否存在某个值和给定的操作数相等，它的含义和IN是相同的。\n10.5.3. ALL 语法格式：\n1 select * from 表 where 字段名 比较操作符 ALL(子查询); ALL关键字作用是，子查询结果集中所有的值和给定的操作数做比较操作比较结果为TRUE，那么整个表达式的结果就为TRUE，否则整个表达式的结果就为FALSE。 ALL 可以与=、\u0026gt;、\u0026gt;=、\u0026lt;、\u0026lt;=、\u0026lt;\u0026gt;结合是来使用，分别表示等于、大于、大于等于、小于、小于等于、不等于其中的其中的所有数据。 ALL 表示指定列中的值必须要大于子查询集的每一个值，即必须要大于子查询集的最大值；如果是小于号即小于子查询集的最小值。同理可以推出其它的比较运算符的情况。 示例：\n1 SELECT * FROM e1 WHERE m1 \u0026gt; ALL(SELECT m2 FROM e2); 查询示例的意思就是，对于 e1 表的某条记录的 m1 列的值来说，如果子查询(SELECT m2 FROM e2)的结果集中的所有值都小于 m1 列的值，那么整个布尔表达式的值就是 TRUE，否则为 FALSE，也就是说只要 m1 列的值大于子查询结果集中最大的值，整个表达式的结果就是 TRUE，所以上边的查询本质上等价于这个查询：\n1 SELECT * FROM e1 WHERE m1 \u0026gt; (SELECT MAX(m2) FROM e2); 10.5.4. EXISTS 子查询 10.5.4.1. 基础使用 1 select * from 表A where exists(子查询语句); EXISTS 的作用是，将主查询表A的每一行数据，放到子查询中作为筛选条件，然后根据子查询中的结果（true 或 false）来决定判断主查询的数据是否保留。如果仅仅需要判断子查询的结果集中是否有记录，而不在乎它的记录具体值，可以使用把 EXISTS 或者 NOT EXISTS 放在子查询语句前边。\nEXISTS(subquery) 只返回 TRUE 或 FALSE，因此子查询中的 SELECT * 也可以用 SELECT 1 替换，官方说法是实际执行时会忽略 SELECT 的清单，因此两者没有区别 该子查询如果“有数据结果”(至少返回一行数据)，则该EXISTS()的结果为“true”，外层查询执行 该子查询如果“没有数据结果”（没有任何数据返回），则该EXISTS()的结果为“false”，外层查询不执行 EXISTS 后面的子查询不返回任何实际数据，只返回真或假，当返回真时 where 条件成立 EXISTS 子查询的实际执行过程可能经过了优化，而不是逐条对比 EXISTS 子查询也可以用 JOIN 来代替，但需要具体问题具体分析才能决定哪种方式最优 1 2 3 4 5 6 7 SELECT * FROM e1 WHERE EXISTS (SELECT 1 FROM e2); -- 查询公司是否有大于60岁的员工，有则输出 select * from emp3 a where exists(select * from emp3 b where a.age \u0026gt; 60); -- 查询有所属部门的员工信息 select * from emp3 a where exists(select * from dept3 b where a.dept_id = b.deptno); 对于子查询(SELECT 1 FROM e2)来说，如果并不关心这个子查询最后到底查询出的结果是什么，所以查询列表里填*、某个列名，或者其他内容都无所谓，真正关心的是子查询的结果集中是否存在记录。也就是说只要(SELECT 1 FROM e2)这个查询中有记录，那么整个EXISTS表达式的结果就为TRUE。\n10.5.4.2. in 和 exists 子查询的区别 mysql 中的 in 语句是把外表和内表作 hash 连接，而 exists 语句是对外表作 loop循环，每次loop循环再对内表进行查询。exists 语句在某些条件下的执行效率比 in 语句高：\n如果查询的两个表大小相当，那么用 in 和 exists 差别不大。 如果两个表中一个较小，一个是大表，则子查询表大的用 exists，子查询表小的用 in。 not in 和 not exists 比较：如果查询语句使用了 not in，那么内外表都进行全表扫描，没有用到索引；而 not extsts 的子查询依然能用到表上的索引。所以无论那个表大，用 not exists 都比 not in 要快。 Tips: 在实际开发中，特别是大数据量时，推荐使用 EXISTS 关键字\n10.6. 子查询的注意事项 子查询语句一定要使用括号括起来，否则无法确定子查询语句什么时候结束。 在SELECT子句中的子查询必须是标量子查询，如果子查询结果集中有多个列或者多个行，都不允许放在SELECT子句中，在想要得到标量子查询或者行子查询，但又不能保证子查询的结果集只有一条记录时，应该使用LIMIT 1语句来限制记录数量。 对于[NOT] IN/ANY/SOME/ALL子查询来说，子查询中不允许有LIMIT语句，而且这类子查询中ORDER BY子句、DISTINCT语句、没有聚集函数以及HAVING子句的GROUP BY子句没有什么意义。因为子查询的结果其实就相当于一个集合，集合里的值排不排序等一点儿都不重要。 不允许在一条语句中增删改某个表的记录时同时还对该表进行子查询。 11. DCL语句使用(了解) 11.1. DCL概述 DCL 英文全称是 Data Control Language(数据控制语言)，用来管理数据库用户的创建和删除、控制用户的数据库访问权限。\n11.2. 管理用户 11.2.1. 查询用户 mysql的用户信息保存在 mysql.user\n1 select * from mysql.user; 查询的结果如下:\n其中 Host 代表当前用户访问的主机，如果为 localhost，仅代表只能够在当前本机访问，是不可以远程访问的。User 代表的是访问该数据库的用户名。在 MySQL 中需要通过 Host 和 User 来唯一标识一个用户。\n11.2.2. 创建用户 1 CREATE USER \u0026#39;用户名\u0026#39;@\u0026#39;主机名\u0026#39; IDENTIFIED BY \u0026#39;密码\u0026#39;; Notes:\n在 MySQL 中需要通过用户名@主机名的方式，来唯一标识一个用户。 “主机名”表示创建的用户使用的IP地址，可以设置为localhost(代表仅允许本机)或者'%'（代表允许所有IP地址登录） 1 2 3 4 5 -- 创建用户 moon, 只能够在当前主机localhost访问, 密码123456; create user \u0026#39;moon\u0026#39;@\u0026#39;localhost\u0026#39; identified by \u0026#39;123456\u0026#39;; -- 创建用户 zero, 可以在任意主机访问该数据库, 密码123456; create user \u0026#39;zero\u0026#39;@\u0026#39;%\u0026#39; identified by \u0026#39;123456\u0026#39;; 11.2.3. 修改用户密码 1 ALTER USER \u0026#39;用户名\u0026#39;@\u0026#39;主机名\u0026#39; IDENTIFIED WITH mysql_native_password BY \u0026#39;新密码\u0026#39;; 示例：\n1 alter user \u0026#39;moon\u0026#39;@\u0026#39;%\u0026#39; identified with mysql_native_password by \u0026#39;1234\u0026#39;; 11.2.4. 删除用户 1 DROP USER \u0026#39;用户名\u0026#39;@\u0026#39;主机名\u0026#39;; 示例：\n1 drop user \u0026#39;moon\u0026#39;@\u0026#39;localhost\u0026#39;; 11.3. 用户权限控制 11.3.1. 常用权限 MySQL 中定义了很多种权限，但是常用的就以下几种：\n权限 说明 ALL, ALL PRIVILEGES 所有权限 SELECT 查询数据 INSERT 新增数据 UPDATE 修改数据 DELETE 删除数据 ALTER 修改表 DROP 删除数据库/表/视图 CREATE 创建数据库/表 上述只是简单罗列了常见的几种权限描述，其他权限描述及含义，可以直接参考MySQL 8.0 版本官方文档\n11.3.2. 查询用户权限 1 SHOW GRANTS FOR \u0026#39;用户名\u0026#39;@\u0026#39;主机名\u0026#39;; 示例：\n1 2 -- 查询 \u0026#39;zero\u0026#39;@\u0026#39;%\u0026#39; 用户的权限 show grants for \u0026#39;zero\u0026#39;@\u0026#39;%\u0026#39;; 11.3.3. 用户授权 创建用户之后，可以使用新用户进行登录，查看数据库只有系统自带的数据库，想要操作自己创建的数据库还需要root用户对新用户进行授权。语法：\n1 GRANT 权限列表 ON 数据库名.表名 TO \u0026#39;用户名\u0026#39;@\u0026#39;主机名\u0026#39;; 示例：\n1 2 3 4 5 6 7 8 9 10 11 -- 授予zero用户temp_db数据库所有表的多个权限 grant delete,insert,update on temp_db.* from \u0026#39;zero\u0026#39;@\u0026#39;*\u0026#39;; -- 授予zero用户temp_db数据库所有表的某个权限 grant delete on temp_db.* from \u0026#39;zero\u0026#39;@\u0026#39;*\u0026#39;; -- 授予zero用户temp_db数据库所有表的所有权限 grant all on temp_db.* from \u0026#39;zero\u0026#39;@\u0026#39;*\u0026#39;; -- 授予moon用户所有数据库所有表的所有权限 grant all on *.* from \u0026#39;moon\u0026#39;@\u0026#39;localhost\u0026#39;; Notes:\n多个权限之间，使用英文逗号,分隔 授权时，数据库名和表名均可使用*进行通配，代表所有 11.3.4. 撤销权限 当需要限制新用户操作数据库的权限时，root 用户可以撤销已授予用户的某些权限。语法：\n1 REVOKE 权限列表 ON 数据库名.表名 FROM \u0026#39;用户名\u0026#39;@\u0026#39;主机名\u0026#39;; 示例：\n1 2 3 4 5 6 7 8 -- 撤销zero用户temp_db数据库所有表的多个权限 revoke delete,insert,update on temp_db.* from \u0026#39;zero\u0026#39;@\u0026#39;*\u0026#39;; -- 撤销zero用户temp_db数据库所有表的某个权限 revoke delete on temp_db.* from \u0026#39;zero\u0026#39;@\u0026#39;*\u0026#39;; -- 撤销zero用户temp_db数据库所有表的所有权限 revoke all on temp_db.* from \u0026#39;zero\u0026#39;@\u0026#39;*\u0026#39;; 12. 数据的约束 12.1. 数据约束概述 约束（constraint），是作用于表中字段上的规则，实质就是对表中存储的数据进行限制，表在设计和创建的时候加入约束的目的就是为了保证表中的记录完整性、有效性和准确性。\n数据完整性分为以下几类：\n实体完整性：规定表的每一行在表中是惟一的实体。 域完整性：是指表中的列必须满足某种特定的数据类型约束，其中约束又包括取值范围、精度等规定。 参照完整性：是指两个表的主关键字和外关键字的数据应一致，保证了表之间的数据的一致性，防止了数据丢失或无意义的数据在数据库中扩散。 用户定义的完整性：不同的关系数据库系统根据其应用环境的不同，往往还需要一些特殊的约束条件。用户定义的完整性即是针对某个特定关系数据库的约束条件，它反映某一具体应用必须满足的语义要求。与表有关的约束：包括列约束(NOT NULL（非空约束）)和表约束(PRIMARY KEY、foreign key、check、UNIQUE) 。 12.1.1. 约束种类 约束 描述 关键字 默认约束 保存数据时，如果未指定该字段的值，则采用默认值 DEFAULT 主键约束 主键是一行数据的唯一标识，要求非空且唯一 PRIMARY KEY 唯一约束 保证该字段的所有数据都是唯一、不重复的 UNIQUE 非空约束 限制该字段的数据不能为null NOT NULL 检查约束 8.0.16 版本之后新增，保证字段值满足某一个条件 CHECK 外键约束 用来让两张表的数据之间建立连接，保证数据的一致性和完整性 FOREIGN KEY 自增长约束 auto_increment 零填充约束 zerofill 扩展：还有一种叫“检查约束”，但 MySQL 不支持，Oracle 支持\n12.1.2. 约束添加时机 创建表结构的同时添加约束（推荐） 创建完表结构之后添加（不推荐）。如果创建完之后再添加约束，可能会添加失败。因为已有的数据可能不符合即将要添加的约束。 12.2. 默认值约束 (default) 默认约束，如果这个字段没有输入任何的值，则数据库使用默认的值\n12.2.1. 定义与语法 在创建表时，指定默认约束，关键字：default 1 2 3 4 create table 表名 ( 列名 数据类型(长度) default 默认值, .... ); 创建表后，修改字段的默认约束 1 alter table 表名 modify 字段名 数据类型(长度) default 默认值; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 -- 创建一个学生表 s1，字段：(编号，姓名，地址（默认值是：广州)），插入 2 条记录，地址使用默认值。 create table st1 ( id int, name varchar(10), -- 默认值 address varchar(20) default \u0026#39;广州\u0026#39; ) -- 写法一：只插入前面 2 列，第 3 列不写 insert into st1 (id,name) values (10, \u0026#39;猪八戒\u0026#39;); select * from st1; -- 写法二：VALUES 前面的列名不写 insert into st1 values (20, \u0026#39;猪九戒\u0026#39;,default); -- 使用自己的值 insert into st1 values (10, \u0026#39;小猪\u0026#39;, \u0026#39;珠海\u0026#39;); -- 如果第 3 列使用 NULL 的常量，会不会插入默认值呢？ insert into st1 values (10, \u0026#39;小猪\u0026#39;, null); 12.2.2. 删除默认约束 删除默认约束只需要将默认值修改为null即可\n1 alter table 表名 modify column 字段名 数据类型(长度) default null; 12.3. 非空约束 (not null) 12.3.1. 定义非空约束 非空约束：约束某一列的值不能为空(null)，必须有值，但可以插入空字符。对于使用了非空约束的字段，如果用户在添加数据时没有指定值，数据库系统就会报错。\n在创建表时，指定非空约束，关键字：not null 1 2 3 4 create table 表名 ( 列名 数据类型(长度) not null, .... ); 创建表后，修改字段为非空约束 1 alter table 表名 modify 字段 数据类型(长度) not null; 示例：\n1 2 3 4 5 6 7 8 9 10 -- 示例：创建表学生表 s2，字段(id，name, gender)，其中姓名不能为 null create table s2 ( id int, name varchar(10) not null, -- 非空 gender char(1) default \u0026#39;男\u0026#39; ) -- 不赋值：第 2 列不写 Column \u0026#39;name\u0026#39; cannot be null insert into s2 (id,name,gender) values (1,null,\u0026#39;女\u0026#39;); insert into s2 (id,gender) values (1,\u0026#39;女\u0026#39;); select * from s2; 12.3.2. 删除非空约束 删除非空约束，其实就是使用alter关键字修改字段的约束，去掉not null即可\n12.3.3. Mysql 允许 null 与 default 值 分为下面4种情况：\n允许null，指定default值。 允许null，不指定default，这个时候可认为default值就是null 不允许null，指定default值，不能指定default值为null，否则报错 Invalid default value for xxx 不允许null，不指定default值。这种情况，新增的时候，必须指定值。否则报错 Field xxx doesn't have a default value 12.3.4. 字段为什么建议定义为 not null MySQL 官网说明:\nNULL columns require additional space in the rowto record whether their values are NULL. For MyISAM tables, each NULL columntakes one bit extra, rounded up to the nearest byte.\nnull 值会占用更多的字节，且会在程序中造成很多与预期不符的情况。\n12.4. 唯一约束 (unique) 12.4.1. 定义与语法 唯一约束，是指所有记录中某一列的数据不允许出现重复值\n在创建表时，指定唯一约束，关键字：unique 1 2 3 4 create table 表名 ( 列名 数据类型(长度) unique, .... ); 创建表后，修改字段为唯一约束 1 alter table 表名 add constraint 约束名 unique(列名); 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 -- 创建学生表 s3，列(id,name)，学生姓名这一列设置成唯一约束，即不能出现同名的学生。 create table s3 ( id int, name varchar(10) unique -- 唯一约束 ); insert into s3 values (1,\u0026#39;Jack\u0026#39;); select * from s3; -- 插入相同的名字： Duplicate entry \u0026#39;Jack\u0026#39; for key \u0026#39;name\u0026#39; insert into s3 values (2,\u0026#39;Jack\u0026#39;); -- 问：出现多个 null 的时候会怎样？因为 null 是没有值，所以不存在重复的问题。 insert into s3 values(3,null); insert into s3 values(4,null); 12.4.2. 删除唯一约束 删除唯一约束的语法：\n1 alter table 表名 drop index 唯一约束名; 注：如果创建唯一约束时没有指定名称，则字段名就是唯一约束名称。\n12.4.3. 注意事项 可以出现多个null，因为null是表示没有任何内容，就没有重复的说法 不可以出现多个空字符，因为空字符也是有内容，所以不能同时出现多个空字符 12.5. 主键约束 (primary key) 12.5.1. 概念 MySQL 主键约束是一个列或者多个列的组合，用于唯一标识表中的一条数据，方便在RDBMS中尽快的找到某一行。每一张表都最多只能允许有一个主键。\n主键约束相当于“唯一约束 + 非空约束”的组合，主键约束列不允许重复，也不允许出现空值。当创建主键的约束时，系统默认会在所在的列和列组合上建立对应的唯一索引。\n主键约束的关键字：\nprimary key：保证列的数据非空，唯一 primary key auto_increment：让主键列数据，实现自动增长 主键设计原则：\n主键列一般是选择对用户没有任何意义的数据。只是用于开发时标识当前记录。 主键列的值一般是由数据库或计算机生成。 主键值生成后，一般不建议修改 12.5.2. 创建单列主键 创建单列主键有两种方式，一种是在定义字段的同时指定主键，一种是定义完字段之后指定主键\n在创建表时创建主键，在字段后面加上 primary key，语法格式： 1 2 3 4 create table tablename( 列名 数据类型(长度) primary key, ....... ) 在创建表时创建主键，在表创建的最后来指定主键，语法格式： 1 2 3 4 create table tablename( ......., constraint 主键名称 primary key(列名) ) 注：上面语法中 constraint 主键名称 是可以省略\n12.5.3. 添加多列主键(联合主键） 联合主键，是由一张表中多个字段组成的。注意事项：\n当主键是由多个字段组成时，不能直接在字段名后面声明主键约束。 一张表只能有一个主键，联合主键只算是一个主键。即有联合主键后，不能再创建单列主键 创建联合主键后，相关每个列的值都不能空 1 2 3 4 create table 表名( ..., constraint 主键名称 primary key (字段1, 字段2, …, 字段n) ); 注：上面语法中 constraint 主键名称 是可以省略\n12.5.4. 通过修改表结构添加主键 主键约束不仅可以在创建表的同时创建，也可以在修改表结构时添加。\n1 alter table 表名 add primary key(字段1, 字段2, ....); 12.5.5. 删除主键 一个表中不需要主键约束时，可以从表中将其删除。删除指定表格的主键语法：\n1 alter table 表名 drop primary key; 示例：\n1 2 -- 删除 sort 表的主键 alter table sort drop primary key; Tips: 因为表只有一个主键，所以删除时不需要指定主键名\n12.5.6. 设置主键自动增长 在 MySQL 中，当主键定义为自增长后，由数据库系统根据定义自动赋值。每增加一条记录，主键会自动以相同的步长进行增长。\n一般主键是自增长的字段，不需要指定。 实现添加自增长语句，主键字段后加 auto_increment 关键字(只适用MySQL) 1 2 3 4 5 -- 创建分类表 CREATE TABLE sort ( sid INT PRIMARY KEY auto_increment, -- 分类ID sname VARCHAR(100) -- 分类名称 ); 12.6. 自增长字段 ( auto_increment ) 12.6.1. 自增长约束 创建表时，指定自增长约束的语法： 1 2 3 4 create table 表名( 列名 数值类型(长度) primary key auto_increment, .... ); 创建表时，指定自增长字段初始值的语法： 1 2 3 4 create table 表名 ( 字段名 int primary key auto_increment, .... ) auto_increment = 初始值; 创建表后，修改自增长起始值： 1 alter table 表名 AUTO_INCREMENT = 新的起始值; 12.6.2. 自增长约束特点 默认情况下，auto_increment 的初始值是 1，每新增一条记录，字段值自动加 1。 一个表中只能有一个字段使用 auto_increment 约束，且该字段必须有唯一索引，以避免序号重复（即为主键或主键的一部分）。 auto_increment 约束的字段必须具备 NOT NULL 属性。 auto_increment 约束的字段只能是整数类型（TINYINT、SMALLINT、INT、BIGINT 等。 auto_increment 约束字段的最大值受该字段的数据类型约束，如果达到上限，auto_increment 就会失效。 12.6.3. delete 和 truncate 删除后自增列的变化 delete 数据之后自动增长从断点开始 truncate 数据之后自动增长从默认起始值开始 12.7. 零填充 如果某一数值列的值不满指定的位数，可以设置在列的值前面使用零填充。在数据类型的后面使用 zerofill 关键字：\n1 2 3 4 create table 表名 ( 列名 数值类型(长度) zerofill, .... ); 作用如果数据的位数是4位，则使用0进行填充整个4位。\n注：当使用 zerofill 时，默认会自动加unsigned（无符号）属性，使用unsigned属性后，数值范围是原值的2倍，例如，有符号为-128~+127，无符号为0~256。\n12.8. 外键约束 12.8.1. 定义 MySQL 外键约束（FOREIGN KEY）是表的一个特殊字段，经常与主键约束一起使用。对于两个具有关联关系的表而言，相关联字段中主键所在的表就是主表（父表），外键所在的表就是从表（子表）。\n外键的主要目的是，用来建立主表与从表的关联关系，为两个表的数据建立连接，约束两个表中数据的一致性和完整性。\n例如：左侧的 emp 表是员工表，里面存储员工的基本信息，包含员工的ID、姓名、年龄、职位、薪资、入职日期、上级主管ID、部门ID，在员工的信息中存储的是部门的ID dept_id，而这个部门的 ID 是关联的部门表 dept 的主键 id，那 emp 表的 dept_id 就是外键，关联的是另一张表的主键。\nNotes: 目前上述两张表，只是在逻辑上存在这样一层关系；在数据库层面，并未建立外键关联，所以是无法通过数据库本身来保证数据的一致性和完整性的。\n外键的优缺点：\n优点：由数据库自身保证数据一致性，完整性，更可靠，因为程序很难 100% 保证数据的完整性，而用外键即使在数据库服务器当机或者出现其他问题的时候，也能够最大限度的保证数据的一致性和完整性。有主外键的数据库设计可以增加 ER 图的可读性，这点在数据库设计时非常重要。外键在一定程度上说明的业务逻辑，会使设计周到具体全面。 缺点：可以用触发器或应用程序保证数据的完整性；过分强调或者说使用外键会增加开发难度，导致表过多，更改业务困难，扩展困难等问题；不用外键时数据管理简单，操作方便，性能高（导入导出等操作，在 insert, update, delete 数据的时候更快）。 12.8.2. 外键需遵守的规则 定义一个外键时，需要遵守下列规则：\n为了避免大量重复的数据出现，数据冗余。就需要使用到外键约束。 从表的某一列值(外键列)和主表的主键值相关关联，外键列的值必须来源于主表的主键值 主表：约束别人，表结构不变；副表/从表：被别人约束 主表必须已经存在于数据库中，或者是当前正在创建的表。 主表必须定义主键。 主键不能包含空值，但允许在外键中出现空值。也就是说，只要外键的每个非空值出现在指定的主键中，这个外键的内容就是正确的。 在主表的表名后面指定列名或列名的组合。这个列或列的组合必须是主表的主键或候选键。 外键中列的数目必须和主表的主键中列的数目相同。 外键中列的数据类型必须和主表主键中对应列的数据类型相同。 注：定义外键的时候，外键的约束比较和主键完全一致才能成功关联\n12.8.3. 外键约束语法格式1(创建表时定义) 在 create table 语句中，通过 foreign key 关键字来指定外键，具体的语法格式如下：\n1 constraint [外键名] foreign key(外键列名1, 外键列名2, ....) references 主表(主键列名1, 主键名列2, ...); 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 create table employee( id int primary key auto_increment, emp_name varchar(20), dept_id int, -- 部门 id -- 定义一个外键 constraint employee_dept_fk foreign key(dept_id) references dept(id) -- 声明 外键名称 外键(员工表中的列) 引用 部门表(部门表 id 主键) ) -- ******** 或者： ********* create table employee ( id int primary key auto_increment, emp_name varchar(10), dept_id int, -- 这里有逗号，没有 constraint 和名字 foreign key (dept_id) references dept(id) -- 外键，关联部门表(部门表的主键) ) 12.8.4. 外键约束语法格式2(创建表后再定义) 外键约束也可以在修改表时添加，但是添加外键约束的前提是：从表中外键列中的数据必须与主表中主键列中的数据一致或者是没有数据。\n1 alter table 表名 add constraint foreign key(外键名) references 主表(主键名); 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -- 创建部门表 create table if not exists dept2( deptno varchar(20) primary key , -- 部门号 name varchar(20) -- 部门名字 ); -- 创建员工表 create table if not exists emp2( eid varchar(20) primary key , -- 员工编号 ename varchar(20), -- 员工名字 age int, -- 员工年龄 dept_id varchar(20) -- 员工所属部门 ); -- 创建外键约束 alter table emp2 add constraint dept_id_fk foreign key(dept_id) references dept2 (deptno); 12.8.5. 在外键约束下的数据操作 外键约束设计插入数据的顺序：先插入主表、再插入副表 外键约束设计更新数据的顺序：先修改从表的外键数据，再修改主表的主键数据。 外键约束设计删除数据的顺序：先修改从表的外键数据，再修改主表的主键数据。 12.8.6. 删除外键约束 当一个表中不需要外键约束时，就需要从表中将其删除。外键一旦删除，就会解除主表和从表间的关联关系。语法：\n1 alter table 表名 drop foreign key 外键约束名; 示例：\n1 alter table emp2 drop foreign key dept_id_fk; 12.8.7. 级联操作 在修改和删除主表的主键值时，同时更新或删除从表的外键值，称为级联操作。具体的删除/更新行为有以下几种:\n行为 说明 NO ACTION 当在父表中删除/更新对应记录时，首先检查该记录是否有对应外键，如果有则不允许删除/更新。(与RESTRICT一致)默认行为 RESTRICT 当在父表中删除/更新对应记录时，首先检查该记录是否有对应外键，如果有则不允许删除/更新。(与NO ACTION一致)默认行为 CASCADE 当在父表中删除/更新对应记录时，首先检查该记录是否有对应外键，如果有，则也删除/更新外键在子表中的记录。 SET NULL 当在父表中删除对应记录时，首先检查该记录是否有对应外键，如果有则设置子表中该外键值为null（这就要求该外键允许取null）。 SET DEFAULT 父表有变更时，子表将外键列设置成一个默认的值 (Innodb不支持) 12.8.7.1. CASCADE CASCADE 外键级联操作(更新和删除)的语法格式：\n1 constraint foreign key(外键名) references 主表(主键名) ON UPDATE CASCADE ON DELETE CASCADE; 级联更新：更新主表的主键值时自动更新从表的相关的外键值，关键字：on update cascade 级联删除：删除主表的主键的记录时自动删除从表中的相关的数据，关键字：on delete cascade。实际开发中，级联删除不常用，需要谨慎使用 测试将修改父表id为1的记录修改为6，\n原来在子表中 dept_id 值为1的记录都修改为6了，这就是cascade级联更新\nTips: 在一般的业务系统中，不会修改一张表的主键值。\n测试删除父表id为6的记录\n父表的数据删除成功了，相应子表中关联的记录也被级联删除了。\n12.8.7.2. SET NULL SET NULL 外键级联操作(更新和删除)的语法格式：\n1 constraint foreign key(外键名) references 主表(主键名) on update set null on delete set null; 级联更新：更新主表的主键值时自动更新从表的相关的外键值，关键字：on update set null 级联删除：删除主表的主键的记录时自动删除从表中的相关的数据，关键字：on delete set null。 测试删除主表id为1的数据\n主表的记录是可以正常的删除，而子表 emp 的 dept_id 字段，原来 dept_id 为 1 的数据，现在都被置为NULL了。\n以上就是 SET NULL 这种删除/更新行为的级联效果。\n13. MySQL 扩展内容 13.1. 系统变量 13.1.1. 简介 系统变量又分为全局变量与会话变量\n全局变量在MYSQL启动的时候由服务器自动将它们初始化为默认值，这些默认值可以通过更改my.ini这个文件来更改。 会话变量在每次建立一个新的连接的时候，由MYSQL来初始化。MYSQL会将当前所有全局变量的值复制一份。来做为会话变量。也就是说，如果在建立会话以后，没有手动更改过会话变量与全局变量的值，那所有这些变量的值都是一样的。 全局变量与会话变量的区别就在于，对全局变量的修改会影响到整个服务器，但是对会话变量的修改，只会影响到当前的会话（也就是当前的数据库连接）。\n有些系统变量的值是可以利用语句来动态进行更改的，但是有些系统变量的值却是只读的，对于那些可以更改的系统变量，可以利用set语句进行更改。\n13.1.2. 系统变量-全局变量 由系统提供，在整个数据库有效。\n语法格式：\n1 @@global.全局变量名称 示例：\n1 2 3 4 5 6 7 -- 查看全局变量 show global variables; -- 查看某全局变量 select @@global.auto_increment_increment; -- 修改全局变量的值 set global sort_buffer_size = 40000; set @@global.sort_buffer_size = 40000; 13.1.3. 系统变量-会话变量 由系统提供，当前会话（连接）有效\n语法格式：\n1 @@session.会话变量名称 示例：\n1 2 3 4 5 6 7 -- 查看会话变量 show session variables; -- 查看某会话变量 select @@session.auto_increment_increment; -- 修改会话变量的值 set session sort_buffer_size = 50000; set @@session.sort_buffer_size = 50000; 13.2. MySQL 的 pymysql 操作 PyMySQL 是一个纯 Python 实现的 MySQL 客户端库，支持兼容 Python 3，用于代替 MySQLdb。\n13.2.1. 查询示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import pymysql # 获取MySQL连接 conn = pymysql.connect(host=\u0026#39;localhost\u0026#39;, port=3306, user=\u0026#39;root\u0026#39;,password=\u0026#39;123456\u0026#39;,database=\u0026#39;mydb17_pymysql\u0026#39;, charset=\u0026#39;utf8\u0026#39;) # 获取游标 cursor = conn.cursor() # 执行SQL语句，返回值就是SQL语句在执行过程中影响的行数 sql = \u0026#34;select * from student;\u0026#34; row_count = cursor.execute(sql) print(\u0026#34;SQL 语句执行影响的行数%d\u0026#34; % row_count) # 取出结果集中一行,返回的结果是一行 # print(cursor.fetchone()) # 取出结果集中的所有数据,返回一行数据 for line in cursor.fetchall(): print(line) # 关闭游标 cursor.close() # 关闭连接 conn.close() 13.2.2. 增删改示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import pymysql #获取MySQL连接 conn = pymysql.connect(host=\u0026#39;localhost\u0026#39;, port=3306, user=\u0026#39;root\u0026#39;,password=\u0026#39;123456\u0026#39;,database=\u0026#39;mydb17_pymysql\u0026#39;, charset=\u0026#39;utf8\u0026#39;) # 获取游标 cursor = conn.cursor() #插入数据 # sql = \u0026#34;insert into student values(%s,%s,%s)\u0026#34; # data = (4, \u0026#39;晁盖\u0026#39;, 34) # cursor.execute(sql, data) #sql和data之间以\u0026#34;,\u0026#34;隔开 # 修改数据 # sql = \u0026#34;update student set sname=%s where sid=%s\u0026#34; # data = (\u0026#39;李逵\u0026#39;, 4) # cursor.execute(sql, data) # 删除数据 sql = \u0026#34;delete from student where sid=%s\u0026#34; data = (4) cursor.execute(sql, data) conn.commit() # 提交，不然无法保存插入或者修改的数据(这个一定不要忘记加上) # 关闭游标 cursor.close() # 关闭连接 conn.close() 13.3. MySQL 数据库的伪表 DUAL 与 Oracle 数据库的伪表 DUAL 一样的用法\n13.4. 在 Unix 和 MySQL 时间戳之间进行转换 UNIX_TIMESTAMP 是从 MySQL 时间戳转换为 Unix 时间戳的命令 FROM_UNIXTIME 是从 Unix 时间戳转换为 MySQL 时间戳的命令 13.5. MySQL_fetch_array 和 MySQL_fetch_object 的区别 MySQL_fetch_array – 将结果行作为关联数组或来自数据库的常规数组返回。 MySQL_fetch_object – 从数据库返回结果行作为对象。 13.6. 如何存储 IP 地址（待整理） 使用字符串。此方式无法使用范围查询。 使用无符号整型。只占用 4 个字节的空间，并且此方式可以支持范围查询，传统的 IP 地址使用 INET_ATON() 和 INET_NTOA()；ipv6 使用 INET6_ATON() 和 INET6_NTOA()。 13.7. SQL 注入 SQL 注入产生的原因：程序开发过程中不注意规范书写 sql 语句和对特殊字符进行过滤，导致客户端可以通过全局变量 POST 和 GET 提交一些 sql 语句正常执行。\n防止 SQL 注入的方案：\n开启 MySQL 配置文件中的 magic_quotes_gpc 和 magic_quotes_runtime 设置。 Sql 语句书写尽量不要省略双引号和单引号。 在程序中过滤掉 sql 语句中的一些关键词。如：update、insert、delete、select、* 等。 执行 sql 语句时使用 addslashes() 进行 sql 语句转换。 提高数据库表和字段的命名技巧，对一些重要的字段根据程序的特点命名，不易被攻击者猜中。 ","permalink":"https://ktzxy.top/posts/524m1ezypi/","summary":"MySQL基础","title":"MySQL基础"},{"content":"一. 文件和目录 cd 命令，用于切换当前目录，它的参数是要切换到的目录的路径，可以是绝对路径，也可以是相对路径。\n1 2 3 4 5 6 cd /home 进入 \u0026#39;/ home\u0026#39; 目录 cd .. 返回上一级目录 cd ../.. 返回上两级目录 cd 进入个人的主目录 cd ~user1 进入个人的主目录 cd - 返回上次所在的目录 pwd 命令，显示工作路径\n1 2 [root@mailvip ~]# pwd /root ls 命令，查看文件与目录的命令，list 之意\n1 2 3 4 5 ls 查看目录中的文件 ls -l 显示文件和目录的详细资料 ls -a 列出全部文件，包含隐藏文件 ls -R 连同子目录的内容一起列出（递归列出），等于该目录下的所有文件都会显示出来 ls [0-9] 显示包含数字的文件名和目录名 cp 命令，用于复制文件，copy 之意，它还可以把多个文件一次性地复制到一个目录下\n1 2 3 4 5 -a ：将文件的特性一起复制 -p ：连同文件的属性一起复制，而非使用默认方式，与-a相似，常用于备份 -i ：若目标文件已经存在时，在覆盖时会先询问操作的进行 -r ：递归持续复制，用于目录的复制行为 //经常使用递归复制 -u ：目标文件与源文件有差异时才会复制 mv 命令，用于移动文件、目录或更名，move 之意\n1 2 3 -f ：force强制的意思，如果目标文件已经存在，不会询问而直接覆盖 -i ：若目标文件已经存在，就会询问是否覆盖 -u ：若目标文件已经存在，且比目标文件新，才会更新 rm 命令，用于删除文件或目录，remove 之意\n1 2 3 -f ：就是force的意思，忽略不存在的文件，不会出现警告消息 -i ：互动模式，在删除前会询问用户是否操作 -r ：递归删除，最常用于目录删除，它是一个非常危险的参数 二、查看文件内容 cat 命令，用于查看文本文件的内容，后接要查看的文件名，通常可用管道与 more 和 less 一起使用\n1 2 3 4 5 6 7 8 9 10 cat file1 从第一个字节开始正向查看文件的内容 tac file1 从最后一行开始反向查看一个文件的内容 cat -n file1 标示文件的行数 more file1 查看一个长文件的内容 head -n 2 file1 查看一个文件的前两行 tail -n 2 file1 查看一个文件的最后两行 tail -n +1000 file1 从1000行开始显示，显示1000行以后的 cat filename | head -n 3000 | tail -n +1000 显示1000行到3000行 cat filename | tail -n +3000 | head -n 1000 从第3000行开始，显示1000(即显示3000~3999行) 三. 文件搜索 find 命令，用来查找系统的\n1 2 3 4 5 6 find / -name file1 从 \u0026#39;/\u0026#39; 开始进入根文件系统搜索文件和目录 find / -user user1 搜索属于用户 \u0026#39;user1\u0026#39; 的文件和目录 find /usr/bin -type f -atime +100 搜索在过去100天内未被使用过的执行文件 find /usr/bin -type f -mtime -10 搜索在10天内被创建或者修改过的文件 whereis halt 显示一个二进制文件、源码或man的位置 which halt 显示一个二进制文件或可执行文件的完整路径 删除大于 50M 的文件：\n1 find /var/mail/ -size +50M -exec rm {} ＼; 四. 文件的权限 - 使用 \u0026ldquo;+\u0026rdquo; 设置权限，使用 \u0026ldquo;-\u0026rdquo; 用于取消 chmod 命令，改变文件 / 文件夹权限\n1 2 3 ls -lh 显示权限 chmod ugo+rwx directory1 设置目录的所有人(u)、群组(g)以及其他人(o)以读（r，4 ）、写(w，2)和执行(x，1)的权限 chmod go-rwx directory1 删除群组(g)与其他人(o)对目录的读写执行权限 chown 命令，改变文件的所有者\n1 2 3 chown user1 file1 改变一个文件的所有人属性 chown -R user1 directory1 改变一个目录的所有人属性并同时改变改目录下所有文件的属性 chown user1:group1 file1 改变一个文件的所有人和群组属性 chgrp 命令，改变文件所属用户组\n1 chgrp group1 file1 改变文件的群组 五. 文本处理 grep 命令，分析一行的信息，若当中有我们所需要的信息，就将该行显示出来，该命令通常与管道命令一起使用，用于对一些命令的输出进行筛选加工等等\n1 2 3 4 5 6 7 8 9 10 grep Aug /var/log/messages 在文件 \u0026#39;/var/log/messages\u0026#39;中查找关键词\u0026#34;Aug\u0026#34; grep ^Aug /var/log/messages 在文件 \u0026#39;/var/log/messages\u0026#39;中查找以\u0026#34;Aug\u0026#34;开始的词汇 grep [0-9] /var/log/messages 选择 \u0026#39;/var/log/messages\u0026#39; 文件中所有包含数字的行 grep Aug -R /var/log/* 在目录 \u0026#39;/var/log\u0026#39; 及随后的目录中搜索字符串\u0026#34;Aug\u0026#34; sed \u0026#39;s/stringa1/stringa2/g\u0026#39; example.txt 将example.txt文件中的 \u0026#34;string1\u0026#34; 替换成 \u0026#34;string2\u0026#34; sed \u0026#39;/^$/d\u0026#39; example.txt 从example.txt文件中删除所有空白行 paste 命令\n1 2 paste file1 file2 合并两个文件或两栏的内容 paste -d \u0026#39;+\u0026#39; file1 file2 合并两个文件或两栏的内容，中间用\u0026#34;+\u0026#34;区分 sort 命令\n1 2 3 4 sort file1 file2 排序两个文件的内容 sort file1 file2 | uniq 取出两个文件的并集(重复的行只保留一份) sort file1 file2 | uniq -u 删除交集，留下其他的行 sort file1 file2 | uniq -d 取出两个文件的交集(只留下同时存在于两个文件中的文件) comm 命令\n1 2 3 comm -1 file1 file2 比较两个文件的内容只删除 \u0026#39;file1\u0026#39; 所包含的内容 comm -2 file1 file2 比较两个文件的内容只删除 \u0026#39;file2\u0026#39; 所包含的内容 comm -3 file1 file2 比较两个文件的内容只删除两个文件共有的部分 六、打包和压缩文件 tar 命令，对文件进行打包，默认情况并不会压缩，如果指定了相应的参数，它还会调用相应的压缩程序（如 gzip 和 bzip 等）进行压缩和解压\n1 2 3 4 5 6 7 8 -c ：新建打包文件 -t ：查看打包文件的内容含有哪些文件名 -x ：解打包或解压缩的功能，可以搭配-C（大写）指定解压的目录，注意-c,-t,-x不能同时出现在同一条命令中 -j ：通过bzip2的支持进行压缩/解压缩 -z ：通过gzip的支持进行压缩/解压缩 -v ：在压缩/解压缩过程中，将正在处理的文件名显示出来 -f filename ：filename为要处理的文件 -C dir ：指定压缩/解压缩的目录dir 压缩：tar -jcv -f filename.tar.bz2 要被处理的文件或目录名称 查询：tar -jtv -f filename.tar.bz2 解压：tar -jxv -f filename.tar.bz2 -C 欲解压缩的目录\n1 2 3 4 5 6 7 8 9 10 11 12 bunzip2 file1.bz2 解压一个叫做 \u0026#39;file1.bz2\u0026#39;的文件 bzip2 file1 压缩一个叫做 \u0026#39;file1\u0026#39; 的文件 gunzip file1.gz 解压一个叫做 \u0026#39;file1.gz\u0026#39;的文件 gzip file1 压缩一个叫做 \u0026#39;file1\u0026#39;的文件 gzip -9 file1 最大程度压缩 rar a file1.rar test_file 创建一个叫做 \u0026#39;file1.rar\u0026#39; 的包 rar a file1.rar file1 file2 dir1 同时压缩 \u0026#39;file1\u0026#39;, \u0026#39;file2\u0026#39; 以及目录 \u0026#39;dir1\u0026#39; rar x file1.rar 解压rar包 zip file1.zip file1 创建一个zip格式的压缩包 unzip file1.zip 解压一个zip格式压缩包 zip -r file1.zip file1 file2 dir1 将几个文件和目录同时压缩成一个zip格式的压缩包 七. 系统和关机（关机、重启和登出） 1 2 3 4 5 6 7 8 9 shutdown -h now 关闭系统(1) init 0 关闭系统(2) telinit 0 关闭系统(3) shutdown -h hours:minutes \u0026amp; 按预定时间关闭系统 shutdown -c 取消按预定时间关闭系统 shutdown -r now 重启(1) reboot 重启(2) logout 注销 time 测算一个命令（即程序）的执行时间 八、进程相关的命令 jps 命令，显示当前系统的 java 进程情况，及其 id 号\njps(Java Virtual Machine Process Status Tool) 是 JDK 1.5 提供的一个显示当前所有 java 进程 pid 的命令，简单实用，非常适合在 linux/unix 平台上简单察看当前 java 进程的一些简单情况。\nps 命令，用于将某个时间点的进程运行情况选取下来并输出，process 之意\n1 2 3 4 5 6 7 8 9 10 -A ：所有的进程均显示出来 -a ：不与terminal有关的所有进程 -u ：有效用户的相关进程 -x ：一般与a参数一起使用，可列出较完整的信息 -l ：较长，较详细地将PID的信息列出 ps aux # 查看系统所有的进程数据 ps ax # 查看不与terminal有关的所有进程 ps -lA # 查看系统所有的进程数据 ps axjf # 查看连同一部分进程树状态 kill 命令, 用于向某个工作（%jobnumber）或者是某个 PID（数字）传送一个信号，它通常与 ps 和 jobs 命令一起使用\n命令格式 : kill[命令参数][进程 id]\n命令参数:\n1 2 3 4 5 -l 信号，若果不加信号的编号参数，则使用“-l”参数会列出全部的信号名称 -a 当处理当前进程时，不限制命令名和进程号的对应关系 -p 指定kill 命令只打印相关进程的进程号，而不发送任何信号 -s 指定发送信号 -u 指定用户 实例 1：列出所有信号名称 命令：kill -l 输出：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [root@localhost test6]# kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX 说明：\n只有第 9 种信号 (SIGKILL) 才可以无条件终止进程，其他信号进程都有权利忽略。 下面是常用的信号：\n1 2 3 4 5 6 7 HUP 1 终端断线 INT 2 中断（同 Ctrl + C） QUIT 3 退出（同 Ctrl + \\） TERM 15 终止 KILL 9 强制终止 CONT 18 继续（与STOP相反， fg/bg命令） STOP 19 暂停（同 Ctrl + Z） 实例 2：得到指定信号的数值\n1 2 3 4 5 [root@localhost test6]# kill -l KILL [root@localhost test6]# kill -l SIGKILL [root@localhost test6]# kill -l TERM [root@localhost test6]# kill -l SIGTERM [root@localhost test6]# 实例 3：先用 ps 查找进程，然后用 kill 杀掉\n1 2 3 4 5 命令：kill 3268 [root@localhost test6]# ps -ef|grep vim root 3268 2884 0 16:21 pts/1 00:00:00 vim install.log root 3370 2822 0 16:21 pts/0 00:00:00 grep vim [root@localhost test6]# kill 3268 实例 4：彻底杀死进程\n1 命令：kill –9 3268 // -9 强制杀掉进程 killall 命令，向一个命令启动的进程发送一个信号，用于杀死指定名字的进程\n命令格式 : killall[命令参数][进程名]\n1 2 3 4 5 6 7 8 9 10 11 12 13 命令参数： -Z 只杀死拥有scontext 的进程 -e 要求匹配进程名称 -I 忽略小写 -g 杀死进程组而不是进程 -i 交互模式，杀死进程前先询问用户 -l 列出所有的已知信号名称 -q 不输出警告信息 -s 发送指定的信号 -v 报告信号是否成功发送 -w 等待进程死亡 --help 显示帮助信息 --version 显示版本显示 示例\n1 2 3 4 5 6 1：杀死所有同名进程 killall nginx killall -9 bash 2.向进程发送指定信号 killall -TERM ngixn 或者 killall -KILL nginx top 命令，是 Linux 下常用的性能分析工具，能够实时显示系统中各个进程的资源占用状况，类似于 Windows 的任务管理器。\n如何杀死进程：\n1 2 3 4 （1）图形化界面的方式 （2）kill -9 pid （-9表示强制关闭） （3）killall -9 程序的名字 （4）pkill 程序的名字 查看进程端口号：\n1 netstat -tunlp|grep 端口号 责编来源：jianshu.com/p/7c0df6fcfc7\n","permalink":"https://ktzxy.top/posts/u7ki415osm/","summary":"Linux常用命令","title":"Linux常用命令"},{"content":"Kubernetes核心技术Helm Helm就是一个包管理工具【类似于npm】\n为什么引入Helm 首先在原来项目中都是基于yaml文件来进行部署发布的，而目前项目大部分微服务化或者模块化，会分成很多个组件来部署，每个组件可能对应一个deployment.yaml,一个service.yaml,一个Ingress.yaml还可能存在各种依赖关系，这样一个项目如果有5个组件，很可能就有15个不同的yaml文件，这些yaml分散存放，如果某天进行项目恢复的话，很难知道部署顺序，依赖关系等，而所有这些包括\n基于yaml配置的集中存放 基于项目的打包 组件间的依赖 但是这种方式部署，会有什么问题呢？\n如果使用之前部署单一应用，少数服务的应用，比较合适 但如果部署微服务项目，可能有几十个服务，每个服务都有一套yaml文件，需要维护大量的yaml文件，版本管理特别不方便 Helm的引入，就是为了解决这个问题\n使用Helm可以把这些YAML文件作为整体管理 实现YAML文件高效复用 使用helm应用级别的版本管理 Helm介绍 Helm是一个Kubernetes的包管理工具，就像Linux下的包管理器，如yum/apt等，可以很方便的将之前打包好的yaml文件部署到kubernetes上。\nHelm有三个重要概念\nhelm：一个命令行客户端工具，主要用于Kubernetes应用chart的创建、打包、发布和管理 Chart：应用描述，一系列用于描述k8s资源相关文件的集合 Release：基于Chart的部署实体，一个chart被Helm运行后将会生成对应的release，将在K8S中创建出真实的运行资源对象。也就是应用级别的版本管理 Repository：用于发布和存储Chart的仓库 Helm组件及架构 Helm采用客户端/服务端架构，有如下组件组成\nHelm CLI是Helm客户端，可以在本地执行 Tiller是服务器端组件，在Kubernetes集群上运行，并管理Kubernetes应用程序 Repository是Chart仓库，Helm客户端通过HTTP协议来访问仓库中Chart索引文件和压缩包 Helm v3变化 2019年11月13日，Helm团队发布了Helm v3的第一个稳定版本\n该版本主要变化如下\n架构变化\n最明显的变化是Tiller的删除 V3版本删除Tiller relesase可以在不同命名空间重用 V3之前\nV3版本\nhelm配置 首先我们需要去 官网下载\n第一步，下载helm安装压缩文件，上传到linux系统中 第二步，解压helm压缩文件，把解压后的helm目录复制到 usr/bin 目录中 使用命令：helm 我们都知道yum需要配置yum源，那么helm就就要配置helm源\nhelm仓库 添加仓库\n1 helm repo add 仓库名 仓库地址 例如\n1 2 3 4 5 6 7 8 9 # 配置微软源 helm repo add stable http://mirror.azure.cn/kubernetes/charts # 配置阿里源 helm repo add aliyun https://kubernetes.oss-cn-hangzhou.aliyuncs.com/charts # 配置google源 helm repo add google https://kubernetes-charts.storage.googleapis.com/ # 更新 helm repo update 然后可以查看我们添加的仓库地址\n1 2 3 4 # 查看全部 helm repo list # 查看某个 helm search repo stable 或者可以删除我们添加的源\n1 helm repo remove stable helm基本命令 chart install chart upgrade chart rollback 使用helm快速部署应用 使用命令搜索应用 首先我们使用命令，搜索我们需要安装的应用\n1 2 # 搜索 weave仓库 helm search repo weave 根据搜索内容选择安装 搜索完成后，使用命令进行安装\n1 helm install ui aliyun/weave-scope 可以通过下面命令，来下载yaml文件【如果】\n1 kubectl apply -f weave-scope.yaml 安装完成后，通过下面命令即可查看\n1 helm list 同时可以通过下面命令，查看更新具体的信息\n1 helm status ui 但是我们通过查看 svc状态，发现没有对象暴露端口\n所以我们需要修改service的yaml文件，添加NodePort\n1 kubectl edit svc ui-weave-scope 这样就可以对外暴露端口了\n然后我们通过 ip + 32185 即可访问\n如果自己创建Chart 使用命令，自己创建Chart\n1 helm create mychart 创建完成后，我们就能看到在当前文件夹下，创建了一个 mychart目录\n目录格式 templates：编写yaml文件存放到这个目录 values.yaml：存放的是全局的yaml文件 chart.yaml：当前chart属性配置信息 在templates文件夹创建两个文件 我们创建以下两个\ndeployment.yaml service.yaml 我们可以通过下面命令创建出yaml文件\n1 2 3 4 # 导出deployment.yaml kubectl create deployment web1 --image=nginx --dry-run -o yaml \u0026gt; deployment.yaml # 导出service.yaml 【可能需要创建 deployment，不然会报错】 kubectl expose deployment web1 --port=80 --target-port=80 --type=NodePort --dry-run -o yaml \u0026gt; service.yaml 安装mychart 执行命令创建\n1 helm install web1 mychart 应用升级 当我们修改了mychart中的东西后，就可以进行升级操作\n1 helm upgrade web1 mychart chart模板使用 通过传递参数，动态渲染模板，yaml内容动态从传入参数生成\n刚刚我们创建mychart的时候，看到有values.yaml文件，这个文件就是一些全局的变量，然后在templates中能取到变量的值，下面我们可以利用这个，来完成动态模板\n在values.yaml定义变量和值 具体yaml文件，获取定义变量值 yaml文件中大题有几个地方不同 image tag label port replicas 定义变量和值 在values.yaml定义变量和值\n获取变量和值 我们通过表达式形式 使用全局变量 {{.Values.变量名称}} 例如： {{.Release.Name}}\n安装应用 在我们修改完上述的信息后，就可以尝试的创建应用了\n1 helm install --dry-run web2 mychart ","permalink":"https://ktzxy.top/posts/zjoinuc8y7/","summary":"15 Kubernetes核心技术Helm","title":"15 Kubernetes核心技术Helm"},{"content":"一、Ansible介绍 Ansible是当下比较流行的自动化运维工具，可通过SSH协议对远程服务器进行集中化的配置管理、应用部署等，常结合Jenkins来实现自动化部署。\n除了Ansible，还有像SaltStack、Fabric（曾经管理100多台服务器上的应用时也曾受益于它）、Puppet等自动化工具。相比之下，Ansible最大的优势就是无需在被管理主机端部署任何客户端代理程序，通过SSH通道就可以进行远程命令的执行或配置的下发，足够轻量级，但同时功能非常强大，且各项功能通过模块来实现，具备良好的扩展性。不足之处是Ansible只支持在Linux系统上安装，不支持Windows。\n如果你需要在多于一台服务器上做相同的操作，那么建议你使用Ansible之类的自动化工具，这将极大提高你的操作效率。\n二、Ansible环境搭建 1. 找一台主机用于做管理服务器，在其上安装Ansible\n1 yum -y install ansible Ansible基于Python实现，一般Linux系统都自带Python，所以可以直接使用yum安装或pip安装。\n安装完后，在/etc/ansible/目录下生成三个主要的文件或目录，\n1 2 3 4 5 [root@tool-server ~]# ll /etc/ansible/ total 24 -rw-r--r--. 1 root root 19179 Jan 30 2018 ansible.cfg -rw-r--r--. 1 root root 1136 Apr 17 15:17 hosts drwxr-xr-x. 2 root root 6 Jan 30 2018 roles ansible.cfg：Ansible的配置文件 hosts：登记被管理的主机 roles：角色项目定义目录，主要用于代码复用 2. 在/etc/ansible/hosts文件中添加需要被管理的服务器节点\n1 2 3 4 5 6 [root@tool-server ~]# vim /etc/ansible/hosts [k8s] 192.168.40.201 192.168.40.202 192.168.40.205 192.168.40.206 [k8s]表示将下面的服务器节点分到k8s的组中，后面执行命令时可指定针对某个组执行。\n3. 生成SSH KEY，并copy到被管理节点上，实现免密SSH访问\n在管理节点执行 ssh-keygen 生成SSH KEY，然后copy到各被管理节点上\n1 ssh-copy-id -i ~/.ssh/id_rsa.pub root@192.168.40.201 上面命令将~/.ssh/id_rsa.pub文件内容添加到被管理节点的/root/.ssh/authorized_keys文件中，实现管理节点到被管理节点的免密SSH访问。\n4. 调试Ansible\n针对k8s服务器组执行ping，验证Ansible到各被管理节点的连通性\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [root@tool-server ~]# ansible k8s -m ping 192.168.40.201 | SUCCESS =\u0026gt; { \u0026#34;changed\u0026#34;: false, \u0026#34;ping\u0026#34;: \u0026#34;pong\u0026#34; } 192.168.40.205 | SUCCESS =\u0026gt; { \u0026#34;changed\u0026#34;: false, \u0026#34;ping\u0026#34;: \u0026#34;pong\u0026#34; } 192.168.40.202 | SUCCESS =\u0026gt; { \u0026#34;changed\u0026#34;: false, \u0026#34;ping\u0026#34;: \u0026#34;pong\u0026#34; } 192.168.40.206 | SUCCESS =\u0026gt; { \u0026#34;changed\u0026#34;: false, \u0026#34;ping\u0026#34;: \u0026#34;pong\u0026#34; } Ansible只需要在管理主机上安装，然后打通管理主机到各被管理主机的SSH免密访问即可进行集中化的管理控制，不需在被管理主机安装任何代理程序。\n三、Ansible命令 Ansible的命令格式为， ansible 主机群组名 -m 命令模块名 -a \u0026quot;批量执行的操作\u0026quot;\n其中-m不是必须的，默认为command模块，-a也不是必须的，表示命令模块的参数，比如前面的ping模块就没有参数。\n可以使用 ansible-doc -l 列出所有可用的命令模块， ansible-doc -s 模块名 查看指定模块的参数信息\n常用命令模块\n3.1 command command是Ansible的默认模块，不指定-m参数时默认使用command。command可以运行远程主机权限范围内的所有shell命令，但不支持管道操作\n1 2 # 查看k8s分组主机内存使用情况 ansible k8s -m command -a \u0026#34;free -g\u0026#34; 3.2 shell shell基本与command相同，但shell支持管道操作\n1 2 #shell支持管道操作 |grep Mem ansible k8s -m shell -a \u0026#34;free -g|grep Mem\u0026#34; 3.3 script script就是在远程主机上执行管理端存储的shell脚本文件，相当于scp+shell\n1 2 # /root/echo.sh为管理端本地shell脚本 ansible k8s -m script -a \u0026#34;/root/echo.sh\u0026#34; 3.4 copy copy实现管理端到远程主机的文件拷贝，相当于scp\n1 2 #拷贝本地echo.sh文件到k8s组中远程主机的/tmp目录下，所属用户、组为 root ，权限为 0755 ansible k8s -m copy -a \u0026#34;src=/root/echo.sh dest=/tmp/ owner=root group=root mode=0755\u0026#34; 3.5 yum 软件包安装或删除\n1 ansible k8s -m yum -a \u0026#34;name=wget state=latest\u0026#34; 其中state有如下取值：\n针对安装，可取值“present，installed，latest”，present，installed即普通安装，两者无区别，latest是使用yum mirror上最新的版本进行安装 针对删除，可取值“absent，removed”，两者无差别 3.6 service 对远程主机的服务进行管理\n1 ansible k8s -m service -a \u0026#34;name=nginx state=stoped\u0026#34; state可取值“started/stopped/restarted/reloaded”。\n3.7 get_url 在远程主机上下载指定URL到本地\n1 ansible k8s -m get_url -a \u0026#34;url=http://www.baidu.com dest=/tmp/index.html mode=0440 force=yes\u0026#34; 3.8 setup 获取远程主机的信息\n1 ansible k8s -m setup 3.9 file 管理远程主机的文件或目录\n1 ansible k8s -m file -a \u0026#34;dest=/opt/test state=touch\u0026#34; state可取值\ndirectory：创建目录 file：如果文件不存在，则创建 link：创建symbolic link absent：删除文件或目录 touch：创建一个不存在的空文件 3.10 cron 管理远程主机的crontab定时任务\n1 ansible k8s -m cron -a \u0026#34;name=\u0026#39;backup servcie\u0026#39; minute=*/5 job=\u0026#39;/usr/sbin/ntpdate time.nist.gov \u0026gt;/dev/null 2\u0026gt;\u0026amp;1\u0026#39;\u0026#34; 支持的参数\nstate：取值present表示创建定时任务，absent表示删除定时任务 disabled：yes表示注释掉定时任务，no表示接触注释 四、Ansible playbook Ansible的playbook由一个或多个play组成，play的功能就是为归为一组的主机编排要执行的一系列task，其中每一个task就是调用Ansible的一个命令模块。\nplaybook的核心元素包括：\nhosts：执行任务的远程主机组或列表 tasks：要执行的任务列表 variables：内置变量或自定义的变量 templates：使用模板语法的文件，通常为配置文件 handlers：和notify结合使用，由特定条件触发，一般用于配置文件变更触发服务重启 tags：标签，可在运行时通过标签指定运行playbook中的部分任务 roles： playbook文件遵循yaml的语法格式，运行命令的格式为 ansible-playbook \u0026lt;filename.yml\u0026gt; ... [options]， 常用options包括\n\u0026ndash;syntax 检查playbook文件语法是否正确 \u0026ndash;check 或 -C 只检测可能会发生的改变，但不真正执行操作 \u0026ndash;list-hosts 列出运行任务的主机 \u0026ndash;list-tags 列出playbook文件中定义所有的tags \u0026ndash;list-tasks 列出playbook文件中定义的所有任务集 \u0026ndash;limit 只针对主机列表中的某个主机或者某个组执行 -f 指定并发数，默认为5个 -t 指定某个或多个tags运行（前提playbook中有定义tags） -v 显示过程 -vv -vvv更详细 下面以批量安装Nginx为例，尽可能介绍playbook各核心元素的用法。\n定义palybook yaml文件nginx_playbook.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 --- - hosts: 192.168.40.201,192.168.40.205 # 主机列表，也可以是/etc/ansible/hosts中定义的主机分组名 remote_user: root # 远程用户 vars: # 自定义变量 version: 1.16.1 vars_files: - ./templates/nginx_locations_vars.yml tasks: - name: install dependencies # 定义任务的名称 yum: name={{item}} state=installed # 调用模块，具体要做的事情，这里使用with_items迭代多个yum任务安装必要的依赖 with_items: - gcc - gcc-c++ - pcre - pcre-devel - zlib - zlib-devel - openssl - openssl-devel - name: download nginx # 通过get_url模块下载nginx get_url: url=http://nginx.org/download/nginx-{{version}}.tar.gz dest=/tmp/ mode=0755 force=no - name: unarchive # 通过unarchive模块解压nginx unarchive: src=/tmp/nginx-{{version}}.tar.gz dest=/tmp/ mode=0755 copy=no - name: configure,make and install # 通过shell模块执行shell命令编译安装 shell: cd /tmp/nginx-{{version}} \u0026amp;\u0026amp; ./configure --prefix=/usr/local/nginx \u0026amp;\u0026amp; make \u0026amp;\u0026amp; make install - name: start nginx # 通过shell模块执行shell命令启动nginx shell: /usr/local/nginx/sbin/nginx - name: update config # 通过template模块动态生成配置文件下发到远程主机目录 template: src=nginx.conf.j2 dest=/usr/local/nginx/conf/nginx.conf notify: reload nginx # 在结束时触发一个操作，具体操作通过handlers来定义 tags: reload # 对任务定义一个标签，运行时通过-t执行带指定标签的任务 handlers: - name: reload nginx # 与notify定义的内容对应 shell: /usr/local/nginx/sbin/nginx -s reload 4.1 变量 在上面的示例中使用vars定义了变量version，在tasks中通过{{version}}进行引用。Ansible支持如下几种定义变量的方式\n1.在playbook文件中定义 前面示例已经说明\n2.命令行指定 在执行playbook时通过-e指定，如ansible-playbook -e \u0026quot;version=1.17.9\u0026quot; nginx_playbook.yml， 这里指定的变量将覆盖playbook中定义的同名变量的值\n3.hosts文件中定义变量 在/etc/ansible/hosts文件中也可以定义针对单个主机或主机组的变量，如\n1 2 3 4 5 [nginx] 192.168.40.201 version=1.17.9 # 定义单个主机的变量 192.168.40.205 [nginx:vars] # 定义整个组的统一变量 version=1.16.1 4.在独立的yaml文件中定义变量 专门定义一个yaml变量文件，然后在playbook文件中通过var_files引用，如\n1 2 3 4 5 6 7 8 9 10 # 定义存放变量的文件 [root@ansible ]# cat var.yml version: 1.16.1 # 编写playbook [root@ansible ]# cat nginx_playbook.yml --- - hosts: nginx remote_user: root vars_files: # 引用变量文件 - ./var.yml # 指定变量文件的path（这里可以是绝对路径，也可以是相对路径） 5.使用setup模块获取到的变量 前面介绍setup模块可获取远程主机的信息，可在playbook中直接引用setup模块获取到的属性，比如系统版本：ansible_distribution_major_version\n4.2 模板 playbook模板为我们提供了动态的配置服务，使用jinja2语言，支持多种条件判断、循环、逻辑运算、比较操作等。应用场景就是定义一个模板配置文件，然后在执行的时候动态生成最终的配置文件下发到远程主机。一般将模板文件放在playbook文件同级的templates目录下，这样在playbook文件中可以直接引用，否则需要通过绝对路径指定，模板文件后缀名一般为 .j2。\n本例中，我们将nginx.conf配置文件作为模板文件，添加需要动态配置的内容，并定义一个变量文件，通过vars_files引入：vars_files: ./templates/nginx_locations_vars.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # 模板文件 [root@tool-server nginx-deploy]# vim templates/nginx.conf.j2 ... server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; # 这里的内容动态生成 {% for location in nginx_locations %} location {{location.path}} { proxy_pass {{location.proxy}}; } {% endfor %} location / { root html; index index.html index.htm; } ... # 独立的自定义变量文件，用于填充模板文件中的变量 [root@tool-server nginx-deploy]# vim templates/nginx_locations_vars.yml nginx_locations: - {\u0026#34;path\u0026#34;: \u0026#34;/cns\u0026#34;, \u0026#34;proxy\u0026#34;: \u0026#34;http://192.168.40.202/cns\u0026#34;} - {\u0026#34;path\u0026#34;: \u0026#34;/admin\u0026#34;, \u0026#34;proxy\u0026#34;: \u0026#34;http://192.168.40.202/admin\u0026#34;} 4.3 handlers handlers和notify结合使用，由特定条件触发，一般用于配置文件变更触发服务重启。在本例中我们在配置文件变更时，通过notify定义了一个“reload nginx”的操作，然后在handlers部分定义“reload nginx”操作——通过shell模块调用nginx的reload来重新加载配置。\n4.4 标签 playbook文件中，如果只想执行某一个或几个任务，则可以给任务打标签，在运行的时候通过 -t 选择带指定标签的任务执行，也可以通过 \u0026ndash;skip-tags 选择不带指定标签的任务执行。比如在本例中，我们在“update config”的task上加了“reload”的标签，如果后面再修改配置，我们只需要执行“update config”的task并触发reload nginx就行了，可以这么执行playbook\n1 [root@tool-server nginx-deploy]# ansible-playbook -t reload nginx_playbook.yml 4.5 when 可以在task上添加when表示当某个条件达到了该任务才执行，如\n1 2 3 4 5 6 tasks: - name: install nginx yum: name=nginx state=installed - name: update config for system6 template: src=nginx.conf.j2 dest=/usr/local/nginx/conf/nginx.conf when: ansible_distribution_major_version == \u0026#34;6\u0026#34; # 判断系统版本，为6才执行上面的template配置的文件 4.6 roles roles就是将变量、文件、任务、模板及处理器放置在单独的目录中，并可以在playbook中include的一种机制，一般用于主机构建服务的场景中，但也可以是用于构建守护进程等场景。\nroles的目录结构，默认的roles目录为/etc/ansible/roles\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 roles: # 所有的角色项目必须放在roles目录下 project: # 具体的角色项目名称，比如nginx、tomcat files： # 用来存放由copy或script模块调用的文件 templates： # 用来存放jinjia2模板，template模块会自动在此目录中寻找jinjia2模板文件 tasks： # 此目录应当包含一个main.yml文件，用于定义此角色的任务列表，此文件可以使用include包含其它的位于此目录的task文件。 main.yml handlers： # 此目录应当包含一个main.yml文件，用于定义此角色中触发条件时执行的动作 main.yml vars： # 此目录应当包含一个main.yml文件，用于定义此角色用到的变量 main.yml defaults： # 此目录应当包含一个main.yml文件，用于为当前角色设定默认变量 main.yml meta： # 此目录应当包含一个main.yml文件，用于定义此角色的特殊设定及其依赖关系 main.yml 我们将上面的例子通过roles改造一下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [root@tool-server ~]# cd /etc/ansible/roles/ [root@tool-server roles]# mkdir -p nginx/{tasks,vars,templates,handlers} ...#创建各目录的mian.yml文件，并将对应的内容加入文件中 #最终目录结构 [root@tool-server roles]# tree . . └── nginx ├── handlers │ └── main.yml # 上例handlers部分的内容，直接 -name开头，不需要再加 `handlers：` ├── tasks │ └── main.yml # tasks部分内容，直接-name开头，不需要加tasks，可以将各个task拆分为多个文件，然后在main.yml中通过 `- include: install.yml` 形式的列表引入 ├── templates │ └── main.yml # templates/nginx.conf.j2的内容 └── vars └── main.yml # templates/nginx_locations_vars.yml的内容 5 directories, 4 files 最后，在playbook中通过roles引入，\n1 2 3 4 5 6 [root@ansible roles]# vim nginx_playbook.yml --- - hosts: nginx remote_user: root roles: - role: nginx # 指定角色名称 roles将playbook的各个部分进行拆分组织，主要用于代码复用度较高的场景。\n总结 Ansible是功能强大但又很轻量级的自动化运维工具，基于SSH协议批量对远程主机进行管理，不仅可用于日常的服务维护，也可与Jenkins等CI/CD工具结合实现自动化部署。如果你需要在多于一台服务器上做重复又稍显复杂的操作，那么建议你使用Ansible，这将极大提高你的操作效率，并且所有操作文档化，更易维护与迁移。\n","permalink":"https://ktzxy.top/posts/2ez2sr21vd/","summary":"Ansible","title":"Ansible"},{"content":"1. 事务(transaction)的概念 在实际的业务开发中，有些业务操作要多次访问数据库。一个业务要发送多条 SQL 语句给数据库执行。需要将多次访问数据库的操作视为一个整体来执行，要么全部执行成功。要么全部执行失败。\n事务就是数据库管理系统（DBMS）执行过程中的一个逻辑单位（不可再进行分割），由一个有限的数据库操作序列构成（多个 DML 语句，select 语句不包含事务），要不全部成功，要不全部不成功。如果其中有一条 SQL 语句执行失败，那么之前已经成功的SQL语句都要进行事务的回滚（撤销）。\n2. 事务特性 事务应该具有4个属性：原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性。\n2.1. 原子性（Atomicity） 原子性：事务操作中的所有SQL语句不可再分割，要么全部执行成功，要么全部执行失败。对于一个事务来说，不能只执行其中的一部分操作。\n1 2 3 4 示例： A卡扣除500元 B卡增加500元 在整个事务操作中，A卡与B卡要同时成功或者同时失败，不能只出现扣除或者只出现增加的情况 2.2. 一致性（Consistency） 一致性：事务将数据库从一种一致性转换到另外一种一致性状态，事务开始之前和事务结束之后的数据要保持一致。\n1 2 3 4 示例： A卡扣除500元 B卡增加500元 在整个事务操作前后，A卡与B卡的总和前后一致 2.3. 隔离性（Isolation） 多个事务的操作是互不干扰的，一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的，并发执行的各个事务之间不能互相干扰。\n1 2 3 4 5 6 7 示例： A卡余额为1200元、B卡余额为300元 A卡两次转入B卡，分别都是500元 从理论上完成以上两次转账后，A卡余额为200元，B卡的余额为1300元 如果将A卡两次转入B卡的操作分别称为T1和T2，在现实世界中T1和T2应该是没有关系的两次操作，但在真实的数据库操作中，可能会出现T1与T2的操作是交替执行的。 此时，T1与T2都先读取了A卡的余额，然后T1基于原来A卡的余额去减操作并分别更新A、B卡，而T2因为某些原因，读取余额后等待了一段时间才继续操作，此时T2的减操作还是基于A卡最原来的余额。所以两次减操作后，其实相当于A卡只减了一次，而B卡却加了两次。 对于现实世界中状态转换对应的某些数据库操作来说，不仅要保证这些操作以原子性的方式执行完成，而且要保证其它的状态转换不会影响到本次状态转换，这个规则被称之为隔离性。\n2.4. 持久性（Durability） 事务一旦提交，则其所做的修改就会永久保存到数据库中，是不可逆的。此时即使系统崩溃，已经提交的修改数据也不会丢失。\n3. 事务隔离级别 3.1. 事务并发引发的问题 MySQL 是一个客户端/服务器架构的软件，对于同一个服务器来说，可以有若干个客户端与之连接，每个客户端与服务器连接上之后，就可以称之为一个会话（Session）。每个客户端都可以在自己的会话中向服务器发出请求语句，一个请求语句可能是某个事务的一部分，也就是对于服务器来说可能同时处理多个事务。\n事务有一个称之为隔离性的特性，理论上在某个事务对某个数据进行访问时，其他事务应该进行排队，当该事务提交之后，其他事务才可以继续访问这个数据，这样的话并发事务的执行就变成了串行化执行。但是对串行化执行性能影响太大，既想保持事务的一定的隔离性，又想让服务器在处理访问同一数据的多个事务时性能尽量高些，当舍弃隔离性的时候，可能会带来以下一些数据问题。\n3.1.1. 脏读（Dirty Read） 一个事务读取到了另一个事务中修改但未提交的数据。\n3.1.2. 不可重复读（Non-Repeatable Read） 一个事务多次读取的数据内容不一致，称之为不可重复读。通常要求是同一个事务中多次读取时数据，结果都应该是一致的。一般是由于另一个事务 update 语句修改数据后并提交后引发。\n3.1.3. 幻读（Phantom） 一个事务中多次查询的数据的记录数不一致。要求在一个事务多次读取的数据的数量是一致的，一般是由另一个事务的 insert 或 delete 引发。\n有一点值得注意：如果事务2是删除了符合的记录而不是插入新记录，事务1之后再根据相同的条件读取的记录变少了，这种现象算不算幻读呢？\n在 SQL92 标准中，这个上面的示例是属于幻读，但在 MySQL 中规定这种现象不属于“幻读”，而是被归纳到“不可重复读”，幻读强调的是一个事务按照某个相同条件多次读取记录时，后读取时读到了之前没有读到的记录。\n3.1.4. 更新丢失(Lost Update)或脏写 当两个或多个事务选择同一行数据修改，有可能发生某个事务更新丢失问题，即最后的更新覆盖了由其他事务所做的更新。\n例如：事务1读取某表中的数据A=20，事务2也读取A=20，事务1修改A=A-1，事务2也修改A=A-1，最终结果A=19，事务1的修改被丢失。\n3.1.5. 不可重复读与幻读的区别 不可重复读与幻读的区别在于：\n不可重复读是指在同一个事务内，多次读取同一条数据的结果不一样。一般由于另一个事务 update 语句修改数据后并提交后引发。 而幻读是指在同一个事务内，多次读取同一个范围内的数据，结果不一样。一般是由另一个事务的 insert 或 delete 引发。 3.2. 隔离级别的作用 隔离级别就用来解决并发访问存在的问题。隔离级别越低，越严重的问题就越可能发生。在 SQL 标准中设立了4个隔离级别。\n并发事务问题按严重性排序：脏读 \u0026gt; 不可重复读 \u0026gt; 幻读\n3.2.1. 常用数据库支持与默认的隔离级别 SQL92 标准的隔离级别分类表如下：\n级别 名字 隔离级别 脏读 不可重复读 幻读 概述 1 读未提交 read uncommitted √ √ √ 一个事务读取到了另一个事务未提交的数据 2 读已提交 read committed × √ √ 一个事务读取到另一个事务已经提交的数据 3 可重复读 repeatable read × × √ 同一个事务中多次读取数据内容一致 4 串行化 serializable × × × 同时只能有一个事务执行。相当于单线程 注：√ 代表存在的问题。一般只要求使用到级别3可重复读即可。MySQL 的隔离级别与 SQL92 标准有点差别，MySQL 的可重复读级别基本上已经解决了“幻读”的问题\n不同的数据库厂商对 SQL 标准中规定的四种隔离级别支持不一样。\nOracle 就只支持 READ COMMITTED 和 SERIALIZABLE 隔离级别。 MySQL 虽然支持 4 种隔离级别，但与 SQL 标准中所规定的各级隔离级别允许发生的问题却有些出入，MySQL 在 REPEATABLE READ 隔离级别下，是可以禁止幻读问题的发生的。 不同数据库厂商默认的隔离级别\nMySQL: 可重复读（REPEATABLE READ） Qracle、SQL Server: 读已提交（READ COMMITTED） 3.2.2. 设置事务的隔离级别 修改事务的隔离级别的命令：\n1 SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level; 其中的level可选值有4个：REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED | SERIALIZABLE\n设置事务的隔离级别的语句中，在SET关键字后可以放置GLOBAL关键字、SESSION关键字或者什么都不放，这样会对不同范围的事务产生不同的影响，具体如下：\n使用 GLOBAL 关键字（在全局范围影响）： 1 2 -- 示例：只对执行完该语句之后产生的会话起作用。当前已经存在的会话无效。 SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE; 使用 SESSION 关键字（在会话范围影响）： 1 2 -- 对当前会话的所有后续的事务有效。该语句可以在已经开启的事务中间执行，但不会影响当前正在执行的事务。如果在事务之间执行，则对后续的事务有效。 SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; 上述两个关键字都不用（只对执行语句后的下一个事务产生影响）： 1 2 -- 只对当前会话中下一个即将开启的事务有效。下一个事务执行完后，后续事务将恢复到之前的隔离级别。该语句不能在已经开启的事务中间执行，会报错的。 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; 在服务器启动时想改变事务的默认隔离级别，修改启动参数transaction-isolation的值。 1 2 # 默认隔离级别就从原来的REPEATABLE READ 变成了 SERIALIZABLE。 --transaction-isolation=SERIALIZABLE 3.2.3. MySQL数据库查看当前事务的隔离级别的命令 想要查看当前会话默认的隔离级别可以通过查看系统变量transaction_isolation的值\n1 2 3 4 5 6 7 8 -- 传统写法 SHOW VARIABLES LIKE \u0026#39;transaction_isolation\u0026#39;; -- 简便的写法：5.7.20版本后 SELECT @@transaction_isolation; -- 简便的写法：在 MySQL 5.7.20 的版本中引入来替换tx_isolation的，以前的版本将上述用到系统变量transaction_isolation 的地方替换为 tx_isolation。 SELECT @@tx_isolation; 4. MySQL 事务操作 4.1. 事务基础操作流程 开启事务。任何一条DML语句(insert、update、delete)执行，标志事务的开启 Notes: 开启一个新的事务，之前的事务会自动提交\n提交事务，将所有的DML语句操作历史记录和底层硬盘数据来一次同步。一旦事务提交了，无法通过回滚撤消 回滚事务，将所有的DML语句操作历史记录全部清空 4.2. 自动事务提交模式（MySQL 默认） MySQL 默认是每一条 DML(增删改)语句都是一个单独的事务，每条语句都会自动开启一个事务，并且执行完毕后自动提交事务。也就是说，当执行完一条DML语句时，MySQL会立即隐式的通过 commit 提交事务。\n4.3. 显式事务提交模式 4.3.1. 控制事务方式1 MySQL中全局变量autocommit默认值是1，自动提交事务。通过以下语句可以查询当前事务的提交方式参数值：\n1 select @@autocommit; 设置全局开启/禁止提交事务（0 关闭，1 开启）。关闭自动提交后，需要手动提交事务 1 set @@autocommit = 0; 提交事务 1 COMMIT; 回滚事务 1 ROLLBACK; Notes: 此方式是修改了全局的事务的自动提交行为。若把默认的自动提交修改为了手动提交，此时往后执行的 DML 语句都不会提交，需要手动的执行提交操作。\n4.3.2. 控制事务方式2 开启事务。 1 start transaction; 提交事务 1 COMMIT; 回滚事务 1 ROLLBACK; Notes: 此方式是开启一次性事务，针对本次一系列的操作。一旦开启事务，接下来的所有的SQL语句都是在同一个事务中，直到提交或回滚，该事务才会结束。\n4.4. 隐式提交模式 当使用START TRANSACTION或者BEGIN语句开启了一个事务，或者把系统变量autocommit的值设置为OFF时，事务就不会进行自动提交，但是如果期间输入了某些语句之后就会MySQL会自动将事务提交，像输入了COMMIT语句一样，这种因为某些特殊的语句而导致事务提交的情况称为隐式提交。\n4.4.1. 执行DDL语句 执行了定义或修改数据库对象的数据定义语言（DDL），所谓的数据库对象，指的就是数据库、表、视图、存储过程等等这些东西。当使用CREATE、ALTER、DROP等语句去修改数据库对象时，就会隐式的提交前边语句所属于的事务\n1 2 3 4 5 BEGIN; SELECT ... # 事务中的一条语句 UPDATE ... # 事务中的一条语句 ... # 事务中的其它语句 CREATE TABLE ... # 此语句会隐式的提交前边语句所属于的事务 4.4.2. 隐式使用或修改 mysql 数据库中的表 当使用 ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、SET PASSWORD等语句时也会隐式的提交前边语句所属于的事务。\n4.4.3. 事务控制或关于锁定的语句 在一个会话里，一个事务还没提交或者回滚时又使用 START TRANSACTION 或者 BEGIN 语句开启了另一个事务时，会隐式的提交上一个事务。 1 2 3 4 5 BEGIN; SELECT ... # 事务中的一条语句 UPDATE ... # 事务中的一条语句 ... # 事务中的其它语句 BEGIN; # 此语句会隐式的提交前边语句所属于的事务 当前的autocommit系统变量的值为OFF，然后手动把它修改为ON时，也会隐式的提交前边语句所属的事务。 使用LOCK TABLES、UNLOCK TABLES 等关于锁定的语句也会隐式的提交前边语句所属的事务。 4.4.4. 加载数据的语句 使用 LOAD DATA 语句来批量往数据库中导入数据时，也会隐式的提交前边语句所属的事务。\n4.4.5. 关于 MySQL 复制的一些语句 使用START SLAVE、STOP SLAVE、RESET SLAVE、CHANGE MASTER TO等语句时也会隐式的提交前边语句所属的事务。\n4.4.6. 其它的相关语句 使用ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH、LOAD INDEX INTOCACHE、OPTIMIZE TABLE、REPAIR TABLE、RESET等语句也会隐式的提交前边语句所属的事务。\n4.5. 保存点 如果你开启了一个事务，执行了很多语句，忽然发现某条语句有点问题，使用 ROLLBACK 语句来让数据库状态恢复到事务执行之前的样子，然后一切从头再来，但是可能根据业务和数据的变化，不需要全部回滚。所以 MySQL 里提出了一个保存点（英文：savepoint）的概念，就是在事务对应的数据库语句中打几个点，在调用 ROLLBACK 语句时可以指定会滚到哪个点，而不是回到最初的原点。\n4.5.1. 基础语法 定义保存点的语法如下：\n1 SAVEPOINT 保存点名称; 当想回滚到某个保存点时，可以使用下边这个语句（下边语句中的单词WORK 和 SAVEPOINT 是可有可无的）：\n1 ROLLBACK TO [SAVEPOINT] 保存点名称; 删除某个保存点语句：\n1 RELEASE SAVEPOINT 保存点名称; 注：很少会使用，通常在存储过程中有可能会用到\n4.5.2. 保存点示例 1 2 3 4 5 6 7 8 9 10 11 12 -- 自动提交事务是开启的 show variables like \u0026#39;%autocommit%\u0026#39;; set autocommit=0; insert into testdemo values(5,5,5); savepoint order_exp; insert into testdemo values(6,6,6); savepoint order_exp_2; insert into testdemo values(7,7,7); savepoint s3; select * from testdemo; rollback to savepoint order_exp_2; rollback; 4.6. 事务注意事项 可重复读（repeatable read）的隔离级别下使用了MVCC(multi-version concurrency control)机制，select 操作是快照读（历史版本）；insert、update 和 delete 等操作是当前读（当前版本）。\n即当开启事务后，后面多次查询的数据均为首次查询时的快照数据，不会受其他后面提交事务的影响；而使用更新操作时，则会读取当前最新的数据，因此一般在事务更新数据，都尽量使用以下方式进行更新，\n1 UPDATE account SET balance = balance - 50 WHERE\tid = 1; 并且在此事务更新后，会为这条数据上行锁，此时其他事务是无法操作此条数据。\n5. 事务使用的总结 5.1. 大事务的影响 并发情况下，数据库连接池容易被撑爆 锁定太多的数据，造成大量的阻塞和锁超时 执行时间长，容易造成主从延迟 回滚所需要的时间比较长 undo log 膨胀 容易导致死锁 5.2. 事务优化 将查询等数据准备操作放到事务外 事务中避免远程调用，远程调用要设置超时，防止事务等待时间太久 事务中避免一次性处理太多数据，可以拆分成多个事务分次处理 更新等涉及加锁的操作尽可能放在事务靠后的位置 能异步处理的尽量异步处理 应用侧(业务代码)保证数据一致性，非事务执行 5.3. 事务问题定位 可以通过查询 MySQl 的系统数据库 information_schema.innodb_trx，查询指定执行耗时的事务信息\n1 2 3 4 5 6 7 -- 查询执行时间超过2秒的事务，用于定位超长事务问题 SELECT * FROM information_schema.innodb_trx WHERE TIME_TO_SEC( timediff( now( ), trx_started ) ) \u0026gt; 2; 使用 kill 事务对应的线程id 命令强制结束事务，即上面语句查出结果里的 trx_mysql_thread_id 字段的值\n6. 事务的底层原理 在事务的实现机制上，MySQL 采用的是 WAL（Write-ahead logging，预写式日志）机制来实现的。在使用 WAL 的系统中，所有的修改都先被写入到日志中，然后再被应用到系统中。通常包含 redo 和 undo 两部分信息。\nredo log 称为重做日志，每当有操作时，在数据变更之前将操作写入 redo log，这样当发生掉电之类的情况时系统可以在重启后继续操作。 undo log 称为撤销日志，当一些变更执行到一半无法完成时，可以根据撤销日志恢复到变更之间的状态。 其中事务的原子性、一致性、持久化，实际上是由 InnoDB 中的两份日志来保证的，一份是 redo log 日志，一份是 undo log 日志。而持久性是通过数据库的锁，加上 MVCC 来保证的。\nMySQL 中用 redo log 来在系统 Crash 重启之类的情况时修复数据（事务的持久性），而 undo log 来保证事务的原子性。\n7. redo log（重做日志） 7.1. redo 日志的作用 InnoDB 存储引擎是以页为单位来管理存储空间，在进行的增删改查操作其实本质上都是在访问页面（包括读页面、写页面、创建新页面等操作）。而在访问页面之前，需要把在磁盘上的页缓存到内存中的 Buffer Pool，如果只在内存的 Buffer Pool 中修改了页面，假设在事务提交后突然发生了某个故障，导致内存中的数据都失效了，那么这个已经提交了的事务对数据库中所做的更改也就跟着丢失了。\n如果在事务提交完成之前把该事务所修改的所有页面都刷新到磁盘，会有以下问题：\n一个页面默认是 16KB 大小，只修改一个字节（或者一小部分内容）就要刷新 16KB 的数据，比较浪费。 一个事务可能包含很多语句，即使是一条语句也可能修改许多页面，该事务修改的这些页面可能并不相邻，这就意味着在将某个事务修改的 Buffer Pool 中的页面刷新到磁盘时，会出现大量的随机 IO，效率很慢。 为了已经提交了的事务对数据库中数据所做的修改永久生效，即使后来系统崩溃，在重启后也能把这种修改恢复出来。其实没有必要在每次事务提交时就把该事务在内存中修改过的全部页面刷新到磁盘，只需要把修改了内容记录下来即可。\n如：某个事务将系统表空间中的第 100 号页面中偏移量为 1000 处的那个字节的值 1 改成 2\n在事务提交时，把上述内容刷新到磁盘中，即使之后系统崩溃了，重启之后只要按照上述内容所记录的步骤重新更新一下数据页，那么该事务对数据库中所做的修改又可以被恢复出来，达到事务持久性的目的。因为在系统崩溃重启时需要按照上述内容所记录的步骤重新更新数据页，所以上述内容也被称之为重做日志，英文名为 redo log，也可以称之为 redo 日志。\n使用此方式相比前端事务提交时将所有修改过的内存中的页面刷新到磁盘的方式有以下优势：\nredo 日志占用的空间非常小。存储表空间 ID、页号、偏移量以及需要更新的值所需的存储空间是很小的。 redo 日志是顺序写入磁盘的。在执行事务的过程中，每执行一条语句，就可能产生若干条 redo 日志，这些日志是按照产生的顺序写入磁盘的，也就是使用顺序 IO。 该日志文件由两部分组成：重做日志缓冲（redo log buffer）以及重做日志文件（redo log file），前者是在内存中，后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中，用于在刷新该页到磁盘，发生错误时，进行数据恢复使用。\n7.2. redo log 关键参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 mysql\u0026gt; SHOW VARIABLES LIKE \u0026#39;%innodb_log_%\u0026#39;; +------------------------------------+----------+ | Variable_name | Value | +------------------------------------+----------+ | innodb_log_buffer_size | 1048576 | | innodb_log_checksums | ON | | innodb_log_compressed_pages | ON | | innodb_log_file_size | 50331648 | | innodb_log_files_in_group | 2 | | innodb_log_group_home_dir | .\\ | | innodb_log_spin_cpu_abs_lwm | 80 | | innodb_log_spin_cpu_pct_hwm | 50 | | innodb_log_wait_for_flush_spin_hwm | 400 | | innodb_log_write_ahead_size | 8192 | | innodb_log_writer_threads | ON | +------------------------------------+----------+ 通过以上 SQL 语句可以查询到 redo log 相关的关键参数，重点关注的参数如下：\ninnodb_log_buffer_size：设置 redo log buffer 大小参数，默认 16M，最大值是 4096M，最小值为 1M。 innodb_log_group_home_dir：设置 redo log 文件存储位置参数，默认值为\u0026quot;./\u0026quot;，即 innodb 数据文件存储位置，其中 redo log 文件如：ib_logfile0、ib_logfile1。（注：mysql 8.0 版本后名称好像不一样了） innodb_log_files_in_group：设置 redo log 文件的个数，命名方式如: ib_logfile0, iblogfile1,...iblogfileN。默认2个，最大100个。 innodb_log_file_size：设置单个 redo log 文件大小，默认值为 48M，最大值为 512G。注意最大值指的是整个 redo log 系列文件之和，即(innodb_log_files_in_group × innodb_log_file_size)不能大于最大值 512G。 Notes: 后面会结合具体场景分析说明配置的作用。\n7.3. redo 日志格式 redo 日志本质上只是记录了一下事务对数据库的修改内容。 InnoDB 们针对事务对数据库的不同修改场景定义了多种类型的 redo 日志，但是绝大部分类型的 redo 日志都有下边这种通用的结构：\n各个部分的详细释义如下：\ntype：该条 redo 日志的类型，redo 日志设计大约有 53 种不同的类型日志。 space ID：表空间 ID。 page number：页号。 data：该条 redo 日志的具体内容。 7.3.1. 简单 redo 日志类型 InnoDB 的记录行格式，如果没有为某个表显式的定义主键，并且表中也没有定义unique键，那么 InnoDB 会自动的为表添加一个称之为 row_id 的隐藏列作为主键。为这个 row_id 隐藏列赋值的方式如下：\n服务器会在内存中维护一个全局变量，每当向某个包含隐藏的 row_id 列的表中插入一条记录时，就会把该变量的值当作新记录的 row_id 列的值，并且把该变量自增 1。每当这个变量的值为 256 的倍数时，就会将该变量的值刷新到系统表空间的页号为 7 的页面中一个称之为 Max Row ID 的属性处。\n当系统启动时，会将上边提到的 Max Row ID 属性加载到内存中，将该值加上 256 之后赋值给前面提到的全局变量。\nMax Row ID 属性占用的存储空间是 8 个字节，当某个事务向某个包含 row_id 隐藏列的表插入一条记录，并且为该记录分配的 row_id 值为 256 的倍数时，就会向系统表空间页号为 7 的页面的相应偏移量处写入 8 个字节的值。\n实际上写入操作是在 Buffer Pool 中完成的，需要为这个页面的修改记录一条 redo 日志，以便在系统崩溃后能将已经提交的该事务对该页面所做的修改恢复出来。这种情况下对页面的修改是极其简单的，redo 日志中只需要记录一下在某个页面的某个偏移量处修改了几个字节的值，具体被修改的内容是啥就好了，InnoDB 把这种极其简单的 redo 日志称之为物理日志，并且根据在页面中写入数据的多少划分了几种不同的 redo 日志类型：\nMLOG_1BYTE（type 字段对应的十进制数字为 1）：表示在页面的某个偏移量处写入 1 个字节的 redo 日志类型。 MLOG_2BYTE（type 字段对应的十进制数字为 2）：表示在页面的某个偏移量处写入 2 个字节的 redo 日志类型。 MLOG_4BYTE（type 字段对应的十进制数字为 4）：表示在页面的某个偏移量处写入 4 个字节的 redo 日志类型。 MLOG_8BYTE（type 字段对应的十进制数字为 8）：表示在页面的某个偏移量处写入 8 个字节的 redo 日志类型。 MLOG_WRITE_STRING（type 字段对应的十进制数字为 30）：表示在页面的某个偏移量处写入一串数据。 Max Row ID 属性实际占用 8 个字节的存储空间，所以在修改页面中的该属性时，会记录一条类型为 MLOG_8BYTE 的 redo 日志，MLOG_8BYTE 的 redo 日志结构如下所示：\noffset 代表在页面中的偏移量。\n其余 MLOG_1BYTE、MLOG_2BYTE、MLOG_4BYTE 类型的 redo 日志结构和MLOG_8BYTE 的类似，只不过具体数据中包含对应个字节的数据而已。MLOG_WRITE_STRING 类型的 redo 日志表示写入一串数据，但是因为不能确定写入的具体数据占用多少字节，所以需要在日志结构中还会多一个 len 字段。\n7.3.2. 复杂 redo 日志类型 有些情况，执行一条语句会修改非常多的页面，包括系统数据页面和用户数据页面（用户数据指的就是聚簇索引和二级索引对应的B+树）。如：以 INSERT 语句为例，它除了要向 B+树的页面中插入数据，也可能更新系统数据 Max Row ID 的值。表中包含多少个索引，一条 INSERT 语句就可能更新多少棵 B+树。\n针对某一棵 B+树来说，既可能更新叶子节点页面，也可能更新非叶子节点页面，也可能创建新的页面（在该记录插入的叶子节点的剩余空间比较少，不足以存放该记录时，会进行页面的分裂，在非叶子节点页面中添加目录项记录）。\n在语句执行过程中，除了 INSERT 语句对所有页面的修改都保存到 redo 日志中去。一个数据页中除了存储实际的记录之后，还有什么 File Header、Page Header、Page Directory 等等部分，所以每往叶子节点代表的数据页里插入一条记录时，还有其他很多地方会跟着更新。\n比如：可能更新 Page Directory 中的槽信息、Page Header 中的各种页面统计信息，比如槽数量可能会更改，还未使用的空间最小地址可能会更改，本页面中的记录数量可能会更改，各种信息都可能会被修改，同时数据页里的记录是按照索引列从小到大的顺序组成一个单向链表的，每插入一条记录，还需要更新上一条记录的记录头信息中的 next_record 属性来维护这个单向链表。\n记录复杂的写入数据，有两种解决方案：\n方案一：在每个修改的地方都记录一条 redo 日志 方案二：将整个页面的第一个被修改的字节到最后一个修改的字节之间所有的数据当成是一条物理 redo 日志中的具体数据。 这些类型的 redo 日志既包含物理层面的意思，也包含逻辑层面的意思，具体指：\n物理层面看，这些日志都指明了对哪个表空间的哪个页进行了修改。 逻辑层面看，在系统崩溃重启时，并不能直接根据这些日志里的记载，将页面内的某个偏移量处恢复成某个数据，而是需要调用一些事先准备好的函数，执行完这些函数后才可以将页面恢复成系统崩溃前的样子。 一个 redo 日志类型而只是把在本页面中变动（比如插入、修改）一条记录所有必备的要素记了下来，之后系统崩溃重启时，服务器会调用相关向某个页面变动（比如插入、修改）一条记录的那个函数，而 redo 日志中的那些数据就可以被当成是调用这个函数所需的参数，在调用完该函数后，页面中的相关值也就都被恢复到系统崩溃前的值。\n总结：redo 日志会把事务在执行过程中对数据库所做的所有修改都记录下来，在之后系统崩溃重启后可以把事务所做的任何修改都恢复出来。\n7.4. Mini-Transaction 7.4.1. Mini-Transaction 的概念 MySQL 把对底层页面中的一次原子访问的过程称之为一个 Mini-Transaction。比如：修改一次 Max Row ID 的值算是一个 Mini-Transaction，向某个索引对应的 B+树中插入一条记录的过程也算是一个Mini-Transaction。\n一个 Mini-Transaction 可以包含一组 redo 日志，在进行崩溃恢复时这一组 redo 日志作为一个不可分割的整体。\n一个事务可以包含若干条语句，每一条语句其实是由若干个 Mini-Transaction 组成，每一个 Mini-Transaction 又可以包含若干条 redo 日志，最终形成了一个树形结构。\n7.4.2. 以组的形式写入 redo 日志 一条 INSERT 语句可能会修改很多的页面，对这些页面的更改都发生在 Buffer Pool 中，所以在修改完页面之后，需要记录一下相应的 redo 日志。而执行语句的过程中产生的 redo 日志被 InnoDB 人为的划分成了若干个不可分割的组。\n以向某个索引对应的 B+树插入一条记录为例，在向 B+树中插入这条记录之前，需要先定位到这条记录应该被插入到哪个叶子节点代表的数据页中，定位到具体的数据页之后，有两种可能的情况：\n情况一：该数据页的剩余的空闲空间充足，足够容纳这一条待插入记录，这种情况处理就很简单，直接把记录插入到这个数据页中，记录一条 redo 日志即可，把这种情况称之为乐观插入。 情况二：该数据页剩余的空闲空间不足。此时处理相对比较复杂，在这个处理过程要对多个页面进行修改，也就意味着会产生很多条 redo 日志，把这种情况称之为悲观插入。这种情况需要进行页的分裂操作： 新建一个叶子节点； 然后把原先数据页中的一部分记录复制到这个新的数据页中 然后再把记录插入进去，把这个叶子节点插入到叶子节点链表中 非叶子节点中添加一条目录项记录指向这个新创建的页面 非叶子节点空间不足，继续分裂 注意：在页的分裂过程中，由于需要新申请数据页，还需要改动一些系统页面，比方说要修改各种段、区的统计信息信息，各种链表的统计信息，也会产生redo日志。当然在乐观插入时也可能产生多条 redo 日志。\nInnoDB 认为向某个索引对应的 B+树中插入一条记录的这个过程必须是原子的，不能说插了一半之后就停止了。redo 日志是为了在系统崩溃重启时恢复崩溃前的状态，如果在悲观插入的过程中只记录了一部分 redo 日志，那么在系统崩溃重启时会将索引对应的 B+树恢复成一种不正确的状态。\n所以规定在执行这些需要保证原子性的操作时必须以组的形式来记录的 redo 日志，在进行系统崩溃重启恢复时，针对某个组中的 redo 日志，要么把全部的日志都恢复掉，要么一条也不恢复。在实现上，根据多个 redo 日志的不同，使用了特殊的 redo 日志类型作为组的结尾，来表示一组完整的 redo 日志。\n7.5. redo 日志的写入过程 7.5.1. redo log block 和日志缓冲区 InnoDB 为了更好的进行系统崩溃恢复，把通过 Mini-Transaction 生成的 redo 日志都放在了大小为 512 字节的块（block）中\nMySQL 为了解决磁盘速度过慢的问题而引入了 Buffer Pool。同理，写入 redo 日志时也不能直接直接写到磁盘上，实际上在服务器启动时就向操作系统申请了一大片称之为 redo log buffer 的连续内存空间，翻译成中文就是 redo 日志缓冲区，也可以简称为 log buffer。这片内存空间被划分成若干个连续的 redo log block，可以通过启动参数 innodb_log_buffer_size 来指定 log buffer 的大小，该启动参数的默认值为 16MB。\n向 log buffer 中写入 redo 日志的过程是顺序的，也就是先往前边的 block 中写，当该 block 的空闲空间用完之后再往下一个 block 中写。\nMini-Transaction 执行过程中可能产生若干条 redo 日志，这些 redo 日志是一个不可分割的组，所以其实并不是每生成一条 redo 日志，就将其插入到 log buffer 中，而是每个 Mini-Transaction 运行过程中产生的日志先暂时存到一个地方，当该 Mini-Transaction 结束的时候，将过程中产生的一组 redo 日志再全部复制到 log buffer 中。\n7.5.2. redo 日志刷盘时机 Mini-Transaction 运行过程中产生的一组 redo 日志是在 Mini-Transaction 结束时会被复制到 log buffer 中，但在一些情况下它们会被刷新到磁盘里，比如：\nlog buffer 空间不足时。log buffer 的大小是有限的（通过系统变量 innodb_log_buffer_size 指定），如果不停的往这个有限大小的 log buffer 里塞入日志，很快它就会被填满。InnoDB 认为如果当前写入 log buffer 的 redo 日志量已经占满了 log buffer 总容量的大约一半左右，就需要把这些日志刷新到磁盘上。 事务提交时。使用 redo 日志记录事务的操作主要是因为它占用的空间少，并且是顺序写，在事务提交时可以不把修改过的 Buffer Pool 页面刷新到磁盘，但是为了保证持久性，必须要把修改这些页面对应的 redo 日志刷新到磁盘 MySQL 后台有一个线程，大约每秒都会刷新一次 log buffer 中的 redo 日志到磁盘 正常关闭服务器时 7.5.3. redo 日志文件组 使用以下命令可以查看 MySQL 的数据目录，其中默认有两个名为 ib_logfile0 和 ib_logfile1 的文件，log buffer 中的日志默认情况下就是刷新到这两个磁盘文件中。\n1 2 -- 查询数据库的数据目录 SHOW VARIABLES LIKE \u0026#39;datadir\u0026#39;; 修改以下启动参数来设置 redo 日志文件\ninnodb_log_group_home_dir，该参数指定了 redo 日志文件所在的目录，默认值就是当前的数据目录 innodb_log_file_size，该参数指定了每个 redo 日志文件的大小，默认值为 48MB innodb_log_files_in_group，该参数指定 redo 日志文件的个数，默认值为 2，最大值为 100 磁盘上的 redo 日志文件可以不只一个，而是以一个日志文件组的形式出现的。这些文件以 ib_logfile[数字]（数字可以是 0、1、2\u0026hellip;）的形式进行命名。在将 redo 日志写入日志文件组时，是从 ib_logfile0 开始写，如果 ib_logfile0 写满了，就接着 ib_logfile1 写，同理，ib_logfile1 写满了就去写 ib_logfile2，依此类推。如果写满最后一个文件，就会重新转到 ib_logfile0 继续写。\n7.5.4. redo 日志文件格式 log buffer 本质上是一片连续的内存空间，被划分成了若干个 512 字节大小的 block。将 log buffer 中的 redo 日志刷新到磁盘的本质就是把 block 的镜像写入日志文件中，所以 redo 日志文件其实也是由若干个 512 字节大小的 block 组成。\nredo 日志文件组中的每个文件大小都一样，格式也一样，都是由两部分组成：前 2048 个字节，也就是前 4 个 block 是用来存储一些管理信息的。从第 2048 字节往后是用来存储 log buffer 中的 block 镜像的。\n7.6. Log Sequence Number 自MySQL系统开始运行，就不断的在修改页面，Redo 日志的量在不断的递增，InnoDB 为记录已经写入的 redo 日志量，设计了一个称之为 Log Sequence Number (日志序列号，简称 LSN)的全局变量。规定初始 LSN 的值为 8704（也就是一条 redo 日志也没写入时，LSN 的值为 8704）。\n在向 log buffer 中写入 redo 日志时不是一条一条写入的，而是以一个 Mini-Transaction 生成的一组 redo 日志为单位进行写入的。从上边的描述中可以看出来，每一组由 Mini-Transaction 生成的 redo 日志都有一个唯一的 LSN 值与其对应，LSN 值越小，说明 redo 日志产生的越早。\n7.6.1. flushed_to_disk_lsn InnoDB 中有一个 buf_next_to_write 的全局变量，标记当前 log buffer 中已经有哪些日志被刷新到磁盘中了。\nLSN 是表示当前系统中写入的 redo 日志量，这包括了写到 log buffer 而没有刷新到磁盘的日志，相应的，InnoDB 也有一个表示刷新到磁盘中的 redo 日志量的全局变量 flushed_to_disk_lsn。\n系统第一次启动时，flushed_to_disk_lsn变量的值和初始的 lsn 值是相同的，都是 8704。随着系统的运行，redo 日志被不断写入 log buffer，但是并不会立即刷新到磁盘，lsn 的值就和 flushed_to_disk_lsn 的值也不断变化\n当有新的 redo 日志写入到 log buffer 时，首先 LSN 的值会增长，但flushed_to_disk_lsn不变，随后随着不断有log buffer中的日志被刷新到磁盘上，flushed_to_disk_lsn的值也跟着增长。如果两者的值相同时，说明 log buffer 中的所有 redo 日志都已经刷新到磁盘中了。\nTips: 应用程序向磁盘写入文件时其实是先写到操作系统的缓冲区中去，如果某个写入操作要等到操作系统确认已经写到磁盘时才返回，那需要调用一下操作系统提供的 fsync 函数。其实只有当系统执行了 fsync 函数后，flushed_to_disk_lsn 的值才会跟着增长，当仅仅把 log buffer 中的日志写入到操作系统缓冲区却没有显式的刷新到磁盘时，另外的一个名为 write_lsn 的值跟着增长。当然系统的 LSN 值远不止前面描述的 lsn，还有很多。\n7.6.2. 查看系统中的各种 LSN 值 查看当前 InnoDB 存储引擎中的各种 LSN 值的情况\n1 SHOW ENGINE INNODB STATUS; Log sequence number：代表系统中的 lsn 值，也就是当前系统已经写入的 redo日志量，包括写入 log buffer 中的日志。 Log flushed up to：代表 flushed_to_disk_lsn 的值，也就是当前系统已经写入磁盘的 redo 日志量。 Pages flushed up to：代表 flush 链表中被最早修改的那个页面对应的 oldest_modification 属性值。 Last checkpoint at：当前系统的 checkpoint_lsn 值。 7.7. redo log 的写入策略参数 innodb_flush_log_at_trx_commit 为了保证事务的持久性，用户线程在事务提交时需要将该事务执行过程中产生的所有 redo 日志都刷新到磁盘上，但这样会很明显的降低数据库性能。\nMySQL 提供了控制 redo log 的写入策略的系统变量参数 innodb_flush_log_at_trx_commit，该变量有 3 个可选的值：\n当该系统变量值为0时，表示在事务提交时不立即将 redo log buffer 中向磁盘中同步 redo 日志，这个任务是交给后台线程做的。数据库宕机可能会丢失数据。 当该系统变量值为1时（默认值），表示在事务提交时需要将 redo 日志同步持久化到磁盘。此方式可以保证事务的持久性，数据最安全，不会因为数据库宕机丢失数据，但是效率稍微差一点，线上系统推荐这个设置。 当该系统变量值为2时，表示在事务提交时需要将 redo 日志写到操作系统的缓冲区（page cache）中，但并不需要马上将日志真正的刷新到磁盘。这种情况如果数据库宕机，事务的持久性还是可以保证的，不会丢失数据的；但如果操作系统宕机了，page cache 里的数据还没来得及写入磁盘文件的话，那就不能保证持久性了，会丢失数据。 7.7.1. redo log 写入策略流程图 InnoDB 有一个后台线程，每隔 1 秒，就会把 redo log buffer 中的日志，调用操作系统函数 write 写到文件系统的 page cache，然后调用操作系统函数 fsync 持久化到磁盘文件。\n7.7.2. 查询与设置参数 查询看当前事务 redo 日志刷盘参数\n1 2 3 4 5 6 7 mysql\u0026gt; show variables like \u0026#39;innodb_flush_log_at_trx_commit\u0026#39;; +--------------------------------+-------+ | Variable_name | Value | +--------------------------------+-------+ | innodb_flush_log_at_trx_commit | 1 | +--------------------------------+-------+ 1 row in set (0.02 sec) 设置 innodb_flush_log_at_trx_commit 参数值(也可以在 my.ini 或 my.cnf 文件里配置)：\n1 set global innodb_flush_log_at_trx_commit=1; 7.8. redo log 的执行流程 假设执行的 SQL 如下：\n1 update T set a =1 where id =666 MySQL 客户端将请求语句 update T set a =1 where id = 666，发往 MySQL Server 层。 MySQL Server 层接收到 SQL 请求后，对其进行分析、优化、执行等处理工作，将生成的 SQL 执行计划发到 InnoDB 存储引擎层执行。 InnoDB 存储引擎层将 a 修改为 1 的这个操作记录到内存中。 记录到内存以后会修改 redo log 的记录，会在添加一行记录，其内容是需要在哪个数据页上做什么修改。 此后，将事务的状态设置为 prepare ，说明已经准备好提交事务了。 等到 MySQL Server 层处理完事务以后，会将事务的状态设置为 commit，也就是提交该事务。 在收到事务提交的请求以后，redo log 会把刚才写入内存中的操作记录写入到磁盘中，从而完成整个日志的记录过程。 7.9. 崩溃后的恢复 7.9.1. 恢复机制 MySQL 可以根据 redo 日志中的各种 LSN 值，来确定恢复的起点和终点。然后将 redo 日志中的数据，以哈希表的形式，将一个页面下的放到哈希表的一个槽中。之后就可以遍历哈希表，因为对同一个页面进行修改的 redo 日志都放在了一个槽里，所以可以一次性将一个页面修复好（避免了很多读取页面的随机 IO）。并且通过各种机制，避免无谓的页面修复，比如已经刷新的页面，进而提升崩溃恢复的速度\n7.9.2. 崩溃后的恢复为什么不用 binlog？ redo log 与 binlog 两者使用方式不一样。binlog 会记录表所有更改操作，包括更新删除数据，更改表结构等等，主要用于人工恢复数据；而 redo log 对于用户是不可见的，它是 InnoDB 用于保证 crash-safe 能力的，也就是在事务提交后 MySQL 崩溃的话，可以保证事务的持久性，即事务提交后其更改是永久性的。 redo log 是 InnoDB 引擎特有的；binlog 是 MySQL 的 Server 层实现的，所有引擎都可以使用。 redo log 是物理日志，记录的是“在某个数据页上做了什么修改”，恢复的速度更快；binlog 是逻辑日志，记录的是这个语句的原始逻辑，比如“给ID=2为的记录的c字段加1” redo log 是“循环写”的日志文件，redo log 只会记录未刷盘的日志，已经刷入磁盘的数据都会从 redo log 这个有限大小的日志文件里删除。binlog 是追加日志，保存的是全量的日志。 重点：当数据库 crash（崩溃）后，想要恢复未刷盘但已经写入 redo log 和 binlog 的数据到内存时，binlog 是无法恢复的。虽然 binlog 拥有全量的日志，但没有一个标志让 innoDB 判断哪些数据已经入表(写入磁盘)，哪些数据还没有。 比如，binlog 记录了两条日志：\n给 ID=2 这一行的 c 字段加 1 给 ID=2 这一行的 c 字段加 1 在记录 1 入表后，记录 2 未入表时，数据库 crash。重启后，只通过 binlog 数据库无法判断这两条记录哪条已经写入磁盘，哪条没有写入磁盘，不管是两条都恢复至内存，还是都不恢复，对 ID=2 这行数据来说，都不对。\n但 redo log 不一样，只要刷入磁盘的数据，都会从 redo log 中抹掉，数据库重启后，直接把 redo log 中的数据都恢复至内存就可以了。\n8. undo log（撤销日志） InnoDB 对 undo log 文件的管理采用段的方式，也就是回滚段（rollback segment）。每个回滚段记录了 1024 个 undo log segment ，每个事务只会使用一个 undo log segment。\nMySQL 5.5 版本，只有一个回滚段，最大同时支持的事务数量为1024个。从 MySQL 5.6 版本开始，InnoDB 支持最大 128 个回滚段，故其支持同时在线的事务限制提高到了 128*1024。\n8.1. 查询 undo log 参数 1 2 3 4 5 6 7 8 9 mysql\u0026gt; SHOW VARIABLES LIKE \u0026#39;%innodb_undo_%\u0026#39;; +--------------------------+-------+ | Variable_name | Value | +--------------------------+-------+ | innodb_undo_directory | .\\ | | innodb_undo_log_encrypt | OFF | | innodb_undo_log_truncate | ON | | innodb_undo_tablespaces | 2 | +--------------------------+-------+ 参数说明：\ninnodb_undo_directory：设置 undo log 文件所在的路径。该参数的默认值为\u0026quot;./\u0026quot;，即 innodb 数据文件存储位置，目录下 ibdata1 文件就是 undo log 存储的位置。 innodb_undo_logs：设置 undo log 文件内部回滚段的个数，默认值为 128。 innodb_undo_tablespaces：设置 undo log 文件的数量，这样回滚段可以较为平均地分布在多个文件中。设置该参数后，会在路径 innodb_undo_directory 看到 undo 为前缀的文件。 8.2. 事务回滚 事务需要保证原子性，要么全部成功，要么全部失败。比如：\n事务执行过程中可能遇到各种错误，比如服务器本身的错误，操作系统错误，甚至是突然断电导致的错误。 在事务执行过程中手动输入 ROLLBACK 语句结束当前的事务的执行。 以上情况都会导致事务执行到一半就结束。为了保证事务的原子性，需要把东西改回原先的样子，这个过程就称之为回滚（英文名：rollback）。\n每当要对一条记录做改动时（改动是指 INSERT、DELETE、UPDATE等），都需要把回滚时所需的内容都给记录下来。例如：\n插入一条记录时，至少要把这条记录的主键值记下来，之后回滚的时候只需要把这个主键值对应的记录删掉。 删除了一条记录，至少要把这条记录中的内容都记下来，这样之后回滚时再把由这些内容组成的记录插入到表中。 修改了一条记录，至少要把修改这条记录前的旧值都记录下来，这样之后回滚时再把这条记录更新为旧值。 为了回滚而记录的这些东西称之为撤销日志，英文名为 undo log（undo日志）。这里需要注意的一点是，由于查询操作（SELECT）并不会修改任何用户记录，所以在查询操作执行时，并不需要记录相应的 undo 日志。在InnoDB中，不同类型的操作产生的 undo 日志的格式也是不同的\n8.3. 事务ID 8.3.1. 给事务分配 id 的时机 一个事务可以是一个只读事务，或者是一个读写事务。\n开启一个只读事务 1 START TRANSACTION READ ONLY; 开启一个读写事务 1 2 3 4 5 START TRANSACTION READ WRITE; -- 以下方式开启的事务默认也算是读写事务。 BEGIN; START TRANSACTION; 如果某个事务执行过程中对某个表执行了增、删、改操作，那么 InnoDB 存储引擎就会给它分配一个独一无二的事务 id，分配方式如下：\n对于只读事务来说，只有在它第一次对某个用户创建的临时表执行增、删、改操作时才会为这个事务分配一个事务 id，否则的话是不分配事务 id 的。 对于读写事务来说，只有在它第一次对某个表（包括用户创建的临时表）执行增、删、改操作时才会为这个事务分配一个事务 id，否则的话也是不分配事务 id 的。 注：上面描述的事务id分配策略是针对 MySQL 5.7 而言，以前版本的分配方式可能不同。\n8.3.2. 事务 id 生成机制 事务 id 本质上就是一个数字。分配策略和隐藏列 row_id（当用户没有为表创建主键和 UNIQUE 键时 InnoDB 自动创建的列）的分配策略大致相同。具体策略如下：\n服务器会在内存中维护一个全局变量，每当需要为某个事务分配一个事务id时，就会把该变量的值当作事务id分配给该事务，并且把该变量自增1。\n每当这个变量的值为 256 的倍数时，就会将该变量的值刷新到系统表空间的页号为 5 的页面中一个称之为 Max Trx ID 的属性处，这个属性占用 8 个字节的存储空间。\n当系统下一次重新启动时，会将上边提到的 Max Trx ID 属性加载到内存中，将该值加上 256 之后赋值给我们前边提到的全局变量（因为在上次关机时该全局变量的值可能大于 Max Trx ID 属性值）。\n这样就可以保证整个系统中分配的事务 id 值是一个递增的数字。先被分配id 的事务得到的是较小的事务 id，后被分配 id 的事务得到的是较大的事务 id。\n8.4. 隐藏列 聚簇索引的记录除了会保存完整的用户数据以外，而且还会自动添加名为 trx_id、roll_pointer 的隐藏列，如果用户没有在表中定义主键以及 UNIQUE 键，还会自动添加一个名为 row_id 的隐藏列。\n其中的 trx_id 列就是某个对这个聚簇索引记录做改动的语句（INSERT、DELETE、UPDATE 操作）所在的事务对应的事务 id\n隐藏字段及其含义分别是：\n隐藏字段 含义 DB_TRX_ID 最近修改事务ID，记录插入这条记录或最后一次修改该记录的事务ID DB_ROLL_PTR 回滚指针，指向这条记录的上一个版本，用于配合undo log，指向上一个版本 DB_ROW_ID 隐藏主键，如果表结构没有指定主键，将会生成该隐藏字段 8.5. undo 日志的格式 InnoDB 存储引擎在实际进行增、删、改记录时，都需要记录对应的 undo 日志。一般每1条记录的改动都对应1条undo日志，但在某些更新记录操作中，可能会对应2条undo日志\n一个事务在执行过程中涉及新增、删除、更新若干条记录，也就是说需要记录很多条对应的 undo 日志，这些 undo 日志会被从0开始编号，也就是说根据生成的顺序分别被称为第0号undo日志、第1号undo日志、\u0026hellip;、第n号undo日志等，这个编号也被称之为undo no。\nundo 日志是被记录到类型为FIL_PAGE_UNDO_LOG的页面中。这些页面可以从系统表空间中分配，也可以从一种专门存放 undo 日志的表空间，也就是所谓的 undo tablespace 中分配。\n8.5.1. INSERT 操作对应的 undo 日志 8.5.1.1. 基础处理流程 InnoDB 的设计了一个类型为TRX_UNDO_INSERT_REC的undo日志。当插入记录后回滚时，只需要将该记录删除即可，所以undo日志主要是记录该记录的主键信息。\n当主键只包含一个列，类型为TRX_UNDO_INSERT_REC的undo日志中只需要记录该列占用的存储空间大小和真实值 当主键包含多个列，则需要记录每个列占用的存储空间大小和对应的真实值 当向某个表中插入一条记录时，实际上需要向聚簇索引和所有的二级索引都插入一条记录。不过记录 undo 日志时，只需要考虑向聚簇索引插入记录时的情况，因为聚簇索引记录和二级索引记录是一一对应的，在回滚插入操作时，只需要知道这条记录的主键信息，然后根据主键信息做对应的删除操作，做删除操作时就会顺带着把所有二级索引中相应的记录也删除掉。DELETE 操作和 UPDATE 操作对应的 undo 日志也都是针对聚簇索引记录而言的。\n8.5.1.2. roll_pointer 的作用 roll_pointer 本质上就是一个指向记录对应的 undo 日志的一个指针。比如向表里插入了 2 条记录，每条记录都有与其对应的一条 undo 日志。记录被存储到了类型为FIL_PAGE_INDEX的页面中（数据页），undo 日志被存放到了类型为FIL_PAGE_UNDO_LOG的页面中。\n8.5.2. DELETE 操作对应的 undo 日志 8.5.2.1. 基础处理流程 插入到页面中的记录会根据记录头信息中的 next_record 属性组成一个单向链表，该链表称之为正常记录链表。删除的记录也会根据记录头信息中的 next_record 属性组成一个链表，只是这个链表中的记录占用的存储空间可以被重新利用，所以这个链表也称为垃圾链表。\nPage Header 部分有一个称之为PAGE_FREE的属性，它指向由被删除记录组成的垃圾链表中的头节点。\n注：上图示例只把记录的 delete_mask 标志位展示了出来\n上面示例，正常记录链表中包含了 3 条正常记录，垃圾链表里包含了 2 条已删除记录。页面的 Page Header 部分的 PAGE_FREE 属性的值代表指向垃圾链表头节点的指针。假设使用 DELETE 语句删除正常记录链表中的最后一条记录，该删除的过程需要经历两个阶段：\n阶段一：将记录的delete_mask标识位设置为1，这个阶段称之为delete mark。正常记录链表中的最后一条记录的delete_mask值被设置为 1，但是并没有被加入到垃圾链表。也就是此时记录处于一个中间状态。在删除语句所在的事务提交之前，被删除的记录一直都处于这种所谓的中间状态。 阶段二：当该删除语句所在的事务提交之后，会有专门的线程来真正删除记录。所谓真正的删除就是把该记录从正常记录链表中移除，并且加入到垃圾链表中，然后还要调整一些页面的其他信息，比如页面中的用户记录数量 PAGE_N_RECS、上次插入记录的位置 PAGE_LAST_INSERT、垃圾链表头节点的指针 PAGE_FREE、页面中可重用的字节数量 PAGE_GARBAGE、还有页目录的一些信息等等。这个阶段称之为 purge。 在阶段二执行完后，这条记录就算是真正的被删除。这条已删除记录占用的存储空间也可以被重新利用。\n在删除语句所在的事务提交之前，只会经历阶段一，也就是 delete mark 阶段（提交之后就不用回滚了，所以只需考虑对删除操作的阶段一做的影响进行回滚）。InnoDB 中就会产生一种称之为TRX_UNDO_DEL_MARK_REC类型的 undo 日志。\n8.5.2.2. 版本链 在对一条记录进行 delete mark 操作前，需要把该记录的旧的 trx_id 和 roll_pointer 隐藏列的值都给记到对应的 undo 日志中来，就是图中显示的 old trx_id 和 old roll_pointer 属性。这样有一个好处，那就是可以通过 undo 日志的 old roll_pointer 找到记录在修改之前对应的 undo 日志。比方说在一个事务中，先插入了一条记录，然后又执行对该记录的删除操作，这个过程的示意图如下：\n执行完 delete mark操作后，它对应的 undo日志和 INSERT 操作对应的 undo 日志就串成了一个链表。这个链表就称之为版本链。\n8.5.3. UPDATE 操作对应的 undo 日志 在执行 UPDATE 语句时，InnoDB 对更新主键和不更新主键这两种情况有不同的处理方案\n8.5.3.1. 不更新主键的情况 在不更新主键的情况下，又可以细分为被更新的列占用的存储空间不发生变化和发生变化的情况。\n就地更新（in-place update）\n更新记录时，对于被更新的每个列来说，如果更新后的列和更新前的列占用的存储空间都一样大，那么就可以进行就地更新，也就是直接在原记录的基础上修改对应列的值。需要注意的一点是，如果有任何一个被更新的列更新前比更新后占用的存储空间大，或者更新前比更新后占用的存储空间小都不能进行就地更新。\n先删除掉旧记录，再插入新记录\n在不更新主键的情况下，如果有任何一个被更新的列更新前和更新后占用的存储空间大小不一致，那么就需要先把这条旧的记录从聚簇索引页面中删除掉，然后再根据更新后列的值创建一条新的记录插入到页面中。\n值得注意的是，此删除并不是 delete mark 操作，而是真正的删除掉，也就是把这条记录从正常记录链表中移除并加入到垃圾链表中，并且修改页面中相应的统计信息（比如 PAGE_FREE、PAGE_GARBAGE 等这些信息）。由用户线程同步执行真正的删除操作，真正删除之后紧接着就要根据各个列更新后的值创建的新记录插入。\n如果新创建的记录占用的存储空间大小不超过旧记录占用的空间，那么可以直接重用被加入到垃圾链表中的旧记录所占用的存储空间，否则的话需要在页面中新申请一段空间以供新记录使用，如果本页面内已经没有可用的空间的话，那就需要进行页面分裂操作，然后再插入新记录。\n针对 UPDATE 不更新主键的情况（包括上边所说的就地更新和先删除旧记录再插入新记录），InnoDB 设计了一种类型为TRX_UNDO_UPD_EXIST_REC的 undo 日志。\n8.5.3.2. 更新主键的情况 在聚簇索引中，记录是按照主键值的大小连成了一个单向链表的，如果更新了某条记录的主键值，意味着这条记录在聚簇索引中的位置将会发生改变。并且更新的记录可能相隔很大，中间隔好多个页，针对 UPDATE 语句中更新了记录主键值的这种情况，InnoDB 在聚簇索引中分了两步处理：\n将旧记录进行 delete mark 操作\n在 UPDATE 语句所在的事务提交前，对旧记录只做一个 delete mark 操作，在事务提交后才由专门的线程做 purge 操作，把它加入到垃圾链表中。注意区别：不更新记录主键值的情况，会先真正删除旧记录，再插入新记录\n之所以只对旧记录做 delete mark 操作，是因为别的事务同时也可能访问这条记录，如果把它真正的删除加入到垃圾链表后，别的事务就不能访问了。此功能就是MVCC\n创建一条新记录\n根据更新后各列的值创建一条新记录，并将其插入到聚簇索引中（需重新定位插入的位置）。由于更新后的记录主键值发生了改变，所以需要重新从聚簇索引中定位这条记录所在的位置，然后把它插进去。\n针对 UPDATE 语句更新记录主键值的这种情况，在对该记录进行 delete mark 操作前，会记录一条类型为TRX_UNDO_DEL_MARK_REC的 undo 日志；之后插入新记录时，会记录一条类型为TRX_UNDO_INSERT_REC的 undo 日志，也就是说每对一条记录的主键值做改动时，会记录 2 条 undo 日志\n8.6. FIL_PAGE_UNDO_LOG 页面 表空间其实是由许许多多的页面构成的，页面默认大小为16KB。这些页面有不同的类型，比如类型为FIL_PAGE_INDEX的页面用于存储聚簇索引以及二级索引，类型为FIL_PAGE_TYPE_FSP_HDR的页面用于存储表空间头部信息的，还有其他各种类型的页面，其中FIL_PAGE_UNDO_LOG类型的页面是专门用来存储 undo 日志的。\n8.7. undo log 日志删除时机 新增操作，在事务提交之后就可以清除掉了。 修改操作，事务提交之后不能立即清除掉，这些日志会用于 mvcc。只有当 MySQL 检测到没有事务用到该版本信息时才可以清除。 9. MVCC（多版本并发控制） 9.1. 事务并发执行遇到的问题 详见前面章节《事务并发引发的3个问题》\n不同的数据库厂商对 SQL 标准中规定的四种隔离级别支持不一样，MySQL 在 REPEATABLE READ 隔离级别下，是可以很大程度避免幻读问题的发生的\n9.2. MVCC 的概述 MVCC (Multi-Version Concurrency Control)，即多版本并发控制，主要是为了提高数据库的并发性能。\n串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的。而在读已提交和可重复读的隔离级别下，是通过 MVCC 机制来保证的，对同一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性，避免了频繁加锁互斥。这里的『读』是指的快照读，而不是当前读，当前读加锁操作是一种悲观锁。\nMVCC 机制是通过 read-view 机制与 undo 版本链比对机制来实现，使得不同的事务会根据版本链对比规则读取同一条数据在版本链上的不同版本数据。\n9.3. MVCC 的核心组成部分 9.3.1. undo 日志版本链 对于使用 InnoDB 存储引擎的表来说，它的聚簇索引记录中都包含两个必要的隐藏列（row_id 并不是必要的，创建的表中有主键或者非 NULL 的 UNIQUE 键时都不会包含 row_id 列）\ntrx_id：每次一个事务对某条聚簇索引记录进行改动时，都会把该事务的事务 id 赋值给 trx_id 隐藏列。 roll_pointer：每次对某条聚簇索引记录进行改动时，都会把旧的版本写入到 undo 日志中，然后这个隐藏列就相当于一个指针，可以通过它来找到该记录修改前的信息。 对该记录每次更新后，都会将旧值放到一条 undo 日志中，就算是该记录的一个旧版本，随着更新次数的增多，所有的版本都会被 roll_pointer 属性连接成一个链表，这个链表称为版本链，版本链的头节点就是当前记录最新的值。另外，每个版本中还包含生成该版本时对应的事务 id。于是可以利用这个记录的版本链来控制并发事务访问相同记录的行为，那么这种机制就被称之为多版本并发控制(Mulit-Version Concurrency Control MVCC)。\n9.3.2. ReadView 为了实现不同事务的控制，InnoDB 提出了一个 ReadView 的概念。ReadView（读视图）是快照读 SQL 执行时 MVCC 提取数据的依据，在 ReadView 内部记录并维护一个系统当前活跃事务链表（未提交的），表示生成 ReadView 的时候还在活跃的事务。该链表包含在创建 ReadView 之前还未提交的事务，不包含创建 ReadView 之后提交的事务。\n值得注意的是，begin/start transaction 命令并不是一个事务的起点，而是在它们之后执行的第一个修改操作或加排它锁操作(比如select...for update)的语句，事务才真正启动，此时才会向 mysql 申请真正的事务 id，mysql 内部是严格按照事务的启动顺序来分配事务 id。\nReadView 中主要包含4个比较重要的内容：\nm_ids：表示在生成 ReadView 时当前系统中活跃的读写事务的事务 id 列表。 min_trx_id：表示在生成 ReadView 时当前系统中活跃的读写事务中最小的事务 id，也就是 m_ids 中的最小值。 max_trx_id：表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。注意 max_trx_id 并不是 m_ids 中的最大值，事务 id 是递增分配的。比方说现在有 id 为 1，2，3 这三个事务，之后 id 为 3 的事务提交了。那么一个新的读事务在生成 ReadView 时，m_ids 就包括 1 和 2，min_trx_id 的值就是 1，max_trx_id的值就是 4。 creator_trx_id：表示生成该 ReadView 的事务 id。 有了这个 ReadView，在访问某条记录时，只需要按照相关的规则，判断记录的某个版本是否可见。\n9.4. undo 日志版本链与 ReadView 机制详解 9.4.1. ReadView 访问版本链数据的规则 假设 trx_id 是当前访问的版本链数据的事务ID。其版本链数据访问规则如下：\ntrx_id == creator_trx_id：说明数据是当前这个事务更改的，可以访问该版本。 trx_id \u0026lt; min_trx_id：说明数据已经提交了，可以访问该版本。 trx_id \u0026gt; max_trx_id：说明该事务是在 ReadView 生成后才开启。 min_trx_id \u0026lt;= trx_id \u0026lt;= max_trx_id，分如下两种情况： 如果 trx_id 不在 m_ids 中，说明数据已经提交。可以访问该版本 如果 trx_id 在 m_ids 中，说明创建 ReadView 时生成该版本的事务还是活跃的，该版本不可以被访问 9.4.2. 不同隔离级别事务读取记录的区别 对于使用 READ UNCOMMITTED 隔离级别的事务来说，由于可以读到未提交事务修改过的记录，所以直接读取记录的最新版本即可。 对于使用 SERIALIZABLE 隔离级别的事务来说，InnoDB 使用加锁的方式来访问记录。 对于使用 READ COMMITTED 和 REPEATABLE READ 隔离级别的事务来说，都必须保证读到已经提交了的事务修改过的记录，也就是说假如另一个事务已经修改了记录但是尚未提交，是不能直接读取最新版本的记录的 因此，READ COMMITTED 和 REPEATABLE READ 这两种隔离级别关键是需要判断一下版本链中的哪个版本是当前事务可见的。\n9.4.3. 不同隔离级别创建 ReadView 的时机 在 MySQL 中，READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同。\n实现 READ COMMITTED 隔离级别，事务里每次执行查询操作时都会按照数据库当前状态重新生成 readview，也就是每次查询都是跟数据库里当前所有事务提交状态来比对数据是否可见，因此可以实现每次都能查到已提交的最新数据的效果。 实现 REPEATABLE READ 隔离级别，事务里每次执行查询操作时都是使用第一次查询时生成的 readview，也就是都是以第一次查询时当时数据库里所有事务提交状态来比对数据是否可见，因此可以实现每次查询的可重复读的效果。 9.4.4. 修改操作的事务实现流程原理解析 以下是 REPEATABLE READ 隔离级别下的事务实现说明。假设插入一条记录的事务 id 为 80 并且已经提交事务，后面有3个事务 id 分别为 300、100、200 的事务对这条记录进行 UPDATE 操作，操作流程如下：\n版本链比对规则如下：\n如果被访问版本的 trx_id 属性值与 ReadView 中的 creator_trx_id 值相同，意味着当前事务在访问它自己修改过的记录，所以该版本可以被当前事务访问。 如果被访问版本的 trx_id 属性值小于 ReadView 中的 min_trx_id 值，表明生成该版本的事务在当前事务生成 ReadView 前已经提交，所以该版本可以被当前事务访问。即 row 的 trx_id 在上图的绿色部分( trx_id \u0026lt; min_trx_id )，数据是可见的。 如果被访问版本的 trx_id 属性值大于或等于 ReadView 中的 max_trx_id 值，表明生成该版本的事务在当前事务生成 ReadView 后才开启，所以该版本不可以被当前事务访问。即 row 的 trx_id 在上图的红色部分，数据是不可见。(若 row 的 trx_id 就是当前自己的事务是可见的） 如果被访问版本的 trx_id 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间(min_trx_id \u0026lt; trx_id \u0026lt; max_trx_id)，即 row 的 trx_id 在上图的黄色部分。那就需要判断一下 trx_id 属性值是不是在 m_ids 列表中，包括以下两种情况： 如果 row 的 trx_id 在视图数组中，说明创建 ReadView 时生成该版本的事务还是活跃的，该版本不可以被访问(若 row 的 trx_id 就是当前自己的事务是可见的)； 如果 row 的 trx_id 不在视图数组中，说明创建 ReadView 时生成该版本的事务已经被提交，该版本可以被访问。 如果某个版本的数据对当前事务不可见的话，那就顺着版本链找到下一个版本的数据，继续按照上边的步骤判断可见性，依此类推，直到版本链中的最后一个版本。如果最后一个版本也不可见的话，那么就意味着该条记录对该事务完全不可见，查询结果就不包含该记录 9.4.5. 删除操作的事务实现原理 对于删除的情况可以认为是 update 操作的特殊情况，会将版本链上最新的数据复制一份，然后将 trx_id 修改成删除操作的 trx_id，同时将该条记录的头信息（record header）里的（deleted_flag）标记位设置为 true，用于表示当前记录已经被删除，在查询时按照上面的规则查到对应的记录如果 delete_flag 标记位为 true，意味着记录已被删除，则不返回数据。\n9.5. (！待整理)MVCC 下的幻读解决和幻读现象 TODO: 待整理\n9.6. MVCC 小结 MVCC（Multi-Version Concurrency Control ，多版本并发控制）指的就是在使用 READ COMMITTD、REPEATABLE READ 这两种隔离级别的事务在执行普通的 SELECT 操作时访问记录的版本链的过程，这样子可以使不同事务的读-写、写-读操作并发执行，从而提升系统性能。\nMVCC 的实现原理就是通过 InnoDB 表的隐藏字段、UndoLog 版本链、ReadView 来实现的。版本链保存有历史版本记录，通过 ReadView 判断当前版本的数据是否可见，如果不可见，再从版本链中找到上一个版本，继续进行判断，直到找到一个可见的版本。\n而 MVCC+锁，则实现了事务的隔离性。而一致性则是由 redolog 与 undolog 保证。\nREAD COMMITTD、REPEATABLE READ 这两个隔离级别的一个很大不同就是：生成 ReadView 的时机不同\nREAD COMMITTD 在每一次进行普通 SELECT 操作前都会生成一个 ReadView REPEATABLE READ 只在第一次进行普通 SELECT 操作前生成一个 ReadView，之后的查询操作都重复使用该 ReadView，从而基本上可以避免幻读现象 执行 DELETE 语句或者更新主键的 UPDATE 语句并不会立即把对应的记录完全从页面中删除，而是执行一个所谓的 delete mark 操作，相当于只是对记录打上了一个删除标志位，这主要就是为MVCC服务。MVCC 只是在进行普通的 SEELCT 查询时才生效\n10. MySQL 的 XA 协议 官方文档：https://dev.mysql.com/doc/refman/8.0/en/xa.html\n10.1. XA 协议 XA 协议是由 X/Open 组织提出的分布式事务处理规范，主要定义了事务管理器 TM 和局部资源管理器 RM 之间的接口。目前主流的数据库，比如 Oracle、DB2 都是支持 XA 协议的。MySQL 从 5.0 版本开始，innoDB 存储引擎已经支持 XA 协议。\nXA 定义了全局的事务管理器（Transaction Manager，用于协调全局事务）和局部的资源管理器（Resource Manager，用于驱动本地事务）之间的通讯接口。XA 接口是双向的，是一个事务管理器和多个资源管理器之间通信的桥梁，通过协调多个数据源的动作保持一致，来实现全局事务的统一提交或者统一回滚。\nMySQL XA 实现了分布式事务，是一种二阶段提交协议。Java 基于 XA 协议提出了 JTA（Java Transaction API）接口规范：\n事务管理器的接口：javax.transaction.TransactionManager 资源定义接口：javax.transaction.xa.XAResource 具体实现由 Java EE 容器提供\n10.2. MySQL 的 XA 协议处理流程 10.2.1. 涉及的角色 AP（Application Program）：应用程序，定义事务边界（定义事务开始和结束）并访问事务边界内的资源。 RM（Resource Manger）资源管理器: 管理共享资源并提供外部访问接口。供外部程序来访问数据库等共享资源。此外，RM还具有事务的回滚能力。 TM（Transaction Manager）事务管理器：TM是分布式事务的协调者，TM与每个RM进行通信，负责管理全局事务，分配事务唯一标识，监控事务的执行进度，并负责事务的提交、回滚、失败恢复等。 10.2.2. 具体处理流程 应用程序AP向事务管理器TM发起事务请求 TM调用xa_open()建立同资源管理器的会话 TM调用xa_start()标记一个事务分支的开头 AP访问资源管理器RM并定义操作，比如插入记录操作 TM调用xa_end()标记事务分支的结束 TM调用xa_prepare()通知RM做好事务分支的提交准备工作。其实就是二阶段提交的提交请求阶段。 TM调用xa_commit()通知RM提交事务分支，也就是二阶段提交的提交执行阶段。 TM调用xa_close管理与RM的会话。 Notes: 这些接口一定要按顺序执行，比如 xa_start 接口一定要在 xa_end 之前。此外，这里千万要注意的是事务管理器只是标记事务分支并不执行事务，事务操作最终是由应用程序通知资源管理器完成的。\n10.2.3. XA 接口方法说明 xa_start：负责开启或者恢复一个事务分支，并且管理 XID 到调用线程 xa_end：负责取消当前线程与事务分支的关系 xa_prepare：负责询问 RM 是否准备好了提交事务分支 xa_commit：通知 RM 提交事务分支 xa_rollback：通知 RM 回滚事务分支 10.3. MySQL XA 事务 MySQL 的 XA 事务分为两部分：\nInnoDB 内部本地普通事务操作协调数据写入与 log 写入两阶段提交 外部分布式事务 5.7 版本查询 XA 事务支持情况：\n1 SHOW VARIABLES LIKE \u0026#39;%innodb_support_xa%\u0026#39;; 8.0 默认开启并且无法关闭。\n","permalink":"https://ktzxy.top/posts/vagl5mvrjc/","summary":"MySQL 事务","title":"MySQL 事务"},{"content":"什么是正则表达式？ 正则表达式（Regular Expression）通常被用来检索、替换那些符合某个模式 (规则) 的文本。\n此处的 Regular 即是规则、规律的意思，Regular Expression 即 “描述某种规则的表达式” 之意。\n本文收集了一些常见的正则表达式用法，方便大家查询取用，并在最后附了详细的正则表达式语法手册。\n案例包括：「邮箱、身份证号、手机号码、固定电话、域名、IP 地址、日期、邮编、密码、中文字符、数字、字符串」\nPython 如何支持正则？ 我用的是 python 来实现正则，并使用 Jupyter Notebook 编写代码。\nPython 通过 re 模块支持正则表达式，re 模块使 Python 语言拥有全部的正则表达式功能。\n这里要注意两个函数的使用：\nre.compile用于编译正则表达式，生成一个正则表达式（ Pattern ）对象;\n.findall用于在字符串中找到正则表达式所匹配的所有子串，并返回一个列表，如果没有找到匹配的，则返回空列表。\n1 2 # 导入re模块 import re 邮箱 包含大小写字母，下划线，阿拉伯数字，点号，中划线\n表达式：\n[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(?:\\.[a-zA-Z0-9_-]+)\n案例：\n1 2 3 4 5 6 pattern = re.compile(r\u0026#34;[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(?:\\.[a-zA-Z0-9_-]+)\u0026#34;) strs = \u0026#39;我的私人邮箱是zhuwjwh@outlook.com，公司邮箱是123456@qq.org，麻烦登记一下？\u0026#39; result = pattern.findall(strs) print(result) 1 [\u0026#39;zhuwjwh@outlook.com\u0026#39;, \u0026#39;123456@qq.org\u0026#39;] 身份证号 xxxxxx yyyy MM dd 375 0 十八位\n地区：[1-9]\\d{5}\n年的前两位：(18|19|([23]\\d)) 1800-2399\n年的后两位：\\d{2}\n月份：((0[1-9])|(10|11|12))\n天数：(([0-2][1-9])|10|20|30|31) 闰年不能禁止 29+\n三位顺序码：\\d{3}\n两位顺序码：\\d{2}\n校验码：[0-9Xx]\n表达式：\n[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]\n案例：\n1 2 3 4 5 6 pattern = re.compile(r\u0026#34;[1-9]\\d{5}(?:18|19|(?:[23]\\d))\\d{2}(?:(?:0[1-9])|(?:10|11|12))(?:(?:[0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]\u0026#34;) strs = \u0026#39;小明的身份证号码是342623198910235163，手机号是13987692110\u0026#39; result = pattern.findall(strs) print(result) 1 [\u0026#39;342623198910235163\u0026#39;] 国内手机号码 手机号都为 11 位，且以 1 开头，第二位一般为 3、5、6、7、8、9 ，剩下八位任意数字\n例如：13987692110、15610098778\n表达式：\n1(3|4|5|6|7|8|9)\\d{9}\n案例：\n1 2 3 4 5 6 pattern = re.compile(r\u0026#34;1[356789]\\d{9}\u0026#34;) strs = \u0026#39;小明的手机号是13987692110，你明天打给他\u0026#39; result = pattern.findall(strs) print(result) 1 [\u0026#39;13987692110\u0026#39;] 国内固定电话 区号 34 位，号码 78 位\n例如：0511-1234567、021-87654321\n表达式：\n\\d{3}-\\d{8}|\\d{4}-\\d{7}\n案例：\n1 2 3 4 5 6 pattern = re.compile(r\u0026#34;\\d{3}-\\d{8}|\\d{4}-\\d{7}\u0026#34;) strs = \u0026#39;0511-1234567是小明家的电话，他的办公室电话是021-87654321\u0026#39; result = pattern.findall(strs) print(result) 1 [\u0026#39;0511-1234567\u0026#39;, \u0026#39;021-87654321\u0026#39;] 域名 包含 http:\\ 或 https:\\\n表达式：\n(?:(?:http:\\/\\/)|(?:https:\\/\\/))?(?:[\\w](?:[\\w\\-]{0,61}[\\w])?\\.)+[a-zA-Z]{2,6}(?:\\/)\n案例：\n1 2 3 4 5 6 pattern = re.compile(r\u0026#34;(?:(?:http:\\/\\/)|(?:https:\\/\\/))?(?:[\\w](?:[\\w\\-]{0,61}[\\w])?\\.)+[a-zA-Z]{2,6}(?:\\/)\u0026#34;) strs = \u0026#39;Python官网的网址是https://www.python.org/\u0026#39; result = pattern.findall(strs) print(result) 1 [\u0026#39;https://www.python.org/\u0026#39;] IP 地址 IP 地址的长度为 32 位 (共有 2^32 个 IP 地址)，分为 4 段，每段 8 位，用十进制数字表示\n每段数字范围为 0～255，段与段之间用句点隔开　表达式：\n((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))\n案例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 pattern = re.compile(r\u0026#34;((?:(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|[01]?\\d?\\d))\u0026#34;) strs = \u0026#39;\u0026#39;\u0026#39;请输入合法IP地址，非法IP地址和其他字符将被过滤！ 增、删、改IP地址后，请保存、关闭记事本！ 192.168.8.84 192.168.8.85 192.168.8.86 0.0.0.1 256.1.1.1 192.256.256.256 192.255.255.255 aa.bb.cc.dd\u0026#39;\u0026#39;\u0026#39; result = pattern.findall(strs) print(result) 1 [\u0026#39;192.168.8.84\u0026#39;, \u0026#39;192.168.8.85\u0026#39;, \u0026#39;192.168.8.86\u0026#39;, \u0026#39;0.0.0.1\u0026#39;, \u0026#39;56.1.1.1\u0026#39;, \u0026#39;192.255.255.255\u0026#39;] 日期 常见日期格式：yyyyMMdd、yyyy-MM-dd、yyyy/MM/dd、yyyy.MM.dd\n表达式：\n\\d{4}(?:-|\\/|.)\\d{1,2}(?:-|\\/|.)\\d{1,2}\n案例：\n1 2 3 4 5 6 pattern = re.compile(r\u0026#34;\\d{4}(?:-|\\/|.)\\d{1,2}(?:-|\\/|.)\\d{1,2}\u0026#34;) strs = \u0026#39;今天是2020/12/20，去年的今天是2019.12.20，明年的今天是2021-12-20\u0026#39; result = pattern.findall(strs) print(result) 1 [\u0026#39;2020/12/20\u0026#39;, \u0026#39;2019.12.20\u0026#39;, \u0026#39;2021-12-20\u0026#39;] 国内邮政编码 我国的邮政编码采用四级六位数编码结构\n前两位数字表示省（直辖市、自治区）\n第三位数字表示邮区；第四位数字表示县（市）\n最后两位数字表示投递局（所）\n表达式：\n[1-9]\\d{5}(?!\\d)\n案例：\n1 2 3 4 5 6 pattern = re.compile(r\u0026#34;[1-9]\\d{5}(?!\\d)\u0026#34;) strs = \u0026#39;上海静安区邮编是200040\u0026#39; result = pattern.findall(strs) print(result) 1 [\u0026#39;200040\u0026#39;] 密码 密码 (以字母开头，长度在 6~18 之间，只能包含字母、数字和下划线)\n表达式：\n[a-zA-Z]\\w{5,17}\n强密码 (以字母开头，必须包含大小写字母和数字的组合，不能使用特殊字符，长度在 8-10 之间)\n表达式：\n[a-zA-Z](?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}\n1 2 3 4 5 6 pattern = re.compile(r\u0026#34;[a-zA-Z]\\w{5,17}\u0026#34;) strs = \u0026#39;密码：q123456_abc\u0026#39; result = pattern.findall(strs) print(result) 1 [\u0026#39;q123456_abc\u0026#39;] 1 2 3 4 5 6 pattern = re.compile(r\u0026#34;[a-zA-Z](?=.*\\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}\u0026#34;) strs = \u0026#39;强密码：q123456ABc，弱密码：q123456abc\u0026#39; result = pattern.findall(strs) p print(result) 1 [\u0026#39;q123456ABc，\u0026#39;] 中文字符 表达式：\n[\\u4e00-\\u9fa5]\n案例：\n1 2 3 4 5 6 pattern = re.compile(r\u0026#34;[\\u4e00-\\u9fa5]\u0026#34;) strs = \u0026#39;apple：苹果\u0026#39; result = pattern.findall(strs) print(result) 1 [\u0026#39;苹\u0026#39;, \u0026#39;果\u0026#39;] 数字 验证数字：^[0-9]*$\n验证 n 位的数字：^\\d{n}$\n验证至少 n 位数字：^\\d{n,}$\n验证 m-n 位的数字：^\\d{m,n}$\n验证零和非零开头的数字：^(0|[1-9][0-9]*)$\n验证有两位小数的正实数：^[0-9]+(.[0-9]{2})?$\n验证有 1-3 位小数的正实数：^[0-9]+(.[0-9]{1,3})?$\n验证非零的正整数：^\\+?[1-9][0-9]*$\n验证非零的负整数：^\\-[1-9][0-9]*$\n验证非负整数（正整数 + 0） ^\\d+$\n验证非正整数（负整数 + 0） ^((-\\d+)|(0+))$\n整数：^-?\\d+$\n非负浮点数（正浮点数 + 0）：^\\d+(\\.\\d+)?$\n正浮点数 ^(([0-9]+\\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\\.[0-9]+)|([0-9]*[1-9][0-9]*))$\n非正浮点数（负浮点数 + 0） ^((-\\d+(\\.\\d+)?)|(0+(\\.0+)?))$\n负浮点数 ^(-(([0-9]+\\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\\.[0-9]+)|([0-9]*[1-9][0-9]*)))$\n浮点数 ^(-?\\d+)(\\.\\d+)?$\n字符串 英文和数字：^[A-Za-z0-9]+$ 或 ^[A-Za-z0-9]{4,40}$\n长度为 3-20 的所有字符：^.{3,20}$\n由 26 个英文字母组成的字符串：^[A-Za-z]+$\n由 26 个大写英文字母组成的字符串：^[A-Z]+$\n由 26 个小写英文字母组成的字符串：^[a-z]+$\n由数字和 26 个英文字母组成的字符串：^[A-Za-z0-9]+$\n由数字、26 个英文字母或者下划线组成的字符串：^\\w+$ 或 ^\\w{3,20}$\n中文、英文、数字包括下划线：^[\\u4E00-\\u9FA5A-Za-z0-9_]+$\n中文、英文、数字但不包括下划线等符号：^[\\u4E00-\\u9FA5A-Za-z0-9]+$ 或 ^[\\u4E00-\\u9FA5A-Za-z0-9]{2,20}$\n可以输入含有 ^%\u0026amp;\u0026rsquo;,;=?$\\” 等字符：`[^%\u0026',;=?$\\x22]+`\n禁止输入含有~ 的字符：[^~\\x22]+\n附：正则表达式语法详解 优先权 ","permalink":"https://ktzxy.top/posts/1bc9uq454r/","summary":"Python 50 个正则表达式写法","title":"Python 50 个正则表达式写法"},{"content":"一、硬件相关的参数调优 根据MySQL实例运行的硬件环境，一些硬件相关的参数需要根据实际情况进行设置，主要如下：\n1.1 innodb_buffer_pool_size 该参数通常设置为总内存大小的50%~70%，充分利用内存缓存，减少换页带来的磁盘性能损耗。 该参数设置的大小尽量不要大于数据库的总大小，否则造成内存浪费。 在MySQL运行过程中，监控buffer pool的使用率，根据实际情况进行调整。 1.2 innodb_log_file_size 该参数通常设置为128M ~ 2G之间。 该参数大小应当能够支持最少一个小时的日志量，保证在刷脏页和做检查点操作期间，有足够的空间顺序写日志。 1.3 innodb_flush_log_at_trx_commit 该参数设置为1，事务实时落盘，保证数据的持久性，但是影响性能。 该参数设置为0或2，性能较高，但是事务无法实时落盘，存在丢失数据的风险。 1.4 sync_binlog 该参数设置为1，binlog日志实时落盘，保证数据的持久性，但是影响性能。 该参数设置为0，性能较高，但是binlog日志无法实时落盘，存在丢失数据的风险。 1.5 innodb_flush_method 将该参数设置为O_DIRECT将避免双缓冲带来的性能损失，从buffer pool直接往磁盘上写，避免经过操作系统的缓冲带来的性能损耗。\n二、参数优化最佳实践 2.1 innodb_file_per_table innodb_file_per_table设置为ON，为每一个表生成一个独立的表空间。\n2.2 innodb_stats_on_metadata innodb_stats_on_metadata设置为OFF，避免不必要的InnoDB统计信息更新，可大大提高读取速度。\n2.3 innodb_buffer_pool_instances innodb_buffer_pool_instances参数，一个比较好的设置值为8，如果buffer pool size 小于 1G，将该参数设置为1。\n2.4 query_cache_type \u0026amp; query_cache_size 这两个参数应当设置为0，禁用查询缓存。在MySQL 8.0 版本，查询缓存功能及参数被整体移除，而5.7及以下版本，应当禁用查询缓存。\n2.5 innodb_autoinc_lock_mode 该参数设置为2（交错模式）能够避免auto-inc锁（表级锁）,显著提高性能，前提binlog格式为ROW或者MIXED。\n2.6 innodb_io_capacity \u0026amp; innodb_io_capacity_max 在写入很重的场景下，这两个参数将会影响MySQL的写入性能。首先需要了解磁盘IO的性能，即IOPS，可以先使用sysbench测试磁盘IO性能，然后再调整innodb_io_capacity和innodb_io_capacity_max，以便最大程度利用磁盘IO的能力。\n","permalink":"https://ktzxy.top/posts/bgyxhclhft/","summary":"MySQL性能参数调优","title":"MySQL性能参数调优"},{"content":"grep [option] file\n-A -B -C 同时显示出匹配位置的n行上下文行内容 -C 2 等价于 -A 2 -B 2 -c 只显示匹配的次数，不显示匹配的内容，等价于 grep ‘xxx’ file |wc -l -n 在输出的每行前面加上它所在的文件中它的行号 -e 后跟基本正则表达式 -r 递归地读每一目录下的所有文件。这样做和 -d recurse 选项等价 -P 将模式 PATTERN 作为一个 Perl 正则表达式来解释 -E 将模式 PATTERN 作为一个扩展的正则表达式来解释 -i 忽略正则匹配的大小写 -o 只显示匹配的行中与 PATTERN 相匹配的部分 -z 在行尾禁用换行符，将其替换为空字符。也就是说，grep知道行尾是什么，但将输入视为一个大行 -v 改变匹配的意义，只选择不匹配的行 -m 最大匹配次数，在找到 NUM 个匹配的行之后，不再读这个文件 -Pzo 使正则表达式适用于多行匹配 配合sed命令批量替换字符串\nsed 用法\n1 2 sed -i “s/oldstring/newstring/g” filename sed -i “s/oldstring/newstring/g” grep oldstring -rl path 其中，oldstring是待被替换的字符串，newstring是待替换oldstring的新字符串，grep操作主要是按照所给的路径查找oldstring，path是所替换文件的路径；\n-i 选项是直接在文件中替换，不在终端输出；\n-r 选项是所给的path中的目录递归查找；\n-l 选项是输出所有匹配到oldstring的文件；\n1 2 # 将6替换为sk sed -i “s/6/sk/g” grep 6 -rl /home/work/test/*.sh linux 多行合并为一行 xargs 1 docker ps -a | grep -v \u0026#34;CON\u0026#34; | awk \u0026#39;{print $1}\u0026#39; | xargs sed 1 docker ps -a | grep -v \u0026#34;CON\u0026#34; | awk \u0026#39;{print $1}\u0026#39; | sed \u0026#39;:a; N;s/\\n/ /; ta\u0026#39; N 代表两行合并一行,中间用\\n替换, :a 做个标记, ta代表命令执行成功后会跳转到 :a, 所以这句话就是循环执行 N 并\\n替换为空格来达到合并成一行的目的.\ntr 1 docker ps -a | grep -v \u0026#34;CON\u0026#34; | awk \u0026#39;{print $1}\u0026#39; | tr \u0026#34;\\n\u0026#34; \u0026#34; \u0026#34; ","permalink":"https://ktzxy.top/posts/kig86f0uqy/","summary":"grep常见用法","title":"grep常见用法"},{"content":"hexo 博客完整部署以及魔改记录 前期基础部署 1.下载安装git Git - Downloads (git-scm.com)\n安装完成可在鼠标右键看到Git Bash\n常用命令\n1 2 3 git config -l //查看所有配置 git config --system --list //查看系统配置 git config --global --list //查看用户（全局）配置 配置用户名和邮箱\n1 2 git config --global user.name \u0026#34;你的用户名\u0026#34; git config --global user.email \u0026#34;你的邮箱\u0026#34; 2.下载安装node.js Node.js中文官网 (nodejs.org)\n推荐使用nvm安装，后续如果涉及node版本更换，更为方便\nnvm安装\n进入官网http://nvm.uihtm.com/ 下载\n解压安装，一直下一步\n基础命令\nnodejs历史版本下载页面\nhttps://www.fomal.cc/posts/e593433d.html\n1 2 3 4 5 6 7 8 9 10 11 12 查询版本号 nvm -v 查询可以下载的node版本 nvm list available 安装指定版本 nvm install xxx 查看已经安装的node版本 nvm list 切换node版本(如果失败那就用管理员身份打开cmd进行切换) nvm use xxx nodejs这里安装的是跟视频博主一样的版本，12.19.0 修改npm源\n1 npm config set registry https://registry.npm.taobao.org 3.安装hexo博客框架 Hexo官方网址\n本地新建一个用于安装博客的文件夹\n用git bash打开该文件夹\n输入以下代码\n1 npm install hexo-cli -g 或者npm install -g hexo 问题：bash: hexo: command not found\n解决：\n4.注册GitHub账号 新建仓库\n选择New repository，创建一个\u0026lt;用户名\u0026gt;.github.io的仓库。\n仓库的格式必须为：\u0026lt;用户名\u0026gt;.github.io\n5.上传博客到github 1 2 3 4 git config --global user.name \u0026#34;你的账号名称\u0026#34; git config --global user.email \u0026#34;你的邮箱\u0026#34; 在新创建的Git 全局设置:中可看到 生成令牌文件\n1 ssh-keygen -t rsa -C \u0026#34;你的邮箱\u0026#34; 获取密钥\n1 cat ~/.ssh/id_rsa.pub 将 SSH KEY 配置到 GitHub 进入github，点击右上角头像 选择settings，进入设置页后选择 SSH and GPG keys，名字随便起，公钥填到Key那一栏\n1 ssh -T git@github.com 出现successful则表示配置成功\n6.初始化hexo项目 在目标路径（我这里选的路径为【C:/Hexo-Blog】）打开cmd命令窗口，执行hexo init初始化项目。\n1 hexo init blog-demo(项目名) 进入blog-demo ，输入npm i安装相关依赖。\n1 2 cd blog-demo //进入blog-demo文件夹 npm i 初始化项目后，blog-demo有如下结构：\n【node_modules】：依赖包 【scaffolds】：生成文章的一些模板 【source】：用来存放你的文章 【themes】：主题 【.npmignore】：发布时忽略的文件（可忽略） 【_config.landscape.yml】：主题的配置文件 【config.yml】：博客的配置文件 【package.json】：项目名称、描述、版本、运行和开发等信息\n输入hexo server或者hexo s 启动项目\n打开浏览器，输入地址：http://localhost:4000/ 。\n7.将静态博客挂载到GitHub Pages 安装 hexo-deployer-git\n1 npm install hexo-deployer-git --save 修改 _config.yml 文件 在blog-demo目录下的_config.yml，就是整个Hexo框架的配置文件了。可以在里面修改大部分的配置。详细可参考官方的配置描述。 修改最后一行的配置，将repository修改为你自己的github项目地址即可，还有分支要改为main代表主分支（注意缩进）。\n1 2 3 4 deploy: type: \u0026#39;git\u0026#39; repository: git@github.com:ktzxy/ktzxy.github.io.git branch: main 修改好配置后，运行如下命令，将代码部署到 GitHub\n1 2 3 4 hexo clean \u0026amp;\u0026amp; hexo generate \u0026amp;\u0026amp; hexo deploy // Git BASH终端 hexo clean; hexo generate; hexo deploy // VSCODE终端 简写：hexo cl \u0026amp;\u0026amp; hexo g \u0026amp;\u0026amp; hexo d 1 2 将本地主分支更名为main，方便与远程分支名称对应： git branch -M main 将本地主分支main，推至GitHub仓库的主分支main: git push -u origin main 第一步骤算是完成；直接访问https://\u0026lt;用户名\u0026gt;.github.io 即可访问成功\nGithub Action 实现自动化部署 使用Github Action实现全自动部署 | Akilarの糖果屋\n常量名 常量释义 [Blogroot] 本地存放博客源码的文件夹路径 [SourceRepo] 存放博客源码的私有仓库名 [SiteBlogRepo] 存放编译好的博客页面的公有仓库名 Site指站点，教程中会替换成 Github、Gitee、Coding [SiteUsername] 用户名 Site指站点，教程中会替换成 Github、Gitee、Coding [SiteToken] 申请到的令牌码 Site指站点，教程中会替换成 Github、Gitee、Coding [GithubEmail] 与github绑定的主邮箱，建议使用Gmail [TokenUser] Coding配置特有的令牌用户名 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 在记事本中逐个记录，方便替换，以下为我的示例 [Blogroot]：E:\\Blogroot [SourceRepo]：ktzxy/Hexo-blog-source [SiteBlogRepo] [GithubBlogRepo]：ktzxy.github.io [SiteUsername] [GithubUsername]：ktzxy [SiteToken] [GithubToken]： [GithubEmail]： [TokenUser]： 1.获取github token Github-\u0026gt;头像（右上角）-\u0026gt;Settings-\u0026gt;Developer Settings-\u0026gt;Personal access tokens-\u0026gt;\n在[Blogroot]新建.github文件夹,注意开头是有个.的。然后在.github内新建workflows文件夹，再在workflows文件夹内新建autodeploy.yml,在[Blogroot]/.github/workflows/autodeploy.yml里面输入\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 # 当有改动推送到main分支时，启动Action name: 自动部署 on: push: branches: - main #2020年10月后github新建仓库默认分支改为main，注意更改 release: types: - published jobs: deploy: runs-on: ubuntu-latest steps: - name: 检查分支 uses: actions/checkout@v2 with: ref: main #2020年10月后github新建仓库默认分支改为main，注意更改 - name: 安装 Node uses: actions/setup-node@v3 # 升级为v3，推荐新版 with: node-version: \u0026#34;21.5.0\u0026#34; - name: 安装 Hexo run: | export TZ=\u0026#39;Asia/Shanghai\u0026#39; npm install hexo-cli -g - name: 缓存 Hexo 依赖 uses: actions/cache@v4 id: cache with: path: | node_modules ~/.npm # 缓存npm加速依赖安装 key: ${{ runner.OS }}-${{ hashFiles(\u0026#39;**/package-lock.json\u0026#39;) }} - name: 安装依赖 if: steps.cache.outputs.cache-hit != \u0026#39;true\u0026#39; run: | npm install - name: 生成静态文件 run: | hexo clean hexo generate - name: 部署 #此处master:master 指从本地的master分支提交到远程仓库的master分支，若远程仓库没有对应分支则新建一个。如有其他需要，可以根据自己的需求更改。 run: | cd ./public git init git checkout -b main git config --local user.name \u0026#34;${{ secrets.GITHUBUSERNAME }}\u0026#34; git config --local user.email \u0026#34;${{ secrets.GITHUBEMAIL }}\u0026#34; git add . git commit -m \u0026#34;${{ github.event.head_commit.message }} $(date \u0026#39;+%Z %Y-%m-%d %A %H:%M:%S\u0026#39;) Updated By Github Actions\u0026#34; git push --force --quiet \u0026#34;https://${{ secrets.GITHUBUSERNAME }}:${{ secrets.GITHUBTOKEN }}@github.com/${{ secrets.GITHUBUSERNAME }}/${{ secrets.GITHUBUSERNAME }}.github.io.git\u0026#34; main:main #git push --force --quiet \u0026#34;https://${{ secrets.TOKENUSER }}:${{ secrets.CODINGTOKEN }}@e.coding.net/${{ secrets.CODINGUSERNAME }}/${{ secrets.CODINGBLOGREPO }}.git\u0026#34; master:master #coding部署写法，需要的自行取消注释 #git push --force --quiet \u0026#34;https://${{ secrets.GITEEUSERNAME }}:${{ secrets.GITEETOKEN }}@gitee.com/${{ secrets.GITEEUSERNAME }}/${{ secrets.GITEEUSERNAME }}.git\u0026#34; master:master #gitee部署写法，需要的自行取消注释 之后需要自己到仓库的Settings-\u0026gt;Secrets-\u0026gt;actions 下添加环境变量，变量名参考脚本中出现的，依次添加\n例如，需要部署在githubpage上，那么脚本中必要的变量为 GITHUBUSERNAME、GITHUBEMAIL、GITHUBTOKEN，因此添加这三条变量。变量具体内容释义可以查看本文开头。\n重新设置远程仓库和分支\n删除或者先把[Blogroot]/themes/butterfly/.git移动到非博客文件夹目录下,原因是主题文件夹下的.git文件夹的存在会导致其被识别成子项目，从而无法被上传到源码仓库。 在博客根目录[Blogroot]路径下运行指令 1 2 3 4 5 git init #初始化 git remote add origin git@github.com:[GithubUsername]/[SourceRepo].git #[SourceRepo]为存放源码的github私有仓库 git checkout -b main # 切换到master分支， #2020年10月后github新建仓库默认分支改为main，注意更改 # 如果不是，后面的所有设置的分支记得保持一致 添加屏蔽项 因为能够使用指令进行安装的内容不包括在需要提交的源码内，所有我们需要将这些内容添加到屏蔽项，表示不上传到github上。这样可以显著减少需要提交的文件量和加快提交速度。 打开[Blogroot]/.gitignore,输入以下内容：\n1 2 3 4 5 6 7 8 9 10 .DS_Store Thumbs.db db.json *.log node_modules/ public/ .deploy*/ .deploy_git*/ .idea themes/butterfly/.git 如果不是butterfly主题，记得替换最后一行内容为你自己当前使用的主题。 之后再运行git提交指令，将博客源码提交到github上。牢记下方的三行指令，以后都是通过这个指令进行提交。 1 2 3 4 git add . git commit -m \u0026#34;github action update\u0026#34; #引号内的内容可以自行更改作为提交记录。 git push origin main #2020年10月后github新建仓库默认分支改为main，注意更改 静态页面托管网站 Netlify部署 使用第三方托管平台部署博客 | Akilarの糖果屋\n安装主题 Themes | Hexo\n在博客根目录里，打开Git Bash工具，运行\n1 2 3 4 git clone -b master https://github.com/jerryc127/hexo-theme-butterfly themes/butterfly #npm安装 或者 npm i hexo-theme-butterfly 应用主题 修改站点配置文件_config.yml，把主题改为butterfly\n1 theme: butterfly 如果你没有pug以及stylus的渲染器，请下载安装，这两个渲染器是Butterfly生成基础页面所需的依赖包：\n1 npm install hexo-renderer-pug hexo-renderer-stylus --save 为了减少升级主题后带来的不便，请使用以下方法（建议，可以不做，高度魔改的一般都不会升级主题了，不然魔改的会被覆盖掉） 把主题文件夹中的 _config.yml 复制到 Hexo 根目录里（我这里路径为【C:/Hexo-Blog/blog-demo】），同时重新命名为 _config.butterfly.yml。以后只需要在 _config.butterfly.yml进行配置即可生效。Hexo会自动合併主题中的_config.yml和 _config.butterfly.yml里的配置，如果存在同名配置，会使用_config.butterfly.yml的配置，其优先度较高\n基础用法说明 Front-matter Front-matter 是 markdown 文件最上方以---分隔的区域，用于指定个别档案的变数。\nPage Front-matter 用于页面配置 Post Front-matter 用于文章页配置 如果标注可选的参数，可根据自己需要添加，不用全部都写\nPage Front-matter：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 --- title: date: updated: type: comments: description: keywords: top_img: mathjax: katex: aside: aplayer: highlight_shrink: --- 写法 解释 title 【必需】页面标题 date 【必需】页面创建日期 type 【必需】标籤、分类和友情链接三个页面需要配置 updated 【可选】页面更新日期 description 【可选】页面描述 keywords 【可选】页面关键字 comments 【可选】显示页面评论模块(默认 true) top_img 【可选】页面顶部图片 mathjax 【可选】显示mathjax(当设置mathjax的per_page: false时，才需要配置，默认 false) kates 【可选】显示katex(当设置katex的per_page: false时，才需要配置，默认 false) aside 【可选】显示侧边栏 (默认 true) aplayer 【可选】在需要的页面加载aplayer的js和css,请参考文章下面的音乐 配置 highlight_shrink 【可选】配置代码框是否展开(true/false)(默认为设置中highlight_shrink的配置) Post Front-matter：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 --- title: date: updated: tags: categories: keywords: description: top_img: comments: cover: toc: toc_number: toc_style_simple: copyright: copyright_author: copyright_author_href: copyright_url: copyright_info: mathjax: katex: aplayer: highlight_shrink: aside: --- 写法 解释 title 【必需】文章标题 date 【必需】文章创建日期 updated 【可选】文章更新日期 tags 【可选】文章标籤 categories 【可选】文章分类 keywords 【可选】文章关键字 description 【可选】文章描述 top_img 【可选】文章顶部图片 cover 【可选】文章缩略图(如果没有设置top_img,文章页顶部将显示缩略图，可设为false/图片地址/留空) comments 【可选】显示文章评论模块(默认 true) toc 【可选】显示文章TOC(默认为设置中toc的enable配置) toc_number 【可选】显示toc_number(默认为设置中toc的number配置) toc_style_simple 【可选】显示 toc 简洁模式 copyright 【可选】显示文章版权模块(默认为设置中post_copyright的enable配置) copyright_author 【可选】文章版权模块的文章作者 copyright_author_href 【可选】文章版权模块的文章作者链接 copyright_url 【可选】文章版权模块的文章连结链接 copyright_info 【可选】文章版权模块的版权声明文字 mathjax 【可选】显示mathjax(当设置mathjax的per_page: false时，才需要配置，默认 false) katex 【可选】显示katex(当设置katex的per_page: false时，才需要配置，默认 false) aplayer 【可选】在需要的页面加载aplayer的js和css,请参考文章下面的音乐 配置 highlight_shrink 【可选】配置代码框是否展开(true/false)(默认为设置中highlight_shrink的配置) aside 【可选】显示侧边栏 (默认 true) 标签页 前往你的Hexo博客根目录，打开Git Bash执行如下命令：\n1 hexo new page tags 在[BlogRoot]\\source\\会生成一个含有index.md文件的tags文件夹。\n修改[BlogRoot]\\source\\tags\\index.md，添加type: \u0026quot;tags\u0026quot;。\n1 2 3 4 5 --- title: tags date: 2023-09-23 21:38:05 type: \u0026#34;tags\u0026#34; --- 友情链接 前往你的Hexo博客根目录，打开cmd命令窗口执行如下命令：\n1 hexo new page link 在[BlogRoot]\\source\\会生成一个含有index.md文件的link文件夹\n修改[BlogRoot]\\source\\link\\index.md，添加type: \u0026quot;link\u0026quot;\n1 2 3 4 5 6 MARKDOWN --- title: link date: 2022-10-28 12:00:00 type: \u0026#34;link\u0026#34; --- 前往[BlogRoot]\\source\\_data创建一个link.yml文件（如果沒有 _data 文件夹，请自行创建），并写入如下信息（根据你的需要写）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 - class_name: 友情鏈接 class_desc: 那些人，那些事 link_list: - name: Hexo link: https://hexo.io/zh-tw/ avatar: https://d33wubrfki0l68.cloudfront.net/6657ba50e702d84afb32fe846bed54fba1a77add/827ae/logo.svg descr: 快速、簡單且強大的網誌框架 - class_name: 網站 class_desc: 值得推薦的網站 link_list: - name: Youtube link: https://www.youtube.com/ avatar: https://i.loli.net/2020/05/14/9ZkGg8v3azHJfM1.png descr: 視頻網站 - name: Weibo link: https://www.weibo.com/ avatar: https://i.loli.net/2020/05/14/TLJBum386vcnI1P.png descr: 中國最大社交分享平台 - name: Twitter link: https://twitter.com/ avatar: https://i.loli.net/2020/05/14/5VyHPQqR6LWF39a.png descr: 社交分享平台 class_name和class_desc支持 html 格式，如不需要，也可以留空。\n图库 undo 图库页面只是普通的页面，你只需要hexo new page xxx创建你的页面就行。\n然后使用外挂标签 galleryGroup，具体用法请查看对应的内容。\n1 2 3 4 5 6 \u0026lt;div class=\u0026#34;gallery-group-main\u0026#34;\u0026gt; {% galleryGroup \u0026#39;封面专区\u0026#39; \u0026#39;本站用作文章封面的图片，不保证分辨率\u0026#39; \u0026#39;/box/Gallery/photo\u0026#39; https://source.fomal.cc/img/default_cover_61.webp %} {% galleryGroup \u0026#39;背景专区\u0026#39; \u0026#39;收藏的一些的背景与壁纸，分辨率很高\u0026#39; \u0026#39;/box/Gallery/wallpaper\u0026#39; https://source.fomal.cc/img/dm11.webp %} \u0026lt;/div\u0026gt; 子页面 undo 子页面也是普通的页面，你只需要hexo new page xxx创建你的页面就行。\n然后使用标签外挂 gallery，具体用法请查看对应的内容。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 {% gallery %} ![p1]( https://source.fomal.cc/img/default_cover_1.webp ) ![p2]( https://source.fomal.cc/img/default_cover_2.webp ) ![p3]( https://source.fomal.cc/img/default_cover_3.webp ) ![p4]( https://source.fomal.cc/img/default_cover_4.webp ) ![p5]( https://source.fomal.cc/img/default_cover_5.webp ) ![p6]( https://source.fomal.cc/img/default_cover_6.webp ) ![p7]( https://source.fomal.cc/img/default_cover_7.webp ) ![p8]( https://source.fomal.cc/img/default_cover_8.webp ) ![p9]( https://source.fomal.cc/img/default_cover_9.webp ) ![p10]( https://source.fomal.cc/img/default_cover_10.webp ) ![p11]( https://source.fomal.cc/img/default_cover_11.webp ) ![p12]( https://source.fomal.cc/img/default_cover_12.webp ) {% endgallery %} 404页面 enable改为true\n1 2 3 4 5 # A simple 404 page error_404: enable: true subtitle: \u0026#34;页面沒有找到\u0026#34; background: hexo 博客基础配置 语言 修改站点配置文件_config.yml，默认语言是 en 。 主题支持三种语言：\ndefault(en) zh-CN (简体中文) zh-TW (繁体中文) 网站资料 修改网站各种资料，例如标题、副标题和邮箱等个人资料，请修改站点配置文件_config.yml。部分参数如下，详细参数可参考官方的配置描述。\n参数 描述 title 网站标题 subtitle 描述 description 网站描述 keywords 网站的关键词。支持多个关键词 author 您的名字 language 网站使用的语言。对于简体中文用户来说，使用不同的主题可能需要设置成不同的值，请参考你的主题的文档自行设置，常见的有 zh-Hans和 zh-CN。 timezone 网站时区。Hexo 默认使用您电脑的时区。请参考 时区列表 进行设置，如 America/New_York, Japan, 和 UTC 。一般的，对于中国大陆地区可以使用 Asia/Shanghai 导航菜单 修改主题配置文件_config.butterfly.yml\n必须是 /xxx/，后面||分开，然后写图标名，如果不想显示图标，图标名可不写 若主题版本大于 v4.0.0，可以直接在子目录里添加 hide 隐藏子目录，如下面的List 1 2 3 4 5 6 7 8 9 10 menu: Home: / || fas fa-home Archives: /archives/ || fas fa-archive Tags: /tags/ || fas fa-tags Categories: /categories/ || fas fa-folder-open List||fas fa-list||hide: Music: /music/ || fas fa-music Movie: /movies/ || fas fa-video Link: /link/ || fas fa-link About: /about/ || fas fa-heart 文字可自行更改，中英文都可以 1 2 3 4 5 6 7 8 9 10 11 menu: 首页: / || fas fa-home 时间轴: /archives/ || fas fa-archive 标签: /tags/ || fas fa-tags 分类: /categories/ || fas fa-folder-open 清单||fa fa-heartbeat: 音乐: /music/ || fas fa-music 照片: /Gallery/ || fas fa-images 电影: /movies/ || fas fa-video 友链: /link/ || fas fa-link 关于: /about/ || fas fa-heart 代码 代码高亮主题 Butterfly支持 6 种代码高亮样式：\ndarker pale night light ocean mac mac light 修改主题配置文件_config.butterfly.yml。中的highlight_theme属性。\n1 highlight_theme: darker 代码复制 修改主题配置文件_config.butterfly.yml中的highlight_copy属性，true表示可以复制。\n1 highlight_copy: true 代码框展开/关闭 修改主题配置文件_config.butterfly.yml。中的highlight_shrink属性。\n1 highlight_shrink: true #代码框不展开，需点击 \u0026#39;\u0026gt;\u0026#39; 打开 代码换行 在默认情况下，Hexo 在编译的时候不会实现代码自动换行。如果你不希望在代码块的区域里有横向滚动条的话，那么你可以考虑开启这个功能。\n修改主题配置文件_config.butterfly.yml。中的code_word_wrap属性。\n1 code_word_wrap: true 代码高度限制 可配置代码高度限制，超出的部分会隐藏，并显示展开按钮。\n1 highlight_height_limit: false # unit: px 单位是px，直接添加数字，如 200 实际限制高度为highlight_height_limit + 30 px ，多增加 30px 限制，目的是避免代码高度只超出highlight_height_limit 一点时，出现展开按钮，展开没内容。 不适用于隐藏后的代码块（ css 设置 display: none）。 社交图标 Butterfly支持font-awesome v6图标。\n书写格式：图标名：url || 描述性文字。\n1 2 3 4 5 social: GitHub: https://github.com/shaunzhao-yu || icon-github || faa-tada Email: mailto:2251511764@qq.com || icon-mail || faa-tada CSDN: https://blog.csdn.net/qq_46087070 || fa-book-open || faa-tada QQ: https://res.abeim.cn/api/qq/?qq=2251511764 || icon-QQ || faa-tada 顶部图 如果不要显示顶部图，可直接配置 disable_top_img: true。\n配置 解释 index_img 主页的 top_img default_top_img 默认的 top_img，当页面的 top_img 没有配置时，会显示 default_top_img archive_img 归档页面的 top_img tag_img tag子页面 的 默认 top_img tag_per_img tag子页面的 top_img，可配置每个 tag 的 top_img category_img category 子页面 的 默认 top_img category_per_img category 子页面的 top_img，可配置每个 category 的 top_img 修改主题配置文件_config.butterfly.yml\n1 index_img: xxx.png 其它页面 （tags/categories/自建页面）和文章页的top_img，请到对应的 md 页面设置front-matter中的top_img\n文章置顶与封面 你可以直接在文章的front-matter区域里添加sticky: 1属性来把这篇文章置顶。数值越大，置顶的优先级越大。\n文章的markdown文档上，在Front-matter添加cover，并填上要显示的图片地址。如果不配置cover，可以设置显示默认的cover；如果不想在首页显示cover，可以设置为false。 修改主题配置文件_config.butterfly.yml。\n1 2 3 4 5 6 7 8 9 10 cover: # 是否显示文章封面 index_enable: true aside_enable: true archives_enable: true # 封面显示的位置 # 三个值可配置 left , right , both position: both # 当没有设置cover时，默认的封面显示 default_cover: 当配置多张图片时，会随机选择一张作为cover，此时写法应为：\n1 2 3 4 default_cover: - https://fastly.jsdelivr.net/gh/jerryc127/CDN@latest/cover/default_bg.png - https://fastly.jsdelivr.net/gh/jerryc127/CDN@latest/cover/default_bg2.png - https://fastly.jsdelivr.net/gh/jerryc127/CDN@latest/cover/default_bg3.png 文章页相关配置 文章meta显示 post_meta这个属性用于显示文章的相关信息的，修改主题配置文件_config.butterfly.yml。\n1 2 3 4 5 6 7 8 9 10 11 12 13 post_meta: page: date_type: both # created or updated or both 主页文章日期是创建日或者更新日或都显示 date_format: relative # date/relative 显示日期还是相对日期 categories: true # true or false 主页是否显示分类 tags: true # true or false 主页是否显示标签 label: true # true or false 显示描述性文字 post: date_type: both # created or updated or both 文章页日期是创建日或者更新日或都显示 date_format: relative # date/relative 显示日期还是相对日期 categories: true # true or false 文章页是否显示分类 tags: true # true or false 文章页是否显示标签 label: true # true or false 显示描述性文字 文章版权和协议许可 修改主题配置文件_config.butterfly.yml\n1 2 3 4 5 6 post_copyright: enable: true decode: false author_href: license: CC BY-NC-SA 4.0 license_url: https://creativecommons.org/licenses/by-nc-sa/4.0/ 由于Hexo 4.1开始，默认对网址进行解码，以至于如果是中文网址，会被解码，可设置decode: true来显示中文网址。如果有文章（例如：转载文章）不需要显示版权，可以在文章页Front-matter中单独设置\n1 copyright: false 文章打赏 修改主题配置文件_config.butterfly.yml\n1 2 3 4 5 6 7 8 9 10 reward: enable: true coinAudio: https://cdn.cbd.int/akilar-candyassets@1.0.36/audio/aowu.m4a QR_code: - img: https://fastly.jsdelivr.net/gh/shaunzhao-yu/img@main/photos/202310072330878.png link: text: wechat - img: https://fastly.jsdelivr.net/gh/shaunzhao-yu/img@main/photos/202310072330877.png link: text: alipay 文章目录TOC 修改主题配置文件_config.butterfly.yml。\n1 2 3 4 5 6 toc: post: true # 文章页是否显示 TOC page: false # 普通页面是否显示 TOC number: true\t# 是否显示章节数 expand: false\t# 是否展开 TOC style_simple: false # for post 简洁模式（侧边栏只显示 TOC, 只对文章页有效 ） 相关文章推荐 相关文章推荐的原理是根据文章tags的比重来推荐，修改主题配置文件_config.butterfly.yml。\n1 2 3 4 related_post: enable: true limit: 6 # 显示推荐文章数目 date_type: created # or created or updated 文章日期显示创建日或者更新日 文章锚点 开启文章锚点后，当你在文章页进行滚动时，文章链接会根据标题ID进行替换。\n注意: 每替换一次，会留下一个歷史记录。所以如果一篇文章有很多锚点的话，网页的歷史记录会很多。\n修改主题配置文件_config.butterfly.yml。\n1 2 3 # anchor # when you scroll in post , the url will update according to header id. anchor: true 文章过期提醒 可设置是否显示文章过期提醒，以更新时间为基准。\n1 2 3 4 5 6 7 8 # Displays outdated notice for a post (文章过期提醒) noticeOutdate: enable: true style: flat # style: simple/flat limit_day: 365 # 距离更新时间多少天才显示文章过期提醒 position: top # position: top/bottom message_prev: It has been # 天数之前的文字 message_next: days since the last update, the content of the article may be outdated. # 天数之后的文字 文章分页按钮 修改主题配置文件_config.butterfly.yml\n1 2 3 4 5 6 # post_pagination (分页) # value: 1 || 2 || false\t# false:为关闭分页按钮；1:下一篇显示的是旧文章；2:下一篇显示的是新文章 # 1: The \u0026#39;next post\u0026#39; will link to old post # 2: The \u0026#39;next post\u0026#39; will link to new post # false: disable pagination post_pagination: false 头像 1 2 3 avatar: img: /assets/head.jpg effect: false # true则会一直转圈 文章内容复制相关配置 1 2 3 4 5 6 7 # copy settings # copyright: Add the copyright information after copied content (复制的内容后面加上版权信息) copy: enable: true\t# 是否开启网站复制权限 copyright:\t# 复制的内容后面加上版权信息 enable: true\t# 是否开启复制版权信息添加 limit_count: 50\t# 字数限制，当复制文字大于这个字数限制时，将在复制的内容后面加上版权信息 Footer 设置 修改主题配置文件_config.butterfly.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 # 注释掉所有的的，添加下面的 # footer_beautify # 页脚计时器：[Native JS Timer](https://akilar.top/posts/b941af/) # 页脚徽标：[Add Github Badge](https://akilar.top/posts/e87ad7f8/) footer_beautify: enable: timer: true # 计时器开关 bdage: true # 徽标开关 priority: 5 #过滤器优先权 enable_page: all # 应用页面 exclude: #屏蔽页面 # - /posts/ # - /about/ layout: # 挂载容器类型 type: id name: footer-wrap index: 0 # 计时器部分配置项 runtime_js: /js/runtime.js runtime_css: /css/runtime.css # 徽标部分配置项 swiperpara: 3 #若非0，则开启轮播功能，每行徽标个数 bdageitem: - link: https://hexo.io/ #徽标指向网站链接 shields: https://img.shields.io/badge/Frame-Hexo-blue?style=flat\u0026amp;logo=hexo #徽标API message: 博客框架为Hexo_v5.4.0 #徽标提示语 - link: https://butterfly.js.org/ shields: https://img.shields.io/badge/Theme-Butterfly-6513df?style=flat\u0026amp;logo=bitdefender message: 主题版本Butterfly_v4.9.0 - link: https://www.jsdelivr.com/ shields: https://img.shields.io/badge/CDN-jsDelivr-orange?style=flat\u0026amp;logo=jsDelivr message: 本站使用JsDelivr为静态资源提供CDN加速 - link: https://vercel.com/ shields: https://img.shields.io/badge/Hosted-Vercel-brightgreen?style=flat\u0026amp;logo=Vercel message: 本站采用双线部署，默认线路托管于Vercel - link: https://vercel.com/ shields: https://img.shields.io/badge/Hosted-Coding-0cedbe?style=flat\u0026amp;logo=Codio message: 本站采用双线部署，联通线路托管于Coding - link: https://github.com/ shields: https://img.shields.io/badge/Source-Github-d021d6?style=flat\u0026amp;logo=GitHub message: 本站项目由Github托管 - link: http://creativecommons.org/licenses/by-nc-sa/4.0/ shields: https://img.shields.io/badge/Copyright-BY--NC--SA%204.0-d42328?style=flat\u0026amp;logo=Claris message: 本站采用知识共享署名-非商业性使用-相同方式共享4.0国际许可协议进行许可 swiper_css: https://npm.elemecdn.com/hexo-butterfly-swiper/lib/swiper.min.css swiper_js: https://npm.elemecdn.com/hexo-butterfly-swiper/lib/swiper.min.js swiperbdage_init_js: https://npm.elemecdn.com/hexo-butterfly-footer-beautify/lib/swiperbdage_init.min.js 对于部分人需要写 ICP 的，也可以写在custom_text里。\n1 custom_text: \u0026lt;a href=\u0026#34;icp链接\u0026#34;\u0026gt;\u0026lt;img class=\u0026#34;icp-icon\u0026#34; src=\u0026#34;icp图片\u0026#34;\u0026gt;\u0026lt;span\u0026gt;备案号：xxxxxx\u0026lt;/span\u0026gt;\u0026lt;/a\u0026gt; 右下角按钮 简繁转换 修改主题配置文件_config.butterfly.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 translate: enable: false # 默认按钮显示文字(网站是简体，应设置为\u0026#39;default: 繁\u0026#39;) default: 繁 # the language of website (1 - Traditional Chinese/ 2 - Simplified Chinese） # 网站默认语言，1: 繁体中文, 2: 简体中文 defaultEncoding: 2 # Time delay 延迟时间,若不在前, 要设定延迟翻译时间, 如100表示100ms,默认为0 translateDelay: 0 # 当文字是简体时，按钮显示的文字 msgToTraditionalChinese: \u0026#39;繁\u0026#39; # 当文字是繁体时，按钮显示的文字 msgToSimplifiedChinese: \u0026#39;簡\u0026#39; 夜间模式 修改主题配置文件_config.butterfly.yml\n1 2 3 4 5 6 7 8 9 10 # dark mode darkmode: enable: false # dark 和 light 两种模式切换按钮 button: true # Switch dark/light mode automatically (自動切換 dark mode和 light mode) # autoChangeMode: 1 Following System Settings, if the system doesn\u0026#39;t support dark mode, it will switch dark mode between 6 pm to 6 am # autoChangeMode: 2 Switch dark mode between 6 pm to 6 am # autoChangeMode: false autoChangeMode: false 阅读模式 阅读模式下会去掉除文章外的内容，避免干扰阅读。只会出现在文章页面，右下角会有阅读模式按钮。\n修改主题配置文件_config.butterfly.yml\n1 readmode: true 侧边栏设置 排版 可自行决定哪个项目需要显示，可决定位置，也可以设置不显示侧边栏。\n修改主题配置文件_config.butterfly.yml，下面是本人博客的配置项可以参考\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 aside: enable: true hide: false button: true mobile: true # display on mobile position: right # left or right display: archive: true tag: true category: true card_author: enable: true description: button: enable: true icon: # fab fa-github text: 🛴前往小家...\t#可以改 link: https://github.com/fomalhaut1998\t#可以改 card_announcement: enable: true content: \u0026lt;center\u0026gt;主域名：\u0026lt;br\u0026gt;\u0026lt;a href=\u0026#34;https://www.fomal.cc\u0026#34;\u0026gt;\u0026lt;b\u0026gt;\u0026lt;font color=\u0026#34;#5ea6e5\u0026#34;\u0026gt;fomal.cc\u0026lt;/font\u0026gt;\u0026lt;/b\u0026gt;\u0026lt;/a\u0026gt;\u0026amp;nbsp;|\u0026amp;nbsp;\u0026lt;a href=\u0026#34;https://www.fomal.cn\u0026#34;\u0026gt;\u0026lt;b\u0026gt;\u0026lt;font color=\u0026#34;#5ea6e5\u0026#34;\u0026gt;fomal.cn\u0026lt;/font\u0026gt;\u0026lt;/b\u0026gt;\u0026lt;/a\u0026gt;\u0026lt;br\u0026gt;备用域名：\u0026lt;br\u0026gt;\u0026lt;a href=\u0026#34;https://blog.fomal.cc\u0026#34;\u0026gt;\u0026lt;b\u0026gt;\u0026lt;font color=\u0026#34;#5ea6e5\u0026#34;\u0026gt;blog.fomal.cc\u0026lt;/font\u0026gt;\u0026lt;/b\u0026gt;\u0026lt;/a\u0026gt;\u0026lt;br\u0026gt;\u0026lt;a href=\u0026#34;https://aa.fomal.cc\u0026#34;\u0026gt;\u0026lt;b\u0026gt;\u0026lt;font color=\u0026#34;#5ea6e5\u0026#34;\u0026gt;aa.fomal.cc\u0026lt;/font\u0026gt;\u0026lt;/b\u0026gt;\u0026lt;/a\u0026gt;\u0026lt;br\u0026gt;\u0026lt;a href=\u0026#34;https://bb.fomal.cc\u0026#34;\u0026gt;\u0026lt;b\u0026gt;\u0026lt;font color=\u0026#34;#5ea6e5\u0026#34;\u0026gt;bb.fomal.cc\u0026lt;/font\u0026gt;\u0026lt;/b\u0026gt;\u0026lt;/a\u0026gt;\u0026lt;br\u0026gt;\u0026lt;a href=\u0026#34;mailto:admin@fomal.cn\u0026#34;\u0026gt;📬：\u0026lt;b\u0026gt;\u0026lt;font color=\u0026#34;#a591e0\u0026#34;\u0026gt;admin@fomal.cn\u0026lt;/font\u0026gt;\u0026lt;/b\u0026gt;\u0026lt;/a\u0026gt;\u0026lt;/center\u0026gt;\t#公告栏内容 card_recent_post: enable: false limit: 3 # if set 0 will show all sort: date # date or updated sort_order: # Don\u0026#39;t modify the setting unless you know how it works card_categories: enable: false limit: 8 # if set 0 will show all expand: none # none/true/false sort_order: # Don\u0026#39;t modify the setting unless you know how it works card_tags: enable: false limit: 20 # if set 0 will show all color: true sort_order: # Don\u0026#39;t modify the setting unless you know how it works card_archives: enable: false type: monthly # yearly or monthly format: MMMM YYYY # eg: YYYY年MM月 order: -1 # Sort of order. 1, asc for ascending; -1, desc for descending limit: 8 # if set 0 will show all sort_order: # Don\u0026#39;t modify the setting unless you know how it works card_webinfo: enable: true post_count: true last_push_date: true sort_order: # Don\u0026#39;t modify the setting unless you know how it works card_weibo: enable: true 访问人数(UV 和 PV) 修改主题配置文件_config.butterfly.yml\n1 2 3 4 busuanzi: site_uv: true # 本站总访客数 site_pv: true # 本站总访问量 page_pv: true # 本文总阅读量 运行时间 修改主题配置文件_config.butterfly.yml\n1 2 3 4 5 6 7 8 # Time difference between publish date and now (網頁運行時間) # Formal: Month/Day/Year Time or Year/Month/Day Time runtimeshow: enable: false publish_date: 21/9/2023 00:00:00 ##网页开通时间 #格式: 月/日/年 时间 #也可以写成 年/月/日 时间 最新评论% v3.1.0 以上支持。如果未配置任何评论，前先不要开启该功能。\n最新评论只会在刷新时才会去读取，并不会实时变化。 由于 API 有 访问次数限制，为了避免调用太多，主题默认存取期限为 10 分鐘。也就是説，调用后资料会存在 localStorage 里，10分鐘内刷新网站只会去 localStorage 读取资料。 10 分鐘期限一过，刷新页面时才会去调取 API 读取新的数据。（3.6.0 新增了 storage 配置，可自行配置缓存时间）。\n修改主题配置文件_config.butterfly.yml\n1 2 3 4 5 6 7 # Aside widget - Newest Comments newest_comments: enable: true sort_order: # Don\u0026#39;t modify the setting unless you know how it works limit: 6 # 显示的数量 storage: 10 # 设置缓存时间，单位 分钟 avatar: true # 是否显示头像 网站背景 修改主题配置文件_config.butterfly.yml\n1 2 3 4 # 图片格式 url(http://xxxxxx.com/xxx.jpg) # 颜色（HEX值/RGB值/颜色单词/渐变色) # 留空 不显示背景 background: url(https://source.fomal.cc/img/dm1.webp) 如果你的网站根目录不是'/'，使用本地图片时，需加上你的根目录。 例如：网站是 https://yoursite.com/blog，引用一张img/xx.png图片，则设置background为 url(/blog/img/xx.png)\n打字效果 % 详见：activate-power-mode\n修改主题配置文件_config.butterfly.yml\n1 2 3 4 5 6 7 # Typewriter Effect (打字效果) # https://github.com/disjukr/activate-power-mode activate_power_mode: enable: false colorful: true # open particle animation (冒光特效) shake: true # open shake (抖動特效) mobile: false footer 背景 修改主题配置文件_config.butterfly.yml\n1 2 # footer是否显示图片背景(与top_img一致) footer_bg: true 留空/false：显示默认的颜色 图片链接：显示所配置的图片 颜色包括HEX值 - #0000FF | RGB值 - rgb(0,0,255) | 颜色单词 - orange | 渐变色 - linear-gradient( 135deg, #E2B0FF 10%, #9F44D3 100%)：对应的颜色 true：显示跟 top_img 一样 背景特效 可设置每次刷新更换彩带，或者每次点击更换彩带。详细配置可查看canvas_ribbon\n修改主题配置文件_config.butterfly.yml\n好看的彩带背景，会飘动。 修改主题配置文件_config.butterfly.yml\n1 2 3 canvas_fluttering_ribbon: enable: true mobile: true # false 手机端不显示 true 手机端显示 鼠标点击效果 烟花 zIndex建议只在-1和9999上选。 -1 代表烟火效果在底部。 9999 代表烟火效果在前面。\n修改主题配置文件_config.butterfly.yml\n1 2 3 4 fireworks: enable: true zIndex: 9999 # -1 or 9999 mobile: false 爱心 修改主题配置文件_config.butterfly.yml\n1 2 3 4 # 点击出現爱心 click_heart: enable: true mobile: false 自定义字体和字体大小 全局字体 修改主题配置文件_config.butterfly.yml中的font-family属性即可，如不需要配置，请留空。\n1 2 3 4 5 6 7 8 9 10 11 # Global font settings # Don\u0026#39;t modify the following settings unless you know how they work (非必要不要修改) font: global-font-size: \u0026#39;15px\u0026#39; code-font-size: \u0026#39;14px\u0026#39; # -apple-system, BlinkMacSystemFont, \u0026#34;Segoe UI\u0026#34; , \u0026#34;Helvetica Neue\u0026#34; , Lato, Roboto, \u0026#34;PingFang SC\u0026#34; , \u0026#34;Microsoft JhengHei\u0026#34; , \u0026#34;Microsoft YaHei\u0026#34; , sans-serif # Wenkai, consolas, -apple-system, \u0026#39;Quicksand\u0026#39;, \u0026#39;Nimbus Roman No9 L\u0026#39;, \u0026#39;PingFang SC\u0026#39;, \u0026#39;Hiragino Sans GB\u0026#39;, \u0026#39;Noto Serif SC\u0026#39;, \u0026#39;Microsoft Yahei\u0026#39;, \u0026#39;WenQuanYi Micro Hei\u0026#39;, \u0026#39;ST Heiti\u0026#39;, sans-serif; font-family: var(--global-font), Consolas_1, -apple-system, \u0026#39;Quicksand\u0026#39;, \u0026#39;Nimbus Roman No9 L\u0026#39;, \u0026#39;PingFang SC\u0026#39;, \u0026#39;Hiragino Sans GB\u0026#39;, \u0026#39;Noto Serif SC\u0026#39;, \u0026#39;Microsoft Yahei\u0026#39;, \u0026#39;WenQuanYi Micro Hei\u0026#39;, \u0026#39;ST Heiti\u0026#39;, sans-serif; # consolas, ZhuZiAWan_light, \u0026#34;Microsoft YaHei\u0026#34;, Menlo, \u0026#34;PingFang SC\u0026#34;, \u0026#34;Microsoft JhengHei\u0026#34;, sans-serif # Consolas_1, ZhuZiAWan_light, \u0026#34;Microsoft YaHei\u0026#34;, Menlo, \u0026#34;PingFang SC\u0026#34;, \u0026#34;Microsoft JhengHei\u0026#34;, sans-serif code-font-family: Consolas_1, var(--global-font), \u0026#34;Microsoft YaHei\u0026#34;, Menlo, \u0026#34;PingFang SC\u0026#34;, \u0026#34;Microsoft JhengHei\u0026#34;, sans-serif Blog 标题字体 修改主题配置文件_config.butterfly.yml中的blog_title_font属性即可，如不需要配置，请留空。 如不需要使用网络字体，只需要把font_link留空就行。\n1 2 3 4 5 6 7 # Font settings for the site title and site subtitle # https://fonts.googleapis.com/css?family=Titillium+Web\u0026amp;display=swap # Titillium Web, \u0026#39;PingFang SC\u0026#39; , \u0026#39;Hiragino Sans GB\u0026#39; , \u0026#39;Microsoft JhengHei\u0026#39; , \u0026#39;Microsoft YaHei\u0026#39; , sans-serif # 左上角網站名字 主頁居中網站名字 blog_title_font: font_link: font-family: var(--global-font) 网站副标题 可设置主页中显示的网站副标题或者喜欢的座右铭。\n修改主题配置文件_config.butterfly.yml中的subtitle\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # the subtitle on homepage (主頁subtitle) subtitle: enable: true # Typewriter Effect (打字效果) effect: true # loop (循環打字) loop: true # source 調用第三方服務 # source: false 關閉調用 # source: 1 調用一言網的一句話（簡體） https://hitokoto.cn/ # source: 2 調用一句網（簡體） http://yijuzhan.com/ # source: 3 調用今日詩詞（簡體） https://www.jinrishici.com/ # subtitle 會先顯示 source , 再顯示 sub 的內容 source: false # 如果關閉打字效果，subtitle 只會顯示 sub 的第一行文字 sub: - \u0026#34;Welcome to Fomalhaut🥝のTiny Home!🤣🤣🤣\u0026#34; - \u0026#34;Hope you have a nice day!🍭🍭🍭\u0026#34; 页面加载动画preloader 当进入网页时，因为加载速度的问题，可能会导致top_img图片出现断层显示，或者网页加载不全而出现等待时间，开启preloader后，会显示加载动画，等页面加载完，加载动画会消失。\n1 2 # 加载动画 Loading Animation preloader: true 字数统计 注意必须要安装依赖才能设置为true，否则会报错！\n安装插件：在你的博客根目录，打开cmd命令窗口执行npm install hexo-wordcount --save。 开启配置：修改主题配置文件_config.butterfly.yml中的wordcount 1 2 3 4 5 wordcount: enable: false post_wordcount: true min2read: true total_wordcount: true 图片大图查看模式 只能开启一个。 如果你并不想为某张图片添加大图查看模式，你可以使用 html 格式引用图片，并为图片添加 no-lightbox class 名，例如：\u0026lt;img src=\u0026quot;xxxx.jpg\u0026quot; class=\u0026quot;no-lightbox\u0026quot;\u0026gt;。\nfancybox（推荐）\n修改主题配置文件_config.butterfly.yml中fancybox属性\n1 2 # fancybox http://fancyapps.com/fancybox/3/ fancybox: true Pjax% 当用户点击链接，通过 ajax 更新页面需要变化的部分，然后使用 HTML5 的 pushState 修改浏览器的 URL 地址。这样可以不用重复加载相同的资源（css/js）， 从而提升网页的加载速度。\n1 2 3 4 5 6 7 8 9 10 11 # Pjax [Beta] # It may contain bugs and unstable, give feedback when you find the bugs. # https://github.com/MoOx/pjax pjax: enable: true # 对于一些第三方插件，有些并不支持 pjax 。 # 你可以把网页加入到 exclude 里，这个网页会被 pjax 排除在外。 # 点击该网页会重新加载网站。 exclude: - /music/ - /no-pjax/ 注意：使用 pjax 后，一些自己DIY的js可能会无效，跳转页面时需要重新调用（例如朋友圈、说说等），具体请参考Pjax文档。\nInject 如想添加额外的 js/css/meta 等等东西，可以在 Inject 里添加，head(\u0026lt;/body\u0026gt;标签之前)， bottom(\u0026lt;/html\u0026gt;标签之前)。\n1 2 3 4 5 6 7 8 # Inject # Insert the code to head (before \u0026#39;\u0026lt;/head\u0026gt;\u0026#39; tag) and the bottom (before \u0026#39;\u0026lt;/body\u0026gt;\u0026#39; tag) # 插入代码到头部 \u0026lt;/head\u0026gt; 之前 和 底部 \u0026lt;/body\u0026gt; 之前 inject: head: - \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;/xxx.css\u0026#34;\u0026gt; bottom: - \u0026lt;script src=\u0026#34;xxxx\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; hexo魔改 配置文章链接转数字或字母 参考\n1 npm install hexo-abbrlink --save 在_config.yml文件中替换\n1 permalink: posts/:abbrlink.html 并在最后加入\n1 2 3 4 # 文章链接转数字或字母：https://github.com/rozbo/hexo-abbrlink abbrlink: alg: crc32 #support crc16(default) and crc32 rep: hex #support dec(default) and hex 本地搜索系统 安装依赖：前往博客根目录\n1 npm install hexo-generator-search --save 注入配置：修改站点配置文件_config.yml，添加如下代码：\n1 2 3 4 5 # 本地搜索：https://github.com/wzpan/hexo-generator-search search: path: search.xml field: all content: true 主题中开启搜索：在主题配置文件_config.butterfly.yml中修改以下内容：\n1 2 3 local_search: - enable: false + enable: true 重新编译运行，即可看到效果：前往博客根目录，打开cmd命令窗口依次执行如下命令：\n1 hexo cl \u0026amp;\u0026amp; hexo g \u0026amp;\u0026amp; hexo s 百度主动推送 undo 1 npm install hexo-baidu-url-submit --save 在_config.yml文件中添加如下\n1 2 3 4 5 6 deploy: - type: \u0026#39;git\u0026#39; repository: github: git@github.com:ktzxy/ktzxy.github.io.git branch: main - type: baidu_url_submitter #这是新加的百度主动推送 1 2 3 4 5 6 7 # 百度主动推送 # https://github.com/huiwang/hexo-baidu-url-submit baidu_url_submit: count: 1 # 提交最新的多少个链接 host: blog.anheyu.com # 在百度站长平台中添加的域名 token: Rgem9kAECSLflQq6 # 秘钥 path: baidu_urls.txt # 文本文档的地址， 新链接会保存在此文本文档里 Live2D教程（店长） 安装 在Hexo根目录[BlogRoot]下打开终端，输入以下指令安装必要插件：\n1 npm install --save hexo-helper-live2d 打开站点配置文件[BlogRoot]\\config.yml 搜索live2d,按照如下注释内容指示进行操作。 如果没有搜到live2d的配置项，就直接把以下内容复制到最底部。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # Live2D ## https://github.com/EYHN/hexo-helper-live2d live2d: enable: true #开关插件版看板娘 scriptFrom: local # 默认 pluginRootPath: live2dw/ # 插件在站点上的根目录(相对路径) pluginJsPath: lib/ # 脚本文件相对与插件根目录路径 pluginModelPath: assets/ # 模型文件相对与插件根目录路径 # scriptFrom: jsdelivr # jsdelivr CDN # scriptFrom: unpkg # unpkg CDN # scriptFrom: https://npm.elemecdn.com/live2d-widget@3.x/lib/L2Dwidget.min.js # 你的自定义 url tagMode: false # 标签模式, 是否仅替换 live2d tag标签而非插入到所有页面中 debug: false # 调试, 是否在控制台输出日志 model: use: live2d-widget-model-shizuku # npm-module package name # use: wanko # 博客根目录/live2d_models/ 下的目录名 # use: ./wives/wanko # 相对于博客根目录的路径 # use: https://npm.elemecdn.com/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json # 你的自定义 url display: position: right #控制看板娘位置 width: 150 #控制看板娘大小 height: 300 #控制看板娘大小 mobile: show: false # 手机中是否展示 完成后保存修改，在Hexo根目录下运行指令。\n1 2 3 hexo clean hexo g hexo s 之所以必须要使用hexo clean是因为我们需要清空缓存重新生成静态页面，不然看板娘没被加入生成的静态页面里，是不会出现的。\n更换 同样是在Hexo根目录[BlogRoot]下，打开终端，选择想要的看板娘进行安装，例如我这里用到的是 live2d-widget-model-koharu，一个Q版小正太。其他的模型也可以在模型预览里查看以供选择。\nHexo添加Live2D看板娘+模型预览_hexo博客看板娘预览-CSDN博客\n输入指令\n1 2 3 npm install --save live2d-widget-model-koharu #npm install --save live2d-widget-model-shizuku 然后在站点配置文件[BlogRoot]\\_config.yml里找到model项修改为期望的模型\n1 2 3 model: use: live2d-widget-model-shizuku # 默认为live2d-widget-model-wanko 之后按部就班的运行\n1 2 3 hexo clean hexo g hexo s 就能在localhost:4000上查看效果了。\n卸载看板娘 卸载插件和卸载模型的指令都是通过npm进行操作的。在博客根目录[BlogRoot]打开终端，输入：\n1 2 npm uninstall hexo-helper-live2d #卸载看板娘插件 npm uninstall live2d-widget-model-modelname #卸载看板娘模型。记得替换modelname为看板娘名称 卸载后为了保证配置项不出错，记得把[BlogRoot]\\_config.yml里的配置项给注释或者删除掉。\nsitemap 1 2 npm install hexo-generator-sitemap --save npm install hexo-generator-baidu-sitemap --save-dev 站点配置文件[BlogRoot]\\_config.yml\n1 2 3 4 5 6 7 8 9 10 11 # https://github.com/hexojs/hexo-generator-sitemap # Sitemap主要是为了让搜索引擎更加了解清楚你的网站结构，通过你的网站结构更改对你网站的抓取策略，同时更深层次的抓取你的链接。让你的网站有更多的收录。 sitemap: path: sitemap.xml rel: false tags: true categories: true # https://github.com/coneycode/hexo-generator-baidu-sitemap baidusitemap: path: baidusitemap.xml Rss 1 npm install hexo-generator-feed --save 1 2 3 4 5 6 7 # https://github.com/hexojs/hexo-generator-feed #Feed Atom feed: type: atom path: atom.xml limit: 20 rss: /atom.xml *追番插件（vmid未设置）undo 1 npm install hexo-bilibili-bangumi --save 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # 追番插件 # https://github.com/HCLonely/hexo-bilibili-bangumi bangumi: # 追番设置 enable: true path: vmid: 372204786 title: \u0026#39;追番列表\u0026#39; quote: \u0026#39;生命不息，追番不止！\u0026#39; show: 1 lazyload: false loading: metaColor: color: webp: progress: extra_options: key: value cinema: # 追剧设置 enable: false path: vmid: 372204786 title: \u0026#39;追剧列表\u0026#39; quote: \u0026#39;生命不息，追剧不止！\u0026#39; show: 1 lazyload: true loading: metaColor: color: webp: progress: extra_options: key: value 番剧更新\n1 hexo bangumi -u aplayer音乐播放器 在博客根目录[BlogRoot]下打开终端，运行以下指令：\n1 npm install hexo-tag-aplayer --save 在网站配置文件_config.yml中修改aplayer 配置项为：\n1 2 3 4 5 # APlayer 吸底音乐 # https://github.com/MoePlayer/hexo-tag-aplayer/blob/master/docs/README-zh_cn.md aplayer: meting: true asset_inject: false 在主题配置文件_config.butterfly.yml中修改aplayerInject配置项为：\n1 2 3 4 # Inject the css and script (aplayer/meting) aplayerInject: enable: true per_page: false 在你想要加入音乐播放器的页面加入以下语句：\n1 2 # aplayer音乐 - \u0026lt;div class=\u0026#34;aplayer no-destroy\u0026#34; data-id=\u0026#34;8152976493\u0026#34; data-server=\u0026#34;netease\u0026#34; data-type=\u0026#34;playlist\u0026#34; data-order=\u0026#34;list\u0026#34; data-fixed=\u0026#34;true\u0026#34; data-preload=\u0026#34;auto\u0026#34; data-autoplay=\u0026#34;false\u0026#34; data-mutex=\u0026#34;true\u0026#34; \u0026gt;\u0026lt;/div\u0026gt; 其中data-id为歌单ID可以换为你喜欢的歌曲，其他参数见详情页这里不再赘述！\npwa配置 undo 有问题 插件安装\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 npm install --global gulp-cli # 全局安装gulp命令集 npm install workbox-build gulp --save # 安装workbox和gulp插件 # 压缩html插件 npm install gulp-htmlclean --save-dev npm install --save gulp-htmlmin # 压缩css插件 npm install gulp-clean-css --save-dev # 压缩js插件 # 使用babel压缩js，与terser二选一 npm install --save-dev gulp-uglify npm install --save-dev gulp-babel @babel/core @babel/preset-env # 使用terser压缩js，与babel二选一 推荐 npm install gulp-terser --save-dev npm install --save-dev gulp-babel @babel/core @babel/preset-env # 压缩图片插件 npm install --save-dev gulp-imagemin # 压缩字体插件(font-min仅支持压缩ttf格式的字体包) npm install gulp-fontmin --save-dev 关于 font-min 的补充说明，在本文中，是通过读取所有编译好的 html 文件（./public/*/.html）中的字符，然后匹配原有字体包内./public/fonts/.ttf 字体样式，输出压缩后的字体包到./public/fontsdest/目录。所以最终引用字体的相对路径应该是/fontsdest/.ttf。而本地测试时，如果没有运行 gulp，自然也就不会输出压缩字体包到 public 目录，也就看不到字体样式。\ngulp-terser 只会直接压缩 js 代码，所以不存在因为语法变动导致的错误 。事实上，当我们使用 jsdelivr 的 CDN 服务时，只需要在 css 或者 js 的后缀前添加.min,例如 example.js-\u0026gt;example.min.js,JsDelivr 就会自动使用 terser 帮我们压缩好代码。\n在 package.json 中添加\n1 \u0026#34;type\u0026#34;: \u0026#34;module\u0026#34;, 创建gulpfile.js 在 Hexo 的根目录，创建一个gulpfile.js文件,打开[Blogroot]/gulpfile.js,\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 /* * @Description: gulp * @Author: 蓝桉 * pwa配置文件 */ import gulp from \u0026#34;gulp\u0026#34;; import cleanCSS from \u0026#34;gulp-clean-css\u0026#34;; import htmlmin from \u0026#34;gulp-htmlmin\u0026#34;; import htmlclean from \u0026#34;gulp-htmlclean\u0026#34;; import workbox from \u0026#34;workbox-build\u0026#34;; import fontmin from \u0026#34;gulp-fontmin\u0026#34;; // 若使用babel压缩js，则取消下方注释，并注释terser的代码 // var uglify = require(\u0026#39;gulp-uglify\u0026#39;); // var babel = require(\u0026#39;gulp-babel\u0026#39;); // 若使用terser压缩js import terser from \u0026#34;gulp-terser\u0026#34;; //pwa gulp.task(\u0026#34;generate-service-worker\u0026#34;, () =\u0026gt; { return workbox.injectManifest({ swSrc: \u0026#34;./sw-template.js\u0026#34;, swDest: \u0026#34;./public/sw.js\u0026#34;, globDirectory: \u0026#34;./public\u0026#34;, globPatterns: [ // 缓存所有以下类型的文件，极端不推荐 // \u0026#34;**/*.{html,css,js,json,woff2,xml}\u0026#34; // 推荐只缓存404，主页和主要样式和脚本。 \u0026#34;404.html\u0026#34;, \u0026#34;index.html\u0026#34;, \u0026#34;js/main.js\u0026#34;, \u0026#34;css/index.css\u0026#34;, ], modifyURLPrefix: { \u0026#34;\u0026#34;: \u0026#34;./\u0026#34;, }, }); }); //minify js babel // 若使用babel压缩js，则取消下方注释，并注释terser的代码 // gulp.task(\u0026#39;compress\u0026#39;, () =\u0026gt; // gulp.src([\u0026#39;./public/**/*.js\u0026#39;, \u0026#39;!./public/**/*.min.js\u0026#39;]) // .pipe(babel({ // presets: [\u0026#39;@babel/preset-env\u0026#39;] // })) // .pipe(uglify().on(\u0026#39;error\u0026#39;, function(e){ // console.log(e); // })) // .pipe(gulp.dest(\u0026#39;./public\u0026#39;)) // ); // minify js - gulp-tester // 若使用terser压缩js gulp.task(\u0026#34;compress\u0026#34;, () =\u0026gt; gulp .src([ \u0026#34;./public/**/*.js\u0026#34;, \u0026#34;!./public/**/*.min.js\u0026#34;, \u0026#34;!./public/js/custom/galmenu.js\u0026#34;, \u0026#34;!./public/js/custom/gitcalendar.js\u0026#34;, ]) .pipe(terser()) .pipe(gulp.dest(\u0026#34;./public\u0026#34;)) ); //css gulp.task(\u0026#34;minify-css\u0026#34;, () =\u0026gt; { return gulp .src(\u0026#34;./public/**/*.css\u0026#34;) .pipe( cleanCSS({ compatibility: \u0026#34;ie11\u0026#34;, }) ) .pipe(gulp.dest(\u0026#34;./public\u0026#34;)); }); // 压缩 public 目录内 html gulp.task(\u0026#34;minify-html\u0026#34;, () =\u0026gt; { return gulp .src(\u0026#34;./public/**/*.html\u0026#34;) .pipe(htmlclean()) .pipe( htmlmin({ removeComments: true, //清除 HTML 註释 collapseWhitespace: true, //压缩 HTML collapseBooleanAttributes: true, //省略布尔属性的值 \u0026lt;input checked=\u0026#34;true\u0026#34;/\u0026gt; ==\u0026gt; \u0026lt;input /\u0026gt; removeEmptyAttributes: true, //删除所有空格作属性值 \u0026lt;input id=\u0026#34;\u0026#34; /\u0026gt; ==\u0026gt; \u0026lt;input /\u0026gt; removeScriptTypeAttributes: true, //删除 \u0026lt;script\u0026gt; 的 type=\u0026#34;text/javascript\u0026#34; removeStyleLinkTypeAttributes: true, //删除 \u0026lt;style\u0026gt; 和 \u0026lt;link\u0026gt; 的 type=\u0026#34;text/css\u0026#34; minifyJS: true, //压缩页面 JS minifyCSS: true, //压缩页面 CSS minifyURLs: true, }) ) .pipe(gulp.dest(\u0026#34;./public\u0026#34;)); }); //压缩字体 function minifyFont(text, cb) { gulp .src(\u0026#34;./public/fonts/*.ttf\u0026#34;) //原字体所在目录 .pipe( fontmin({ text: text, }) ) .pipe(gulp.dest(\u0026#34;./public/fontsdest/\u0026#34;)) //压缩后的输出目录 .on(\u0026#34;end\u0026#34;, cb); } gulp.task(\u0026#34;mini-font\u0026#34;, cb =\u0026gt; { var buffers = []; gulp .src([\u0026#34;./public/**/*.html\u0026#34;]) //HTML文件所在目录请根据自身情况修改 .on(\u0026#34;data\u0026#34;, function (file) { buffers.push(file.contents); }) .on(\u0026#34;end\u0026#34;, function () { var text = Buffer.concat(buffers).toString(\u0026#34;utf-8\u0026#34;); minifyFont(text, cb); }); }); // 执行 gulp 命令时执行的任务 gulp.task( \u0026#34;default\u0026#34;, gulp.series(\u0026#34;generate-service-worker\u0026#34;, gulp.parallel(\u0026#34;compress\u0026#34;, \u0026#34;minify-html\u0026#34;, \u0026#34;minify-css\u0026#34;, \u0026#34;mini-font\u0026#34;)) ); 创建在 Hexo 的根目录，创建一个sw-template.js文件,打开[Blogroot]/sw-template.js,输入以下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 /* * @Description: sw * @Author: 蓝桉 * pwa配置文件 */ const workboxVersion = \u0026#34;5.1.3\u0026#34;; importScripts(`https://storage.googleapis.com/workbox-cdn/releases/${workboxVersion}/workbox-sw.js`); workbox.core.setCacheNameDetails({ prefix: \u0026#34;蓝桉\u0026#34;, }); workbox.core.skipWaiting(); workbox.core.clientsClaim(); // 注册成功后要立即缓存的资源列表 // 具体缓存列表在gulpfile.js中配置，见下文 workbox.precaching.precacheAndRoute(self.__WB_MANIFEST, { directoryIndex: null, }); // 清空过期缓存 workbox.precaching.cleanupOutdatedCaches(); // 图片资源（可选，不需要就注释掉） // workbox.routing.registerRoute( // /\\.(?:png|jpg|jpeg|gif|bmp|webp|svg|ico)$/, // new workbox.strategies.CacheFirst({ // cacheName: \u0026#39;images\u0026#39;, // plugins: [ // new workbox.expiration.ExpirationPlugin({ // maxEntries: 1000, // maxAgeSeconds: 60 * 60 * 24 * 30, // }), // new workbox.cacheableResponse.CacheableResponsePlugin({ // statuses: [0, 200], // }), // ], // }) // ) // 字体文件（可选，不需要就注释掉） workbox.routing.registerRoute( /\\.(?:eot|ttf|woff|woff2)$/, new workbox.strategies.CacheFirst({ cacheName: \u0026#34;fonts\u0026#34;, plugins: [ new workbox.expiration.ExpirationPlugin({ maxEntries: 1000, maxAgeSeconds: 60 * 60 * 24 * 30, }), new workbox.cacheableResponse.CacheableResponsePlugin({ statuses: [0, 200], }), ], }) ); // 谷歌字体（可选，不需要就注释掉） workbox.routing.registerRoute( /^https:\\/\\/fonts\\.googleapis\\.com/, new workbox.strategies.StaleWhileRevalidate({ cacheName: \u0026#34;google-fonts-stylesheets\u0026#34;, }) ); workbox.routing.registerRoute( /^https:\\/\\/fonts\\.gstatic\\.com/, new workbox.strategies.CacheFirst({ cacheName: \u0026#34;google-fonts-webfonts\u0026#34;, plugins: [ new workbox.expiration.ExpirationPlugin({ maxEntries: 1000, maxAgeSeconds: 60 * 60 * 24 * 30, }), new workbox.cacheableResponse.CacheableResponsePlugin({ statuses: [0, 200], }), ], }) ); // jsdelivr的CDN资源（可选，不需要就注释掉） // workbox.routing.registerRoute( // /^https:\\/\\/cdn\\.jsdelivr\\.net/, // new workbox.strategies.CacheFirst({ // cacheName: \u0026#39;static-libs\u0026#39;, // plugins: [ // new workbox.expiration.ExpirationPlugin({ // maxEntries: 1000, // maxAgeSeconds: 60 * 60 * 24 * 30, // }), // new workbox.cacheableResponse.CacheableResponsePlugin({ // statuses: [0, 200], // }), // ], // }) // ) workbox.googleAnalytics.initialize(); 在[Blogroot]\\themes\\butterfly\\layout\\includes\\third-party\\目录下新建pwanotice.pug文件， 打开[Blogroot]\\themes\\butterfly\\layout\\includes\\third-party\\pwanotice.pug,输入：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #app-refresh.app-refresh(style=\u0026#39;position: fixed;top: -2.2rem;left: 0;right: 0;z-index: 99999;padding: 0 1rem;font-size: 15px;height: 2.2rem;transition: all 0.3s ease;\u0026#39;) .app-refresh-wrap(style=\u0026#39; display: flex;color: #fff;height: 100%;align-items: center;justify-content: center;\u0026#39;) label ✨ 有新文章啦！ 👉 a(href=\u0026#39;javascript:void(0)\u0026#39; onclick=\u0026#39;location.reload()\u0026#39;) span(style=\u0026#39;color: #fff;text-decoration: underline;cursor: pointer;\u0026#39;) 🍗点击食用🍔 script. if (\u0026#39;serviceWorker\u0026#39; in navigator) { if (navigator.serviceWorker.controller) { navigator.serviceWorker.addEventListener(\u0026#39;controllerchange\u0026#39;, function() { showNotification() }) } window.addEventListener(\u0026#39;load\u0026#39;, function() { navigator.serviceWorker.register(\u0026#39;/sw.js\u0026#39;) }) } function showNotification() { if (GLOBAL_CONFIG.Snackbar) { var snackbarBg = document.documentElement.getAttribute(\u0026#39;data-theme\u0026#39;) === \u0026#39;light\u0026#39; ? GLOBAL_CONFIG.Snackbar.bgLight : GLOBAL_CONFIG.Snackbar.bgDark var snackbarPos = GLOBAL_CONFIG.Snackbar.position Snackbar.show({ text: \u0026#39;✨ 有新文章啦！ 👉\u0026#39;, backgroundColor: snackbarBg, duration: 500000, pos: snackbarPos, actionText: \u0026#39;🍗点击食用🍔\u0026#39;, actionTextColor: \u0026#39;#fff\u0026#39;, onActionClick: function(e) { location.reload() }, }) } else { var showBg = document.documentElement.getAttribute(\u0026#39;data-theme\u0026#39;) === \u0026#39;light\u0026#39; ? \u0026#39;#3b70fc\u0026#39; : \u0026#39;#1f1f1f\u0026#39; var cssText = `top: 0; background: ${showBg};` document.getElementById(\u0026#39;app-refresh\u0026#39;).style.cssText = cssText } } 修改[Blogroot]\\themes\\butterfly\\layout\\includes\\additional-js.pug,在文件底部添加以下内容，注意缩进。butterfly_v3.6.0取消了缓存配置，转为完全默认，需要将{cache:theme.fragment_cache}改为{cache: true}:\n1 2 3 4 5 6 7 if theme.pjax.enable !=partial(\u0026#39;includes/third-party/pjax\u0026#39;, {}, {cache:theme.fragment_cache}) !=partial(\u0026#39;includes/third-party/baidu_push\u0026#39;, {}, {cache:theme.fragment_cache}) + if theme.pwa.enable + !=partial(\u0026#39;includes/third-party/pwanotice\u0026#39;, {}, {cache: true}) 将你的图标包移入相应的目录，例如我是/img/siteicon/，所以放到[Blogroot]/source/img/siteicon/目录下。\n新建文件名为manifest.json并将其放到[Blogroot]/source目录下，此时还不能直接用，需要添加一些内容，以下是我的manifest.json配置内容，权且作为参考，其中的theme_color建议用取色器取设计的图标的主色调，同时务必配置 start_url 和 name 的配置项，这关系到你之后能否看到浏览器的应用安装按钮。：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 { \u0026#34;name\u0026#34;: \u0026#34;蓝桉`Blog\u0026#34;, \u0026#34;short_name\u0026#34;: \u0026#34;蓝桉\u0026#34;, \u0026#34;theme_color\u0026#34;: \u0026#34;#3b70fc\u0026#34;, \u0026#34;background_color\u0026#34;: \u0026#34;#3b70fc\u0026#34;, \u0026#34;display\u0026#34;: \u0026#34;standalone\u0026#34;, \u0026#34;scope\u0026#34;: \u0026#34;/\u0026#34;, \u0026#34;start_url\u0026#34;: \u0026#34;/\u0026#34;, \u0026#34;icons\u0026#34;: [ { \u0026#34;src\u0026#34;: \u0026#34;/img/siteicon/16.png\u0026#34;, \u0026#34;sizes\u0026#34;: \u0026#34;16x16\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;image/png\u0026#34; }, { \u0026#34;src\u0026#34;: \u0026#34;/img/siteicon/32.png\u0026#34;, \u0026#34;sizes\u0026#34;: \u0026#34;32x32\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;image/png\u0026#34; }, { \u0026#34;src\u0026#34;: \u0026#34;/img/siteicon/48.png\u0026#34;, \u0026#34;sizes\u0026#34;: \u0026#34;48x48\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;image/png\u0026#34; }, { \u0026#34;src\u0026#34;: \u0026#34;/img/siteicon/64.png\u0026#34;, \u0026#34;sizes\u0026#34;: \u0026#34;64x64\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;image/png\u0026#34; }, { \u0026#34;src\u0026#34;: \u0026#34;/img/siteicon/128.png\u0026#34;, \u0026#34;sizes\u0026#34;: \u0026#34;128x128\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;image/png\u0026#34; }, { \u0026#34;src\u0026#34;: \u0026#34;/img/siteicon/144.png\u0026#34;, \u0026#34;sizes\u0026#34;: \u0026#34;144x144\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;image/png\u0026#34; }, { \u0026#34;src\u0026#34;: \u0026#34;/img/siteicon/512.png\u0026#34;, \u0026#34;sizes\u0026#34;: \u0026#34;512x512\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;image/png\u0026#34; } ], \u0026#34;splash_pages\u0026#34;: null } 打开主题配置文件[Blogroot]/_config.butterfly.yml,找到PWA配置项。添加图标路径。这里的 theme_color 建议改成你图标的主色调，包括manifest.json中的theme_color也是如此。\n1 2 3 4 5 6 7 8 pwa: enable: true manifest: /manifest.json theme_color: \u0026#34;#3b70fc\u0026#34; apple_touch_icon: /img/siteicon/128.png favicon_32_32: /img/siteicon/32.png favicon_16_16: /img/siteicon/16.png mask_icon: /img/siteicon/128.png 1 hexo cl;hexo g;gulp;hexo s 留言板：薇尔莉特信封 在[BlogRoot]运行指令\n1 npm install hexo-butterfly-envelope --save 在站点配置文件_config.yml或主题配置文件_config.butterfly.yml添加以下配置项\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # envelope_comment # see https://akilar.top/posts/e2d3c450/ envelope_comment: enable: true #控制开关 custom_pic: cover: https://npm.elemecdn.com/hexo-butterfly-envelope/lib/violet.jpg #信笺头部图片 line: https://npm.elemecdn.com/hexo-butterfly-envelope/lib/line.png #信笺底部图片 beforeimg: https://npm.elemecdn.com/hexo-butterfly-envelope/lib/before.png # 信封前半部分 afterimg: https://npm.elemecdn.com/hexo-butterfly-envelope/lib/after.png # 信封后半部分 message: #信笺正文，多行文本，写法如下 - 有什么想问的？ - 有什么想说的？ - 有什么想吐槽的？ - 哪怕是有什么想吃的，都可以告诉我哦~ bottom: 自动书记人偶竭诚为您服务！ #仅支持单行文本 height: #1050px，信封划出的高度 path: #【可选】comments 的路径名称。默认为 comments，生成的页面为 comments/index.html front_matter: #【可选】comments页面的 front_matter 配置 title: 留言板 comments: true 1 留言板: /comments/ || fas fa-envelope wowjs动画 安装插件,在博客根目录[BlogRoot]下打开终端，运行以下指令：\n1 npm install hexo-butterfly-wowjs --save 添加配置信息，以下为写法示例 在站点配置文件_config.yml或者主题配置文件_config.butterfly.yml中添加\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 wowjs: enable: true #控制动画开关。true是打开，false是关闭 priority: 10 #过滤器优先级 mobile: false #移动端是否启用，默认移动端禁用 animateitem: - class: recent-post-item #必填项，需要添加动画的元素的class style: animate__zoomIn #必填项，需要添加的动画 duration: 1.5s #选填项，动画持续时间，单位可以是ms也可以是s。例如3s，700ms。 delay: 200ms #选填项，动画开始的延迟时间，单位可以是ms也可以是s。例如3s，700ms。 offset: 30 #选填项，开始动画的距离（相对浏览器底部） iteration: 1 #选填项，动画重复的次数 - class: card-widget style: animate__zoomIn delay: 200ms # - class: flink-list-card # style: wowpanels - class: flink-list-card style: animate__flipInY duration: 3s - class: flink-list-card style: animate__animated duration: 3s - class: article-sort-item style: animate__slideInRight duration: 1.5s - class: site-card style: animate__flipInY duration: 3s - class: site-card style: animate__animated duration: 3s animate_css: https://cdn.cbd.int/hexo-butterfly-wowjs/lib/animate.min.css wow_js: https://cdn.cbd.int/hexo-butterfly-wowjs/lib/wow.min.js wow_init_js: https://cdn.cbd.int/hexo-butterfly-wowjs/lib/wow_init.js 参数释义\n参数 备选值/类型 释义 enable true/false 【必选】控制开关 priority number 【可选】过滤器优先级，数值越小，执行越早，默认为10，选填 mobile true/false 控制移动端是否启用，默认移动端禁用 animateitem.class class 【可选】添加动画类名，只支持给class添加 animateitem.style text 【可选】动画样式，具体类型参考animate.css animateitem.duration time,单位为s或ms 【可选】动画持续时间，单位可以是ms也可以是s。例如3s，700ms。 animateitem.delay time,单位为s或ms 【可选】动画开始的延迟时间，单位可以是ms也可以是s。例如3s，700ms。 animateitem.offset number,单位为px 【可选】开始动画的距离（相对浏览器底部）。 animateitem.iteration number,单位为s或ms 【可选】动画重复的次数 animate_css URL 【可选】animate.css的CDN链接,默认为https://npm.elemecdn.com/hexo-butterfly-wowjs/lib/animate.min.css wow_js URL 【可选】wow.min.js的CDN链接,默认为https://npm.elemecdn.com/hexo-butterfly-wowjs/lib/wow.min.js wow_init_js URL 【可选】wow_init.js的CDN链接,默认为https://npm.elemecdn.com/hexo-butterfly-wowjs/lib/wow_init.js wowjs详细用法见原帖。\n配置自定义css，一图流 参考安知鱼博客\n在[BlogRoot]\\source文件夹下新建一个文件夹css，该文件夹用于存放自定义的css样式，再新建一个名为custom.css，在里面写入以下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 /* 自定义样式 */ /* @font-face { font-family: Candyhome; src: url(https://npm.elemecdn.com/anzhiyu-blog@1.1.6/fonts/Candyhome.ttf); font-display: swap; font-weight: lighter; } */ @font-face { font-family: ZhuZiAYuanJWD; src: url(https://npm.elemecdn.com/anzhiyu-blog@1.1.6/fonts/ZhuZiAWan.woff2); font-display: swap; font-weight: lighter; } div#menus { font-family: \u0026#34;ZhuZiAYuanJWD\u0026#34;; } h1#site-title { font-family: ZhuZiAYuanJWD; font-size: 3em !important; } a.article-title, a.blog-slider__title, a.categoryBar-list-link, h1.post-title { font-family: ZhuZiAYuanJWD; } .iconfont { font-family: \u0026#34;iconfont\u0026#34; !important; font-size: 3em; /* 可以定义图标大小 */ font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* 时间轴生肖icon */ svg.icon { /* 这里定义svg.icon，避免和Butterfly自带的note标签冲突 */ width: 1em; height: 1em; /* width和height定义图标的默认宽度和高度*/ vertical-align: -0.15em; fill: currentColor; overflow: hidden; } .icon-zhongbiao::before { color: #f7c768; } /* bilibli番剧插件 */ #article-container .bangumi-tab.bangumi-active { background: var(--anzhiyu-theme); color: var(--anzhiyu-ahoverbg); border-radius: 10px; } a.bangumi-tab:hover { text-decoration: none !important; } .bangumi-button:hover { background: var(--anzhiyu-theme) !important; border-radius: 10px !important; color: var(--anzhiyu-ahoverbg) !important; } a.bangumi-button.bangumi-nextpage:hover { text-decoration: none !important; } .bangumi-button { padding: 5px 10px !important; } a.bangumi-tab { padding: 5px 10px !important; } svg.icon.faa-tada { font-size: 1.1em; } .bangumi-info-item { border-right: 1px solid #f2b94b; } .bangumi-info-item span { color: #f2b94b; } .bangumi-info-item em { color: #f2b94b; } /* 解决artitalk的图标问题 */ #uploadSource \u0026gt; svg { width: 1.19em; height: 1.5em; } /*top-img黑色透明玻璃效果移除，不建议加，除非你执着于完全一图流或者背景图对比色明显 */ #page-header:not(.not-top-img):before { background-color: transparent !important; } /* 首页文章卡片 */ #recent-posts \u0026gt; .recent-post-item { background: rgba(255, 255, 255, 0.9); } /* 首页侧栏卡片 */ #aside-content .card-widget { background: rgba(255, 255, 255, 0.9); } /* 文章页面正文背景 */ div#post { background: rgba(255, 255, 255, 0.9); } /* 分页页面 */ div#page { background: rgba(255, 255, 255, 0.9); } /* 归档页面 */ div#archive { background: rgba(255, 255, 255, 0.9); } /* 标签页面 */ div#tag { background: rgba(255, 255, 255, 0.9); } /* 分类页面 */ div#category { background: rgba(255, 255, 255, 0.9); } /*夜间模式伪类遮罩层透明*/ [data-theme=\u0026#34;dark\u0026#34;] #recent-posts \u0026gt; .recent-post-item { background: #121212; } [data-theme=\u0026#34;dark\u0026#34;] .card-widget { background: #121212 !important; } [data-theme=\u0026#34;dark\u0026#34;] div#post { background: #121212 !important; } [data-theme=\u0026#34;dark\u0026#34;] div#tag { background: #121212 !important; } [data-theme=\u0026#34;dark\u0026#34;] div#archive { background: #121212 !important; } [data-theme=\u0026#34;dark\u0026#34;] div#page { background: #121212 !important; } [data-theme=\u0026#34;dark\u0026#34;] div#category { background: #121212 !important; } [data-theme=\u0026#34;dark\u0026#34;] div#category { background: transparent !important; } /* 页脚透明 */ #footer { background: transparent !important; } /* 头图透明 */ #page-header { background: transparent !important; } #rightside \u0026gt; div \u0026gt; button { border-radius: 5px; } /* 滚动条 */ ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-thumb { background-color: #3b70fc; border-radius: 2em; } ::-webkit-scrollbar-corner { background-color: transparent; } ::-moz-selection { color: #fff; background-color: #3b70fc; } /* 音乐播放器 */ /* .aplayer .aplayer-lrc { display: none !important; } */ .aplayer.aplayer-fixed.aplayer-narrow .aplayer-body { left: -66px !important; transition: all 0.3s; /* 默认情况下缩进左侧66px，只留一点箭头部分 */ } .aplayer.aplayer-fixed.aplayer-narrow .aplayer-body:hover { left: 0 !important; transition: all 0.3s; /* 鼠标悬停是左侧缩进归零，完全显示按钮 */ } .aplayer.aplayer-fixed { z-index: 999999 !important; } /* 评论框 */ .vwrap { box-shadow: 2px 2px 5px #bbb; background: rgba(255, 255, 255, 0.3); border-radius: 8px; padding: 30px; margin: 30px 0px 30px 0px; } /* 设置评论框 */ .vcard { box-shadow: 2px 2px 5px #bbb; background: rgba(255, 255, 255, 0.3); border-radius: 8px; padding: 30px; margin: 30px 0px 0px 0px; } /* md网站下划线 */ #article-container a:hover { text-decoration: none !important; } #article-container #hpp_talk p img { display: inline; } /* 404页面 */ #error-wrap { position: absolute; top: 40%; right: 0; left: 0; margin: 0 auto; padding: 0 1rem; max-width: 1000px; transform: translate(0, -50%); } #error-wrap .error-content { display: flex; flex-direction: row; justify-content: center; align-items: center; margin: 0 1rem; height: 18rem; border-radius: 8px; background: var(--card-bg); box-shadow: var(--card-box-shadow); transition: all 0.3s; } #error-wrap .error-content .error-img { box-flex: 1; flex: 1; height: 100%; border-top-left-radius: 8px; border-bottom-left-radius: 8px; background-color: #3b70fc; background-position: center; background-size: cover; } #error-wrap .error-content .error-info { box-flex: 1; flex: 1; padding: 0.5rem; text-align: center; font-size: 14px; font-family: Titillium Web, \u0026#34;PingFang SC\u0026#34;, \u0026#34;Hiragino Sans GB\u0026#34;, \u0026#34;Microsoft JhengHei\u0026#34;, \u0026#34;Microsoft YaHei\u0026#34;, sans-serif; } #error-wrap .error-content .error-info .error_title { margin-top: -4rem; font-size: 9em; } #error-wrap .error-content .error-info .error_subtitle { margin-top: -3.5rem; word-break: break-word; font-size: 1.6em; } #error-wrap .error-content .error-info a { display: inline-block; margin-top: 0.5rem; padding: 0.3rem 1.5rem; background: var(--btn-bg); color: var(--btn-color); } #body-wrap.error .aside-list { display: flex; flex-direction: row; flex-wrap: nowrap; bottom: 0px; position: absolute; padding: 1rem; width: 100%; overflow: scroll; } #body-wrap.error .aside-list .aside-list-group { display: flex; flex-direction: row; flex-wrap: nowrap; max-width: 1200px; margin: 0 auto; } #body-wrap.error .aside-list .aside-list-item { padding: 0.5rem; } #body-wrap.error .aside-list .aside-list-item img { width: 100%; object-fit: cover; border-radius: 12px; } #body-wrap.error .aside-list .aside-list-item .thumbnail { overflow: hidden; width: 230px; height: 143px; background: var(--anzhiyu-card-bg); display: flex; } #body-wrap.error .aside-list .aside-list-item .content .title { -webkit-line-clamp: 2; overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; line-height: 1.5; justify-content: center; align-items: flex-end; align-content: center; padding-top: 0.5rem; color: white; } #body-wrap.error .aside-list .aside-list-item .content time { display: none; } /* 代码框主题 */ #article-container figure.highlight { border-radius: 10px; } 在主题配置文件[BlogRoot]\\_config.butterfly.yml文件中的inject配置项的head子项加入以下代码，代表引入刚刚创建的custom.css文件（这是相对路径的写法）\n1 2 3 inject: head: - \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;/css/custom.css\u0026#34; media=\u0026#34;defer\u0026#34; onload=\u0026#34;this.media=\u0026#39;all\u0026#39;\u0026#34;\u0026gt; 在主题配置文件[BlogRoot]\\_config.butterfly.yml文件中的index_img和footer_bg配置项取消头图与页脚图的加载项避免冗余加载\n1 2 3 4 5 # The banner image of home page index_img: # Footer Background footer_bg: false 部分人反映一图流改完了背景图也没了，那大概率是你之前没设置背景图。在主题配置文件[BlogRoot]\\_config.butterfly.yml文件中的background配置项设置背景图\n1 background: url(https://source.fomal.cc/img/home_bg.webp) 页脚Github徽标（店长） 安装插件,在博客根目录[BlogRoot]下打开终端，运行以下指令：\n1 npm install hexo-butterfly-footer-beautify --save 添加配置信息，以下为写法示例 在站点配置文件_config.yml或者主题配置文件_config.butterfly.yml中添加（这是我的配置）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 # footer_beautify # 页脚计时器：[Native JS Timer](https://akilar.top/posts/b941af/) # 页脚徽标：[Add Github Badge](https://akilar.top/posts/e87ad7f8/) footer_beautify: enable: timer: true # 计时器开关 bdage: true # 徽标开关 priority: 5 #过滤器优先权 enable_page: all # 应用页面 exclude: #屏蔽页面 # - /posts/ # - /about/ layout: # 挂载容器类型 type: id name: footer-wrap index: 0 # 计时器部分配置项（看你喜欢哪个，最好下载下来放到自己的项目中不然会增加我网站的负载） # 这是我的 # runtime_js: https://www.fomal.cc/static/js/runtime.js # runtime_css: https://www.fomal.cc/static/css/runtime.min.css # 这是店长的 runtime_js: https://npm.elemecdn.com/hexo-butterfly-footer-beautify@1.0.0/lib/runtime.js runtime_css: https://npm.elemecdn.com/hexo-butterfly-footer-beautify@1.0.0/lib/runtime.css # 徽标部分配置项 swiperpara: 0 #若非0，则开启轮播功能，每行徽标个数 bdageitem: - link: https://hexo.io/ #徽标指向网站链接 shields: https://img.shields.io/badge/Frame-Hexo-blue?style=flat\u0026amp;logo=hexo #徽标API message: 博客框架为Hexo_v6.2.0 #徽标提示语 - link: https://butterfly.js.org/ shields: https://img.shields.io/badge/Theme-Butterfly-6513df?style=flat\u0026amp;logo=bitdefender message: 主题版本Butterfly_v4.3.1 - link: https://vercel.com/ shields: https://img.shields.io/badge/Hosted-Vercel-brightgreen?style=flat\u0026amp;logo=Vercel message: 本站采用多线部署，主线路托管于Vercel - link: https://dashboard.4everland.org/ # https://img.shields.io/badge/Hosted-4EVERLAND-3FE2C1?style=flat\u0026amp;logo=IPFS shields: https://img.shields.io/badge/Hosted-4EVERLAND-22DDDD?style=flat\u0026amp;logo=IPFS message: 本站采用多线部署，备用线路托管于4EVERLAND - link: https://github.com/ shields: https://img.shields.io/badge/Source-Github-d021d6?style=flat\u0026amp;logo=GitHub message: 本站项目由Github托管 - link: http://creativecommons.org/licenses/by-nc-sa/4.0/ shields: https://img.shields.io/badge/Copyright-BY--NC--SA%204.0-d42328?style=flat\u0026amp;logo=Claris message: 本站采用知识共享署名-非商业性使用-相同方式共享4.0国际许可协议进行许可 swiper_css: https://npm.elemecdn.com/hexo-butterfly-swiper/lib/swiper.min.css swiper_js: https://npm.elemecdn.com/hexo-butterfly-swiper/lib/swiper.min.js swiperbdage_init_js: https://npm.elemecdn.com/hexo-butterfly-footer-beautify/lib/swiperbdage_init.min.js 参数释义\n参数 备选值/类型 释义 priority number 【可选】过滤器优先级，数值越小，执行越早，默认为10，选填 enable.timer true/false 【必选】计时器控制开关 enable.bdage true/false 【必选】徽标控制开关 enable_page path 【可选】填写想要应用的页面,如根目录就填’/‘,分类页面就填’/categories/‘。若要应用于所有页面，就填all，默认为all exclude path 【可选】填写想要屏蔽的页面，可以多个。仅当enable_page为’all’时生效。写法见示例。原理是将屏蔽项的内容逐个放到当前路径去匹配，若当前路径包含任一屏蔽项，则不会挂载。 layout.type id/class 【可选】挂载容器类型，填写id或class，不填则默认为id layout.name text 【必选】挂载容器名称 layout.index 0和正整数 【可选】前提是layout.type为class，因为同一页面可能有多个class，此项用来确认究竟排在第几个顺位 runtime_js url 【必选】页脚计时器脚本，可以下载上文填写示例的链接，参照注释和教程：Native JS Timer自行修改。 runtime_css url 【可选】自定义样式，预留开发者接口，可自行下载。 swiperpara number 【可选】若非零，则开启轮播功能，此项表示每行最多容纳徽标个数，用来应对徽标过多显得页脚拥挤的问题 bdageitem.link url 【可选】页脚徽标指向的网站链接 bdageitem.shields url 【必选】页脚徽标对应的API，API具体写法示例参照教程Add Github Badge bdageitem.message text 【可选】页脚徽标悬停时显示的信息 swiper_css url 【可选】swiper的依赖 swiper_js url 【可选】swiper的依赖 swiperbdage_init_js url 【可选】swiper初始化方法 其中，计时器部分和备案号部分自行下载到本地，然后引入\n*首页分类磁贴新版(店长)kill 1 hexo new page categories 安装插件,在博客根目录[BlogRoot]下打开终端，运行以下指令：\n1 npm install hexo-butterfly-categories-card --save 添加配置信息，以下为写法示例 在站点配置文件_config.yml或者主题配置文件_config.butterfly.yml中添加以下代码，注意要根据他的默认描述排序改为你自己对应的分类名字：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # hexo-butterfly-categories-card # see https://akilar.top/posts/a9131002/ categoryBar: enable: true # 开关 priority: 5 #过滤器优先权 enable_page: / # 应用页面 layout: # 挂载容器类型 type: id name: recent-posts index: 0 column: odd # odd：3列 | even：4列 row: 1 #显示行数，默认两行，超过行数切换为滚动显示 message: - descr: Ubuntu指南 cover: https://assets.akilar.top/image/cover1.webp - descr: 玩转Win10 cover: https://assets.akilar.top/image/cover2.webp - descr: 长篇小说连载 cover: https://assets.akilar.top/image/cover3.webp - descr: 个人日记 cover: https://assets.akilar.top/image/cover4.webp - descr: 诗词歌赋 cover: https://assets.akilar.top/image/cover5.webp - descr: 杂谈教程 cover: https://assets.akilar.top/image/cover6.webp custom_css: https://npm.elemecdn.com/hexo-butterfly-categories-card@1.0.0/lib/categorybar.css 参数释义\n参数 备选值/类型 释义 priority number 【可选】过滤器优先级，数值越小，执行越早，默认为10，选填 enable true/false 【必选】控制开关 enable_page path/all 【可选】填写想要应用的页面的相对路径（即路由地址）,如根目录就填’/‘,分类页面就填’/categories/‘。若要应用于所有页面，就填’all’，默认为’/‘ layout.type id/class 【可选】挂载容器类型，填写id或class，不填则默认为id layout.name text 【必选】挂载容器名称 layout.index 0和正整数 【可选】前提是layout.type为class，因为同一页面可能有多个class，此项用来确认究竟排在第几个顺位 column odd/even 【可选】显示列数，考虑到比例问题，只提供3列和4列，odd为3列， even为4列 row number 【可选】显示行数，默认两行，超过行数切换为滚动显示 message.descr text 分类描述,需要和你自己的文章分类一一对应。 message.cover url 分类背景,需要和你自己的文章分类一一对应。 custom_css url 【可选】自定义样式，会替换默认的css链接，可以下载文档给出的cdn链接后自主修改 *首页分类磁贴1.0（小冰） 在博客根目录[BlogRoot]下打开终端，运行以下指令：\n1 npm i hexo-magnet --save 注意，一定要加 --save，不然本地预览的时候可能不会显示！！！\n在网站配置文件_config.yml新增以下项 (注意不是主题配置文件)，这里的分类名字必须和你文章的分类名字一一对应：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 magnet: enable: true priority: 1 enable_page: / type: categories devide: 2 display: - name: 教程 display_name: 小冰の魔改教程 icon: 📚 - name: 游戏评测 display_name: 小冰の游戏评测 icon: 🎮 - name: 生活趣闻 display_name: 小冰の生活趣闻 icon: 🐱‍👓 - name: vue display_name: 小冰の编程学习 icon: 👩‍💻 - name: 学习 display_name: 小冰の读书笔记 icon: 📒 - name: 随想 display_name: 小冰の胡思乱想 icon: 💡 color_setting: text_color: black text_hover_color: white background_color: \u0026#34;#f2f2f2\u0026#34; background_hover_color: \u0026#34;#b30070\u0026#34; layout: type: id name: recent-posts index: 0 temple_html: \u0026#39;\u0026lt;div class=\u0026#34;recent-post-item\u0026#34; style=\u0026#34;width:100%;height: auto\u0026#34;\u0026gt;\u0026lt;div id=\u0026#34;catalog_magnet\u0026#34;\u0026gt;${temple_html_item}\u0026lt;/div\u0026gt;\u0026lt;/div\u0026gt;\u0026#39; plus_style: \u0026#34;\u0026#34; 配置项的含义：\nenable\n参数：true/false 含义：是否开启插件\nenable_page\n参数：/ 含义：路由地址，如 / 代表主页。/me/ 代表自我介绍页等等\npriority\n参数：1 含义：插件的叠放顺序，数字越大，叠放约靠前。\ntype\n参数：categories/tags 含义：选择筛选分类还是标签\ndevide\n参数：2 含义：表示分隔的列数，2 表示分为两列展示\ndisplay\n参数：\n1 2 3 - name: 教程 # 这里是tags或者categories的名称 display_name: 小冰の魔改教程 # 这里是替换的名称 icon: 📚 # 这里是展示的图标 含义：配置项，可自行设置，按照设置的顺序展示\ncolor_setting\n参数：\n1 2 3 4 text_color: black # 文字默认颜色 text_hover_color: white # 文字鼠标悬浮颜色 background_color: \u0026#34;#f2f2f2\u0026#34; # 文字背景默认颜色 background_hover_color: \u0026#34;#b30070\u0026#34; # 文字背景悬浮颜色 含义：颜色配置项，可自行设置\nlayout\n参数：type; （class\u0026amp;id） 参数：name; 参数：index；（数字） 含义：如果说 magnet 是一幅画，那么这个 layout 就是指定了哪面墙来挂画 而在 HTML 的是世界里有两种墙分别 type 为 id 和 class。 其中在定义 class 的时候会出现多个 class 的情况，这时就需要使用 index，确定是哪一个。 最后墙的名字即是 name;\n1 2 3 4 5 6 7 8 \u0026lt;div name=\u0026#34;我是墙\u0026#34; id=\u0026#34;recent-posts\u0026#34;\u0026gt; \u0026lt;!-- id=\u0026gt;type recent-posts=\u0026gt;name --\u0026gt; \u0026lt;div name=\u0026#34;我是画框\u0026#34;\u0026gt; \u0026lt;div name=\u0026#34;我是纸\u0026#34;\u0026gt; \u0026lt;!--这里通过js挂载magnet，也就是画画--\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; temple_html\n参数：html 模板字段 含义：包含挂载容器\n1 2 3 4 5 \u0026lt;div class=\u0026#34;recent-post-item\u0026#34; style=\u0026#34;width:100%;height: auto\u0026#34;\u0026gt; \u0026lt;!--文章容器--\u0026gt; \u0026lt;div id=\u0026#34;catalog_magnet\u0026#34;\u0026gt; \u0026lt;!--挂载容器--\u0026gt; ${temple_html_item} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; plus_style\n参数：“” 含义：提供可自定义的 style，如加入黑夜模式。\n执行 hexo 三连\n1 2 3 hexo clean hexo g hexo s 我们可以看到黑夜模式看起来特别的别扭，因此还要做一下黑夜模式的颜色适配，在custom.css文件中添加以下代码适配黑夜模式(具体颜色可以自己调节)：\n1 2 3 4 5 6 7 8 9 10 11 /* 小冰分类分类磁铁黑夜模式适配 */ /* 一般状态 */ [data-theme=\u0026#34;dark\u0026#34;] .magnet_link_context { background: #1e1e1e; color: antiquewhite; } /* 鼠标悬浮状态 */ [data-theme=\u0026#34;dark\u0026#34;] .magnet_link_context:hover { background: #3ecdf1; color: #f2f2f2; } *文章置顶滚动栏(店长) 安装插件,在博客根目录[BlogRoot]下打开终端，运行以下指令：\n1 npm install hexo-butterfly-swiper --save 添加配置信息，以下为写法示例 在站点配置文件_config.yml或者主题配置文件_config.butterfly.yml中添加\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # hexo-butterfly-swiper # see https://akilar.top/posts/8e1264d1/ swiper: enable: true # 开关 priority: 5 #过滤器优先权 enable_page: all # 应用页面 timemode: date #date/updated layout: # 挂载容器类型 type: id name: recent-posts index: 0 default_descr: 再怎么看我也不知道怎么描述它的啦！ swiper_css: https://npm.elemecdn.com/hexo-butterfly-swiper/lib/swiper.min.css #swiper css依赖 swiper_js: https://npm.elemecdn.com/hexo-butterfly-swiper/lib/swiper.min.js #swiper js依赖 custom_css: https://npm.elemecdn.com/hexo-butterfly-swiper/lib/swiperstyle.css # 适配主题样式补丁 custom_js: https://npm.elemecdn.com/hexo-butterfly-swiper/lib/swiper_init.js # swiper初始化方法 参数释义\n参数 备选值/类型 释义 priority number 【可选】过滤器优先级，数值越小，执行越早，默认为10，选填 enable true/false 【必选】控制开关 enable_page path/all 【可选】填写想要应用的页面的相对路径（即路由地址）,如根目录就填’/‘,分类页面就填’/categories/‘。若要应用于所有页面，就填’all’，默认为all timemode date/updated 【可选】时间显示，date为显示创建日期，updated为显示更新日期,默认为date layout.type id/class 【可选】挂载容器类型，填写id或class，不填则默认为id layout.name text 【必选】挂载容器名称 layout.index 0和正整数 【可选】前提是layout.type为class，因为同一页面可能有多个class，此项用来确认究竟排在第几个顺位 default_descr text 默认文章描述 swiper_css url 【可选】自定义的swiper依赖项css链接 swiper_js url 【可选】自定义的swiper依赖项加js链接 custom_css url 【可选】适配主题样式补丁 custom_js url 【可选】swiper初始化方法 使用方法:在文章的front_matter中添加swiper_index配置项即可。\n1 2 3 4 5 6 7 8 --- title: 文章标题 date: 创建日期 updated: 更新日期 cover: 文章封面 description: 文章描述 swiper_index: 1 #置顶轮播图顺序，非负整数，数字越大越靠前 --- *自定义字体 准备好字体文件后，在[BlogRoot]\\source\\css\\custom.css（没有就自己创建）中添加以下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 @font-face { /* 为载入的字体取名字(随意) */ font-family: \u0026#39;YSHST\u0026#39;;\t/* 字体文件地址(相对或者绝对路径都可以) */ src: url(/font/优设好身体.woff2); /* 定义加粗样式(加粗多少) */ font-weight: normal; /* 定义字体样式(斜体/非斜体) */ font-style: normal; /* 定义显示样式 */ font-display: block; } 各个属性的定义：\nfont-family属性值中使用webfont来声明使用的是服务器端字体，即设置文本的字体名称。 src属性值中首先指定了字体文件所在的路径。 format声明字体文件的格式，可以省略文件格式的声明，单独使用src属性值。 font-style：设置文本样式。取值：normal:不使用斜体；italic:使用斜体；oblique:使用倾斜体；inherit：从父元素继承。 支持格式：.eot(老版本IE)，.otf，.ttf，.woff，*.woff2(推荐) 在主题配置文件_config.butterfly.yml中的font配置项以及blog_title_font配置项写上你刚刚引入的字体名称，系统会根据先后次序从前到后依次加载这些字体：\n1 2 3 4 5 6 7 8 9 10 11 12 # Global font settings # Don\u0026#39;t modify the following settings unless you know how they work (非必要不要修改) font: global-font-size: \u0026#39;15px\u0026#39; code-font-size: \u0026#39;14px\u0026#39; font-family: YSHST, -apple-system, \u0026#39;Quicksand\u0026#39;, \u0026#39;Nimbus Roman No9 L\u0026#39;, \u0026#39;PingFang SC\u0026#39;, \u0026#39;Hiragino Sans GB\u0026#39;, \u0026#39;Noto Serif SC\u0026#39;, \u0026#39;Microsoft Yahei\u0026#39;, \u0026#39;WenQuanYi Micro Hei\u0026#39;, \u0026#39;ST Heiti\u0026#39;, sans-serif; code-font-family: Consolas, YSHST, \u0026#34;Microsoft YaHei\u0026#34;, Menlo, \u0026#34;PingFang SC\u0026#34;, \u0026#34;Microsoft JhengHei\u0026#34;, sans-serif # 左上角網站名字 主頁居中網站名字 blog_title_font: font_link: font-family: YSHST, -apple-system, BlinkMacSystemFont, \u0026#34;Segoe UI\u0026#34; , \u0026#34;Helvetica Neue\u0026#34; , Lato, Roboto, \u0026#34;PingFang SC\u0026#34; , \u0026#34;Microsoft JhengHei\u0026#34; , \u0026#34;Microsoft YaHei\u0026#34; , sans-serif 重启项目即可看到\n1 hexo cl; hexo s 文章双侧栏显示(小冰) 在博客根目录[BlogRoot]下打开终端，运行以下指令：\n1 npm i hexo-butterfly-article-double-row --save 在网站配置文件_config.yml新增以下项 (注意不是主题配置文件)：\n1 2 butterfly_article_double_row: enable: true 这时候插件有个bug，就是最后一页文章数目为奇数的时候，会出现这种情况\n会显得很不舒服，感谢唐志远大佬修复了这个bug，只需要在custom.css文件添加以下代码即可：\n1 2 3 4 5 /* 翻页按钮居中 */ #pagination { width: 100%; margin: auto; } 执行 hexo 三连：\n1 2 3 hexo clean hexo g hexo s *GitCalendar(店长) 安装 安装插件,在博客根目录[BlogRoot]下打开终端，运行以下指令：\n1 npm install hexo-filter-gitcalendar --save 添加配置信息，以下为写法示例 在站点配置文件_config.yml或者主题配置文件如_config.butterfly.yml中添加\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # hexo-filter-gitcalendar gitcalendar: enable: true # 开关 priority: 5 #过滤器优先权 enable_page: / # 应用页面 # butterfly挂载容器 layout: # 挂载容器类型 type: id name: recent-posts index: 0 user: Fomalhaut-Blog #git用户名 apiurl: \u0026#39;https://gitcalendar.fomal.cc\u0026#39;\t# 这是我的API，最好自己弄一个 minheight: pc: 280px #桌面端最小高度 mibile: 0px #移动端最小高度 color: \u0026#34;[\u0026#39;#d9e0df\u0026#39;, \u0026#39;#c6e0dc\u0026#39;, \u0026#39;#a8dcd4\u0026#39;, \u0026#39;#9adcd2\u0026#39;, \u0026#39;#89ded1\u0026#39;, \u0026#39;#77e0d0\u0026#39;, \u0026#39;#5fdecb\u0026#39;, \u0026#39;#47dcc6\u0026#39;, \u0026#39;#39dcc3\u0026#39;, \u0026#39;#1fdabe\u0026#39;, \u0026#39;#00dab9\u0026#39;]\u0026#34; # 目前我在用的 # \u0026#34;[\u0026#39;#e4dfd7\u0026#39;, \u0026#39;#f9f4dc\u0026#39;, \u0026#39;#f7e8aa\u0026#39;, \u0026#39;#f7e8aa\u0026#39;, \u0026#39;#f8df72\u0026#39;, \u0026#39;#fcd217\u0026#39;, \u0026#39;#fcc515\u0026#39;, \u0026#39;#f28e16\u0026#39;, \u0026#39;#fb8b05\u0026#39;, \u0026#39;#d85916\u0026#39;, \u0026#39;#f43e06\u0026#39;]\u0026#34; #橘黄色调 # color: \u0026#34;[\u0026#39;#ebedf0\u0026#39;, \u0026#39;#fdcdec\u0026#39;, \u0026#39;#fc9bd9\u0026#39;, \u0026#39;#fa6ac5\u0026#39;, \u0026#39;#f838b2\u0026#39;, \u0026#39;#f5089f\u0026#39;, \u0026#39;#c4067e\u0026#39;, \u0026#39;#92055e\u0026#39;, \u0026#39;#540336\u0026#39;, \u0026#39;#48022f\u0026#39;, \u0026#39;#30021f\u0026#39;]\u0026#34; #浅紫色调 # color: \u0026#34;[\u0026#39;#ebedf0\u0026#39;, \u0026#39;#f0fff4\u0026#39;, \u0026#39;#dcffe4\u0026#39;, \u0026#39;#bef5cb\u0026#39;, \u0026#39;#85e89d\u0026#39;, \u0026#39;#34d058\u0026#39;, \u0026#39;#28a745\u0026#39;, \u0026#39;#22863a\u0026#39;, \u0026#39;#176f2c\u0026#39;, \u0026#39;#165c26\u0026#39;, \u0026#39;#144620\u0026#39;]\u0026#34; #翠绿色调 # color: \u0026#34;[\u0026#39;#ebedf0\u0026#39;, \u0026#39;#f1f8ff\u0026#39;, \u0026#39;#dbedff\u0026#39;, \u0026#39;#c8e1ff\u0026#39;, \u0026#39;#79b8ff\u0026#39;, \u0026#39;#2188ff\u0026#39;, \u0026#39;#0366d6\u0026#39;, \u0026#39;#005cc5\u0026#39;, \u0026#39;#044289\u0026#39;, \u0026#39;#032f62\u0026#39;, \u0026#39;#05264c\u0026#39;]\u0026#34; #天青色调 container: .recent-post-item(style=\u0026#39;width:100%;height:auto;padding:10px;\u0026#39;) #父元素容器，需要使用pug语法 gitcalendar_css: https://npm.elemecdn.com/hexo-filter-gitcalendar/lib/gitcalendar.css gitcalendar_js: https://npm.elemecdn.com/hexo-filter-gitcalendar/lib/gitcalendar.js\t参数释义\n参数 备选值/类型 释义 priority number 【可选】过滤器优先级，数值越小，执行越早，默认为10，选填 enable true/false 【必选】控制开关 enable_page path/all 【可选】填写想要应用的页面的相对路径（即路由地址）,如根目录就填’/‘,分类页面就填’/categories/‘。若要应用于所有页面，就填’all’，默认为’/‘ layout.type id/class 【可选】挂载容器类型，填写id或class，不填则默认为id layout.name text 【必选】挂载容器名称 layout.index 0和正整数 【可选】前提是layout.type为class，因为同一页面可能有多个class，此项用来确认究竟排在第几个顺位 user text 【必选】git用户名 apiurl url 【可选】默认使用提供文档提供的api，但还是建议自建api，参考教程：自建API部署 minheight.pc 280px 【可选】桌面端最小高度，默认为280px minheight.mobile 0px 【可选】移动端最小高度，默认为0px color list 【可选】一个包含11个色值的数组，文档给出了四款预设值 container pug 【可选】预留的父元素容器，用以适配多主题，需要用pug语法填写，目前已适配butterfly，volantis，matery，mengd主题，这四个主题，插件会自自动识别_config.yml内填写的theme配置项。其余主题需要自己填写父元素容器。 gitcalendar_css URL 【可选】自定义CSS样式链接 gitcalendar_js URL 【可选】自定义js链接 自定义挂载容器 很多人反映不想挂在首页，想挂在关于或者统计等页面，只需要做2步:\n在对应页面创建一个DOM让插件有地方挂载，例如演示的就是在关于页面(/about/)的文件中直接写入一个div块\n1 2 \u0026lt;!-- GitCalendar容器 --\u0026gt; \u0026lt;div id=\u0026#34;gitZone\u0026#34;\u0026gt;\u0026lt;/div\u0026gt; 在对应配置项改为与你容器id以创建页面路径相关的（是改不是加!!!）\n1 2 3 4 5 6 enable_page: /about/ # 应用页面(记住最后带/) layout: # 挂载容器类型 type: id name: gitZone index: 0 重启项目就会看见\n1 hexo cl; hexo s 自建API部署 虽然Vercel的访问应当没有次数限制，但是不排除存在因访问次数过多而限流等限制。所以还是建议各位自建API。使用Vercel部署，完全免费，且无需服务器。\n将此项目fork到你的Github仓库\n访问Vercel官网，点击右上角的sign up进行注册，注册并登录后点击右上角创建一个项目，并选择以Github继续。 此时应该会看到你刚刚fork过来的你仓库的项目，看不到就输入关键字进行搜索。\n点击该仓库右边的Import进行导入，Vercel的PROJECT NAME可以自定义，不用太过在意，但是之后不支持修改，若要改名，只能删除PROJECT以后重建一个了，下方三个选项保持默认就好，点击Deploy进行部署。\n到此时，Vercel的部署已经完成，可以使用Vercel提供的默认域名来访问api链接。例如我获取到的默认域名为github-calendar-api.vercel.app,则用它来替换冰老师教程中的自建API，填写到[BlogRoot]\\_config.butterfly.yml中关于gitcalendar的apiurl中。注意源码修改版不要带协议，不要带后缀。就填写给你的默认域名就好。而插件版需要带协议\n1 2 3 4 5 6 gitcalendar: enable: true simplemode: true user: Akilarlxh apiurl: github-calendar-api.vercel.app color: \u0026#34;[\u0026#39;#e4dfd7\u0026#39;, \u0026#39;#f9f4dc\u0026#39;, \u0026#39;#f7e8aa\u0026#39;, \u0026#39;#f7e8aa\u0026#39;, \u0026#39;#f8df72\u0026#39;, \u0026#39;#fcd217\u0026#39;, \u0026#39;#fcc515\u0026#39;, \u0026#39;#f28e16\u0026#39;, \u0026#39;#fb8b05\u0026#39;, \u0026#39;#d85916\u0026#39;, \u0026#39;#f43e06\u0026#39;]\u0026#34; *导航栏魔改 在[BlogRoot]\\source\\css\\custom.css中引入如下css代码，然后在主题配置文件_config.butterfly.yml中引入该文件：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* 一级菜单居中 */ #nav .menus_items { position: absolute !important; width: fit-content !important; left: 50% !important; transform: translateX(-50%) !important; } /* 子菜单横向展示 */ #nav .menus_items .menus_item:hover .menus_item_child { display: flex !important; } /* 这里的2是代表导航栏的第2个元素，即有子菜单的元素，可以按自己需求修改 */ .menus_items .menus_item:nth-child(2) .menus_item_child { left: -125px; } 此处的css实现了两个作用：菜单栏居中、子菜单横向显示。其中子菜单横向显示要根据自己的实际情况来改，例如你的以及菜单的第2个选项中有子菜单，那就要加一项调节第2个选项中的子菜单，这个具体调节多少要根据你的具体情况为准，可以自己慢慢调到中间。\n此时我们的手机端子菜单默认是展开显示的，如下图所示：\n此时我们只需要在主题配置文件_config.butterfly.yml中列表对应的地方加一个hide即可，如下图的列表选项：\n1 2 3 4 5 6 7 8 9 10 11 menu: 首页: / || fas fa-home 归档: /archives/ || fas fa-archive 标签: /tags/ || fas fa-tags 分类: /categories/ || fas fa-folder-open 列表||fas fa-list || hide: 音乐: /music/ || fas fa-music 电影: /movies/ || fas fa-video 留言板: /comments/ || fas fa-envelope-open 友链: /link/ || fas fa-link 关于: /about/ || fas fa-heart 此时有人觉得右边搜索按钮露出搜索两个字很丑，我们也可以把它隐藏了，在[BlogRoot]\\themes\\Butterfly\\layout\\includes\\header\\nav.pug（npm安装的在[BlogRoot]\\node_moudules\\hexo-theme-butterfly\\layout\\includes\\header\\nav.pug）中把以下语句删除或注释掉即可，搜索两个字就不会显示出来(这种语句统一写法是直接删除+就可以，不用补空格)。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 nav#nav span#blog_name a#site-name(href=url_for(\u0026#39;/\u0026#39;)) #[=config.title] #menus if (theme.algolia_search.enable || theme.local_search.enable) #search-button a.site-page.social-icon.search i.fas.fa-search.fa-fw - span=\u0026#39; \u0026#39;+_p(\u0026#39;search.title\u0026#39;) !=partial(\u0026#39;includes/header/menu_item\u0026#39;, {}, {cache: true}) #toggle-menu a.site-page i.fas.fa-bars.fa-fw *黑夜霓虹灯1.0（js计时器实现） 此教程会有两处地方有霓虹灯效果：一个是大标题和个人信息的动态霓虹灯，默认周期为1200ms；另外的是菜单栏的小字有夜光效果，为你的博客增添几分赛博朋克风~\n首先在自定义的样式文件[BlogRoot]\\source\\css\\custom.css中引入以下代码，变量部分var(--theme-color)可以换为自己喜欢的颜色，例如紫色rgb(179, 71, 241)，后面的颜色连续渐变效果根据个人喜好选择，有的人喜欢连续的，有的人喜欢断续的\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 /* 夜间模式菜单栏发光字 */ [data-theme=\u0026#34;dark\u0026#34;] #nav .site-page, [data-theme=\u0026#34;dark\u0026#34;] #nav .menus_items .menus_item .menus_item_child li a { text-shadow: 0 0 2px var(--theme-color) !important; } /* 手机端适配 */ [data-theme=\u0026#34;dark\u0026#34;] #sidebar #sidebar-menus .menus_items .site-page { text-shadow: 0 0 2px var(--theme-color) !important; } /* 闪烁变动颜色连续渐变 */ #site-name, #site-title, #site-subtitle, #post-info, .author-info__name, .author-info__description { transition: text-shadow 1s linear !important; } 新建文件[BlogRoot]\\source\\js\\light.js并写入以下代码，本质就是计时器，大家可以根据自己的喜好调节闪烁周期，默认为1200ms：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 // 霓虹灯效果 // 颜色数组 var arr = [\u0026#34;#39c5bb\u0026#34;, \u0026#34;#f14747\u0026#34;, \u0026#34;#f1a247\u0026#34;, \u0026#34;#f1ee47\u0026#34;, \u0026#34;#b347f1\u0026#34;, \u0026#34;#1edbff\u0026#34;, \u0026#34;#ed709b\u0026#34;, \u0026#34;#5636ed\u0026#34;]; // 颜色索引 var idx = 0; // 切换颜色 function changeColor() { // 仅夜间模式才启用 if (document.getElementsByTagName(\u0026#39;html\u0026#39;)[0].getAttribute(\u0026#39;data-theme\u0026#39;) == \u0026#39;dark\u0026#39;) { if (document.getElementById(\u0026#34;site-name\u0026#34;)) document.getElementById(\u0026#34;site-name\u0026#34;).style.textShadow = arr[idx] + \u0026#34; 0 0 15px\u0026#34;; if (document.getElementById(\u0026#34;site-title\u0026#34;)) document.getElementById(\u0026#34;site-title\u0026#34;).style.textShadow = arr[idx] + \u0026#34; 0 0 15px\u0026#34;; if (document.getElementById(\u0026#34;site-subtitle\u0026#34;)) document.getElementById(\u0026#34;site-subtitle\u0026#34;).style.textShadow = arr[idx] + \u0026#34; 0 0 10px\u0026#34;; if (document.getElementById(\u0026#34;post-info\u0026#34;)) document.getElementById(\u0026#34;post-info\u0026#34;).style.textShadow = arr[idx] + \u0026#34; 0 0 5px\u0026#34;; try { document.getElementsByClassName(\u0026#34;author-info__name\u0026#34;)[0].style.textShadow = arr[idx] + \u0026#34; 0 0 12px\u0026#34;; document.getElementsByClassName(\u0026#34;author-info__description\u0026#34;)[0].style.textShadow = arr[idx] + \u0026#34; 0 0 12px\u0026#34;; } catch { } idx++; if (idx == 8) { idx = 0; } } else { // 白天模式恢复默认 if (document.getElementById(\u0026#34;site-name\u0026#34;)) document.getElementById(\u0026#34;site-name\u0026#34;).style.textShadow = \u0026#34;#1e1e1ee0 1px 1px 1px\u0026#34;; if (document.getElementById(\u0026#34;site-title\u0026#34;)) document.getElementById(\u0026#34;site-title\u0026#34;).style.textShadow = \u0026#34;#1e1e1ee0 1px 1px 1px\u0026#34;; if (document.getElementById(\u0026#34;site-subtitle\u0026#34;)) document.getElementById(\u0026#34;site-subtitle\u0026#34;).style.textShadow = \u0026#34;#1e1e1ee0 1px 1px 1px\u0026#34;; if (document.getElementById(\u0026#34;post-info\u0026#34;)) document.getElementById(\u0026#34;post-info\u0026#34;).style.textShadow = \u0026#34;#1e1e1ee0 1px 1px 1px\u0026#34;; try { document.getElementsByClassName(\u0026#34;author-info__name\u0026#34;)[0].style.textShadow = \u0026#34;\u0026#34;; document.getElementsByClassName(\u0026#34;author-info__description\u0026#34;)[0].style.textShadow = \u0026#34;\u0026#34;; } catch { } } } // 开启计时器 window.onload = setInterval(changeColor, 1200); 在主题配置文件_config.butterfly.yml引入以上两个文件，要注意的是，js文件这里必须为defer，不能为ansyc，保证脚本会延迟到整个页面都解析完后再执行，此时才有对应的元素进行操作：\n1 2 3 4 5 inject: head: - \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;/css/custom.css\u0026#34; media=\u0026#34;defer\u0026#34; onload=\u0026#34;this.media=\u0026#39;all\u0026#39;\u0026#34;\u0026gt; bottom: - \u0026lt;script defer src=\u0026#34;/js/light.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; # 霓虹灯(必须defer否则有时候会不生效) 重启项目即可看到效果\n1 hexo cl; hexo s *星空背景和流星特效 在[BlogRoot]/source/js目录下新建universe.js，输入以下代码：\n1 2 3 JS function dark() {window.requestAnimationFrame=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame;var n,e,i,h,t=.05,s=document.getElementById(\u0026#34;universe\u0026#34;),o=!0,a=\u0026#34;180,184,240\u0026#34;,r=\u0026#34;226,225,142\u0026#34;,d=\u0026#34;226,225,224\u0026#34;,c=[];function f(){n=window.innerWidth,e=window.innerHeight,i=.216*n,s.setAttribute(\u0026#34;width\u0026#34;,n),s.setAttribute(\u0026#34;height\u0026#34;,e)}function u(){h.clearRect(0,0,n,e);for(var t=c.length,i=0;i\u0026lt;t;i++){var s=c[i];s.move(),s.fadeIn(),s.fadeOut(),s.draw()}}function y(){this.reset=function(){this.giant=m(3),this.comet=!this.giant\u0026amp;\u0026amp;!o\u0026amp;\u0026amp;m(10),this.x=l(0,n-10),this.y=l(0,e),this.r=l(1.1,2.6),this.dx=l(t,6*t)+(this.comet+1-1)*t*l(50,120)+2*t,this.dy=-l(t,6*t)-(this.comet+1-1)*t*l(50,120),this.fadingOut=null,this.fadingIn=!0,this.opacity=0,this.opacityTresh=l(.2,1-.4*(this.comet+1-1)),this.do=l(5e-4,.002)+.001*(this.comet+1-1)},this.fadeIn=function(){this.fadingIn\u0026amp;\u0026amp;(this.fadingIn=!(this.opacity\u0026gt;this.opacityTresh),this.opacity+=this.do)},this.fadeOut=function(){this.fadingOut\u0026amp;\u0026amp;(this.fadingOut=!(this.opacity\u0026lt;0),this.opacity-=this.do/2,(this.x\u0026gt;n||this.y\u0026lt;0)\u0026amp;\u0026amp;(this.fadingOut=!1,this.reset()))},this.draw=function(){if(h.beginPath(),this.giant)h.fillStyle=\u0026#34;rgba(\u0026#34;+a+\u0026#34;,\u0026#34;+this.opacity+\u0026#34;)\u0026#34;,h.arc(this.x,this.y,2,0,2*Math.PI,!1);else if(this.comet){h.fillStyle=\u0026#34;rgba(\u0026#34;+d+\u0026#34;,\u0026#34;+this.opacity+\u0026#34;)\u0026#34;,h.arc(this.x,this.y,1.5,0,2*Math.PI,!1);for(var t=0;t\u0026lt;30;t++)h.fillStyle=\u0026#34;rgba(\u0026#34;+d+\u0026#34;,\u0026#34;+(this.opacity-this.opacity/20*t)+\u0026#34;)\u0026#34;,h.rect(this.x-this.dx/4*t,this.y-this.dy/4*t-2,2,2),h.fill()}else h.fillStyle=\u0026#34;rgba(\u0026#34;+r+\u0026#34;,\u0026#34;+this.opacity+\u0026#34;)\u0026#34;,h.rect(this.x,this.y,this.r,this.r);h.closePath(),h.fill()},this.move=function(){this.x+=this.dx,this.y+=this.dy,!1===this.fadingOut\u0026amp;\u0026amp;this.reset(),(this.x\u0026gt;n-n/4||this.y\u0026lt;0)\u0026amp;\u0026amp;(this.fadingOut=!0)},setTimeout(function(){o=!1},50)}function m(t){return Math.floor(1e3*Math.random())+1\u0026lt;10*t}function l(t,i){return Math.random()*(i-t)+t}f(),window.addEventListener(\u0026#34;resize\u0026#34;,f,!1),function(){h=s.getContext(\u0026#34;2d\u0026#34;);for(var t=0;t\u0026lt;i;t++)c[t]=new y,c[t].reset();u()}(),function t(){document.getElementsByTagName(\u0026#39;html\u0026#39;)[0].getAttribute(\u0026#39;data-theme\u0026#39;)==\u0026#39;dark\u0026#39;\u0026amp;\u0026amp;u(),window.requestAnimationFrame(t)}()}; dark() 在[BlogRoot]/source/css目录下新建universe.css，输入以下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /* 背景宇宙星光 */ #universe{ display: block; position: fixed; margin: 0; padding: 0; border: 0; outline: 0; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; /* 这个是调置顶的优先级的，-1在文章页下面，背景上面，个人推荐这种 */ z-index: -1; } 在主题配置文件_config.butterfly.yml的inject配置项中bottom下填入：\n1 2 3 4 inject: bottom: - \u0026lt;canvas id=\u0026#34;universe\u0026#34;\u0026gt;\u0026lt;/canvas\u0026gt; - \u0026lt;script defer src=\u0026#34;/js/universe.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 在主题配置文件_config.butterfly.yml的inject配置项中head下填入：\n1 2 3 inject: head: - \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;/css/universe.css\u0026#34;\u0026gt; 重新编译即可看到效果。\n侧边栏电子时钟(安知鱼) 如果有安装店长的插件版侧边栏电子钟（与店长的电子钟冲突），在博客根目录[BlogRoot]下打开终端，运行以下指令\n1 2 # 卸载原版电子钟 npm uninstall hexo-butterfly-clock 安装插件,在博客根目录[BlogRoot]下打开终端，运行以下指令：\n1 npm install hexo-butterfly-clock-anzhiyu --save 添加配置信息，以下为写法示例 在主题配置文件_config.butterfly.yml（注意一定要主题配置文件）中添加：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # electric_clock (安知鱼电子钟) # see https://anzhiy.cn/posts/fc18.html electric_clock: enable: true # 开关 priority: 5 #过滤器优先权 enable_page: / # 应用页面 exclude: # - /posts/ # - /about/ layout: # 挂载容器类型 type: class name: sticky_layout index: 0 loading: https://cdn.cbd.int/hexo-butterfly-clock-anzhiyu/lib/loading.gif #加载动画自定义 clock_css: https://cdn.cbd.int/hexo-butterfly-clock-anzhiyu/lib/clock.min.css clock_js: https://cdn.cbd.int/hexo-butterfly-clock-anzhiyu/lib/clock.min.js ip_api: https://widget.qweather.net/simple/static/js/he-simple-common.js?v=2.0 qweather_key: # 和风天气key gaud_map_key: # 高得地图web服务key default_rectangle: false # 开启后将一直显示rectangle位置的天气，否则将获取访问者的地理位置与天气 rectangle: 113.34532,23.15624 # 获取访问者位置失败时会显示该位置的天气，同时该位置为开启default_rectangle后的位置 其中qweather_key 和gaud_map_key 最好自己去申请对应的 api key，不保证可靠性。\n参数释义\n参数 备选值/类型 释义 priority number 【可选】过滤器优先级，数值越小，执行越早，默认为 10，选填 enable true/false 【必选】控制开关 enable_page path/all 【可选】填写想要应用的页面的相对路径（即路由地址）,如根目录就填’/‘,分类页面就填’/categories/‘。若要应用于所有页面，就填’all’，默认为 all exclude path 【可选】填写想要屏蔽的页面，可以多个。写法见示例。原理是将屏蔽项的内容逐个放到当前路径去匹配，若当前路径包含任一屏蔽项，则不会挂载。 layout.type id/class 【可选】挂载容器类型，填写 id 或 class，不填则默认为 id layout.name text 【必选】挂载容器名称 layout.index 0和正整数 【可选】前提是 layout.type 为 class，因为同一页面可能有多个 class，此项用来确认究竟排在第几个顺位 loading URL 【可选】电子钟加载动画的图片 clock_css URL 【可选】电子钟样式 CDN 资源 clock_js URL 【可选】电子钟执行脚本 CDN 资源 ip_api URL 【可选】获取时钟 IP 的 API qweather_key text 【可选】和风天气 key gaud_map_key text 【可选】高得地图 web 服务 key default_rectangle text 【可选】开启后将一直显示 rectangle 位置的天气，否则将获取访问者的地理位置与天气 rectangle text 【可选】获取访问者位置失败时会显示该位置的天气，同时该位置为开启 default_rectangle 后的位置 一、qweather_key申请地址: https://id.qweather.com/#/login\n登录后进入控制台 创建应用 填写应用名称和 key 名称随意 选择 WebApi 复制 key 二、gaud_map_key 申请地址: https://lbs.amap.com/\n登录后进入控制台 创建应用，名称随意，类型选其他 点击添加, key名称随意，服务平台选择Web服务,点击提交 复制 key 个人卡片渐变色 在[BlogRoot]\\source\\css\\custom.css自定义样式的文件中引入如下代码（最后记得在inject配置项引入!!!）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 /* 侧边栏个人信息卡片动态渐变色 */ #aside-content \u0026gt; .card-widget.card-info { background: linear-gradient( -45deg, #e8d8b9, #eccec5, #a3e9eb, #bdbdf0, #eec1ea ); box-shadow: 0 0 5px rgb(66, 68, 68); position: relative; background-size: 400% 400%; -webkit-animation: Gradient 10s ease infinite; -moz-animation: Gradient 10s ease infinite; animation: Gradient 10s ease infinite !important; } @-webkit-keyframes Gradient { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } @-moz-keyframes Gradient { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } @keyframes Gradient { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } /* 黑夜模式适配 */ [data-theme=\u0026#34;dark\u0026#34;] #aside-content \u0026gt; .card-widget.card-info { background: #191919ee; } /* 个人信息Follow me按钮 */ #aside-content \u0026gt; .card-widget.card-info \u0026gt; #card-info-btn { background-color: #3eb8be; border-radius: 8px; } 外挂标签的引入（店长） 安装插件,在博客根目录[BlogRoot]下打开终端，运行以下指令：\n1 npm install hexo-butterfly-tag-plugins-plus --save 考虑到hexo自带的markdown渲染插件hexo-renderer-marked与外挂标签语法的兼容性较差，建议您将其替换成hexo-renderer-kramed\n1 2 npm uninstall hexo-renderer-marked --save npm install hexo-renderer-kramed --save 添加配置信息，以下为写法示例 在站点配置文件_config.yml或者主题配置文件_config.butterfly.yml中添加\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 外挂标签 # tag-plugins-plus # see https://akilar.top/posts/615e2dec/ tag_plugins: enable: true # 开关 priority: 5 #过滤器优先权 issues: false #issues标签依赖注入开关 link: placeholder: /img/siteicon/64.png #link_card标签默认的图标图片 CDN: anima: https://cdn.cbd.int/hexo-butterfly-tag-plugins-plus@latest/lib/assets/font-awesome-animation.min.css #动画标签anima的依赖 jquery: https://npm.elemecdn.com/jquery@latest/dist/jquery.min.js #issues标签依赖 issues: https://npm.elemecdn.com/hexo-butterfly-tag-plugins-plus@latest/lib/assets/issues.js #issues标签依赖 iconfont: /js/ali_font_all.js #参看https://akilar.top/posts/d2ebecef/ carousel: https://npm.elemecdn.com/hexo-butterfly-tag-plugins-plus@latest/lib/assets/carousel-touch.js tag_plugins_css: https://npm.elemecdn.com/hexo-butterfly-tag-plugins-plus@latest/lib/tag_plugins.css 参数释义\n参数 备选值/类型 释义 enable true/false 【必选】控制开关 priority number 【可选】过滤器优先级，数值越小，执行越早，默认为10，选填 issues true/false 【可选】issues标签控制开关，默认为false link.placeholder 【必选】link卡片外挂标签的默认图标 CDN.anima URL 【可选】动画标签anima的依赖 CDN.jquery URL 【可选】issues标签依赖 CDN.issues URL 【可选】issues标签依赖 CDN.iconfont URL 【可选】iconfont标签symbol样式引入，如果不想引入，则设为false CDN.carousel URL 【可选】carousel旋转相册标签鼠标拖动依赖，如果不想引入则设为false CDN.tag_plugins_css URL 【可选】外挂标签样式的CSS依赖，为避免CDN缓存延迟，建议将@latest改为具体版本号 具体样式和写法可见：Markdown语法与外挂标签写法汇总\niconfont选项，可将font自行下载放在source/js/目录下，新建目录\n*听话的鼠标魔改 新建文件[BlogRoot]\\source\\js\\cursor.js，在里面写上如下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 JS 复制成功var CURSOR; Math.lerp = (a, b, n) =\u0026gt; (1 - n) * a + n * b; const getStyle = (el, attr) =\u0026gt; { try { return window.getComputedStyle ? window.getComputedStyle(el)[attr] : el.currentStyle[attr]; } catch (e) {} return \u0026#34;\u0026#34;; }; class Cursor { constructor() { this.pos = {curr: null, prev: null}; this.pt = []; this.create(); this.init(); this.render(); } move(left, top) { this.cursor.style[\u0026#34;left\u0026#34;] = `${left}px`; this.cursor.style[\u0026#34;top\u0026#34;] = `${top}px`; } create() { if (!this.cursor) { this.cursor = document.createElement(\u0026#34;div\u0026#34;); this.cursor.id = \u0026#34;cursor\u0026#34;; this.cursor.classList.add(\u0026#34;hidden\u0026#34;); document.body.append(this.cursor); } var el = document.getElementsByTagName(\u0026#39;*\u0026#39;); for (let i = 0; i \u0026lt; el.length; i++) if (getStyle(el[i], \u0026#34;cursor\u0026#34;) == \u0026#34;pointer\u0026#34;) this.pt.push(el[i].outerHTML); document.body.appendChild((this.scr = document.createElement(\u0026#34;style\u0026#34;))); // 这里改变鼠标指针的颜色 由svg生成 this.scr.innerHTML = `* {cursor: url(\u0026#34;data:image/svg+xml,\u0026lt;svg xmlns=\u0026#39;http://www.w3.org/2000/svg\u0026#39; viewBox=\u0026#39;0 0 8 8\u0026#39; width=\u0026#39;8px\u0026#39; height=\u0026#39;8px\u0026#39;\u0026gt;\u0026lt;circle cx=\u0026#39;4\u0026#39; cy=\u0026#39;4\u0026#39; r=\u0026#39;4\u0026#39; opacity=\u0026#39;.5\u0026#39;/\u0026gt;\u0026lt;/svg\u0026gt;\u0026#34;) 4 4, auto}`; } refresh() { this.scr.remove(); this.cursor.classList.remove(\u0026#34;hover\u0026#34;); this.cursor.classList.remove(\u0026#34;active\u0026#34;); this.pos = {curr: null, prev: null}; this.pt = []; this.create(); this.init(); this.render(); } init() { document.onmouseover = e =\u0026gt; this.pt.includes(e.target.outerHTML) \u0026amp;\u0026amp; this.cursor.classList.add(\u0026#34;hover\u0026#34;); document.onmouseout = e =\u0026gt; this.pt.includes(e.target.outerHTML) \u0026amp;\u0026amp; this.cursor.classList.remove(\u0026#34;hover\u0026#34;); document.onmousemove = e =\u0026gt; {(this.pos.curr == null) \u0026amp;\u0026amp; this.move(e.clientX - 8, e.clientY - 8); this.pos.curr = {x: e.clientX - 8, y: e.clientY - 8}; this.cursor.classList.remove(\u0026#34;hidden\u0026#34;);}; document.onmouseenter = e =\u0026gt; this.cursor.classList.remove(\u0026#34;hidden\u0026#34;); document.onmouseleave = e =\u0026gt; this.cursor.classList.add(\u0026#34;hidden\u0026#34;); document.onmousedown = e =\u0026gt; this.cursor.classList.add(\u0026#34;active\u0026#34;); document.onmouseup = e =\u0026gt; this.cursor.classList.remove(\u0026#34;active\u0026#34;); } render() { if (this.pos.prev) { this.pos.prev.x = Math.lerp(this.pos.prev.x, this.pos.curr.x, 0.15); this.pos.prev.y = Math.lerp(this.pos.prev.y, this.pos.curr.y, 0.15); this.move(this.pos.prev.x, this.pos.prev.y); } else { this.pos.prev = this.pos.curr; } requestAnimationFrame(() =\u0026gt; this.render()); } } (() =\u0026gt; { CURSOR = new Cursor(); // 需要重新获取列表时，使用 CURSOR.refresh() })(); 其中比较重要的参数就是鼠标的尺寸和颜色，已经在上图中标出，目前发现颜色只支持RGB写法和固有名称写法（例如red这种），其他参数也可以自行摸索：\n1 * {cursor: url(\u0026#34;data:image/svg+xml,\u0026lt;svg xmlns=\u0026#39;http://www.w3.org/2000/svg\u0026#39; viewBox=\u0026#39;0 0 8 8\u0026#39; width=\u0026#39;8px\u0026#39; height=\u0026#39;8px\u0026#39;\u0026gt;\u0026lt;circle cx=\u0026#39;4\u0026#39; cy=\u0026#39;4\u0026#39; r=\u0026#39;4\u0026#39; opacity=\u0026#39;1.0\u0026#39; fill=\u0026#39;rgb(57, 197, 187)\u0026#39;/\u0026gt;\u0026lt;/svg\u0026gt;\u0026#34;) 4 4, auto} 在[BlogRoot]\\source\\css\\custom.css添加如下代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 /* 鼠标样式 */ #cursor { position: fixed; width: 16px; height: 16px; /* 这里改变跟随的底色 */ background: var(--theme-color); border-radius: 8px; opacity: 0.25; z-index: 10086; pointer-events: none; transition: 0.2s ease-in-out; transition-property: background, opacity, transform; } #cursor.hidden { opacity: 0; } #cursor.hover { opacity: 0.1; transform: scale(2.5); -webkit-transform: scale(2.5); -moz-transform: scale(2.5); -ms-transform: scale(2.5); -o-transform: scale(2.5); } #cursor.active { opacity: 0.5; transform: scale(0.5); -webkit-transform: scale(0.5); -moz-transform: scale(0.5); -ms-transform: scale(0.5); -o-transform: scale(0.5); } 这里比较重要的参数就是鼠标跟随的圆形颜色，可以根据自己的喜好进行更改：\n1 2 3 4 #cursor { /* 这里改变跟随的底色 */ background: rgb(57, 197, 187); } 在主题配置文件_config.butterfly.yml文件的inject配置项引入刚刚创建的css文件和js文件：\n1 2 3 inject: bottom: + - \u0026lt;script defer src=\u0026#34;/js/cursor.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 重启项目即可看见效果：\n1 hexo cl; hexo s *页面样式调节 这个教程是通过css样式调节各个页面透明度、模糊度（亚克力效果）、圆角、边框样式等，看起来会更加舒适。\n复制以下代码进去自定义的custom.css文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 :root { --trans-light: rgba(255, 255, 255, 0.88); --trans-dark: rgba(25, 25, 25, 0.88); --border-style: 1px solid rgb(169, 169, 169); --backdrop-filter: blur(5px) saturate(150%); } /* 首页文章卡片 */ #recent-posts \u0026gt; .recent-post-item { background: var(--trans-light); backdrop-filter: var(--backdrop-filter); border-radius: 25px; border: var(--border-style); } /* 首页侧栏卡片 */ #aside-content .card-widget { background: var(--trans-light); backdrop-filter: var(--backdrop-filter); border-radius: 18px; border: var(--border-style); } /* 文章页、归档页、普通页面 */ div#post, div#page, div#archive { background: var(--trans-light); backdrop-filter: var(--backdrop-filter); border: var(--border-style); border-radius: 20px; } /* 导航栏 */ #page-header.nav-fixed #nav { background: rgba(255, 255, 255, 0.75); backdrop-filter: var(--backdrop-filter); } [data-theme=\u0026#34;dark\u0026#34;] #page-header.nav-fixed #nav { background: rgba(0, 0, 0, 0.7) !important; } /* 夜间模式遮罩 */ [data-theme=\u0026#34;dark\u0026#34;] #recent-posts \u0026gt; .recent-post-item, [data-theme=\u0026#34;dark\u0026#34;] #aside-content .card-widget, [data-theme=\u0026#34;dark\u0026#34;] div#post, [data-theme=\u0026#34;dark\u0026#34;] div#archive, [data-theme=\u0026#34;dark\u0026#34;] div#page { background: var(--trans-dark); } /* 夜间模式页脚页头遮罩透明 */ [data-theme=\u0026#34;dark\u0026#34;] #footer::before { background: transparent !important; } [data-theme=\u0026#34;dark\u0026#34;] #page-header::before { background: transparent !important; } /* 阅读模式 */ .read-mode #aside-content .card-widget { background: rgba(158, 204, 171, 0.5) !important; } .read-mode div#post { background: rgba(158, 204, 171, 0.5) !important; } /* 夜间模式下的阅读模式 */ [data-theme=\u0026#34;dark\u0026#34;] .read-mode #aside-content .card-widget { background: rgba(25, 25, 25, 0.9) !important; color: #ffffff; } [data-theme=\u0026#34;dark\u0026#34;] .read-mode div#post { background: rgba(25, 25, 25, 0.9) !important; color: #ffffff; } 参数说明：\n--trans-light：白天模式带透明度的背景色，如rgba(255, 255, 255, 0.88)底色是纯白色，其中0.88就透明度，在0-1之间调节，值越大越不透明； --trans-dark: 夜间模式带透明度的背景色，如rgba(25, 25, 25, 0.88)底色是柔和黑色，其中0.88就透明度，在0-1之间调节，值越大越不透明; --border-style: 边框样式，1px solid rgb(169, 169, 169)指宽度为1px的灰色实体边框; --backdrop-filter: 背景过滤器，如blur(5px) saturate(150%)表示饱和度为150%的、高斯模糊半径为5px的过滤器，这是亚克力效果的一种实现方法; 大家可以根据自己喜好进行调节，不用拘泥于我的样式！ 记住在主题配置文件_config.butterfly.yml的inject配置项中引入该css文件：\n1 2 3 inject: head: + - \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;/css/custom.css\u0026#34;\u0026gt; 重启项目即可看见效果：\n1 hexo cl; hexo s 引入iconfont自定义图标（店长） 新建图标项目 访问阿里巴巴矢量图标库,注册登录。\n搜索自己心仪的图标，然后选择添加入库，加到购物车。\n选择完毕后点击右上角的购物车图标，打开侧栏，选择添加到项目，如果没有项目就新建一个。\n可以通过上方顶栏菜单-\u0026gt;资源管理-\u0026gt;我的项目，找到之前添加的图标项目。(现在的iconfont可以在图标库的项目设置里直接打开彩色设置，然后采用fontclass的引用方式即可使用多彩图标。但是单一项目彩色图标上限是40个图标，酌情采用。)\n引入图标 线上引入方案，我使用的是官方文档中最便捷的font-class方案。这一方案偶尔会出现图标加载不出的情况。但是便于随时对图标库进行升级，换一下在线链接即可，适合新手使用。最新版本的iconfont支持直接在项目设置中开启彩色图标，从而实现直接用class添加多彩色图标。（推荐直接用这个即可）\n在[BlogRoot]\\themes\\butterfly\\source\\css\\custom.css中填写如下内容，引入Unicode和Font-class的线上资源：\n1 @import \u0026#34;//at.alicdn.com/t/font_2264842_b004iy0kk2b.css\u0026#34;; 更推荐在在主题配置文件inject配置项进行全局引入：\n1 2 3 4 5 inject: head: - \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;//at.alicdn.com/t/font_2264842_b004iy0kk2b.css\u0026#34; media=\u0026#34;defer\u0026#34; onload=\u0026#34;this.media=\u0026#39;all\u0026#39;\u0026#34;\u0026gt; bottom: - \u0026lt;script async src=\u0026#34;//at.alicdn.com/t/font_2264842_b004iy0kk2b.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 同时可以在自定义CSS中添加如下样式来控制图标默认大小和颜色等属性（若已经在项目设置中勾选了彩色选项，则无需再定义图标颜色），写法与字体样式类似，这恐怕也是它被称为iconfont（图标字体）的原因:\n1 2 3 4 5 6 7 8 .iconfont { font-family: \u0026#34;iconfont\u0026#34; !important; /* 这里可以自定义图标大小 */ font-size: 3em; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } 可以通过自己的阿里图标库的font-class方案查询复制相应的icon-xxxx。\n菜单栏多色动态图标（店长） 前置教程：Hexo引入阿里矢量图标库-iconfont inject和基于Butterfly的外挂标签引入-Tag Plugins Plus中关于动态标签anima的内容。请确保您已经完成了前置教程，并实现了在文章中使用symbol写法来引入iconfont图标。同时引入了fontawesome_animation的前置依赖。 主要检查您的inject配置项中是否有这两个依赖\n1 2 3 4 5 6 7 inject: head: #动画标签anima的依赖 - \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;https://fastly.jsdelivr.net/gh/l-lin/font-awesome-animation/dist/font-awesome-animation.min.css\u0026#34; media=\u0026#34;defer\u0026#34; onload=\u0026#34;this.media=\u0026#39;all\u0026#39;\u0026#34;\u0026gt; bottom: # 阿里矢量图标,这串是我的图标库，你的链接会有所不同。 - \u0026lt;script async src=\u0026#34;//at.alicdn.com/t/font_2032782_ev6ytrh30f.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 替换[BlogRoot]\\themes\\butterfly\\layout\\includes\\header\\menu_item.pug为以下代码，本方案默认使用观感最佳的悬停父元素触发子元素动画效果，默认动画为faa-tada。注意：可以把之前的代码注释掉，再在后面加上如下代码，以便于回滚，此代码在butterfly 4.3.1 上可以运行并保留hide字段隐藏子菜单的功能，其他版本自行测试。代码的本质并不复杂，就是扫描配置文件对应的配置项，然后根据||的分割标志筛选出对应的图标名称、对应链接等，从而渲染出html页面。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 if theme.menu .menus_items each value, label in theme.menu if typeof value !== \u0026#39;object\u0026#39; .menus_item - const valueArray = value.split(\u0026#39;||\u0026#39;) a.site-page.faa-parent.animated-hover(href=url_for(trim(value.split(\u0026#39;||\u0026#39;)[0]))) if valueArray[1] i.fa-fw(class=trim(valueArray[1])) - var icon_value = trim(value.split(\u0026#39;||\u0026#39;)[1]) - var anima_value = value.split(\u0026#39;||\u0026#39;)[2] ? trim(value.split(\u0026#39;||\u0026#39;)[2]) : \u0026#39;faa-tada\u0026#39; if icon_value.substring(0,2)==\u0026#34;fa\u0026#34; i.fa-fw(class=icon_value + \u0026#39; \u0026#39; + anima_value) else if icon_value.substring(0,4)==\u0026#34;icon\u0026#34; svg.icon(aria-hidden=\u0026#34;true\u0026#34; class=anima_value) use(xlink:href=`#`+ icon_value) span=\u0026#39; \u0026#39;+label else .menus_item - const labelArray = label.split(\u0026#39;||\u0026#39;) - const hideClass = labelArray[3] \u0026amp;\u0026amp; trim(labelArray[3]) === \u0026#39;hide\u0026#39; ? \u0026#39;hide\u0026#39; : \u0026#39;\u0026#39; a.site-page.group.faa-parent.animated-hover(class=`${hideClass}` href=\u0026#39;javascript:void(0);\u0026#39;) if labelArray[1] - var icon_label = trim(label.split(\u0026#39;||\u0026#39;)[1]) - var anima_label = label.split(\u0026#39;||\u0026#39;)[2] ? trim(label.split(\u0026#39;||\u0026#39;)[2]) : \u0026#39;faa-tada\u0026#39; if icon_label.substring(0,2)==\u0026#34;fa\u0026#34; i.fa-fw(class=icon_label + \u0026#39; \u0026#39; + anima_label) else if icon_label.substring(0,4)==\u0026#34;icon\u0026#34; svg.icon(aria-hidden=\u0026#34;true\u0026#34; class=anima_label) use(xlink:href=`#`+ icon_label) span=\u0026#39; \u0026#39;+ trim(labelArray[0]) i.fas.fa-chevron-down ul.menus_item_child each val,lab in value - const valArray = val.split(\u0026#39;||\u0026#39;) li a.site-page.child.faa-parent.animated-hover(href=url_for(trim(val.split(\u0026#39;||\u0026#39;)[0]))) if valArray[1] - var icon_val = trim(val.split(\u0026#39;||\u0026#39;)[1]) - var anima_val = val.split(\u0026#39;||\u0026#39;)[2] ? trim(val.split(\u0026#39;||\u0026#39;)[2]) : \u0026#39;faa-tada\u0026#39; if icon_val.substring(0,2)==\u0026#34;fa\u0026#34; i.fa-fw(class=icon_val + \u0026#39; \u0026#39; + anima_val) else if icon_val.substring(0,4)==\u0026#34;icon\u0026#34; svg.icon(aria-hidden=\u0026#34;true\u0026#34; class=anima_val) use(xlink:href=`#`+ icon_val) span=\u0026#39; \u0026#39;+ lab 以下是填写示例，在[BlogRoot]\\_config.butterfly.yml中修改。icon-xxx字样的为iconfont的symbol引入方案的id值，可以在你的iconfont图标库内查询，其中hide属性也是可以用的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 menu: 首页: / || fas fa-home || faa-tada 时间轴: /archives/ || fas fa-archive 闲言碎语: /artitalk/ || fas fa-comment-dots 音乐馆: /music/ || fas fa-music 分类: /categories/ || fas fa-folder-open # List||fas fa-list: # Music: /music/ || fas fa-music # Movie: /movies/ || fas fa-video 朋友圈: /fcircle/ || fab fa-artstation 留言板: /comments/ || fas fa-envelope 友人帐: /link/ || fas fa-link # 闲言碎语: /hpptalk/ || fas fa-comment-dots 追番: /bangumis/ || icon-bilibili1 关于: /about/ || icon-zhifeiji 要注意的是，这里的动态图标是svg.icon的标签，因此上面调节.iconfont的css并不使用，我们需要在自定义样式文件custom.css里加上一些样式来限制图标的大小和颜色等，具体大小自行调节。\n1 2 3 4 5 6 7 svg.icon { width: 1.28em; height: 1.28em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; } 重启项目即可看到效果：\n1 hexo cl; hexo s Social卡片彩色图标引入（店长） 代码原理和上面的菜单栏基本一致，所以各个前置教程都不再重复，这里只提供代码魔改内容和配置项编写方案。（记住要引入了自己的图标再来看这个教程！！！）\n重写[BlogRoot]\\themes\\butterfly\\layout\\includes\\header\\social.pug,替换为以下代码：\n1 2 3 4 5 6 7 8 9 10 each value, title in theme.social a.social-icon.faa-parent.animated-hover(href=url_for(trim(value.split(\u0026#39;||\u0026#39;)[0])) target=\u0026#34;_blank\u0026#34; title=title === undefined ? \u0026#39;\u0026#39; : trim(title)) if value.split(\u0026#39;||\u0026#39;)[1] - var icon_value = trim(value.split(\u0026#39;||\u0026#39;)[1]) - var anima_value = value.split(\u0026#39;||\u0026#39;)[2] ? trim(value.split(\u0026#39;||\u0026#39;)[2]) : \u0026#39;faa-tada\u0026#39; if icon_value.substring(0,2)==\u0026#34;fa\u0026#34; i.fa-fw(class=icon_value + \u0026#39; \u0026#39; + anima_value) else if icon_value.substring(0,4)==\u0026#34;icon\u0026#34; svg.icon(aria-hidden=\u0026#34;true\u0026#34; class=anima_value) use(xlink:href=`#`+ icon_value) 以下为对应的social配置项。写法沿用menu_item的写法示例，修改[BlogRoot]\\_config.butterfly.yml的social配置项，具体的链接改为自己的。\n1 2 3 4 5 6 social: Github: https://github.com/ktzxy || icon-gitHub || faa-tada Email: https://mail.qq.com/cgi-bin/qm_share?t=qm_mailme\u0026amp;email=222251511764@qq.com || icon-youxiang || faa-tada RSS: atom.xml || icon-rss || faa-tada BiliBili: https://space.bilibili.com/496148176 || icon-bilibili || faa-tada QQ: tencent://Message/?Uin=2251511764\u0026amp;amp;websiteName=local.edu.com:8888=\u0026amp;amp;Menu=yes || icon-QQ1 || faa-tada 要注意的是，这里的动态图标是svg.icon的标签，因此上面调节.iconfont的css并不使用，我们需要在自定义样式文件custom.css里加上一些样式来限制图标的大小和颜色等，具体大小自行调节（如果上面弄过菜单栏的图标大小，这里也就不需要再重复写了）。\n1 2 3 4 5 6 7 svg.icon { width: 1.28em; height: 1.28em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; } 进阶操作：不知道大家发现没有，这个css对菜单栏的图标和对社交图标同时生效，但是有时候我们想这两者有不一样的大小，怎么办？其实很简单，只要我们给这两部分的图标元素贴上不同的“标签”就可以，这个标签可以是id，也可以是class，但是众所周知html中的id是唯一的，我们这里有多个图标，因此贴上不通的class比较合适，因此我们改造一下[BlogRoot]\\themes\\butterfly\\layout\\includes\\header\\social.pug这个文件\n1 2 3 4 5 6 7 8 9 10 11 each value, title in theme.social a.social-icon.faa-parent.animated-hover(href=url_for(trim(value.split(\u0026#39;||\u0026#39;)[0])) target=\u0026#34;_blank\u0026#34; title=title === undefined ? \u0026#39;\u0026#39; : trim(title)) if value.split(\u0026#39;||\u0026#39;)[1] - var icon_value = trim(value.split(\u0026#39;||\u0026#39;)[1]) - var anima_value = value.split(\u0026#39;||\u0026#39;)[2] ? trim(value.split(\u0026#39;||\u0026#39;)[2]) : \u0026#39;faa-tada\u0026#39; if icon_value.substring(0,2)==\u0026#34;fa\u0026#34; i.fa-fw(class=icon_value + \u0026#39; \u0026#39; + anima_value) else if icon_value.substring(0,4)==\u0026#34;icon\u0026#34; - svg.icon(aria-hidden=\u0026#34;true\u0026#34; class=anima_value) + svg.social_icon(aria-hidden=\u0026#34;true\u0026#34; class=anima_value) use(xlink:href=`#`+ icon_value) 上面的改动会将图标渲染成class=social_icon的标签，现在我们可以区分菜单栏还是社交的图标的，如果想调节社交图标的大小就用以下的css\n1 2 3 4 5 6 7 svg.social_icon { width: 1.20em; height: 1.20em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; } 举一反三，要想专门用css改动菜单栏图标大小，只需要将[BlogRoot]\\themes\\butterfly\\layout\\includes\\header\\menu_item.pug文件中的svg.icon替换成svg.menu_icon，然后用以下的css\n1 2 3 4 5 6 7 svg.menu_icon { width: 1.28em; height: 1.28em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; } 重启项目即可看到效果：\n1 hexo cl; hexo s *侧边栏图标和文字自定义 进入[BlogRoot]\\themes\\butterfly\\layout\\includes\\widget\\card_webinfo.pug，进行以下修改，因为默认的图标是font-awesome的黑白图标，就是i.fas.fa-chart-line那一行，删除，然后引入新的图标标签，其中图标的样式、名称等参考自己的需要进行更改，样式主要是width、height、position、top这几个属性，这里的animated-hover和faa-tada是给对应的元素套上对应的class，如果装了动画依赖，扫描到这些class的元素会自动挂载动画样式，如果不想要可以去除。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 if theme.aside.card_webinfo.enable .card-widget.card-webinfo .item-headline - i.fas.fa-chart-line + a.faa-parent.animated-hover + svg.faa-tada.icon(style=\u0026#34;height:25px;width:25px;fill:currentColor;position:relative;top:5px\u0026#34; aria-hidden=\u0026#34;true\u0026#34;) + use(xlink:href=\u0026#39;#icon-shujutongji2\u0026#39;) span= _p(\u0026#39;aside.card_webinfo.headline\u0026#39;) .webinfo if theme.aside.card_webinfo.post_count .webinfo-item .item-name= _p(\u0026#39;aside.card_webinfo.article_name\u0026#39;) + \u0026#34; :\u0026#34; .item-count= site.posts.length if theme.runtimeshow.enable .webinfo-item .item-name= _p(\u0026#39;aside.card_webinfo.runtime.name\u0026#39;) + \u0026#34; :\u0026#34; .item-count#runtimeshow(data-publishDate=date_xml(theme.runtimeshow.publish_date)) i.fa-solid.fa-spinner.fa-spin if theme.wordcount.enable \u0026amp;\u0026amp; theme.wordcount.total_wordcount .webinfo-item .item-name=_p(\u0026#39;aside.card_webinfo.site_wordcount\u0026#39;) + \u0026#34; :\u0026#34; .item-count=totalcount(site) if theme.busuanzi.site_uv .webinfo-item .item-name= _p(\u0026#39;aside.card_webinfo.site_uv_name\u0026#39;) + \u0026#34; :\u0026#34; .item-count#busuanzi_value_site_uv i.fa-solid.fa-spinner.fa-spin if theme.busuanzi.site_pv .webinfo-item .item-name= _p(\u0026#39;aside.card_webinfo.site_pv_name\u0026#39;) + \u0026#34; :\u0026#34; .item-count#busuanzi_value_site_pv i.fa-solid.fa-spinner.fa-spin if theme.aside.card_webinfo.last_push_date .webinfo-item .item-name= _p(\u0026#39;aside.card_webinfo.last_push_date.name\u0026#39;) + \u0026#34; :\u0026#34; .item-count#last-push-date(data-lastPushDate=date_xml(Date.now())) i.fa-solid.fa-spinner.fa-spin 接下来就是改文了，注意到第8行的span= _p('aside.card_webinfo.headline')，这行代码就是渲染图标后面的文字，我们其实可以直接改成span= _p('小站资讯')，这样就已经按照自己的文字显示了，但是为了更好维护，我们遵循主题的设计原则，注意到变量aside.card_webinfo.headline，这其实是在写好的语言包中扫描对应的值，因为不同的语言对应不同的文字，如果我们设置了语言为zh-CN那么就到[BlogRoot]\\themes\\butterfly\\languages\\zh-CN.yml进行修改。yml文件是以缩进区分层级的，我们只需要寻找aside-\u0026gt;card_webinfo-\u0026gt;headline这一项修改为自己喜欢的内容即可\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 aside: articles: 文章 tags: 标签 categories: 分类 card_announcement: 公告栏 card_categories: 分类 card_tags: 标签 card_archives: 归档 card_recent_post: 最新文章 card_friend_link: 通讯录 card_webinfo: - headline: 网站资讯 + headline: 网站资讯 article_name: 文章数目 runtime: name: 已运行时间 unit: 天 last_push_date: name: 最后更新时间 site_wordcount: 本站总字数 site_uv_name: 本站访客数 site_pv_name: 本站总访问量 more_button: 查看更多 card_newest_comments: headline: 最新评论 loading_text: 正在加载中... error: 无法获取评论，请确认相关配置是否正确 zero: 没有评论 image: 图片 link: 链接 code: 代码 card_toc: 目录 最后重新编译运行即可看见效果。\n1 hexo cl; hexo s *渐变色版权美化（店长+微调） 修改[BlogRoot]\\themes\\butterfly\\layout\\includes\\post\\post-copyright.pug,直接复制以下内容替换原文件内容。此处多次用到了三元运算符作为默认项设置，在确保有主题配置文件的默认项的情况下，也可以在相应文章的front-matter中重新定义作者，原文链接，开源许可协议等内容。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 PLAINTEXT if theme.post_copyright.enable \u0026amp;\u0026amp; page.copyright !== false - let author = page.copyright_author ? page.copyright_author : config.author - let url = page.copyright_url ? page.copyright_url : page.permalink - let license = page.license ? page.license : theme.post_copyright.license - let license_url = page.license_url ? page.license_url : theme.post_copyright.license_url .post-copyright .post-copyright__title span.post-copyright-info h #[=page.title] .post-copyright__type span.post-copyright-info a(href=url_for(url))= theme.post_copyright.decode ? decodeURI(url) : url .post-copyright-m .post-copyright-m-info .post-copyright-a h 作者 .post-copyright-cc-info h=author .post-copyright-c h 发布于 .post-copyright-cc-info h=date(page.date, config.date_format) .post-copyright-u h 更新于 .post-copyright-cc-info h=date(page.updated, config.date_format) .post-copyright-c h 许可协议 .post-copyright-cc-info a.icon(rel=\u0026#39;noopener\u0026#39; target=\u0026#39;_blank\u0026#39; title=\u0026#39;Creative Commons\u0026#39; href=\u0026#39;https://creativecommons.org/\u0026#39;) i.fab.fa-creative-commons a(rel=\u0026#39;noopener\u0026#39; target=\u0026#39;_blank\u0026#39; title=license href=url_for(license_url))=license 修改[BlogRoot]\\themes\\butterfly\\source\\css\\_layout\\post.styl,直接复制以下内容，替换原文件，这个文件就是自己调节样式的。其中，184行是白天模式的背景色，这里默认是我网站的渐变色，大家可以根据自己的喜好调节；253行是夜间模式的发光光圈颜色，大家也可以自行替换成自己喜欢的颜色：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 STYLUS beautify() headStyle(fontsize) padding-left: unit(fontsize + 12, \u0026#39;px\u0026#39;) \u0026amp;:before margin-left: unit((-(fontsize + 6)), \u0026#39;px\u0026#39;) font-size: unit(fontsize, \u0026#39;px\u0026#39;) \u0026amp;:hover padding-left: unit(fontsize + 18, \u0026#39;px\u0026#39;) h1, h2, h3, h4, h5, h6 transition: all .2s ease-out \u0026amp;:before position: absolute top: calc(50% - 7px) color: $title-prefix-icon-color content: $title-prefix-icon line-height: 1 transition: all .2s ease-out @extend .fontawesomeIcon \u0026amp;:hover \u0026amp;:before color: $light-blue h1 headStyle(20) h2 headStyle(18) h3 headStyle(16) h4 headStyle(14) h5 headStyle(12) h6 headStyle(12) ol, ul p margin: 0 0 8px li \u0026amp;::marker color: $light-blue font-weight: 600 font-size: 1.05em \u0026amp;:hover \u0026amp;::marker color: var(--pseudo-hover) ul \u0026gt; li list-style-type: circle #article-container word-wrap: break-word overflow-wrap: break-word a color: $theme-link-color \u0026amp;:hover text-decoration: underline img display: block margin: 0 auto 20px max-width: 100% transition: filter 375ms ease-in .2s p margin: 0 0 16px iframe margin: 0 0 20px if hexo-config(\u0026#39;anchor\u0026#39;) a.headerlink \u0026amp;:after @extend .fontawesomeIcon float: right color: var(--headline-presudo) content: \u0026#39;\\f0c1\u0026#39; font-size: .95em opacity: 0 transition: all .3s \u0026amp;:hover \u0026amp;:after color: var(--pseudo-hover) h1, h2, h3, h4, h5, h6 \u0026amp;:hover a.headerlink \u0026amp;:after opacity: 1 ol, ul ol, ul padding-left: 20px li margin: 4px 0 p margin: 0 0 8px if hexo-config(\u0026#39;beautify.enable\u0026#39;) if hexo-config(\u0026#39;beautify.field\u0026#39;) == \u0026#39;site\u0026#39; beautify() else if hexo-config(\u0026#39;beautify.field\u0026#39;) == \u0026#39;post\u0026#39; \u0026amp;.post-content beautify() \u0026gt; :last-child margin-bottom: 0 !important #post .tag_share .post-meta \u0026amp;__tag-list display: inline-block \u0026amp;__tags display: inline-block margin: 8px 8px 8px 0 padding: 0 12px width: fit-content border: 1px solid $light-blue border-radius: 12px color: $light-blue font-size: .85em transition: all .2s ease-in-out \u0026amp;:hover background: $light-blue color: var(--white) .post_share display: inline-block float: right margin: 8px 0 width: fit-content .social-share font-size: .85em .social-share-icon margin: 0 4px width: w = 1.85em height: w font-size: 1.2em line-height: w .post-copyright position: relative margin: 40px 0 10px padding: 10px 16px border: 1px solid var(--light-grey) transition: box-shadow .3s ease-in-out overflow: hidden border-radius: 12px!important background: linear-gradient(45deg, #f6d8f5, #c2f1f0, #f0debf); \u0026amp;:before background var(--heo-post-blockquote-bg) position absolute right -26px top -120px content \u0026#39;\\f25e\u0026#39; font-size 200px font-family \u0026#39;Font Awesome 5 Brands\u0026#39; opacity .2 \u0026amp;:hover box-shadow: 0 0 8px 0 rgba(232, 237, 250, .6), 0 2px 4px 0 rgba(232, 237, 250, .5) .post-copyright \u0026amp;-meta color: $light-blue font-weight: bold \u0026amp;-info padding-left: 6px a text-decoration: none word-break: break-word \u0026amp;:hover text-decoration: none .post-copyright-cc-info color: $theme-color; .post-outdate-notice position: relative margin: 0 0 20px padding: .5em 1.2em border-radius: 3px background-color: $noticeOutdate-bg color: $noticeOutdate-color if hexo-config(\u0026#39;noticeOutdate.style\u0026#39;) == \u0026#39;flat\u0026#39; padding: .5em 1em .5em 2.6em border-left: 5px solid $noticeOutdate-border \u0026amp;:before @extend .fontawesomeIcon position: absolute top: 50% left: .9em color: $noticeOutdate-border content: \u0026#39;\\f071\u0026#39; transform: translateY(-50%) .ads-wrap margin: 40px 0 .post-copyright-m-info .post-copyright-a, .post-copyright-c, .post-copyright-u display inline-block width fit-content padding 2px 5px [data-theme=\u0026#34;dark\u0026#34;] #post .post-copyright background #07080a text-shadow #bfbeb8 0 0 2px border 1px solid rgb(19 18 18 / 35%) box-shadow 0 0 5px var(--theme-color) animation flashlight 1s linear infinite alternate .post-copyright-info color #e0e0e4 #post .post-copyright__title font-size 22px .post-copyright__notice font-size 15px .post-copyright box-shadow 2px 2px 5px 默认项的配置\n作者：[BlogRoot]\\_config.yml中的author配置项\n1 2 3 4 5 6 7 8 # Site title: Akilarの糖果屋 subtitle: Akilar.top description: keywords: author: Akilar #默认作者 language: zh-CN timezone: \u0026#39;\u0026#39; 许可协议：[BlogRoot]\\_config.butterfly.yml中的license和license_url配置项\n1 2 3 4 5 post_copyright: enable: true decode: true license: CC BY-NC-SA 4.0 license_url: https://creativecommons.org/licenses/by-nc-sa/4.0/ 页面覆写配置项，修改对应文章的front-matter\n1 2 3 4 5 6 7 8 9 10 MARKDOWN --- title: Copyright-beautify # 文章名称 date: 2021-03-02 13:52:46 # 文章发布日期 updated: 2021-03-02 13:52:46 # 文章更新日期 copyright_author: Nesxc # 作者覆写 copyright_url: https://www.nesxc.com/post/hexocc.html # 原文链接覆写 license: # 许可协议名称覆写 license_url: # 许可协议链接覆写 --- 顶部渐变条色加载条 新建[BlogRoot]\\source\\css\\progress_bar.css文件，写入以下内容（或者你在[BlogRoot]\\source\\css\\custom.css直接加也行，最后在配置文件记得引入即可）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 .pace { -webkit-pointer-events: none; pointer-events: none; -webkit-user-select: none; -moz-user-select: none; user-select: none; z-index: 2000; position: fixed; margin: auto; top: 4px; left: 0; right: 0; height: 8px; border-radius: 8px; width: 7rem; background: #eaecf2; border: 1px #e3e8f7; overflow: hidden } .pace-inactive .pace-progress { opacity: 0; transition: .3s ease-in } .pace .pace-progress { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; -o-box-sizing: border-box; box-sizing: border-box; -webkit-transform: translate3d(0, 0, 0); -moz-transform: translate3d(0, 0, 0); -ms-transform: translate3d(0, 0, 0); -o-transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0); max-width: 200px; position: absolute; z-index: 2000; display: block; top: 0; right: 100%; height: 100%; width: 100%; /* linear-gradient(to right, #3494e6, #ec6ead) */ background: linear-gradient(to right, #43cea2, #3866ca); animation: gradient 2s ease infinite; background-size: 200% } .pace.pace-inactive { opacity: 0; transition: .3s; top: -8px } 在主题配置文件_config.butterfly.yml的inject配置项加入刚刚的css样式和必须的js依赖：\n1 2 3 4 5 6 7 inject: head: - xxx - \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;/css/progress_bar.css\u0026#34; media=\u0026#34;defer\u0026#34; onload=\u0026#34;this.media=\u0026#39;all\u0026#39;\u0026#34;\u0026gt; bottom: - xxx - \u0026lt;script async src=\u0026#34;//npm.elemecdn.com/pace-js@1.2.4/pace.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; *Bilibili视频适配% *文章页局部 html 代码不渲染 在你的 md 文章页中，部分内容不想经过 Hexo 渲染，则包一层 raw 标签：\n1 2 3 4 {% raw %} \u0026lt;div class=\u0026#34;\u0026#34;\u0026gt;你的一些代码...\u0026lt;/div\u0026gt; \u0026lt;script\u0026gt;你的一些代码...\u0026lt;/script\u0026gt; {% endraw %} 那么标签内的代码就不会被框架渲染了~\n文章H1~H6标题小风车转动效果 修改主题配置文件_config.butterfly.yml文件的beautify配置项：\n1 2 3 4 5 6 beautify: enable: true field: post # site/post # title-prefix-icon: \u0026#39;\\f0c1\u0026#39; 原内容 title-prefix-icon: \u0026#39;\\f863\u0026#39; title-prefix-icon-color: \u0026#34;#F47466\u0026#34; 在[BlogRoot]\\source\\css\\custom.css 中加入以下代码，可以自己调节一下转速:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 /* 文章页H1-H6图标样式效果 */ /* 控制风车转动速度 4s那里可以自己调节快慢 */ h1::before, h2::before, h3::before, h4::before, h5::before, h6::before { -webkit-animation: ccc 4s linear infinite; animation: ccc 4s linear infinite; } /* 控制风车转动方向 -1turn 为逆时针转动，1turn 为顺时针转动，相同数字部分记得统一修改 */ @-webkit-keyframes ccc { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } to { -webkit-transform: rotate(-1turn); transform: rotate(-1turn); } } @keyframes ccc { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } to { -webkit-transform: rotate(-1turn); transform: rotate(-1turn); } } /* 设置风车颜色 */ #content-inner.layout h1::before { color: #ef50a8; margin-left: -1.55rem; font-size: 1.3rem; margin-top: -0.23rem; } #content-inner.layout h2::before { color: #fb7061; margin-left: -1.35rem; font-size: 1.1rem; margin-top: -0.12rem; } #content-inner.layout h3::before { color: #ffbf00; margin-left: -1.22rem; font-size: 0.95rem; margin-top: -0.09rem; } #content-inner.layout h4::before { color: #a9e000; margin-left: -1.05rem; font-size: 0.8rem; margin-top: -0.09rem; } #content-inner.layout h5::before { color: #57c850; margin-left: -0.9rem; font-size: 0.7rem; margin-top: 0rem; } #content-inner.layout h6::before { color: #5ec1e0; margin-left: -0.9rem; font-size: 0.66rem; margin-top: 0rem; } /* s设置风车hover动效 6s那里可以自己调节快慢*/ #content-inner.layout h1:hover, #content-inner.layout h2:hover, #content-inner.layout h3:hover, #content-inner.layout h4:hover, #content-inner.layout h5:hover, #content-inner.layout h6:hover { color: var(--theme-color); } #content-inner.layout h1:hover::before, #content-inner.layout h2:hover::before, #content-inner.layout h3:hover::before, #content-inner.layout h4:hover::before, #content-inner.layout h5:hover::before, #content-inner.layout h6:hover::before { color: var(--theme-color); -webkit-animation: ccc 6s linear infinite; animation: ccc 6s linear infinite; } 在主题配置文件_config.butterfly.yml的inject配置项进行引入（不再赘述）。\n挂绳小猫咪(tzy大佬) 制作一个盛放内容的盒子，在[BlogRoot]/node_modules/hexo-theme-butterfly/layout/includes/head.pug(如果是git clone 安装在[BlogRoot]/themes/butterfly/layout/includes/head.pug)最后一行加入如下代码：\n1 #myscoll 其实随便放在哪里都行，只要能加载出来就行\n在[BlogRoot]/node_modules/hexo-theme-butterfly/source/js文件夹下新建一个cat.js，将以下代码复制到文件中。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 if (document.body.clientWidth \u0026gt; 992) { function getBasicInfo() { /* 窗口高度 */ var ViewH = $(window).height(); /* document高度 */ var DocH = $(\u0026#34;body\u0026#34;)[0].scrollHeight; /* 滚动的高度 */ var ScrollTop = $(window).scrollTop(); /* 可滚动的高度 */ var S_V = DocH - ViewH; var Band_H = ScrollTop / (DocH - ViewH) * 100; return { ViewH: ViewH, DocH: DocH, ScrollTop: ScrollTop, Band_H: Band_H, S_V: S_V } }; function show(basicInfo) { if (basicInfo.ScrollTop \u0026gt; 0.001) { $(\u0026#34;.neko\u0026#34;).css(\u0026#39;display\u0026#39;, \u0026#39;block\u0026#39;); } else { $(\u0026#34;.neko\u0026#34;).css(\u0026#39;display\u0026#39;, \u0026#39;none\u0026#39;); } } (function ($) { $.fn.nekoScroll = function (option) { var defaultSetting = { top: \u0026#39;0\u0026#39;, scroWidth: 6 + \u0026#39;px\u0026#39;, z_index: 9999, zoom: 0.9, borderRadius: 5 + \u0026#39;px\u0026#39;, right: 60 + \u0026#39;px\u0026#39;, // 这里可以换为你喜欢的图片，例如我就换为了雪人，但是要抠图 nekoImg: \u0026#34;https://bu.dusays.com/2022/07/20/62d812db74be9.png\u0026#34;, hoverMsg: \u0026#34;喵喵喵~\u0026#34;, color: \u0026#34;#6f42c1\u0026#34;, during: 500, blog_body: \u0026#34;body\u0026#34;, }; var setting = $.extend(defaultSetting, option); var getThis = this.prop(\u0026#34;className\u0026#34;) !== \u0026#34;\u0026#34; ? \u0026#34;.\u0026#34; + this.prop(\u0026#34;className\u0026#34;) : this.prop(\u0026#34;id\u0026#34;) !== \u0026#34;\u0026#34; ? \u0026#34;#\u0026#34; + this.prop(\u0026#34;id\u0026#34;) : this.prop(\u0026#34;nodeName\u0026#34;); if ($(\u0026#34;.neko\u0026#34;).length == 0) { this.after(\u0026#34;\u0026lt;div class=\\\u0026#34;neko\\\u0026#34; id=\u0026#34; + setting.nekoname + \u0026#34; data-msg=\\\u0026#34;\u0026#34; + setting.hoverMsg + \u0026#34;\\\u0026#34;\u0026gt;\u0026lt;/div\u0026gt;\u0026#34;); } let basicInfo = getBasicInfo(); $(getThis) .css({ \u0026#39;position\u0026#39;: \u0026#39;fixed\u0026#39;, \u0026#39;width\u0026#39;: setting.scroWidth, \u0026#39;top\u0026#39;: setting.top, \u0026#39;height\u0026#39;: basicInfo.Band_H * setting.zoom * basicInfo.ViewH * 0.01 + \u0026#39;px\u0026#39;, \u0026#39;z-index\u0026#39;: setting.z_index, \u0026#39;background-color\u0026#39;: setting.bgcolor, \u0026#34;border-radius\u0026#34;: setting.borderRadius, \u0026#39;right\u0026#39;: setting.right, \u0026#39;background-image\u0026#39;: \u0026#39;url(\u0026#39; + setting.scImg + \u0026#39;)\u0026#39;, \u0026#39;background-image\u0026#39;: \u0026#39;-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.1) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.1) 50%, rgba(255, 255, 255, 0.1) 75%, transparent 75%, transparent)\u0026#39;, \u0026#39;border-radius\u0026#39;: \u0026#39;2em\u0026#39;, \u0026#39;background-size\u0026#39;: \u0026#39;contain\u0026#39; }); $(\u0026#34;#\u0026#34; + setting.nekoname) .css({ \u0026#39;position\u0026#39;: \u0026#39;fixed\u0026#39;, \u0026#39;top\u0026#39;: basicInfo.Band_H * setting.zoom * basicInfo.ViewH * 0.01 - 50 + \u0026#39;px\u0026#39;, \u0026#39;z-index\u0026#39;: setting.z_index * 10, \u0026#39;right\u0026#39;: setting.right, \u0026#39;background-image\u0026#39;: \u0026#39;url(\u0026#39; + setting.nekoImg + \u0026#39;)\u0026#39;, }); show(getBasicInfo()); $(window) .scroll(function () { let basicInfo = getBasicInfo(); show(basicInfo); $(getThis) .css({ \u0026#39;position\u0026#39;: \u0026#39;fixed\u0026#39;, \u0026#39;width\u0026#39;: setting.scroWidth, \u0026#39;top\u0026#39;: setting.top, \u0026#39;height\u0026#39;: basicInfo.Band_H * setting.zoom * basicInfo.ViewH * 0.01 + \u0026#39;px\u0026#39;, \u0026#39;z-index\u0026#39;: setting.z_index, \u0026#39;background-color\u0026#39;: setting.bgcolor, \u0026#34;border-radius\u0026#34;: setting.borderRadius, \u0026#39;right\u0026#39;: setting.right, \u0026#39;background-image\u0026#39;: \u0026#39;url(\u0026#39; + setting.scImg + \u0026#39;)\u0026#39;, \u0026#39;background-image\u0026#39;: \u0026#39;-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.1) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.1) 50%, rgba(255, 255, 255, 0.1) 75%, transparent 75%, transparent)\u0026#39;, \u0026#39;border-radius\u0026#39;: \u0026#39;2em\u0026#39;, \u0026#39;background-size\u0026#39;: \u0026#39;contain\u0026#39; }); $(\u0026#34;#\u0026#34; + setting.nekoname) .css({ \u0026#39;position\u0026#39;: \u0026#39;fixed\u0026#39;, \u0026#39;top\u0026#39;: basicInfo.Band_H * setting.zoom * basicInfo.ViewH * 0.01 - 50 + \u0026#39;px\u0026#39;, \u0026#39;z-index\u0026#39;: setting.z_index * 10, \u0026#39;right\u0026#39;: setting.right, \u0026#39;background-image\u0026#39;: \u0026#39;url(\u0026#39; + setting.nekoImg + \u0026#39;)\u0026#39;, }); if (basicInfo.ScrollTop == basicInfo.S_V) { $(\u0026#34;#\u0026#34; + setting.nekoname) .addClass(\u0026#34;showMsg\u0026#34;) } else { $(\u0026#34;#\u0026#34; + setting.nekoname) .removeClass(\u0026#34;showMsg\u0026#34;); $(\u0026#34;#\u0026#34; + setting.nekoname) .attr(\u0026#34;data-msg\u0026#34;, setting.hoverMsg); } }); this.click(function (e) { btf.scrollToDest(0, 500) }); $(\u0026#34;#\u0026#34; + setting.nekoname) .click(function () { btf.scrollToDest(0, 500) }); return this; } })(jQuery); $(document).ready(function () { //部分自定义 $(\u0026#34;#myscoll\u0026#34;).nekoScroll({ bgcolor: \u0026#39;rgb(0 0 0 / .5)\u0026#39;, //背景颜色，没有绳子背景图片时有效 borderRadius: \u0026#39;2em\u0026#39;, zoom: 0.9 } ); //自定义（去掉以下注释，并注释掉其他的查看效果） /* $(\u0026#34;#myscoll\u0026#34;).nekoScroll({ nekoname:\u0026#39;neko1\u0026#39;, //nekoname，相当于id nekoImg:\u0026#39;img/猫咪.png\u0026#39;, //neko的背景图片 scImg:\u0026#34;img/绳1.png\u0026#34;, //绳子的背景图片 bgcolor:\u0026#39;#1e90ff\u0026#39;, //背景颜色，没有绳子背景图片时有效 zoom:0.9, //绳子长度的缩放值 hoverMsg:\u0026#39;你好~喵\u0026#39;, //鼠标浮动到neko上方的对话框信息 right:\u0026#39;100px\u0026#39;, //距离页面右边的距离 fontFamily:\u0026#39;楷体\u0026#39;, //对话框字体 fontSize:\u0026#39;14px\u0026#39;, //对话框字体的大小 color:\u0026#39;#1e90ff\u0026#39;, //对话框字体颜色 scroWidth:\u0026#39;8px\u0026#39;, //绳子的宽度 z_index:100, //不用解释了吧 during:1200, //从顶部到底部滑动的时长 }); */ }) } 在[BlogRoot]/node_modules/hexo-theme-butterfly/source/css文件夹下新建一个cat.css，将以下代码复制到文件中。当然你也可以选择不新建 css 文件，复制代码到custom.css也行，总之有地方引入就行。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 body::-webkit-scrollbar { width: 0; } .neko { width: 64px; height: 64px; background-image: url(\u0026#34;https://bu.dusays.com/2022/07/20/62d812db74be9.png\u0026#34;); position: absolute; right: 32px; background-repeat: no-repeat; background-size: contain; transform: translateX(50%); cursor: pointer; font-family: tzy; font-weight: 600; font-size: 16px; color: #6f42c1; display: none; } .neko::after { display: none; width: 100px; height: 100px; background-image: url(\u0026#34;https://bu.dusays.com/2022/07/20/62d812d95e6f5.png\u0026#34;); background-size: contain; z-index: 9999; position: absolute; right: 50%; text-align: center; line-height: 100px; top: -115%; } .neko.showMsg::after { content: attr(data-msg); display: block; overflow: hidden; text-overflow: ellipsis; } .neko:hover::after { content: attr(data-msg); display: block; overflow: hidden; text-overflow: ellipsis; } .neko.fontColor::after { color: #333; } /** * @description: 滚动条样式 跟猫二选一 */ @media screen and (max-width:992px) { ::-webkit-scrollbar { width: 8px !important; height: 8px !important } ::-webkit-scrollbar-track { border-radius: 2em; } ::-webkit-scrollbar-thumb { background-color: rgb(255 255 255 / .3); background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.1) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.1) 50%, rgba(255, 255, 255, 0.1) 75%, transparent 75%, transparent); border-radius: 2em } ::-webkit-scrollbar-corner { background-color: transparent } } 在主题配置文件_config.butterfly.yml中引入cat.js和cat.css，当然还有在bottom的最前面引入jQuery，因为cat.js的语法依赖jQuery。\n1 2 3 4 5 6 inject: head: - \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; href=\u0026#34;/css/cat.css\u0026#34;\u0026gt; bottom: - \u0026lt;script defer src=\u0026#34;https://npm.elemecdn.com/jquery@latest/dist/jquery.min.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; - \u0026lt;script defer data-pjax src=\u0026#34;/js/cat.js\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 最后重新编译运行即可看见效果。\n1 hexo cl; hexo s 打赏按钮投币彩蛋效果 收款码样式 可上传到草料网进行样式修改\n修改[Blogroot]\\themes\\butterfly\\languages\\zh-CN.yml\n1 2 3 4 5 6 7 8 9 10 date_suffix: just: 刚刚 min: 分钟前 hour: 小时前 day: 天前 month: 个月前 - donate: 打赏 + donate: 不给糖果就捣蛋 share: 分享 修改[Blogroot]\\themes\\butterfly\\layout\\includes\\post\\reward.pug,整体替换为以下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 link(rel=\u0026#39;stylesheet\u0026#39; href=url_for(theme.CDN.option.coin_css) media=\u0026#34;defer\u0026#34; onload=\u0026#34;this.media=\u0026#39;all\u0026#39;\u0026#34;) .post-reward button.tip-button.reward-button span.tip-button__text= _p(\u0026#39;donate\u0026#39;) .coin-wrapper .coin .coin__middle .coin__back .coin__front .reward-main ul.reward-all each item in theme.reward.QR_code - var clickTo = (item.itemlist||item).link ? (item.itemlist||item).link : (item.itemlist||item).img li.reward-item a(href=clickTo target=\u0026#39;_blank\u0026#39;) img.post-qr-code-img(src=url_for((item.itemlist||item).img) alt=(item.itemlist||item).text) .post-qr-code-desc=(item.itemlist||item).text if theme.reward.coinAudio - var coinAudio = theme.reward.coinAudio ? url_for(theme.reward.coinAudio) : \u0026#39;https://cdn.cbd.int/akilar-candyassets@1.0.36/audio/coin.mp3\u0026#39; audio#coinAudio(src=coinAudio) script(defer src=url_for(theme.CDN.option.coin_js)) 新建[Blogroot]source\\css\\coin\\coin.css\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 .tip-button { border: 0; border-radius: 0.25rem; cursor: pointer; font-size: 20px; font-weight: 600; height: 2.6rem; margin-bottom: -4rem; outline: 0; position: relative; top: 0; transform-origin: 0% 100%; transition: transform 50ms ease-in-out; width: auto; -webkit-tap-highlight-color: transparent; } .tip-button:active { transform: rotate(4deg); } .tip-button.clicked { animation: 150ms ease-in-out 1 shake; pointer-events: none; } .tip-button.clicked .tip-button__text { opacity: 0; transition: opacity 100ms linear 200ms; } .tip-button.clicked::before { height: 0.5rem; width: 60%; background: $button-hover-color; } .tip-button.clicked .coin { transition: margin-bottom 1s linear 200ms; margin-bottom: 0; } .tip-button.shrink-landing::before { transition: width 200ms ease-in; width: 0; } .tip-button.coin-landed::after { opacity: 1; transform: scale(1); transform-origin: 50% 100%; } .tip-button.coin-landed .coin-wrapper { background: radial-gradient(circle at 35% 97%, rgba(3, 16, 50, 0.4) 0.04rem, transparent 0.04rem), radial-gradient( circle at 45% 92%, rgba(3, 16, 50, 0.4) 0.04rem, transparent 0.02rem ), radial-gradient(circle at 55% 98%, rgba(3, 16, 50, 0.4) 0.04rem, transparent 0.04rem), radial-gradient(circle at 65% 96%, rgba(3, 16, 50, 0.4) 0.06rem, transparent 0.06rem); background-position: center bottom; background-size: 100%; bottom: -1rem; opacity: 0; transform: scale(2) translateY(-10px); } .tip-button__text { color: #fff; margin-right: 1.8rem; opacity: 1; position: relative; transition: opacity 100ms linear 500ms; z-index: 3; } .tip-button::before { border-radius: 0.25rem; bottom: 0; content: \u0026#34;\u0026#34;; display: block; height: 100%; left: 50%; position: absolute; transform: translateX(-50%); transition: height 250ms ease-in-out 400ms, width 250ms ease-in-out 300ms; width: 100%; z-index: 2; } .tip-button::after { bottom: -1rem; color: white; content: \u0026#34;ヾ(≧O≦)〃嗷~\u0026#34;; /*点击后显示的内容*/ height: 110%; left: 0; opacity: 0; position: absolute; pointer-events: none; text-align: center; transform: scale(0); transform-origin: 50% 20%; transition: transform 200ms cubic-bezier(0, 0, 0.35, 1.43); width: 100%; z-index: 1; } .coin-wrapper { background: none; bottom: 0; height: 18rem; left: 0; opacity: 1; overflow: hidden; pointer-events: none; position: absolute; transform: none; transform-origin: 50% 100%; transition: opacity 200ms linear 100ms, transform 300ms ease-out; width: 100%; } .coin { --front-y-multiplier: 0; --back-y-multiplier: 0; --coin-y-multiplier: 0; --coin-x-multiplier: 0; --coin-scale-multiplier: 0; --coin-rotation-multiplier: 0; --shine-opacity-multiplier: 0.4; --shine-bg-multiplier: 50%; bottom: calc(var(--coin-y-multiplier) * 1rem - 3.5rem); height: 3.5rem; margin-bottom: 3.05rem; position: absolute; right: calc(var(--coin-x-multiplier) * 34% + 16%); transform: translateX(50%) scale(calc(0.4 + var(--coin-scale-multiplier))) rotate(calc(var( --coin-rotation-multiplier ) * -1deg)); transition: opacity 100ms linear 200ms; width: 3.5rem; z-index: 3; } .coin__front, .coin__middle, .coin__back, .coin::before, .coin__front::after, .coin__back::after { border-radius: 50%; box-sizing: border-box; height: 100%; left: 0; position: absolute; width: 100%; z-index: 3; } .coin__front { background: radial-gradient(circle at 50% 50%, transparent 50%, rgba(115, 124, 153, 0.4) 54%, #c2cadf 54%), linear-gradient(210deg, #8590b3 32%, transparent 32%), linear-gradient(150deg, #8590b3 32%, transparent 32%), linear-gradient(to right, #8590b3 22%, transparent 22%, transparent 78%, #8590b3 78%), linear-gradient( to bottom, #fcfaf9 44%, transparent 44%, transparent 65%, #fcfaf9 65%, #fcfaf9 71%, #8590b3 71% ), linear-gradient(to right, transparent 28%, #fcfaf9 28%, #fcfaf9 34%, #8590b3 34%, #8590b3 40%, #fcfaf9 40%, #fcfaf9 47%, #8590b3 47%, #8590b3 53%, #fcfaf9 53%, #fcfaf9 60%, #8590b3 60%, #8590b3 66%, #fcfaf9 66%, #fcfaf9 72%, transparent 72%); background-color: #8590b3; background-size: 100% 100%; transform: translateY(calc(var(--front-y-multiplier) * 0.3181818182rem / 2)) scaleY(var(--front-scale-multiplier)); } .coin__front::after { background: rgba(0, 0, 0, 0.2); content: \u0026#34;\u0026#34;; opacity: var(--front-y-multiplier); } .coin__middle { background: #737c99; transform: translateY(calc(var(--middle-y-multiplier) * 0.3181818182rem / 2)) scaleY(var(--middle-scale-multiplier)); } .coin__back { background: radial-gradient(circle at 50% 50%, transparent 50%, rgba(115, 124, 153, 0.4) 54%, #c2cadf 54%), radial-gradient(circle at 50% 40%, #fcfaf9 23%, transparent 23%), radial-gradient(circle at 50% 100%, #fcfaf9 35%, transparent 35%); background-color: #8590b3; background-size: 100% 100%; transform: translateY(calc(var(--back-y-multiplier) * 0.3181818182rem / 2)) scaleY(var(--back-scale-multiplier)); } .coin__back::after { background: rgba(0, 0, 0, 0.2); content: \u0026#34;\u0026#34;; opacity: var(--back-y-multiplier); } .coin::before { background: radial-gradient(circle at 25% 65%, transparent 50%, rgba(255, 255, 255, 0.9) 90%), linear-gradient(55deg, transparent calc(var(--shine-bg-multiplier) + 0%), #e9f4ff calc(var(--shine-bg-multiplier) + 0%), transparent calc(var( --shine-bg-multiplier ) + 50%)); content: \u0026#34;\u0026#34;; opacity: var(--shine-opacity-multiplier); transform: translateY(calc(var(--middle-y-multiplier) * 0.3181818182rem / -2)) scaleY(var(--middle-scale-multiplier)) rotate(calc(var(--coin-rotation-multiplier) * 1deg)); z-index: 10; } .coin::after { background: #737c99; content: \u0026#34;\u0026#34;; height: 0.3181818182rem; left: 0; position: absolute; top: 50%; transform: translateY(-50%); width: 100%; z-index: 2; } @keyframes shake { 0% { transform: rotate(4deg); } 66% { transform: rotate(-4deg); } 100% { transform: rotate(); } } 新建[Blogroot]\\source\\js\\coin\\coin.js\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 var tipButtons = document.querySelectorAll(\u0026#34;.tip-button\u0026#34;); function coinAudio() { var coinAudio = document.getElementById(\u0026#34;coinAudio\u0026#34;); if (coinAudio) { coinAudio.play(); //有音频时播放 } } // Loop through all buttons (allows for multiple buttons on page) tipButtons.forEach(button =\u0026gt; { var coin = button.querySelector(\u0026#34;.coin\u0026#34;); // The larger the number, the slower the animation coin.maxMoveLoopCount = 90; button.addEventListener(\u0026#34;click\u0026#34;, () =\u0026gt; { if (/Android|webOS|BlackBerry/i.test(navigator.userAgent)) return true; //媒体选择 if (button.clicked) return; button.classList.add(\u0026#34;clicked\u0026#34;); // Wait to start flipping th coin because of the button tilt animation setTimeout(() =\u0026gt; { // Randomize the flipping speeds just for fun coin.sideRotationCount = Math.floor(Math.random() * 5) * 90; coin.maxFlipAngle = (Math.floor(Math.random() * 4) + 3) * Math.PI; button.clicked = true; flipCoin(); coinAudio(); }, 50); }); var flipCoin = () =\u0026gt; { coin.moveLoopCount = 0; flipCoinLoop(); }; var resetCoin = () =\u0026gt; { coin.style.setProperty(\u0026#34;--coin-x-multiplier\u0026#34;, 0); coin.style.setProperty(\u0026#34;--coin-scale-multiplier\u0026#34;, 0); coin.style.setProperty(\u0026#34;--coin-rotation-multiplier\u0026#34;, 0); coin.style.setProperty(\u0026#34;--shine-opacity-multiplier\u0026#34;, 0.4); coin.style.setProperty(\u0026#34;--shine-bg-multiplier\u0026#34;, \u0026#34;50%\u0026#34;); coin.style.setProperty(\u0026#34;opacity\u0026#34;, 1); // Delay to give the reset animation some time before you can click again setTimeout(() =\u0026gt; { button.clicked = false; }, 300); }; var flipCoinLoop = () =\u0026gt; { coin.moveLoopCount++; var percentageCompleted = coin.moveLoopCount / coin.maxMoveLoopCount; coin.angle = -coin.maxFlipAngle * Math.pow(percentageCompleted - 1, 2) + coin.maxFlipAngle; // Calculate the scale and position of the coin moving through the air coin.style.setProperty(\u0026#34;--coin-y-multiplier\u0026#34;, -11 * Math.pow(percentageCompleted * 2 - 1, 4) + 11); coin.style.setProperty(\u0026#34;--coin-x-multiplier\u0026#34;, percentageCompleted); coin.style.setProperty(\u0026#34;--coin-scale-multiplier\u0026#34;, percentageCompleted * 0.6); coin.style.setProperty(\u0026#34;--coin-rotation-multiplier\u0026#34;, percentageCompleted * coin.sideRotationCount); // Calculate the scale and position values for the different coin faces // The math uses sin/cos wave functions to similate the circular motion of 3D spin coin.style.setProperty(\u0026#34;--front-scale-multiplier\u0026#34;, Math.max(Math.cos(coin.angle), 0)); coin.style.setProperty(\u0026#34;--front-y-multiplier\u0026#34;, Math.sin(coin.angle)); coin.style.setProperty(\u0026#34;--middle-scale-multiplier\u0026#34;, Math.abs(Math.cos(coin.angle), 0)); coin.style.setProperty(\u0026#34;--middle-y-multiplier\u0026#34;, Math.cos((coin.angle + Math.PI / 2) % Math.PI)); coin.style.setProperty(\u0026#34;--back-scale-multiplier\u0026#34;, Math.max(Math.cos(coin.angle - Math.PI), 0)); coin.style.setProperty(\u0026#34;--back-y-multiplier\u0026#34;, Math.sin(coin.angle - Math.PI)); coin.style.setProperty(\u0026#34;--shine-opacity-multiplier\u0026#34;, 4 * Math.sin((coin.angle + Math.PI / 2) % Math.PI) - 3.2); coin.style.setProperty(\u0026#34;--shine-bg-multiplier\u0026#34;, -40 * (Math.cos((coin.angle + Math.PI / 2) % Math.PI) - 0.5) + \u0026#34;%\u0026#34;); // Repeat animation loop if (coin.moveLoopCount \u0026lt; coin.maxMoveLoopCount) { if (coin.moveLoopCount === coin.maxMoveLoopCount - 6) button.classList.add(\u0026#34;shrink-landing\u0026#34;); window.requestAnimationFrame(flipCoinLoop); } else { button.classList.add(\u0026#34;coin-landed\u0026#34;); coin.style.setProperty(\u0026#34;opacity\u0026#34;, 0); setTimeout(() =\u0026gt; { button.classList.remove(\u0026#34;clicked\u0026#34;, \u0026#34;shrink-landing\u0026#34;, \u0026#34;coin-landed\u0026#34;); setTimeout(() =\u0026gt; { resetCoin(); }, 300); }, 1500); } }; }); 修改_config.butterfly.yml,添加音频文件配置项，添加 CDN 配置项：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # Sponsor/reward reward: enable: true + coinAudio: https://cdn.cbd.int/akilar-candyassets@1.0.36/audio/aowu.m4a QR_code: - img: https://npm.elemecdn.com/anzhiyu-blog@1.1.6/img/post/common/qrcode-weichat.png link: text: wechat - img: https://npm.elemecdn.com/anzhiyu-blog@1.1.6/img/post/common/qrcode-alipay.png link: text: alipay CDN: # main main_css: /css/index.css jquery: https://cdn.cbd.int/jquery@latest/dist/jquery.min.js main: /js/main.js utils:/js/utils.js option: + # 打赏按钮投币效果 + coin_js: /js/coin/coin.js + coin_css: /css/coin/coin.css 现在的打赏按钮样式需要稍作适配，当前若提示语太长，悬停时会无法显示完全。需要微调一下,修改[Blogroot]\\themes\\butterfly\\source\\css_layout\\reward.styl，整体替换为以下内容\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 .post-reward position: relative margin-top: 4rem width: 100% text-align: center .reward-button display: inline-block padding: .2rem 1.2rem background: var(--btn-bg) color: var(--btn-color) cursor: pointer transition: all .4s \u0026amp;:hover box-shadow: inset 15em 0 0 0 var(--btn-hover-color) .reward-main display: block .reward-main position: absolute bottom: 40px left: -25% z-index: 100 display: none padding: 0 0 15px width: 150% .reward-all display: inline-block margin: 0 padding: 1rem .5rem border-radius: 4px background: var(--reward-pop) \u0026amp;:before position: absolute bottom: -10px left: 0 width: 100% height: 20px content: \u0026#39;\u0026#39; \u0026amp;:after position: absolute right: 0 bottom: 2px left: 0 margin: 0 auto width: 0 height: 0 border-top: 13px solid var(--reward-pop) border-right: 13px solid transparent border-left: 13px solid transparent content: \u0026#39;\u0026#39; .reward-item display: inline-block padding: 0 8px list-style-type: none vertical-align: top img width: 130px height: 130px .post-qr-code-desc padding-top: .4rem width: 130px color: $reward-pop-up-color 404 页面展示最近文章 替换themes\\butterfly\\layout\\includes\\404.pug为以下代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 - var top_img = theme.error_404.background || theme.default_top_img - var bg_img = `background-image: url(\u0026#39;${url_for(top_img)}\u0026#39;)` #body-wrap.error div(style=\u0026#39;display: none\u0026#39;) include ./header/index.pug #error-wrap .error-content .error-img(style=bg_img) .error-info h1.error_title= \u0026#39;404\u0026#39; .error_subtitle= theme.error_404.subtitle a.button--animated(href=config.root) i.fas.fa-rocket = _p(\u0026#39;返回主页\u0026#39;) .aside-list .aside-list-group - let postLimit = theme.aside.card_recent_post.limit === 0 ? site.posts.length : theme.aside.card_recent_post.limit || 5 - let sort = theme.aside.card_recent_post.sort === \u0026#39;updated\u0026#39; ? \u0026#39;updated\u0026#39; : \u0026#39;date\u0026#39; - site.posts.sort(sort, -1).limit(postLimit).each(function(article){ - let link = article.link || article.path - let title = article.title || _p(\u0026#39;no_title\u0026#39;) - let no_cover = article.cover === false || !theme.cover.aside_enable ? \u0026#39;no-cover\u0026#39; : \u0026#39;\u0026#39; - let post_cover = article.cover .aside-list-item(class=no_cover) if post_cover \u0026amp;\u0026amp; theme.cover.aside_enable a.thumbnail(href=url_for(link) title=title) img(src=url_for(post_cover) onerror=`this.onerror=null;this.src=\u0026#39;${url_for(theme.error_img.post_page)}\u0026#39;` alt=title) .content a.title(href=url_for(link) title=title)= title if theme.aside.card_recent_post.sort === \u0026#39;updated\u0026#39; time(datetime=date_xml(article.updated) title=_p(\u0026#39;post.updated\u0026#39;) + \u0026#39; \u0026#39; + full_date(article.updated)) #[=date(article.updated, config.date_format)] else time(datetime=date_xml(article.date) title=_p(\u0026#39;post.created\u0026#39;) + \u0026#39; \u0026#39; + full_date(article.date)) #[=date(article.date, config.date_format)] - }) 标签云增加文章数上下标 搜索 cloudTags 函数，可以在 \\themes\\butterfly\\scripts\\helpers\\page.js 找到绘制标签云的代码，增加 ${tag.length} 或 ${tag.length} 可绘制表示标签文章数的上下标。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 hexo.extend.helper.register(\u0026#39;cloudTags\u0026#39;, function (options = {}) { const theme = hexo.theme.config const env = this let source = options.source const minfontsize = options.minfontsize const maxfontsize = options.maxfontsize const limit = options.limit const unit = options.unit || \u0026#39;px\u0026#39; let result = \u0026#39;\u0026#39; if (limit \u0026gt; 0) { source = source.limit(limit) } const sizes = [] source.sort(\u0026#39;length\u0026#39;).forEach(tag =\u0026gt; { const { length } = tag if (sizes.includes(length)) return sizes.push(length) }) const length = sizes.length - 1 source.forEach(tag =\u0026gt; { const ratio = length ? sizes.indexOf(tag.length) / length : 0 const size = minfontsize + ((maxfontsize - minfontsize) * ratio) let style = `font-size: ${parseFloat(size.toFixed(2))}${unit};` const color = \u0026#39;rgb(\u0026#39; + Math.floor(Math.random() * 201) + \u0026#39;, \u0026#39; + Math.floor(Math.random() * 201) + \u0026#39;, \u0026#39; + Math.floor(Math.random() * 201) + \u0026#39;)\u0026#39; // 0,0,0 -\u0026gt; 200,200,200 style += ` color: ${color}` - result += `\u0026lt;a href=\u0026#34;${env.url_for(tag.path)}\u0026#34; style=\u0026#34;${style}\u0026#34;\u0026gt;${tag.name}\u0026lt;/a\u0026gt;` + result += `\u0026lt;a href=\u0026#34;${env.url_for(tag.path)}\u0026#34; style=\u0026#34;${style}\u0026#34;\u0026gt;${tag.name}\u0026lt;sup\u0026gt;${tag.length}\u0026lt;/sup\u0026gt;\u0026lt;/a\u0026gt;` }) return result }) *自定义右键菜单 %kill Hexo + Butterfly 自定义右键菜单 | 唐志远の博客 (fe32.top)\nbutterfly博客自定义右键菜单升级版 | Ariasakaの小窝 (yisous.xyz)\n文章统计图 Hexo 博客文章统计图 | Eurkon\n*即刻短文 Hexo的Butterfly魔改教程：即刻短文静态部署版 | 张洪Heo (zhheo.com)\n*侧边栏标签修改 打开themes/butterfly/scripts/helpers/page.js修改第 52 行左右, 其中 表示上标， 表示下标。开启了排序。\n1 2 3 4 5 6 7 8 9 10 11 12 13 const length = sizes.length - 1 - source.forEach(tag =\u0026gt; { + source.sort(\u0026#39;name\u0026#39;).forEach(tag =\u0026gt; { const ratio = length ? sizes.indexOf(tag.length) / length : 0 const size = minfontsize + ((maxfontsize - minfontsize) * ratio) let style = `font-size: ${parseFloat(size.toFixed(2))}${unit};` const color = \u0026#39;rgb(\u0026#39; + Math.floor(Math.random() * 201) + \u0026#39;, \u0026#39; + Math.floor(Math.random() * 201) + \u0026#39;, \u0026#39; + Math.floor(Math.random() * 201) + \u0026#39;)\u0026#39; // 0,0,0 -\u0026gt; 200,200,200 style += ` color: ${color}` - result += `\u0026lt;a href=\u0026#34;${env.url_for(tag.path)}\u0026#34; style=\u0026#34;${style}\u0026#34;\u0026gt;${tag.name}\u0026lt;/a\u0026gt;` + result += `\u0026lt;a href=\u0026#34;${env.url_for(tag.path)}\u0026#34; style=\u0026#34;${style}\u0026#34;\u0026gt;${tag.name}\u0026lt;sup\u0026gt;${tag.length}\u0026lt;/sup\u0026gt;\u0026lt;/a\u0026gt;` }) return result }) 加入以下css\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /* tags样式 */ #aside-content .card-tag-cloud a { color: var(--anzhiyu-fontcolor) !important; font-size: 1.05rem !important; border-radius: 8px; display: inline-block; margin-right: 4px; } #aside-content .card-tag-cloud a:hover { background: var(--anzhiyu-theme); color: var(--anzhiyu-white) !important; box-shadow: var(--anzhiyu-shadow-theme); } @media screen and (min-width: 1300px) { #aside-content .card-tag-cloud a:hover { transform: scale(1.03); } #aside-content .card-tag-cloud a:active { transform: scale(0.97); } } #aside-content .card-tag-cloud a sup { opacity: 0.4; margin-left: 2px; } *侧边栏归档修改 打开themes/butterfly/scripts/helpers/aside_archives.js修改第 92 行左右, 其中 表示上标， 表示下标。开启了排序。\n1 2 3 4 5 6 7 8 9 result += transform ? transform(item.name) : item.name result += \u0026#39;\u0026lt;/span\u0026gt;\u0026#39; if (showCount) { - result += `\u0026lt;span class=\u0026#34;card-archive-list-count\u0026#34;\u0026gt;${item.count}\u0026lt;/span\u0026gt;` + result += `\u0026lt;div class=\u0026#34;card-archive-list-count-group\u0026#34;\u0026gt;\u0026lt;span class=\u0026#34;card-archive-list-count\u0026#34;\u0026gt;${item.count}\u0026lt;/span\u0026gt;\u0026lt;span\u0026gt;篇\u0026lt;/span\u0026gt;\u0026lt;/div\u0026gt;` } result += \u0026#39;\u0026lt;/a\u0026gt;\u0026#39; result += \u0026#39;\u0026lt;/li\u0026gt;\u0026#39; 加入以下 css\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 /* 归档样式 */ span.card-archive-list-count { width: auto; text-align: left; font-size: 1.5rem; line-height: 0.9; font-weight: 700; } .card-archive-list-count-group { display: flex; flex-direction: row; align-items: baseline; } #aside-content .card-archives ul.card-archive-list \u0026gt; .card-archive-list-item a span:last-child, #aside-content .card-categories ul.card-category-list \u0026gt; .card-category-list-item a span:last-child { width: fit-content; margin-left: 4px; } span.card-archive-list-count { width: auto; text-align: left; font-size: 1.1rem; line-height: 0.9; font-weight: 700; } .card-archive-list-date { font-size: 14px; opacity: 0.6; } li.card-archive-list-item { width: 100%; flex: 0 0 48%; } #aside-content .card-archives ul.card-archive-list \u0026gt; .card-archive-list-item a:hover, #aside-content .card-categories ul.card-category-list \u0026gt; .card-category-list-item a:hover { color: var(--anzhiyu-white); background-color: var(--anzhiyu-theme); box-shadow: var(--anzhiyu-shadow-theme); border-radius: 8px; padding-left: 0.5rem; padding-right: 0.5rem; } @media screen and (min-width: 1300px) { #aside-content .card-archives ul.card-archive-list \u0026gt; .card-archive-list-item a:hover, #aside-content .card-categories ul.card-category-list \u0026gt; .card-category-list-item a:hover { transform: scale(1.03); } #aside-content .card-archives ul.card-archive-list \u0026gt; .card-archive-list-item a:active, #aside-content .card-categories ul.card-category-list \u0026gt; .card-category-list-item a:active { transform: scale(0.97); } } #aside-content .card-archives ul.card-archive-list \u0026gt; .card-archive-list-item a, #aside-content .card-categories ul.card-category-list \u0026gt; .card-category-list-item a { border-radius: 8px; margin: 4px 0; display: flex; flex-direction: column; align-content: space-between; border: var(--style-border); } #aside-content .card-archives ul.card-archive-list \u0026gt; .card-archive-list-item a span:first-child, #aside-content .card-categories ul.card-category-list \u0026gt; .card-category-list-item a span:first-child { width: auto; flex: inherit; } #aside-content .card-archives ul.card-archive-list, #aside-content .card-categories ul.card-category-list { display: flex; flex-direction: row; justify-content: space-between; flex-wrap: wrap; } *侧边栏最近文章修改 去除首页最近文章显示，改为文章页显示，修改themes/butterfly/layout/includes/widget/index.pug\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 else //- page !=partial(\u0026#39;includes/widget/card_author\u0026#39;, {}, {cache: true}) !=partial(\u0026#39;includes/widget/card_announcement\u0026#39;, {}, {cache: true}) !=partial(\u0026#39;includes/widget/card_top_self\u0026#39;, {}, {cache: true}) .sticky_layout if showToc include ./card_post_toc.pug - !=partial(\u0026#39;includes/widget/card_recent_post\u0026#39;, {}, {cache: true}) !=partial(\u0026#39;includes/widget/card_ad\u0026#39;, {}, {cache: true}) !=partial(\u0026#39;includes/widget/card_newest_comment\u0026#39;, {}, {cache: true}) !=partial(\u0026#39;includes/widget/card_categories\u0026#39;, {}, {cache: true}) !=partial(\u0026#39;includes/widget/card_tags\u0026#39;, {}, {cache: true}) 加入以下 css\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 #aside-content .aside-list \u0026gt; .aside-list-item .content \u0026gt; time { display: none; } #aside-content .aside-list \u0026gt; .aside-list-item .content \u0026gt; .title { -webkit-line-clamp: 3; font-weight: 700; padding: 2px 0; } #aside-content .aside-list \u0026gt; .aside-list-item { padding: 8px; padding-top: 6px !important; padding-bottom: 6px !important; border-radius: 12px; transition: 0.3s; margin: 4px 0; cursor: pointer; } @media screen and (min-width: 1300px) { #aside-content .aside-list \u0026gt; .aside-list-item:hover { transform: scale(1.03); } #aside-content .aside-list \u0026gt; .aside-list-item:active { transform: scale(0.97); } } #aside-content .aside-list \u0026gt; .aside-list-item:hover .thumbnail \u0026gt; img { transform: scale(1); } #aside-content .aside-list \u0026gt; .aside-list-item:not(:last-child) { border-bottom: 0 dashed var(--anzhiyu-background) !important; } #aside-content .aside-list \u0026gt; .aside-list-item .thumbnail { border-radius: 8px; border: var(--style-border); } #aside-content .aside-list \u0026gt; .aside-list-item:hover { background: var(--anzhiyu-blue-main); color: var(--anzhiyu-white); transition: 0.3s; box-shadow: var(--anzhiyu-shadow-main); } #aside-content .aside-list \u0026gt; .aside-list-item:hover a { color: var(--anzhiyu-white) !important; } .card-widget.card-recent-post { padding: 0.4rem 0.6rem !important; } 页面配置 关于页面配置 | 安知鱼主题官方文档 (anheyu.com)\n音乐馆 给你的博客加一个优雅的音乐界面 | 安知鱼 (anheyu.com)\n自动部署 使用Github Action实现全自动部署 | Akilarの糖果屋\n1 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 1 2 3 git add . git commit -m \u0026#34;github action update\u0026#34; git push origin main butterfly 重装日记 | 安知鱼 (anheyu.com)\nhexo图床处理 Typora到CSDN博客图片上传问题-CSDN博客\n1 https://fastly.jsdelivr.net/gh/shaunzhao-yu/img@main github faster github faster | Akilarの糖果屋\n评论 基于 Hexo 键入评论功能 | 唐志远の博客 (fe32.top)\n悬挂灯笼 博客魔改教程总结(一) | Fomalhaut🥝\n在线聊天 基于 Hexo 键入在线聊天功能 | 唐志远の博客 (fe32.top)\n节日弹窗和公祭日 博客魔改教程总结(一) | Fomalhaut🥝\n文章加密插件 博客魔改教程总结(一) | Fomalhaut🥝\nhexo 教程 教程：\nHexo 官方文档：https://hexo.io/zh-cn/docs/ Hexo 官方提供的详细文档，包含了安装、配置和使用 Hexo 的指南。\n博客库：\nHexo 官方主题库：https://hexo.io/themes/ Hexo 官方提供的主题库，包含了各种风格的主题，可以根据个人喜好选择。\nHexo 主题 - GitHub：https://github.com/hexojs/hexo/wiki/Themes 在 GitHub 上的 Hexo Wiki 页面，收集了各种开源的 Hexo 主题，你可以在这里找到适合自己的主题。\n插件和扩展：\nHexo 官方插件列表：https://hexo.io/plugins/ Hexo 官方提供的插件列表，包含了各种功能和工具，如SEO优化、站点统计、评论系统等。\nHexo 非官方插件列表 - Awesome Hexo：https://github.com/hexojs/awesome-hexo 一个收集了大量 Hexo 非官方插件的 GitHub 项目，其中包括了许多有用的插件和扩展。\nHexo 部署扩展 - GitHub Pages：https://hexo.io/docs/github-pages 如果您计划将博客托管在 GitHub Pages 上，这个官方文档将指导您如何进行部署。\n社区和交流：\nHexo 官方论坛：https://github.com/hexojs/hexo/issues Hexo 官方论坛是一个开放的 GitHub 问题页面，您可以在这里提问、报告问题或参与讨论。\nHexo 中文社区 - SegmentFault：https://segmentfault.com/t/hexo SegmentFault 是一个中文开发者社区，在这里您可以找到关于 Hexo 的问题和讨论。\nHexo 官方博客：http s://hexo.io/blog/ Hexo 官方博客上发布了一些有关 Hexo 的最新动态、教程和技巧\n参考 评论 前端 - 基于 Hexo 键入评论功能 - 个人文章 - SegmentFault 思否\n添加 utterances评论系统\nHexo next主题博客搭建及美化 - 个人文章 - SegmentFault 思否\n部署 前端 - 飞只因太美，给你的首页装上吧！ - 个人文章 - SegmentFault 思否\nObisidian结合 Hexo + Obsidian + Git 完美的博客部署与编辑方案 - 个人文章 - SegmentFault 思否\n语雀结合 Hexo+Travis+yuque-hexo+Serverless自动化部署博客问题解决方案 - 个人文章 - SegmentFault 思否\n图床 七牛云\n1 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 git javascript - hexo配合github action 自动构建（多种形式） - 前端与算法 - SegmentFault 思否\n参考 服务器部署 安装依赖 1 yum install -y gcc gcc-c++ make zlib zlib-devel libtool openssl openssl-devel 安装PCRE库 1 2 3 4 5 6 7 8 9 10 11 12 13 cd /usr/local/ wget http://downloads.sourceforge.net/project/pcre/pcre/8.37/pcre-8.37.tar.gz tar -xvf pcre-8.37.tar.gz cd pcre-8.37 ./configure make \u0026amp;\u0026amp; make install pcre-config --version 安装nginx 安装nginx一定要在local文件夹下\n1 2 3 4 5 6 7 8 9 10 11 cd /usr/local/ wget http://nginx.org/download/nginx-1.17.9.tar.gz tar -xvf nginx-1.17.9.tar.gz cd nginx-1.17.9 ./configure make \u0026amp;\u0026amp; make install 常用命令 1 2 3 4 5 # 启动 /usr/local/nginx/sbin/nginx # 重启 /usr/local/nginx/sbin/nginx -s reload 修改配置文件server 80 端口下的root项 为/home/www/website;\n1 vi /usr/local/nginx/conf/nginx.conf 然后重新加载\n安装Git以及Node.js 1 2 3 4 curl -sL https://rpm.nodesource.com/setup_10.x | bash - yum install -y nodejs # 如果安装失败看错误集锦 查看是否成功 1 2 3 node -v npm -v 安装Git及配置仓库 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 yum install git -y adduser git chmod 740 /etc/sudoers # 设置权限 vi /etc/sudoers 使用 set: nu 显示行号，找到100行左右，添加如下信息 在如下位置添加 root ALL=(ALL) ALL git ALL=(ALL) ALL\t#加入这一行 vi指令执行之后按i进入输入模式 编辑完成之后按一下esc 然后输入:wq即可退出 执行以下指令更改文件夹权限 1 2 3 4 chmod 400 /etc/sudoers sudo passwd git 密码：123456 切换git用户并且建立密钥 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 su git cd ~ mkdir .ssh cd .ssh vi authorized_keys #内容为自己本地 C:\\Users\\Administrator\\.ssh\\id_rsa.pub文件里的内容 chmod 600 ~/.ssh/authorized_keys chmod 700 ~/.ssh 创建git仓库 1 2 3 4 5 cd ~ git init --bare blog.git vi ~/blog.git/hooks/post-receive 1 git --work-tree=/home/www/website --git-dir=/home/git/blog.git checkout -f 保存退出\n1 chmod +x ~/blog.git/hooks/post-receive *以上指令都需要在su git 之后执行 如果中途断开重新连接过，需要重新执行 su git指令 进入git账户。\n新建/home/www/website文件夹 在root用户下执行，所限先su root切换为root账户\n1 2 3 4 5 6 7 8 9 10 11 12 13 su root 输入密码 cd /home mkdir -p www/website 修改文件夹权限 这步很重要 chmod 777 /home/www/website chmod 777 /home/www 上传到服务器 在本地电脑输入\n1 ssh -v git@服务器的公网ip ==修改本地配置文件==\n注意：删除本地博客中.deploy_git文件，第一次部署忽略\n我们需要在config.yml中的最后一行编辑以下信息，然后咱们就可以把自己的博客推送上去了\n1 2 3 4 deploy: - type: git repository: git@这里改为服务器公网IP:/home/git/blog.git branch: master 然后就可以通过以下命令进行推送了\n1 hexo g -d 写入启动脚本 在/etc/init.d/路径下添加脚本文件，名称为nginx，内容如下\n1 vi /etc/init.d/nginx 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #!/bin/bash #Startup script for the nginx Web Server #chkconfig: 2345 85 15 nginx=/usr/local/nginx/sbin/nginx conf=/usr/local/nginx/conf/nginx.conf case $1 in start) echo -n \u0026#34;Starting Nginx\u0026#34; $nginx -c $conf echo \u0026#34; done.\u0026#34; ;; stop) echo -n \u0026#34;Stopping Nginx\u0026#34; killall -9 nginx echo \u0026#34; done.\u0026#34; ;; test) $nginx -t -c $conf echo \u0026#34;Success.\u0026#34; ;; reload) echo -n \u0026#34;Reloading Nginx\u0026#34; ps auxww | grep nginx | grep master | awk \u0026#39;{print $2}\u0026#39; | xargs kill -HUP echo \u0026#34; done.\u0026#34; ;; restart) $nginx -s reload echo \u0026#34;reload done.\u0026#34; ;; *) echo \u0026#34;Usage: $0 {start|restart|reload|stop|test|show}\u0026#34; ;; esac 然后执行\n1 chmod +x nginx 1 2 3 4 控制指令 启动service nginx start 停止service nginx stop 重启service nginx reload 以下非必要不设置\n开放80端口，如果https需要开放443端口\n1 2 firewall-cmd --permanent --zone=public --add-port=80/tcp firewall-cmd --reload 错误集锦 -bash: npm: command not found\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #从 https://nodejs.org/en/ 下载 tar 文件并解压缩。 curl https://nodejs.org/dist/v17.6.0/node-v17.6.0-linux-x64.tar.xz -o node-v17.6.0-linux-x64.tar.xz tar -xvf node-v17.6.0-linux-x64.tar.xz #在 /usr/local/ 下创建一个目录。 sudo mkdir -p /usr/local/nodejs #在 /usr/local/nodejs/ 下移动节点文件。 sudo mv node-v17.6.0-linux-x64/* /usr/local/nodejs/ #将 /usr/local/nodejs/bin 目录添加到 .bashrc 文件中的 PATH。 sudo vim ~/.bashrc export PATH=$PATH:/usr/local/nodejs/bin #重新加载 .bashrc 文件。 source ~/.bashrc #安装 npm。 curl -L https://npmjs.org/install.sh | sudo sh node --version npm --version $ ssh -v git@121.196.144.77:/home/git/blog.git OpenSSH_9.2p1, OpenSSL 1.1.1t 7 Feb 2023 debug1: Reading configuration data /etc/ssh/ssh_config debug1: resolve_canonicalize: hostname 121.196.144.77:/home/git/blog.git is an unrecognised address ssh: Could not resolve hostname 121.196.144.77:/home/git/blog.git: Name or service not known\n1 2 3 4 1.先到本地C:\\Users\\Administrator\\.ssh\\文件下，删除known_hosts文件 2.执行ssh -v git@121.196.144.77 提示Welcome to Alibaba Cloud Elastic Compute Service !就算ok了 ✘ nginx Error 0.6s ✘ lsky-pro Error 0.6s ✘ mysql Error 0.6s Error response from daemon: Get \u0026ldquo;https://registry-1.docker.io/v2/\u0026quot;: dial tcp: lookup registry-1.docker.io on 192.168.101.2:53: server misbehaving\n1 2 3 4 5 vi /etc/resolv.conf #添加这两行 nameserver 8.8.8.8 nameserver 8.8.4.4 参考 图床部署 弃用 简介 Lsky Pro 是一个用于在线上传、管理图片的图床程序，中文名：兰空图床，你可以将它作为自己的云上相册，亦可以当作你的写作贴图库。\n兰空图床始于 2017 年 10 月，最早的版本由 ThinkPHP 5 开发，后又经历了数个版本的迭代，在 2021 年末启动了新的重写计划并于 2022 年 3 月份发布全新的 2.0 版本。\n项目地址：https://github.com/lsky-org/lsky-pro\n官网地址：https://www.lsky.pro/\n文档地址：https://docs.lsky.pro/\nDocker镜像地址：https://hub.docker.com/r/dko0/lsky-pro\nPicGo插件：https://hellodk.cn/post/964\n功能特性\n支持本地等多种第三方云储存 AWS S3、阿里云 OSS、腾讯云 COS、七牛云、又拍云、SFTP、FTP、WebDav、Minio 多种数据库驱动支持，MySQL 5.7+、PostgreSQL 9.6+、SQLite 3.8.8+、SQL Server 2017+ 支持配置使用多种缓存驱动，Memcached、Redis、DynamoDB、等其他关系型数据库，默认以文件的方式缓存 多图上传、拖拽上传、粘贴上传、动态设置策略上传、复制、一键复制链接 强大的图片管理功能，瀑布流展示，支持鼠标右键、单选多选、重命名等操作 自由度极高的角色组配置，可以为每个组配置多个储存策略，同时储存策略可以配置多个角色组 可针对角色组设置上传文件、文件夹路径命名规则、上传频率限制、图片审核等功能 支持图片水印、文字水印、水印平铺、设置水印位置、X/y 轴偏移量设置、旋转角度等 支持通过接口上传、管理图片、管理相册 支持在线增量更新、跨版本更新 图片广场 环境搭建 安装常用工具包 1 2 3 sudo -i # 切换到root用户 yum update -y yum install -y wget curl sudo vim git lsof 设置 SWAP 脚本（可选） 1 wget -O box.sh https://raw.githubusercontent.com/BlueSkyXN/SKY-BOX/main/box.sh \u0026amp;\u0026amp; chmod +x box.sh \u0026amp;\u0026amp; clear \u0026amp;\u0026amp; ./box.sh 注意：VPS的内存如果过小，建议设置一下SWAP，一般为内存的1-1.5倍即可，可以让运行更流畅！\n安装 Docker 环境\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 1.卸载旧的版本 $sudo yum remove docker \\ docker-client \\ docker-client-latest \\ docker-common \\ docker-latest \\ docker-latest-logrotate \\ docker-logrotate \\ docker-engine 2.需要的安装包 $sudo yum install -y yum-utils 3.设置镜像的仓库（官方） $sudo yum-config-manager \\ --add-repo \\ https://download.docker.com/linux/centos/docker-ce.repo （官网速度慢） $sudo yum-config-manager \\ --add-repo \\ http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo（推荐使用，阿里云速度快） 4.更新yum软件包索引 $sudo yum makecache fast 5.安装docker docker-ce 社区版 docker-ee 企业版 $sudo yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 6.启动docker $sudo systemctl start docker 7.查看docker是否安装成功 $sudo docker version 卸载\n1 2 3 4 5 1.卸载依赖 $sudo yum remove docker-ce docker-ce-cli containerd.io 2.删除资源 $sudo rm -rf /var/lib/docker # /var/lib/docker docker的默认工作路径 阿里云镜像加速 1 2 3 4 5 6 7 8 9 10 配置镜像加速器 通过shell脚本追加文本内容 $sudo mkdir -p /etc/docker $sudo tee /etc/docker/daemon.json \u0026lt;\u0026lt;-\u0026#39;EOF\u0026#39; { \u0026#34;registry-mirrors\u0026#34;: [\u0026#34;https://f2k3b83v.mirror.aliyuncs.com\u0026#34;] } EOF $sudo systemctl daemon-reload $sudo systemctl restart docker 设置开机自动启动 1 #systemctl enable docker 查看docker服务状态 1 #systemctl status docker 查看docker具体信息 1 #docker info docker-compose 这里采用离线安装，在线安装或多或少有各种问题\n所有版本预览Releases · docker/compose (github.com)\n1 2 3 4 5 6 7 8 9 10 #最好是进入到/usr/local/bin/目录下安装，我第一次在根目录下安装，报错docker-compose: command not found，第二次在上述目录下安装可以成功 #将文件上传到linux后，重命名 mv docker-compose-linux-x86_64.docker-compose-linux-x86_64 /usr/local/bin/docker-compose #添加可执行权限 chmod +x /usr/local/bin/docker-compose #测试 docker-compose version 1 2 安装docker-compose $ curl -SL https://github.com/docker/compose/releases/download/v2.4.1/docker-compose-linux-x86_64 -o /usr/local/bin/docker-compose 创建容器 在系统任意位置创建一个文件夹，此文档以 /opt/docker/lsky 为例 1 2 mkdir -p /opt/docker/lsky \u0026amp;\u0026amp; cd /opt/docker/lsky mkdir -p ./{conf,data,logs} 注意：后续操作中，产生的所有数据都会保存在这个目录，请妥善保存/opt/docker/lsky\n创建 docker-compose.yaml\n1 vim docker-compose.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 version: \u0026#39;3\u0026#39; services: lsky-pro: container_name: lsky-pro image: dko0/lsky-pro restart: always volumes: - ./data/html:/var/www/html #映射到本地 ports: - 7791:80 environment: - MYSQL_HOST=mysql - MYSQL_DATABASE=lsky-pro - MYSQL_USER=lsky-pro - MYSQL_PASSWORD=lsky-pro mysql: image: mysql:8.0 container_name: lsky-pro-db restart: always environment: - MYSQL_DATABASE=lsky-pro - MYSQL_USER=lsky-pro - MYSQL_PASSWORD=lsky-pro - MYSQL_ROOT_PASSWORD=lsky-pro volumes: - ./data/db:/var/lib/mysql 注意：查看端口是否被占用\nlsof -i:7791\n启动服务 1 docker-compose up -d 实时查看日志：\n1 docker-compose logs -f 用浏览器访问 http://ip:端口号 即可http://192.168.101.128:7791 查看服务器 IP： 1 curl ip.sb 如果需要配置域名访问，建议先配置好反向代理以及域名解析再进行初始化。如果通过 http://ip:端口号 的形式无法访问，请到服务器厂商后台将运行的端口号添加到安全组，如果服务器使用了 Linux 面板，请检查此 Linux 面板是否有还有安全组配置，需要同样将端口号添加到安全组。\n更新容器 停止运行中的容器组\n1 cd /opt/docker/lsky \u0026amp;\u0026amp; docker-compose down 备份数据（重要） 1 2 cp -r /opt/docker/lsky /opt/docker/lsky.archive 需要注意的是，lsky.archive 文件名不一定要根据此文档命名，这里仅仅是个示例。 更新服务 修改 docker-compose.yaml 中配置的镜像版本\n拉取镜像 1 docker-compose pull lsky-pro 重新启动容器 1 docker-compose up -d 初始化安装 访问网页地址，进入安装界面，按照提示进行安装即可\n注意，数据库连接地址，填 docker-compose 文件里的容器名称 lsky-pro-db\n[[博客问题记录]]\n","permalink":"https://ktzxy.top/posts/l1om86bxy2/","summary":"\u003ch1 id=\"hexo-博客完整部署以及魔改记录\"\u003ehexo 博客完整部署以及魔改记录\u003c/h1\u003e\n\u003ch1 id=\"前期基础部署\"\u003e前期基础部署\u003c/h1\u003e\n\u003ch2 id=\"1下载安装git\"\u003e1.下载安装git\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://git-scm.com/downloads\"\u003eGit - Downloads (git-scm.com)\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e安装完成可在鼠标右键看到Git Bash\u003c/p\u003e\n\u003cp\u003e常用命令\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e3\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egit config -l  //查看所有配置\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egit config --system --list //查看系统配置\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egit config --global --list //查看用户（全局）配置\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e配置用户名和邮箱\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e2\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egit config --global user.name \u0026#34;你的用户名\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003egit config --global user.email \u0026#34;你的邮箱\u0026#34;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003ch2 id=\"2下载安装nodejs\"\u003e2.下载安装node.js\u003c/h2\u003e\n\u003cp\u003e\u003ca href=\"https://nodejs.org/zh-cn\"\u003eNode.js中文官网 (nodejs.org)\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e推荐使用nvm安装，后续如果涉及node版本更换，更为方便\u003c/p\u003e\n\u003cp\u003envm安装\u003c/p\u003e\n\u003cp\u003e进入官网\u003ca href=\"https://link.zhihu.com/?target=http%3A//nvm.uihtm.com/\"\u003ehttp://nvm.uihtm.com/\u003c/a\u003e 下载\u003c/p\u003e\n\u003cp\u003e解压安装，一直下一步\u003c/p\u003e\n\u003cp\u003e基础命令\u003c/p\u003e\n\u003cp\u003enodejs历史版本下载页面\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://www.fomal.cc/posts/e593433d.html\"\u003ehttps://www.fomal.cc/posts/e593433d.html\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cdiv style=\"color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\n\u003ctable style=\"border-spacing:0;padding:0;margin:0;border:0;\"\u003e\u003ctr\u003e\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e 1\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e 2\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e 3\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e 4\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e 5\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e 6\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e 7\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e 8\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e 9\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e10\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e11\n\u003c/span\u003e\u003cspan style=\"white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#737679\"\u003e12\n\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\n\u003ctd style=\"vertical-align:top;padding:0;margin:0;border:0;;width:100%\"\u003e\n\u003cpre tabindex=\"0\" style=\"color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-fallback\" data-lang=\"fallback\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e查询版本号\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003envm -v\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e查询可以下载的node版本\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003envm list available\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e安装指定版本\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003envm install xxx\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e查看已经安装的node版本\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003envm list\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e切换node版本(如果失败那就用管理员身份打开cmd进行切换)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003envm use xxx\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003enodejs这里安装的是跟视频博主一样的版本，12.19.0\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/td\u003e\u003c/tr\u003e\u003c/table\u003e\n\u003c/div\u003e\n\u003c/div\u003e\u003cp\u003e修改npm源\u003c/p\u003e","title":"hexo博客部署"},{"content":"﻿## 01、基础入门-SpringBoot2课程介绍\nSpring Boot 2核心技术\nSpring Boot 2响应式编程\n学习要求 -熟悉Spring基础 -熟悉Maven使用 环境要求 Java8及以上 Maven 3.3及以上 学习资料 Spring Boot官网 Spring Boot官方文档 本课程文档地址 源码地址 02、基础入门-Spring生态圈 Spring官网\nSpring能做什么 Spring的能力 Spring的生态 覆盖了：\nweb开发 数据访问 安全控制 分布式 消息服务 移动开发 批处理 \u0026hellip;\u0026hellip; Spring5重大升级 响应式编程 内部源码设计 基于Java8的一些新特性，如：接口默认实现。重新设计源码架构。\n为什么用SpringBoot Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can \u0026ldquo;just run\u0026rdquo;.link\n能快速创建出生产级别的Spring应用。\nSpringBoot优点 Create stand-alone Spring applications\n创建独立Spring应用 Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)\n内嵌web服务器 Provide opinionated \u0026lsquo;starter\u0026rsquo; dependencies to simplify your build configuration\n自动starter依赖，简化构建配置 Automatically configure Spring and 3rd party libraries whenever possible\n自动配置Spring以及第三方功能 Provide production-ready features such as metrics, health checks, and externalized configuration\n提供生产级别的监控、健康检查及外部化配置 Absolutely no code generation and no requirement for XML configuration\n无代码生成、无需编写XML SpringBoot是整合Spring技术栈的一站式框架\nSpringBoot是简化Spring技术栈的快速开发脚手架\nSpringBoot缺点 人称版本帝，迭代快，需要时刻关注变化 封装太深，内部原理复杂，不容易精通 03、基础入门-SpringBoot的大时代背景 微服务 JamesLewis and Martin Fowler(2014) 提出微服务完整概念。https://martinfowler.com/articles/microservices.html\nIn short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.——James Lewis and Martin Fowler (2014)\n微服务是一种架构风格 一个应用拆分为一组小型服务 每个服务运行在自己的进程内，也就是可独立部署和升级 服务之间使用轻量级HTTP交互 服务围绕业务功能拆分 可以由全自动部署机制独立部署 去中心化，服务自治。服务可以使用不同的语言、不同的存储技术 分布式 分布式的困难 远程调用 服务发现 负载均衡 服务容错 配置管理 服务监控 链路追踪 日志管理 任务调度 \u0026hellip;\u0026hellip; 分布式的解决 SpringBoot + SpringCloud 云原生 原生应用如何上云。 Cloud Native\n上云的困难 服务自愈 弹性伸缩 服务隔离 自动化部署 灰度发布 流量治理 \u0026hellip;\u0026hellip; 04、基础入门-SpringBoot官方文档架构 Spring Boot官网 Spring Boot官方文档 官网文档架构 查看版本新特性\n05、基础入门-SpringBoot-HelloWorld 系统要求 Java 8 Maven 3.3+ IntelliJ IDEA 2019.1.2 Maven配置文件 新添内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 \u0026lt;mirrors\u0026gt; \u0026lt;mirror\u0026gt; \u0026lt;id\u0026gt;nexus-aliyun\u0026lt;/id\u0026gt; \u0026lt;mirrorOf\u0026gt;central\u0026lt;/mirrorOf\u0026gt; \u0026lt;name\u0026gt;Nexus aliyun\u0026lt;/name\u0026gt; \u0026lt;url\u0026gt;http://maven.aliyun.com/nexus/content/groups/public\u0026lt;/url\u0026gt; \u0026lt;/mirror\u0026gt; \u0026lt;/mirrors\u0026gt; \u0026lt;profiles\u0026gt; \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;jdk-1.8\u0026lt;/id\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;activeByDefault\u0026gt;true\u0026lt;/activeByDefault\u0026gt; \u0026lt;jdk\u0026gt;1.8\u0026lt;/jdk\u0026gt; \u0026lt;/activation\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;maven.compiler.compilerVersion\u0026gt;1.8\u0026lt;/maven.compiler.compilerVersion\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;/profile\u0026gt; \u0026lt;/profiles\u0026gt; HelloWorld项目 需求：浏览发送/hello请求，响应 “Hello，Spring Boot 2”\n创建maven工程 引入依赖 1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.4.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 创建主程序 1 2 3 4 5 6 7 8 9 10 import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } } 编写业务 1 2 3 4 5 6 7 8 9 10 import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @RequestMapping(\u0026#34;/hello\u0026#34;) public String handle01(){ return \u0026#34;Hello, Spring Boot 2!\u0026#34;; } } 运行\u0026amp;测试 运行MainApplication类 浏览器输入http://localhost:8888/hello，将会输出Hello, Spring Boot 2!。 设置配置 maven工程的resource文件夹中创建application.properties文件。\n1 2 # 设置端口号 server.port=8888 更多配置信息\n打包部署 在pom.xml添加\n1 2 3 4 5 6 7 8 \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; 在IDEA的Maven插件上点击运行 clean 、package，把helloworld工程项目的打包成jar包，\n打包好的jar包被生成在helloworld工程项目的target文件夹内。\n用cmd运行java -jar boot-01-helloworld-1.0-SNAPSHOT.jar，既可以运行helloworld工程项目。\n将jar包直接在目标服务器执行即可。\n06、基础入门-SpringBoot-依赖管理特性 父项目做依赖管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 依赖管理 \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.4.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; 上面项目的父项目如下： \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.4.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/parent\u0026gt; 它几乎声明了所有开发中常用的依赖的版本号，自动版本仲裁机制 开发导入starter场景启动器 见到很多 spring-boot-starter-* ： *就某种场景 只要引入starter，这个场景的所有常规需要的依赖我们都自动引入 更多SpringBoot所有支持的场景 见到的 *-spring-boot-starter： 第三方为我们提供的简化开发的场景启动器。 1 2 3 4 5 6 7 所有场景启动器最底层的依赖 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.4.RELEASE\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;compile\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 无需关注版本号，自动版本仲裁\n引入依赖默认都可以不写版本 引入非版本仲裁的jar，要写版本号。 可以修改默认版本号\n查看spring-boot-dependencies里面规定当前依赖的版本 用的 key。 在当前项目里面重写配置，如下面的代码。 1 2 3 \u0026lt;properties\u0026gt; \u0026lt;mysql.version\u0026gt;5.1.43\u0026lt;/mysql.version\u0026gt; \u0026lt;/properties\u0026gt; IDEA快捷键：\nctrl + shift + alt + U：以图的方式显示项目中依赖之间的关系。 alt + ins：相当于Eclipse的 Ctrl + N，创建新类，新包等。 07、基础入门-SpringBoot-自动配置特性 自动配好Tomcat 引入Tomcat依赖。 配置Tomcat 1 2 3 4 5 6 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-tomcat\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.4.RELEASE\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;compile\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 自动配好SpringMVC\n引入SpringMVC全套组件 自动配好SpringMVC常用组件（功能） 自动配好Web常见功能，如：字符编码问题\nSpringBoot帮我们配置好了所有web开发的常见场景 1 2 3 4 5 6 7 8 9 10 public static void main(String[] args) { //1、返回我们IOC容器 ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); //2、查看容器里面的组件 String[] names = run.getBeanDefinitionNames(); for (String name : names) { System.out.println(name); } } 默认的包结构 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来 无需以前的包扫描配置 想要改变扫描路径 @SpringBootApplication(scanBasePackages=\u0026ldquo;com.hbnu\u0026rdquo;) @ComponentScan 指定扫描路径 1 2 3 4 5 @SpringBootApplication 等同于 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(\u0026#34;com.hbnu\u0026#34;) 各种配置拥有默认值\n默认配置最终都是映射到某个类上，如：MultipartProperties 配置文件的值最终会绑定每个类上，这个类会在容器中创建对象 按需加载所有自动配置项\n非常多的starter 引入了哪些场景这个场景的自动配置才会开启 SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面 \u0026hellip;\u0026hellip;\n08、底层注解-@Configuration详解 基本使用 Full模式与Lite模式 示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /** * 1、配置类里面使用@Bean标注在方法上给容器注册组件，默认也是单实例的 * 2、配置类本身也是组件 * 3、proxyBeanMethods：代理bean的方法 * Full(proxyBeanMethods = true)（保证每个@Bean方法被调用多少次返回的组件都是单实例的）（默认） * Lite(proxyBeanMethods = false)（每个@Bean方法被调用多少次返回的组件都是新创建的） */ @Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件 public class MyConfig { /** * Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册容器中的单实例对象 * @return */ @Bean //给容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值，就是组件在容器中的实例 public User user01(){ User zhangsan = new User(\u0026#34;zhangsan\u0026#34;, 18); //user组件依赖了Pet组件 zhangsan.setPet(tomcatPet()); return zhangsan; } @Bean(\u0026#34;tom\u0026#34;) public Pet tomcatPet(){ return new Pet(\u0026#34;tomcat\u0026#34;); } } @Configuration测试代码如下:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(\u0026#34;com.atguigu.boot\u0026#34;) public class MainApplication { public static void main(String[] args) { //1、返回我们IOC容器 ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); //2、查看容器里面的组件 String[] names = run.getBeanDefinitionNames(); for (String name : names) { System.out.println(name); } //3、从容器中获取组件 Pet tom01 = run.getBean(\u0026#34;tom\u0026#34;, Pet.class); Pet tom02 = run.getBean(\u0026#34;tom\u0026#34;, Pet.class); System.out.println(\u0026#34;组件：\u0026#34;+(tom01 == tom02)); //4、com.atguigu.boot.config.MyConfig$$EnhancerBySpringCGLIB$$51f1e1ca@1654a892 MyConfig bean = run.getBean(MyConfig.class); System.out.println(bean); //如果@Configuration(proxyBeanMethods = true)代理对象调用方法。SpringBoot总会检查这个组件是否在容器中有。 //保持组件单实例 User user = bean.user01(); User user1 = bean.user01(); System.out.println(user == user1); User user01 = run.getBean(\u0026#34;user01\u0026#34;, User.class); Pet tom = run.getBean(\u0026#34;tom\u0026#34;, Pet.class); System.out.println(\u0026#34;用户的宠物：\u0026#34;+(user01.getPet() == tom)); } } 最佳实战 配置 类组件之间无依赖关系用Lite模式加速容器启动过程，减少判断 配置 类组件之间有依赖关系，方法会被调用得到之前单实例组件，用Full模式（默认） lite 英 [laɪt] 美 [laɪt]\nadj. 低热量的，清淡的(light的一种拼写方法);类似…的劣质品\nIDEA快捷键：\nAlt + Ins:生成getter，setter、构造器等代码。 Ctrl + Alt + B:查看类的具体实现代码。 09、底层注解-@Import导入组件 @Bean、@Component、@Controller、@Service、@Repository，它们是Spring的基本标签，在Spring Boot中并未改变它们原来的功能。\n@ComponentScan 在07、基础入门-SpringBoot-自动配置特性有用例。\n@Import({User.class, DBHelper.class})给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名\n1 2 3 4 @Import({User.class, DBHelper.class}) @Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件 public class MyConfig { } 测试类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 //1、返回我们IOC容器 ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); //... //5、获取组件 String[] beanNamesForType = run.getBeanNamesForType(User.class); for (String s : beanNamesForType) { System.out.println(s); } DBHelper bean1 = run.getBean(DBHelper.class); System.out.println(bean1); 10、底层注解-@Conditional条件装配 条件装配：满足Conditional指定的条件，则进行组件注入\n用@ConditionalOnMissingBean举例说明\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(name = \u0026#34;tom\u0026#34;)//没有tom名字的Bean时，MyConfig类的Bean才能生效。 public class MyConfig { @Bean public User user01(){ User zhangsan = new User(\u0026#34;zhangsan\u0026#34;, 18); zhangsan.setPet(tomcatPet()); return zhangsan; } @Bean(\u0026#34;tom22\u0026#34;) public Pet tomcatPet(){ return new Pet(\u0026#34;tomcat\u0026#34;); } } public static void main(String[] args) { //1、返回我们IOC容器 ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); //2、查看容器里面的组件 String[] names = run.getBeanDefinitionNames(); for (String name : names) { System.out.println(name); } boolean tom = run.containsBean(\u0026#34;tom\u0026#34;); System.out.println(\u0026#34;容器中Tom组件：\u0026#34;+tom);//false boolean user01 = run.containsBean(\u0026#34;user01\u0026#34;); System.out.println(\u0026#34;容器中user01组件：\u0026#34;+user01);//true boolean tom22 = run.containsBean(\u0026#34;tom22\u0026#34;); System.out.println(\u0026#34;容器中tom22组件：\u0026#34;+tom22);//true } 11、底层注解-@ImportResource导入Spring配置文件 比如，公司使用bean.xml文件生成配置bean，然而你为了省事，想继续复用bean.xml，@ImportResource粉墨登场。\nbean.xml：\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans ...\u0026#34;\u0026gt; \u0026lt;bean id=\u0026#34;haha\u0026#34; class=\u0026#34;com.lun.boot.bean.User\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;name\u0026#34; value=\u0026#34;zhangsan\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;age\u0026#34; value=\u0026#34;18\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;bean id=\u0026#34;hehe\u0026#34; class=\u0026#34;com.lun.boot.bean.Pet\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;name\u0026#34; value=\u0026#34;tomcat\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/beans\u0026gt; 使用方法：\n1 2 3 4 @ImportResource(\u0026#34;classpath:beans.xml\u0026#34;) public class MyConfig { ... } 测试类：\n1 2 3 4 5 6 7 8 9 public static void main(String[] args) { //1、返回我们IOC容器 ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); boolean haha = run.containsBean(\u0026#34;haha\u0026#34;); boolean hehe = run.containsBean(\u0026#34;hehe\u0026#34;); System.out.println(\u0026#34;haha：\u0026#34;+haha);//true System.out.println(\u0026#34;hehe：\u0026#34;+hehe);//true } 12、底层注解-@ConfigurationProperties配置绑定 如何使用Java读取到properties文件中的内容，并且把它封装到JavaBean中，以供随时使用\n传统方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 public class getProperties { public static void main(String[] args) throws FileNotFoundException, IOException { Properties pps = new Properties(); pps.load(new FileInputStream(\u0026#34;a.properties\u0026#34;)); Enumeration enum1 = pps.propertyNames();//得到配置文件的名字 while(enum1.hasMoreElements()) { String strKey = (String) enum1.nextElement(); String strValue = pps.getProperty(strKey); System.out.println(strKey + \u0026#34;=\u0026#34; + strValue); //封装到JavaBean。 } } } Spring Boot一种配置配置绑定：\n@ConfigurationProperties + @Component\n假设有配置文件application.properties\n1 2 mycar.brand=BYD mycar.price=100000 只有在容器中的组件，才会拥有SpringBoot提供的强大功能\n1 2 3 4 5 @Component @ConfigurationProperties(prefix = \u0026#34;mycar\u0026#34;) public class Car { ... } Spring Boot另一种配置配置绑定：\n@EnableConfigurationProperties + @ConfigurationProperties\n开启Car配置绑定功能 把这个Car这个组件自动注册到容器中 1 2 3 4 @EnableConfigurationProperties(Car.class) public class MyConfig { ... } 1 2 3 4 @ConfigurationProperties(prefix = \u0026#34;mycar\u0026#34;) public class Car { ... } 13、自动配置【源码分析】-自动包规则原理 Spring Boot应用的启动类：\n1 2 3 4 5 6 7 8 @SpringBootApplication public class MainApplication { public static void main(String[] args) { SpringApplication.run(MainApplication.class, args); } } 分析下@SpringBootApplication\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class} ), @Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class} )} ) public @interface SpringBootApplication { ... } 重点分析@SpringBootConfiguration，@EnableAutoConfiguration，@ComponentScan。\n@SpringBootConfiguration 1 2 3 4 5 6 7 8 9 10 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration public @interface SpringBootConfiguration { @AliasFor( annotation = Configuration.class ) boolean proxyBeanMethods() default true; } @Configuration代表当前是一个配置类。\n@ComponentScan 指定扫描哪些Spring注解。\n@ComponentScan 在07、基础入门-SpringBoot-自动配置特性有用例。\n@EnableAutoConfiguration 1 2 3 4 5 6 7 8 9 10 11 12 13 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = \u0026#34;spring.boot.enableautoconfiguration\u0026#34;; Class\u0026lt;?\u0026gt;[] exclude() default {}; String[] excludeName() default {}; } 重点分析@AutoConfigurationPackage，@Import(AutoConfigurationImportSelector.class)。\n@AutoConfigurationPackage 标签名直译为：自动配置包，指定了默认的包规则。\n1 2 3 4 5 6 7 8 9 10 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import(AutoConfigurationPackages.Registrar.class)//给容器中导入一个组件 public @interface AutoConfigurationPackage { String[] basePackages() default {}; Class\u0026lt;?\u0026gt;[] basePackageClasses() default {}; } 利用Registrar给容器中导入一系列组件 将指定的一个包下的所有组件导入进MainApplication所在包下。 14、自动配置【源码分析】-初始加载自动配置类 @Import(AutoConfigurationImportSelector.class) 利用getAutoConfigurationEntry(annotationMetadata);给容器中批量导入一些组件 调用List\u0026lt;String\u0026gt; configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类 利用工厂加载 Map\u0026lt;String, List\u0026lt;String\u0026gt;\u0026gt; loadSpringFactories(@Nullable ClassLoader classLoader);得到所有的组件 从META-INF/spring.factories位置来加载一个文件。 默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件 spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories 1 2 3 4 5 6 7 # 文件里面写死了spring-boot一启动就要给容器中加载的所有配置类 # spring-boot-autoconfigure-2.3.4.RELEASE.jar/META-INF/spring.factories # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\\ ... 虽然我们127个场景的所有自动配置启动的时候默认全部加载，但是xxxxAutoConfiguration按照条件装配规则（@Conditional），最终会按需配置。\n如AopAutoConfiguration类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration( proxyBeanMethods = false ) @ConditionalOnProperty( prefix = \u0026#34;spring.aop\u0026#34;, name = \u0026#34;auto\u0026#34;, havingValue = \u0026#34;true\u0026#34;, matchIfMissing = true ) public class AopAutoConfiguration { public AopAutoConfiguration() { } ... } 15、自动配置【源码分析】-自动配置流程 以DispatcherServletAutoConfiguration的内部类DispatcherServletConfiguration为例子:\n1 2 3 4 5 6 7 8 9 @Bean @ConditionalOnBean(MultipartResolver.class) //容器中有这个类型组件 @ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名字 multipartResolver 的组件 public MultipartResolver multipartResolver(MultipartResolver resolver) { //给@Bean标注的方法传入了对象参数，这个参数的值就会从容器中找。 //SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范 // Detect if the user has created a MultipartResolver but named it incorrectly return resolver;//给容器中加入了文件上传解析器； } SpringBoot默认会在底层配好所有的组件，但是如果用户自己配置了以用户的优先。\n总结：\nSpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration 每个自动配置类按照条件进行生效，默认都会绑定配置文件指定的值。（xxxxProperties里面读取，xxxProperties和配置文件进行了绑定） 生效的配置类就会给容器中装配很多组件 只要容器中有这些组件，相当于这些功能就有了 定制化配置 用户直接自己@Bean替换底层的组件 用户去看这个组件是获取的配置文件什么值就去修改。 xxxxxAutoConfiguration \u0026mdash;\u0026gt; 组件 \u0026mdash;\u0026gt; xxxxProperties里面拿值 \u0026mdash;-\u0026gt; application.properties\n16、最佳实践-SpringBoot应用如何编写 引入场景依赖 官方文档 查看自动配置了哪些（选做） 自己分析，引入场景对应的自动配置一般都生效了 配置文件中debug=true开启自动配置报告。 Negative（不生效） Positive（生效） 是否需要修改 参照文档修改配置项 官方文档 自己分析。xxxxProperties绑定了配置文件的哪些。 自定义加入或者替换组件 @Bean、@Component\u0026hellip; 自定义器 XXXXXCustomizer； \u0026hellip;\u0026hellip; 17、最佳实践-Lombok简化开发 Lombok用标签方式代替构造器、getter/setter、toString()等鸡肋代码。\nspring boot已经管理Lombok。引入依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; IDEA中File-\u0026gt;Settings-\u0026gt;Plugins，搜索安装Lombok插件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @NoArgsConstructor //@AllArgsConstructor @Data @ToString @EqualsAndHashCode public class User { private String name; private Integer age; private Pet pet; public User(String name,Integer age){ this.name = name; this.age = age; } } 简化日志开发\n1 2 3 4 5 6 7 8 9 @Slf4j @RestController public class HelloController { @RequestMapping(\u0026#34;/hello\u0026#34;) public String handle01(@RequestParam(\u0026#34;name\u0026#34;) String name){ log.info(\u0026#34;请求进来了....\u0026#34;); return \u0026#34;Hello, Spring Boot 2!\u0026#34;+\u0026#34;你好：\u0026#34;+name; } } 18、最佳实践-dev-tools Spring Boot includes an additional set of tools that can make the application development experience a little more pleasant. The spring-boot-devtools module can be included in any project to provide additional development-time features.——link\nApplications that use spring-boot-devtools automatically restart whenever files on the classpath change. This can be a useful feature when working in an IDE, as it gives a very fast feedback loop for code changes. By default, any entry on the classpath that points to a directory is monitored for changes. Note that certain resources, such as static assets and view templates, do not need to restart the application.——link\nTriggering a restart\nAs DevTools monitors classpath resources, the only way to trigger a restart is to update the classpath. The way in which you cause the classpath to be updated depends on the IDE that you are using:\nIn Eclipse, saving a modified file causes the classpath to be updated and triggers a restart. In IntelliJ IDEA, building the project (Build -\u0026gt; Build Project)(shortcut: Ctrl+F9) has the same effect. 添加依赖：\n1 2 3 4 5 6 7 \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-devtools\u0026lt;/artifactId\u0026gt; \u0026lt;optional\u0026gt;true\u0026lt;/optional\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 在IDEA中，项目或者页面修改以后：Ctrl+F9。\n19、最佳实践-Spring Initailizr Spring Initailizr是创建Spring Boot工程向导。\n在IDEA中，菜单栏New -\u0026gt; Project -\u0026gt; Spring Initailizr。\n20、配置文件-yaml的用法 同以前的properties用法\nYAML 是 \u0026ldquo;YAML Ain\u0026rsquo;t Markup Language\u0026rdquo;（YAML 不是一种标记语言）的递归缩写。在开发的这种语言时，YAML 的意思其实是：\u0026ldquo;Yet Another Markup Language\u0026rdquo;（仍是一种标记语言）。\n非常适合用来做以数据为中心的配置文件。\n基本语法 key: value；kv之间有空格 大小写敏感 使用缩进表示层级关系 缩进不允许使用tab，只允许空格 缩进的空格数不重要，只要相同层级的元素左对齐即可 \u0026lsquo;#\u0026lsquo;表示注释 字符串无需加引号，如果要加，单引号\u0026rsquo;\u0026rsquo;、双引号\u0026quot;\u0026ldquo;表示字符串内容会被 转义、不转义 数据类型 字面量：单个的、不可再分的值。date、boolean、string、number、null 1 k: v 对象：键值对的集合。map、hash、set、object 1 2 3 4 5 6 7 8 9 10 #行内写法： k: {k1:v1,k2:v2,k3:v3} #或 k: k1: v1 k2: v2 k3: v3 数组：一组按次序排列的值。array、list、queue 1 2 3 4 5 6 7 8 9 10 #行内写法： k: [v1,v2,v3] #或者 k: - v1 - v2 - v3 实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Data public class Person { private String userName; private Boolean boss; private Date birth; private Integer age; private Pet pet; private String[] interests; private List\u0026lt;String\u0026gt; animal; private Map\u0026lt;String, Object\u0026gt; score; private Set\u0026lt;Double\u0026gt; salarys; private Map\u0026lt;String, List\u0026lt;Pet\u0026gt;\u0026gt; allPets; } @Data public class Pet { private String name; private Double weight; } 用yaml表示以上对象\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 person: userName: zhangsan boss: false birth: 2019/12/12 20:12:33 age: 18 pet: name: tomcat weight: 23.4 interests: [篮球,游泳] animal: - jerry - mario score: english: first: 30 second: 40 third: 50 math: [131,140,148] chinese: {first: 128,second: 136} salarys: [3999,4999.98,5999.99] allPets: sick: - {name: tom} - {name: jerry,weight: 47} health: [{name: mario,weight: 47}] 21、配置文件-自定义类绑定的配置提示 You can easily generate your own configuration metadata file from items annotated with @ConfigurationProperties by using the spring-boot-configuration-processor jar. The jar includes a Java annotation processor which is invoked as your project is compiled.——link\n自定义的类和配置文件绑定一般没有提示。若要提示，添加如下依赖：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-configuration-processor\u0026lt;/artifactId\u0026gt; \u0026lt;optional\u0026gt;true\u0026lt;/optional\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 下面插件作用是工程打包时，不将spring-boot-configuration-processor打进包内，让其只在编码的时候有用 --\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;excludes\u0026gt; \u0026lt;exclude\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-configuration-processor\u0026lt;/artifactId\u0026gt; \u0026lt;/exclude\u0026gt; \u0026lt;/excludes\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; 22、web场景-web开发简介 Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)\nThe auto-configuration adds the following features on top of Spring’s defaults:\nInclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.\n内容协商视图解析器和BeanName视图解析器 Support for serving static resources, including support for WebJars (covered later in this document)).\n静态资源（包括webjars） Automatic registration of Converter, GenericConverter, and Formatter beans.\n自动注册 Converter，GenericConverter，Formatter Support for HttpMessageConverters (covered later in this document).\n支持 HttpMessageConverters （后来我们配合内容协商理解原理） Automatic registration of MessageCodesResolver (covered later in this document).\n自动注册 MessageCodesResolver （国际化用） Static index.html support.\n静态index.html 页支持 Custom Favicon support (covered later in this document).\n自定义 Favicon Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).\n自动使用 ConfigurableWebBindingInitializer ，（DataBinder负责将请求数据绑定到JavaBean上） If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.\n不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则\nIf you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.\n声明 WebMvcRegistrations 改变默认底层组件\nIf you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.\n使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC\n23、web场景-静态资源规则与定制化 静态资源目录 只要静态资源放在类路径下： called /static (or /public or /resources or /META-INF/resources\n访问 ： 当前项目根路径/ + 静态资源名\n原理： 静态映射/**。\n请求进来，先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面。\n也可以改变默认的静态资源路径，/static，/public,/resources, /META-INF/resources失效\n1 2 resources: static-locations: [classpath:/haha/] 静态资源访问前缀 1 2 3 spring: mvc: static-path-pattern: /res/** 当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找\nwebjar 可用jar方式添加css，js等资源文件，\nhttps://www.webjars.org/\n例如，添加jquery\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.webjars\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jquery\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 访问地址：http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径。\n24、web场景-welcome与favicon功能 官方文档\n欢迎页支持 静态资源路径下 index.html。\n可以配置静态资源路径 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问 1 2 3 4 5 spring: # mvc: # static-path-pattern: /res/** 这个会导致welcome page功能失效 resources: static-locations: [classpath:/haha/] controller能处理/index。 自定义Favicon 指网页标签上的小图标。\nfavicon.ico 放在静态资源目录下即可。\n1 2 3 spring: # mvc: # static-path-pattern: /res/** 这个会导致 Favicon 功能失效 25、web场景-【源码分析】-静态资源原理 SpringBoot启动默认加载 xxxAutoConfiguration 类（自动配置类） SpringMVC功能的自动配置类WebMvcAutoConfiguration，生效 1 2 3 4 5 6 7 8 9 10 @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration { ... } 给容器中配置的内容： 配置文件的相关属性的绑定：WebMvcProperties==spring.mvc、ResourceProperties==spring.resources 1 2 3 4 5 6 7 @Configuration(proxyBeanMethods = false) @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer { ... } 配置类只有一个有参构造器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ////有参构造器所有参数的值都会从容器中确定 public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, ObjectProvider\u0026lt;HttpMessageConverters\u0026gt; messageConvertersProvider, ObjectProvider\u0026lt;ResourceHandlerRegistrationCustomizer\u0026gt; resourceHandlerRegistrationCustomizerProvider, ObjectProvider\u0026lt;DispatcherServletPath\u0026gt; dispatcherServletPath, ObjectProvider\u0026lt;ServletRegistrationBean\u0026lt;?\u0026gt;\u0026gt; servletRegistrations) { this.mvcProperties = mvcProperties; this.beanFactory = beanFactory; this.messageConvertersProvider = messageConvertersProvider; this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable(); this.dispatcherServletPath = dispatcherServletPath; this.servletRegistrations = servletRegistrations; this.mvcProperties.checkConfiguration(); } ResourceProperties resourceProperties；获取和spring.resources绑定的所有的值的对象 WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象 ListableBeanFactory beanFactory Spring的beanFactory HttpMessageConverters 找到所有的HttpMessageConverters ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。 DispatcherServletPath ServletRegistrationBean 给应用注册Servlet、Filter\u0026hellip;. 资源处理的默认规则 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ... public class WebMvcAutoConfiguration { ... public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware { ... @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { super.addResourceHandlers(registry); if (!this.resourceProperties.isAddMappings()) { logger.debug(\u0026#34;Default resource handling disabled\u0026#34;); return; } ServletContext servletContext = getServletContext(); addResourceHandler(registry, \u0026#34;/webjars/**\u0026#34;, \u0026#34;classpath:/META-INF/resources/webjars/\u0026#34;); addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -\u0026gt; { registration.addResourceLocations(this.resourceProperties.getStaticLocations()); if (servletContext != null) { registration.addResourceLocations(new ServletContextResource(servletContext, SERVLET_LOCATION)); } }); } ... } ... } 根据上述代码，我们可以同过配置禁止所有静态资源规则。\n1 2 3 spring: resources: add-mappings: false #禁用所有静态资源规则 静态资源规则：\n1 2 3 4 5 6 7 8 9 10 11 12 13 @ConfigurationProperties(prefix = \u0026#34;spring.resources\u0026#34;, ignoreUnknownFields = false) public class ResourceProperties { private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { \u0026#34;classpath:/META-INF/resources/\u0026#34;, \u0026#34;classpath:/resources/\u0026#34;, \u0026#34;classpath:/static/\u0026#34;, \u0026#34;classpath:/public/\u0026#34; }; /** * Locations of static resources. Defaults to classpath:[/META-INF/resources/, * /resources/, /static/, /public/]. */ private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS; ... } 欢迎页的处理规则 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ... public class WebMvcAutoConfiguration { ... public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware { ... @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping( new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), this.mvcProperties.getStaticPathPattern()); welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations()); return welcomePageHandlerMapping; } WelcomePageHandlerMapping的构造方法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) { if (welcomePage != null \u0026amp;\u0026amp; \u0026#34;/**\u0026#34;.equals(staticPathPattern)) { //要用欢迎页功能，必须是/** logger.info(\u0026#34;Adding welcome page: \u0026#34; + welcomePage); setRootViewName(\u0026#34;forward:index.html\u0026#34;); } else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) { //调用Controller /index logger.info(\u0026#34;Adding welcome page template: index\u0026#34;); setRootViewName(\u0026#34;index\u0026#34;); } } 这构造方法内的代码也解释了web场景-welcome与favicon功能中配置static-path-pattern了，welcome页面和小图标失效的问题。\n26、请求处理-【源码分析】-Rest映射及源码解析 请求映射 @xxxMapping;\n@GetMapping @PostMapping @PutMapping @DeleteMapping Rest风格支持（使用HTTP请求方式动词来表示对资源的操作）\n以前： /getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveUser保存用户 现在： /user GET-获取用户 DELETE-删除用户 PUT-修改用户 POST-保存用户 核心Filter；HiddenHttpMethodFilter 用法\n开启页面表单的Rest功能 页面 form的属性method=post，隐藏域 _method=put、delete等（如果直接get或post，无需隐藏域） 编写请求映射 1 2 3 4 5 spring: mvc: hiddenmethod: filter: enabled: true #开启页面表单的Rest功能 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;form action=\u0026#34;/user\u0026#34; method=\u0026#34;get\u0026#34;\u0026gt; \u0026lt;input value=\u0026#34;REST-GET提交\u0026#34; type=\u0026#34;submit\u0026#34; /\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;form action=\u0026#34;/user\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;input value=\u0026#34;REST-POST提交\u0026#34; type=\u0026#34;submit\u0026#34; /\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;form action=\u0026#34;/user\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;input name=\u0026#34;_method\u0026#34; type=\u0026#34;hidden\u0026#34; value=\u0026#34;DELETE\u0026#34;/\u0026gt; \u0026lt;input value=\u0026#34;REST-DELETE 提交\u0026#34; type=\u0026#34;submit\u0026#34;/\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;form action=\u0026#34;/user\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;input name=\u0026#34;_method\u0026#34; type=\u0026#34;hidden\u0026#34; value=\u0026#34;PUT\u0026#34; /\u0026gt; \u0026lt;input value=\u0026#34;REST-PUT提交\u0026#34;type=\u0026#34;submit\u0026#34; /\u0026gt; \u0026lt;form\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @GetMapping(\u0026#34;/user\u0026#34;) //@RequestMapping(value = \u0026#34;/user\u0026#34;,method = RequestMethod.GET) public String getUser(){ return \u0026#34;GET-张三\u0026#34;; } @PostMapping(\u0026#34;/user\u0026#34;) //@RequestMapping(value = \u0026#34;/user\u0026#34;,method = RequestMethod.POST) public String saveUser(){ return \u0026#34;POST-张三\u0026#34;; } @PutMapping(\u0026#34;/user\u0026#34;) //@RequestMapping(value = \u0026#34;/user\u0026#34;,method = RequestMethod.PUT) public String putUser(){ return \u0026#34;PUT-张三\u0026#34;; } @DeleteMapping(\u0026#34;/user\u0026#34;) //@RequestMapping(value = \u0026#34;/user\u0026#34;,method = RequestMethod.DELETE) public String deleteUser(){ return \u0026#34;DELETE-张三\u0026#34;; } Rest原理（表单提交要使用REST的时候） 表单提交会带上\\_method=PUT 请求过来被HiddenHttpMethodFilter拦截 请求是否正常，并且是POST 获取到\\_method的值。 兼容以下请求；PUT.DELETE.PATCH 原生request（post），包装模式requesWrapper重写了getMethod方法，返回的是传入的值。 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public class HiddenHttpMethodFilter extends OncePerRequestFilter { private static final List\u0026lt;String\u0026gt; ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name())); /** Default method parameter: {@code _method}. */ public static final String DEFAULT_METHOD_PARAM = \u0026#34;_method\u0026#34;; private String methodParam = DEFAULT_METHOD_PARAM; /** * Set the parameter name to look for HTTP methods. * @see #DEFAULT_METHOD_PARAM */ public void setMethodParam(String methodParam) { Assert.hasText(methodParam, \u0026#34;\u0026#39;methodParam\u0026#39; must not be empty\u0026#34;); this.methodParam = methodParam; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { HttpServletRequest requestToUse = request; if (\u0026#34;POST\u0026#34;.equals(request.getMethod()) \u0026amp;\u0026amp; request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) { String paramValue = request.getParameter(this.methodParam); if (StringUtils.hasLength(paramValue)) { String method = paramValue.toUpperCase(Locale.ENGLISH); if (ALLOWED_METHODS.contains(method)) { requestToUse = new HttpMethodRequestWrapper(request, method); } } } filterChain.doFilter(requestToUse, response); } /** * Simple {@link HttpServletRequest} wrapper that returns the supplied method for * {@link HttpServletRequest#getMethod()}. */ private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper { private final String method; public HttpMethodRequestWrapper(HttpServletRequest request, String method) { super(request); this.method = method; } @Override public String getMethod() { return this.method; } } } Rest使用客户端工具。 如PostMan可直接发送put、delete等方式请求。 27、请求处理-【源码分析】-怎么改变默认的_method 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class }) @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class }) public class WebMvcAutoConfiguration { ... @Bean @ConditionalOnMissingBean(HiddenHttpMethodFilter.class) @ConditionalOnProperty(prefix = \u0026#34;spring.mvc.hiddenmethod.filter\u0026#34;, name = \u0026#34;enabled\u0026#34;, matchIfMissing = false) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); } ... } @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)意味着在没有HiddenHttpMethodFilter时，才执行hiddenHttpMethodFilter()。因此，我们可以自定义filter，改变默认的\\_method。例如：\n1 2 3 4 5 6 7 8 9 10 @Configuration(proxyBeanMethods = false) public class WebConfig{ //自定义filter @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); methodFilter.setMethodParam(\u0026#34;_m\u0026#34;); return methodFilter; } } 将\\_method改成_m。\n1 2 3 4 \u0026lt;form action=\u0026#34;/user\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;input name=\u0026#34;_m\u0026#34; type=\u0026#34;hidden\u0026#34; value=\u0026#34;DELETE\u0026#34;/\u0026gt; \u0026lt;input value=\u0026#34;REST-DELETE 提交\u0026#34; type=\u0026#34;submit\u0026#34;/\u0026gt; \u0026lt;/form\u0026gt; 28、请求处理-【源码分析】-请求映射原理 SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet -\u0026gt; doDispatch()\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // 找到当前请求使用哪个Handler（Controller的方法）处理 mappedHandler = getHandler(processedRequest); //HandlerMapping：处理器映射。/xxx-\u0026gt;\u0026gt;xxxx ... } getHandler()方法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 @Nullable protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { for (HandlerMapping mapping : this.handlerMappings) { HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; } } } return null; } this.handlerMappings在Debug模式下展现的内容：\n其中，保存了所有@RequestMapping 和handler的映射规则。\n所有的请求映射都在HandlerMapping中：\nSpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html；\nSpringBoot自动配置了默认 的 RequestMappingHandlerMapping\n请求进来，挨个尝试所有的HandlerMapping看是否有请求信息。\n如果有就找到这个请求对应的handler 如果没有就是下一个 HandlerMapping 我们需要一些自定义的映射处理，我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping\nIDEA快捷键：\nCtrl + Alt + U : 以UML的类图展现类有哪些继承类，派生类以及实现哪些接口。 Crtl + Alt + Shift + U : 同上，区别在于上条快捷键结果在新页展现，而本条快捷键结果在弹窗展现。 Ctrl + H : 以树形方式展现类层次结构图。 29、请求处理-常用参数注解使用 注解：\n@PathVariable 路径变量 @RequestHeader 获取请求头 @RequestParam 获取请求参数（指问号后的参数，url?a=1\u0026amp;b=2） @CookieValue 获取Cookie值 @RequestAttribute 获取request域属性 @RequestBody 获取请求体[POST] @MatrixVariable 矩阵变量 @ModelAttribute 使用用例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @RestController public class ParameterTestController { // car/2/owner/zhangsan @GetMapping(\u0026#34;/car/{id}/owner/{username}\u0026#34;) public Map\u0026lt;String,Object\u0026gt; getCar(@PathVariable(\u0026#34;id\u0026#34;) Integer id, @PathVariable(\u0026#34;username\u0026#34;) String name, @PathVariable Map\u0026lt;String,String\u0026gt; pv, @RequestHeader(\u0026#34;User-Agent\u0026#34;) String userAgent, @RequestHeader Map\u0026lt;String,String\u0026gt; header, @RequestParam(\u0026#34;age\u0026#34;) Integer age, @RequestParam(\u0026#34;inters\u0026#34;) List\u0026lt;String\u0026gt; inters, @RequestParam Map\u0026lt;String,String\u0026gt; params, @CookieValue(\u0026#34;_ga\u0026#34;) String _ga, @CookieValue(\u0026#34;_ga\u0026#34;) Cookie cookie){ Map\u0026lt;String,Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); // map.put(\u0026#34;id\u0026#34;,id); // map.put(\u0026#34;name\u0026#34;,name); // map.put(\u0026#34;pv\u0026#34;,pv); // map.put(\u0026#34;userAgent\u0026#34;,userAgent); // map.put(\u0026#34;headers\u0026#34;,header); map.put(\u0026#34;age\u0026#34;,age); map.put(\u0026#34;inters\u0026#34;,inters); map.put(\u0026#34;params\u0026#34;,params); map.put(\u0026#34;_ga\u0026#34;,_ga); System.out.println(cookie.getName()+\u0026#34;===\u0026gt;\u0026#34;+cookie.getValue()); return map; } @PostMapping(\u0026#34;/save\u0026#34;) public Map postMethod(@RequestBody String content){ Map\u0026lt;String,Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;content\u0026#34;,content); return map; } } 30、请求处理-@RequestAttribute 用例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @Controller public class RequestController { @GetMapping(\u0026#34;/goto\u0026#34;) public String goToPage(HttpServletRequest request){ request.setAttribute(\u0026#34;msg\u0026#34;,\u0026#34;成功了...\u0026#34;); request.setAttribute(\u0026#34;code\u0026#34;,200); return \u0026#34;forward:/success\u0026#34;; //转发到 /success请求 } @GetMapping(\u0026#34;/params\u0026#34;) public String testParam(Map\u0026lt;String,Object\u0026gt; map, Model model, HttpServletRequest request, HttpServletResponse response){ map.put(\u0026#34;hello\u0026#34;,\u0026#34;world666\u0026#34;); model.addAttribute(\u0026#34;world\u0026#34;,\u0026#34;hello666\u0026#34;); request.setAttribute(\u0026#34;message\u0026#34;,\u0026#34;HelloWorld\u0026#34;); Cookie cookie = new Cookie(\u0026#34;c1\u0026#34;,\u0026#34;v1\u0026#34;); response.addCookie(cookie); return \u0026#34;forward:/success\u0026#34;; } ///\u0026lt;-----------------主角@RequestAttribute在这个方法 @ResponseBody @GetMapping(\u0026#34;/success\u0026#34;) public Map success(@RequestAttribute(value = \u0026#34;msg\u0026#34;,required = false) String msg, @RequestAttribute(value = \u0026#34;code\u0026#34;,required = false)Integer code, HttpServletRequest request){ Object msg1 = request.getAttribute(\u0026#34;msg\u0026#34;); Map\u0026lt;String,Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); Object hello = request.getAttribute(\u0026#34;hello\u0026#34;); Object world = request.getAttribute(\u0026#34;world\u0026#34;); Object message = request.getAttribute(\u0026#34;message\u0026#34;); map.put(\u0026#34;reqMethod_msg\u0026#34;,msg1); map.put(\u0026#34;annotation_msg\u0026#34;,msg); map.put(\u0026#34;hello\u0026#34;,hello); map.put(\u0026#34;world\u0026#34;,world); map.put(\u0026#34;message\u0026#34;,message); return map; } } 31、请求处理-@MatrixVariable与UrlPathHelper 语法： 请求路径：/cars/sell;low=34;brand=byd,audi,yd\nSpringBoot默认是禁用了矩阵变量的功能\n手动开启：原理。对于路径的处理。UrlPathHelper的removeSemicolonContent设置为false，让其支持矩阵变量的。 矩阵变量必须有url路径变量才能被解析\n手动开启矩阵变量：\n实现WebMvcConfigurer接口： 1 2 3 4 5 6 7 8 9 10 11 @Configuration(proxyBeanMethods = false) public class WebConfig implements WebMvcConfigurer { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除；后面的内容。矩阵变量功能就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } } 创建返回WebMvcConfigurerBean： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Configuration(proxyBeanMethods = false) public class WebConfig{ @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 不移除；后面的内容。矩阵变量功能就可以生效 urlPathHelper.setRemoveSemicolonContent(false); configurer.setUrlPathHelper(urlPathHelper); } } } } @MatrixVariable的用例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @RestController public class ParameterTestController { ///cars/sell;low=34;brand=byd,audi,yd @GetMapping(\u0026#34;/cars/{path}\u0026#34;) public Map carsSell(@MatrixVariable(\u0026#34;low\u0026#34;) Integer low, @MatrixVariable(\u0026#34;brand\u0026#34;) List\u0026lt;String\u0026gt; brand, @PathVariable(\u0026#34;path\u0026#34;) String path){ Map\u0026lt;String,Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;low\u0026#34;,low); map.put(\u0026#34;brand\u0026#34;,brand); map.put(\u0026#34;path\u0026#34;,path); return map; } // /boss/1;age=20/2;age=10 @GetMapping(\u0026#34;/boss/{bossId}/{empId}\u0026#34;) public Map boss(@MatrixVariable(value = \u0026#34;age\u0026#34;,pathVar = \u0026#34;bossId\u0026#34;) Integer bossAge, @MatrixVariable(value = \u0026#34;age\u0026#34;,pathVar = \u0026#34;empId\u0026#34;) Integer empAge){ Map\u0026lt;String,Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;bossAge\u0026#34;,bossAge); map.put(\u0026#34;empAge\u0026#34;,empAge); return map; } } 32、请求处理-【源码分析】-各种类型参数解析原理 这要从DispatcherServlet开始说起：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class DispatcherServlet extends FrameworkServlet { protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); ... HandlerMapping中找到能处理请求的Handler（Controller.method()）。 为当前Handler 找一个适配器 HandlerAdapter，用的最多的是RequestMappingHandlerAdapter。 适配器执行目标方法并确定方法参数的每一个值。 HandlerAdapter 默认会加载所有HandlerAdapter\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class DispatcherServlet extends FrameworkServlet { /** Detect all HandlerAdapters or just expect \u0026#34;handlerAdapter\u0026#34; bean?. */ private boolean detectAllHandlerAdapters = true; ... private void initHandlerAdapters(ApplicationContext context) { this.handlerAdapters = null; if (this.detectAllHandlerAdapters) { // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts. Map\u0026lt;String, HandlerAdapter\u0026gt; matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerAdapters = new ArrayList\u0026lt;\u0026gt;(matchingBeans.values()); // We keep HandlerAdapters in sorted order. AnnotationAwareOrderComparator.sort(this.handlerAdapters); } } ... 有这些HandlerAdapter：\n支持方法上标注@RequestMapping\n支持函数式编程的\n\u0026hellip;\n\u0026hellip;\n执行目标方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class DispatcherServlet extends FrameworkServlet { protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ModelAndView mv = null; ... // Determine handler for the current request. mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); ... //本节重点 // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); HandlerAdapter接口实现类RequestMappingHandlerAdapter（主要用来处理@RequestMapping）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... //AbstractHandlerMethodAdapter类的方法，RequestMappingHandlerAdapter继承AbstractHandlerMethodAdapter public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return handleInternal(request, response, (HandlerMethod) handler); } @Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; //handleInternal的核心 mav = invokeHandlerMethod(request, response, handlerMethod);//解释看下节 //... return mav; } } 参数解析器 确定将要执行的目标方法的每一个参数的值是什么;\nSpringMVC目标方法能写多少种参数类型。取决于参数解析器argumentResolvers。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Nullable protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) {//\u0026lt;-----关注点 invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } ... this.argumentResolvers在afterPropertiesSet()方法内初始化\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { @Nullable private HandlerMethodArgumentResolverComposite argumentResolvers; @Override public void afterPropertiesSet() { ... if (this.argumentResolvers == null) {//初始化argumentResolvers List\u0026lt;HandlerMethodArgumentResolver\u0026gt; resolvers = getDefaultArgumentResolvers(); this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); } ... } //初始化了一堆的实现HandlerMethodArgumentResolver接口的 private List\u0026lt;HandlerMethodArgumentResolver\u0026gt; getDefaultArgumentResolvers() { List\u0026lt;HandlerMethodArgumentResolver\u0026gt; resolvers = new ArrayList\u0026lt;\u0026gt;(30); // Annotation-based argument resolution resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new PathVariableMethodArgumentResolver()); resolvers.add(new PathVariableMapMethodArgumentResolver()); resolvers.add(new MatrixVariableMethodArgumentResolver()); resolvers.add(new MatrixVariableMapMethodArgumentResolver()); resolvers.add(new ServletModelAttributeMethodProcessor(false)); resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new SessionAttributeMethodArgumentResolver()); resolvers.add(new RequestAttributeMethodArgumentResolver()); // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); resolvers.add(new ServletResponseMethodArgumentResolver()); resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RedirectAttributesMethodArgumentResolver()); resolvers.add(new ModelMethodProcessor()); resolvers.add(new MapMethodProcessor()); resolvers.add(new ErrorsMethodArgumentResolver()); resolvers.add(new SessionStatusMethodArgumentResolver()); resolvers.add(new UriComponentsBuilderMethodArgumentResolver()); if (KotlinDetector.isKotlinPresent()) { resolvers.add(new ContinuationHandlerMethodArgumentResolver()); } // Custom arguments if (getCustomArgumentResolvers() != null) { resolvers.addAll(getCustomArgumentResolvers()); } // Catch-all resolvers.add(new PrincipalMethodArgumentResolver()); resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true)); resolvers.add(new ServletModelAttributeMethodProcessor(true)); return resolvers; } } HandlerMethodArgumentResolverComposite类如下：（众多参数解析器argumentResolvers的包装类）。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { private final List\u0026lt;HandlerMethodArgumentResolver\u0026gt; argumentResolvers = new ArrayList\u0026lt;\u0026gt;(); ... public HandlerMethodArgumentResolverComposite addResolvers( @Nullable HandlerMethodArgumentResolver... resolvers) { if (resolvers != null) { Collections.addAll(this.argumentResolvers, resolvers); } return this; } ... } 我们看看HandlerMethodArgumentResolver的源码：\n1 2 3 4 5 6 7 8 9 10 public interface HandlerMethodArgumentResolver { //当前解析器是否支持解析这种参数 boolean supportsParameter(MethodParameter parameter); @Nullable//如果支持，就调用 resolveArgument Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; } 返回值处理器 ValueHandler\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Nullable protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) {//\u0026lt;---关注点 invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } ... this.returnValueHandlers在afterPropertiesSet()方法内初始化\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { @Nullable private HandlerMethodReturnValueHandlerComposite returnValueHandlers; @Override public void afterPropertiesSet() { ... if (this.returnValueHandlers == null) { List\u0026lt;HandlerMethodReturnValueHandler\u0026gt; handlers = getDefaultReturnValueHandlers(); this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } } //初始化了一堆的实现HandlerMethodReturnValueHandler接口的 private List\u0026lt;HandlerMethodReturnValueHandler\u0026gt; getDefaultReturnValueHandlers() { List\u0026lt;HandlerMethodReturnValueHandler\u0026gt; handlers = new ArrayList\u0026lt;\u0026gt;(20); // Single-purpose return value types handlers.add(new ModelAndViewMethodReturnValueHandler()); handlers.add(new ModelMethodProcessor()); handlers.add(new ViewMethodReturnValueHandler()); handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(), this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager)); handlers.add(new StreamingResponseBodyReturnValueHandler()); handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice)); handlers.add(new HttpHeadersReturnValueHandler()); handlers.add(new CallableMethodReturnValueHandler()); handlers.add(new DeferredResultMethodReturnValueHandler()); handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory)); // Annotation-based return value types handlers.add(new ServletModelAttributeMethodProcessor(false)); handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager, this.requestResponseBodyAdvice)); // Multi-purpose return value types handlers.add(new ViewNameMethodReturnValueHandler()); handlers.add(new MapMethodProcessor()); // Custom return value types if (getCustomReturnValueHandlers() != null) { handlers.addAll(getCustomReturnValueHandlers()); } // Catch-all if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) { handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers())); } else { handlers.add(new ServletModelAttributeMethodProcessor(true)); } return handlers; } } HandlerMethodReturnValueHandlerComposite类如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler { private final List\u0026lt;HandlerMethodReturnValueHandler\u0026gt; returnValueHandlers = new ArrayList\u0026lt;\u0026gt;(); ... public HandlerMethodReturnValueHandlerComposite addHandlers( @Nullable List\u0026lt;? extends HandlerMethodReturnValueHandler\u0026gt; handlers) { if (handlers != null) { this.returnValueHandlers.addAll(handlers); } return this; } } HandlerMethodReturnValueHandler接口：\n1 2 3 4 5 6 7 8 public interface HandlerMethodReturnValueHandler { boolean supportsReturnType(MethodParameter returnType); void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception; } 回顾执行目标方法 1 2 3 4 5 6 public class DispatcherServlet extends FrameworkServlet { ... protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ModelAndView mv = null; ... mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); RequestMappingHandlerAdapter的handle()方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... //AbstractHandlerMethodAdapter类的方法，RequestMappingHandlerAdapter继承AbstractHandlerMethodAdapter public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return handleInternal(request, response, (HandlerMethod) handler); } @Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; //handleInternal的核心 mav = invokeHandlerMethod(request, response, handlerMethod);//解释看下节 //... return mav; } } RequestMappingHandlerAdapter的invokeHandlerMethod()方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { ... ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } ... //关注点：执行目标方法 invocableMethod.invokeAndHandle(webRequest, mavContainer); if (asyncManager.isConcurrentHandlingStarted()) { return null; } return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } } invokeAndHandle()方法如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); ... try { //returnValue存储起来 this.returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception ex) { ... } } @Nullable//InvocableHandlerMethod类的，ServletInvocableHandlerMethod类继承InvocableHandlerMethod类 public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { ////获取方法的参数值 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); ... return doInvoke(args); } @Nullable protected Object doInvoke(Object... args) throws Exception { Method method = getBridgedMethod();//@RequestMapping的方法 ReflectionUtils.makeAccessible(method); try { if (KotlinDetector.isSuspendingFunction(method)) { return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args); } //通过反射调用 return method.invoke(getBean(), args);//getBean()指@RequestMapping的方法所在类的对象。 } catch (IllegalArgumentException ex) { ... } catch (InvocationTargetException ex) { ... } } } 如何确定目标方法每一个参数的值 重点分析ServletInvocableHandlerMethod的getMethodArgumentValues方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { ... @Nullable//InvocableHandlerMethod类的，ServletInvocableHandlerMethod类继承InvocableHandlerMethod类 public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { ////获取方法的参数值 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); ... return doInvoke(args); } //本节重点，获取方法的参数值 protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } Object[] args = new Object[parameters.length]; for (int i = 0; i \u0026lt; parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); args[i] = findProvidedArgument(parameter, providedArgs); if (args[i] != null) { continue; } //查看resolvers是否有支持 if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, \u0026#34;No suitable resolver\u0026#34;)); } try { //支持的话就开始解析吧 args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); } catch (Exception ex) { .... } } return args; } } this.resolvers的类型为HandlerMethodArgumentResolverComposite（在参数解析器章节提及）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { return getArgumentResolver(parameter) != null; } @Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); if (resolver == null) { throw new IllegalArgumentException(\u0026#34;Unsupported parameter type [\u0026#34; + parameter.getParameterType().getName() + \u0026#34;]. supportsParameter should be called first.\u0026#34;); } return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); } @Nullable private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { //挨个判断所有参数解析器那个支持解析这个参数 for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { if (resolver.supportsParameter(parameter)) { result = resolver; this.argumentResolverCache.put(parameter, result);//找到了，resolver就缓存起来，方便稍后resolveArgument()方法使用 break; } } } return result; } } 小结 本节描述，一个请求发送到DispatcherServlet后的具体处理流程，也就是SpringMVC的主要原理。\n本节内容较多且硬核，对日后编程很有帮助，需耐心对待。\n可以运行一个示例，打断点，在Debug模式下，查看程序流程。\n33、请求处理-【源码分析】-Servlet API参数解析原理 WebRequest ServletRequest MultipartRequest HttpSession javax.servlet.http.PushBuilder Principal InputStream Reader HttpMethod Locale TimeZone ZoneId ServletRequestMethodArgumentResolver用来处理以上的参数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver { @Nullable private static Class\u0026lt;?\u0026gt; pushBuilder; static { try { pushBuilder = ClassUtils.forName(\u0026#34;javax.servlet.http.PushBuilder\u0026#34;, ServletRequestMethodArgumentResolver.class.getClassLoader()); } catch (ClassNotFoundException ex) { // Servlet 4.0 PushBuilder not found - not supported for injection pushBuilder = null; } } @Override public boolean supportsParameter(MethodParameter parameter) { Class\u0026lt;?\u0026gt; paramType = parameter.getParameterType(); return (WebRequest.class.isAssignableFrom(paramType) || ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType) || HttpSession.class.isAssignableFrom(paramType) || (pushBuilder != null \u0026amp;\u0026amp; pushBuilder.isAssignableFrom(paramType)) || (Principal.class.isAssignableFrom(paramType) \u0026amp;\u0026amp; !parameter.hasParameterAnnotations()) || InputStream.class.isAssignableFrom(paramType) || Reader.class.isAssignableFrom(paramType) || HttpMethod.class == paramType || Locale.class == paramType || TimeZone.class == paramType || ZoneId.class == paramType); } @Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { Class\u0026lt;?\u0026gt; paramType = parameter.getParameterType(); // WebRequest / NativeWebRequest / ServletWebRequest if (WebRequest.class.isAssignableFrom(paramType)) { if (!paramType.isInstance(webRequest)) { throw new IllegalStateException( \u0026#34;Current request is not of type [\u0026#34; + paramType.getName() + \u0026#34;]: \u0026#34; + webRequest); } return webRequest; } // ServletRequest / HttpServletRequest / MultipartRequest / MultipartHttpServletRequest if (ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType)) { return resolveNativeRequest(webRequest, paramType); } // HttpServletRequest required for all further argument types return resolveArgument(paramType, resolveNativeRequest(webRequest, HttpServletRequest.class)); } private \u0026lt;T\u0026gt; T resolveNativeRequest(NativeWebRequest webRequest, Class\u0026lt;T\u0026gt; requiredType) { T nativeRequest = webRequest.getNativeRequest(requiredType); if (nativeRequest == null) { throw new IllegalStateException( \u0026#34;Current request is not of type [\u0026#34; + requiredType.getName() + \u0026#34;]: \u0026#34; + webRequest); } return nativeRequest; } @Nullable private Object resolveArgument(Class\u0026lt;?\u0026gt; paramType, HttpServletRequest request) throws IOException { if (HttpSession.class.isAssignableFrom(paramType)) { HttpSession session = request.getSession(); if (session != null \u0026amp;\u0026amp; !paramType.isInstance(session)) { throw new IllegalStateException( \u0026#34;Current session is not of type [\u0026#34; + paramType.getName() + \u0026#34;]: \u0026#34; + session); } return session; } else if (pushBuilder != null \u0026amp;\u0026amp; pushBuilder.isAssignableFrom(paramType)) { return PushBuilderDelegate.resolvePushBuilder(request, paramType); } else if (InputStream.class.isAssignableFrom(paramType)) { InputStream inputStream = request.getInputStream(); if (inputStream != null \u0026amp;\u0026amp; !paramType.isInstance(inputStream)) { throw new IllegalStateException( \u0026#34;Request input stream is not of type [\u0026#34; + paramType.getName() + \u0026#34;]: \u0026#34; + inputStream); } return inputStream; } else if (Reader.class.isAssignableFrom(paramType)) { Reader reader = request.getReader(); if (reader != null \u0026amp;\u0026amp; !paramType.isInstance(reader)) { throw new IllegalStateException( \u0026#34;Request body reader is not of type [\u0026#34; + paramType.getName() + \u0026#34;]: \u0026#34; + reader); } return reader; } else if (Principal.class.isAssignableFrom(paramType)) { Principal userPrincipal = request.getUserPrincipal(); if (userPrincipal != null \u0026amp;\u0026amp; !paramType.isInstance(userPrincipal)) { throw new IllegalStateException( \u0026#34;Current user principal is not of type [\u0026#34; + paramType.getName() + \u0026#34;]: \u0026#34; + userPrincipal); } return userPrincipal; } else if (HttpMethod.class == paramType) { return HttpMethod.resolve(request.getMethod()); } else if (Locale.class == paramType) { return RequestContextUtils.getLocale(request); } else if (TimeZone.class == paramType) { TimeZone timeZone = RequestContextUtils.getTimeZone(request); return (timeZone != null ? timeZone : TimeZone.getDefault()); } else if (ZoneId.class == paramType) { TimeZone timeZone = RequestContextUtils.getTimeZone(request); return (timeZone != null ? timeZone.toZoneId() : ZoneId.systemDefault()); } // Should never happen... throw new UnsupportedOperationException(\u0026#34;Unknown parameter type: \u0026#34; + paramType.getName()); } /** * Inner class to avoid a hard dependency on Servlet API 4.0 at runtime. */ private static class PushBuilderDelegate { @Nullable public static Object resolvePushBuilder(HttpServletRequest request, Class\u0026lt;?\u0026gt; paramType) { PushBuilder pushBuilder = request.newPushBuilder(); if (pushBuilder != null \u0026amp;\u0026amp; !paramType.isInstance(pushBuilder)) { throw new IllegalStateException( \u0026#34;Current push builder is not of type [\u0026#34; + paramType.getName() + \u0026#34;]: \u0026#34; + pushBuilder); } return pushBuilder; } } } 用例：\n1 2 3 4 5 6 7 8 9 10 11 @Controller public class RequestController { @GetMapping(\u0026#34;/goto\u0026#34;) public String goToPage(HttpServletRequest request){ request.setAttribute(\u0026#34;msg\u0026#34;,\u0026#34;成功了...\u0026#34;); request.setAttribute(\u0026#34;code\u0026#34;,200); return \u0026#34;forward:/success\u0026#34;; //转发到 /success请求 } } 34、请求处理-【源码分析】-Model、Map原理 复杂参数：\nMap\nModel（map、model里面的数据会被放在request的请求域 request.setAttribute）\nErrors/BindingResult\nRedirectAttributes（ 重定向携带数据）\nServletResponse（response）\nSessionStatus\nUriComponentsBuilder\nServletUriComponentsBuilder\n用例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @GetMapping(\u0026#34;/params\u0026#34;) public String testParam(Map\u0026lt;String,Object\u0026gt; map, Model model, HttpServletRequest request, HttpServletResponse response){ //下面三位都是可以给request域中放数据 map.put(\u0026#34;hello\u0026#34;,\u0026#34;world666\u0026#34;); model.addAttribute(\u0026#34;world\u0026#34;,\u0026#34;hello666\u0026#34;); request.setAttribute(\u0026#34;message\u0026#34;,\u0026#34;HelloWorld\u0026#34;); Cookie cookie = new Cookie(\u0026#34;c1\u0026#34;,\u0026#34;v1\u0026#34;); response.addCookie(cookie); return \u0026#34;forward:/success\u0026#34;; } @ResponseBody @GetMapping(\u0026#34;/success\u0026#34;) public Map success(@RequestAttribute(value = \u0026#34;msg\u0026#34;,required = false) String msg, @RequestAttribute(value = \u0026#34;code\u0026#34;,required = false)Integer code, HttpServletRequest request){ Object msg1 = request.getAttribute(\u0026#34;msg\u0026#34;); Map\u0026lt;String,Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); Object hello = request.getAttribute(\u0026#34;hello\u0026#34;);//得出testParam方法赋予的值 world666 Object world = request.getAttribute(\u0026#34;world\u0026#34;);//得出testParam方法赋予的值 hello666 Object message = request.getAttribute(\u0026#34;message\u0026#34;);//得出testParam方法赋予的值 HelloWorld map.put(\u0026#34;reqMethod_msg\u0026#34;,msg1); map.put(\u0026#34;annotation_msg\u0026#34;,msg); map.put(\u0026#34;hello\u0026#34;,hello); map.put(\u0026#34;world\u0026#34;,world); map.put(\u0026#34;message\u0026#34;,message); return map; } Map\u0026lt;String,Object\u0026gt; map\nModel model\nHttpServletRequest request\n上面三位都是可以给request域中放数据，用request.getAttribute()获取\n接下来我们看看，Map\u0026lt;String,Object\u0026gt; map与Model model用什么参数处理器。\nMap\u0026lt;String,Object\u0026gt; map参数用MapMethodProcessor处理：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class MapMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { @Override public boolean supportsParameter(MethodParameter parameter) { return (Map.class.isAssignableFrom(parameter.getParameterType()) \u0026amp;\u0026amp; parameter.getParameterAnnotations().length == 0); } @Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { Assert.state(mavContainer != null, \u0026#34;ModelAndViewContainer is required for model exposure\u0026#34;); return mavContainer.getModel(); } ... } mavContainer.getModel()如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class ModelAndViewContainer { ... private final ModelMap defaultModel = new BindingAwareModelMap(); @Nullable private ModelMap redirectModel; ... public ModelMap getModel() { if (useDefaultModel()) { return this.defaultModel; } else { if (this.redirectModel == null) { this.redirectModel = new ModelMap(); } return this.redirectModel; } } private boolean useDefaultModel() { return (!this.redirectModelScenario || (this.redirectModel == null \u0026amp;\u0026amp; !this.ignoreDefaultModelOnRedirect)); } ... } Model model用ModelMethodProcessor处理：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class ModelMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler { @Override public boolean supportsParameter(MethodParameter parameter) { return Model.class.isAssignableFrom(parameter.getParameterType()); } @Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { Assert.state(mavContainer != null, \u0026#34;ModelAndViewContainer is required for model exposure\u0026#34;); return mavContainer.getModel(); } ... } return mavContainer.getModel();这跟MapMethodProcessor的一致\nModel也是另一种意义的Map。\n接下来看看Map\u0026lt;String,Object\u0026gt; map与Model model值是如何做到用request.getAttribute()获取的。\n众所周知，所有的数据都放在 ModelAndView包含要去的页面地址View，还包含Model数据。\n先看ModelAndView接下来是如何处理的？\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 public class DispatcherServlet extends FrameworkServlet { ... protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ... try { ModelAndView mv = null; ... // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); ... } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { // As of 4.3, we\u0026#39;re processing Errors thrown from handler methods as well, // making them available for @ExceptionHandler methods and other scenarios. dispatchException = new NestedServletException(\u0026#34;Handler dispatch failed\u0026#34;, err); } //处理分发结果 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } ... } private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { ... // Did the handler return a view to render? if (mv != null \u0026amp;\u0026amp; !mv.wasCleared()) { render(mv, request, response); ... } ... } protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { ... View view; String viewName = mv.getViewName(); if (viewName != null) { // We need to resolve the view name. view = resolveViewName(viewName, mv.getModelInternal(), locale, request); if (view == null) { throw new ServletException(\u0026#34;Could not resolve view with name \u0026#39;\u0026#34; + mv.getViewName() + \u0026#34;\u0026#39; in servlet with name \u0026#39;\u0026#34; + getServletName() + \u0026#34;\u0026#39;\u0026#34;); } } else { // No need to lookup: the ModelAndView object contains the actual View object. view = mv.getView(); if (view == null) { throw new ServletException(\u0026#34;ModelAndView [\u0026#34; + mv + \u0026#34;] neither contains a view name nor a \u0026#34; + \u0026#34;View object in servlet with name \u0026#39;\u0026#34; + getServletName() + \u0026#34;\u0026#39;\u0026#34;); } } view.render(mv.getModelInternal(), request, response); ... } } 在Debug模式下，view属为InternalResourceView类。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class InternalResourceView extends AbstractUrlBasedView { @Override//该方法在AbstractView，AbstractUrlBasedView继承了AbstractView public void render(@Nullable Map\u0026lt;String, ?\u0026gt; model, HttpServletRequest request, HttpServletResponse response) throws Exception { ... Map\u0026lt;String, Object\u0026gt; mergedModel = createMergedOutputModel(model, request, response); prepareResponse(request, response); //看下一个方法实现 renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); } @Override protected void renderMergedOutputModel( Map\u0026lt;String, Object\u0026gt; model, HttpServletRequest request, HttpServletResponse response) throws Exception { // Expose the model object as request attributes. // 暴露模型作为请求域属性 exposeModelAsRequestAttributes(model, request);//\u0026lt;---重点 // Expose helpers as request attributes, if any. exposeHelpers(request); // Determine the path for the request dispatcher. String dispatcherPath = prepareForRendering(request, response); // Obtain a RequestDispatcher for the target resource (typically a JSP). RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath); ... } //该方法在AbstractView，AbstractUrlBasedView继承了AbstractView protected void exposeModelAsRequestAttributes(Map\u0026lt;String, Object\u0026gt; model, HttpServletRequest request) throws Exception { model.forEach((name, value) -\u0026gt; { if (value != null) { request.setAttribute(name, value); } else { request.removeAttribute(name); } }); } } exposeModelAsRequestAttributes方法看出，Map\u0026lt;String,Object\u0026gt; map，Model model这两种类型数据可以给request域中放数据，用request.getAttribute()获取。\n35、请求处理-【源码分析】-自定义参数绑定原理 1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController public class ParameterTestController { /** * 数据绑定：页面提交的请求数据（GET、POST）都可以和对象属性进行绑定 * @param person * @return */ @PostMapping(\u0026#34;/saveuser\u0026#34;) public Person saveuser(Person person){ return person; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 /** * 姓名： \u0026lt;input name=\u0026#34;userName\u0026#34;/\u0026gt; \u0026lt;br/\u0026gt; * 年龄： \u0026lt;input name=\u0026#34;age\u0026#34;/\u0026gt; \u0026lt;br/\u0026gt; * 生日： \u0026lt;input name=\u0026#34;birth\u0026#34;/\u0026gt; \u0026lt;br/\u0026gt; * 宠物姓名：\u0026lt;input name=\u0026#34;pet.name\u0026#34;/\u0026gt;\u0026lt;br/\u0026gt; * 宠物年龄：\u0026lt;input name=\u0026#34;pet.age\u0026#34;/\u0026gt; */ @Data public class Person { private String userName; private Integer age; private Date birth; private Pet pet; } @Data public class Pet { private String name; private String age; } 封装过程用到ServletModelAttributeMethodProcessor\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodProcessor { @Override//本方法在ModelAttributeMethodProcessor类， public boolean supportsParameter(MethodParameter parameter) { return (parameter.hasParameterAnnotation(ModelAttribute.class) || (this.annotationNotRequired \u0026amp;\u0026amp; !BeanUtils.isSimpleProperty(parameter.getParameterType()))); } @Override @Nullable//本方法在ModelAttributeMethodProcessor类， public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { ... String name = ModelFactory.getNameForParameter(parameter); ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class); if (ann != null) { mavContainer.setBinding(name, ann.binding()); } Object attribute = null; BindingResult bindingResult = null; if (mavContainer.containsAttribute(name)) { attribute = mavContainer.getModel().get(name); } else { // Create attribute instance try { attribute = createAttribute(name, parameter, binderFactory, webRequest); } catch (BindException ex) { ... } } if (bindingResult == null) { // Bean property binding and validation; // skipped in case of binding failure on construction. WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); if (binder.getTarget() != null) { if (!mavContainer.isBindingDisabled(name)) { //web数据绑定器，将请求参数的值绑定到指定的JavaBean里面** bindRequestParameters(binder, webRequest); } validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() \u0026amp;\u0026amp; isBindExceptionRequired(binder, parameter)) { throw new BindException(binder.getBindingResult()); } } // Value type adaptation, also covering java.util.Optional if (!parameter.getParameterType().isInstance(attribute)) { attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter); } bindingResult = binder.getBindingResult(); } // Add resolved attribute and BindingResult at the end of the model Map\u0026lt;String, Object\u0026gt; bindingResultModel = bindingResult.getModel(); mavContainer.removeAttributes(bindingResultModel); mavContainer.addAllAttributes(bindingResultModel); return attribute; } } WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中\n在过程当中，用到GenericConversionService：在设置每一个值的时候，找它里面的所有converter那个可以将这个数据类型（request带来参数的字符串）转换到指定的类型\n36、请求处理-【源码分析】-自定义Converter原理 未来我们可以给WebDataBinder里面放自己的Converter；\n下面演示将字符串“啊猫,3”转换成Pet对象。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 //1、WebMvcConfigurer定制化SpringMVC的功能 @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new Converter\u0026lt;String, Pet\u0026gt;() { @Override public Pet convert(String source) { // 啊猫,3 if(!StringUtils.isEmpty(source)){ Pet pet = new Pet(); String[] split = source.split(\u0026#34;,\u0026#34;); pet.setName(split[0]); pet.setAge(Integer.parseInt(split[1])); return pet; } return null; } }); } }; } 37、响应处理-【源码分析】-ReturnValueHandler原理 假设给前端自动返回json数据，需要引入相关的依赖\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- web场景自动引入了json场景 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-json\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3.4.RELEASE\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;compile\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; 控制层代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Controller public class ResponseTestController { @ResponseBody //利用返回值处理器里面的消息转换器进行处理 @GetMapping(value = \u0026#34;/test/person\u0026#34;) public Person getPerson(){ Person person = new Person(); person.setAge(28); person.setBirth(new Date()); person.setUserName(\u0026#34;zhangsan\u0026#34;); return person; } } 32、请求处理-【源码分析】-各种类型参数解析原理 - 返回值处理器有讨论ReturnValueHandler。现在直接看看重点：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... @Nullable protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { ... ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) {//\u0026lt;----关注点 invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } ... invocableMethod.invokeAndHandle(webRequest, mavContainer);//看下块代码 if (asyncManager.isConcurrentHandlingStarted()) { return null; } return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); ... try { //看下块代码 this.returnValueHandlers.handleReturnValue( returnValue, getReturnValueType(returnValue), mavContainer, webRequest); } catch (Exception ex) { ... } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler { ... @Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { //selectHandler()实现在下面 HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); if (handler == null) { throw new IllegalArgumentException(\u0026#34;Unknown return value type: \u0026#34; + returnType.getParameterType().getName()); } //开始处理 handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); } @Nullable private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) { boolean isAsyncValue = isAsyncReturnValue(value, returnType); for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) { if (isAsyncValue \u0026amp;\u0026amp; !(handler instanceof AsyncHandlerMethodReturnValueHandler)) { continue; } if (handler.supportsReturnType(returnType)) { return handler; } } return null; } @ResponseBody 注解，即RequestResponseBodyMethodProcessor，它实现HandlerMethodReturnValueHandler接口\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { ... @Override public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { mavContainer.setRequestHandled(true); ServletServerHttpRequest inputMessage = createInputMessage(webRequest); ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); // 使用消息转换器进行写出操作，本方法下一章节介绍： // Try even with null return value. ResponseBodyAdvice could get involved. writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); } } 38、响应处理-【源码分析】-HTTPMessageConverter原理 返回值处理器ReturnValueHandler原理：\n返回值处理器判断是否支持这种类型返回值 supportsReturnType 返回值处理器调用 handleReturnValue 进行处理 RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的。 利用 MessageConverters 进行处理 将数据写为json 内容协商（浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型） 服务器最终根据自己自身的能力，决定服务器能生产出什么样内容类型的数据， SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter ，看谁能处理？ 得到MappingJackson2HttpMessageConverter可以将对象写为json 利用MappingJackson2HttpMessageConverter将对象转为json再写出去。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 //RequestResponseBodyMethodProcessor继承这类 public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler { ... //承接上一节内容 protected \u0026lt;T\u0026gt; void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object body; Class\u0026lt;?\u0026gt; valueType; Type targetType; if (value instanceof CharSequence) { body = value.toString(); valueType = String.class; targetType = String.class; } else { body = value; valueType = getReturnValueType(body, returnType); targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass()); } ... //内容协商（浏览器默认会以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型） MediaType selectedMediaType = null; MediaType contentType = outputMessage.getHeaders().getContentType(); boolean isContentTypePreset = contentType != null \u0026amp;\u0026amp; contentType.isConcrete(); if (isContentTypePreset) { if (logger.isDebugEnabled()) { logger.debug(\u0026#34;Found \u0026#39;Content-Type:\u0026#34; + contentType + \u0026#34;\u0026#39; in response\u0026#34;); } selectedMediaType = contentType; } else { HttpServletRequest request = inputMessage.getServletRequest(); List\u0026lt;MediaType\u0026gt; acceptableTypes = getAcceptableMediaTypes(request); //服务器最终根据自己自身的能力，决定服务器能生产出什么样内容类型的数据 List\u0026lt;MediaType\u0026gt; producibleTypes = getProducibleMediaTypes(request, valueType, targetType); if (body != null \u0026amp;\u0026amp; producibleTypes.isEmpty()) { throw new HttpMessageNotWritableException( \u0026#34;No converter found for return value of type: \u0026#34; + valueType); } List\u0026lt;MediaType\u0026gt; mediaTypesToUse = new ArrayList\u0026lt;\u0026gt;(); for (MediaType requestedType : acceptableTypes) { for (MediaType producibleType : producibleTypes) { if (requestedType.isCompatibleWith(producibleType)) { mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); } } } if (mediaTypesToUse.isEmpty()) { if (body != null) { throw new HttpMediaTypeNotAcceptableException(producibleTypes); } if (logger.isDebugEnabled()) { logger.debug(\u0026#34;No match for \u0026#34; + acceptableTypes + \u0026#34;, supported: \u0026#34; + producibleTypes); } return; } MediaType.sortBySpecificityAndQuality(mediaTypesToUse); //选择一个MediaType for (MediaType mediaType : mediaTypesToUse) { if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } } if (logger.isDebugEnabled()) { logger.debug(\u0026#34;Using \u0026#39;\u0026#34; + selectedMediaType + \u0026#34;\u0026#39;, given \u0026#34; + acceptableTypes + \u0026#34; and supported \u0026#34; + producibleTypes); } } if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); //本节主角：HttpMessageConverter for (HttpMessageConverter\u0026lt;?\u0026gt; converter : this.messageConverters) { GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter\u0026lt;?\u0026gt;) converter : null); //判断是否可写 if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) { body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class\u0026lt;? extends HttpMessageConverter\u0026lt;?\u0026gt;\u0026gt;) converter.getClass(), inputMessage, outputMessage); if (body != null) { Object theBody = body; LogFormatUtils.traceDebug(logger, traceOn -\u0026gt; \u0026#34;Writing [\u0026#34; + LogFormatUtils.formatValue(theBody, !traceOn) + \u0026#34;]\u0026#34;); addContentDispositionHeader(inputMessage, outputMessage); //开始写入 if (genericConverter != null) { genericConverter.write(body, targetType, selectedMediaType, outputMessage); } else { ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); } } else { if (logger.isDebugEnabled()) { logger.debug(\u0026#34;Nothing to write: null body\u0026#34;); } } return; } } } ... } HTTPMessageConverter接口：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 /** * Strategy interface for converting from and to HTTP requests and responses. */ public interface HttpMessageConverter\u0026lt;T\u0026gt; { /** * Indicates whether the given class can be read by this converter. */ boolean canRead(Class\u0026lt;?\u0026gt; clazz, @Nullable MediaType mediaType); /** * Indicates whether the given class can be written by this converter. */ boolean canWrite(Class\u0026lt;?\u0026gt; clazz, @Nullable MediaType mediaType); /** * Return the list of {@link MediaType} objects supported by this converter. */ List\u0026lt;MediaType\u0026gt; getSupportedMediaTypes(); /** * Read an object of the given type from the given input message, and returns it. */ T read(Class\u0026lt;? extends T\u0026gt; clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; /** * Write an given object to the given output message. */ void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; } HttpMessageConverter: 看是否支持将 此 Class类型的对象，转为MediaType类型的数据。\n例子：Person对象转为JSON，或者 JSON转为Person，这将用到MappingJackson2HttpMessageConverter\n1 2 3 public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter { ... } 关于MappingJackson2HttpMessageConverter的实例化请看下节。\n关于HttpMessageConverters的初始化 DispatcherServlet的初始化时会调用initHandlerAdapters(ApplicationContext context)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class DispatcherServlet extends FrameworkServlet { ... private void initHandlerAdapters(ApplicationContext context) { this.handlerAdapters = null; if (this.detectAllHandlerAdapters) { // Find all HandlerAdapters in the ApplicationContext, including ancestor contexts. Map\u0026lt;String, HandlerAdapter\u0026gt; matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerAdapters = new ArrayList\u0026lt;\u0026gt;(matchingBeans.values()); // We keep HandlerAdapters in sorted order. AnnotationAwareOrderComparator.sort(this.handlerAdapters); } } ... 上述代码会加载ApplicationContext的所有HandlerAdapter，用来处理@RequestMapping的RequestMappingHandlerAdapter实现HandlerAdapter接口，RequestMappingHandlerAdapter也被实例化。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... private List\u0026lt;HttpMessageConverter\u0026lt;?\u0026gt;\u0026gt; messageConverters; ... public RequestMappingHandlerAdapter() { this.messageConverters = new ArrayList\u0026lt;\u0026gt;(4); this.messageConverters.add(new ByteArrayHttpMessageConverter()); this.messageConverters.add(new StringHttpMessageConverter()); if (!shouldIgnoreXml) { try { this.messageConverters.add(new SourceHttpMessageConverter\u0026lt;\u0026gt;()); } catch (Error err) { // Ignore when no TransformerFactory implementation is available } } this.messageConverters.add(new AllEncompassingFormHttpMessageConverter()); } 在构造器中看到一堆HttpMessageConverter。接着，重点查看AllEncompassingFormHttpMessageConverter类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConverter { /** * Boolean flag controlled by a {@code spring.xml.ignore} system property that instructs Spring to * ignore XML, i.e. to not initialize the XML-related infrastructure. * \u0026lt;p\u0026gt;The default is \u0026#34;false\u0026#34;. */ private static final boolean shouldIgnoreXml = SpringProperties.getFlag(\u0026#34;spring.xml.ignore\u0026#34;); private static final boolean jaxb2Present; private static final boolean jackson2Present; private static final boolean jackson2XmlPresent; private static final boolean jackson2SmilePresent; private static final boolean gsonPresent; private static final boolean jsonbPresent; private static final boolean kotlinSerializationJsonPresent; static { ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader(); jaxb2Present = ClassUtils.isPresent(\u0026#34;javax.xml.bind.Binder\u0026#34;, classLoader); jackson2Present = ClassUtils.isPresent(\u0026#34;com.fasterxml.jackson.databind.ObjectMapper\u0026#34;, classLoader) \u0026amp;\u0026amp; ClassUtils.isPresent(\u0026#34;com.fasterxml.jackson.core.JsonGenerator\u0026#34;, classLoader); jackson2XmlPresent = ClassUtils.isPresent(\u0026#34;com.fasterxml.jackson.dataformat.xml.XmlMapper\u0026#34;, classLoader); jackson2SmilePresent = ClassUtils.isPresent(\u0026#34;com.fasterxml.jackson.dataformat.smile.SmileFactory\u0026#34;, classLoader); gsonPresent = ClassUtils.isPresent(\u0026#34;com.google.gson.Gson\u0026#34;, classLoader); jsonbPresent = ClassUtils.isPresent(\u0026#34;javax.json.bind.Jsonb\u0026#34;, classLoader); kotlinSerializationJsonPresent = ClassUtils.isPresent(\u0026#34;kotlinx.serialization.json.Json\u0026#34;, classLoader); } public AllEncompassingFormHttpMessageConverter() { if (!shouldIgnoreXml) { try { addPartConverter(new SourceHttpMessageConverter\u0026lt;\u0026gt;()); } catch (Error err) { // Ignore when no TransformerFactory implementation is available } if (jaxb2Present \u0026amp;\u0026amp; !jackson2XmlPresent) { addPartConverter(new Jaxb2RootElementHttpMessageConverter()); } } if (jackson2Present) { addPartConverter(new MappingJackson2HttpMessageConverter());//\u0026lt;----重点看这里 } else if (gsonPresent) { addPartConverter(new GsonHttpMessageConverter()); } else if (jsonbPresent) { addPartConverter(new JsonbHttpMessageConverter()); } else if (kotlinSerializationJsonPresent) { addPartConverter(new KotlinSerializationJsonHttpMessageConverter()); } if (jackson2XmlPresent \u0026amp;\u0026amp; !shouldIgnoreXml) { addPartConverter(new MappingJackson2XmlHttpMessageConverter()); } if (jackson2SmilePresent) { addPartConverter(new MappingJackson2SmileHttpMessageConverter()); } } } public class FormHttpMessageConverter implements HttpMessageConverter\u0026lt;MultiValueMap\u0026lt;String, ?\u0026gt;\u0026gt; { ... private List\u0026lt;HttpMessageConverter\u0026lt;?\u0026gt;\u0026gt; partConverters = new ArrayList\u0026lt;\u0026gt;(); ... public void addPartConverter(HttpMessageConverter\u0026lt;?\u0026gt; partConverter) { Assert.notNull(partConverter, \u0026#34;\u0026#39;partConverter\u0026#39; must not be null\u0026#34;); this.partConverters.add(partConverter); } ... } 在AllEncompassingFormHttpMessageConverter类构造器看到MappingJackson2HttpMessageConverter类的实例化，AllEncompassingFormHttpMessageConverter包含MappingJackson2HttpMessageConverter。\nReturnValueHandler是怎么与MappingJackson2HttpMessageConverter关联起来？请看下节。\nReturnValueHandler与MappingJackson2HttpMessageConverter关联 再次回顾RequestMappingHandlerAdapter\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { ... @Nullable private HandlerMethodReturnValueHandlerComposite returnValueHandlers;//我们关注的returnValueHandlers @Override @Nullable//本方法在AbstractHandlerMethodAdapter public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return handleInternal(request, response, (HandlerMethod) handler); } @Override protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ModelAndView mav; ... mav = invokeHandlerMethod(request, response, handlerMethod); ... return mav; } @Nullable protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) {//\u0026lt;---我们关注的returnValueHandlers invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } ... invocableMethod.invokeAndHandle(webRequest, mavContainer); if (asyncManager.isConcurrentHandlingStarted()) { return null; } return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } } @Override public void afterPropertiesSet() { // Do this first, it may add ResponseBody advice beans ... if (this.returnValueHandlers == null) {//赋值returnValueHandlers List\u0026lt;HandlerMethodReturnValueHandler\u0026gt; handlers = getDefaultReturnValueHandlers(); this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); } } private List\u0026lt;HandlerMethodReturnValueHandler\u0026gt; getDefaultReturnValueHandlers() { List\u0026lt;HandlerMethodReturnValueHandler\u0026gt; handlers = new ArrayList\u0026lt;\u0026gt;(20); ... // Annotation-based return value types //这里就是 ReturnValueHandler与 MappingJackson2HttpMessageConverter关联 的关键点 handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),//\u0026lt;---MessageConverters也就传参传进来的 this.contentNegotiationManager, this.requestResponseBodyAdvice));// ... return handlers; } //------ public List\u0026lt;HttpMessageConverter\u0026lt;?\u0026gt;\u0026gt; getMessageConverters() { return this.messageConverters; } //RequestMappingHandlerAdapter构造器已初始化部分messageConverters public RequestMappingHandlerAdapter() { this.messageConverters = new ArrayList\u0026lt;\u0026gt;(4); this.messageConverters.add(new ByteArrayHttpMessageConverter()); this.messageConverters.add(new StringHttpMessageConverter()); if (!shouldIgnoreXml) { try { this.messageConverters.add(new SourceHttpMessageConverter\u0026lt;\u0026gt;()); } catch (Error err) { // Ignore when no TransformerFactory implementation is available } } this.messageConverters.add(new AllEncompassingFormHttpMessageConverter()); } ... } 应用中WebMvcAutoConfiguration（底层是WebMvcConfigurationSupport实现）传入更多messageConverters，其中就包含MappingJackson2HttpMessageConverter。\n39、响应处理-【源码分析】-内容协商原理 根据客户端接收能力不同，返回不同媒体类型的数据。\n引入XML依赖：\n1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.dataformat\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-dataformat-xml\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 可用Postman软件分别测试返回json和xml：只需要改变请求头中Accept字段（application/json、application/xml）。\nHttp协议中规定的，Accept字段告诉服务器本客户端可以接收的数据类型。\n内容协商原理：\n判断当前响应头中是否已经有确定的媒体类型MediaType。 获取客户端（PostMan、浏览器）支持接收的内容类型。（获取客户端Accept请求头字段application/xml）（这一步在下一节有详细介绍） contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略 HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型 遍历循环所有当前系统的 MessageConverter，看谁支持操作这个对象（Person） 找到支持操作Person的converter，把converter支持的媒体类型统计出来。 客户端需要application/xml，服务端有10种MediaType。 进行内容协商的最佳匹配媒体类型 用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 //RequestResponseBodyMethodProcessor继承这类 public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler { ... //跟上一节的代码一致 protected \u0026lt;T\u0026gt; void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object body; Class\u0026lt;?\u0026gt; valueType; Type targetType; if (value instanceof CharSequence) { body = value.toString(); valueType = String.class; targetType = String.class; } else { body = value; valueType = getReturnValueType(body, returnType); targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass()); } ... //本节重点 //内容协商（浏览器默认会以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型） MediaType selectedMediaType = null; MediaType contentType = outputMessage.getHeaders().getContentType(); boolean isContentTypePreset = contentType != null \u0026amp;\u0026amp; contentType.isConcrete(); if (isContentTypePreset) { if (logger.isDebugEnabled()) { logger.debug(\u0026#34;Found \u0026#39;Content-Type:\u0026#34; + contentType + \u0026#34;\u0026#39; in response\u0026#34;); } selectedMediaType = contentType; } else { HttpServletRequest request = inputMessage.getServletRequest(); List\u0026lt;MediaType\u0026gt; acceptableTypes = getAcceptableMediaTypes(request); //服务器最终根据自己自身的能力，决定服务器能生产出什么样内容类型的数据 List\u0026lt;MediaType\u0026gt; producibleTypes = getProducibleMediaTypes(request, valueType, targetType); if (body != null \u0026amp;\u0026amp; producibleTypes.isEmpty()) { throw new HttpMessageNotWritableException( \u0026#34;No converter found for return value of type: \u0026#34; + valueType); } List\u0026lt;MediaType\u0026gt; mediaTypesToUse = new ArrayList\u0026lt;\u0026gt;(); for (MediaType requestedType : acceptableTypes) { for (MediaType producibleType : producibleTypes) { if (requestedType.isCompatibleWith(producibleType)) { mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); } } } if (mediaTypesToUse.isEmpty()) { if (body != null) { throw new HttpMediaTypeNotAcceptableException(producibleTypes); } if (logger.isDebugEnabled()) { logger.debug(\u0026#34;No match for \u0026#34; + acceptableTypes + \u0026#34;, supported: \u0026#34; + producibleTypes); } return; } MediaType.sortBySpecificityAndQuality(mediaTypesToUse); //选择一个MediaType for (MediaType mediaType : mediaTypesToUse) { if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } } if (logger.isDebugEnabled()) { logger.debug(\u0026#34;Using \u0026#39;\u0026#34; + selectedMediaType + \u0026#34;\u0026#39;, given \u0026#34; + acceptableTypes + \u0026#34; and supported \u0026#34; + producibleTypes); } } if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); //本节主角：HttpMessageConverter for (HttpMessageConverter\u0026lt;?\u0026gt; converter : this.messageConverters) { GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter\u0026lt;?\u0026gt;) converter : null); //判断是否可写 if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) { body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, (Class\u0026lt;? extends HttpMessageConverter\u0026lt;?\u0026gt;\u0026gt;) converter.getClass(), inputMessage, outputMessage); if (body != null) { Object theBody = body; LogFormatUtils.traceDebug(logger, traceOn -\u0026gt; \u0026#34;Writing [\u0026#34; + LogFormatUtils.formatValue(theBody, !traceOn) + \u0026#34;]\u0026#34;); addContentDispositionHeader(inputMessage, outputMessage); //开始写入 if (genericConverter != null) { genericConverter.write(body, targetType, selectedMediaType, outputMessage); } else { ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); } } else { if (logger.isDebugEnabled()) { logger.debug(\u0026#34;Nothing to write: null body\u0026#34;); } } return; } } } ... } 40、响应处理-【源码分析】-基于请求参数的内容协商原理 上一节内容协商原理的第二步：\n获取客户端（PostMan、浏览器）支持接收的内容类型。（获取客户端Accept请求头字段application/xml）\ncontentNegotiationManager 内容协商管理器 默认使用基于请求头的策略 HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 //RequestResponseBodyMethodProcessor继承这类 public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler { ... //跟上一节的代码一致 protected \u0026lt;T\u0026gt; void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Object body; Class\u0026lt;?\u0026gt; valueType; Type targetType; ... //本节重点 //内容协商（浏览器默认会以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型） MediaType selectedMediaType = null; MediaType contentType = outputMessage.getHeaders().getContentType(); boolean isContentTypePreset = contentType != null \u0026amp;\u0026amp; contentType.isConcrete(); if (isContentTypePreset) { if (logger.isDebugEnabled()) { logger.debug(\u0026#34;Found \u0026#39;Content-Type:\u0026#34; + contentType + \u0026#34;\u0026#39; in response\u0026#34;); } selectedMediaType = contentType; } else { HttpServletRequest request = inputMessage.getServletRequest(); List\u0026lt;MediaType\u0026gt; acceptableTypes = getAcceptableMediaTypes(request); //服务器最终根据自己自身的能力，决定服务器能生产出什么样内容类型的数据 List\u0026lt;MediaType\u0026gt; producibleTypes = getProducibleMediaTypes(request, valueType, targetType); ... } //在AbstractMessageConverterMethodArgumentResolver类内 private List\u0026lt;MediaType\u0026gt; getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { //内容协商管理器 默认使用基于请求头的策略 return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request)); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver { ... public ContentNegotiationManager() { this(new HeaderContentNegotiationStrategy());//内容协商管理器 默认使用基于请求头的策略 } @Override public List\u0026lt;MediaType\u0026gt; resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { for (ContentNegotiationStrategy strategy : this.strategies) { List\u0026lt;MediaType\u0026gt; mediaTypes = strategy.resolveMediaTypes(request); if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) { continue; } return mediaTypes; } return MEDIA_TYPE_ALL_LIST; } ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 //基于请求头的策略 public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy { /** * {@inheritDoc} * @throws HttpMediaTypeNotAcceptableException if the \u0026#39;Accept\u0026#39; header cannot be parsed */ @Override public List\u0026lt;MediaType\u0026gt; resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException { String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT); if (headerValueArray == null) { return MEDIA_TYPE_ALL_LIST; } List\u0026lt;String\u0026gt; headerValues = Arrays.asList(headerValueArray); try { List\u0026lt;MediaType\u0026gt; mediaTypes = MediaType.parseMediaTypes(headerValues); MediaType.sortBySpecificityAndQuality(mediaTypes); return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST; } catch (InvalidMediaTypeException ex) { throw new HttpMediaTypeNotAcceptableException( \u0026#34;Could not parse \u0026#39;Accept\u0026#39; header \u0026#34; + headerValues + \u0026#34;: \u0026#34; + ex.getMessage()); } } } 开启浏览器参数方式内容协商功能 为了方便内容协商，开启基于请求参数的内容协商功能。\n1 2 3 4 spring: mvc: contentnegotiation: favor-parameter: true #开启请求参数内容协商模式 内容协商管理器，就会多了一个ParameterContentNegotiationStrategy（由Spring容器注入）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy { private String parameterName = \u0026#34;format\u0026#34;;// /** * Create an instance with the given map of file extensions and media types. */ public ParameterContentNegotiationStrategy(Map\u0026lt;String, MediaType\u0026gt; mediaTypes) { super(mediaTypes); } /** * Set the name of the parameter to use to determine requested media types. * \u0026lt;p\u0026gt;By default this is set to {@code \u0026#34;format\u0026#34;}. */ public void setParameterName(String parameterName) { Assert.notNull(parameterName, \u0026#34;\u0026#39;parameterName\u0026#39; is required\u0026#34;); this.parameterName = parameterName; } public String getParameterName() { return this.parameterName; } @Override @Nullable protected String getMediaTypeKey(NativeWebRequest request) { return request.getParameter(getParameterName()); } //---以下方法在AbstractMappingContentNegotiationStrategy类 @Override public List\u0026lt;MediaType\u0026gt; resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException { return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest)); } /** * An alternative to {@link #resolveMediaTypes(NativeWebRequest)} that accepts * an already extracted key. * @since 3.2.16 */ public List\u0026lt;MediaType\u0026gt; resolveMediaTypeKey(NativeWebRequest webRequest, @Nullable String key) throws HttpMediaTypeNotAcceptableException { if (StringUtils.hasText(key)) { MediaType mediaType = lookupMediaType(key); if (mediaType != null) { handleMatch(key, mediaType); return Collections.singletonList(mediaType); } mediaType = handleNoMatch(webRequest, key); if (mediaType != null) { addMapping(key, mediaType); return Collections.singletonList(mediaType); } } return MEDIA_TYPE_ALL_LIST; } } 然后，浏览器地址输入带format参数的URL：\n1 2 3 http://localhost:8080/test/person?format=json 或 http://localhost:8080/test/person?format=xml 这样，后端会根据参数format的值，返回对应json或xml格式的数据。\n41、响应处理-【源码分析】-自定义MessageConverter 实现多协议数据兼容。json、xml、x-guigu（这个是自创的）\n@ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理\nProcessor 处理方法返回值。通过 MessageConverter处理\n所有 MessageConverter 合起来可以支持各种媒体类型数据的操作（读、写）\n内容协商找到最终的 messageConverter\nSpringMVC的什么功能，一个入口给容器中添加一个 WebMvcConfigurer\n1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration(proxyBeanMethods = false) public class WebConfig { @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void extendMessageConverters(List\u0026lt;HttpMessageConverter\u0026lt;?\u0026gt;\u0026gt; converters) { converters.add(new GuiguMessageConverter()); } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 /** * 自定义的Converter */ public class GuiguMessageConverter implements HttpMessageConverter\u0026lt;Person\u0026gt; { @Override public boolean canRead(Class\u0026lt;?\u0026gt; clazz, MediaType mediaType) { return false; } @Override public boolean canWrite(Class\u0026lt;?\u0026gt; clazz, MediaType mediaType) { return clazz.isAssignableFrom(Person.class); } /** * 服务器要统计所有MessageConverter都能写出哪些内容类型 * * application/x-guigu * @return */ @Override public List\u0026lt;MediaType\u0026gt; getSupportedMediaTypes() { return MediaType.parseMediaTypes(\u0026#34;application/x-guigu\u0026#34;); } @Override public Person read(Class\u0026lt;? extends Person\u0026gt; clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return null; } @Override public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { //自定义协议数据的写出 String data = person.getUserName()+\u0026#34;;\u0026#34;+person.getAge()+\u0026#34;;\u0026#34;+person.getBirth(); //写出去 OutputStream body = outputMessage.getBody(); body.write(data.getBytes()); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import java.util.Date; @Controller public class ResponseTestController { /** * 1、浏览器发请求直接返回 xml [application/xml] jacksonXmlConverter * 2、如果是ajax请求 返回 json [application/json] jacksonJsonConverter * 3、如果硅谷app发请求，返回自定义协议数据 [appliaction/x-guigu] xxxxConverter * 属性值1;属性值2; * * 步骤： * 1、添加自定义的MessageConverter进系统底层 * 2、系统底层就会统计出所有MessageConverter能操作哪些类型 * 3、客户端内容协商 [guigu---\u0026gt;guigu] * * 作业：如何以参数的方式进行内容协商 * @return */ @ResponseBody //利用返回值处理器里面的消息转换器进行处理 @GetMapping(value = \u0026#34;/test/person\u0026#34;) public Person getPerson(){ Person person = new Person(); person.setAge(28); person.setBirth(new Date()); person.setUserName(\u0026#34;zhangsan\u0026#34;); return person; } } 用Postman发送/test/person（请求头Accept:application/x-guigu)，将返回自定义协议数据的写出。\n42、响应处理-【源码分析】-浏览器与PostMan内容协商完全适配 假设你想基于自定义请求参数的自定义内容协商功能。\n换句话，在地址栏输入http://localhost:8080/test/person?format=gg返回数据，跟http://localhost:8080/test/person且请求头参数Accept:application/x-guigu的返回自定义协议数据的一致。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Configuration(proxyBeanMethods = false) public class WebConfig /*implements WebMvcConfigurer*/ { //1、WebMvcConfigurer定制化SpringMVC的功能 @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { /** * 自定义内容协商策略 * @param configurer */ @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { //Map\u0026lt;String, MediaType\u0026gt; mediaTypes Map\u0026lt;String, MediaType\u0026gt; mediaTypes = new HashMap\u0026lt;\u0026gt;(); mediaTypes.put(\u0026#34;json\u0026#34;,MediaType.APPLICATION_JSON); mediaTypes.put(\u0026#34;xml\u0026#34;,MediaType.APPLICATION_XML); //自定义媒体类型 mediaTypes.put(\u0026#34;gg\u0026#34;,MediaType.parseMediaType(\u0026#34;application/x-guigu\u0026#34;)); //指定支持解析哪些参数对应的哪些媒体类型 ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes); // parameterStrategy.setParameterName(\u0026#34;ff\u0026#34;); //还需添加请求头处理策略，否则accept:application/json、application/xml则会失效 HeaderContentNegotiationStrategy headeStrategy = new HeaderContentNegotiationStrategy(); configurer.strategies(Arrays.asList(parameterStrategy, headeStrategy)); } } } ... } 日后开发要注意，有可能我们添加的自定义的功能会覆盖默认很多功能，导致一些默认的功能失效。\n43、视图解析-Thymeleaf初体验 Thymeleaf is a modern server-side Java template engine for both web and standalone environments.\nThymeleaf\u0026rsquo;s main goal is to bring elegant natural templates to your development workflow — HTML that can be correctly displayed in browsers and also work as static prototypes, allowing for stronger collaboration in development teams.\nWith modules for Spring Framework, a host of integrations with your favourite tools, and the ability to plug in your own functionality, Thymeleaf is ideal for modern-day HTML5 JVM web development — although there is much more it can do.——Link\nThymeleaf官方文档\nthymeleaf使用 引入Starter 1 2 3 4 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-thymeleaf\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 自动配置好了thymeleaf 1 2 3 4 5 6 7 @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(ThymeleafProperties.class) @ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class }) @AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class }) public class ThymeleafAutoConfiguration { ... } 自动配好的策略\n所有thymeleaf的配置值都在 ThymeleafProperties\n配置好了 SpringTemplateEngine\n配好了 ThymeleafViewResolver\n我们只需要直接开发页面\n1 2 public static final String DEFAULT_PREFIX = \u0026#34;classpath:/templates/\u0026#34;;//模板放置处 public static final String DEFAULT_SUFFIX = \u0026#34;.html\u0026#34;;//文件的后缀名 编写一个控制层：\n1 2 3 4 5 6 7 8 9 10 @Controller public class ViewTestController { @GetMapping(\u0026#34;/hello\u0026#34;) public String hello(Model model){ //model中的数据会被放在请求域中 request.setAttribute(\u0026#34;a\u0026#34;,aa) model.addAttribute(\u0026#34;msg\u0026#34;,\u0026#34;一定要大力发展工业文化\u0026#34;); model.addAttribute(\u0026#34;link\u0026#34;,\u0026#34;http://www.baidu.com\u0026#34;); return \u0026#34;success\u0026#34;; } } /templates/success.html：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34; xmlns:th=\u0026#34;http://www.thymeleaf.org\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Title\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1 th:text=\u0026#34;${msg}\u0026#34;\u0026gt;nice\u0026lt;/h1\u0026gt; \u0026lt;h2\u0026gt; \u0026lt;a href=\u0026#34;www.baidu.com\u0026#34; th:href=\u0026#34;${link}\u0026#34;\u0026gt;去百度\u0026lt;/a\u0026gt; \u0026lt;br/\u0026gt; \u0026lt;a href=\u0026#34;www.google.com\u0026#34; th:href=\u0026#34;@{/link}\u0026#34;\u0026gt;去百度\u0026lt;/a\u0026gt; \u0026lt;/h2\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 1 2 3 server: servlet: context-path: /app #设置应用名 这个设置后，URL要插入/app, 如http://localhost:8080/app/hello.html。\n基本语法 表达式 表达式名字 语法 用途 变量取值 ${\u0026hellip;} 获取请求域、session域、对象等值 选择变量 *{\u0026hellip;} 获取上下文对象值 消息 #{\u0026hellip;} 获取国际化等值 链接 @{\u0026hellip;} 生成链接 片段表达式 ~{\u0026hellip;} jsp:include 作用，引入公共页面片段 字面量 文本值: \u0026lsquo;one text\u0026rsquo; , \u0026lsquo;Another one!\u0026rsquo; ,… 数字: 0 , 34 , 3.0 , 12.3 ,… 布尔值: true , false 空值: null 变量： one，two，\u0026hellip;. 变量不能有空格 文本操作 字符串拼接: + 变量替换: |The name is ${name}| 数学运算 运算符: + , - , * , / , % 布尔运算 运算符: and , or 一元运算: ! , not 比较运算 比较: \u0026gt; , \u0026lt; , \u0026gt;= , \u0026lt;= ( gt , lt , ge , le ) 等式: == , != ( eq , ne ) 条件运算 If-then: (if) ? (then) If-then-else: (if) ? (then) : (else) Default: (value) ?: (defaultvalue) 特殊操作 无操作： _ 设置属性值-th:attr 设置单个值 1 2 3 4 5 6 \u0026lt;form action=\u0026#34;subscribe.html\u0026#34; th:attr=\u0026#34;action=@{/subscribe}\u0026#34;\u0026gt; \u0026lt;fieldset\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;email\u0026#34; /\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;Subscribe!\u0026#34; th:attr=\u0026#34;value=#{subscribe.submit}\u0026#34;/\u0026gt; \u0026lt;/fieldset\u0026gt; \u0026lt;/form\u0026gt; 设置多个值 1 ![](../../images/gtvglogo.png) 官方文档 - 5 Setting Attribute Values\n迭代 1 2 3 4 5 \u0026lt;tr th:each=\u0026#34;prod : ${prods}\u0026#34;\u0026gt; \u0026lt;td th:text=\u0026#34;${prod.name}\u0026#34;\u0026gt;Onions\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${prod.price}\u0026#34;\u0026gt;2.41\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${prod.inStock}? #{true} : #{false}\u0026#34;\u0026gt;yes\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; 1 2 3 4 5 \u0026lt;tr th:each=\u0026#34;prod,iterStat : ${prods}\u0026#34; th:class=\u0026#34;${iterStat.odd}? \u0026#39;odd\u0026#39;\u0026#34;\u0026gt; \u0026lt;td th:text=\u0026#34;${prod.name}\u0026#34;\u0026gt;Onions\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${prod.price}\u0026#34;\u0026gt;2.41\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${prod.inStock}? #{true} : #{false}\u0026#34;\u0026gt;yes\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; 条件运算 1 2 3 \u0026lt;a href=\u0026#34;comments.html\u0026#34; th:href=\u0026#34;@{/product/comments(prodId=${prod.id})}\u0026#34; th:if=\u0026#34;${not #lists.isEmpty(prod.comments)}\u0026#34;\u0026gt;view\u0026lt;/a\u0026gt; 1 2 3 4 5 \u0026lt;div th:switch=\u0026#34;${user.role}\u0026#34;\u0026gt; \u0026lt;p th:case=\u0026#34;\u0026#39;admin\u0026#39;\u0026#34;\u0026gt;User is an administrator\u0026lt;/p\u0026gt; \u0026lt;p th:case=\u0026#34;#{roles.manager}\u0026#34;\u0026gt;User is a manager\u0026lt;/p\u0026gt; \u0026lt;p th:case=\u0026#34;*\u0026#34;\u0026gt;User is some other thing\u0026lt;/p\u0026gt; \u0026lt;/div\u0026gt; 属性优先级 Order Feature Attributes 1 Fragment inclusion th:insert th:replace 2 Fragment iteration th:each 3 Conditional evaluation th:if th:unless th:switch th:case 4 Local variable definition th:object th:with 5 General attribute modification th:attr th:attrprepend th:attrappend 6 Specific attribute modification th:value th:href th:src ... 7 Text (tag body modification) th:text th:utext 8 Fragment specification th:fragment 9 Fragment removal th:remove 官方文档 - 10 Attribute Precedence\n44、web实验-后台管理系统基本功能 项目创建 使用IDEA的Spring Initializr。\nthymeleaf、 web-starter、 devtools、 lombok 登陆页面 /static 放置 css，js等静态资源\n/templates/login.html 登录页\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;html lang=\u0026#34;en\u0026#34; xmlns:th=\u0026#34;http://www.thymeleaf.org\u0026#34;\u0026gt;\u0026lt;!-- 要加这玩意thymeleaf才能用 --\u0026gt; \u0026lt;form class=\u0026#34;form-signin\u0026#34; action=\u0026#34;index.html\u0026#34; method=\u0026#34;post\u0026#34; th:action=\u0026#34;@{/login}\u0026#34;\u0026gt; ... \u0026lt;!-- 消息提醒 --\u0026gt; \u0026lt;label style=\u0026#34;color: red\u0026#34; th:text=\u0026#34;${msg}\u0026#34;\u0026gt;\u0026lt;/label\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;userName\u0026#34; class=\u0026#34;form-control\u0026#34; placeholder=\u0026#34;User ID\u0026#34; autofocus\u0026gt; \u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34; class=\u0026#34;form-control\u0026#34; placeholder=\u0026#34;Password\u0026#34;\u0026gt; \u0026lt;button class=\u0026#34;btn btn-lg btn-login btn-block\u0026#34; type=\u0026#34;submit\u0026#34;\u0026gt; \u0026lt;i class=\u0026#34;fa fa-check\u0026#34;\u0026gt;\u0026lt;/i\u0026gt; \u0026lt;/button\u0026gt; ... \u0026lt;/form\u0026gt; /templates/main.html 主页 thymeleaf内联写法：\n1 \u0026lt;p\u0026gt;Hello, [[${session.user.name}]]!\u0026lt;/p\u0026gt; 登录控制层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 @Controller public class IndexController { /** * 来登录页 * @return */ @GetMapping(value = {\u0026#34;/\u0026#34;,\u0026#34;/login\u0026#34;}) public String loginPage(){ return \u0026#34;login\u0026#34;; } @PostMapping(\u0026#34;/login\u0026#34;) public String main(User user, HttpSession session, Model model){ //RedirectAttributes if(StringUtils.hasLength(user.getUserName()) \u0026amp;\u0026amp; \u0026#34;123456\u0026#34;.equals(user.getPassword())){ //把登陆成功的用户保存起来 session.setAttribute(\u0026#34;loginUser\u0026#34;,user); //登录成功重定向到main.html; 重定向防止表单重复提交 return \u0026#34;redirect:/main.html\u0026#34;; }else { model.addAttribute(\u0026#34;msg\u0026#34;,\u0026#34;账号密码错误\u0026#34;); //回到登录页面 return \u0026#34;login\u0026#34;; } } /** * 去main页面 * @return */ @GetMapping(\u0026#34;/main.html\u0026#34;) public String mainPage(HttpSession session, Model model){ //最好用拦截器,过滤器 Object loginUser = session.getAttribute(\u0026#34;loginUser\u0026#34;); if(loginUser != null){ return \u0026#34;main\u0026#34;; }else { //session过期，没有登陆过 //回到登录页面 model.addAttribute(\u0026#34;msg\u0026#34;,\u0026#34;请重新登录\u0026#34;); return \u0026#34;login\u0026#34;; } } } 模型 1 2 3 4 5 6 7 @AllArgsConstructor @NoArgsConstructor @Data public class User { private String userName; private String password; } ","permalink":"https://ktzxy.top/posts/bkjzuy1c2e/","summary":"SpringBoot2学习笔记","title":"SpringBoot2学习笔记"},{"content":"使用kubeadm方式搭建K8S集群 kubeadm是官方社区推出的一个用于快速部署kubernetes集群的工具。\n这个工具能通过两条指令完成一个kubernetes集群的部署：\n1 2 3 4 5 # 创建一个 Master 节点 kubeadm init # 将一个 Node 节点加入到当前集群中 kubeadm join \u0026lt;Master节点的IP和端口 \u0026gt; Kubeadm方式搭建K8S集群 使用kubeadm方式搭建K8s集群主要分为以下几步\n准备三台虚拟机，同时安装操作系统CentOS 7.x 对三个安装之后的操作系统进行初始化操作 在三个节点安装 docker kubelet kubeadm kubectl 在master节点执行kubeadm init命令初始化 在node节点上执行 kubeadm join命令，把node节点添加到当前集群 配置CNI网络插件，用于节点之间的连通【失败了可以多试几次】 通过拉取一个nginx进行测试，能否进行外网测试 安装要求 在开始之前，部署Kubernetes集群机器需要满足以下几个条件：\n一台或多台机器，操作系统 CentOS7.x-86_x64 硬件配置：2GB或更多RAM，2个CPU或更多CPU，硬盘30GB或更多【注意master需要两核】 可以访问外网，需要拉取镜像，如果服务器不能上网，需要提前下载镜像并导入节点 禁止swap分区 准备环境 角色 IP master 192.168.177.130 node1 192.168.177.131 node2 192.168.177.132 然后开始在每台机器上执行下面的命令\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 # 关闭防火墙 systemctl stop firewalld systemctl disable firewalld # 关闭selinux # 永久关闭 sed -i \u0026#39;s/enforcing/disabled/\u0026#39; /etc/selinux/config # 临时关闭 setenforce 0 # 关闭swap # 临时 swapoff -a # 永久关闭 sed -ri \u0026#39;s/.*swap.*/#\u0026amp;/\u0026#39; /etc/fstab # 根据规划设置主机名【master节点上操作】 hostnamectl set-hostname k8smaster # 根据规划设置主机名【node1节点操作】 hostnamectl set-hostname k8snode1 # 根据规划设置主机名【node2节点操作】 hostnamectl set-hostname k8snode2 # 在master添加hosts cat \u0026gt;\u0026gt; /etc/hosts \u0026lt;\u0026lt; EOF 192.168.177.130 k8smaster 192.168.177.131 k8snode1 192.168.177.132 k8snode2 EOF # 将桥接的IPv4流量传递到iptables的链 cat \u0026gt; /etc/sysctl.d/k8s.conf \u0026lt;\u0026lt; EOF net.bridge.bridge-nf-call-ip6tables = 1 net.bridge.bridge-nf-call-iptables = 1 EOF # 生效 sysctl --system # 时间同步 yum install ntpdate -y ntpdate time.windows.com 安装Docker/kubeadm/kubelet 所有节点安装Docker/kubeadm/kubelet ，Kubernetes默认CRI（容器运行时）为Docker，因此先安装Docker\n安装Docker 首先配置一下Docker的阿里yum源\n1 2 3 4 5 6 7 8 cat \u0026gt;/etc/yum.repos.d/docker.repo\u0026lt;\u0026lt;EOF [docker-ce-edge] name=Docker CE Edge - \\$basearch baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/7/\\$basearch/edge enabled=1 gpgcheck=1 gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg EOF 然后yum方式安装docker\n1 2 3 4 5 6 7 8 9 # yum安装 yum -y install docker-ce # 查看docker版本 docker --version # 启动docker systemctl enable docker systemctl start docker 配置docker的镜像源\n1 2 3 4 5 cat \u0026gt;\u0026gt; /etc/docker/daemon.json \u0026lt;\u0026lt; EOF { \u0026#34;registry-mirrors\u0026#34;: [\u0026#34;https://b9pmyelo.mirror.aliyuncs.com\u0026#34;] } EOF 然后重启docker\n1 systemctl restart docker 添加kubernetes软件源 然后我们还需要配置一下yum的k8s软件源\n1 2 3 4 5 6 7 8 9 cat \u0026gt; /etc/yum.repos.d/kubernetes.repo \u0026lt;\u0026lt; EOF [kubernetes] name=Kubernetes baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64 enabled=1 gpgcheck=0 repo_gpgcheck=0 gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg EOF 安装kubeadm，kubelet和kubectl 由于版本更新频繁，这里指定版本号部署：\n1 2 3 4 # 安装kubelet、kubeadm、kubectl，同时指定版本 yum install -y kubelet-1.18.0 kubeadm-1.18.0 kubectl-1.18.0 # 设置开机启动 systemctl enable kubelet 部署Kubernetes Master【master节点】 在 192.168.177.130 执行，也就是master节点\n1 kubeadm init --apiserver-advertise-address=192.168.177.130 --image-repository registry.aliyuncs.com/google_containers --kubernetes-version v1.18.0 --service-cidr=10.96.0.0/12 --pod-network-cidr=10.244.0.0/16 由于默认拉取镜像地址k8s.gcr.io国内无法访问，这里指定阿里云镜像仓库地址，【执行上述命令会比较慢，因为后台其实已经在拉取镜像了】，我们 docker images 命令即可查看已经拉取的镜像\n当我们出现下面的情况时，表示kubernetes的镜像已经安装成功\n使用kubectl工具 【master节点操作】\n1 2 3 mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config 执行完成后，我们使用下面命令，查看我们正在运行的节点\n1 kubectl get nodes 能够看到，目前有一个master节点已经运行了，但是还处于未准备状态\n下面我们还需要在Node节点执行其它的命令，将node1和node2加入到我们的master节点上\n加入Kubernetes Node【Slave节点】 下面我们需要到 node1 和 node2服务器，执行下面的代码向集群添加新节点\n执行在kubeadm init输出的kubeadm join命令：\n注意，以下的命令是在master初始化完成后，每个人的都不一样！！！需要复制自己生成的\n1 2 kubeadm join 192.168.177.130:6443 --token 8j6ui9.gyr4i156u30y80xf \\ --discovery-token-ca-cert-hash sha256:eda1380256a62d8733f4bddf926f148e57cf9d1a3a58fb45dd6e80768af5a500 默认token有效期为24小时，当过期之后，该token就不可用了。这时就需要重新创建token，操作如下：\n1 kubeadm token create --print-join-command 当我们把两个节点都加入进来后，我们就可以去Master节点 执行下面命令查看情况\n1 kubectl get node 部署CNI网络插件 上面的状态还是NotReady，下面我们需要网络插件，来进行联网访问\n1 2 # 下载网络插件配置 wget https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml 默认镜像地址无法访问，sed命令修改为docker hub镜像仓库。\n1 2 3 4 5 6 7 8 9 10 11 # 添加 kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml ##①首先下载v0.13.1-rc2-amd64 镜像 ##参考博客：https://www.cnblogs.com/pyxuexi/p/14288591.html ##② 导入镜像，命令，，特别提示，3个机器都需要导入，3个机器都需要导入，3个机器都需要导入，3个机器都需要导入，重要的事情说3遍。不然抱错。如果没有操作，报错后，需要删除节点，重置，在导入镜像，重新加入才行。本地就是这样操作成功的！ docker load \u0026lt; flanneld-v0.13.1-rc2-amd64.docker #####下载本地，替换将image: quay.io/coreos/flannel:v0.13.1-rc2 替换为 image: quay.io/coreos/flannel:v0.13.1-rc2-amd64 # 查看状态 【kube-system是k8s中的最小单元】 kubectl get pods -n kube-system 运行后的结果\n运行完成后，我们查看状态可以发现，已经变成了Ready状态了\n如果上述操作完成后，还存在某个节点处于NotReady状态，可以在Master将该节点删除\n1 2 3 4 5 6 7 8 9 10 # master节点将该节点删除 ##20210223 yan 查阅资料添加###kubectl drain k8snode1 --delete-local-data --force --ignore-daemonsets kubectl delete node k8snode1 # 然后到k8snode1节点进行重置 kubeadm reset # 重置完后在加入 kubeadm join 192.168.177.130:6443 --token 8j6ui9.gyr4i156u30y80xf --discovery-token-ca-cert-hash sha256:eda1380256a62d8733f4bddf926f148e57cf9d1a3a58fb45dd6e80768af5a500 测试kubernetes集群 我们都知道K8S是容器化技术，它可以联网去下载镜像，用容器的方式进行启动\n在Kubernetes集群中创建一个pod，验证是否正常运行：\n1 2 3 4 # 下载nginx 【会联网拉取nginx镜像】 kubectl create deployment nginx --image=nginx # 查看状态 kubectl get pod 如果我们出现Running状态的时候，表示已经成功运行了\n下面我们就需要将端口暴露出去，让其它外界能够访问\n1 2 3 4 # 暴露端口 kubectl expose deployment nginx --port=80 --type=NodePort # 查看一下对外的端口 kubectl get pod,svc 能够看到，我们已经成功暴露了 80端口 到 30529上\n我们到我们的宿主机浏览器上，访问如下地址\n1 http://192.168.177.130:30529/ 发现我们的nginx已经成功启动了\n到这里为止，我们就搭建了一个单master的k8s集群\n错误汇总 错误一 在执行Kubernetes init方法的时候，出现这个问题\n1 2 error execution phase preflight: [preflight] Some fatal errors occurred: [ERROR NumCPU]: the number of available CPUs 1 is less than the required 2 是因为VMware设置的核数为1，而K8S需要的最低核数应该是2，调整核数重启系统即可\n错误二 我们在给node1节点使用 kubernetes join命令的时候，出现以下错误\n1 2 error execution phase preflight: [preflight] Some fatal errors occurred: [ERROR Swap]: running with swap on is not supported. Please disable swap 错误原因是我们需要关闭swap\n1 2 3 4 5 # 关闭swap # 临时 swapoff -a # 临时 sed -ri \u0026#39;s/.*swap.*/#\u0026amp;/\u0026#39; /etc/fstab 错误三 在给node1节点使用 kubernetes join命令的时候，出现以下错误\n1 The HTTP call equal to \u0026#39;curl -sSL http://localhost:10248/healthz\u0026#39; failed with error: Get http://localhost:10248/healthz: dial tcp [::1]:10248: connect: connection refused 解决方法，首先需要到 master 节点，创建一个文件\n1 2 3 4 5 6 7 8 9 10 11 # 创建文件夹 mkdir /etc/systemd/system/kubelet.service.d # 创建文件 vim /etc/systemd/system/kubelet.service.d/10-kubeadm.conf # 添加如下内容 Environment=\u0026#34;KUBELET_SYSTEM_PODS_ARGS=--pod-manifest-path=/etc/kubernetes/manifests --allow-privileged=true --fail-swap-on=false\u0026#34; # 重置 kubeadm reset 然后删除刚刚创建的配置目录\n1 rm -rf $HOME/.kube 然后 在master重新初始化\n1 kubeadm init --apiserver-advertise-address=202.193.57.11 --image-repository registry.aliyuncs.com/google_containers --kubernetes-version v1.18.0 --service-cidr=10.96.0.0/12 --pod-network-cidr=10.244.0.0/16 初始完成后，我们再到 node1节点，执行 kubeadm join命令，加入到master\n1 2 kubeadm join 202.193.57.11:6443 --token c7a7ou.z00fzlb01d76r37s \\ --discovery-token-ca-cert-hash sha256:9c3f3cc3f726c6ff8bdff14e46b1a856e3b8a4cbbe30cab185f6c5ee453aeea5 添加完成后，我们使用下面命令，查看节点是否成功添加\n1 kubectl get nodes 错误四 我们再执行查看节点的时候， kubectl get nodes 会出现问题\n1 Unable to connect to the server: x509: certificate signed by unknown authority (possibly because of \u0026#34;crypto/rsa: verification error\u0026#34; while trying to verify candidate authority certificate \u0026#34;kubernetes\u0026#34;) 这是因为我们之前创建的配置文件还存在，也就是这些配置\n1 2 3 mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config 我们需要做的就是把配置文件删除，然后重新执行一下\n1 rm -rf $HOME/.kube 然后再次创建一下即可\n1 2 3 mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config 这个问题主要是因为我们在执行 kubeadm reset 的时候，没有把 $HOME/.kube 给移除掉，再次创建时就会出现问题了\n错误五 安装的时候，出现以下错误\n1 Another app is currently holding the yum lock; waiting for it to exit... 是因为yum上锁占用，解决方法\n1 yum -y install docker-ce 错误六 在使用下面命令，添加node节点到集群上的时候\n1 kubeadm join 192.168.177.130:6443 --token jkcz0t.3c40t0bqqz5g8wsb --discovery-token-ca-cert-hash sha256:bc494eeab6b7bac64c0861da16084504626e5a95ba7ede7b9c2dc7571ca4c9e5 然后出现了这个错误\n1 2 3 4 5 6 7 8 [root@k8smaster ~]# kubeadm join 192.168.177.130:6443 --token jkcz0t.3c40t0bqqz5g8wsb --discovery-token-ca-cert-hash sha256:bc494eeab6b7bac64c0861da16084504626e5a95ba7ede7b9c2dc7571ca4c9e5 W1117 06:55:11.220907 11230 join.go:346] [preflight] WARNING: JoinControlPane.controlPlane settings will be ignored when control-plane flag is not set. [preflight] Running pre-flight checks [WARNING IsDockerSystemdCheck]: detected \u0026#34;cgroupfs\u0026#34; as the Docker cgroup driver. The recommended driver is \u0026#34;systemd\u0026#34;. Please follow the guide at https://kubernetes.io/docs/setup/cri/ error execution phase preflight: [preflight] Some fatal errors occurred: [ERROR FileContent--proc-sys-net-ipv4-ip_forward]: /proc/sys/net/ipv4/ip_forward contents are not set to 1 [preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...` To see the stack trace of this error execute with --v=5 or higher 出于安全考虑，Linux系统默认是禁止数据包转发的。所谓转发即当主机拥有多于一块的网卡时，其中一块收到数据包，根据数据包的目的ip地址将包发往本机另一网卡，该网卡根据路由表继续发送数据包。这通常就是路由器所要实现的功能。也就是说 /proc/sys/net/ipv4/ip_forward 文件的值不支持转发\n0：禁止 1：转发 所以我们需要将值修改成1即可\n1 echo “1” \u0026gt; /proc/sys/net/ipv4/ip_forward 修改完成后，重新执行命令即可\n","permalink":"https://ktzxy.top/posts/ag31mut3sc/","summary":"3 使用kubeadm方式搭建K8S集群","title":"3 使用kubeadm方式搭建K8S集群"},{"content":"﻿### 定义一个用户标量函数,用以实现指定年月,当月有多少天\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 create function getDaysOfMonth(@year int,@month int) returns int as begin declare @days int if @year%400 =0 or (@year %4 =0 and @year %100 != 0) set @days = 29 else set @days = 28 set @days = case when @month = 2 then @days when @month = 4 or @month = 6 or @month = 9 or @month = 11 then 30 else 31 end return @days end 用流程控制语句编写程序,求1×2×3×\u0026hellip;×100的积。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 create function getDaysOfMonth(@year int,@month int) returns int as begin declare @days int if @year%400 =0 or (@year %4 =0 and @year %100 != 0) set @days = 29 else set @days = 28 set @days = case when @month = 2 then @days when @month = 4 or @month = 6 or @month = 9 or @month = 11 then 30 else 31 end return @days end ","permalink":"https://ktzxy.top/posts/uxs88584o5/","summary":"笔记四：学习通习题部分","title":"笔记四：学习通习题部分"},{"content":"Kubernetes集群管理工具kubectl 概述 kubectl是Kubernetes集群的命令行工具，通过kubectl能够对集群本身进行管理，并能够在集群上进行容器化应用的安装和部署\n命令格式 命令格式如下\n1 kubectl [command] [type] [name] [flags] 参数\ncommand：指定要对资源执行的操作，例如create、get、describe、delete type：指定资源类型，资源类型是大小写敏感的，开发者能够以单数 、复数 和 缩略的形式 例如：\n1 2 3 kubectl get pod pod1 kubectl get pods pod1 kubectl get po pod1 name：指定资源的名称，名称也是大小写敏感的，如果省略名称，则会显示所有的资源，例如 1 kubectl get pods flags：指定可选的参数，例如，可用 -s 或者 -server参数指定Kubernetes API server的地址和端口 常见命令 kubectl help 获取更多信息 通过 help命令，能够获取帮助信息\n1 2 3 4 5 # 获取kubectl的命令 kubectl --help # 获取某个命令的介绍和使用 kubectl get --help 基础命令 常见的基础命令\n命令 介绍 create 通过文件名或标准输入创建资源 expose 将一个资源公开为一个新的Service run 在集群中运行一个特定的镜像 set 在对象上设置特定的功能 get 显示一个或多个资源 explain 文档参考资料 edit 使用默认的编辑器编辑一个资源 delete 通过文件名，标准输入，资源名称或标签来删除资源 部署命令 命令 介绍 rollout 管理资源的发布 rolling-update 对给定的复制控制器滚动更新 scale 扩容或缩容Pod数量，Deployment、ReplicaSet、RC或Job autoscale 创建一个自动选择扩容或缩容并设置Pod数量 集群管理命令 命令 介绍 certificate 修改证书资源 cluster-info 显示集群信息 top 显示资源(CPU/M) cordon 标记节点不可调度 uncordon 标记节点可被调度 drain 驱逐节点上的应用，准备下线维护 taint 修改节点taint标记 故障和调试命令 命令 介绍 describe 显示特定资源或资源组的详细信息 logs 在一个Pod中打印一个容器日志，如果Pod只有一个容器，容器名称是可选的 attach 附加到一个运行的容器 exec 执行命令到容器 port-forward 转发一个或多个 proxy 运行一个proxy到Kubernetes API Server cp 拷贝文件或目录到容器中 auth 检查授权 其它命令 命令 介绍 apply 通过文件名或标准输入对资源应用配置 patch 使用补丁修改、更新资源的字段 replace 通过文件名或标准输入替换一个资源 convert 不同的API版本之间转换配置文件 label 更新资源上的标签 annotate 更新资源上的注释 completion 用于实现kubectl工具自动补全 api-versions 打印受支持的API版本 config 修改kubeconfig文件（用于访问API，比如配置认证信息） help 所有命令帮助 plugin 运行一个命令行插件 version 打印客户端和服务版本信息 目前使用的命令 1 2 3 4 5 6 7 8 # 创建一个nginx镜像 kubectl create deployment nginx --image=nginx # 对外暴露端口 kubectl expose deployment nginx --port=80 --type=NodePort # 查看资源 kubectl get pod, svc ","permalink":"https://ktzxy.top/posts/ge9oetn7no/","summary":"6 Kubernetes集群管理工具kubectl","title":"6 Kubernetes集群管理工具kubectl"},{"content":"﻿### 利用超市管理数据库的商品表，编程实现：如果商品表中啤酒类平均售价低于10，则将所有啤酒的售价增加10%，直到平均售价达到10为止\n1 2 3 4 5 6 7 use supermarket WHILE (SELECT AVG(SalePrice) FROM Goods) \u0026lt; 10 BEGIN UPDATE Goods SET SalePrice = SalePrice*1.1 IF (SELECT AVG(SalePrice) FROM Goods) \u0026gt;= 10 BREAK END 创建一个函数fun_avgallgoodsale,求超市管理数据库中所有商品的平均售价 1 2 3 4 5 6 7 8 9 10 CREATE FUNCTION fun_avgallgoodsale() RETURNS decimal(18,2) AS BEGIN DECLARE @name varchar(100),@avg_price decimal(18,2) SELECT @name = (SELECT GoodsName) FROM Goods SELECT @avg_price = (SELECT AVG(SalePrice)) FROM Goods RETURN @name RETURN @avg_price END 创建一个多语句表值函数fun_avggoodsale,求超市管理数据库各类商品的平均售价 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 USE supermarket GO CREATE FUNCTION fun_avggoodsale() RETURNS @avg_salePrice TABLE ( good_category varchar(100), good_saleprice decimal(18,2) ) AS BEGIN INSERT INTO @avg_salePrice SELECT CategoryNO,AVG(SalePrice) FROM Goods GROUP BY CategoryNO RETURN END 创建一个存储过程proc_avgnumsale,显示指定商品类别的平均数量和平均售价 1 2 3 4 5 6 7 8 9 USE supermarket GO CREATE PROCEDURE proc_avgnumsale;1 @category varchar(100) AS SELECT AVG(Number) 平均数量,AVG(SalePrice) FROM Goods G JOIN Category CA ON G.CategoryNO = CA.CategoryNO WHERE CategoryName = @category GO 通过游标cur遍历商品表的数据,并将每个商品的售价增加10% 1 2 3 4 5 6 7 8 9 10 11 12 use supermarket DECLARE cur CURSOR FOR SELECT SalePrice FROM Goods FOR UPDATE DECLARE @saleprice decimal(18,2) OPEN cur FETCH NEXT FROM cur INTO @saleprice WHILE @@fetch_status = 0 BEGIN UPDATE Goods SET SalePrice = SalePrice * 1.1 WHERE CURRENT OF cur; END CLOSE cur DEALLOCATE cur ","permalink":"https://ktzxy.top/posts/kqisufekrt/","summary":"笔记三：测试部分编码","title":"笔记三：测试部分编码"},{"content":" Git 命令进阶 - 经典操作场景\n1. 提交操作(commit) 1.1. 查看提交的历史 如果使用 git commit -a 提交了一次变化(changes)，而又不确定到底这次提交了哪些内容。此时就可以用下面的命令显示当前HEAD上的最近一次的提交(commit):\n1 2 3 (main)$ git show # 或者 $ git log -n1 -p 1.2. 修改提交信息(commit message) 如果提交信息(commit message)写错了且这次提交(commit)还没有推送(push)，可以通过下面的方法来修改提交信息(commit message)：\n1 $ git commit --amend --only 再次打开默认编辑器, 在这里可以重新编辑信息。另外也可以用以下一条命令一次完成：\n1 $ git commit --amend --only -m \u0026#39;xxxxxxx\u0026#39; 如果已经推送(push)了这次提交(commit)，则可以修改这次提交(commit)然后强推(force push)，但是不推荐这么做。\n1.3. 修改提交(commit)里的用户名和邮箱 如果只是单个提交(commit)，则通过以下命令修改：\n1 $ git commit --amend --author \u0026#34;New Authorname \u0026lt;authoremail@mydomain.com\u0026gt;\u0026#34; 如果需要修改所有历史，参考 \u0026lsquo;git filter-branch\u0026rsquo;的指南页.\n1.4. 从一个提交(commit)里移除一个文件 通过下面的方法，从一个提交(commit)里移除一个文件：\n1 2 3 $ git checkout HEAD^ myfile $ git add -A $ git commit --amend 这将非常有用，当有一个开放的补丁(open patch)，往上面提交了一个不必要的文件，需要强推(force push)去更新这个远程补丁。\n1.5. 删除最后一次提交(commit) 如果需要删除已推送了的提交(pushed commits)，可以使用下面的方法。可是，这会不可逆的改变提交的历史，也会搞乱那些已经从该仓库拉取(pulled)了的人的历史。简而言之，如果不是很确定，千万不要这么做。\n1 2 $ git reset HEAD^ --hard $ git push -f [remote] [branch] 如果还没有推送到远程，把Git重置(reset)到最后一次提交前的状态就可以了(同时保存暂存的变化):\n1 (my-branch*)$ git reset --soft HEAD@{1} 以上只能用在没有推送之前。如果已经推送了，唯一安全能做的是 git revert SHAofBadCommit， 那会创建一个新的提交(commit)用于撤消前一个提交的所有变化(changes)；或者如果推送的这个分支是rebase-safe的 (例如：其它开发者不会从这个分支拉取)，则只需要使用 git push -f。\n1.6. 删除任意提交(commit) Notes: 同样的警告，不到万不得已的时候不要这么做。\n1 2 $ git rebase --onto SHA1_OF_BAD_COMMIT^ SHA1_OF_BAD_COMMIT $ git push -f [remote] [branch] 或者做一个 交互式rebase 删除那些想要删除的提交(commit)里所对应的行。\n1.7. 尝试推一个修正后的提交(amended commit)到远程，但是报错： 1 2 3 4 5 6 7 To https://github.com/yourusername/repo.git ! [rejected] mybranch -\u0026gt; mybranch (non-fast-forward) error: failed to push some refs to \u0026#39;https://github.com/tanay1337/webmaker.org.git\u0026#39; hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. Integrate the remote changes (e.g. hint: \u0026#39;git pull ...\u0026#39;) before pushing again. hint: See the \u0026#39;Note about fast-forwards\u0026#39; in \u0026#39;git push --help\u0026#39; for details. 注意，rebasing(见下面)和修正(amending)会用一个新的提交(commit)代替旧的，所以如果之前已经往远程仓库上推过一次修正前的提交(commit)，那现在就必须强推(force push) (-f)。注意总是确保指明一个分支!\n1 (my-branch)$ git push origin mybranch -f 一般来说，要避免强推。最好是创建和推(push)一个新的提交(commit)，而不是强推一个修正后的提交。后者会使那些与该分支或该分支的子分支工作的开发者，在源历史中产生冲突。\n1.8. 做了一次硬重置(hard reset)，想找回之前内容 如果意外的做了 git reset --hard，通常能找回之前提交(commit)，因为Git对每件事都会有日志，且都会保存几天。\n1 (main)$ git reflog 将会看到一个过去提交(commit)的列表和一个重置的提交。选择想要回到的提交(commit)的SHA，再重置一次：\n1 (main)$ git reset --hard SHA1234 2. 暂存(Staging) 2.1. 把暂存的内容添加到上一次的提交(commit) 1 (my-branch*)$ git commit --amend 2.2. 暂存一个新文件的一部分，而不是这个文件的全部 如果想暂存一个文件的一部分，可这样做：\n1 $ git add --patch filename.x -p 简写。这会打开交互模式，将能够用 s 选项来分隔提交(commit)；然而如果这个文件是新的，会没有这个选择，添加一个新文件时可以使用以下操作：\n1 $ git add -N filename.x 然后需要使用 e 选项来手动选择需要添加的行，执行 git diff --cached 将会显示哪些行暂存了哪些行只是保存在本地了。\n2.3. 在一个文件里的变化(changes)加到两个提交(commit)里 git add 会把整个文件加入到一个提交；git add -p 允许交互式的选择想要提交的部分。\n2.4. 把暂存的内容变成未暂存，把未暂存的内容暂存起来 多数情况下，应该将所有的内容变为未暂存，然后再选择想要的内容进行commit。但假定需要这么做，可以创建一个临时的commit来保存已暂存的内容，然后暂存那些未暂存的内容并进行stash。然后reset最后一个commit将原本暂存的内容变为未暂存，最后stash pop回来。\n1 2 3 4 5 $ git commit -m \u0026#34;WIP\u0026#34; $ git add . $ git stash $ git reset HEAD^ $ git stash pop --index 0 Notes:\n这里使用pop仅仅是因为想尽可能保持幂等。 假如不加上--index，会把暂存的文件标记为存储。 3. 未暂存(Unstaged)的内容 3.1. 把未暂存的内容移动到一个新分支 1 $ git checkout -b my-branch 3.2. 把未暂存的内容移动到另一个已存在的分支 1 2 3 $ git stash $ git checkout my-branch $ git stash pop 3.3. 丢弃本地未提交的变化(uncommitted changes) 如果只是想重置源(origin)和本地(local)之间的一些提交(commit)，使用以下命令：\n1 2 3 4 5 6 7 8 # one commit (my-branch)$ git reset --hard HEAD^ # two commits (my-branch)$ git reset --hard HEAD^^ # four commits (my-branch)$ git reset --hard HEAD~4 # or (main)$ git checkout -f 重置某个特殊的文件，可以用文件名做为参数：\n1 $ git reset filename 3.4. 丢弃某些未暂存的内容 如果想丢弃工作拷贝中的一部分内容，而不是全部。签出(checkout)不需要的内容，保留需要的。\n1 2 $ git checkout -p # Answer y to all of the snippets you want to drop 另外一个方法是使用 stash，Stash 所有要保留下的内容，重置工作拷贝，重新应用保留的部分。\n1 2 3 4 $ git stash -p # Select all of the snippets you want to save $ git reset --hard $ git stash pop 或者 stash 不需要的部分，然后 stash drop。\n1 2 3 $ git stash -p # Select all of the snippets you don\u0026#39;t want to save $ git stash drop 4. 分支(Branches) 4.1. 从错误的分支拉取了内容，或把内容拉取到了错误的分支 这是另外一种使用 git reflog 情况，找到在这次错误拉(pull) 之前HEAD的指向。\n1 2 3 (main)$ git reflog ab7555f HEAD@{0}: pull origin wrong-branch: Fast-forward c5bc55a HEAD@{1}: checkout: checkout message goes here 重置分支到所需的提交(desired commit)：\n1 $ git reset --hard c5bc55a 4.2. 扔掉本地的提交(commit)，让本地分支与远程的保持一致 先确认没有推送(push)本地的内容到远程。使用git status 命令显示领先(ahead)源(origin)多少个提交：\n1 2 3 4 5 (my-branch)$ git status # On branch my-branch # Your branch is ahead of \u0026#39;origin/my-branch\u0026#39; by 2 commits. # (use \u0026#34;git push\u0026#34; to publish your local commits) # 另一种方法：\n1 (main)$ git reset --hard origin/my-branch 4.3. 提交到一个新分支，但错误的提交到了 main/master 在main下创建一个新分支，不切换到新分支，仍在main下:\n1 (main)$ git branch my-branch 把 main 分支重置到前一个提交：\n1 (main)$ git reset --hard HEAD^ Tips: HEAD^ 是 HEAD^1 的简写，可以通过指定要设置的HEAD来进一步重置。\n或者，如果不想使用 HEAD^，找到想重置到的提交(commit) 的 hash(git log 能够完成)，然后重置到这个hash。使用git push 同步内容到远程。\n例如，main 分支想重置到的提交的 hash 为a13b85e：\n1 2 (main)$ git reset --hard a13b85e HEAD is now at a13b85e 签出(checkout)刚才新建的分支继续工作：\n1 (main)$ git checkout my-branch 4.4. 保留来自另外一个ref-ish的整个文件 假设正在做一个原型方案(原文为working spike (see note))，有成百的内容，每个都工作得很好。现在，提交到了一个分支，保存工作内容：\n1 (solution)$ git add -A \u0026amp;\u0026amp; git commit -m \u0026#34;Adding all changes from this spike into one big commit.\u0026#34; 当想要把它放到一个分支里 (可能是feature 或者 develop)，关心是保持整个文件的完整，想要一个大的提交分隔成比较小。假设：\n分支 solution，拥有原型方案，领先 develop 分支 分支 develop，在这里应用原型方案的一些内容 可以通过把内容拿到 develop 分支里，来解决这个问题：\n1 (develop)$ git checkout solution -- file1.txt 这会把这个文件内容从分支 solution 拿到分支 develop 里来：\n1 2 3 4 5 6 # On branch develop # Your branch is up-to-date with \u0026#39;origin/develop\u0026#39;. # Changes to be committed: # (use \u0026#34;git reset HEAD \u0026lt;file\u0026gt;...\u0026#34; to unstage) # # modified: file1.txt 然后正常提交\n4.5. 把几个提交(commit)提交到了同一个分支，而这些提交应该分布在不同的分支里 假设有一个main分支，执行git log，看到做过两次提交：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 (main)$ git log commit e3851e817c451cc36f2e6f3049db528415e3c114 Author: Alex Lee \u0026lt;alexlee@example.com\u0026gt; Date: Tue Jul 22 15:39:27 2014 -0400 Bug #21 - Added CSRF protection commit 5ea51731d150f7ddc4a365437931cd8be3bf3131 Author: Alex Lee \u0026lt;alexlee@example.com\u0026gt; Date: Tue Jul 22 15:39:12 2014 -0400 Bug #14 - Fixed spacing on title commit a13b85e984171c6e2a1729bb061994525f626d14 Author: Aki Rose \u0026lt;akirose@example.com\u0026gt; Date: Tue Jul 21 01:12:48 2014 -0400 First commit 先用提交hash(commit hash)标记bug (e3851e8 for #21, 5ea5173 for #14).\n首先把main分支重置到正确的提交(a13b85e)：\n1 2 (main)$ git reset --hard a13b85e HEAD is now at a13b85e 对 bug #21 创建一个新的分支：\n1 2 (main)$ git checkout -b 21 (21)$ 接着，用_cherry-pick_把对bug #21的提交放入当前分支。这意味着将应用(apply)这个提交(commit)，仅仅这一个提交(commit)，直接在HEAD上面。\n1 (21)$ git cherry-pick e3851e8 此时这里可能会产生冲突，参见交互式 rebasing 章 冲突节 解决冲突。然后为 bug #14 创建一个新的分支, 也基于main分支\n1 2 3 (21)$ git checkout main (main)$ git checkout -b 14 (14)$ 最后为 bug #14 执行 cherry-pick:\n1 (14)$ git cherry-pick 5ea5173 4.6. 删除上游(upstream)分支被删除了的本地分支 一旦在 github 上面合并(merge)了一个pull request，就可以删除 fork 里被合并的分支。如果不准备继续在这个分支里工作，删除这个分支的本地拷贝会更干净，使不会陷入工作分支和一堆陈旧分支的混乱之中。\n1 $ git fetch -p 4.7. 不小心删除了分支 如果定期推送到远程，多数情况下应该是安全的，但有些时候还是可能删除了还没有推到远程的分支。下面模拟这种场景，先创建一个分支和一个新的文件\n1 2 3 4 5 (main)$ git checkout -b my-branch (my-branch)$ git branch (my-branch)$ touch foo.txt (my-branch)$ ls README.md foo.txt 添加文件并做一次提交\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 (my-branch)$ git add . (my-branch)$ git commit -m \u0026#39;foo.txt added\u0026#39; (my-branch)$ foo.txt added 1 files changed, 1 insertions(+) create mode 100644 foo.txt (my-branch)$ git log commit 4e3cd85a670ced7cc17a2b5d8d3d809ac88d5012 Author: siemiatj \u0026lt;siemiatj@example.com\u0026gt; Date: Wed Jul 30 00:34:10 2014 +0200 foo.txt added commit 69204cdf0acbab201619d95ad8295928e7f411d5 Author: Kate Hudson \u0026lt;katehudson@example.com\u0026gt; Date: Tue Jul 29 13:14:46 2014 -0400 Fixes #6: Force pushing after amending commits 现在切回到主(main)分支，删除my-branch分支\n1 2 3 4 5 (my-branch)$ git checkout main Switched to branch \u0026#39;main\u0026#39; Your branch is up-to-date with \u0026#39;origin/main\u0026#39;. (main)$ git branch -D my-branch Deleted branch my-branch (was 4e3cd85). 而reflog是一个升级版的日志，它存储了仓库(repo)里面所有动作的历史。\n1 2 3 4 (main)$ git reflog 69204cd HEAD@{0}: checkout: moving from my-branch to main 4e3cd85 HEAD@{1}: commit: foo.txt added 69204cd HEAD@{2}: checkout: moving from main to my-branch 有一个来自删除分支的提交hash(commit hash)，接下来恢复删除了的分支。\n1 2 3 4 5 6 (main)$ git checkout -b my-branch-help Switched to a new branch \u0026#39;my-branch-help\u0026#39; (my-branch-help)$ git reset --hard 4e3cd85 HEAD is now at 4e3cd85 foo.txt added (my-branch-help)$ ls README.md foo.txt Git的 reflog 在rebasing出错的时候也是同样有用的。\n4.8. 删除一个分支 删除一个远程分支：\n1 2 3 (main)$ git push origin --delete my-branch # 或者 (main)$ git push origin :my-branch 删除一个本地分支\n1 (main)$ git branch -D my-branch 4.9. 从别人正在工作的远程分支签出(checkout)一个分支 从远程拉取(fetch) 所有分支：\n1 (main)$ git fetch --all 假设想要从远程的daves分支签出到本地的daves\n1 2 3 (main)$ git checkout --track origin/daves Branch daves set up to track remote branch daves from origin. Switched to a new branch \u0026#39;daves\u0026#39; (--track 是 git checkout -b [branch] [remotename]/[branch] 的简写)。这样就得到了一个daves分支的本地拷贝, 任何推过(pushed)的更新，远程都能看到.\n5. Rebasing 和合并(Merging) 5.1. 撤销rebase/merge 可以合并(merge)或rebase了一个错误的分支, 或者完成不了一个进行中的rebase/merge。Git 在进行危险操作的时候会把原始的HEAD保存在一个叫``ORIG_HEAD`的变量里, 所以要把分支恢复到rebase/merge前的状态是很容易的。\n1 (my-branch)$ git reset --hard ORIG_HEAD 5.2. 已经rebase过, 但是我不想强推(force push) 如果想把这些变化(changes)反应到远程分支上，就必须得强推(force push)。是因快进(Fast forward)了提交，改变了Git历史, 远程分支不会接受变化(changes)，除非强推(force push)。\n这就是许多人使用 merge 工作流，而不是 rebasing 工作流的主要原因之一，开发者的强推(force push)会使大的团队陷入麻烦。使用时需要注意，一种安全使用 rebase 的方法是，不要把你的变化(changes)反映到远程分支上，而是按下面的做:\n1 2 3 4 (main)$ git checkout my-branch (my-branch)$ git rebase -i main (my-branch)$ git checkout main (main)$ git merge --ff-only my-branch 5.3. 组合(combine)几个提交(commit) 假设工作分支将会做对于 main 的 pull-request。一般情况下不关心提交(commit)的时间戳，只想组合所有提交(commit) 到一个单独的里面, 然后重置(reset)重提交(recommit)。确保主(main)分支是最新的和本地变化都已经提交了，然后\n1 2 (my-branch)$ git reset --soft main (my-branch)$ git commit -am \u0026#34;New awesome feature\u0026#34; 如果想要更多的控制，想要保留时间戳，则需要做交互式rebase (interactive rebase)\n1 (my-branch)$ git rebase -i main 如果没有相对的其它分支， 将不得不相对自己的HEAD 进行 rebase。例如：想组合最近的两次提交(commit)，将相对于HEAD~2 进行rebase，组合最近3次提交(commit)，相对于HEAD~3, 等等\n1 (main)$ git rebase -i HEAD~2 在执行了交互式 rebase的命令(interactive rebase command)后，将在编辑器里看到类似下面的内容\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 pick a9c8a1d Some refactoring pick 01b2fd8 New awesome feature pick b729ad5 fixup pick e3851e8 another fix # Rebase 8074d12..b729ad5 onto 8074d12 # # Commands: # p, pick = use commit # r, reword = use commit, but edit the commit message # e, edit = use commit, but stop for amending # s, squash = use commit, but meld into previous commit # f, fixup = like \u0026#34;squash\u0026#34;, but discard this commit\u0026#39;s log message # x, exec = run command (the rest of the line) using shell # # These lines can be re-ordered; they are executed from top to bottom. # # If you remove a line here THAT COMMIT WILL BE LOST. # # However, if you remove everything, the rebase will be aborted. # # Note that empty commits are commented out 所有以 # 开头的行都是注释，不会影响 rebase。然后可以用任何上面命令列表的命令替换 pick，也可以通过删除对应的行来删除一个提交(commit)。\n例如如果想单独保留最旧(first)的提交(commit)，组合所有剩下的到第二个里面，就应该编辑第二个提交(commit)后面的每个提交(commit) 前的单词为 f：\n1 2 3 4 pick a9c8a1d Some refactoring pick 01b2fd8 New awesome feature f b729ad5 fixup f e3851e8 another fix 如果想组合这些提交(commit)并重命名这个提交(commit)，应该在第二个提交(commit)旁边添加一个r，或者更简单的用s 替代 f：\n1 2 3 4 pick a9c8a1d Some refactoring pick 01b2fd8 New awesome feature s b729ad5 fixup s e3851e8 another fix 可以在接下来弹出的文本提示框里重命名提交(commit)\n1 2 3 4 5 6 7 8 9 10 Newer, awesomer features # Please enter the commit message for your changes. Lines starting # with \u0026#39;#\u0026#39; will be ignored, and an empty message aborts the commit. # rebase in progress; onto 8074d12 # You are currently editing a commit while rebasing branch \u0026#39;main\u0026#39; on \u0026#39;8074d12\u0026#39;. # # Changes to be committed: # modified: README.md # 如果成功了，应该看到类似下面的内容：\n1 (main)$ Successfully rebased and updated refs/heads/main. 5.3.1. 安全合并(merging)策略 --no-commit 执行合并(merge)但不自动提交，给用户在做提交前检查和修改的机会。no-ff 会为特性分支(feature branch)的存在过留下证据，保持项目历史一致。\n1 (main)$ git merge --no-ff --no-commit my-branch 5.3.2. 将一个分支合并成一个提交(commit) 1 (main)$ git merge --squash my-branch 5.3.3. 组合(combine)未推的提交(unpushed commit) 有时候在将数据推向上游之前，有几个正在进行的工作提交(commit)。这时候不希望把已经推(push)过的组合进来，因为其他人可能已经有提交(commit)引用它们了。\n1 (main)$ git rebase -i @{u} 这会产生一次交互式的rebase(interactive rebase)，只会列出没有推(push)的提交(commit)，在这个列表时进行reorder/fix/squash 都是安全的。\n5.4. 检查是否分支上的所有提交(commit)都合并(merge)过了 检查一个分支上的所有提交(commit)是否都已经合并(merge)到了其它分支，应该在这些分支的head(或任何 commits)之间做一次diff\n1 (main)$ git log --graph --left-right --cherry-pick --oneline HEAD...feature/120-on-scroll 这会说明在一个分支里有而另一个分支没有的所有提交(commit)，和分支之间不共享的提交(commit)的列表。另一个做法可以是：\n1 (main)$ git log main ^feature/120-on-scroll --no-merges 5.5. 交互式rebase(interactive rebase)可能出现的问题 5.5.1. 这个rebase 编辑屏幕出现\u0026rsquo;noop\u0026rsquo; 如果看到的是这样：\n1 noop 这意味着 rebase 的分支和当前分支在同一个提交(commit)上，或者*领先(ahead)*当前分支。可以尝试：\n检查确保主(main)分支没有问题 rebase HEAD~2 或者更早 5.5.2. 有冲突的情况 如果不能成功的完成 rebase，可能必须要解决冲突。首先执行 git status 找出哪些文件有冲突:\n1 2 3 4 5 6 7 (my-branch)$ git status On branch my-branch Changes not staged for commit: (use \u0026#34;git add \u0026lt;file\u0026gt;...\u0026#34; to update what will be committed) (use \u0026#34;git checkout -- \u0026lt;file\u0026gt;...\u0026#34; to discard changes in working directory) modified: README.md 在这个例子里面, README.md 有冲突。打开这个文件找到类似下面的内容：\n1 2 3 4 5 \u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; HEAD some code ========= some code \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; new-commit 需要解决新提交的代码(示例里, 从中间==线到new-commit的地方)与HEAD 之间不一样的地方。有时候这些合并非常复杂，应该使用可视化的差异编辑器(visual diff editor):\n1 (main*)$ git mergetool -t opendiff 在解决完所有冲突和测试过后，git add 变化了的(changed)文件，然后用git rebase --continue 继续rebase。\n1 2 (my-branch)$ git add README.md (my-branch)$ git rebase --continue 如果在解决完所有的冲突过后，得到了与提交前一样的结果，可以执行git rebase --skip。\n任何时候想结束整个rebase 过程，回来rebase前的分支状态,，可以做：\n1 (my-branch)$ git rebase --abort 6. Stash 6.1. 暂存所有改动 暂存工作目录下的所有改动：\n1 $ git stash 也可以使用-u来排除一些文件\n1 $ git stash -u 6.2. 暂存指定文件 只暂存某一个文件\n1 $ git stash push working-directory-path/filename.ext 暂存多个文件\n1 $ git stash push working-directory-path/filename1.ext working-directory-path/filename2.ext 6.3. 暂存时记录消息 可以在list时看到它\n1 2 3 $ git stash save \u0026lt;message\u0026gt; # 或者 $ git stash push -m \u0026lt;message\u0026gt; 6.4. 指定暂存 首先可以查看 stash记录\n1 $ git stash list 然后可以apply某个stash\n1 $ git stash apply \u0026#34;stash@{n}\u0026#34; 这里的 n 是 stash 在栈中的位置，最上层的stash会是0。除此之外，也可以使用时间标记(假如你能记得的话)。\n1 $ git stash apply \u0026#34;stash@{2.hours.ago}\u0026#34; 6.5. 暂存时保留未暂存的内容 需要手动 create 一个stash commit，然后使用git stash store。\n1 2 $ git stash create $ git stash store -m \u0026#34;commit-message\u0026#34; CREATED_SHA1 7. 杂项(Miscellaneous Objects) 7.1. 克隆所有子模块 1 $ git clone --recursive git://github.com/foo/bar.git 如果已经克隆了:\n1 $ git submodule update --init --recursive 7.2. 删除标签(tag) 1 2 $ git tag -d \u0026lt;tag_name\u0026gt; $ git push \u0026lt;remote\u0026gt; :refs/tags/\u0026lt;tag_name\u0026gt; 7.3. 恢复已删除标签(tag) 如果想恢复一个已删除标签(tag)，可以按照下面的步骤：\n需要找到无法访问的标签(unreachable tag)： 1 $ git fsck --unreachable | grep tag 记下这个标签(tag)的hash，然后用 Git 的 update-ref 1 $ git update-ref refs/tags/\u0026lt;tag_name\u0026gt; \u0026lt;hash\u0026gt; 7.4. 已删除补丁(patch) 如果某人在 GitHub 上发了一个pull request，但是然后他删除了他自己的原始 fork，这样将没法克隆他们的提交(commit)或使用 git am。在这种情况下，最好手动的查看他们的提交(commit)，并把它们拷贝到一个本地新分支，然后做提交。\n做完提交后，再修改作者（参见“变更作者”章节）。然后应用变化，再发起一个新的pull request。\n8. 跟踪文件(Tracking Files) 8.1. 改变一个文件名字的大小写，而不修改内容 1 (main)$ git mv --force myfile MyFile 8.2. 从 Git 删除一个文件，但保留该文件 1 (main)$ git rm --cached log.txt 9. 配置(Configuration) 9.1. 给一些 Git 命令添加别名(alias) 在 OS X 和 Linux 下，Git的配置文件储存在 ~/.gitconfig。在[alias] 部分添加了一些快捷别名(和一些容易拼写错误的)，如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [alias] a = add amend = commit --amend c = commit ca = commit --amend ci = commit -a co = checkout d = diff dc = diff --changed ds = diff --staged f = fetch loll = log --graph --decorate --pretty=oneline --abbrev-commit m = merge one = log --pretty=oneline outstanding = rebase -i @{u} s = status unpushed = log @{u} wc = whatchanged wip = rebase -i @{u} zap = fetch -p 9.2. 缓存一个仓库(repository)的用户名和密码 有一个仓库需要授权，此时可以缓存用户名和密码，而不用每次推/拉(push/pull)的时候都输入，可以使用 Credential helper 来实现。\n1 2 3 4 $ git config --global credential.helper cache # Set git to use the credential memory cache $ git config --global credential.helper \u0026#39;cache --timeout=3600\u0026#39; # Set the cache to timeout after 1 hour (setting is in seconds) 9.3. reflog 如果 重置(reset) 了一些东西，或者合并了错误的分支，又或强推了后找不到自己的提交(commit)了，想回到以前的某个状态。\n这就是 git reflog 的目的，reflog 记录对分支顶端(the tip of a branch)的任何改变，即使那个顶端没有被任何分支或标签引用。基本上，每次HEAD的改变，一条新的记录就会增加到reflog。遗憾的是，这只对本地分支起作用，且它只跟踪动作 (例如，不会跟踪一个没有被记录的文件的任何改变)。\n1 2 3 4 (main)$ git reflog 0a2e358 HEAD@{0}: reset: moving to HEAD~2 0254ea7 HEAD@{1}: checkout: moving from 2.2 to main c10f740 HEAD@{2}: checkout: moving from main to 2.2 上面的 reflog 展示了从 main 分支签出(checkout)到2.2 分支，然后再签回。还有一个硬重置(hard reset)到一个较旧的提交。最新的动作出现在最上面以 HEAD@{0}标识.\n如果事实证明不小心回移(move back)了提交(commit)，reflog 会包含不小心回移前 main 上指向的提交(0254ea7)。\n1 $ git reset --hard 0254ea7 然后使用git reset就可以把main改回到之前的commit，这提供了一个在历史被意外更改情况下的安全网。\n","permalink":"https://ktzxy.top/posts/2f8f53lj5u/","summary":"Git 命令进阶","title":"Git 命令进阶"},{"content":"[TOC]\n1、prometheus.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 # 全局配置 global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: # 监控prometheus本身 - job_name: \u0026#39;服务器Prometheus\u0026#39; static_configs: - targets: [\u0026#39;ip:9090\u0026#39;] # 通过node_exporter将监控数据传给prometheus，如果要监控多台服务器，只要在每个服务器上安装node_exporter，指定不同多ip地址就好了 - job_name: \u0026#39;Linux服务器监控\u0026#39; file_sd_configs: - refresh_interval: 1m files: - \u0026#34;/home/prometheus/node_exporter.yml\u0026#34; # 监控mysql - job_name: \u0026#39;MySql实例监控\u0026#39; static_configs: - targets: [\u0026#39;ip:9104\u0026#39;] # 监控Docker - job_name: \u0026#39;Docker实例监控\u0026#39; file_sd_configs: - refresh_interval: 1m files: - \u0026#34;/home/prometheus/docker_exporter.yml\u0026#34; # 监控Redis集群 - job_name: \u0026#39;SpringBoot应用监控\u0026#39; metrics_path: \u0026#39;/actuator/prometheus\u0026#39; file_sd_configs: - refresh_interval: 1m files: - \u0026#34;/home/prometheus/springboot_exporter.yml\u0026#34; - job_name: \u0026#39;64.63-Redis集群实例监控\u0026#39; static_configs: - targets: - redis://ip:7000 - redis://ip:7001 - redis://ip:7002 - redis://ip:7003 - redis://ip:7004 - redis://ip:7005 metrics_path: /scrape relabel_configs: - source_labels: [__address__] target_label: __param_target - source_labels: [__param_target] target_label: instance - target_label: __address__ replacement: ip:9121 - job_name: \u0026#39;Spark程序监控\u0026#39; static_configs: - targets: [\u0026#39;ip:9108\u0026#39;] - job_name: \u0026#39;Windows服务器监控\u0026#39; static_configs: - targets: [\u0026#39;ip:9182\u0026#39;,\u0026#39;ip:9182\u0026#39;] alerting: alertmanagers: - static_configs: - targets: - ip:9093 rule_files: - \u0026#34;/home/prometheus/node_down.yml\u0026#34; # 实例存活报警规则文件 - \u0026#34;/home/prometheus/memory_over.yml\u0026#34; # 内存报警规则文件 - \u0026#34;/home/prometheus/cpu_over.yml\u0026#34; # cpu报警规则文件 2、java_springboot.yml 1 2 3 4 5 6 7 8 - targets: - \u0026#34;192.168.0.50:8085\u0026#34; labels: instance: ui-zy - targets: - \u0026#34;192.168.0.50:8086\u0026#34; labels: instance: ui-dz ","permalink":"https://ktzxy.top/posts/zsglj9t661/","summary":"yml配置文件解析","title":"yml配置文件解析"},{"content":" jupyter: jupytext: formats: ipynb,md text_representation: extension: .md format_name: markdown format_version: \u0026lsquo;1.3\u0026rsquo; jupytext_version: 1.10.2 kernelspec: display_name: Python 3 language: python name: python3 [toc]\n安装 1 pip install --upgrade pyinstaller 安装最新开发版\n1 pip install https://github.com/pyinstaller/pyinstaller/tarball/develop 验证安装\n1 pyinstaller -v 结果应当输出类似与 3.x 或者 3.n.dev0-xxxxxx 的开发版本号\n💊温馨提示 在虚拟环境下打包，只安装程序需要的依赖包，避免打包后文件过大 在打包成单个 exe 文件之前，请先尝试打包成一个文件夹测试是否正常运行 打包成单个文件启动较慢，原因是程序需要按原目录结构解压到临时目录下，在用户目录下生成临时文件，结束后会自动删除 建议阅读本文全文，以免遗漏 用法 基本用法 1. 直接打包 将生成一个文件夹，里面包含可执行程序及其依赖，启动较单文件快\n1 pyinstaller myscript.py 2. 打包成单文件 1 pyinstaller -F myscript.py 3. 去掉控制台窗口，黑窗口 不显示控制台窗口(类似 cmd 的黑框框), 如果你写的是带 UI 的程序, 此选项基本必选.如果入口程序是 pyw 文件, 此选项默认生效.\n1 pyinstaller -w myscript.py 4. 添加图标 1 pyinstaller -i icon.ico myscript.py 高级用法 打包一遍后，会生成一个xxx.spec文件, 是一个打包参数配置文件，该文件内容由上一次打包命令决定，可以通过添加打包参数重新生成，也可以可以直接修改其内容供下次使用。\n😘 注1： spec 文件生成方法\n将 pyinstaller 命令换成 pyi-makespec\n例：\n1 pyi-makespec -F filename 💊 注2：当你修改过 spec 文件并且想将其应用，你需要以下命令\n1 pyinstaller ***.spec 1. 解决模块缺失 使用场景：\n有些模块在代码里是动态加载的，比如通过 __import__(xxx) 或者 importlib 模块加载的，在打包过程中是无法检测出来的，但是不打包进去这些模块，程序会抛出 ModuleNotFoundError: No module named 'xxx' 异常。这时候根据具体缺失的模块你有三种方法解决。\n一是，在代码里显式地 import 这些模块，尽管你不去使用（pycharm 里显示灰色） 二是，在打包时添加打包参数，可以通过命令行参数，也可通过改 xxx.spec 配置文件，详见下文 三，以上两种方法解决可常见的模块缺失，但有些包就是不管用，该缺失还是缺失，这就需要一个额外的 hook 文件来解决，详见下文。 测试文件\n我们写一个测试文件先： 里面就一行代码\ntest.py 1 print(__import__(\u0026#39;flask\u0026#39;, fromlist=[\u0026#39;\u0026#39;]).Flask) 这是 from flask import Flask 的动态导入写法，正常会输出一个类变量的描述字符串\n1 \u0026lt;class \u0026#39;flask.app.Flask\u0026#39;\u0026gt; 然后执行 pyinstaller test.py，你会发现5秒钟就打完包了😅😅，到打包好的dist/test 下运行可执行文件，就会发现报错：\n1 2 3 4 Traceback (most recent call last): File \u0026#34;test.py\u0026#34;, line 1, in \u0026lt;module\u0026gt; ModuleNotFoundError: No module named \u0026#39;flask\u0026#39; [84851] Failed to execute script \u0026#39;test\u0026#39; due to unhandled exception! 下面就解决这一类常见的打包模块缺失问题\n方法一：简单暴力，缺失补啥 提示缺失哪个模块就手动导入\n在程序入口处，或者根据个人喜好单独建个文件也好，将 import xxx 写进去，比如我上面的 test.py缺失 flask 模块，就 写 import flask。\n方法二：隐式导入 又分两种使用方式\n命令行模式：添加隐式导入参数 \u0026ndash;hidden-import MODULENAME1\n例如：\n1 pyinstaller test.py --hidden-import flask 如果缺失多个模块，可以多次使用\n例如缺失 docx、pillow 两个模块：\n1 --hidden-import docx --hidden-import Pillow 然后你可以将该打包命令成 脚本文件 比如 build.sh 或者 build.bat\n修改 spec 模式： 打开你上次执行完pyinstaller后生成的 xxx.spec 文件(我的test.spec)，修改以下字段，在列表里补充上缺失的模块\n例：\n1 hiddenimports=[\u0026#39;flask\u0026#39;], 例：\n1 hiddenimports=[\u0026#39;docx\u0026#39;, \u0026#39;Pillow\u0026#39;] 然后将可以将该spec 文件保存，提交到 git 😀\n====================================\n以上两种方法只能解决一小部分情况下包缺失的问题，还有问题，就得用第三种方法了。\n假设有这种多层路径的模块动态导入（通常是你安装的第三方包里面的乱七八糟的运行时动态导入）\n比如有个包叫 paho-mqtt（mqtt协议客户端），我在使用的时候需要，这样导入模块\n1 2 3 4 # 注意 # client 是个模块，模块就是单个 py 文件 # 而 Client 是个 class 类 from paho.mqtt.client import Client 改成动态导入语句就是\n1 Client = __import__(\u0026#39;paho.mqtt.client\u0026#39;, fromlist=[\u0026#39;\u0026#39;]).Client 打包后会提示如下信息\n1 ModuleNotFoundError: No module named \u0026#39;paho\u0026#39; 那你写个 import paho 能解决问题吗？写 import paho.mqtt 也不行啊，然后你写完发现还是不行，也许根据错误提示你最终会写上 import paho.mqtt.client，问题才得以解决，这就比较蛋疼了，况且如果缺失多个包呢？\n方法三：hook文件（待完善） 这时我们需要写个 hook 文件，将缺失的整个包全部包含进去就好了。\n步骤：\n在你执行打包命令的目录下新建一个py文件，比如 hook-ctypes.macholib.py\n写入以下内容\n1 2 3 4 5 from PyInstaller.utils.hooks import copy_metadata datas = copy_metadata(\u0026#39;flask\u0026#39;) + copy_metadata(\u0026#39;paho_mqtt\u0026#39;) # 如果缺失多个就用加号继续追加 # 名称是你pip安装时的名称 然后执行命令时再加个 --additional-hooks-dir参数\n1 pyinstaller test.py --additional-hooks-dir=. 应该就没问题了\n如果还是不行。。。。欢迎评论区提问，我再好好研究研究\n2. 打包静态文件 当你程序中需要读取一些静态文件比如 setting.json ,就需要你将他们一起打包进去，否则 pyinstaller 也会忽略他们。\n命令行模式 可以多次使用\n1 --add-data \u0026lt;SRC;DEST or SRC:DEST\u0026gt; 👉 注意：格式为 一个原文件名和目标文件夹名！，中间用一个分号或者冒号分割。\n👉 注意：路径中需要用 双反斜杠！！\n例：\n将原路径（绝对路径或者相对路径都可）setting 目录下的 aaa.json 文件打包到目标的 setting 文件夹下\n1 --add-data \u0026#34;.\\\\setting\\\\aaa.json;.\\\\setting\u0026#34; 例：\n将原路径（绝对路径或者相对路径都可）config 目录下的所有文件 文件打包到目标的 config 文件夹下\n1 --add-data \u0026#34;.\\\\config\\\\*;.\\\\config\u0026#34; 修改 spec 模式 1 2 datas=[(\u0026#39;.\\\\config\\\\*\u0026#39;, \u0026#39;.\\\\config\u0026#39;), (\u0026#39;.\\\\setting\\\\aaa.json\u0026#39;, \u0026#39;\\\\setting\u0026#39;) ], 💊 注意事项 将文件打包成单文件时（但文件夹除外），你在程序中访问这些静态文件时切勿使用相对路径，因为单文件在运行时先解压所有需要的文件到 用户临时文件夹下（ /temp 目录），并不在你当前运行路径下寻找这些静态文件。你需要将程序里访问这些静态文件的路径改为绝对路径，才能在其解压出的临时路径下找到他们：\n1 2 3 4 5 6 7 if getattr(sys, \u0026#39;frozen\u0026#39;, None): basedir = sys._MEIPASS else: basedir = os.path.dirname(__file__) #接上例，打包进去的 aaa.json，加到了这个绝对目录。 aaa = os.path.join(basedir, \u0026#39;setting\u0026#39;, \u0026#39;aaa.json\u0026#39;) 👉 效果参考：\nhttps://blog.csdn.net/Iv_zzy/article/details/107407167\n3. 打包二进制依赖文件 将程序中依赖的 dll 或者 so 文件 一起打包进去。（编译好的 python 可调用模块或者 ctypes 加载的 dll 文件都可）\n\u0026ndash;add-binary \u0026lt;SRC;DEST or SRC:DEST\u0026gt;\n可以多次使用；参数格式参考上一小标题（打包静态文件）\n例：\n1 --add-binary D:\\\\test\\\\pack\\\\HCNetSDK.dll;.\\\\lib 4. 加密字节码 详细介绍：https://pyinstaller.readthedocs.io/en/stable/usage.html#encrypting-python-bytecode\n使用 \u0026ndash;key 参数 指定一个长度为 16 的字符串，来加密 python 字节码文件， 你需要先执行以下命令\n1 pip install pyinstaller[encryption] 然后\n1 pyinstaller.exe --key=xxxx -F hellow.py （🙏 过程可能不太顺利，过程慢长，打包完文件也较大）\n","permalink":"https://ktzxy.top/posts/roul9l3r6j/","summary":"Pyinstaller 打包常见用法和问题","title":"Pyinstaller 打包常见用法和问题"},{"content":"Go的流程控制 流程控制是每种编程语言控制逻辑走向和执行次序的重要部分，流程控制可以说是一门语言的“经脉\u0026quot;\nGo 语言中最常用的流程控制有if和for，而switch和goto主要是为了简化代码、降低重复代码而生的结构，属于扩展类的流程控制。\nif else 推荐if后面不适用括号，当然也可以使用括号括起来\n1 2 3 4 5 6 7 8 9 10 func main() { var num = 10 if num == 10 { fmt.Println(\u0026#34;hello == 10\u0026#34;) } else if(num \u0026gt; 10) { fmt.Println(\u0026#34;hello \u0026gt; 10\u0026#34;) } else { fmt.Println(\u0026#34;hello \u0026lt; 10\u0026#34;) } } if的另外一种写法，下面的方法的区别是 num2是局部变量\n1 2 3 if num2:= 10; num2\u0026gt;=10 { fmt.Println(\u0026#34;hello \u0026gt;=10\u0026#34;) } for 循环结构 Go语言中的所有循环类型均可使用for关键字来完成\nfor循环的基本格式如下：\n1 2 3 for 初始语句; 条件表达式; 结束语句 { 循环体 } 条件表达式返回true时循环体不停地进行循环，直到条件表达式返回false时自动退出循环\n实例：打印1 ~ 10\n1 2 3 for i := 0; i \u0026lt; 10; i++ { fmt.Printf(\u0026#34;%v \u0026#34;, i+1) } 注意，在Go语言中，没有while语句，我们可以通过for来代替\n1 2 3 for { 循环体 } for循环可以通过break、goto、return、panic语句退出循环\nfor range（键值循环） Go 语言中可以使用for range遍历数组、切片、字符串、map及通道（channel）。通过for range遍历的返回值有以下规律：\n数组、切片、字符串返回索引和值。 map返回键和值。 通道（channel）只返回通道内的值。 实例：遍历字符串\n1 2 3 4 var str = \u0026#34;你好golang\u0026#34; for key, value := range str { fmt.Printf(\u0026#34;%v - %c \u0026#34;, key, value) } 遍历切片（数组）\n1 2 3 4 var array = []string{\u0026#34;php\u0026#34;, \u0026#34;java\u0026#34;, \u0026#34;node\u0026#34;, \u0026#34;golang\u0026#34;} for index, value := range array { fmt.Printf(\u0026#34;%v %s \u0026#34;, index, value) } switch case 使用switch语句可方便的对大量的值进行条件判断\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 extname := \u0026#34;.a\u0026#34; switch extname { case \u0026#34;.html\u0026#34;: { fmt.Println(\u0026#34;.html\u0026#34;) break } case \u0026#34;.doc\u0026#34;: { fmt.Println(\u0026#34;.doc\u0026#34;) break } case \u0026#34;.js\u0026#34;: { fmt.Println(\u0026#34;.js\u0026#34;) } default: { fmt.Println(\u0026#34;其它后缀\u0026#34;) } } switch的另外一种写法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 switch extname := \u0026#34;.a\u0026#34;; extname { case \u0026#34;.html\u0026#34;: { fmt.Println(\u0026#34;.html\u0026#34;) break } case \u0026#34;.doc\u0026#34;: { fmt.Println(\u0026#34;.doc\u0026#34;) break } case \u0026#34;.js\u0026#34;: { fmt.Println(\u0026#34;.js\u0026#34;) } default: { fmt.Println(\u0026#34;其它后缀\u0026#34;) } } 同时一个分支可以有多个值\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 extname := \u0026#34;.txt\u0026#34; switch extname { case \u0026#34;.html\u0026#34;: { fmt.Println(\u0026#34;.html\u0026#34;) break } case \u0026#34;.txt\u0026#34;,\u0026#34;.doc\u0026#34;: { fmt.Println(\u0026#34;传递来的是文档\u0026#34;) break } case \u0026#34;.js\u0026#34;: { fmt.Println(\u0026#34;.js\u0026#34;) } default: { fmt.Println(\u0026#34;其它后缀\u0026#34;) } } tip：在golang中，break可以不写，也能够跳出case，而不会执行其它的。\n如果我们需要使用switch的穿透 fallthrought，fallthrough语法可以执行满足条件的 case 的下一个case，为了兼容c语言中的case设计\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 extname := \u0026#34;.txt\u0026#34; switch extname { case \u0026#34;.html\u0026#34;: { fmt.Println(\u0026#34;.html\u0026#34;) fallthrought } case \u0026#34;.txt\u0026#34;,\u0026#34;.doc\u0026#34;: { fmt.Println(\u0026#34;传递来的是文档\u0026#34;) fallthrought } case \u0026#34;.js\u0026#34;: { fmt.Println(\u0026#34;.js\u0026#34;) fallthrought } default: { fmt.Println(\u0026#34;其它后缀\u0026#34;) } } fallthrought 只能穿透紧挨着的一层，不会一直穿透，但是如果每一层都写的话，就会导致每一层都进行穿透\nbreak：跳出循环 Go语言中break 语句用于以下几个方面：\n用于循环语句中跳出循环，并开始执行循环之后的语句。 break在switch（开关语句）中在执行一条case后跳出语句的作用。 在多重循环中，可以用标号label标出想break的循环。 1 2 3 4 5 6 7 8 9 var i = 0 for { if i == 10{ fmt.Println(\u0026#34;跳出循环\u0026#34;) break } i++ fmt.Println(i) } go：跳转到指定标签 goto 语句通过标签进行代码间的无条件跳转。goto 语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto语句能简化一些代码的实现过程。\n1 2 3 4 5 6 7 8 9 10 11 12 var n = 20 if n \u0026gt; 24 { fmt.Println(\u0026#34;成年人\u0026#34;) } else { goto lable3 } fmt.Println(\u0026#34;aaa\u0026#34;) fmt.Println(\u0026#34;bbb\u0026#34;) lable3: fmt.Println(\u0026#34;ccc\u0026#34;) fmt.Println(\u0026#34;ddd\u0026#34;) ","permalink":"https://ktzxy.top/posts/n25sexz31t/","summary":"5 Go的流程控制","title":"5 Go的流程控制"},{"content":"实施工程师技能体系全维度罗列（含技术栈与发展路线延伸） 一、操作系统 1. Linux 常用基础命令 文件管理\nls、cd、pwd、mkdir、rm、cp、mv、touch 文件内容操作\ncat、more、less、tail、head、grep 权限管理\nchmod、chown、chgrp 进程与系统监控\nps、top、htop、kill、df、du、free 网络相关\nifconfig/ip、netstat、ss、ping、traceroute 软件包管理\napt/yum/dnf、rpm、dpkg 用户与用户组管理 用户创建与删除：useradd、userdel、passwd 用户组管理：groupadd、groupdel、gpasswd 服务与进程管理 服务控制：systemctl、service 定时任务：crontab 日志分析 日志查看：journalctl、dmesg、logrotate 一、性能调优基础\n1.1 核心性能指标与工具\n组件 关键指标 正常范围参考 异常表现 CPU us/sy/id/wa us+sy \u0026lt; 70%；load \u0026lt; CPU核心数 进程卡顿、响应延迟、wa持续过高 内存 已用内存、swap使用率 swap used \u0026lt; 20% OOM、频繁使用swap 磁盘I/O 读写吞吐量、await、avgqu-sz await \u0026lt; 10ms；avgqu-sz \u0026lt; 2 磁盘读写卡顿、应用加载缓慢 网络 带宽、连接数、延迟 无明确标准 连接超时、数据包丢失 1.2 常用性能监控工具\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # CPU监控 top -c htop vmstat 1 5 # 内存监控 free -h vmstat -s # 磁盘I/O监控 iostat -x 1 5 iotop -o sar -d 1 5 # 网络监控 iftop nethogs netstat -s ss -s 二、CPU 优化 2.1优化方案 1.限制高耗CPU进程\n1 2 3 4 5 6 7 8 # 限制java进程CPU使用率（最多50%） cpulimit -p $(pgrep java) -l 50 # 通过cgroups限制进程组CPU mkdir /sys/fs/cgroup/cpu/mygroup echo 50000 \u0026gt; /sys/fs/cgroup/cpu/mygroup/cpu.cfs_period_us echo 50000 \u0026gt; /sys/fs/cgroup/cpu/mygroup/cpu.cfs_quota_us echo \u0026lt;PID\u0026gt; \u0026gt; /sys/fs/cgroup/cpu/mygroup/tasks 2.进程亲和性配置\n1 2 3 4 5 # 将Nginx绑定到CPU 0-3核心 taskset -cp 0-3 $(pgrep nginx) # 查看进程CPU亲和性 taskset -p \u0026lt;PID\u0026gt; 三、内存优化 3.1 优化方案 1.调整swappiness参数\n1 2 3 4 5 6 # 临时生效（重启失效） sysctl vm.swappiness=10 # 永久生效 echo \u0026#34;vm.swappiness=10\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf sysctl -p 2.启用大页内存（Huge Pages）\n1 2 3 4 5 6 # 临时设置256个大页 sysctl vm.nr_hugepages=256 # 永久生效 echo \u0026#34;vm.nr_hugepages=256\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf sysctl -p 四、磁盘 I/O 优化\n4.1优化方案 1.选择合适的I/O调度器\n1 2 3 4 5 6 7 8 9 10 11 # 查看当前调度器 cat /sys/block/sda/queue/scheduler # SSD使用noop echo \u0026#34;none\u0026#34; \u0026gt; /sys/block/sda/queue/scheduler # HDD使用deadline echo \u0026#34;deadline\u0026#34; \u0026gt; /sys/block/sda/queue/scheduler # 混合负载使用mq-deadline echo \u0026#34;mq-deadline\u0026#34; \u0026gt; /sys/block/sda/queue/scheduler 2.文件系统挂载优化\n1 2 3 4 5 # 编辑/etc/fstab（SSD适用） /dev/sda1 / ext4 defaults,noatime,discard 0 0 # 重新挂载 mount -o remount /dev/sda1 五、网络优化 5.1优化方案 1.调整内核网络参数\n1 2 3 4 5 6 7 8 9 10 11 # 增大TCP监听队列 sysctl -w net.core.somaxconn=65535 # 增大SYN队列 sysctl -w net.ipv4.tcp_max_syn_backlog=65535 # 允许复用TIME_WAIT端口 sysctl -w net.ipv4.tcp_tw_reuse=1 # 优化TCP拥塞控制 sysctl -w net.ipv4.tcp_congestion_control=htcp 2.优化网络缓冲区\n1 2 3 4 5 6 7 8 # 增加接收/发送缓冲区 sysctl -w net.core.rmem_max=16777216 sysctl -w net.core.wmem_max=16777216 # 永久生效 echo \u0026#34;net.core.rmem_max=16777216\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf echo \u0026#34;net.core.wmem_max=16777216\u0026#34; \u0026gt;\u0026gt; /etc/sysctl.conf sysctl -p 六、系统服务优化 6.1精简系统服务\n1 2 3 4 5 6 # 关闭蓝牙服务 systemctl stop bluetooth systemctl disable bluetooth # 常见可关闭服务（/etc/systemd/system/multi-user.target.wants/） bluetooth cups postfix avahi-daemon 6.2调整ulimit设置\n1 2 3 4 5 # 永久生效（/etc/security/limits.conf） echo \u0026#34;* soft nofile 65535\u0026#34; \u0026gt;\u0026gt; /etc/security/limits.conf echo \u0026#34;* hard nofile 65535\u0026#34; \u0026gt;\u0026gt; /etc/security/limits.conf echo \u0026#34;* soft nproc 65535\u0026#34; \u0026gt;\u0026gt; /etc/security/limits.conf echo \u0026#34;* hard nproc 65535\u0026#34; \u0026gt;\u0026gt; /etc/security/limits.conf 6.3使用tuned自动优化\n1 2 3 4 5 # 安装并启用 yum install tuned -y tuned-adm profile throughput-performance systemctl start tuned systemctl enable tuned 七.行业最佳实践\n1.数据库服务器优化\n1 2 3 4 5 6 7 # /etc/sysctl.conf vm.swappiness=10 vm.dirty_ratio=10 vm.dirty_background_ratio=5 net.core.somaxconn=65535 net.ipv4.tcp_tw_reuse=1 sysctl -p 2.Web服务器优化\n1 2 3 4 5 6 # /etc/sysctl.conf net.core.somaxconn=65535 net.ipv4.tcp_max_syn_backlog=65535 net.ipv4.tcp_tw_reuse=1 net.ipv4.tcp_fin_timeout=15 sysctl -p 优化前基准测试\n1 sar -u -d -n DEV 1 10 \u0026gt; baseline.log 优化后验证\n1 2 3 vmstat 1 5 iostat -x 1 5 netstat -s 2. Windows Server 1. 服务器角色配置 角色 关键配置项 典型应用场景 IIS - 安装勾选：ASP.NET、HTTP Redirection- 绑定配置：0.0.0.0:80- 应用池：.NET 4.0/5.0 部署 .NET Web 应用反向代理（Nginx → IIS） DNS - 创建正向/反向区域（example.com）- 配置 A 记录（www.example.com → 192.168.1.10）- 区域传输限制 内网域名解析负载均衡（CNAME 别名） DHCP - 作用域：192.168.1.100-192.168.1.200- 网关/DNS 设置- 租期：24 小时 办公网 IP 自动分配移动设备接入 AD 域控 - 提升域控制器：Install-ADDSDomainController- OU 结构（Finance/HR/IT）- GPO 链接配置 统一身份认证域内资源权限管控 行业实践\n金融：启用 LAPS 保护本地管理员密码 医疗：配置 HIPAA_Compliance 组强制 MFA 2. PowerShell 脚本编写 1 2 3 4 5 6 7 8 9 10 11 12 # 对象操作：导出 AD 用户邮箱 Get-ADUser -Filter * -Properties Email | Select-Object Name, Email | Export-Csv users.csv # 管道符：监控高 CPU 进程 Get-Process | Where-Object {$_.CPU -gt 50} | Sort-Object -Property CPU -Descending # 模块管理：安装 Azure 模块 Install-Module -Name Az -Scope AllUsers -Force Import-Module Az # 远程执行：检查域控服务 Invoke-Command -ComputerName DC01 -ScriptBlock {Get-Service -Name \u0026#34;Spooler\u0026#34;} 安全规范\n1 Set-ExecutionPolicy RemoteSigned -Scope CurrentUser # 仅允许本地脚本 禁止使用 Invoke-Expression 避免安全风险\n组策略（GPO）配置与调试\n策略项 配置路径 业务价值 账户策略 计算机配置 → Windows 设置 → 安全设置 → 账户策略 强制密码长度≥12天过期 软件设置 用户配置 → 策略 → 软件设置 → 部署应用 自动安装 Office 365 脚本执行 计算机配置 → 策略 → Windows 设置 → 脚本 启动自动清理临时文件（cleanmgr /sagerun:1） 安全设置 计算机配置 → Windows 设置 → 安全设置 禁用 USB 读写（Removable Storage Access） 1 2 3 4 5 6 7 8 9 10 # 强制刷新策略 gpupdate /force # 生成策略报告 gpresult /r # 常见故障排查 # 1. 策略未生效 → 检查 OU 链接层级 # 2. 策略冲突 → 启用策略应用顺序 # 3. 更新延迟 → 用 /force 强制刷新 Windows Server性能优化详细操作命令 一、网络优化 1.RSS (接收侧缩放) 配置\n1 2 3 4 5 6 7 8 9 10 11 # 启用RSS Set-NetAdapterRss -Name \u0026#34;Ethernet\u0026#34; -Enabled $true # 查看当前RSS配置 Get-NetAdapterRss # 设置RSS队列数量（示例：设置为4个队列） Set-NetAdapterRss -Name \u0026#34;Ethernet\u0026#34; -QueueCount 4 # 查看可用的RSS配置文件 Get-NetAdapterRss -Name \u0026#34;Ethernet\u0026#34; | Format-List 2.TCP/IP参数优化\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 启用TCP窗口自动调优 netsh int tcp set global autotuninglevel=normal # 禁用Nagle算法（延迟敏感应用） netsh int tcp set global nonsack=disabled netsh int tcp set global ecncapability=disabled # 设置TCP窗口大小 netsh int tcp set global TcpWindowSize=16777216 # 配置注册表参数（需重启） New-ItemProperty -Path \u0026#34;HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\u0026#34; -Name \u0026#34;EnableTCPA\u0026#34; -Value 1 -PropertyType DWord -Force New-ItemProperty -Path \u0026#34;HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\u0026#34; -Name \u0026#34;EnableRSS\u0026#34; -Value 1 -PropertyType DWord -Force New-ItemProperty -Path \u0026#34;HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\u0026#34; -Name \u0026#34;EnablePMTUDiscovery\u0026#34; -Value 1 -PropertyType DWord -Force New-ItemProperty -Path \u0026#34;HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\u0026#34; -Name \u0026#34;TcpWindowSize\u0026#34; -Value 0x00FFFFFF -PropertyType DWord -Force New-ItemProperty -Path \u0026#34;HKLM:\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\u0026#34; -Name \u0026#34;GlobalMaxTcpWindowSize\u0026#34; -Value 0x00FFFFFF -PropertyType DWord -Force 3.低延迟网络优化\n1 2 3 4 5 6 7 # 设置高性能电源计划 powercfg /setactive 8c5e7fda-e8d8-4e4e-a569-a44231632534 # 启用UDP校验和、TCP校验和和发送大型卸载(LSO) Set-NetAdapterAdvancedProperty -Name \u0026#34;Ethernet\u0026#34; -DisplayName \u0026#34;UDP Checksum Offload\u0026#34; -DisplayValue \u0026#34;Enabled\u0026#34; Set-NetAdapterAdvancedProperty -Name \u0026#34;Ethernet\u0026#34; -DisplayName \u0026#34;TCP Checksum Offload\u0026#34; -DisplayValue \u0026#34;Enabled\u0026#34; Set-NetAdapterAdvancedProperty -Name \u0026#34;Ethernet\u0026#34; -DisplayName \u0026#34;Large Send Offload (IPv4)\u0026#34; -DisplayValue \u0026#34;Enabled\u0026#34; 二、系统性能优化\n1.虚拟内存设置\n1 2 3 4 # 设置固定大小的分页文件（示例：4096MB） $vm = Get-WmiObject -Class Win32_PageFileSetting $vm.SetInitialSize(4096) $vm.SetMaximumSize(4096) 2.电源计划优化\n1 2 3 4 5 # 设置高性能电源计划 powercfg /setactive 8c5e7fda-e8d8-4e4e-a569-a44231632534 # 检查当前电源计划 powercfg /getactivescheme 3.系统缓存优化\n1 2 3 4 5 # 禁用系统缓存 Set-ItemProperty -Path \u0026#34;HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Memory Management\u0026#34; -Name \u0026#34;DisablePagingExecutive\u0026#34; -Value 1 # 配置系统缓存大小（示例：1GB） Set-ItemProperty -Path \u0026#34;HKLM:\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Memory Management\u0026#34; -Name \u0026#34;SystemPages\u0026#34; -Value 1024 三、DNS优化 1.DNS缓存优化\n1 2 3 4 5 6 7 8 # 设置DNS缓存时间 Set-DnsServerCache -MaxCacheTTL 86400 -NegativeCacheTTL 300 # 清除DNS缓存 Clear-DnsServerCache # 添加DNS转发器 Add-DnsServerForwarder -IPAddress 223.5.5.5, 119.29.29.29 2.DNS安全优化\n1 2 3 4 5 # 启用DNSSEC Set-DnsServerDnsSecZoneSetting -ZoneName \u0026#34;example.com\u0026#34; -EnableDnsSec $true # 设置DNS响应速率限制 Set-DnsServerResourceRecord -Name \u0026#34;@\u0026#34; -ZoneName \u0026#34;example.com\u0026#34; -Type SOA -NewResourceRecordData @{MinimumTTL=300; Refresh=1800; Retry=300; Expire=604800; Serial=1} 四、存储优化 1.SSD存储优化\n1 2 3 4 5 6 7 8 # 禁用SSD碎片整理 Disable-ScheduledTask -TaskName \u0026#34;\\Microsoft\\Windows\\Defrag\\ScheduledDefrag\u0026#34; # 启用TRIM Optimize-Volume -DriveLetter C -ReTrim # 为SSD配置适当的写入缓存 Set-Disk -Number 0 -WriteCacheEnabled $true 2.ReFS文件系统优化\n1 2 3 4 5 # 创建ReFS文件系统卷 New-Volume -FileSystem ReFS -DriveLetter D -Size 100GB # 启用ReFS的连续文件支持 Set-Volume -DriveLetter D -FileSystemLabel \u0026#34;Data\u0026#34; -AllocationUnitSize 64KB 五、性能监控与验证 1.性能监控命令\n1 2 3 4 5 6 7 8 9 10 11 # 启动实时性能监控 Get-Counter -Counter \u0026#34;\\Processor(*)\\% Processor Time\u0026#34;, \u0026#34;\\Memory\\Available MBytes\u0026#34;, \u0026#34;\\Network Interface(*)\\Bytes Total/sec\u0026#34;, \u0026#34;\\TCPv4\\Connections Established\u0026#34;, \u0026#34;\\PhysicalDisk(*)\\Disk Bytes/sec\u0026#34; -SampleInterval 2 -MaxSamples 10 # 查看网络适配器状态 Get-NetAdapter | Where-Object { $_.Status -eq \u0026#34;Up\u0026#34; } | Format-Table Name, InterfaceDescription, LinkSpeed # 查看TCP/IP设置 Get-NetTCPSetting # 查看RSS配置 Get-NetAdapterRss | Format-Table Name, Enabled, BaseProcessorGroup, MaxProcessorGroup 2.系统健康检查\n1 2 3 4 5 6 7 8 # 生成性能分析报告 powercfg /energy /report # 检查系统性能 perfmon /report # 查看系统资源使用情况 Get-Process | Sort-Object CPU -Descending | Select-Object -First 10 六、Hyper-V优化\n1.虚拟机网络优化\n1 2 3 4 5 6 7 8 # 启用SR-IOV Set-VMNetworkAdapter -VMName \u0026#34;VMName\u0026#34; -IovEnabled $true # 启用VMQ Set-VMNetworkAdapter -VMName \u0026#34;VMName\u0026#34; -VirtualMachineQueue $true # 配置虚拟机CPU亲和性 Set-VMProcessor -VMName \u0026#34;VMName\u0026#34; -Count 4 -Reserve 50 2.存储优化\n1 2 3 4 5 # 启用存储空间直通 Enable-VMHostFeature -FeatureName \u0026#34;StorageSpacesDirect\u0026#34; # 配置存储池 New-StoragePool -FriendlyName \u0026#34;DataPool\u0026#34; -StorageSubsystemFriendlyName \u0026#34;Windows Storage Spaces\u0026#34; -PhysicalDisks (Get-PhysicalDisk -CanPool $true) 七、Windows Server 2022特有优化\n1.网络子系统优化\n1 2 3 4 5 6 # 启用HTTP/2 Set-WebConfigurationProperty -PSPath \u0026#39;MACHINE/WEBROOT/APPHOST\u0026#39; -Filter \u0026#34;system.webServer/httpProtocol\u0026#34; -Name \u0026#34;allowKeepAlive\u0026#34; -Value \u0026#34;true\u0026#34; Set-WebConfigurationProperty -PSPath \u0026#39;MACHINE/WEBROOT/APPHOST\u0026#39; -Filter \u0026#34;system.webServer/httpProtocol\u0026#34; -Name \u0026#34;http2\u0026#34; -Value \u0026#34;true\u0026#34; # 启用TCP快速打开 Set-NetTCPSetting -SettingName \u0026#34;Internet\u0026#34; -TcpFastOpen $true 2.软件定义网络(SDN)优化\n1 2 3 4 5 # 配置HNV（主机网络虚拟化） New-HnvNetwork -Name \u0026#34;ExternalNetwork\u0026#34; -Type External -Adapter \u0026#34;Ethernet\u0026#34; # 配置SLB（软件负载均衡） New-SlbLoadBalancer -Name \u0026#34;LB01\u0026#34; -Network \u0026#34;ExternalNetwork\u0026#34; -FrontEndPort 80 -BackEndPort 80 -Protocol TCP 💡 重要提示：执行优化命令前，请先备份系统并创建还原点。某些优化可能需要重启服务器才能生效。建议在非高峰时段进行优化操作。\n八、验证优化效果\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 优化前性能基线 $baseline = Get-Counter -Counter \u0026#34;\\Processor(*)\\% Processor Time\u0026#34;, \u0026#34;\\Memory\\Available MBytes\u0026#34;, \u0026#34;\\Network Interface(*)\\Bytes Total/sec\u0026#34; -SampleInterval 10 -MaxSamples 5 # 优化后性能对比 $optimized = Get-Counter -Counter \u0026#34;\\Processor(*)\\% Processor Time\u0026#34;, \u0026#34;\\Memory\\Available MBytes\u0026#34;, \u0026#34;\\Network Interface(*)\\Bytes Total/sec\u0026#34; -SampleInterval 10 -MaxSamples 5 # 比较优化效果 $baseline.CounterSamples | ForEach-Object { $optimizedValue = $optimized.CounterSamples | Where-Object { $_.Path -eq $_.Path } [PSCustomObject]@{ Counter = $_.Path Baseline = $_.CookedValue Optimized = $optimizedValue.CookedValue Improvement = [Math]::Round((($_.CookedValue - $optimizedValue.CookedValue) / $_.CookedValue) * 100, 2) + \u0026#34;%\u0026#34; } } 二、网络基础 TCP/IP协议栈 一、子网划分（CIDR、VLSM）\n1.1 基础概念\nCIDR (Classless Inter-Domain Routing)\n无类别域间路由，取代了传统的A/B/C类IP地址划分 格式：IP地址/前缀长度（如192.168.1.0/24） 前缀长度表示网络部分的位数（/24表示前24位是网络部分） VLSM (Variable Length Subnet Mask)\n可变长子网掩码，允许在同一个网络中使用不同长度的子网掩码 解决了传统子网划分中IP地址浪费的问题 1.2 子网划分原理\n原始地址 子网掩码 网络地址 可用主机 子网数 192.168.1.0/24 255.255.255.0 192.168.1.0 254 1 192.168.1.0/26 255.255.255.192 192.168.1.0 62 4 192.168.1.0/28 255.255.255.240 192.168.1.0 14 16 1.3 实际配置示例\n在Linux中配置子网\n1 2 3 4 5 6 7 8 # 为eth0配置IP地址192.168.1.10/26 ip addr add 192.168.1.10/26 dev eth0 # 查看网络配置 ip addr show eth0 # 通过路由表添加默认网关 ip route add default via 192.168.1.1 在Windows中配置子网\n1 2 # 使用PowerShell配置IP地址 New-NetIPAddress -InterfaceAlias \u0026#34;Ethernet\u0026#34; -IPAddress 192.168.1.10 -PrefixLength 26 -DefaultGateway 192.168.1.1 💡 关键提示：CIDR和VLSM是现代网络设计的基础，可以大幅提高IP地址利用率。例如，一个/24网络可以划分为多个/26子网，每个子网提供62个可用IP地址，避免了传统/24网络中254个地址的浪费。\n二、DNS解析原理与配置 2.1 DNS解析原理 DNS工作流程：\n用户输入域名（如www.example.com） 本地DNS客户端查询本地DNS缓存 若缓存未命中，查询本地DNS服务器 DNS服务器查询根域名服务器 根域名服务器返回顶级域名（TLD）服务器地址 TLD服务器返回权威域名服务器地址 权威域名服务器返回IP地址 DNS客户端缓存结果并返回给应用程序 2.2 DNS服务器配置 2.2.1 BIND 配置（Linux）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # 安装BIND sudo apt-get install bind9 # 主配置文件：/etc/bind/named.conf options { directory \u0026#34;/var/cache/bind\u0026#34;; recursion yes; allow-query { any; }; }; # 区域配置：/etc/bind/named.conf.local zone \u0026#34;example.com\u0026#34; { type master; file \u0026#34;/etc/bind/db.example.com\u0026#34;; }; # 正向区域文件：/etc/bind/db.example.com $TTL 86400 @ IN SOA ns1.example.com. admin.example.com. ( 2023010101 ; Serial 3600 ; Refresh 1800 ; Retry 604800 ; Expire 86400 ; Minimum TTL ) @ IN NS ns1.example.com. ns1 IN A 192.168.1.10 www IN A 192.168.1.11 2.2.2 PowerDNS 配置\n1 2 3 4 5 6 7 8 9 # 安装PowerDNS sudo apt-get install pdns-server pdns-backend-mysql # 配置文件：/etc/powerdns/pdns.conf launch=gmysql gmysql-host=localhost gmysql-user=pdns gmysql-password=yourpassword gmysql-dbname=pdns 2.2.3 DNS客户端配置\nLinux系统\n1 2 3 # 编辑/etc/resolv.conf nameserver 192.168.1.10 nameserver 8.8.8.8 Windows系统\n进入\u0026quot;网络和共享中心\u0026quot; → \u0026ldquo;本地连接\u0026rdquo; → \u0026ldquo;属性\u0026rdquo; 选择\u0026quot;Internet协议版本4 (TCP/IPv4)\u0026quot; → \u0026ldquo;属性\u0026rdquo; 选择\u0026quot;使用下面的DNS服务器地址\u0026quot; 设置首选DNS服务器和备用DNS服务器 💡 行业实践：根据华为云文档[1]，在云环境中，通常使用云提供商提供的DNS服务器（如100.125.1.250），并建议锁定/etc/resolv.conf文件以防止配置被重置：chattr +i /etc/resolv.conf\n三、VLAN与Trunk配置 3.1 VLAN与Trunk原理 VLAN (Virtual Local Area Network)\n逻辑上将物理网络划分为多个广播域\n增强网络安全性，限制广播域\n灵活构建虚拟工作组\nTrunk (中继)\n用于交换机之间传输多个VLAN的流量 使用IEEE 802.1Q标准添加VLAN标签 允许单条物理链路承载多个VLAN 3.2 VLAN与Trunk配置示例 3.2.1 基于华为设备的配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 创建VLAN 100 [Switch] vlan 100 [Switch-vlan100] name Marketing [Switch-vlan100] quit # 配置接口为Access模式（连接终端设备） [Switch] interface GigabitEthernet 0/0/1 [Switch-GigabitEthernet0/0/1] port link-type access [Switch-GigabitEthernet0/0/1] port default vlan 100 [Switch-GigabitEthernet0/0/1] quit # 配置接口为Trunk模式（交换机间连接） [Switch] interface GigabitEthernet 0/0/24 [Switch-GigabitEthernet0/0/24] port link-type trunk [Switch-GigabitEthernet0/0/24] port trunk allow-pass vlan 100 200 300 [Switch-GigabitEthernet0/0/24] quit 3.2.2基于思科设备的配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 创建VLAN 100 Switch(config)# vlan 100 Switch(config-vlan)# name Sales Switch(config-vlan)# exit # 配置Access端口 Switch(config)# interface FastEthernet 0/1 Switch(config-if)# switchport mode access Switch(config-if)# switchport access vlan 100 Switch(config-if)# exit # 配置Trunk端口 Switch(config)# interface FastEthernet 0/24 Switch(config-if)# switchport mode trunk Switch(config-if)# switchport trunk allowed vlan 100,200,300 Switch(config-if)# exit 3.3 VLAN与IP子网划分\n1 2 3 4 5 6 7 8 # 基于IP子网划分VLAN（华为设备） [Switch] vlan 100 [Switch-vlan100] ip-subnet-vlan 1 ip 192.168.1.2 24 priority 2 [Switch-vlan100] quit [Switch] vlan 200 [Switch-vlan200] ip-subnet-vlan 1 ip 192.168.2.2 24 priority 3 [Switch-vlan200] quit 💡 行业最佳实践：根据华为云文档[1]和CSDN文章[4][6][7]，在企业网络中，建议：\n为不同部门或功能划分VLAN（如VLAN 10: 人事，VLAN 20: 财务） 使用Trunk端口连接交换机，允许多个VLAN通过 为VLAN配置IP子网，实现不同VLAN间的路由 配置Native VLAN（默认VLAN）为VLAN 1（华为设备需手动配置） 网络设备管理\n一、路由器/交换机配置\n1.1 Cisco IOS 配置\n1.1.1 基础配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 进入特权模式 Router\u0026gt; enable Router# # 进入全局配置模式 Router# configure terminal Router(config)# # 设置主机名 Router(config)# hostname R1 # 配置密码 Router(config)# enable secret cisco123 Router(config)# line vty 0 15 Router(config-line)# login Router(config-line)# password cisco123 Router(config-line)# exit # 配置管理IP地址 R1(config)# interface vlan 1 R1(config-if)# ip address 192.168.1.1 255.255.255.0 R1(config-if)# no shutdown R1(config-if)# exit # 配置默认网关 R1(config)# ip default-gateway 192.168.1.254 1.1.2 路由配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 静态路由 R1(config)# ip route 0.0.0.0 0.0.0.0 192.168.1.254 # RIP 路由协议 R1(config)# router rip R1(config-router)# version 2 R1(config-router)# network 192.168.1.0 R1(config-router)# network 10.0.0.0 R1(config-router)# exit # OSPF 路由协议 R1(config)# router ospf 1 R1(config-router)# router-id 1.1.1.1 R1(config-router)# network 192.168.1.0 0.0.0.255 area 0 R1(config-router)# network 10.0.0.0 0.0.0.255 area 0 1.1.3 交换机配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 创建VLAN Switch(config)# vlan 10 Switch(config-vlan)# name Sales Switch(config-vlan)# exit # 配置Access端口 Switch(config)# interface FastEthernet 0/1 Switch(config-if)# switchport mode access Switch(config-if)# switchport access vlan 10 Switch(config-if)# exit # 配置Trunk端口 Switch(config)# interface FastEthernet 0/24 Switch(config-if)# switchport mode trunk Switch(config-if)# switchport trunk allowed vlan 10,20,30 Switch(config-if)# exit 💡 关键提示：Cisco IOS配置中，show running-config查看当前配置，copy running-config startup-config保存配置。\n1.2 华为VRP 配置\n1.2.1 基础配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 进入系统视图 \u0026lt;Huawei\u0026gt; system-view [Huawei] # 设置主机名 [Huawei] sysname SW1 # 配置密码 [Huawei] user-interface vty 0 4 [Huawei-ui-vty0-4] authentication-mode password [Huawei-ui-vty0-4] set password cipher Huawei123 [Huawei-ui-vty0-4] quit # 配置管理IP地址 [SW1] interface Vlanif 1 [SW1-Vlanif1] ip address 192.168.1.1 255.255.255.0 [SW1-Vlanif1] quit # 配置默认网关 [SW1] ip route-static 0.0.0.0 0.0.0.0 192.168.1.254 1.2.2 路由配置\n1 2 3 4 5 6 7 8 9 10 # 静态路由 [SW1] ip route-static 0.0.0.0 0.0.0.0 192.168.1.254 # OSPF 路由协议 [SW1] ospf 1 [SW1-ospf-1] area 0 [SW1-ospf-1-area-0.0.0.0] network 192.168.1.0 0.0.0.255 [SW1-ospf-1-area-0.0.0.0] network 10.0.0.0 0.0.0.255 [SW1-ospf-1-area-0.0.0.0] quit [SW1-ospf-1] quit 1.2.3 VLAN配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 创建VLAN [SW1] vlan 10 [SW1-vlan10] description Sales [SW1-vlan10] quit # 配置Access端口 [SW1] interface GigabitEthernet 0/0/1 [SW1-GigabitEthernet0/0/1] port link-type access [SW1-GigabitEthernet0/0/1] port default vlan 10 [SW1-GigabitEthernet0/0/1] quit # 配置Trunk端口 [SW1] interface GigabitEthernet 0/0/24 [SW1-GigabitEthernet0/0/24] port link-type trunk [SW1-GigabitEthernet0/0/24] port trunk allow-pass vlan 10 20 30 [SW1-GigabitEthernet0/0/24] quit 💡 关键提示：华为VRP中，display current-configuration查看当前配置，save保存配置。\n二、防火墙规则 2.1 iptables/nftables 配置 2.1.1 iptables 基础\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 查看当前规则 iptables -L -n # 清空规则 iptables -F # 设置默认策略 iptables -P INPUT DROP iptables -P FORWARD DROP iptables -P OUTPUT ACCEPT # 允许本地回环 iptables -A INPUT -i lo -j ACCEPT # 允许已建立的连接 iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # 允许SSH iptables -A INPUT -p tcp --dport 22 -j ACCEPT # 允许HTTP/HTTPS iptables -A INPUT -p tcp --dport 80 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT # 拒绝所有其他入站流量 iptables -A INPUT -j DROP 2.1.2 iptables 高级配置\n1 2 3 4 5 6 # 限制SSH登录尝试 iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set --name SSH --rsource iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 5 --name SSH -j DROP # 端口转发 iptables -t nat -A PREROUTING -p tcp --dport 8080 -j DNAT --to-destination 192.168.1.10:80 2.1.3 nftables 配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 创建表 nft add table ip filter # 添加链 nft add chain ip filter input { type filter hook input priority 0 \\; } # 添加规则 nft add rule ip filter input ct state established,related accept nft add rule ip filter input iif lo accept nft add rule ip filter input tcp dport 22 accept nft add rule ip filter input tcp dport 80 accept nft add rule ip filter input tcp dport 443 accept nft add rule ip filter input drop 💡 关键提示：使用iptables-save保存iptables规则，nft list ruleset查看nftables规则。\n2.2 Windows Firewall 配置 2.2.1 通过PowerShell配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 查看当前防火墙规则 Get-NetFirewallRule | Format-Table Name,Enabled,Direction,Action # 启用远程桌面（RDP）规则 Enable-NetFirewallRule -DisplayGroup \u0026#34;Remote Desktop\u0026#34; # 添加入站规则（允许HTTP） New-NetFirewallRule -DisplayName \u0026#34;Allow HTTP\u0026#34; -Direction Inbound -Protocol TCP -LocalPort 80 -Action Allow # 添加出站规则（允许HTTPS） New-NetFirewallRule -DisplayName \u0026#34;Allow HTTPS\u0026#34; -Direction Outbound -Protocol TCP -LocalPort 443 -Action Allow # 禁用默认规则 Disable-NetFirewallRule -DisplayName \u0026#34;File and Printer Sharing (SMB-In)\u0026#34; 2.2.2 通过图形界面配置\n打开\u0026quot;控制面板\u0026quot; → \u0026ldquo;系统和安全\u0026rdquo; → \u0026ldquo;Windows Defender 防火墙\u0026rdquo;\n点击\u0026quot;高级设置\u0026quot;进入高级防火墙配置\n在\u0026quot;入站规则\u0026quot;中，右键\u0026quot;新建规则\u0026quot;：\n选择\u0026quot;端口\u0026quot; → \u0026ldquo;TCP\u0026rdquo; → \u0026ldquo;特定本地端口\u0026rdquo;：80\n选择\u0026quot;允许连接\u0026quot;\n选择配置文件（域、专用、公用）\n命名规则（如\u0026quot;HTTP\u0026quot;）\n2.3 通过组策略配置\n打开\u0026quot;组策略管理编辑器\u0026quot;（gpedit.msc）\n导航至：计算机配置 → Windows 设置 → 安全设置 → Windows 防火墙与高级安全\n配置入站/出站规则：\n右键\u0026quot;入站规则\u0026quot; → \u0026ldquo;新建规则\u0026rdquo;\n选择\u0026quot;端口\u0026quot; → \u0026ldquo;TCP\u0026rdquo; → \u0026ldquo;特定端口\u0026rdquo;：80\n选择\u0026quot;允许连接\u0026quot;\n选择配置文件\n命名规则\n💡 关键提示：Windows防火墙配置后，建议使用netsh firewall show config验证配置。\n三.行业最佳实践 3.1 网络设备管理最佳实践\n配置备份： Cisco: copy running-config tftp://192.168.1.100/cisco.cfg 华为: save cisco.cfg 访问控制： 仅允许管理IP地址访问设备 使用SSH代替Telnet 配置ACL限制管理访问 安全加固： 禁用未使用的服务（如HTTP、SNMP） 设置合理的密码策略 定期更新设备固件 3.2 防火墙管理最佳实践\n最小权限原则： 仅开放必要的端口 限制源IP地址范围 日志监控： 启用防火墙日志 定期分析防火墙日志 规则管理： 保持规则简洁 添加规则注释 定期清理过期规则 💡 行业案例：根据思科安全文档[1]，在企业网络中，建议将防火墙规则按功能分组（如Web服务、数据库、管理），并使用命名规则提高可读性。\n网络安全\n一、SSL/TLS证书部署与管理\n1.1 核心概念\nSSL/TLS协议原理\n通过非对称加密建立安全通道，保护数据传输过程 客户端验证服务器身份需满足：证书由可信CA签发、域名匹配、证书未过期 TLS 1.2/1.3 是目前最安全的广泛支持版本 Let\u0026rsquo;s Encrypt 优势\n免费自动化证书颁发 支持 ACME 协议自动续期 90 天有效期（需配置自动更新） 1.2 证书部署流程（以 Nginx 为例）\n步骤 1：安装 Certbot 工具\n1 2 3 4 5 # Ubuntu/Debian sudo apt install certbot python3-certbot-nginx # CentOS sudo yum install certbot python3-certbot-nginx 步骤 2：获取证书\n1 sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com 步骤 3：验证 Nginx 配置\n1 2 3 4 5 6 7 8 9 10 11 12 server { listen 443 ssl; server_name yourdomain.com; ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # 强制启用 TLS 1.2+ ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers \u0026#39;ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384\u0026#39;; } 步骤 4：HTTP 重定向到 HTTPS\n1 2 3 4 5 server { listen 80; server_name yourdomain.com; return 301 https://$host$request_uri; } 1.3 证书自动续期\n1 2 3 4 5 # 测试续期 sudo certbot renew --dry-run # 添加定时任务（每月执行） 0 0 1 * * /usr/bin/certbot renew --quiet 1.4 安全加固建议\nHSTS 头部\n1 add_header Strict-Transport-Security \u0026#34;max-age=31536000; includeSubDomains\u0026#34; always; 密钥轮换\n1 openssl ecparam -genkey -name secp384r1 | tee /etc/ssl/private/dhparam.pem 在 Nginx 配置中引用：\n1 ssl_dhparam /etc/ssl/private/dhparam.pem; 1.5云平台证书管理\nAWS Certificate Manager (ACM) 通过 AWS 控制台请求公有证书 自动部署到 CloudFront、ELB、API Gateway 免费、自动续期\nAWS SSL证书管理 - 免费SSL证书申请 - AWS云服务\n阿里云 SSL 证书管理\n支持 Let\u0026rsquo;s Encrypt 和商业证书 一键部署到 CDN、ECS、SLB 支持国密标准证书 二、SSH密钥认证与端口转发 2.1 SSH认证机制\n认证方式 安全性 适用场景 原理 密码认证 低 临时使用 明文密码经传输层加密 公钥认证 中高 生产环境 非对称加密验证身份 证书认证 高 企业级应用 基于CA签发的证书 公钥认证工作流程： 客户端生成密钥对（如 ed25519、RSA） 客户端将公钥上传到服务器的 ~/.ssh/authorized_keys 客户端发起认证：发送用户名→服务器返回\u0026quot;需要公钥认证\u0026quot; 客户端用私钥对\u0026quot;服务器随机生成的挑战字符串\u0026quot;签名 服务器验证签名→有效则认证通过 2.2 SSH密钥认证配置 生成密钥对\n1 2 3 ssh-keygen -t ed25519 -C \u0026#34;your_email@example.com\u0026#34; # 或 ssh-keygen -t rsa -b 4096 -C \u0026#34;your_email@example.com\u0026#34; 将公钥添加到服务器\n1 ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server 服务器端配置\n1 2 3 # 确保权限正确 chmod 700 ~/.ssh chmod 600 ~/.ssh/authorized_keys 2.3 SSH端口转发 正向转发（本地端口转发）\n1 2 ssh -L 8080:localhost:80 user@remote_server # 本地访问 http://localhost:8080 即可访问远程服务器的80端口 反向转发（远程端口转发）\n1 2 ssh -R 8080:localhost:80 user@remote_server # 远程服务器可访问 http://localhost:8080 访问本地80端口 动态端口转发（SOCKS代理）\n1 2 ssh -D 1080 user@remote_server # 配置浏览器使用 SOCKS5 代理：127.0.0.1:1080 2.4 SSH高级配置 配置文件 ~/.ssh/config\n1 2 3 4 5 6 7 Host dev-server HostName 192.168.1.10 User admin Port 2222 IdentityFile ~/.ssh/id_ed25519 ServerAliveInterval 60 TCPKeepAlive yes 保持持久连接\n1 2 3 # 在 /etc/ssh/sshd_config 中 ClientAliveInterval 60 ClientAliveCountMax 3 💡 关键提示：SSH 密钥认证比密码认证更安全，建议在生产环境中禁用密码认证（PasswordAuthentication no）\n三、IDS/IPS基础配置（Snort、Suricata） 3.1 IDS/IPS概述 IDS (Intrusion Detection System)：检测入侵行为，但不阻止 IPS (Intrusion Prevention System)：检测并阻止入侵行为\n特性 Snort Suricata 模式 仅 IDS IDS/IPS双模式 性能 较低 较高（多线程） 规则库 传统 现代化 社区支持 大 大 3.2 Snort 基础配置 安装 Snort\n1 2 3 4 5 # Ubuntu/Debian sudo apt install snort # CentOS sudo yum install snort 配置 Snort\n1 2 3 4 5 6 7 # 编辑配置文件 sudo nano /etc/snort/snort.conf # 修改以下关键设置 var HOME_NET 192.168.1.0/24 var EXTERNAL_NET any var RULE_PATH /etc/snort/rules 启动 Snort\n1 2 3 4 5 # 以入侵检测模式运行 sudo snort -i eth0 -c /etc/snort/snort.conf -l /var/log/snort # 以IPS模式运行（需要配置规则） sudo snort -i eth0 -c /etc/snort/snort.conf -A full 查看日志\n1 tail -f /var/log/snort/alert 3.3 Suricata 基础配置 安装 Suricata\n1 2 3 4 5 # Ubuntu/Debian sudo apt install suricata # CentOS sudo yum install suricata 配置 Suricata\n1 2 3 4 5 6 7 # 编辑配置文件 sudo nano /etc/suricata/suricata.yaml # 修改以下关键设置 home-net: 192.168.1.0/24 external-net: !192.168.1.0/24 default-rule-path: /etc/suricata/rules 启动 Suricata\n1 2 3 4 5 # 以入侵检测模式运行 sudo suricata -c /etc/suricata/suricata.yaml -i eth0 # 以IPS模式运行 sudo suricata -c /etc/suricata/suricata.yaml -i eth0 -l /var/log/suricata --af-packet 查看日志\n1 tail -f /var/log/suricata/fast.log 3.4 规则管理 更新规则库\n1 2 3 4 5 # Snort sudo snort -T -c /etc/snort/snort.conf # Suricata sudo suricata-update 添加自定义规则\n1 2 # 在 /etc/snort/rules/local.rules 中添加 alert tcp any any -\u0026gt; any any (msg:\u0026#34;Suspicious SSH Login\u0026#34;; content:\u0026#34;SSH\u0026#34;; sid:1000001; rev:1;) 3.5 优化配置 Snort 性能优化\n1 2 3 # /etc/snort/snort.conf preprocessor http_inspect: server, default, port 80, 443 preprocessor http_inspect: server, default, port 80, 443, memcap 2000000 Suricata 性能优化\n1 2 3 4 5 # /etc/suricata/suricata.yaml af-packet: - interface: eth0 threads: 4 buffer-size: 16777216 💡 关键提示：IDS/IPS 应部署在关键网络节点，如网关、核心交换机，以监控所有进出流量。建议定期更新规则库（每周一次），并根据实际网络环境调整规则。\n三、数据库管理 关系型数据库 MySQL/PostgreSQL\n备份恢复（mysqldump、pg_dump） mysqldump（MySQL）和 pg_dump（PostgreSQL）是各自数据库系统提供的逻辑备份工具，用于将数据库结构和数据导出为 SQL 脚本或自定义格式文件，便于在系统崩溃、误操作或迁移场景下快速恢复数据，保障业务连续性和数据安全。\n主从复制 主从复制是一种高可用与读写分离架构，主库（Master）负责处理写操作，并将变更日志（如 MySQL 的 binlog 或 PostgreSQL 的 WAL）实时同步到一个或多个从库（Slave/Replica），从库可承担读请求以分担主库压力，同时提供故障切换能力，提升系统可靠性与扩展性。\n索引优化 索引优化指通过合理设计、创建或调整数据库索引（如 B-tree、Hash、复合索引等），加速查询执行速度、减少全表扫描，从而显著提升应用性能；但需权衡索引对写入性能和存储空间的开销，避免过度索引导致维护成本上升。\nSQL Server\nT-SQL调试 T-SQL调试是指在SQL Server中对Transact-SQL脚本或存储过程进行逐步执行、断点设置、变量监视和错误定位的过程，用于排查逻辑错误、验证数据处理流程，确保代码按预期运行，提升开发效率与代码质量。\n事务管理 事务管理是通过BEGIN TRANSACTION、COMMIT和ROLLBACK等语句控制数据库操作的原子性、一致性、隔离性和持久性（ACID），确保一组相关操作要么全部成功提交，要么在发生错误时全部回滚，从而维护数据完整性和业务逻辑正确性。\nAlwaysOn高可用 AlwaysOn高可用（Availability Groups）是SQL Server提供的企业级高可用与灾难恢复解决方案，通过将主数据库的事务日志实时同步到一个或多个辅助副本，实现自动故障转移、读写分离和跨地域容灾，最大限度保障数据库服务的连续性和数据可靠性。\n非关系型数据库 Redis\n持久化配置\nRedis 持久化配置是指通过 RDB（快照）或 AOF（追加日志文件）机制将内存中的数据保存到磁盘，以防止服务重启或崩溃导致数据丢失；RDB 适合做备份和灾难恢复，AOF 提供更高的数据安全性，两者可结合使用以兼顾性能与可靠性。\n集群搭建\nRedis 集群搭建是通过部署多个 Redis 节点并启用 Redis Cluster 模式，实现数据自动分片（sharding）、高可用和横向扩展，当部分节点故障时仍能继续提供服务，适用于大规模、高并发的缓存或存储场景。\n缓存淘汰策略\nRedis 缓存淘汰策略是在内存达到上限时，根据预设规则（如 LRU、LFU、TTL 等）自动移除部分键以腾出空间，确保系统稳定运行；合理选择策略（如 allkeys-lru 或 volatile-ttl）可有效平衡命中率与资源利用率。\nMongoDB\n分片集群\nMongoDB 分片集群通过将数据水平拆分（sharding）分布到多个 shard 节点上，配合 config server 和 mongos 路由层，实现海量数据存储与高吞吐读写能力，解决单机容量和性能瓶颈问题，适用于超大规模数据场景。\n副本集\nMongoDB 副本集是由一个主节点（Primary）和多个从节点（Secondary）组成的高可用架构，通过自动故障转移和数据冗余保障服务连续性与数据安全，同时支持读写分离以提升读取性能。\n聚合查询\nMongoDB 聚合查询使用聚合管道（Aggregation Pipeline）对集合中的文档进行多阶段处理（如筛选、分组、排序、连接等），实现复杂的数据分析与转换，适用于报表生成、实时统计和数据清洗等场景。\n数据库工具链 监控：Prometheus + Grafana、Zabbix 迁移：ETL工具（Apache NiFi、Talend） 一、ETL 概述\nETL（Extract, Transform, Load） 是数据集成的核心流程：\nExtract（抽取）：从源系统（数据库、API、日志、文件等）提取原始数据 Transform（转换）：清洗、格式化、聚合、脱敏、标准化等处理 Load（加载）：将处理后的数据写入目标系统（数据仓库、湖、数据库等） 在现代数据迁移、数据中台建设、云迁移等场景中，ETL 工具是实现高效、可靠、可监控数据流转的关键基础设施。\n二、Apache NiFi\n2.1 简介\nApache NiFi 是由美国国家安全局（NSA）开源、现由 Apache 基金会维护的数据流自动化平台，专为实时、高吞吐、安全的数据路由与转换设计。其核心理念是“数据流即代码”，通过可视化拖拽构建数据管道。\n2.2 核心特性\n特性 说明 可视化编程 基于 Web UI 的拖拽式流程编排（Flow-based Programming） 强可靠性 内置背压控制、数据溯源（Provenance）、重试/失败处理机制 高扩展性 支持集群部署、自动负载均衡、水平扩展 丰富连接器 内置 300+ 处理器（Processor），支持 Kafka、S3、JDBC、HTTP、FTP、HDFS 等 安全合规 支持 TLS/SSL、LDAP/Kerberos 认证、细粒度权限控制（基于角色） 数据血缘追踪 完整记录每条数据的来源、处理路径、时间戳 2.3 典型迁移场景配置示例\n场景：MySQL → S3（CSV 格式） Extract：使用 QueryDatabaseTable 处理器定期轮询 MySQL 表 Transform：用 ConvertAvroToJSON 或 JoltTransformJSON 转换格式 Load：通过 PutS3Object 写入 AWS S3 存储桶 ✅ 优势：支持增量同步（通过 max-value-column 自动跟踪最大 ID）、断点续传、失败重试。\n2.4 适用场景\n实时数据管道（如 IoT 数据采集） 跨云/混合云数据迁移 敏感数据脱敏与合规传输 日志聚合与分发 三、Talend\n3.1 简介\nTalend 是企业级数据集成与治理平台，提供开源版（Talend Open Studio）和商业版（Talend Data Fabric）。它以 “代码生成” 为核心，通过图形化设计自动生成 Java/Spark 代码，适合批处理与复杂 ETL 作业。\n3.2 核心特性\n特性 说明 可视化开发 拖拽组件构建 ETL Job，自动生成可执行代码 多引擎支持 支持本地运行、Spark、Snowflake、Databricks 等 元数据管理 自动解析源/目标表结构，支持数据字典复用 数据质量 内置数据剖析、去重、校验规则（如 Talend Data Quality） CI/CD 集成 支持 Git、Jenkins、Docker，便于 DevOps 流程 云原生 提供 Talend Cloud，支持 AWS、Azure、GCP 一键部署 3.3 典型迁移场景配置示例\n场景：Oracle → Snowflake（带数据清洗） Extract：使用 tOracleInput 组件连接 Oracle 数据库 Transform：通过 tMap 组件进行字段映射、空值处理、类型转换 Load：用 tSnowflakeOutput 写入 Snowflake 表 ✅ 优势：自动处理字符集转换、大对象（LOB）支持、错误行隔离（Rejects 输出）。\n3.4 适用场景\n企业级数据仓库构建（如从 OLTP 到 EDW） 主数据管理（MDM） 合规性数据迁移（GDPR、CCPA） 批量历史数据迁移（TB 级别） 四、NiFi vs Talend 对比\n维度 Apache NiFi Talend 架构模式 流式处理（事件驱动） 批处理为主（支持流） 开发方式 可视化流程 + 少量脚本 可视化设计 → 自动生成代码 实时性 ⭐⭐⭐⭐⭐（毫秒级） ⭐⭐（分钟级，依赖调度） 学习曲线 中等（需理解 Flow 概念） 较陡（需理解组件逻辑） 部署复杂度 需 JVM + ZooKeeper（集群） 单机可运行，云版简化部署 成本 完全开源免费 开源版功能有限，企业版收费 典型用户 运维/数据工程师 ETL 开发者/数据架构师 五、选型建议\n选 Apache NiFi 如果：\n需要实时/近实时数据迁移 涉及异构协议（MQTT、Syslog、Kafka 等） 强调数据血缘与审计 团队偏好运维友好型工具 选 Talend 如果：\n以批量 ETL 为主（如每日同步） 需要强数据质量与元数据管理 已有 Java/Spark 技术栈 企业要求商业支持与 SLA 💡 最佳实践：在大型迁移项目中，可结合两者——用 NiFi 做实时数据采集，用 Talend 做深度清洗与加载。\n四、开发与脚本 编程语言 Python\n自动化脚本（Paramiko、Fabric）、YAML/JSON解析 Shell\n文本处理（awk、sed）、函数封装 PowerShell\nWindows服务自动化、WMI调用 开发框架与工具 Web开发\nFlask/Django（API接口调试）、Nginx反向代理配置 消息队列\nKafka/RabbitMQ（生产者/消费者模式） 代码管理 Git\n分支策略（GitFlow）、Rebase/Merge冲突解决 CI/CD\nJenkins/GitLab CI流水线配置 五、运维与部署 自动化运维 Ansible\nPlaybook编写（角色化部署、Vault加密） Puppet/Chef\n资源抽象模型、状态收敛机制 Terraform\n基础设施即代码（IaC） 容器化技术 Docker\n镜像构建（Dockerfile优化）、网络（bridge/host/macvlan） Kubernetes\nPod调度、HPA/VPA自动扩缩容、Helm Chart管理 监控与告警 Prometheus\nExporter集成、Alertmanager规则编写 ELK栈\n日志收集（Filebeat）、分析（Elasticsearch）、可视化（Kibana） 云平台 AWS\nEC2/EKS/S3资源管理、CloudFormation模板 Azure\nAKS/Azure SQL部署、ARM模板 阿里云\nACK/SLB/OSS、Serverless函数计算 六、项目管理与协作 项目管理工具 Jira\n敏捷看板（Scrum/Kanban）、史诗/用户故事拆解 Confluence\n文档协同、知识库搭建 需求分析 UML建模\n用例图、时序图 需求文档撰写\nPRD、用户故事地图 变更管理 变更流程设计\nRFC、回滚方案 配置管理数据库\nCMDB维护 七、行业特定技能 ERP/CRM系统 SAP S/4HANA\n模块配置（SD/MM/PP）、ABAP基础 Salesforce\n流程自动化（Process Builder）、数据清洗 物联网（IoT） MQTT协议调试\nMosquitto Broker 设备固件升级\nOTA方案设计 金融/医疗行业 合规性标准\nISO 27001、HIPAA 数据脱敏工具\nDelphix 八、未来发展路线技能延伸 云原生进阶 Service Mesh\nIstio/Linkerd Serverless架构\nAWS Lambda/Azure Functions AI/机器学习基础 数据预处理\nPandas/Numpy 模型部署\nTensorFlow Serving、ONNX Runtime 边缘计算 边缘节点资源调度\nK3s、EdgeX Foundry 5G网络切片配置 安全加固 零信任架构\nZero Trust 容器安全\nNotary、Clair 九、软技能 客户沟通 需求挖掘\nSPIN销售法 技术方案通俗化表达\n降维沟通 问题解决 根因分析\n5Why、鱼骨图 应急响应\nSLA保障、故障复盘 跨团队协作 Scrum角色\nPO、Scrum Master 跨时区团队协作工具\nSlack、Notion 十、认证体系（市场认可度排序） PMP/PRINCE2（项目管理） AWS/Azure/Aliyun解决方案架构师（云原生） RHCE/CCNA（运维/网络） CKA/CKS（Kubernetes） CISSP/CISP（安全方向） 总结 实施工程师的核心竞争力在于“技术深度+业务理解+协作能力”的三角模型。短期需聚焦自动化工具链与云平台实战能力，中期向架构设计或行业解决方案专家转型，长期可选择技术管理（CTO路线）或商业分析（售前顾问）路径。技术栈需动态更新，建议每年投入100小时学习前沿技术（如Service Mesh、AIops）。\n","permalink":"https://ktzxy.top/posts/ov4hkgytzc/","summary":"实施工程师技能体系全维度罗列","title":"实施工程师技能体系全维度罗列"},{"content":"1. MySQL 数据库概述 MySQL 官网：https://www.mysql.com/\nMySQL 5.7 版本官方文档： https://dev.mysql.com/doc/refman/5.7/en/ MySQL 8.0 版本官方文档： https://dev.mysql.com/doc/refman/8.0/en/ 1.1. 相关版本说明 MySQL官方提供了两种不同的版本：\n社区版本（MySQL Community Server）：免费， MySQL不提供任何技术支持 商业版本（MySQL Enterprise Edition）：收费，可以使用30天，官方提供技术支持 1.2. MySQL 安装 MySQL 下载地址：https://downloads.mysql.com/archives/community/\n此部分内容详见《MySQL 安装与部署》文档\n1.3. MySQL 数据库特点 MySQL数据库是用C和C++语言编写的，以保证源码的可移植性 支持多个操作系统例如：Windows、Linux、Mac OS等等 支持多线程，可以充分的利用CPU资源 为多种编程语言提供API，包括C语言，Java，PHP。Python语言等 MySQL优化了SQL算法，有效的提高了查询速度 MySQL开放源代码且无版权制约，自主性强、使用成本低。 MySQL历史悠久、社区及用户非常活跃，遇到问题，可以很快获取到帮助。 2. MySQL 体系架构 2.1. 架构概述 从上图可以看出，MySQL 最上层是连接组件。服务器层是由连接池、管理工具和服务、SQL 接口、解析器、优化器、缓存、存储引擎、文件系统组成\n连接池：由于每次建立建立需要消耗很多时间，连接池的作用就是将这些连接缓存下来，下次可以直接用已经建立好的连接，提升服务器性能。 管理工具和服务：系统管理和控制工具，例如备份恢复、Mysql 复制、集群等 SQL 接口：接受用户的 SQL 命令，并且返回用户需要查询的结果。比如 select from xx 语句就是调用 SQL Interface 解析器: SQL 命令传递到解析器的时候会被解析器验证和解析。解析器主要功能： 将 SQL 语句分解成数据结构，并将这个结构传递到后续步骤，以后 SQL 语句的传递和处理就是基于这个结构的 如果在分解构成中遇到错误，那么就说明这个 sql 语句是不合理的 优化器：查询优化器，SQL 语句在查询之前会使用查询优化器对查询进行优化 缓存器：查询缓存，如果查询缓存有命中的查询结果，查询语句就可以直接去查询缓存中取数据。这个缓存机制是由一系列小缓存组成的。比如表缓存，记录缓存，key 缓存，权限缓存等。 存储引擎：存储引擎是底层物理结构和实际文件读写的实现。MySQL 数据库其中一个特点就是其插件式的表存储引擎。 文件系统：即存储数据的地方 2.2. SQL 语句的执行流程 2.2.1. 查询语句执行流程 查询语句的执行流程如下：\n客户端请求连接器，验证用户身份，权限校验 查询缓存，存在缓存则直接返回，不存在则执行后续操作 分析器，对 SQL 进行词法分析和语法分析操作 优化器，主要对执行的 SQL 优化选择最优的执行方案方法 权限校验，在执行时会先看用户是否有执行权限，有才去使用相应引擎提供的接口 执行器，操作引擎层获取数据返回。如果开启查询缓存则会缓存查询结果 例如有查询语句如下：\n1 select * from user where id \u0026gt; 1 and name = \u0026#39;MooNkirA\u0026#39;; 首先检查权限，没有权限则返回错误； MySQL 8.0 以前会查询缓存，缓存命中则直接返回，没有则执行下一步； 词法分析和语法分析。提取表名、查询条件，检查语法是否有错误； 两种执行方案，先查 id \u0026gt; 1 还是 name = 'MooNkirA'，优化器根据自己的优化算法选择执行效率最好的方案； 校验权限，有权限就调用数据库引擎接口，返回引擎的执行结果。 2.2.2. 更新语句执行过程 更新语句执行流程如下：分析器 -\u0026gt; 权限校验 -\u0026gt; 执行器 -\u0026gt; 引擎 -\u0026gt; redo log(prepare 状态) -\u0026gt; binlog -\u0026gt; redo log(commit 状态)。例如有更新语句如下：\n1 update user set name = \u0026#39;MooNkirA\u0026#39; where id = 1; 先查询到 id 为 1 的记录，有缓存会使用缓存。 拿到查询结果，将 name 更新为『MooNkirA』，然后调用引擎接口，写入更新数据，innodb 引擎将数据保存在内存中，同时记录 redo log，此时 redo log 进入 prepare 状态。 执行器收到通知后记录 binlog，然后调用引擎接口，提交 redo log 为 commit 状态。 更新完成。 Tips: 执行更新语句时，在记录完 redo log，不直接提交，而是先进入 prepare 状态。这是因为假设先写 redo log 直接提交，然后写 binlog，如果在写完 redo log 后，服务器挂了，此时 binlog 日志没有被写入，那么服务器重启后，会通过 redo log 恢复数据，但是此时 binlog 并没有记录该数据，后续进行机器备份的时候，就会丢失这一条数据，同时主从同步也会丢失这一条数据。\n2.2.3. SQL 执行流程图 2.3. 连接层 最上层是一些客户端和链接服务，包含本地 sock 通信和大多数基于客户端/服务端工具实现的类似于 TCP/IP 的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念，为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于 SSL 的安全链接。\n当 MySQL 启动（MySQL 服务器就是一个进程），等待客户端连接，每一个客户端连接请求，服务器都会新建一个线程处理（如果是线程池的话，则是分配一个空的线程），每个线程独立，拥有各自的内存处理空间。通过查询系统参数max_connections可以知道服务器最大的连接数。\n1 2 3 4 5 6 7 mysql\u0026gt; show VARIABLES like \u0026#39;%max_connections%\u0026#39;; +-----------------+-------+ | Variable_name | Value | +-----------------+-------+ | max_connections | 151 | +-----------------+-------+ 1 row in set (0.02 sec) 连接到服务器，服务器也会为安全接入的每个客户端验证它所具有的操作权限。即用户名、IP、密码验证，一旦连接成功，连接器会到权限表里面查询，并验证是否具有执行某个特定查询的权限（例如，是否允许客户端对某个数据库某个表的某个操作）。这就意味着，一个用户成功建立连接后，即使再使用管理员账号修改该用户的权限，也不会影响已经存在连接的权限。修改完成后，只有再新建的连接才会使用新的权限设置。\n2.4. Server 层(SQL 处理层) 2.4.1. Server 层功能作用 第二层架构（Server 层）主要完成大多数的核心服务功能，如 SQL 接口，并完成缓存的查询，SQL 语句的解析和优化，部分内置函数的执行，所有跨存储引擎的功能（所谓跨存储引擎就是说每个引擎都需提供的功能（引擎需对外提供接口））如：存储过程、函数、触发器、视图等。该层具体的操作如下：\n如果是查询语句（select语句），首先会查询缓存是否已有相应结果，有则返回结果，无则进行下一步（如果不是查询语句，同样调到下一步） 解析查询，创建一个内部数据结构（解析树），这个解析树主要用来SQL语句的语义与语法解析； 优化 SQL 语句：例如重写查询，决定表的读取顺序，以及选择需要的索引等。这一阶段用户是可以查询的，查询服务器优化器是如何进行优化的，便于用户重构查询和修改相关配置，达到最优化。这一阶段还涉及到存储引擎，优化器会询问存储引擎，比如某个操作的开销信息、是否对特定索引有查询优化等。 2.4.2. 查询缓存 当连接建立后执行查询语句时，会先查询缓存。QC（query cache） 严格要求 2 次 SQL 请求要完全一样，包括 SQL 语句，连接的数据库、协议版本、字符集等因素都会影响。之前执行过的语句及其结果可能会以 key-value 对的形式，被直接缓存在内存中。key 是查询的语句，value 是查询的结果。如果查询能够直接在这个缓存中找到 key，那么这个 value 就会被直接返回给客户端。\n2.4.2.1. 查询相关缓存的参数 1 2 3 4 -- 默认不开启 show variables like \u0026#39;%query_cache_type%\u0026#39;; -- 默认值 1M show variables like \u0026#39;%query_cache_size%\u0026#39;; 2.4.2.2. 查询缓存参数配置 query_cache_type只能配置在 my.cnf 文件中，这大大限制了 qc 的作用。在生产环境建议不开启，除非经常有 sql 完全一模一样的查询。\n1 2 -- 会报错,query_cache_type 只能配置在 my.cnf 文件中 SET GLOBAL query_cache_type = 1; 一般建议只在更新频率极低的表里使用查询缓存，比如系统配置表、字典表等。在 MySQL 手动指定是否开启缓存的功能，只需要将 my.cnf 参数 query_cache_type 设置成 DEMAND。具体配置如下：\n1 2 # query_cache_type 有3个值 0-关闭查询缓存OFF，1-开启ON，2-（DEMAND）代表当sql语句中有SQL_CACHE关键词时才缓存 query_cache_type=2 以上配置默认的 SQL 语句都不使用查询缓存。而对于确定要使用查询缓存的语句，可以用 SQL_CACHE 关键字显式指定，例如：\n1 select SQL_CACHE * from test where ID=5； 2.4.2.3. MySQL 8.0 移除查询缓存功能 从 8.0 开始，MySQL 已经移除查询缓存功能。MySQL 的工程团队发现启用缓存的好处并不多。\n查询缓存的效果取决于缓存的命中率，只有命中缓存的查询效果才能有改善，因此无法预测其性能。 查询缓存的另一个大问题是它受到单个互斥锁的保护。在具有多个内核的服务器上，大量查询会导致大量的互斥锁争用。 查询缓存的失效非常频繁，只要有对一个表的更新，这个表上所有的查询缓存都会被清空。 通过基准测试发现，大多数工作负载最好禁用查询缓存(5.6 的默认设置)：按照官方所说的：造成的问题比它解决问题要多的多，弊大于利就直接砍掉了\n2.4.3. 分析器 分析器会对 SQL 语句进行“词法分析”，即识别 SQL 语句中字符串分别是什么，代表什么操作。根据词法分析的结果，语法分析器会根据语法规则，判断输入的 SQL 语句是否满足 MySQL 语法。如果语句不对则提示“You have an error in your SQL syntax”的错误，比如下面这个语句 from 写成了 \u0026ldquo;rom\u0026rdquo;。\n1 2 mysql\u0026gt; select * fro test where id=1; ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near \u0026#39;fro test where id=1\u0026#39; at line 1 分析器对sql的分析过程步骤如下图：\nSQL 语句经过分析器分析之后，会生成一个这样的语法树\n2.4.4. 优化器 经过了分析器，MySQL 知道了语句是执行什么操作。在开始执行之前，还要先经过优化器的处理。\n优化器是在表里面有多个索引的时候，决定使用哪个索引；或者在一个语句有多表关联（join）的时候，决定各个表的连接顺序；以及一些mysql自己内部的优化机制。\n2.4.5. 执行器 开始执行的时候，要先判断一下当前登陆的用户对该表是否有执行查询的权限。如果没有，就会返回没有权限的错误(在工程实现上，如果命中查询缓存，会在查询缓存返回结果的时候，做权限验证)；如果有权限，就打开表继续执行。打开表的时候，执行器就会根据表的引擎定义，去使用这个引擎提供的接口。\n2.5. 存储引擎层 MySQL 数据库区别于其他数据库的最重要的一个特点就是其插件式的表存储引擎。MySQL 插件式的存储引擎架构提供了一系列标准的管理和服务支持，这些标准与存储引擎本身无关，可能是每个数据库系统本身都必需的，如 SQL 分析器和优化器等，而存储引擎是底层物理结构和实际文件读写的实现，服务器通过API和存储引擎进行通信。\n每个存储引擎开发者可以按照自己的意愿来进行开发。不同的存储引擎具有不同的存储机制、索引技巧、锁定水平等功能，这样使用者也可以根据自己的需要，来选取合适的存储引擎。存储引擎可以分为 MySQL 官方存储引擎和第三方存储引擎\nTips: 数据库中的索引是在存储引擎层实现的。\n插件式存储引擎的好处是，每个存储引擎都有各自的特点，能够根据具体的应用建立不同存储引擎表。由于 MySQL 数据库的开源特性，用户可以根据 MySQL 预定义的存储引擎接口编写自己的存储引擎。若用户对某一种存储引擎的性能或功能不满意，可以通过修改源码来得到想要的特性。\nNotes: MySQL 的核心就是存储引擎。值得注意的是，存储引擎是针对表！！官方建议在同一个数据库中，尽量所有表都使用同一个存储引擎，否则会出现一些奇怪的问题。\n2.5.1. MySQL 官方引擎概要 2.5.1.1. InnoDB 存储引擎 InnoDB 是 MySQL 的默认事务型存储引擎，有行级锁定和外键约束。也是最重要、使用最广泛的存储引擎。它被设计用来处理大量的短期(short-lived)事务，短期事务大部分情况是正常提交的，很少会被回滚。InnoDB 的性能和自动崩溃恢复特性，使得它在非事务型存储的需求中也很流行。\n在 MySQL 5.5 之后，InnoDB 是默认的 MySQL 存储引擎。主要特点：\nDML操作遵循ACID模型，支持事务； 行级锁，提高并发访问性能； 支持外键FOREIGN KEY约束，保证数据的完整性和正确性； 2.5.1.2. MyISAM 存储引擎 在 MySQL 5.1 及之前的版本，MyISAM 是默认的存储引擎。MyISAM 提供了大量的特性，包括全文索引、压缩、空间函数（GIS）等，但 MyISAM 不支持事务、行级锁和外键，而且有一个毫无疑问的缺陷就是崩溃后无法安全恢复。但有插入数据快，空间和内存使用比较低等优点。\n尽管MyISAM引擎不支持事务、不支持崩溃后的安全恢复，但它绝不是一无是处的。对于只读的数据，或者表比较小、可以忍受修复（repair）操作，则依然可以继续使用 MyISAM（但请不要默认使用 MyISAM，而是应当默认使用 InnoDB)。但是 MyISAM 对整张表加锁，而不是针对行。读取时会对需要读到的所有表加共享锁，写入时则对表加排他锁。MyISAM 很容易因为表锁的问题导致典型的的性能问题。\n2.5.1.3. Mrg_MyISAM 存储引擎 Merge 存储引擎，是一组 MyIsam 的组合，也就是说，他将 MyIsam 引擎的多个表聚合起来，但是他的内部没有数据，真正的数据依然是 MyIsam 引擎的表中，但是可以直接进行查询、删除更新等操作。\n2.5.1.4. Archive 存储引擎 Archive 存储引擎只支持 INSERT 和 SELECT 操作，在 MySQL 5.1 之前也不支持索引。Archive 引擎会缓存所有的写并利用 zlib 对插入的行进行压缩，所以比 MyISAM 表的磁盘 I/O 更少。但是每次 SELECT 查询都需要执行全表扫描。所以 Archive 表适合日志和数据采集类应用，这类应用做数据分析时往往需要全表扫描。或者在一些需要更快速的 INSERT 操作的场合下也可以使用。\nArchive 引擎不是一个事务型的引擎，而是一个针对高速插入和压缩做了优化的简单引擎。\n2.5.1.5. Blackhole 存储引擎 Blackhole 引擎没有实现任何的存储机制，它会丢弃所有插入的数据，不做任何存储，Select 语句的内容永远是空。但是服务器会记录 Blackhole 表的日志，所以可以用于复制数据到备份数据库，或者只是简单地记录到日志。这种特殊的存储引擎可以在一些特殊的复制架构和日志审核时发挥作用。但这种引擎在应用方式上有很多问题，因此并不推荐。其使用场景有：\n验证 dump file 语法的正确性 以使用 blackhole 引擎来检测 binlog 功能所需要的额外负载 1 2 3 4 5 6 7 8 CREATE TABLE `Blackhole` ( `id` BIGINT (20) UNSIGNED NOT NULL, `fname` VARCHAR (100) NOT NULL, `lname` VARCHAR (100) NOT NULL, `age` TINYINT (3) UNSIGNED NOT NULL, `sex` TINYINT (1) UNSIGNED NOT NULL, PRIMARY KEY (`id`) ) ENGINE = Blackhole DEFAULT CHARSET = utf8 2.5.1.6. CSV 引存储擎 CSV 引擎可以将普通的 CSV 文件(逗号分割值的文件）作为 MySQL 的表来处理，但这种表不支持索引。CSV 引擎可以在数据库运行时拷入或者拷出文件。可以将 Excel 等的数据存储为 CSV 文件，然后复制到 MySQL 数据目录下，就能在MySQL 中打开使用。同样，如果将数据写入到一个 CSV 引擎表，其他的外部程序也能立即从表的数据文件中读取 CSV 格式的数据。因此 CSV 引擎可以作为一种数据交换的机制，非常有用。\n2.5.1.7. Federated 存储引擎 Federated 引擎是访问其他 MySQL 服务器的一个代理，它会创建一个到远程 MySQL 服务器的客户端连接，并将查询传输到远程服务器执行，然后提取或者发送需要的数据。最初设计该存储引擎是为了和企业级数据库如 Microsoft SQL Server 和 Oracle 的类似特性竞争的，可以说更多的是一种市场行为。尽管该引擎看起来提供了一种很好的跨服务器的灵活性，但也经常带来问题，因此默认是禁用的。\n2.5.1.8. Memory 存储引擎 MEMORY 存储引擎的所有数据都在内存中，数据的处理速度快，但安全性不高。\n如果需要快速地访问数据，并且这些数据不会被修改，重启以后丢失也没有关系，那么使用 Memory 表(以前也叫做 HEAP 表）是非常有用的。Memory 表至少比 MyISAM 表要快一个数量级，因为每个基于 MEMORY 存储引擎的表实际对应一个磁盘文件。该文件的文件名与表名相同，类型为 frm 类型。该文件中只存储表的结构。而其数据文件，都是存储在内存中，这样有利于数据的快速处理，提高整个表的效率，不需要进行磁盘 I/O。所以 Memory 表的结构在重启以后还会保留，但数据会丢失。Memroy 表在很多场景可以发挥好的作用:\n用于查找(lookup）或者映射(mapping）表，例如将邮编和州名映射的表。 用于缓存周期性聚合数据(periodically aggregated data)的结果。 用于保存数据分析中产生的中间数据。 Memory 表支持 Hash 索引，因此查找操作非常快。虽然 Memory 表的速度非常快，但还是无法取代传统的基于磁盘的表。Memroy 表是表级锁，因此并发写入的性能较低。它不支持 BLOB 或 TEXT 类型的列，并且每行的长度是固定的，所以即使指定了 VARCHAR 列，实际存储时也会转换成 CHAR，这可能导致部分内存的浪费。\n优点：\n访问速度较快。\n缺点：\n哈希索引数据不是按照索引值顺序存储，无法用于排序。 不支持部分索引匹配查找，因为哈希索引是使用索引列的全部内容来计算哈希值的。 只支持等值比较，不支持范围查询。 当出现哈希冲突时，存储引擎需要遍历链表中所有的行指针，逐行进行比较，直到找到符合条件的行。 2.5.1.9. NDB 集群引擎 使用 MySQL 服务器、NDB 集群存储引擎，以及分布式的、share-nothing 的、容灾的、高可用的 NDB 数据库的组合，被称为 MySQL 集群（(MySQL Cluster)。\n2.5.2. 值得了解的第三方引擎 2.5.2.1. Percona 的 XtraDB 存储引擎 基于 InnoDB 引擎的一个改进版本，已经包含在 Percona Server 和 MariaDB 中，它的改进点主要集中在性能、可测量性和操作灵活性方面。XtraDB 可以作为 InnoDB 的一个完全的替代产品，甚至可以兼容地读写 InnoDB 的数据文件，并支持 InnoDB 的所有查询。\n2.5.2.2. TokuDB 引擎 TokuDB 存储引擎使用了一种新的叫做分形树(Fractal Trees)的索引数据结构。该结构是缓存无关的，与 B+树有些类似，在 Fractal Tree中，每一个 child 指针除了需要指向一个 child 节点外，还会带有一个 Message Buffer（FIFO 的队列），用来缓存更新操作。\n例如，一次插入操作只需要落在某节点的 Message Buffer 就可以马上返回了，并不需要搜索到叶子节点。这些缓存的更新会在查询时或后台异步合并应用到对应的节点中。\n因此 Fractal Trees 结构即使其大小超过内存性能也不会下降，也就没有内存生命周期和碎片的问题。TokuDB 是一种大数据（Big Data)存储引擎，因为其拥有很高的压缩比，可以在很大的数据量上创建大量索引。现在该引擎也被 Percona 公司收购。\nTips：分形树，是一种写优化的磁盘索引数据结构。在一般情况下，分形树的写操作（Insert/Update/Delete）性能比较好，同时它还能保证读操作近似于B+树的读性能。据测试结果显示，TokuDB 分形树的写性能优于 InnoDB 的 B+树，读性能略低于 B+树。分形树核心思想是利用节点的 Message Buffer 缓存更新操作，充分利用数据局部性原理，将随机写转换为顺序写，这样极大的提高了随机写的效率。Fractal-tree 在事务实现上有优势，它主要适用于访问频率不高的数据或历史数据归档。\n2.5.2.3. Infobright MySQL 默认是面向行的，每一行的数据是一起存储的，服务器的查询也是以行为单位处理的。而在大数据量处理时，面向列的方式可能效率更高，比如 HBASE 就是面向列存储的。\nInfobright 是最有名的面向列的存储引擎。在非常大的数据量（数十 TB)时，该引擎工作良好。Infobright 是为数据分析和数据仓库应用设计的。数据高度压缩，按照块进行排序，每个块都对应有一组元数据。在处理查询时，访问元数据可决定跳过该块，甚至可能只需要元数据即可满足查询的需求。但该引擎不支持索引，不过在这么大的数据量级，即使有索引也很难发挥作用，而且块结构也是一种准索引 (quasi-index)。Infobright 需要对 MySQL 服务器做定制，因为一些地方需要修改以适应面向列存储的需要。如果查询无法在存储层使用面向列的模式执行，则需要在服务器层转换成按行处理，这个过程会很慢。Infobright 有社区版和商业版两个版本。\n2.5.3. 指定表的存储引擎 在创建表的时候，通过engine关键字指定存储引擎。语法结构如下：\n1 create table xxx(...) engine=存储引擎名称; 2.5.4. 表的存储引擎转换 有很多种方法可以将表的存储引擎转换成另外一种引擎。每种方法都有其优点和缺点。常用的有三种方法：\n2.5.4.1. ALTER TABLE 将表从一个引擎修改为另一个引擎最简单的办法是使用 ALTER TABLE 语句。\n1 ALTER TABLE mytable ENGINE = InnoDB; 上面语句将 mytable 的引擎修改为 InnoDB。该语法可以适用任何存储引擎。但需要执行很长时间，在实现上，MySQL 会按行将数据从原表复制到一张新的表中，在复制期间可能会消耗系统所有的I/O 能力，同时原表上会加上读锁。所以，在繁忙的表上执行此操作要特别小心。如果转换表的存储引擎，将会失去和原引擎相关的所有特性。\n2.5.4.2. 导出与导入 还可以使用 mysqldump 工具将数据导出到文件，然后修改文件中 CREATE TABLE 语句的存储引擎选项，注意同时修改表名，因为同一个数据库中不能存在相同的表名，即使它们使用的是不同的存储引擎。\n2.5.4.3. CREATE 和 SELECT 先创建一个新的存储引擎的表，然后利用 INSERT…SELECT 语法来导数据:\n1 2 3 CREATE TABLE innodb_table LIKE myisam_table; ALTER TABLE innodb_table ENGINE=InnoDB; INSERT INTO innodb_table SELECT * FROM myisam_table; 如果数据量很大，则可以考虑做分批处理，针对每一段数据执行事务提交操作。\n2.5.5. 检查 MySQL 的引擎 查询 MySQL 已提供哪些存储引擎 1 2 3 4 5 6 7 8 9 10 11 12 13 14 mysql\u0026gt; show engines; +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+ | Engine | Support | Comment | Transactions | XA | Savepoints | +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+ | InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES | | MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO | | MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO | | BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO | | MyISAM | YES | MyISAM storage engine | NO | NO | NO | | CSV | YES | CSV storage engine | NO | NO | NO | | ARCHIVE | YES | Archive storage engine | NO | NO | NO | | PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO | | FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL | +--------------------+---------+----------------------------------------------------------------+--------------+------+------------+ 查询 MySQL 当前默认的存储引擎 1 2 3 4 5 6 7 8 9 mysql\u0026gt; show variables like \u0026#39;%storage_engine%\u0026#39;; +----------------------------------+--------+ | Variable_name | Value | +----------------------------------+--------+ | default_storage_engine | InnoDB | | default_tmp_storage_engine | InnoDB | | disabled_storage_engines | | | internal_tmp_disk_storage_engine | InnoDB | +----------------------------------+--------+ 2.5.6. MyISAM、InnoDB 与 Memory 比较 MyISAM、InnoDB 与 Memory 区别汇总：\n功能 InnoDB MyISAM Memory 存储限制 64TB 256TB 依赖RAM的大小 事务 √ - - 锁机制 行锁/表锁 表锁 表锁 B+tree索引 √ √ √ Hash索引 - - √ 全文索引 √(5.6版本后支持) √ - 集群索引 √ - - 数据索引 √ - √ 数据压缩 - √ - 空间使用率 高 低 - 内存使用 高 低 中等 批量插入速度 低 高 高 支持外键 √ - - MyISAM 与 InnoDB 的一些其他区别说明：\n是否支持行级锁：MyISAM 支持表级锁，即使操作一条记录也会锁住整个表，不适合高并发的操作；InnoDB 支持行级锁和表级锁，默认是行级锁，操作时只锁某一行，不对其它行有影响，适合高并发的操作。 缓存内容：MyISAM 只缓存索引，不缓存真实数据；InnoDB 不仅缓存索引还要缓存真实数据，对内存要求较高，而且内存大小对性能有决定性影响。 是否支持事务和崩溃后的安全恢复：MyISAM 不提供事务支持。而 InnoDB 提供事务支持，具有事务、回滚和崩溃修复能力。 是否支持外键：MyISAM 不支持，而 InnoDB 支持。 是否支持 MVCC：MyISAM 不支持，InnoDB 支持。应对高并发事务，MVCC 比单纯的加锁更高效。 是否有聚集索引：MyISAM 不支持聚集索引；InnoDB 支持聚集索引。 数据存储的结构：MyISAM 数据与索引分开文件存储，索引保存的是数据文件的指针；InnoDB 的聚集索引是数据和索引保存在同一个文件中。 查询全表总记录数的效率：MyISAM 用一个变量保存了整个表的行数，查询表记录数时只需要读出该变量即可，速度很快；InnoDB 不保存表的具体行数，查询表总记录数时需要全表扫描。 2.5.7. 选择合适的引擎 大部分情况下，InnoDB 都是正确的选择，所以在 MySQL 5.5 版本将 InnoDB 作为默认的存储引擎了。对于如何选择存储引擎，可以简单地归纳为一句话：“除非需要用到某些 InnoDB 不具备的特性，并且没有其他办法可以替代，否则都应该优先选择 InnoDB 引擎”。比如，MySQL 中只有 MyISAM 支持地理空间搜索。\n如果不需要用到 InnoDB 的特性，同时其他引擎的特性能够更好地满足需求，也可以考虑一下其他存储引擎。举个例子，如果不在乎可扩展能力和并发能力，也不在乎崩溃后的数据丢失问题，却对 InnoDB 的空间占用过多比较敏感，这种场合下选择 MyISAM 就比较合适。\n小结如下：\nInnoDB：是 Mysql 的默认存储引擎，支持事务、外键。如果应用对事务的完整性有比较高的要求，在并发条件下要求数据的一致性，数据操作除了插入和查询之外，还包含很多的更新、删除操作，那么 InnoDB 存储引擎是比较合适的选择。 MyISAM：如果应用是以读操作和插入操作为主，只有很少的更新和删除操作，并且对事务的完整性、并发性要求不是很高，那么选择这个存储引擎是非常合适的。 MEMORY：将所有数据保存在内存中，访问速度快，通常用于临时表及缓存。MEMORY 的缺陷就是对表的大小有限制，太大的表无法缓存在内存中，而且无法保障数据的安全性。 建议不要混合使用多种存储引擎，否则可能带来一系列复杂的问题，以及一些潜在的 bug 和边界问题。存储引擎层和服务器层的交互已经比较复杂，更不用说混合多个存储引擎了。至少，混合存储对一致性备份和服务器参数配置都带来了一些困难。\n3. 启动选项和参数 3.1. 配置参数文件 当 MySQL 实例启动时，数据库会先去读一个配置参数文件，用来寻找数据库的各种文件所在位置以及指定某些初始化参数。在默认情况下，MySQL 实例会在按一定的顺序中的指定位置读取配置，用户只需通过命令即可查看到相应的配置位置读取顺序。\n1 mysql --help | grep my.cnf 注：都是后面配置文件中的配置项会覆盖前面配置文件中的相同的配置项\nMySQL 实例可以不需要参数文件，这时所有的参数值取决于编译 MySQL 时指定的默认值和源代码中指定参数的默认值。MySQL 数据库的参数文件是以文本方式进行存储的。通过文本编辑软件即可进行参数的修改\n3.2. 参数的查看和修改 在命令行中输入以下命令可查看数据库中的所有参数。\n1 2 3 4 5 -- 查询数据库中的所有参数 SHOW VARIABLES; -- 模糊查询数据库参数 SHOW VARIABLES LIKE \u0026#39;%xxx%\u0026#39;; 从 MySQL 5.1 版本开始，还可以通过 information_schema 架构下的 GLOBAL_VARIABLES 视图来进行查找，推荐使用命令show variables，使用更为简单，且各版本的 MySQL 数据库都支持。\n3.2.1. MySQL 数据库中的参数的分类 从不同的角度来说，主要分成两类\n从类型上：动态(dynamic)参数和静态(static)参数\n动态参数意味着可以在 MySQL 实例运行中进行更改 静态参数说明在整个实例生命周期内都不得进行更改，即只读(read only) 从作用范围上：全局变量(GLOBAL)和会话变量(SESSION/LOCAL)\n全局变量（GLOBAL）影响服务器的整体操作。 会话变量（SESSION/LOCAL）影响某个客户端连接的操作。 用 default_storage_engine 来作为示例说明，在服务器启动时会初始化一个名为 default_storage_engine，作用范围为 GLOBAL 的系统变量。之后每当有一个客户端连接到该服务器时，服务器都会单独为该客户端分配一个名为default_storage_engine，作用范围为SESSION的系统变量，该作用范围为SESSION的系统变量值按照当前作用范围为GLOBAL的同名系统变量值进行初始化。\n3.2.2. 动态参数值的修改 通过 SET 命令对动态的参数值进行修改。语法如下：\n1 2 SET [global | session ] system_var_name= expr SET [@@global. | @@session.] system_var_name= expr 示例：\n1 2 SET read_ buffer_size=524288; SET @@global.read_ buffer_size=524288; 3.3. MySQL官方手册（系统参数部分） 官方文档（5.7版本）地址：https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html\n4. MySQL 数据目录结构分析 像 InnoDB、MyIASM 这样的存储引擎都是把表存储在磁盘上。\n4.1. 数据目录的位置 通过以下命令可以查看当前 MySql 数据库存储数据的目录位置\n1 2 3 4 5 6 mysql\u0026gt; show variables like \u0026#39;datadir\u0026#39;; +---------------+----------------+ | Variable_name | Value | +---------------+----------------+ | datadir | /var/lib/mysql | +---------------+----------------+ Notes: 存储目录位置可以通过配置文件进行修改。\n4.2. 数据目录的文件 数据目录中包含创建的数据库、表、视图和触发器等用户数据，除了这些用户数据，为了程序更好的运行，MySQL 也会创建一些其他的额外数据\n4.2.1. 数据库的存储 在使用 CREATE DATABASE 语句创建一个数据库时，MySQL 会进行以下的处理：\n在数据目录下创建一个和数据库名同名的子目录（文件夹) 在该与数据库名同名的子目录下创建一个名为db.opt的文件，这个文件中包含了该数据库的各种属性，例如该数据库的字符集和比较规则等等。 进入datadir数据目录查看，除了 information_schema 这个系统数据库（比较特殊）外，其他的数据库在数据目录下都有对应的子目录。\n4.2.2. 表的存储 MySQL 数据库表的信息可以分成：\n表结构的定义：是定义了表每列的数据类型、约束条件、索引、字符集等等信息。InnoDB 和 MyIASM 这两种存储引擎都在数据目录下对应的数据库子目录下创建了一个专门用于存储描述表结构信息的文件，其文件名是：表名.frm 表中的数据：就是实际每个表的存储的数据。而不同的存储引擎保存的文件格式、数量也不一样。 4.2.3. InnoDB 表数据的存储 InnoDB 的数据会放在一个表空间或者文件空间（英文名: table space 或者 filespace)的概念，这个表空间是一个抽象的概念，它可以对应文件系统上一个或多个真实文件〈不同表空间对应的文件数量可能不同)。每一个表空间可以被划分为很多个页，表数据就存放在某个表空间下的某些页里。表空间有好几种类型。\n系统表空间(system tablespace) 系统表空间可以对应文件系统上一个或多个实际的文件，默认情况下，InnoDB 会在数据目录下创建一个名为 ibdata1(在数据目录下)、大小为 12M 的文件，这个文件就是对应的系纳表空间在文件系统上的表示。\n此文件是自扩展文件，即存储空间不够用时，它会自己增加文件大小。如果想让系统表空间对应文件系统上多个实际文件，可以在 MySQL 启动时配置对应的文件路径以及它们的大小，也可以把系统表空间对应的文件路径不配置到数据目录下，甚至可以配置到单独的磁盘分区上\n需要注意的一点是：在一个 MySQL 服务器中，系统表空间只有一份。从 MySQL5.5.7 到 MySQL5.6.6 之间的各个版本中，表中的数据都会被默认存储到这个系统表空间。\n独立表空间(file-per-table tablespace) 在 MySQL5.6.6 以及之后的版本中，InnoB 并不会默认的把各个表的数据存储到系统表空间中，而是为每一个表建立一个独立表空间，也就是说用户创建了多少个表，就有多少个独立表空间。\n使用独立表空间来存储表数据的话，会在该表所属数据库对应的子目录下创建一个表示该独立表空间的文件，文件名和表名相同，文件的扩展名是.ibd，即：表名.ibd\n如上例，consult_content.ibd文件就用来存储consult_content表中的数据和索引。也可以指定使用系统表空间还是独立表空间来存储数据，这个功能由启动参数innodb_file_per_table控制。配置示例如下：\n1 2 [server] innodb_file_per_table=0 上面的配置意思是：当imodb_file_per table的值为0时，代表使用系统表空间；当innodb_file pertable的值为1时，代表使用独立表空间。需要注意的是inmodb_file_per_table参数只对新建的表起作用，对于已经分配了表空间的表并不起作用。\n可以直接通过命令查询：\n1 2 3 4 5 6 7 mysql\u0026gt; show variables like \u0026#39;innodb_file_per_table\u0026#39;; +-----------------------+-------+ | Variable_name | Value | +-----------------------+-------+ | innodb_file_per_table | ON | +-----------------------+-------+ 1 row in set (0.04 sec) 其他类型的表空间 随着 MySQL 的发展，除了上述两种老牌表空间之外，现在还新提出了一些不同类型的表空间，比如通用表空间(general tablespace)，undo 表空间(undotablespace)、临时表空间（temporary tablespace)等。\n4.2.4. MyIASM 表数据的存储 在 MyISAM 存储引擎表中的数据和索引是分开存放的。所以在文件系统中也是使用不同的文件来存储数据文件和索引文件。与 InnoDB 不同的是，MyISAM 并没有表空间的概念，表数据都存放到对应的数据库子目录下。\n1 2 -- 创建不同的是，MyISAM的表 create table a_myisam(c1 int) engine=MyISAM; 如上例所示：a_myisam.MYD是表的数据文件；a_myisam.MYI是表的索引文件；a_myisam.frm是存储表结构定义。\n4.3. 日志文件 服务器运行过程中，会产生各种各样的日志，比如常规的查询日志、错误日志、二进制日志、redo日志、Undo日志等等，日志文件记录了影响 MySQL 数据库的各种类型活动。\nMySQL 常见的日志文件有：错误日志（error log）、慢查询日志（slow query log）、查询日志（query log）、二进制文件（bin log）。\n4.3.1. 错误日志 错误日志文件对 MySQL 的启动、运行、关闭过程进行了记录。遇到问题时应该首先查看该文件以便定位问题。该文件不仅记录了所有的错误信息，也记录一些警告信息或正确的信息。通过下面命令来查看错误日志文件的位置：\n1 show variables like \u0026#39;log_error\u0026#39;; 默认日志文件名称：主机名.err\nTips: 错误日志功能是默认开启的，而且无法被关闭。当 MySQL 不能正常启动时，第一个必须查找的文件应该就是错误日志文件\n4.3.2. 慢查询日志 慢查询日志可以帮助定位可能存在问题的 SQL 语句，从而进行 SQL 语句层面的优化。\n慢查询日志记录了所有执行时间超过参数 long_query_time 设置值并且扫描记录数不小于 min_examined_row_limit 的所有的SQL语句的日志。long_query_time 默认为 10 秒，最小为 0， 精度可以到微秒。\n慢查询日志相关配置\n1 2 3 4 5 6 7 8 # 该参数用来控制慢查询日志是否开启，可取值：1|0，1 代表开启， 0 代表关闭 slow_query_log=1 # 该参数用来指定慢查询日志的文件名。日志文件名称：`主机名-slow.log` slow_query_log_file=slow_query.log # 该选项用来配置查询的时间限制，超过这个时间将认为值慢查询，将需要进行日志记录，默认10s long_query_time=10 4.3.3. 查询日志 查询日志记录了用户对 MySQL 数据库的所有操作，包括启动和关闭 MySQL 服务、所有用户的连接开始时间和截止时间、发给 MySQL 数据库服务器的所有 SQL 指令等，如 select、show 等，无论 SQL 的语法正确还是错误、执行成功还是失败，MySQL 都会将其记录下来。与之相比，二进制日志是不包含查询数据的SQL语句。\n4.3.3.1. 查询日志配置 默认情况下，查询日志是未开启的。如果需要开启查询日志，可以设置以下配置：\n1 2 3 4 5 # 该选项用来开启查询日志，可选值：0|1；0代表关闭，1代表开启 general_log=1 # 设置日志的文件名，如果没有指定， 默认日志文件名称：主机名.log general_log_file=file_name 命令配置：\n1 2 # 打开通用查询日志 SET GLOBAL general_log=on; 4.3.3.2. 查询日志参数查看 查看 MySQL 是否开启了查询日志\n1 2 3 4 5 6 7 mysql\u0026gt; show variables like \u0026#39;%general_log%\u0026#39;; +------------------+--------------+ | Variable_name | Value | +------------------+--------------+ | general_log | OFF | | general_log_file | MOONKIRA.log | +------------------+--------------+ 参数说明：\ngeneral_log：是否开启日志参数，默认为OFF，处于关闭状态，因为开启会消耗系统资源并且占用磁盘空间。一般不建议开启，只在需要调试查询问题时开启。 general_log_file：通用查询日志记录的位置参数。 从 MySQL 5.1 开始，可以将查询日志的记录放入 mysql 架构下的 general_log 表中\n1 SELECT * FROM general_log; 4.3.4. binlog（二进制归档日志） 详见后面章节\n4.3.5. redo log（重做日志）、undo log（撤销日志） redo log（重做日志）、undo log（撤销日志）是 Innodb 引擎级别的日志，主要用于实现事务，此部分内容详见《MySQL数据库-事务》笔记\n4.4. 其他数据文件 MySQL数据目录除了以上的数据文件之外，还有运行程序的额外文件。这些额外的文件可以在配置文件或者启动时另外指定存放目录。主要包括这几种类型的文件：\n服务器进程文件：每运行一个 MySQL 服务器程序，都意味着启动一个进程。MySQL 服务器会把自己的进程 ID 写入到一个 pid 文件中。 socket文件 默认/自动生成的 SSL 和 RSA 证书和密钥文件 5. bin log（二进制归档日志） binlog 二进制日志记录了对 MySQL 数据库所有执行过的修改操作，若操作本身没有导致数据库发生变化，该操作可能也会写入二进制文件。但是不包括 select 和 show 这类操作语句（因为这些操作对数据本身不会进行修改）。如果 MySQL 服务意外停止，可通过二进制日志文件排查，用户操作或表结构操作，从而来恢复数据库数据。\nTips: 开启 binlog 记录功能，会对 MySQL 的性能造成影响，但是性能损失十分有限。根据 MySQL 官方手册中的测试指明，开启二进制日志会使性能下降 1%。但如果需要恢复数据或主从复制功能，则好处则大于对服务器的影响。\n5.1. 二进制日志的作用 恢复（recovery）：某些数据的恢复需要二进制日志，例如，在一个数据库全备文件恢复后，用户可以通过二进制文件进行 point-in-time 的恢复 复制（replication）：其原理与恢复类似，通过复制和执行二进制日志使一台远程的 MySQL 数据库（一般称为 slave 或 standby）与一台 MySQL 数据库（一般称为 master 或 primary）进行实时同步 审计（audit）：用户可以通过二进制日志中的信息来进行审计，判断是否有对数据库进行注入的攻击 5.2. binlog 相关参数 5.2.1. 查询 binlog 参数 log-bin 参数用来控制是否开启二进制日志。通过以下命令可以查询相关二进制日志的参数：\n1 2 3 4 5 6 7 8 9 10 11 mysql\u0026gt; show variables like \u0026#39;%log_bin%\u0026#39;; +---------------------------------+------------------------------------------------------------+ | Variable_name | Value | +---------------------------------+------------------------------------------------------------+ | log_bin | ON | | log_bin_basename | D:\\development\\MySQL\\MySQL Server 8.0\\Data\\mysql-bin | | log_bin_index | D:\\development\\MySQL\\MySQL Server 8.0\\Data\\mysql-bin.index | | log_bin_trust_function_creators | OFF | | log_bin_use_v1_row_events | OFF | | sql_log_bin | ON | +---------------------------------+------------------------------------------------------------+ 参数说明：\nlog_bin：binlog：日志是否打开状态 log_bin_basename：是 binlog 日志的基本文件名，后面会追加标识来表示每一个文件，binlog 日志文件会滚动增加 log_bin_index：指定的是 binlog 文件的索引文件，这个文件管理了所有的 binlog 文件的目录。 sql_log_bin：sql 语句是否写入 binlog 文件，ON 代表需要写入；OFF 代表不需要写入。如果想在主库上执行一些操作，但不复制到 slave 库上，可以通过修改参数 sql_log_bin 来实现。比如，模拟主从同步复制异常。 5.2.2. 配置 binlog 参数 在 MySQL 5.7 版本中，binlog 默认是关闭的；8.0 版本默认是打开的。如果想要开启二进制日志的功能，需要修改 MySQL 的配置文件 my.ini(windows) 或 my.cnf(linux) 手动指定参数来启动，并重启数据库。\n在配置文件中的[mysqld]部分增加如下配置：\n1 2 3 4 5 6 7 8 9 10 11 # 开启日志 log-bin 设置 binlog 的存放位置，可以是绝对路径，也可以是相对路径，此示例是相对路径，则binlog文件默认会放在data数据目录下 log-bin=mysql-bin # Server Id 是数据库服务器id，随便写一个数都可以，这个id用来在mysql集群环境中标记唯一mysql服务器，集群环境中每台mysql服务器的id不能一样，不加启动会报错 server-id=1 # 其他配置 # 日志文件格式 binlog_format=row # 执行自动删除距离当前以前N天的binlog日志文件，默认为0，表示不自动删除 expire_logs_days=15 # 单个binlog日志文件的大小限制，默认为 1GB max_binlog_size=200M 配置说明：\nlog-bin：是否开启二进制日志。需要注意，该值是指定二进制日志文件的名称。如果不提供指定名称，那么数据库会使用默认的日志文件名（文件名为主机名，后缀名为二进制日志的序列号），且文件保存在数据库所在的目录（datadir下）。配置以后，就会在数据目录下产生类似于：bin_log.00001 即为二进制日志文件；bin_log.index 为二进制的索引文件，用来存储过往产生的二进制日志序号，通常情况下，不建议手动修改这个文件。 binlog_format：设置日志文件格式。可选值详见下面章节 expire_logs_days：设置执行自动删除距离当前以前该值的天数的 binlog 日志文件，默认值是0，表示不自动删除 max_binlog_size：设置单个 binlog 日志文件的大小限制，默认值为 1GB 5.2.3. binlog_format 日志记录格式 查看 binlog 日志的格式\n1 2 3 4 5 6 mysql\u0026gt; show variables like \u0026#39;binlog_format\u0026#39;; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | binlog_format | ROW | +---------------+-------+ STATEMENT（默认）：该日志格式在日志文件中记录的都是 SQL 语句（statement），每一条对数据进行修改的 SQL 都会记录在日志文件中，通过 Mysql 提供的 mysqlbinlog 工具，可以清晰的查看到每条语句的文本。主从复制的时候，从库（slave）会将日志解析为原文本，并在从库重新执行一次。这种方式日志量小，节约IO开销，提高性能，但是对于一些执行过程中才能确定结果的函数，比如UUID()、SYSDATE()等函数如果随 sql 同步到 slave 机器去执行，则结果跟 master 机器执行的不一样。 ROW：该日志格式在日志文件中记录的是每一行的数据变更，而不是记录 SQL 语句。比如执行 SQL 语句 update tb_book set status='1'，如果是 STATEMENT 日志格式，在日志中会记录一行 SQL 文件；如果是 ROW，由于是对全表进行更新，也就是每一行记录都会发生变更，ROW 格式的日志中会记录每一行的数据变更。但是由于很多操作，会导致大量行的改动(比如 alter table)，因此这种模式的文件保存的信息太多，日志量太大。这种方式可以解决函数、存储过程等在 slave 机器的复制问题，但性能不如Statement。 Tips: 新版的 MySQL 中对 row 级别也做了一些优化，当表结构发生变化的时候，会记录语句而不是逐行记录。\nMIXED：混合了 STATEMENT 和 ROW 两种格式。在 MIXED 模式下，MySQL 会根据执行的每一条具体的 sql 语句来区分对待记录的日志形式，也就是在 Statement 和 Row 之间选择一种，如果 sql 里有函数或一些在执行时才知道结果的情况，会选择 Row，其它情况选择 Statement。推荐使用此方式 5.2.4. sync_binlog 写入磁盘机制 binlog 写入磁盘机制主要通过 sync_binlog 参数控制，默认值是 0。\n1 2 3 4 5 6 mysql\u0026gt; show variables like \u0026#39;%sync_binlog%\u0026#39;; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | sync_binlog | 1 | +---------------+-------+ 参数配置说明：\n0：表示每次提交事务都只写到 page cache，由系统自行判断什么时候执行 fsync 系统函数写入磁盘。虽然性能得到提升，但是机器宕机时 page cache 里面的 binlog 会丢失。 1：表示每次提交事务都会执行 fsync 系统函数写入磁盘，这种方式最安全，是强一致的选择。 还可以设置为N(N\u0026gt;1)，是一种折中方式。表示每次提交事务都写到 page cache，但累积 N 个事务后才 fsync 写入磁盘，这种如果机器宕机会丢失 N 个事务的 binlog。 5.3. 删除 binlog 日志文件 语法：\n1 2 3 4 5 6 -- 删除当前所有的 binlog 文件 reset master; -- 删除指定日志文件之前的所有日志文件，下面这个是删除6之前的所有日志文件，当前这个文件不删除 purge master logs to \u0026#39;mysql-binlog.000006\u0026#39;; -- 删除指定日期前的日志索引中binlog日志文件 purge master logs before \u0026#39;2023-01-21 14:00:00\u0026#39;; 5.4. binlog 日志文件的重新生成 发生以下任一情况时，binlog 日志文件会重新生成：\n服务器启动或重新启动 服务器刷新日志，执行命令 flush logs 日志文件大小达到 max_binlog_size 值，默认值为 1GB 5.5. 查看 binlog 日志文件 5.5.1. 查询日志文件情况 1 2 3 4 5 -- 查看所有日志 show binlog events; -- 查看最新的日志 show master status; 查看当前有多少 binlog 文件\n1 2 3 4 5 6 mysql\u0026gt; show binary logs; +------------------+-----------+-----------+ | Log_name | File_size | Encrypted | +------------------+-----------+-----------+ | mysql-bin.000001 | 157 | No | +------------------+-----------+-----------+ 5.5.2. 查看日志内容 可以用 mysql 自带的命令工具 mysqlbinlog 查看 binlog 日志内容。注：不需要登录 mysql，如果在对应的命令工具所在目录，则需要配置系统变量\n1 2 3 4 5 # 查看bin-log二进制文件 mysqlbinlog --no-defaults -v --base64-output=decode-rows D:/dev/mysql-5.7.25-winx64/data/mysql-binlog.000007 # 查看bin-log二进制文件（带查询条件） mysqlbinlog --no-defaults -v --base64-output=decode-rows D:/dev/mysql-5.7.25-winx64/data/mysql-binlog.000007 start-datetime=\u0026#34;2023-01-21 00:00:00\u0026#34; stop-datetime=\u0026#34;2023-02-01 00:00:00\u0026#34; start-position=\u0026#34;5000\u0026#34; stop-position=\u0026#34;20000\u0026#34; 查出来的 binlog 日志文件内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 /*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/; /*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/; DELIMITER /*!*/; # at 4 #230127 21:13:51 server id 1 end_log_pos 123 CRC32 0x084f390f Start: binlog v 4, server v 5.7.25-log created 230127 21:13:51 at startup # Warning: this binlog is either in use or was not closed properly. ROLLBACK/*!*/; # at 123 #230127 21:13:51 server id 1 end_log_pos 154 CRC32 0x672ba207 Previous-GTIDs # [empty] # at 154 #230127 21:22:48 server id 1 end_log_pos 219 CRC32 0x8349d010 Anonymous_GTID last_committed=0 sequence_number=1 rbr_only=yes /*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/; SET @@SESSION.GTID_NEXT= \u0026#39;ANONYMOUS\u0026#39;/*!*/; # at 219 #230127 21:22:48 server id 1 end_log_pos 291 CRC32 0xbf49de02 Query thread_id=3 exec_time=0 error_code=0 SET TIMESTAMP=1674825768/*!*/; SET @@session.pseudo_thread_id=3/*!*/; SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/; SET @@session.sql_mode=1342177280/*!*/; SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/; /*!\\C utf8 *//*!*/; SET @@session.character_set_client=33,@@session.collation_connection=33,@@session.collation_server=33/*!*/; SET @@session.lc_time_names=0/*!*/; SET @@session.collation_database=DEFAULT/*!*/; BEGIN /*!*/; # at 291 #230127 21:22:48 server id 1 end_log_pos 345 CRC32 0xc4ab653e Table_map: `test`.`account` mapped to number 99 # at 345 #230127 21:22:48 server id 1 end_log_pos 413 CRC32 0x54a124bd Update_rows: table id 99 flags: STMT_END_F ### UPDATE `test`.`account` ### WHERE ### @1=1 ### @2=\u0026#39;lilei\u0026#39; ### @3=1000 ### SET ### @1=1 ### @2=\u0026#39;lilei\u0026#39; ### @3=2000 # at 413 #230127 21:22:48 server id 1 end_log_pos 444 CRC32 0x23355595 Xid = 10 COMMIT/*!*/; # at 444 .... 省略 5.6. binlog 日志文件恢复数据 用 binlog 日志文件恢复数据本质是，重新执行之前记录在 binlog 文件里的 sql。示例如下：\n先执行刷新日志的命令生成一个新的 binlog 文件 mysql-binlog.000008，从而让后续的修改操作日志都会记录在最新的这个文件里。\n1 flush logs; 执行两条插入语句\n1 2 3 4 INSERT INTO `test`.`account` (`id`, `name`, `balance`) VALUES (\u0026#39;4\u0026#39;, \u0026#39;MooN\u0026#39;, \u0026#39;666\u0026#39;); INSERT INTO `test`.`account` (`id`, `name`, `balance`) VALUES (\u0026#39;5\u0026#39;, \u0026#39;MoonZero\u0026#39;, \u0026#39;888\u0026#39;); -- 假设现在误操作执行了一条删除语句把刚新增的两条数据删掉了 delete from account where id \u0026gt; 3; 现在需要恢复被删除的两条数据，先查看binlog日志文件\n1 mysqlbinlog --no-defaults -v --base64-output=decode-rows D:/dev/mysql-5.7.25-winx64/data/mysql-binlog.000008 文件内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 SET @@SESSION.GTID_NEXT= \u0026#39;ANONYMOUS\u0026#39;/*!*/; # at 219 #230127 23:32:24 server id 1 end_log_pos 291 CRC32 0x4528234f Query thread_id=5 exec_time=0 error_code=0 SET TIMESTAMP=1674833544/*!*/; SET @@session.pseudo_thread_id=5/*!*/; SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/; SET @@session.sql_mode=1342177280/*!*/; SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/; /*!\\C utf8 *//*!*/; SET @@session.character_set_client=33,@@session.collation_connection=33,@@session.collation_server=33/*!*/; SET @@session.lc_time_names=0/*!*/; SET @@session.collation_database=DEFAULT/*!*/; BEGIN /*!*/; # at 291 #230127 23:32:24 server id 1 end_log_pos 345 CRC32 0x7482741d Table_map: `test`.`account` mapped to number 99 # at 345 #230127 23:32:24 server id 1 end_log_pos 396 CRC32 0x5e443cf0 Write_rows: table id 99 flags: STMT_END_F ### INSERT INTO `test`.`account` ### SET ### @1=4 ### @2=\u0026#39;MooN\u0026#39; ### @3=666 # at 396 #230127 23:32:24 server id 1 end_log_pos 427 CRC32 0x8a0d8a3c Xid = 56 COMMIT/*!*/; # at 427 #230127 23:32:40 server id 1 end_log_pos 492 CRC32 0x5261a37e Anonymous_GTID last_committed=1 sequence_number=2 rbr_only=yes /*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/; SET @@SESSION.GTID_NEXT= \u0026#39;ANONYMOUS\u0026#39;/*!*/; # at 492 #230127 23:32:40 server id 1 end_log_pos 564 CRC32 0x01086643 Query thread_id=5 exec_time=0 error_code=0 SET TIMESTAMP=1674833560/*!*/; BEGIN /*!*/; # at 564 #230127 23:32:40 server id 1 end_log_pos 618 CRC32 0xc26b6719 Table_map: `test`.`account` mapped to number 99 # at 618 #230127 23:32:40 server id 1 end_log_pos 670 CRC32 0x8e272176 Write_rows: table id 99 flags: STMT_END_F ### INSERT INTO `test`.`account` ### SET ### @1=5 ### @2=\u0026#39;MoonZero\u0026#39; ### @3=888 # at 670 #230127 23:32:40 server id 1 end_log_pos 701 CRC32 0xb5e63d00 Xid = 58 COMMIT/*!*/; # at 701 #230127 23:34:23 server id 1 end_log_pos 766 CRC32 0xa0844501 Anonymous_GTID last_committed=2 sequence_number=3 rbr_only=yes /*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/; SET @@SESSION.GTID_NEXT= \u0026#39;ANONYMOUS\u0026#39;/*!*/; # at 766 #230127 23:34:23 server id 1 end_log_pos 838 CRC32 0x687bdf88 Query thread_id=7 exec_time=0 error_code=0 SET TIMESTAMP=1674833663/*!*/; BEGIN /*!*/; # at 838 #230127 23:34:23 server id 1 end_log_pos 892 CRC32 0x4f7b7d6a Table_map: `test`.`account` mapped to number 99 # at 892 #230127 23:34:23 server id 1 end_log_pos 960 CRC32 0xc47ac777 Delete_rows: table id 99 flags: STMT_END_F ### DELETE FROM `test`.`account` ### WHERE ### @1=4 ### @2=\u0026#39;MooN\u0026#39; ### @3=666 ### DELETE FROM `test`.`account` ### WHERE ### @1=5 ### @2=\u0026#39;MoonZero\u0026#39; ### @3=888 # at 960 #230127 23:34:23 server id 1 end_log_pos 991 CRC32 0x386699fe Xid = 65 COMMIT/*!*/; SET @@SESSION.GTID_NEXT= \u0026#39;AUTOMATIC\u0026#39; /* added by mysqlbinlog */ /*!*/; DELIMITER ; # End of log file 找到两条插入数据的 sql，每条 sql 的上下都有 BEGIN 和 COMMIT，找到第一条 sql BEGIN 前面的文件位置标识 at 219(这是文件的位置标识)，再找到第二条 sql COMMIT 后面的文件位置标识 at 701，则可以根据文件位置标识来恢复数据，执行如下sql：\n1 mysqlbinlog --no-defaults --start-position=219 --stop-position=701 --database=test D:/dev/mysql-5.7.25-winx64/data/mysql-binlog.000009 | mysql -uroot -p123456 -v test 补充：根据时间来恢复数据的命令，找到第一条sql BEGIN前面的时间戳标记 SET TIMESTAMP=1674833544，再找到第二条sql COMMIT后面的时间戳标记 SET TIMESTAMP=1674833663，转成 datetime 格式，就可以恢复指定的时间段之间的数据：\n1 mysqlbinlog --no-defaults --start-datetime=\u0026#34;2023-1-27 23:32:24\u0026#34; --stop-datetime=\u0026#34;2023-1-27 23:34:23\u0026#34; --database=test D:/dev/mysql-5.7.25-winx64/data/mysql-binlog.000009 | mysql -uroot -p123456 -v test 5.7. MySQL binlog 典型的业务应用场景 binlog 的业务应用主要是：构建一个中间件系统，“伪装”成 master 的一个 slave 节点，从而感知数据的变化。当读取了 binlog 中的数据变化后，根据相应的业务场景做各种业务处理。\n5.7.1. 数据异构 一般系统会拆分多个模块，可能有一些表是各个业务都会关注，但是对相关的字段的运用场景不同，所以这样一份元数据怎样更好的为各个系统服务就成了问题。当然，多写或者读写分离可以从物理节点上减少对数据服务器的压力，但是对业务并没有做到足够的支持，因为这些表都是一样的。因此可以通过 binlog 进行数据异构。\n如图所示，订单系统生成订单后，通过 binlog 可以解析生成用户维度的订单信息供用户中心查询、商户维度订单表供运营管理，以及搜索系统的搜索数据，提供全文搜索功能。通过原始的订单数据异构到三个系统中，提供了丰富的数据访问功能。不仅从节点上降低了数据服务器的压力，数据表现形式也更贴近自己的服务，减少不必要的字段冗余。\n5.7.2. 缓存数据的补充 对于高并发的系统，数据库往往是系统性能的瓶颈，毕竟 IO 响应速度是远远小于 CPU 的运算速度的。因此，很多查询类服务都会在 CPU 与数据库之间加上一层缓存。即现从缓存获取，命中后直接返回，否则从 DB 中获取并存入缓存后返回。而如果原始数据变化了但缓存尚未超时，则缓存中的数据就是过时的数据。所以当数据有变更的时候需要主动修改缓存数据。\n当客户端更改了数据之后，中间件系统通过 binlog 获得数据变更，并同步到缓存中。这样就保证了缓存中数据有效性，减少了对数据库的调用，从而提高整体性能。\n5.7.3. 基于数据的任务分发 有时很多系统都依赖同一块重要数据，当这些数据发生变化的时候，需要调用其他相关系统的通知接口同步数据变化，或者 mq 消息告知变化并等待其主动同步。这两种情况都对原始系统造成了侵入。此时可以通过 binlog 进行任务分发。\n当原始业务系统修改数据后，不需要进行其他的业务关联。由调度系统读取 binlog 进行相应的任务分发、消息发送以及同步其他业务状态。这样可以将其他业务与原始业务系统解耦，并从数据的角度将所有管理功能放在了同一个调度系统中，责任清晰。\n5.8. 扩展知识 5.8.1. bin log 和 redo log 的区别 bin log 会记录所有日志记录，包括 InnoDB、MyISAM 等存储引擎的日志；而 redo log 只记录 innoDB 自身的事务日志。 bin log 只在事务提交前写入到磁盘，一个事务只写一次；而在事务进行过程，会有 redo log 不断写入磁盘。 bin log 是逻辑日志，记录的是SQL语句的原始逻辑；而 redo log 是物理日志，记录的是在某个数据页上做了什么修改。 5.8.2. 关于数据库备份与恢复配置建议 假设不小心将数据库所有数据都删除后要恢复数据，如果数据库之前没有备份，而所有的 binlog 日志都在的话，就从 binlog 第一个文件开始逐个恢复每个 binlog 文件里的数据。但这种一般不太可能实现，因为 binlog 日志比较大，通常都会定期删除的指定时间以前的 binlog 文件，所以一般不可能用 binlog 文件恢复整个数据库的。\n一般推荐的是每天(在凌晨后)需要做一次全量数据库备份，那么恢复数据库可以用最近的一次全量备份再加上备份时间点之后的 binlog 来恢复数据。\n备份数据库一般可以使用 mysqldump 命令工具（具体的使用及其参数作用详见后面章节）。示例如下：\n1 2 3 4 5 6 # 备份整个数据库 mysqldump -u root 数据库名\u0026gt;备份文件名; # 备份整个表 mysqldump -u root 数据库名 表名字\u0026gt;备份文件名; # 恢复整个数据库，test 为数据库名称，需要先建一个数据库 test mysql -u root test \u0026lt; 备份文件名 6. MySQL 中的默认系统库（了解扩展）（TODO mark: 补充中\u0026hellip;） MySQL 有几个系统数据库，这几个数据库包含了 MySQL 服务器运行过程中所需的一些信息以及一些运行状态信息\nperformance_schema：这个数据库里主要保存 MySQL 服务器运行过程中的一些状态信息，为 MySQL 服务器运行时状态的一个性能监控信息数据库。包括统计最近执行了哪些语句，在执行过程的每个阶段都花费了多长时间，内存的使用情况等等信息。 information_schema：这个数据库保存着 MySQL 服务器维护的所有其他数据库的信息元数据，包含了表、视图、触发器、列、索引字段类型及访问权限等等。这些信息并不是真实的用户数据，而是一些描述性信息。 sys：这个数据库主要是通过视图的形式把 information_schema 和 performance_schema 结合起来，让程序员可以更方便的了解 MySQL 服务器的一些性能信息，进行性能调优和诊断。 mysql：这个数据库核心，它存储了 MySQL 的用户账户和权限信息，一些存储过程、事件的定义信息，一些运行过程中产生的日志信息，一些帮助信息以及时区信息等。 -test：测试数据库。 6.1. performance_schema 系统库 6.1.1. 简介 MySQL 的 performance_schema 系统库是运行在较低级别的用于监控 MySQL Server 运行过程中的资源消耗、资源等待等情况的一个功能特性，有以下的特点：\n6.2. sys 系统库 TODO: 待整理\n6.3. information_schema 系统库 TODO: 待整理\n6.4. mysql 系统库（名称为mysql） TODO: 待整理\n7. InnoDB 存储引擎架构 7.1. Innodb 底层原理流程图 7.2. InnoDB 整体架构概述 7.2.1. InnoDB 逻辑存储结构 在 MySQL 默认的存储引擎 InnoDB 中，所有数据都被逻辑地存放在一个空间内，称为表空间（tablespace），而表空间则由段（sengment）、区（extent）、页（page）组成。InnoDB 的逻辑存储结构如下图所示：\n表空间：InnoDB 存储引擎逻辑结构的最高层，ibd 文件其实就是表空间文件，在表空间中可以包含多个 Segment 段。 如果用户启用了参数 innodb_file_per_table(在8.0版本中默认开启)，则每张表都会有一个表空间（xxx.ibd），一个 mysql 实例可以对应多个表空间，用于存储记录、索引等数据。 段：表空间是由各个段组成的，分为数据段（Leaf node segment）、索引段（Non-leaf node segment）、回滚段（Rollback segment）等。InnoDB 是索引组织表，数据段就是 B+ 树的叶子节点， 索引段即为B+树的非叶子节点。段用来管理多个Extent（区）。InnoDB 中对于段的管理，都是引擎自身完成，不需要人为对其控制。 区：是表空间的单元结构，每个区的大小为 1M。 默认情况下，InnoDB 存储引擎页大小为16K，即一个区中一共有64个连续的页。 页：是组成区的最小单元，页也是 InnoDB 存储引擎磁盘管理的最小单元，每个页的大小默认为 16KB。为了保证页的连续性，InnoDB 存储引擎每次从磁盘申请 4-5 个区。 行：InnoDB 存储引擎是面向行的，也就是说数据是按行进行存放的，在每一行中除了定义表时所指定的字段以外，还包含两个隐藏字段 Trx_id：每次对某条记录进行改动时，都会把对应的事务 id 赋值给 trx_id 隐藏列。 Roll_pointer：每次对某条引记录进行改动时，都会把旧的版本写入到 undo 日志中，然后这个隐藏列就相当于一个指针，可以通过它来找到该记录修改前的信息。 表空间包含多个段，段包含多个区，区包含多个页。这种层次结构帮助 InnoDB 有效地管理存储空间，并提供高性能和可靠性的数据访问。\n7.2.2. InnoDB 物理架构 MySQL5.5 版本开始，默认使用 InnoDB 存储引擎，它擅长事务处理，具有崩溃恢复特性，在日常开发中使用非常广泛。下面是 InnoDB 架构图，左侧为内存结构，右侧为磁盘结构。\n7.3. 内存结构 在左侧的内存结构中，主要分为四大块： Buffer Pool、Change Buffer、Adaptive Hash Index、Log Buffer。\n7.3.1. Buffer Pool InnoDB 存储引擎基于磁盘文件存储，访问物理硬盘和在内存中进行访问，速度相差很大，为了尽可能弥补这两者之间的 I/O 效率的差值，Innodb 存储引擎设计了一个缓冲池（Buffer Pool），来提高数据库的读写性能。\n缓冲池 Buffer Pool，是主内存中的一个区域，里面可以缓存磁盘上经常操作的真实数据，在执行增删改查操作时，先操作缓冲池中的数据（若缓冲池没有数据，则从磁盘加载并缓存），然后再以一定频率刷新到磁盘，从而减少磁盘IO，加快处理速度。\n缓冲池以 Page 页为单位，底层采用链表数据结构管理 Page。根据状态，将 Page 分为三种类型：\nfree page：空闲 page，未被使用。 clean page：被使用 page，数据没有被修改过。 dirty page：脏页，被使用 page，数据被修改过，也中数据与磁盘的数据产生了不一致。 7.3.1.1. Buffer Pool 大小 Buffer Pool 是在 MySQL 启动的时候，向操作系统申请的一片连续的内存空间，默认配置下 Buffer Pool 只有 128MB。在专用服务器上，通常建议配置 60%~80% 的物理内存分配给缓冲池。相关参数设置：innodb_buffer_pool_size\n1 2 3 4 5 6 mysql\u0026gt; show variables like \u0026#39;innodb_buffer_pool_size\u0026#39;; +-------------------------+---------+ | Variable_name | Value | +-------------------------+---------+ | innodb_buffer_pool_size | 8388608 | +-------------------------+---------+ 7.3.2. Change Buffer Change Buffer，更改缓冲区（针对于非唯一二级索引页），在执行 DML 语句时，如果这些数据 Page 没有在 Buffer Pool 中，不会直接操作磁盘，而会将数据变更存在更改缓冲区 Change Buffer 中，在未来数据被读取时，再将数据合并恢复到 Buffer Pool 中，再将合并后的数据刷新到磁盘中。\n以下是二级索引的结构图：\n与聚集索引不同，二级索引通常是非唯一的，并且以相对随机的顺序插入二级索引。同样，删除和更新可能会影响索引树中不相邻的二级索引页，如果每一次都操作磁盘，会造成大量的磁盘IO。有了 ChangeBuffer 之后，就可以在缓冲池中进行合并处理，减少磁盘IO。\n7.3.3. Adaptive Hash Index 自适应 hash 索引，用于优化对 Buffer Pool 数据的查询。MySQL 的 innoDB 引擎中虽然没有直接支持 hash 索引，但是给我们提供了一个功能就是这个自适应 hash 索引。因为前面我们讲到过，hash 索引在进行等值匹配时，一般性能是要高于B+树的，因为 hash 索引一般只需要一次IO即可，而B+树，可能需要几次匹配，所以 hash 索引的效率要高，但是 hash 索引又不适合做范围查询、模糊匹配等。\nInnoDB 存储引擎会监控对表上各索引页的查询，如果观察到在特定的条件下 hash 索引可以提升速度，则建立 hash 索引，称之为自适应 hash 索引。自适应哈希索引，无需人工干预，是系统根据情况自动完成。\n7.3.4. Log Buffer Log Buffer：日志缓冲区，用来保存要写入到磁盘中的log日志数据（redo log 、undo log），默认大小为 16MB，日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事务，增加日志缓冲区的大小可以节省磁盘 I/O。\n日志缓冲区相关参数：\ninnodb_log_buffer_size：缓冲区大小 innodb_flush_log_at_trx_commit：日志刷新到磁盘时机，取值主要包含以下三个： 日志在每次事务提交时写入并刷新到磁盘，默认值。 每秒将日志写入并刷新到磁盘一次。 日志在每次事务提交后写入，并每秒刷新到磁盘一次。 1 2 3 4 5 6 mysql\u0026gt; show variables like \u0026#39;innodb_flush_log_trx_commit\u0026#39;; +-----------------------------+---------+ | Variable_name | Value | +-----------------------------+---------+ | innodb_flush_log_trx_commit | 1 | +-----------------------------+---------+ 7.4. 磁盘结构 InnoDB 体系结构的右边部分，也就是磁盘结构：\nSystem Tablespace：系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建的，它也可能包含表和索引数据。(在MySQL5.x版本中还包含InnoDB数据字典、undolog等)。相关参数：innodb_data_file_path 1 2 3 4 5 6 mysql\u0026gt; show variables like \u0026#39;innodb_data_file_path\u0026#39;; +-----------------------+------------------------+ | Variable_name | Value | +-----------------------+------------------------+ | innodb_data_file_path | ibdata1:12M:autoextend | +-----------------------+------------------------+ 系统表空间，默认的文件名叫 ibdata1。\nFile-Per-Table Tablespaces：开关参数 innodb_file_per_table，该参数默认开启。如果开启该开关，则每个表的文件表空间包含单个InnoDB表的数据和索引，并存储在文件系统上的单个数据文件中。 1 2 3 4 5 6 mysql\u0026gt; show variables like \u0026#39;innodb_file_per_table\u0026#39;; +-----------------------+-------+ | Variable_name | Value | +-----------------------+-------+ | innodb_file_per_table | ON | +-----------------------+-------+ 即没创建一个表，都会产生一个表空间文件，如图：\nGeneral Tablespaces：通用表空间，需要通过 CREATE TABLESPACE 语法创建通用表空间，在创建表时，可以指定该表空间。 创建表空间 1 2 3 4 5 -- 语法 CREATE TABLESPACE ts_name ADD DATAFILE \u0026#39;file_name\u0026#39; ENGINE = engine_name; -- 示例 create tablespace ts_testdb add datafile \u0026#39;mytest.ibd\u0026#39; engine = innodb; 创建表时指定表空间 1 2 3 4 5 -- 语法 CREATE TABLE xxx ... TABLESPACE ts_name; -- 示例 create table a(id int primary key auto_increment, name varchar(10)) engine=innodb tablespace ts_testdb; Undo Tablespaces：撤销表空间，MySQL实例在初始化时会自动创建两个默认的undo表空间（初始大小16M），用于存储 undo log日志。 Temporary Tablespaces：InnoDB 使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。 Doublewrite Buffer Files：双写缓冲区，innoDB引擎将数据页从Buffer Pool刷新到磁盘前，先将数据页写入双写缓冲区文件中，便于系统异常时恢复数据。 Redo Log：重做日志，是用来实现事务的持久性。该日志文件由两部分组成：重做日志缓冲（redo log buffer）以及重做日志文件（redo log）,前者是在内存中，后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中，用于在刷新脏页到磁盘时，发生错误时，进行数据恢复使用。 以循环方式写入重做日志文件，涉及两个文件：\n7.5. 后台线程 在 InnoDB 的后台线程中，分为4类，分别是：Master Thread 、IO Thread、Purge Thread、Page Cleaner Thread。\nMaster Thread：核心后台线程，负责调度其他线程，还负责将缓冲池中的数据异步刷新到磁盘中, 保持数据的一致性，还包括脏页的刷新、合并插入缓存、undo页的回收 IO Thread：在 InnoDB 存储引擎中大量使用了AIO来处理IO请求，这样可以极大地提高数据库的性能，而IO Thread主要负责这些IO请求的回调 线程类型 默认个数 职责 Read thread 4 负责读操作 Write thread 4 负责写操作 Log thread 1 负责将日志缓冲区刷新到磁盘 Insert buffer thread 1 负责将写缓冲区内容刷新到磁盘 通过以下的这条指令，查看到 InnoDB 的状态信息，其中就包含IO Thread信息。\n1 show engine innodb status \\G; Purge Thread：主要用于回收事务已经提交了的undo log，在事务提交之后，undo log可能不用了，就用它来回收 Page Cleaner Thread：协助 Master Thread 刷新脏页到磁盘的线程，它可以减轻 Master Thread 的工作压力，减少阻塞 7.6. 数据页结构 一个数据页大致划分七个部分\nFile Header：表示页的一些通用信息，占固定的 38 字节。 page Header：表示数据页专有信息，占固定的 56 字节。 inimum+Supermum：两个虚拟的伪记录，分别表示页中的最小记录和最大记录，占固定的 26 字节。 User Records：真正存储我们插入的数据，大小不固定。 Free Space：页中尚未使用的部分，大小不固定。 Page Directory：页中某些记录的相对位置，也就是各个槽对应的记录在页面中的地址偏移量。 File Trailer：用于检验页是否完整，占固定大小 8 字节。 8. 扩展内容 8.1. 为什么 Mysql 更新数据时不直接刷新到磁盘 为什么 Mysql 不能直接更新磁盘上的数据而设置这么一套复杂的机制来执行 SQL 了？\n当一个请求就直接对磁盘文件进行随机读写，然后更新磁盘文件里的数据性能可能相当差。因为磁盘随机读写的性能是非常差的，所以直接更新磁盘文件是不能让数据库抗住很高并发的。\nMysql 这套机制看起来复杂，但它可以保证每个更新请求都是更新内存 BufferPool，然后顺序写日志文件，同时还能保证各种异常情况下的数据一致性。更新内存的性能是极高的，然后顺序写磁盘上的日志文件的性能也是非常高的，要远高于随机读写磁盘文件。正是通过这套机制，才能让 MySQL 数据库在较高配置的机器上每秒可以抗下几干甚至上万的读写请求。\n","permalink":"https://ktzxy.top/posts/nk93ibpo4a/","summary":"MySQL 体系架构","title":"MySQL 体系架构"},{"content":" MySQL数据恢复大法 相关文章\nMySQL备份策略\nhttps://sourl.cn/2EfkX5\nMySQL数据恢复\nhttps://sourl.cn/X4hXCK\n数据恢复的前提的做好备份，且开启 binlog, 格式为 row。\n如果没有备份文件，那么删掉库表后就真的删掉了，lsof 中还有记录的话，有可能恢复一部分文件，但若刚好数据库没有打开这个表文件，那就只能跑路了。\n如果没有开启 binlog，那么恢复数据后，从备份时间点开始的数据都没得了。\n如果 binlog 格式不为 row，那么在误操作数据后就没有办法做闪回操作，只能老老实实地走备份恢复流程。\n1 直接恢复 直接恢复是使用备份文件做全量恢复，这是最常见的场景。\n1.1 mysqldump备份全量恢复 使用 mysqldump 文件恢复数据非常简单，直接解压了执行\n1 gzip -d backup.sql.gz | mysql -u\u0026lt;user\u0026gt; -h\u0026lt;host\u0026gt; -P\u0026lt;port\u0026gt; -p 1.2 xtrabackup备份全量恢复 恢复过程\n1 2 3 4 5 6 7 8 # 步骤一：解压（如果没有压缩可以忽略这一步） innobackupex --decompress \u0026lt;备份文件所在目录\u0026gt; # 步骤二：应用日志 innobackupex --apply-log \u0026lt;备份文件所在目录\u0026gt; # 步骤三：复制备份文件到数据目录 innobackupex --datadir=\u0026lt;MySQL数据目录\u0026gt; --copy-back \u0026lt;备份文件所在目录\u0026gt; 1.3 基于时间点恢复 基于时间点的恢复依赖的是binlog日志，需要从 binlog 中找过从备份点到恢复点的所有日志，然后应用，我们测试一下\n新建测试表\n1 2 3 4 5 6 7 8 chengqm-3306\u0026gt;\u0026gt;show create table mytest.mytest \\G; *************************** 1. row *************************** Table: mytest Create Table: CREATE TABLE `mytest` ( `id` int(11) NOT NULL AUTO_INCREMENT, `ctime` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 每秒插入一条数据\n1 [mysql@mysql-test ~]$ while true; do mysql -S /tmp/mysql.sock -e \u0026#39;insert into mytest.mytest(ctime)values(now())\u0026#39;;date;sleep 1;done 备份\n1 [mysql@mysql-test ~]$ mysqldump --opt --single-transaction --master-data=2 --default-character-set=utf8 -S /tmp/mysql.sock -A \u0026gt; backup.sql 找出备份时的日志位置\n1 2 [mysql@mysql-test ~]$ head -n 25 backup.sql | grep \u0026#39;CHANGE MASTER TO MASTER_LOG_FILE\u0026#39; -- CHANGE MASTER TO MASTER_LOG_FILE=\u0026#39;mysql-bin.000032\u0026#39;, MASTER_LOG_POS=39654; 假设要恢复到 2019-08-09 11:01:54 这个时间点，我们从 binlog 中查找从 39654 到 019-08-09 11:01:54 的日志\n1 2 3 4 5 6 7 8 [mysql@mysql-test ~]$ mysqlbinlog --start-position=39654 --stop-datetime=\u0026#39;2019-08-09 11:01:54\u0026#39; /data/mysql_log/mysql_test/mysql-bin.000032 \u0026gt; backup_inc.sql [mysql@mysql-test-83 ~]$ tail -n 20 backup_inc.sql ...... ### INSERT INTO `mytest`.`mytest` ### SET ### @1=161 /* INT meta=0 nullable=0 is_null=0 */ ### @2=\u0026#39;2019-08-09 11:01:53\u0026#39; /* DATETIME(0) meta=0 nullable=1 is_null=0 */ ...... 当前数据条数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 -- 2019-08-09 11:01:54之前的数据条数 chengqm-3306\u0026gt;\u0026gt;select count(*) from mytest.mytest where ctime \u0026lt; \u0026#39;2019-08-09 11:01:54\u0026#39;; +----------+ | count(*) | +----------+ | 161 | +----------+ 1 row in set (0.00 sec) -- 所有数据条数 chengqm-3306\u0026gt;\u0026gt;select count(*) from mytest.mytest; +----------+ | count(*) | +----------+ | 180 | +----------+ 1 row in set (0.00 sec) 然后执行恢复\n1 2 3 4 5 # 全量恢复 [mysql@mysql-test ~]$ mysql -S /tmp/mysql.sock \u0026lt; backup.sql # 应用增量日志 [mysql@mysql-test ~]$ mysql -S /tmp/mysql.sock \u0026lt; backup_inc.sql 检查数据\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 chengqm-3306\u0026gt;\u0026gt;select count(*) from mytest.mytest; +----------+ | count(*) | +----------+ | 161 | +----------+ 1 row in set (0.00 sec) chengqm-3306\u0026gt;\u0026gt;select * from mytest.mytest order by id desc limit 5; +-----+---------------------+ | id | ctime | +-----+---------------------+ | 161 | 2019-08-09 11:01:53 | | 160 | 2019-08-09 11:01:52 | | 159 | 2019-08-09 11:01:51 | | 158 | 2019-08-09 11:01:50 | | 157 | 2019-08-09 11:01:49 | +-----+---------------------+ 5 rows in set (0.00 sec) 已经恢复到 2019-08-09 11:01:54 这个时间点\n2 恢复一个表 2.1 从mysqldump备份恢复一个表 假设要恢复的表是 mytest.mytest\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 提取某个库的所有数据 sed -n \u0026#39;/^-- Current Database: `mytest`/,/^-- Current Database:/p\u0026#39; backup.sql \u0026gt; backup_mytest.sql # 从库备份文件中提取建表语句 sed -e\u0026#39;/./{H;$!d;}\u0026#39; -e \u0026#39;x;/CREATE TABLE `mytest`/!d;q\u0026#39; backup_mytest.sql \u0026gt; mytest_table_create.sql # 从库备份文件中提取插入数据语句 grep -i \u0026#39;INSERT INTO `mytest`\u0026#39; backup_mytest.sql \u0026gt; mytest_table_insert.sql # 恢复表结构到 mytest 库 mysql -u\u0026lt;user\u0026gt; -p mytest \u0026lt; mytest_table_create.sql # 恢复表数据到 mytest.mytest 表 mysql -u\u0026lt;user\u0026gt; -p mytest \u0026lt; mytest_table_insert.sql 2.2 从xtrabackup备份恢复一个表 假设 ./backup_xtra_full 目录为解压后应用过日志的备份文件\n2.2.1 MyISAM 表 假设从备份文件中恢复表 mytest.t_myisam，从备份文件中找到 t_myisam.frm t_myisam.MYD t_myisam.MYI 这 3 个文件，复制到对应的数据目录中，并授权。\n进入 MySQL，检查表情况\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 chengqm-3306\u0026gt;\u0026gt;show tables; +------------------+ | Tables_in_mytest | +------------------+ | mytest | | t_myisam | +------------------+ 2 rows in set (0.00 sec) chengqm-3306\u0026gt;\u0026gt;check table t_myisam; +-----------------+-------+----------+----------+ | Table | Op | Msg_type | Msg_text | +-----------------+-------+----------+----------+ | mytest.t_myisam | check | status | OK | +-----------------+-------+----------+----------+ 1 row in set (0.00 sec) 2.2.2 Innodb 表 假设从备份文件中恢复表 mytest.t_innodb，恢复前提是设置了 innodb_file_per_table = on\n起一个新实例 在实例上建一个和原来一模一样的表 执行 alter table t_innodb discard tablespace;，删除表空间，这个操作会把 t_innodb.ibd 删除 从备份文件中找到 t_innodb.ibd 这个文件，复制到对应的数据目录，并授权 执行 alter table t_innodb IMPORT tablespace; 加载表空间 执行 flush table t_innodb;check table t_innodb; 检查表 使用 mysqldump 导出数据，然后再导入到要恢复的数据库 注意：\n在新实例上恢复再dump出来是为了避免风险，如果是测试，可以直接在原库上操作步骤 2-6 只在 8.0 以前的版本有效 跳过误操作SQL\n3 跳过误操作 SQL 一般用于执行了无法闪回的操作比如 drop table\\database\n3.1 使用备份文件恢复跳过 不开启 GTID 使用备份文件恢复的步骤和基于时间点恢复的操作差不多，区别在于多一个查找 binlog 操作\n举个例子，我这里建立了两个表 a 和 b，每分钟插入一条数据，然后做全量备份，再删除表 b，现在要跳过这条 SQL。\n删除表 b 后的数据库状态\n1 2 3 4 5 6 7 chgnqm-3306\u0026gt;\u0026gt;show tables; +------------------+ | Tables_in_mytest | +------------------+ | a | +------------------+ 1 row in set (0.00 sec) 1 找出备份时的日志位置\n1 2 [mysql@mysql-test ~]$ head -n 25 backup.sql | grep \u0026#39;CHANGE MASTER TO MASTER_LOG_FILE\u0026#39; -- CHANGE MASTER TO MASTER_LOG_FILE=\u0026#39;mysql-bin.000034\u0026#39;, MASTER_LOG_POS=38414; 2 找出执行了 drop table 语句的 pos 位置\n1 2 3 4 5 [mysql@mysql-test mysql_test]$ mysqlbinlog -vv /data/mysql_log/mysql_test/mysql-bin.000034 | grep -i -B 3 \u0026#39;drop table `b`\u0026#39;; # at 120629 #190818 19:48:30 server id 83 end_log_pos 120747 CRC32 0x6dd6ab2a Query thread_id=29488 exec_time=0 error_code=0 SET TIMESTAMP=1566128910/*!*/; DROP TABLE `b` /* generated by server */ 从结果中我们可以看到 drop 所在语句的开始位置是 120629，结束位置是 120747\n3 从 binglog 中提取跳过这条语句的其他记录\n1 2 3 4 5 # 第一条的 start-position 为备份文件的 pos 位置，stop-position 为 drop 语句的开始位置 mysqlbinlog -vv --start-position=38414 --stop-position=120629 /data/mysql_log/mysql_test/mysql-bin.000034 \u0026gt; backup_inc_1.sql # 第二条的 start-position 为 drop 语句的结束位置 mysqlbinlog -vv --start-position=120747 /data/mysql_log/mysql_test/mysql-bin.000034 \u0026gt; backup_inc_2.sql 4 恢复备份文件\n1 [mysql@mysql-test ~]$ mysql -S /tmp/mysql.sock \u0026lt; backup.sql 全量恢复后状态\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 chgnqm-3306\u0026gt;\u0026gt;show tables; +------------------+ | Tables_in_mytest | +------------------+ | a | | b | +------------------+ 2 rows in set (0.00 sec) chgnqm-3306\u0026gt;\u0026gt;select count(*) from a; +----------+ | count(*) | +----------+ | 71 | +----------+ 1 row in set (0.00 sec) 5 恢复增量数据\n1 2 [mysql@mysql-test ~]$ mysql -S /tmp/mysql.sock \u0026lt; backup_inc_1.sql [mysql@mysql-test ~]$ mysql -S /tmp/mysql.sock \u0026lt; backup_inc_2.sql 恢复后状态，可以看到已经跳过了 drop 语句\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 chgnqm-3306\u0026gt;\u0026gt;show tables; +------------------+ | Tables_in_mytest | +------------------+ | a | | b | +------------------+ 2 rows in set (0.00 sec) chgnqm-3306\u0026gt;\u0026gt;select count(*) from a; +----------+ | count(*) | +----------+ | 274 | +----------+ 1 row in set (0.00 sec) 开启 GTID 使用 GTID 可以直接跳过错误的 SQL\n找出备份时的日志位置 找出执行了 drop table 语句的 GTID 值 导出备份时日志位置到最新的 binglog 日志 恢复备份文件 跳过这个 GTID 1 2 3 SET SESSION GTID_NEXT=\u0026#39;对应的 GTID 值\u0026#39;; BEGIN; COMMIT; SET SESSION GTID_NEXT = AUTOMATIC; 应用步骤 3 得到的增量 binlog 日志 3.2 使用延迟库跳过 不开启 GTID 使用延迟库恢复的关键操作在于 start slave until\n我在测试环境搭建了两个 MySQL 节点，节点二延迟600秒，新建 a，b 两个表，每秒插入一条数据模拟业务数据插入。\n1 localhost:3306 -\u0026gt; localhost:3307(delay 600) 当前节点二状态\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 chengqm-3307\u0026gt;\u0026gt;show slave status \\G; ... Master_Port: 3306 Connect_Retry: 60 Master_Log_File: mysql-bin.000039 Read_Master_Log_Pos: 15524 Relay_Log_File: mysql-relay-bin.000002 Relay_Log_Pos: 22845 Relay_Master_Log_File: mysql-bin.000038 Slave_IO_Running: Yes Slave_SQL_Running: Yes ... Seconds_Behind_Master: 600 ... 当前节点二表\n1 2 3 4 5 6 7 chengqm-3307\u0026gt;\u0026gt;show tables; +------------------+ | Tables_in_mytest | +------------------+ | a | | b | +------------------+ 在节点一删除表 b\n1 2 3 4 5 6 7 8 9 10 chengqm-3306\u0026gt;\u0026gt;drop table b; Query OK, 0 rows affected (0.00 sec) chengqm-3306\u0026gt;\u0026gt;show tables; +------------------+ | Tables_in_mytest | +------------------+ | a | +------------------+ 1 row in set (0.00 sec) 接下来就是跳过这条 SQL 的操作步骤\n1 延迟库停止同步\n1 stop slave; 2 找出执行了 drop table 语句的前一句的 pos 位置\n1 2 3 4 5 6 7 8 9 10 [mysql@mysql-test ~]$ mysqlbinlog -vv /data/mysql_log/mysql_test/mysql-bin.000039 | grep -i -B 10 \u0026#39;drop table `b`\u0026#39;; ... # at 35134 #190819 11:40:25 server id 83 end_log_pos 35199 CRC32 0x02771167 Anonymous_GTID last_committed=132 sequence_number=133 rbr_only=no SET @@SESSION.GTID_NEXT= \u0026#39;ANONYMOUS\u0026#39;/*!*/; # at 35199 #190819 11:40:25 server id 83 end_log_pos 35317 CRC32 0x50a018aa Query thread_id=37155 exec_time=0 error_code=0 use `mytest`/*!*/; SET TIMESTAMP=1566186025/*!*/; DROP TABLE `b` /* generated by server */ 从结果中我们可以看到 drop 所在语句的前一句开始位置是 35134，所以我们同步到 35134 (这个可别选错了)\n3 延迟库同步到要跳过的 SQL 前一条\n1 2 change master to master_delay=0; start slave until master_log_file=\u0026#39;mysql-bin.000039\u0026#39;,master_log_pos=35134; 查看状态看到已经同步到对应节点\n1 2 3 4 5 6 7 8 9 10 11 12 13 chengqm-3307\u0026gt;\u0026gt;show slave status \\G; ... Master_Port: 3306 Connect_Retry: 60 Master_Log_File: mysql-bin.000039 Read_Master_Log_Pos: 65792 ... Slave_IO_Running: Yes Slave_SQL_Running: No Exec_Master_Log_Pos: 35134 ... Until_Log_File: mysql-bin.000039 Until_Log_Pos: 35134 4 跳过一条 SQL 后开始同步\n1 2 set global sql_slave_skip_counter=1; start slave; 查看同步状态，删除表 b 的语句已经被跳过\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 chengqm-3307\u0026gt;\u0026gt;show slave status \\G; ... Slave_IO_Running: Yes Slave_SQL_Running: Yes ... 1 row in set (0.00 sec) chengqm-3307\u0026gt;\u0026gt;show tables; +------------------+ | Tables_in_mytest | +------------------+ | a | | b | +------------------+ 2 rows in set (0.00 sec) 开启 GTID 使用 GTID 跳过的步骤会简单很多，只要执行一条和要跳过的 SQL 的 GTID 相同的事务就可以跳过了\n停止同步 找出执行了 drop table 语句的 GTID 执行这个 GTID 的事务 1 2 3 SET SESSION GTID_NEXT=\u0026#39;对应的 GTID 值\u0026#39;; BEGIN; COMMIT; SET SESSION GTID_NEXT = AUTOMATIC; 继续同步 4 闪回 闪回操作就是反向操作，比如执行了 delete from a where id=1，闪回就会执行对应的插入操作 insert into a (id,\u0026hellip;) values(1,\u0026hellip;)，用于误操作数据，只对 DML 语句有效，且要求 binlog 格式设为 ROW。本章介绍两个比较好用的开源工具\n4.1 binlog2sql binlog2sql 是大众点评开源的一款用于解析 binlog 的工具，可以用于生成闪回语句，项目地址 binlog2sql（https://sourl.cn/ZuNJPN）\n安装\n1 2 3 4 5 6 wget https://github.com/danfengcao/binlog2sql/archive/master.zip -O binlog2sql.zip unzip binlog2sql.zip cd binlog2sql-master/ # 安装依赖 pip install -r requirements.txt 生成回滚SQL\n1 2 3 4 5 6 7 8 9 10 11 python binlog2sql/binlog2sql.py --flashback \\ -h\u0026lt;host\u0026gt; -P\u0026lt;port\u0026gt; -u\u0026lt;user\u0026gt; -p\u0026#39;\u0026lt;password\u0026gt;\u0026#39; -d\u0026lt;dbname\u0026gt; -t\u0026lt;table_name\u0026gt;\\ --start-file=\u0026#39;\u0026lt;binlog_file\u0026gt;\u0026#39; \\ --start-datetime=\u0026#39;\u0026lt;start_time\u0026gt;\u0026#39; \\ --stop-datetime=\u0026#39;\u0026lt;stop_time\u0026gt;\u0026#39; \u0026gt; ./flashback.sql python binlog2sql/binlog2sql.py --flashback \\ -h\u0026lt;host\u0026gt; -P\u0026lt;port\u0026gt; -u\u0026lt;user\u0026gt; -p\u0026#39;\u0026lt;password\u0026gt;\u0026#39; -d\u0026lt;dbname\u0026gt; -t\u0026lt;table_name\u0026gt; \\ --start-file=\u0026#39;\u0026lt;binlog_file\u0026gt;\u0026#39; \\ --start-position=\u0026lt;start_pos\u0026gt; \\ --stop-position=\u0026lt;stop_pos\u0026gt; \u0026gt; ./flashback.sql 4.2 MyFlash MyFlash 是由美团点评公司技术工程部开发维护的一个回滚 DML 操作的工具，项目链接 MyFlash\n限制:\nbinlog格式必须为row,且 binlog_row_image=full 仅支持5.6与5.7 只能回滚DML（增、删、改） 安装\n1 2 3 4 5 6 7 8 9 10 11 # 依赖(centos) yum install gcc* pkg-config glib2 libgnomeui-devel -y # 下载文件 wget https://github.com/Meituan-Dianping/MyFlash/archive/master.zip -O MyFlash.zip unzip MyFlash.zip cd MyFlash-master # 编译安装 gcc -w `pkg-config --cflags --libs glib-2.0` source/binlogParseGlib.c -o binary/flashback mv binary /usr/local/MyFlash ln -s /usr/local/MyFlash/flashback /usr/bin/flashback 使用\n生成回滚语句\n1 flashback --databaseNames=\u0026lt;dbname\u0026gt; --binlogFileNames=\u0026lt;binlog_file\u0026gt; --start-position=\u0026lt;start_pos\u0026gt; --stop-position=\u0026lt;stop_pos\u0026gt; 执行后会生成 binlog_output_base.flashback 文件，需要用 mysqlb****inlog 解析出来再使用。\n1 mysqlbinlog -vv binlog_output_base.flashback | mysql -u\u0026lt;user\u0026gt; -p ","permalink":"https://ktzxy.top/posts/95fp70i8u7/","summary":"MySQL数据恢复大法","title":"MySQL数据恢复大法"},{"content":"一、通过 show status 命令了解各种 sql 的执行频率 mysql 客户端连接成功后，通过 show [session|global] status 命令可以提供服务器状态信息，也可以在操作系统上使用 mysqladmin extend-status 命令获取这些消息。 show status 命令中间可以加入选项 session（默认） 或 global：\nsession （当前连接） global （自数据上次启动至今） 1 2 # Com_xxx 表示每个 xxx 语句执行的次数。 mysql\u0026gt; show status like \u0026#39;Com_%\u0026#39;; 通常比较关心的是以下几个统计参数：\nCom_select : 执行 select 操作的次数，一次查询只累加 1。 Com_insert : 执行 insert 操作的次数，对于批量插入的 insert 操作，只累加一次。 Com_update : 执行 update 操作的次数。 Com_delete : 执行 delete 操作的次数。 上面这些参数对于所有存储引擎的表操作都会进行累计。下面这几个参数只是针对 innodb 的，累加的算法也略有不同：\nInnodb_rows_read : select 查询返回的行数。 Innodb_rows_inserted : 执行 insert 操作插入的行数。 Innodb_rows_updated : 执行 update 操作更新的行数。 Innodb_rows_deleted : 执行 delete 操作删除的行数。 通过以上几个参数，可以很容易地了解当前数据库的应用是以插入更新为主还是以查询操作为主，以及各种类型的 sql 大致的执行比例是多少。对于更新操作的计数，是对执行次数的计数，不论提交还是回滚都会进行累加。\n对于事务型的应用，通过 Com_commit 和 Com_rollback 可以了解事务提交和回滚的情况，对于回滚操作非常频繁的数据库，可能意味着应用编写存在问题。 此外，以下几个参数便于用户了解数据库的基本情况：\nConnections ： 试图连接 mysql 服务器的次数。 Uptime ： 服务器工作时间。 Slow_queries : 慢查询次数。 二、定义执行效率较低的 sql 语句 通过慢查询日志定位那些执行效率较低的 sql 语句，用 \u0026ndash;log-slow-queries[=file_name] 选项启动时，mysqld 写一个包含所有执行时间超过 long_query_time 秒的 sql 语句的日志文件。\n慢查询日志在查询结束以后才记录，所以在应用反映执行效率出现问题的时候慢查询日志并不能定位问题，可以使用 show processlist 命令查看当前 mysql 在进行的线程，包括线程的状态、是否锁表等，可以实时的查看 sql 的执行情况，同时对一些锁表操作进行优化。\n三、通过 explain 分析低效 sql 的执行计划 四、通过 performance_schema 分析 sql 性能 五、通过 trace 分析优化器如何选择执行计划。 mysql5.6 提供了对 sql 的跟踪 trace，可以进一步了解为什么优化器选择 A 执行计划而不是 B 执行计划，帮助我们更好的理解优化器的行为。\n使用方式：首先打开 trace ，设置格式为 json，设置 trace 最大能够使用的内存大小，避免解析过程中因为默认内存过小而不能够完整显示。\n","permalink":"https://ktzxy.top/posts/8ni22mjzb6/","summary":"优化 sql 语句的一般步骤","title":"优化 sql 语句的一般步骤"},{"content":" MySQL在处理group by语句时，最常规的方式是扫描整个表，然后创建一个临时表，使用临时表存储分组和聚合函数的值。但是在一些特别的场景下，通过索引可以避免创建临时表，以获取更好的性能。\ngroup by 子句使用索引的先决条件是group by的字段必须都在同一个索引里，这样才能使用索引已经排好序的特性。group by 走索引，通常有两种索引扫描方式。\n松散索引扫描，Loose Index Scan 紧凑索引扫描，Tight Index Scan 一、松散索引扫描 最有效的处理group by的方式就是通过索引能够直接获取到所有分组字段，这种方式能够利用索引本身有序的特性，另外通过where条件，可以不用扫描索引中的所有key，只需要读取与分组数量相同的key数量，这个数量显然大多数场景下会比索引中所有key的数量要小很多。尤其对于范围分组查询，松散索引扫描从每个分组的第一个key开始查找，而不是从所有key的最小值那个开始查找。\n松散索引扫描需要满足一定的条件：\n查询只能涉及一张表 group by字段只能是一个索引的最左前缀，且不能有其他的字段。比如一个表有一个联合索引(c1,c2,c3)，group by c1,c2 可以使用松散索引扫描，而group by c2,c3 或者 group by c1,c2,c4 则不能使用松散索引扫描 聚合函数只能使用min或者max，如果这两个函数同时使用的话，只能在同一个字段上使用，这个字段也必须在索引中。看一个示例，如下： select c1,c2,min(c3),max(c3) from t1 group by c1,c2; 联合索引：(c1,c2,c3) 联合索引中的字段，除了group by 以外的，必须为常量。示例如下： SELECT c1, c2 FROM t1 WHERE c3 = const GROUP BY c1, c2; 联合索引：(c1,c2,c3) 联合索引中的字段必须是整个字段值被索引，而不能是前缀，比如(c1(10),c2,c3) 如果group by 可以走松散索引扫描，那么执行计划中的Extra字段将会显示为：Using index for group-by。\n二、紧凑索引扫描 紧凑索引扫描会根据条件，对索引进行一次全索引扫描或者一个范围的索引扫描。相对于松散索引扫描只需要扫描部分满足条件的key，紧凑索引扫描需要扫描全部索引或者整个范围索引里面的key。\n紧凑索引扫描示例：\nSELECT c1, c2, c3 FROM t1 WHERE c2 = \u0026lsquo;a\u0026rsquo; GROUP BY c1, c3;\nSELECT c1, c2, c3 FROM t1 WHERE c1 = \u0026lsquo;a\u0026rsquo; GROUP BY c2, c3;\n联合索引：idx(c1,c2,c3)\n三、优化建议： group by 尽量通过走索引而减少临时表、排序、全表扫描。尽可能创造走松散索引扫描的条件，其次是紧凑索引扫描，通过已有索引，提高group by 性能。\n","permalink":"https://ktzxy.top/posts/a5md3em355/","summary":"MySQL性能优化 group by语句优化","title":"MySQL性能优化 group by语句优化"},{"content":"1. Swappiness swappiness内核参数将在内存不够用的条件下，决定内存换页的策略。该参数取值范围0~100，取值越大，内核会越积极地使用swap分区，设置为0表示最大限度使用物理内存，对于数据库应用，建议该参数设置为0。\n查看当前的swappiness值： cat /proc/sys/vm/swappiness\n设置swappiness值： echo 0 \u0026gt; /proc/sys/vm/swappiness\n持久化到配置文件： /etc/sysctl.conf vm.swappiness=0\n2. I/O Scheduler 数据库对I/O性能要求高，存储数据、读取数据都需要大量消耗I/O，因此磁盘I/O调度策略是数据库需要关注的重点内核参数。\n目前 Linux 上有如下几种 I/O 调度算法：\nnoop(No Operation)，noop调度算法是内核中最简单的IO调度算法。noop调度算法也叫作电梯调度算法，它将IO请求放入到一个FIFO队列中，然后逐个执行这些IO请求，当然对于一些在磁盘上连续的IO请求，noop算法会适当做一些合并。这个调度算法特别适合那些不希望调度器重新组织IO请求顺序的应用。 cfq(Completely Fair Scheduler )，完全公平调度器，它试图为竞争块设备使用权的所有进程分配一个请求队列和一个时间片，在调度器分配给进程的时间片内，进程可以将其读写请求发送给底层块设备，当进程的时间片消耗完，进程的请求队列将被挂起，等待调度，进程平均使用IO带宽。 deadline，deadline算法的核心在于保证每个IO请求在一定的时间内一定要被服务到，以此来避免某个请求饥饿。 anticipatory，启发式调度，类似 deadline 算法，但是引入预测机制提高性能。 CentOS 7.x 默认支持的是deadline算法，CentOS 6.x 下默认支持的cfq算法，而一般我们会在SSD固态盘硬盘环境中使用noop算法。\n查看I/O Scheduler值： cat /sys/block/sdb/queue/scheduler\n设置I/O Scheduler值： sudo echo noop \u0026gt; /sys/block/sdb/queue/scheduler\n3. TCP 3.1 tcp接受连接设置 net.core.netdev_max_backlog = 65535，当网络接收速率大于内核处理速率时，允许发送到队列中的包数目。 net.ipv4.tcp_max_syn_backlog = 65535，表示SYN队列长度，默认1024，改成65535，可以容纳更多等待连接的网络连接数。 net.core.somaxconn = 65535，每个端口监听队列的最大长度。 3.2 tcp失效连接资源回收 对于tcp失效连接占用系统资源的优化，加快资源回收效率：\nnet.ipv4.tcp_keepalive_time = 30，表示当keepalive启用的时候，TCP发送keepalive消息的频度，默认为2小时，改为30秒。 net.ipv4.tcp_keepalive_intvl = 3，tcp未获得响应时重发间隔。 net.ipv4.tcp_keepalive_probes = 3，tcp未获得响应时重发数量。 控制tcp连接等待时间，加快tcp链接回收：\nnet.ipv4.tcp_fin_timeout = 10，如果套接字由本端要求关闭，这个参数决定了它保持在FIN-WAIT-2状态的时间。 net.ipv4.tcp_tw_reuse = 1，开启socket重用,允许将TIME-WAIT socket重新用于新的TCP连接，默认为0，表示关闭。 net.ipv4.tcp_tw_recycle = 1，开启TCP连接中TIME-WAIT socket的快速回收，默认为0，表示关闭。 3.3 tcp接受连接缓冲区大小 控制tcp接受缓冲区的大小，设置大一些比较好：\nnet.core.wmem_default = 8388608 net.core.wmem_max = 16777216 net.core.rmem_default = 8388608 net.core.rmem_max = 16777216 4. 最大文件句柄 操作系统最大文件句柄数限制： vim /etc/security/limits.conf\nsoft nofile 65535 hard nofile 65535 5. CPU Governor 大多数现代处理器都能在许多不同的时钟频率和电压配置下工作。一般来说，时钟频率越高，电压越高，CPU可以执行的指令就越多。同样，时钟频率和电压越高，消耗的能量越多。因此，在CPU容量和处理器消耗的功率之间有一个权衡。\n使用以下命令检查正在使用的驱动程序和调控器：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 analyzing CPU 0: driver: intel_pstate CPUs which run at the same hardware frequency: 0 CPUs which need to have their frequency coordinated by software: 0 maximum transition latency: Cannot determine or is not supported. hardware limits: 1.20 GHz - 3.20 GHz available cpufreq governors: performance powersave current policy: frequency should be within 1.20 GHz and 3.20 GHz. The governor \u0026#34;powersave\u0026#34; may decide which speed to use within this range. current CPU frequency: 2.40 GHz (asserted by call to hardware) boost state support: Supported: no Active: no 查看CPU加速设置： cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor\n设置CPU加速策略： echo \u0026ldquo;performance\u0026rdquo; | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor\n6. NUMA NUMA表示Non-uniform memory access，带有NUMA功能，SMP系统的处理器访问它自己的本地内存比非本地内存更快。这可能会导致内存换出(swap)，从而对数据库性能产生负面影响。当分配给InnoDB缓冲池的内存大于可用内存的数量，并且选择了默认内存分配策略时，可能会发生内存交换(swap)。启用NUMA的服务器将报告CPU节点之间的不同节点距离(distances)。\n通过如下命令查看： numactl \u0026ndash;hardware\n不管numactl显示的跨节点间的距离是多少，应当开启MySQL参数innodb_numa_interleave，来确保内存可以交错使用。\n","permalink":"https://ktzxy.top/posts/tx6bn28k3p/","summary":"MySQL Linux 内核参数调优","title":"MySQL Linux 内核参数调优"},{"content":"﻿# Day-08-常用类\n什么是包装类: 以前定义变量，经常使用基本数据类型， 对于基本数据类型来说，它就是一个数，加点属性，加点方法，加点构造器,将基本数据类型对应进行了一个封装，产生了一个新的类，\u0026mdash;》包装类。int,byte\u0026hellip;..\u0026mdash;\u0026gt;基本数据类型 包装类\u0026mdash;\u0026gt;引用数据类型\n对应关系\n已经有基本数据类型了，为什么要封装为包装类?\n(1) java语言面向对象的语言，最擅长的操作各种各样的类。\n(2)以前学习装数据的\u0026mdash;》数组，int[] String[] double[]Student[]以后学习的装数据的\u0026ndash;\n-\u0026gt;集合，有一个特点，只能装引用数据类型的数据\n是不是有了包装类以后就不用基本数据类型了?\n不是。\nInteger 【1】直接使用，无需导包\n【2】类的继承关系\n【3】实现接口\n【4】这个类被final修饰，那么这个类不能有子类，不能被继承\n【5】包装类是对基本数据类型的封装：对int类型封装产生了Integer\n【6】属性\n1 2 3 4 5 6 //属性 System.out.println(Integer.MAX_VALUE); System.out.println(Integer.MIN_VALUE); //“物极必反” System.out.println(Integer.MAX_VALUE+1); System.out.println(Integer.MIN_VALUE-1); 【7】构造器（发现没有空构造器）\n（1）int类型作为构造器的参数：\n1 2 Integer i1 = new Integer(10); System.out.println(i1.toString()); //10 （2）String类型作为构造器的参数\n1 2 3 4 Integer i2 = new Integer(\u0026#34;12\u0026#34;); System.out.println(i2); //12 Integer i3 = new Integer(\u0026#34;abc\u0026#34;); System.out.println(i3); //NumberFormatException: For input string: \u0026#34;abc\u0026#34; 【8】包装类特有的机制：自动装箱 自动拆箱\n1 2 3 4 5 6 7 //自动装箱：int---\u0026gt;Integer Integer i = 10; System.out.println(i); //自动拆箱：Integer---\u0026gt;int Integer i1 = new Integer(10); int num = i1; System.out.println(num); 自动装箱 自动拆箱 是从JDK1.5以后新出的特性\n自动装箱 自动拆箱 ：将基本数据类型和包装类进行快速的类型转换。\n【9】常用方法：\nvalueOf方法的底层：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //comparable:只返回三个值：0 -1 1 Integer i1 = new Integer(12); Integer i2 = new Integer(13); System.out.println(i1.compareTo(i2)); //-1 return (x \u0026lt; y) ? -1 : ((x == y) ? 0 : 1) //equals：Integer对Object中equals方法进行了重写，比较的是底层封装的那个value值。 //Integer对象是通过new关键自创建的对象： Integer i3 = new Integer(12); Integer i4 = new Integer(12); System.out.println(i3 == i4); //false 因为==比较的是两个对象的地址 boolean flag = i3.equals(i4); System.out.println(flag); //true //Integer对象通过自动装箱来完成： Integer i5 = 130; Integer i6 = 130; System.out.println(i5.equals(i6)); //true System.out.println(i5 == i6); //flase /* 如果自动装箱值在-128~127之间，那么比较的就是具体的数值 否则，比较的就是对象的地址 */ 1 2 3 4 5 6 7 8 9 10 11 12 //intValue() :作用将Integer----\u0026gt;int Integer i7 = 130; int i = i7.intValue(); System.out.println(i); //130 //parseInt(String s) :String ---\u0026gt;int int i2 = Integer.parseInt(\u0026#34;12\u0026#34;); System.out.println(i2); //12 //toString:Integer ----\u0026gt;String Integer i9 = 130; System.out.println(i9.toString()); //130 日期相关类 java.util.Date 1 2 3 4 5 6 7 8 9 //java.util.Date: Date d = new Date(); System.out.println(d); //Wed Feb 17 17:35:52 CST 2021 System.out.println(d.toString()); //Wed Feb 17 17:35:52 CST 2021 System.out.println(d.toLocaleString()); //方法过时 2021-2-17 17:35:52 System.out.println(d.toGMTString()); //17 Feb 2021 09:35:52 GMT System.out.println(d.getYear()); //121+1900=2021 System.out.println(d.getMonth()); //1 :返回的值在0~11之间，0表示1月 java.sql.Date 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 //java.sql.Date: Date d = new Date(1613555029609L); System.out.println(d); /* (1)java.sql.Date和java.util.Date的区别： java.util.Date：年月日 时分秒 java.sql.Date ：年月日 （2）java.sql.Date和java.util.Date的联系： java.sql.Date(子类) extends java.util.Date(父类) */ //java.sql.Date和java.util.Date相互转换 //util--\u0026gt;sql: java.util.Date date = new java.util.Date(); //创建util.Date对象 //方式一：向下转型 Date date1 = (Date) date; //方式二：利用构造器 Date date2 = new Date(date.getTime()); //sql---\u0026gt;util java.util.Date date3 = d; //String--\u0026gt;sql.Date Date date4 = Date.valueOf(\u0026#34;2021-1-1\u0026#34;); SimpleDateFormat 【1】日期转换：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 //日期转换： //SimpleDateFormat(子类) extends DateFormat(父类) DateFormat df = new SimpleDateFormat(\u0026#34;yyyy-MM-dd HH:mm:ss\u0026#34;); //String---\u0026gt;Date try { Date d = df.parse(\u0026#34;2020-1-1 12:12:24\u0026#34;); System.out.println(d); } catch (ParseException e) { e.printStackTrace(); } //Date ----\u0026gt;String String format = df.format(new Date()); System.out.println(format); 【2】日期格式：\nCalendar 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 //Calendar是一个抽象类，不可以直接创建对象 //GregorianCalendar（子类）extends Calendar(父类)父类是一个抽象类 //创建对象方式一 Calendar cal = new GregorianCalendar(); //创建对象方式二 Calendar cal2 = Calendar.getInstance(); System.out.println(cal); //常用的方法:get方法，传入参数：Calendar中定义的常量 System.out.println(cal.get(Calendar.YEAR)); System.out.println(cal.get(Calendar.MONTH)); System.out.println(cal.get(Calendar.DATE)); System.out.println(cal.get(Calendar.DAY_OF_WEEK)); System.out.println(cal.getActualMaximum(Calendar.DATE)); //当前月份最大值 System.out.println(cal.getActualMinimum(Calendar.DATE)); //当前月份最小值 //set方法：可以改变Calendar中的内容 cal.set(Calendar.YEAR,2000); cal.set(Calendar.MONTH,2); cal.set(Calendar.DATE,15); System.out.println(cal); System.out.println(cal.get(Calendar.YEAR)); //String-----\u0026gt;Calendar: java.sql.Date date = java.sql.Date.valueOf(\u0026#34;2021-1-1\u0026#34;); cal.setTime(date); System.out.println(cal); 练习\n需求：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package com.zy.test1; import java.util.Calendar; import java.util.Scanner; /** * @Auther: 赵羽 * @Description: com.zy.test1 * @version: 1.0 */ public class Demo5 { public static void main(String[] args) { //录入日期的String Scanner sc = new Scanner(System.in); System.out.print(\u0026#34;请输入你想要查看的日期：（提示：请按照例如2019-3-7的格式）\u0026#34;); String strDate = sc.next(); //String ----\u0026gt;Calendar java.sql.Date date = java.sql.Date.valueOf(strDate); Calendar cal = Calendar.getInstance(); cal.setTime(date); //星期提示 System.out.println(\u0026#34;日\\t一\\t二\\t三\\t四\\t五\\t六\\t\u0026#34;); //获取本月的最大天数： int maxDay = cal.getActualMaximum(Calendar.DATE); //获取当前日期中的日 int nowDay = cal.get(Calendar.DATE); //将日期凋萎本月的1号： cal.set(Calendar.DATE,1); //获取这个月一号是本周的第几天： int num= cal.get(Calendar.DAY_OF_WEEK); //前面空出来的天数为： int day = num - 1; //引入一个计时器： int count = 0; //打印日期前的空格 for (int i = 1; i \u0026lt;=day ; i++) { System.out.print(\u0026#34;\\t\u0026#34;); } //空出来的天数也要放入计数器 count = count + day; //遍历：从1号开始到maxDay号 for (int i = 1; i \u0026lt;= maxDay; i++) { if (i==nowDay){ //如果遍历的i和当前日子一样的话，后面加一个* System.out.print(i+\u0026#34;*\u0026#34;+\u0026#34;\\t\u0026#34;); }else { System.out.print(i+\u0026#34;\\t\u0026#34;); } count++; if (count%7==0){ //当计数器的个数是7的倍数的时候，就换行操作 System.out.println(); } } } } JDK1.8新增日期类 LocalDate/LocalTime/LocalDateTime使用\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package com.zy.test1; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; /** * @Auther: 赵羽 * @Description: com.zy.test1 * @version: 1.0 */ public class Demo6 { public static void main(String[] args) { //1.实例化 //方法1：now() -- 获取当前的日期，时间，日期+时间 LocalDate localDate = LocalDate.now(); System.out.println(localDate); LocalTime localTime = LocalTime.now(); System.out.println(localTime); LocalDateTime localDateTime = LocalDateTime.now(); System.out.println(localDateTime); //方法2：of() --设置指定的日期，时间，日期和时间 LocalDate of1 = localDate.of(2020,1,1); System.out.println(of1); LocalTime of2 = localTime.of(10,30,30); System.out.println(of2); LocalDateTime of3 = localDateTime.of(2020,1,1,10,30,30); System.out.println(of3); //get方法 主要用LocalDateTime讲解： System.out.println(localDateTime.getYear()); //2021 System.out.println(localDateTime.getMonth()); //FEBRUARY System.out.println(localDateTime.getMonthValue()); //2 System.out.println(localDateTime.getDayOfWeek()); //THURSDAY System.out.println(localDateTime.getHour()); //10 System.out.println(localDateTime.getMinute()); //54 System.out.println(localDateTime.getSecond()); //48 //with方法 相当于之前的set方法 //不可变性 LocalDateTime localDateTime1 = localDateTime.withMonth(5); System.out.println(localDateTime1); //2021-05-18T10:59:12.298 System.out.println(localDateTime); //2021-02-18T10:59:12.298 //加减操作 //加操作 LocalDateTime localDateTime2 = localDateTime.plusMonths(5); System.out.println(localDateTime); //2021-02-18T11:01:14.230 System.out.println(localDateTime2); //2021-07-18T11:01:14.230 //减操作 LocalDateTime localDateTime3 = localDateTime.minusMonths(6); System.out.println(localDateTime); //2021-02-18T11:03:26.694 System.out.println(localDateTime3); //2020-08-18T11:03:26.694 } } DateTimeFormatter 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 //格式化类：DateTimeFormatter //方式一：预定义的标准格式。如：ISO_LOCAL_DATE_TIME;ISO_LOCAL_DATE;ISO_LOCAL_TIME DateTimeFormatter df1 = DateTimeFormatter.ISO_LOCAL_DATE_TIME; //df1就可以帮我们完成LocalDateTime和String之间的相互转换： //LocalDateTime ----》String LocalDateTime now = LocalDateTime.now(); String str = df1.format(now); System.out.println(str); //2021-02-18T11:22:46.796 //String---\u0026gt;LocalDateTime TemporalAccessor parse = df1.parse(\u0026#34;2021-02-18T11:22:46.796\u0026#34;); System.out.println(parse); //{},ISO resolved to 2021-02-18T11:22:46.796 //方式二：本地化相关的格式。如ofLocalizedDateTime() //参数： // FormatStyle.LONG 2021年2月18日 下午02时48分27秒 // FormatStyle.MEDIUM 2021-2-18 14:49:49 // FormatStyle.SHORT 21-2-18 下午2:50 DateTimeFormatter df2 = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT); //LocalDateTime ----》String LocalDateTime now1 = LocalDateTime.now(); String str2 = df2.format(now1); System.out.println(str2); //2021年2月18日 下午02时48分27秒 //String---\u0026gt;LocalDateTime TemporalAccessor parse1 = df2.parse(\u0026#34;21-2-18 下午2:50\u0026#34;); System.out.println(parse1); //{},ISO resolved to 2021-02-18T14:50 //方式三：自定义的格式，如：ofPattern(\u0026#34;yyyy-MM-dd hh:mm:ss\u0026#34;); DateTimeFormatter df3 = DateTimeFormatter.ofPattern(\u0026#34;yyyy-MM-dd hh:mm:ss\u0026#34;); //LocalDateTime ----》String LocalDateTime now2 = LocalDateTime.now(); String format = df3.format(now2); System.out.println(format); //2021-02-18 02:54:57 //String---\u0026gt;LocalDateTime TemporalAccessor parse2 = df3.parse(\u0026#34;2021-02-18 02:54:57\u0026#34;); System.out.println(parse2); Math类 【1】直接使用，无需导包\n【2】这个类被final修饰，那么这个类不能有子类，不能被继承\n【3】构造器私有化，不能创建Math类的对象：\n【4】Math内部的所有的属性，方法都被static修饰：类名.直接调用，无需创建对象。\n【5】常用方法\n1 2 3 4 5 6 7 8 9 10 //常用属性： System.out.println(Math.PI); //常用方法： System.out.println(\u0026#34;随机数\u0026#34;+Math.random()); System.out.println(\u0026#34;绝对值\u0026#34;+Math.abs(-10)); System.out.println(\u0026#34;向上取值\u0026#34;+Math.ceil(9.4)); System.out.println(\u0026#34;向下取值\u0026#34;+Math.floor(9.4)); System.out.println(\u0026#34;四舍五入\u0026#34;+Math.round(2.6)); System.out.println(\u0026#34;取大的那个值\u0026#34;+Math.max(12, 46)); System.out.println(\u0026#34;取小的那个值\u0026#34;+Math.min(12, 46)); 【6】静态导入\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.zy.test1; import static java.lang.Math.*; /** * @Auther: 赵羽 * @Description: com.zy.test1 * @version: 1.0 */ public class Demo8 { public static void main(String[] args) { //常用属性： System.out.println(PI); //常用方法： System.out.println(\u0026#34;随机数\u0026#34;+random()); System.out.println(\u0026#34;绝对值\u0026#34;+abs(-10)); System.out.println(\u0026#34;向上取值\u0026#34;+ceil(9.4)); System.out.println(\u0026#34;向下取值\u0026#34;+floor(9.4)); System.out.println(\u0026#34;四舍五入\u0026#34;+round(2.6)); System.out.println(\u0026#34;取大的那个值\u0026#34;+max(12, 46)); System.out.println(\u0026#34;取小的那个值\u0026#34;+min(12, 46)); } //如果跟Math中方法重复了，那么会优先走本类中的方法 public static int random(){ return 200; } } Random类 1 2 3 4 5 6 7 8 9 //(1)利用带参数的构造器创建对象： Random r1 = new Random(System.currentTimeMillis()); int i = r1.nextInt(); System.out.println(i); //(2)利用空参构造器创建对象： Random r2 = new Random(); //表面上是在调用无参构造器，实际上底层还是调用了带参构造器 System.out.println(r2.nextInt(10)); //在 0（包括）和指定值（不包括）之间均匀分布的 int 值。 System.out.println(r2.nextDouble()); //在 0.0 和 1.0 之间均匀分布的 double 值。 String类 【1】直接使用，无需导包\n【2】这个类被final修饰，不可以被继承，不能有子类。\n【3】String底层是一个char类型的数组\n【4】构造器：底层就是给对象底层的value数组进行赋值操作\n1 2 3 4 //通过构造器来创建对象： String s1 = new String(); String s2 = new String(\u0026#34;abc\u0026#34;); String s3 = new String(new char[]{\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;}); 【5】常用方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 String s4 = \u0026#34;abc\u0026#34;; System.out.println(\u0026#34;字符串的长度为：\u0026#34;+s4.length()); String s5 = new String(\u0026#34;abc\u0026#34;); System.out.println(\u0026#34;字符串是否为空：\u0026#34;+s5.isEmpty()); System.out.println(\u0026#34;获取字符串的下标对应的字符为：\u0026#34;+s5.charAt(1));String s4 = \u0026#34;abc\u0026#34;; System.out.println(\u0026#34;字符串的长度为：\u0026#34;+s4.length()); String s5 = new String(\u0026#34;abc\u0026#34;); System.out.println(\u0026#34;字符串是否为空：\u0026#34;+s5.isEmpty()); System.out.println(\u0026#34;获取字符串的下标对应的字符为：\u0026#34;+s5.charAt(1)); 【6】equals：\n1 2 3 String s6 = new String(\u0026#34;abc\u0026#34;); String s7 = new String(\u0026#34;abc\u0026#34;); System.out.println(s6.equals(s7)); 【7】String类实现了Comparable，里面有一个抽象方法叫compareTo，所以String中一定要对这个方法重写：\n1 2 3 String s8 = new String(\u0026#34;abc\u0026#34;); String s9 = new String(\u0026#34;abcdef\u0026#34;); System.out.println(s8.compareTo(s9)); 【8】常用方法：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 //字符串的截取 String s10 = new String(\u0026#34;abcdefgki\u0026#34;); System.out.println(s10.substring(4)); System.out.println(s10.substring(4, 6)); //字符串的拼接 System.out.println(s10.concat(\u0026#34;aaaaaa\u0026#34;)); //字符串的替换 String s11 = \u0026#34;abanagavah\u0026#34;; System.out.println(s11.replace(\u0026#39;a\u0026#39;, \u0026#39;w\u0026#39;)); //字符串的切割 String s12 = \u0026#34;a-b-c-d-e-f\u0026#34;; String[] strs = s12.split(\u0026#34;-\u0026#34;); System.out.println(Arrays.toString(strs)); //转换大小写 String s13 = \u0026#34;abc\u0026#34;; System.out.println(s13.toUpperCase()); System.out.println(s13.toUpperCase().toLowerCase()); //去除首尾空格 String s14 = \u0026#34; a b c \u0026#34;; System.out.println(s14.trim()); //toString() String s15 = \u0026#34;abc\u0026#34;; System.out.println(s15.toString()); //转换为String类型： System.out.println(String.valueOf(false)); String内存分析 【1】字符串拼接\n1 2 3 4 5 6 7 8 9 10 public class Demo3 { public static void main(String[] args) { String s1 = \u0026#34;a\u0026#34;+\u0026#34;b\u0026#34;+\u0026#34;c\u0026#34;; String s2 = \u0026#34;ab\u0026#34;+\u0026#34;c\u0026#34;; String s3 = \u0026#34;a\u0026#34;+\u0026#34;bc\u0026#34;; String s4= \u0026#34;abc\u0026#34;; String s5 = \u0026#34;abc\u0026#34;+\u0026#34;\u0026#34;; } } 上面的字符串，会进行编译器优化，直接合并成为完整的字符串，我们可以反编译验证：\n然后在常量池中，常量池的特点是第一次如果没有这个字符串，就放进去，如果有这个字符串，就直接从常量池中取。\n内存分析：\n【2】new关键字创建对象：\n1 String s6 = new String(\u0026#34;abc\u0026#34;); 内存：开辟两个空间（1.字符串常量池中的字符串 2. 堆中的开辟的空间）\n【3】有变量参与的字符串拼接：\n1 2 3 String a = \u0026#34;abc\u0026#34;; String b = a + \u0026#34;def\u0026#34;; System.out.println(b); a变量在编译的时候不知道a是“abc”字符串，所以不会进行编译期优化，不会直接合并为“abcdef\u0026quot; 反汇编过程:为了更好的帮我分析字节码文件是如何进行解析的:\nStringBuilder类 【1】字符串的分类: (1)不可变字符串:String (2)可变字符串: StringBuilder，StringBuffer\n【2】StringBuilder底层：非常重要的两个属性。\n1 2 3 4 5 6 7 //创建StringBuilder的对象： StringBuilder sb3 = new StringBuilder(); //表面上调用StringBuilder的空构造器，实际底层是对value数组进行初始化，长度为16 StringBuilder sb2 = new StringBuilder(3); //表面上调用StringBuilder的有参构造器，传入一个int类型的数，实际底层是对value数组进行初始化，长度为你传入的数字 StringBuilder sb = new StringBuilder(\u0026#34;abc\u0026#34;); sb.append(\u0026#34;def\u0026#34;).append(\u0026#34;aaaa\u0026#34;).append(\u0026#34;bbb\u0026#34;); 【3】StringBuilder和StringBuffer常用方法相似\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 StringBuilder sb = new StringBuilder(\u0026#34;abcdefghijklmno\u0026#34;); //增 sb.append(\u0026#34;@@@\u0026#34;); System.out.println(sb); //删 sb.delete(1,3); //删除位置在[1,3)上的字符 System.out.println(sb); sb.deleteCharAt(8); //删除位置在8上的字符 System.out.println(sb); //改---》插入 StringBuilder sb2 = new StringBuilder(\u0026#34;你好，我喜欢学习\u0026#34;); sb2.insert(3,\u0026#34;,\u0026#34;); //在下标为3的位置上插入, System.out.println(sb2); //改---》替换 sb2.replace(3,5,\u0026#34;唉唉\u0026#34;); System.out.println(sb2); sb2.setCharAt(4,\u0026#39;!\u0026#39;); System.out.println(sb2); //查 for (int i = 0; i \u0026lt; sb2.length(); i++) { System.out.println(sb2.charAt(i) + \u0026#34;\\t\u0026#34;); } System.out.println(sb2); //截取 String str = sb2.substring(2, 4); System.out.println(sb2); String、StringBuffer、StringBuilder区别于联系\n1.String类是不可变类，即一旦一个String对象被创建后，包含在这个对象中的字符序列是不可改变的，直至这个对象销毁。\n2.StringBuffer类则代表一个字符序列可变的字符串，可以通过append、insert、reverse、setChartAt、setLength等方法改变其内容。一旦生成了最终的字符串，调用toString方法将其转变为String\n3.JDK1.5新增了一个StringBuilder类，与StringBuffer相似，构造方法和方法基本相同。不同是StringBuffer是线程安全的，而StringBuilder是线程不安全的，所以性能略高。通常情况下，创建一个内容可变的字符串，应该优先考虑使用StringBuilder\nStringBuilder:JDK1.5开始效率高线程不安全 StringBuffer:JDK1.0开始效率低线程安全\n","permalink":"https://ktzxy.top/posts/7mrznf9d6j/","summary":"Day 08 常用类","title":"Day 08 常用类"},{"content":"Jenkins [TOC]\n安装 rpm安装 下载\n下载地址：https://mirrors.tuna.tsinghua.edu.cn/jenkins/redhat/\n下载版本：jenkins-2.356-1.1.noarch.rpm\n高于这个版本的Jenkins不支持JDK8\n安装\n1 rpm -ivh jenkins-2.356-1.1.noarch.rpm 卸载\n1 2 3 4 5 6 # rpm卸载 rpm -e jenkins # rpm检查是否卸载成功 rpm -ql jenkins # 删除残留文件 find / -iname jenkins | xargs -n 1000 rm -rf #Environment=\u0026ldquo;JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64\u0026rdquo; #Environment=\u0026quot;/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.171-8.b10.el7_5.x86_64\u0026quot;\n修改配置\n新版本配置文件 /usr/lib/systemd/system/jenkins.service，旧版本配置文件 /etc/sysconfig/jenkins\n1 2 3 4 5 6 7 8 9 10 11 # 添加JDK路径 vim /usr/lib/systemd/system/jenkins.service # 可以搜索JAVA_HOME查找, 这个配置被注释了 Environment=\u0026#34;JAVA_HOME=JDK路径\u0026#34; # 如果不知自己的jdk路径可以使用如下命令 which java # 修改Jenkins后台访问前缀, 搜索JENKINS_PREFIX查找, 自行添加需要的访问前缀, 如果需要配置nginx必须要配置http路径 # 换源; 将里面的url标签内容替换为 https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json # 换源下载插件速度会快一些 vim /var/lib/jenkins/hudson.model.UpdateCenter.xml 启动\n1 2 3 # 如果启动报错可以参考：https://www.cnblogs.com/aisonsun/p/16576587.html systemctl daemon-reload systemctl start jenkins.service 常用命令\n1 2 3 4 5 6 7 8 9 10 # 重新加载配置 systemctl daemon-reload # 启动Jenkins systemctl start jenkins.service # 重启Jenkins systemctl restart jenkins.service # 停止Jenkins systemctl stop jenkins.service # 查看Jenkins当前状态, 是否可以启动 systemctl status jenkins.service 新手安装\n输入访问地址 http://ip:port/ 访问管理页面，如果配置了访问前缀记得加上，使用新手插件安装，自行设置管理员账号密码\n解决Jenkins\n配置Nginx反向代理，并在 location 块中添加 proxy_set_header X-Forwarded-Proto $scheme;\n自动化部署 Git提交代码后自动部署对应项目\n下载插件 搜索：Generic Webhook Trigger Plugin 进行安装\n创建任务 选择项目\n配置Git 在Jenkins工作空间下自动拉取代码，如果你只是想要Jenkins去指定目录执行命令来部署项目，可以不用配置这个\nRepository URL：Git地址，推荐使用https地址 Credentials：可以pull项目的Git账号 Branches to build：拉取到工作空间的分支名称 配置Webhook 配置Git钩子，Jenkins收到钩子通知后会自动进行构建\n参考：https://help.aliyun.com/document_detail/306411.html#sectiondiv-eyf-d39-ozl\n选择 Generic Webhook Trigger，新增一个 Post content parameters\n配置 Post content parameters 配置 Token\nToken可以随便填，但是不要重复，最好使用任务名称，保证不会重复\n过滤内容\n只处理指定分支的推送事件\n配置构建步骤 也可以选择在构建后操作中进行配置 如果是在当前服务器进行构建，选择 执行 shell 选项\n配置 shell 脚本 一下命令也可以写成 sh 文件随 git 一起提交，配置 shell 命令时执行 sh 文件就行\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/bin/bash # 重新加载环境变量, Jenkins可能无法运行java、mvn等命令, 加这个命令可以解决; 或者使用全路径运行命令, 比如: /usr/bin/java source /etc/profile # 进入项目目录 cd /datanew/code/dev/xxx # 拉取指定分支 git pull origin master # maven打包 mvn clean package # 终止进程 kill $(lsof -i:8196|awk \u0026#39;{print $2}\u0026#39; |sort|uniq|awk \u0026#39;{if($0!=\u0026#34;PID\u0026#34;) print \u0026#34;\u0026#34;$0\u0026#34; \u0026#34; }\u0026#39;) # 启动项目 nohup java -jar target/jarName.jar \u0026amp; # 没有这个命令, nohup java -jar命令启动的进程将会被Jenkins杀死 sleep 1s SpringBoot自动部署Shell\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 #!/bin/bash #------------------------------------------------------------------------------------------------ #程序部署目录, pom.xml所在目录, logs文件夹和脚本日志会生成在这个目录下 TODO 修改部署目录 _workDir=/datanew/code/dev/Application #部署程序jar名称 TODO 修改部署jar包名称 _program=$_workDir/target/Application.jar #部署程序jar springboot启动参数如：--server.port=9000 --spring.profiles.active=dev #不指定参数时，可以为空 _program_param=\u0026#39;\u0026#39; #启动时指定日志输出文件 _default_log_file=$_workDir/nohup.out # TODO 正式环境如果服务器是 8G 内存，需要改成 Xmx 4096M Xms 4096M -XX:MetaspaceSize=256m #最大堆内存 _xmx=512m #初始堆内存 _xms=512m #最大元空间大小 _xxms=102m #远程debug调试端口号 _debugPort=5008 #脚本日志输出位置 _log=load.log #------------------------------------------------------------------------------------------------ _programId=0 # which查找可执行命令路径, Jenkins会在/usr/bin目录下找, 如果你的命令不在这个目录下, 可以创建一个软连接解决 cmd_java=$(which java) cmd_mvn=$(which mvn) cd $_workDir || exist #启动项目 function start() { prnt \u0026#34;starting $_program\u0026#34; #判断jar是否存在 if [ ! -f $_program ]; then prnt \u0026#34;文件不存在: $_program\u0026#34; exit 1 fi setProgramId if [ -z $_programId ]; then _debugParam=\u0026#34;\u0026#34; if [ \u0026#34;$1\u0026#34; = \u0026#34;debug\u0026#34; -o \u0026#34;$2\u0026#34; = \u0026#34;debug\u0026#34; ]; then _debugParam=\u0026#34;-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$_debugPort -agentpath:/usr/local/java/jrebel/lib/libjrebel64.so -Drebel.remoting_plugin=true -Xdebug\u0026#34; prnt \u0026#34;debug模式启动\u0026#34; fi echo \u0026#34;$1\u0026#34;\u0026#34; \u0026#34;\u0026#34;$2\u0026#34; nohup \u0026#34;$cmd_java\u0026#34; -Xmx$_xmx -Xms$_xms -XX:MetaspaceSize=$_xxms $_debugParam -verbose:gc -Xloggc:./logs/gc.log -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=8 -XX:+UseCMSCompactAtFullCollection -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./logs/heapDump.hprof -XX:+CMSParallelRemarkEnabled -XX:+CMSClassUnloadingEnabled -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=80 -XX:+DisableExplicitGC -XX:+PrintGCTimeStamps -XX:+UseCompressedOops -XX:+DoEscapeAnalysis -XX:MaxTenuringThreshold=10 -Dfile.encoding=UTF-8 -jar $_program $_program_param \u0026gt;\u0026gt;$_default_log_file 2\u0026gt;\u0026amp;1 \u0026amp; setProgramId if [ -z $_programId ]; then prnt \u0026#34;程序启动失败, $_default_log_file 查看原因\u0026#34; else prnt \u0026#34;程序启动成功, PID:$_programId\u0026#34; sleep 2s if [ \u0026#34;$1\u0026#34; != \u0026#34;nlog\u0026#34; -a \u0026#34;$2\u0026#34; != \u0026#34;nlog\u0026#34; ]; then log fi fi else prnt \u0026#34;程序正在运行, PID:$_programId\u0026#34; fi } # stop project function stop() { setProgramId if [ -z $_programId ]; then prnt \u0026#34;not running\u0026#34; else prnt \u0026#34;程序PID: $_programId\u0026#34; prnt \u0026#34;Stopping...\u0026#34; kill $_programId sleep 1 prnt \u0026#34;程序已停止\u0026#34; fi } # 查看程序当前运行状态 function status() { setProgramId if [ -z $_programId ]; then prnt \u0026#34;程序已停止\u0026#34; else prnt \u0026#34;程序正在运行, PID:$_programId\u0026#34; fi } # 部署 function update() { prnt \u0026#34;git pull origin master\u0026#34; git pull origin master prnt \u0026#34;mvn clean package\u0026#34; $cmd_mvn clean package restart \u0026#34;$1\u0026#34; \u0026#34;$2\u0026#34; } #重启 function restart() { stop start \u0026#34;$1\u0026#34; \u0026#34;$2\u0026#34; } #设置pid到_programId function setProgramId() { _programId=$(ps aux | grep $_program | grep -v grep | awk \u0026#39;END{print $2}\u0026#39;) } # 展示日志 function log(){ tail -n 500 -f $_default_log_file } # 展示使用方式 function usage() { echo \u0026#34;使用: $0 [start|stop|status|restart|update|log] \u0026#34; echo \u0026#34; \u0026lt;1\u0026gt; sh bin/load.sh start , 在后台启动程序。\u0026#34; echo \u0026#34; \u0026lt;2\u0026gt; sh bin/load.sh stop , 停止程序\u0026#34; echo \u0026#34; \u0026lt;3\u0026gt; sh bin/load.sh status , 显示程序运行状态。\u0026#34; echo \u0026#34; \u0026lt;4\u0026gt; sh bin/load.sh restart , 重启程序\u0026#34; echo \u0026#34; \u0026lt;5\u0026gt; sh bin/load.sh update , 部署项目\u0026#34; echo \u0026#34; \u0026lt;6\u0026gt; sh bin/load.sh log , 查看日志\u0026#34; echo \u0026#34; \u0026lt;7\u0026gt; 其他shell命令暂未支持\u0026#34; echo \u0026#34;[start|restart|update] [debug|nlog]\u0026#34; echo \u0026#34; 增加debug参数可以开启远程debug调试模式\u0026#34; echo \u0026#34; 增加nlog参数启动后不打开日志\u0026#34; } # 打印日志并输出到文件中 function prnt(){ echo -e $(date +%Y-%m-%d\u0026#34; \u0026#34;%T)\u0026#34; $1\u0026#34; | tee -a $_log } # use tips if [ $# -ge 1 ]; then # 打印一个换行, 与上一次执行分开 echo -e \u0026#34;\\r\u0026#34; | tee -a $_log # 打印java、maven使用的可执行命令全路径 prnt \u0026#34;java命令路径: $cmd_java\u0026#34; prnt \u0026#34;mvn命令路径: $cmd_mvn\u0026#34; echo $# case $1 in start) start \u0026#34;$2\u0026#34; \u0026#34;$3\u0026#34; ;; stop) stop ;; status) status ;; restart) restart \u0026#34;$2\u0026#34; \u0026#34;$3\u0026#34; ;; update) update \u0026#34;$2\u0026#34; \u0026#34;$3\u0026#34; ;; log) log ;; *) usage ;; esac else usage fi Git代码库配置 Webhooks 云效为例\nhttp://{你的Jenkins地址}/generic-webhook-trigger/invoke?token={Generic Webhook Trigger中配置的Token}\nJenkins 安装服务器与项目实际运行不是一台服务器的示例 下载插件：SSH Slaves plugin 允许使用SSH协议的Java实现通过SSH启动代理。\n添加 SSH 配置\n找到 Publish over SSH 进行配置\n填写相关信息\nName：SSH 配置的名称，可以随便填 Hostname：服务器 IP Username：登录用户名 Remote Directory：这个最好填 / 点击 高级 按钮，按下方图片示例填写密码，最后使用 Test Configuration 按钮进行测试\n配置构建步骤\n选择 Send files or execute commands over SSH\n填写相关信息\nSSH Server：选择上面配置的 SSH Source files：位置基于Jenkins的工作空间 /{JENKINS_HOME}/workspace/{任务名称}，传输到远程的文件，填写文件路径，使用 , 分割 Remove prefix：移除的文件路径的前缀，可选，如果填写，Source files 中填写的所有路径都必须带有这个前缀 Remote directory：将文件移动到远程服务器上的哪个目录下，这个配置会使用系统配置中 Publish over SSH 配置的 Remote directory 作为前缀 Exec command：远程服务器上执行的命令 注意：Source files 和 Exec command 必须填写一项 参考：方式2没试过\n如果想让源代码在远程服务器中，则只配置 Exec command 进行拉取代码、打包、运行就行 如果只想让远程服务器保留打包后的文件，先配置Git，再在 构建设置 选择进行打包，然后在 构建后操作 选项将打包后的文件传输过去执行启动命令； 使用手册 前言\n控制台访问地址：https://api.qianhai12315.com/jenkins\nJenkins 在 zsb上使用的用户加入了 www 分组，并将 Jenkins 运行所需的文件夹拥有者修改为了这个用户\n前端使用手册 任务对应源代码所在位置 /datanew/program/jenkins/workspace/任务名，可以在这里进行 Git 等操作\n新建任务 选择 font 视图，前端任务都在这里进行创建，example 为前端示例项目\n填写任务名称，填写示例项目名称创建新任务\n修改Git 修改 Repository URL，选择 Git 用户\n修改 Shell 修改 dir 变量，值为 zsb 上的部署目录，基于 /datanew/www/dev 路径，如果需要基于别的路径需要修改 basedir 变量\nbasedir + dir 目录下的文件都会被删除，请谨慎修改 basedir\n构建项目 点击任务进入详情页，立即构建按钮点击后进行构建，下方是历史构建日志\n后端使用手册 创建项目 选择 back 视图，后端任务都在这里进行创建，run-sh 为后端示例项目\n填写任务名称，填写示例项目名称创建新任务\n修改 Token 修改 Token，使用任务名称作为 Token，保证唯一性\n钩子分支调整 如果你只想当 master 分支有新提交的时候才自动构建，那么你下图位置的内容修改为 master，这里默认就是 master\n修改 Shell 自行调整 Shell\n云效配置钩子 详情请见 自动化部署-Git代码库配置 Webhooks\n","permalink":"https://ktzxy.top/posts/7oz04i87az/","summary":"Jenkins","title":"Jenkins"},{"content":"初识Shell 一、程序 1、什么是程序 程序是为实现特定目标或解决特定问题而用计算机语言编写的命令序列的集合。\n二、语言 有哪些编程语言？ 编程语言总体分以为机器语言、汇编语言、高级语言\n1、机器语言 由于计算机内部只能接受二进制代码，因此，用二进制代码0和1描述的指令称为机器指令，全部机器指令的集合构成计算机的机器语言，用机器语言编程的程序称为目标程序。只有目标程序才能被计算机直接识别和执行。但是机器语言编写的程序无明显特征，难以记忆，不便阅读和书写，且依赖于具体机种，局限性很 大，机器语言属于低级语言。 用机器语言编写程序，编程人员要首先熟记所用计算机的全部指令代码和代码的涵义。手编程序时，程序员得自己处理每条指令和每一数据的存储分配和输入输出，还得记住编程过程中每步所使用的工作单元处在何种状态。这是一件十分繁琐的工作。编写程序花费的时间往往是实际运行时间的几十倍或几百倍。而且，编出的程序全是些0和1的指令代码，直观性差，还容易出错。除了计算机生产厂家的专业人员外，绝大多数的程序员已经不再去学习机器语言了。机器语言是微处理器理解和使用的，用于控制它的操作二进制代码。 2、汇编语言 汇编语言的实质和机器语言是相同的，都是直接对硬件操作，只不过指令采用了英文缩写的标识符，更容易识别和记忆。它同样需要编程者将每一步具体的操作用命令的形式写出来。 汇编程序的每一句指令只能对应实际操作过程中的一个很细微的动作。例如移动、自增，因此汇编源程序一般比较冗长、复杂、容易出错，而且使用汇编语言编程需要有更多的计算机专业知识，但汇编语言的优点也是显而易见的，用汇编语言所能完成的操作不是一般高级语言所能够实现的，而且源程序经汇编生成的可执行文件不仅比较小，而且执行速度很快。 3、高级语言 高级语言是大多数编程者的选择。和汇编语言相比，它不但将许多相关的机器指令合成为单条指令，并且去掉了与具体操作有关但与完成工作无关的细节，例如使用堆栈、寄存器等，这样就大大简化了程序中的指令。同时，由于省略了很多细节，编程者也就不需要有太多的专业知识。 高级语言主要是相对于汇编语言而言，它并不是特指某一种具体的语言，而是包括了很多编程语言，像最简单的编程语言PASCAL语言也属于高级语言。 高级语言所编制的程序不能直接被计算机识别，必须经过转换才能被执行，按转换方式可将它们分为两类： 编译类 编译是指在应用源程序执行之前，就将程序源代码“翻译”成目标代码（机器语言），因此其目标程序可以脱离其语言环境独立执行(编译后生成的可执行文件，是cpu可以理解的2进制的机器码组成的)，使用比较方便、效率较高。但应用程序一旦需要修改，必须先修改源代码，再重新编译生成新的目标文件（*.obj，也就是OBJ文件）才能执行，只有目标文件而没有源代码，修改很不方便。编译后程序运行时不需要重新翻译，直接使用编译的结果就行了。程序执行效率高，依赖编译器，跨平台性差些。如C、C++、Delphi等 解释类 执行方式类似于我们日常生活中的“同声翻译”，应用程序源代码一边由相应语言的解释器“翻译”成目标代码（机器语言），一边执行，因此效率比较低，而且不能生成可独立执行的可执行文件，应用程序不能脱离其解释器(想运行，必须先装上解释器，就像跟老外说话，必须有翻译在场)，但这种方式比较灵活，可以动态地调整、修改应用程序。如Shell，Python、Java、PHP、Ruby等语言。 4、总结 1、机器语言\n优点是最底层，速度最快，缺点是最复杂，开发效率最低 2、汇编语言\n优点是比较底层，速度最快，缺点是复杂，开发效率最低 3、高级语言\n编译型语言执行速度快，不依赖语言环境运行，跨平台差 解释型跨平台好，一份代码，到处使用，缺点是执行速度慢，依赖解释器运行 三、Shell 的定义 1、Shell 的含义 首先Shell的英文含义是“壳”； 它是相对于内核来说的，因为它是建立在内核的基础上，面向于用户的一种表现形式，比如我们看到一个球，见到的是它的壳，而非核。 Linux中的Shell，是指一个面向用户的命令接口，表现形式就是一个可以由用户录入的界面，这个界面也可以反馈运行信息；\n2、Shell 在Linux中的存在形式 由于Linux不同于Windows，Linux是内核与界面分离的，它可以脱离图形界面而单独运行，同样也可以在内核的基础上运行图形化的桌面。 这样，在Linux系统中，就出现了两种Shell表现形式，一种是在无图形界面下的终端运行环境下的Shell，另一种是桌面上运行的类似Windows 的MS-DOS运行窗口，前者我们一般习惯性地简称为终端，后者一般直接称为Shell\n3、Shell 如何执行用户的指令 1、Shell有两种执行指令的方式，\n第一种方法是用户事先编写一个sh脚本文件，内含Shell脚本，而后使用Shell程序执行该脚本，这种方式，我们习惯称为Shell编程。 第二种形式，则是用户直接在Shell界面上执行Shell命令，由于Shell界面的关系，大家都习惯一行行的书写，很少写出成套的程序来一起执行，所以也称命令行。 总结\nShell 只是为用户与机器之间搭建成的一个桥梁，让我们能够通过Shell来对计算机进行操作和交互，从而达到让计算机为我们服务的目的。 四、Shell 的分类 Linux中默认的Shell是/bin/bash，流行的Shell有ash、bash、ksh、csh、zsh等，不同的Shell都有自己的特点以及用途。 1、bash\n大多数Linux系统默认使用的Shell，bash Shell是Bourne Shell 的一个免费版本，它是最早的Unix Shell,bash还有一个特点，可以通过help命令来查看帮助。包含的功能几乎可以涵盖Shell所具有的功能，所以一般的Shell脚本都会指定它为执行路径。 2、csh\nC Shell 使用的是“类C”语法，csh是具有C语言风格的一种Shell，其内部命令有52个，较为庞大。目前使用的并不多，已经被/bin/tcsh所取代。 3、ksh\nKorn Shell 的语法与Bourne Shell相同，同时具备了C Shell的易用特点。许多安装脚本都使用ksh,ksh 有42条内部命令，与bash相比有一定的限制性。 4、tcsh\ntcsh是csh的增强版，与C Shell完全兼容。 5、sh\n是一个快捷方式，已经被/bin/bash所取代。 6、nologin\n指用户不能登录 7、zsh\n目前Linux里最庞大的一种 zsh。它有84个内部命令，使用起来也比较复杂。一般情况下，不会使用该Shell。 五、Shell 能做什么 自动化批量系统初始化程序 （update，软件安装，时区设置，安全策略\u0026hellip;）\n自动化批量软件部署程序 （LAMP，LNMP，Tomcat，LVS，Nginx）\n应用管理程序 (KVM，集群管理扩容，MySQL，DELLR720批量RAID）\n日志分析处理程序（PV, UV, 200, !200, top 100, grep/awk）\n自动化备份恢复程序（MySQL完全备份/增量 + Crond）\n自动化管理程序（批量远程修改密码，软件升级，配置更新）\n自动化信息采集及监控程序（收集系统/应用状态信息，CPU,Mem,Disk,Net,TCP Status,Apache,MySQL）\n配合Zabbix信息采集（收集系统/应用状态信息，CPU,Mem,Disk,Net,TCP Status,Apache,MySQL）\n自动化扩容（增加云主机——\u0026gt;业务上线）\nzabbix监控CPU 80%+|-50% Python API AWS/EC2（增加/删除云主机） + Shell Script（业务上线）\n俄罗斯方块，打印三角形，打印圣诞树，打印五角星，运行小火车，坦克大战，排序算法实现\nShell可以做任何事（一切取决于业务需求）\n六、bash 的初始化 1、bash 环境变量文件的加载 1、/etc/profile 全局（公有）配置，不管是哪个用户，登录时都会读取该文件。 2、/ect/bashrc Ubuntu 没有此文件，与之对应的是 /ect/bash.bashrc 它也是全局（公有）的 bash 执行时，不管是何种方式，都会读取此文件。 3、~/.profile 若 bash 是以 login 方式执行时，读取 ~/.bash_profile，若它不存在，则读取 /.bash_login，若前两者不存在，读取/.profile。 图形模式登录时，此文件将被读取，即使存在 ~/.bash_profile 和 ~/.bash_login。 4、~/.bash_login 若 bash 是以 login 方式执行时，读取 ~/.bash_profile，若它不存在，则读取 ~/.bash_login，若前两者不存在，读取 ~/.profile。 5、~/.bash_profile Unbutu 默认没有此文件，可新建。 只有 bash 是以 login 形式执行时，才会读取此文件。通常该配置文件还会配置成去读取 ~/.bashrc。 6、~/.bashrc 当 bash 是以 non-login 形式执行时，读取此文件。若是以 login 形式执行，则不会读取此文件。 7、~/.bash_logout 注销时，且是 longin 形式，此文件才会读取。在文本模式注销时，此文件会被读取，图形模式注销时，此文件不会被读取。 2、bash 环境变量加载 图形模式登录时，顺序读取：/etc/profile 和 ~/.profile\n图形模式登录后，打开终端时，顺序读取：/etc/bash.bashrc 和 ~/.bashrc\n文本模式登录时，顺序读取：/etc/bash.bashrc，/etc/profile 和 ~/.bash_profile\n从其它用户 su 到该用户，则分两种情况：\n如果带 -l 参数（或-参数，\u0026ndash;login 参数），如：su -l username，则 bash 是 login 的，它将顺序读取以下配置文件：/etc/bash.bashrc，/etc/profile 和~/.bash_profile。\n如果没有带 -l 参数，则 bash 是 non-login 的，它将顺序读取：/etc/bash.bashrc 和 ~/.bashrc\n注销时，或退出 su 登录的用户，如果是 longin 方式，那么 bash 会读取：~/.bash_logout\n执行自定义的 Shell 文件时，若使用 bash -l a.sh 的方式，则 bash 会读取行：/etc/profile 和~/.bash_profile，若使用其它方式，如：bash a.sh，./a.sh，sh a.sh（这个不属于bash Shell），则不会读取上面的任何文件。\n上面的例子凡是读取到 ~/.bash_profile 的，若该文件不存在，则读取 ~/.bash_login，若前两者不存在，读取 ~/.profile。\n七、bash 特性 1、命令和文件自动补齐 很多命令都会提供一个 bash-complete 的脚本，在执行该命令时，敲 tab 可以自动补全参数，会极大提高生产效率。 linux命令自动补全需要安装 bash-completion 1 [root@localhost ~]# yum -y install bash-complete 注意： 重启系统后可正常 tab 补齐\n默认情况下，Bash 为 Linux 用户提供了下列标准补全功能。\n变量补全\n用户名补全\n主机名补全\n路径补全\n文件名补全\n2、命令历史记忆功能 Bash 有自动记录命令的功能，自动记录到.bash_history隐藏文件中。还可以在下次需要是直接调用历史记录中的命令 centos 可以通过/etc/profile中的文件来定义一些参数、 在bash中,使用history 命令来查看和操作之前的命令,以此来提高工作效率。 history是bash的内部命令,所以可以使用 help history命令调出history命令的帮助文档。 调用命令的方法： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 查看之前使用的所有命令 [root@localhost.com ~]# history # 显示最近的n个命令 [root@localhost.com ~]# history n # 删除相应的第n个命令 [root@localhost.com ~]# history ‐d n # 指定执行命令历史中的第n条语句 [root@localhost.com ~]# !n # 指定执行命令历史中倒数第n条语句 [root@localhost.com ~]# !‐n # 执行命令历史中最后一条语句 [root@localhost.com ~]# !! # 执行命令历史中最近一条以[String]开头的语句 [root@localhost.com ~]# ![String] # 引用上一个命令中的最后一个参数 [root@localhost.com ~]# !$ # COMMAND + Esc键 + . 输入COMMAND之后,按下Esc键,松开后再按 . 则可以自动输入最近一条语句使用的参数 # COMMAND + Alt + . 输入COMMAND之后,同时按下Alt和. 键,也可以自动输入最近一条语句使用的参数 # 将命令历史写入命令历史的文件中 [root@localhost.com ~]# history ‐w # 回显 echo 之后的语句,而使用 echo $FILENAME 命令可以查看该 file 所在的路径 [root@localhost.com ~]# echo $HISTFILE # 查看命令历史的内容 [root@localhost.com ~]# cat .bash_history # 删除所有的命令历史记录 [root@localhost.com ~]# history ‐c 3、 别名功能 alias命令，别名的好处是可以把本来很长的指令简化缩写，来提高工作效率。\n1 2 3 [root@localhost.com ~]# alias #查看系统当前所有的别名 [root@localhost.com ~]# alias h5=‘head ‐5’ #定义新的别名。这时候输入h5就等于输入’head‐5‘ [root@localhost.com ~]# unalias h5 #取消别名定义 如果想要文件永久生效，只需将上述别名命令写到对应用户或者系统 bashrc 文件中 如果想用真实命令可以在命令前面添加反斜杠 ，使别名失效\n1 [root@localhost.com ~]# \\cp ‐rf /etc/hosts 4、快捷键 快捷 作用键 ctrl+A 把光标移动到命令行开头。如果我们输入的命令过长，想要把光标移动到命令行开头时使用。 ctrl+E 把光标移动到命令行结尾。 ctrl+C 强制终止当前的命令。 ctrl+L 清屏，相当于clear命令。 ctrl+U 删除或剪切光标之前的命令。我输入了一行很长的命令，不用使用退格键一个一个字符的删除，使用这个快捷键会更加方便 ctrl+K 删除或剪切光标之后的内容。 ctrl+Y 粘贴ctrl+U或ctul+K剪切的内容。 ctrl+R 在历史命令中搜索，按下ctrl+R之后，就会出现搜索界面，只要输入搜索内容，就会从历史命令中搜索。 ctrl+D 退出当前终端。 ctrl+Z 暂停，并放入后台。这个快捷键牵扯工作管理的内容，我们在系统管理章节详细介绍。 ctrl+S 暂停屏幕输出。 ctrl+Q 恢复屏幕输出。 5、前后台作业控制 Linux bash Shell单一终端界面下，经常需要管理或同时完成多个作业，如一边执行编译，一边实现数据备份，以及执行SQL查询等其他的任务。所有的上述的这些工作可以在一个 bash 内实现，在同一个终端窗口完成。\n1、前后台作业的定义 前后台作业实际上对应的也就是前后台进程，因此也就有对应的 pid。在这里统称为作业。 无论是前台作业还是后台作业，两者都来自当前的Shell，是当前Shell的子程序。 前台作业：可以由用户参与交互及控制的作业我们称之为前台作业。 后台作业：在内存可以自运行的作业，用户无法参与交互以及使用[ctrl]+c来终止，只能通过bg或fg来调用该作业。 2、几个常用的作业命令 command \u0026amp; 直接让作业进入后台运行\n[ctrl]+z 将当前作业切换到后台\njobs 查看后台作业状态\nfg %n 让后台运行的作业n切换到前台来\nbg %n 让指定的作业n在后台运行\nkill %n 移除指定的作业n\n\u0026ldquo;n\u0026rdquo; 为jobs命令查看到的job编号，不是进程id。\n每一个job会有一个对应的job编号，编号在当前的终端从1开始分配。\njob 编号的使用样式为[n]，后面可能会跟有 \u0026ldquo;+\u0026rdquo; 号或者 \u0026ldquo;-\u0026rdquo; 号，或者什么也不跟。\n\u0026ldquo;+\u0026rdquo; 号表示最近的一个job，\n\u0026ldquo;-\u0026rdquo; 号表示倒数第二个被执行的Job。\n注，\u0026quot;+\u0026quot; 号与 \u0026ldquo;-\u0026rdquo; 号会随着作业的完成或添加而动态发生变化。\n通过jobs方式来管理作业，当前终端的作业在其他终端不可见。\n3、演示后台作业命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 # 直接将作业放入到后台(附加 \u0026amp; 符号) [root@localhost.com ~]# tar ‐czvf temp.tar.gz abc.tar.gz \u0026amp; [1] 12500 [root@localhost.com ~]# abc.tar.gz [root@localhost.com ~]# # 此时可进行其它操作，作业一旦完成，会弹出如下的提示 [1]+ Done tar ‐czvf temp.tar.gz abc.tar.gz [root@localhost.com ~]# ls ‐hltr temp* ‐rwxr‐xr‐x 1 robin oinstall 490M 2013‐05‐02 17:48 abc.tar.gz ‐rw‐r‐‐r‐‐ 1 robin oinstall 174M 2013‐05‐02 17:50 temp.tar.gz # 已经开始执行，但需要放入后台(使用[ctrl]+z) [root@localhost.com ~]# tar ‐czvf temp2.tar.gz abc.tar.gz localhost.tar.gz [1]+ Stopped tar ‐czvf temp2.tar.gz abc.tar.gz [root@localhost.com ~]# jobs [1]+ Stopped tar ‐czvf temp2.tar.gz abc.tar.gz # 下面同时发布两个作业，并且在中途按下[ctrl]+z以便将当前作业提交到后台 [root@localhost.com ~]# find /u02 ‐type f ‐size +100000k [root@localhost.com ~]# find / ‐type f ‐size +100000k # 再次查看当前的jobs时，jobs管理器里出现了3个处于stopp状态的job [root@localhost.com ~]# jobs [1] Stopped tar ‐czvf temp2.tar.gz abc.tar.gz [2]‐ Stopped find / ‐type f ‐size +100000k [3]+ Stopped find /u02 ‐type f ‐size +100000k [root@localhost.com ~]# jobs ‐l # 使用‐l参数查看当前Shell下所有的作业以及对应的job number，进程pid [1] 32682 Stopped tar ‐czvf temp2.tar.gz abc.tar.gz [2]‐ 32687 Stopped find /u02 ‐type f ‐size +100000k [3]+ 32707 Stopped find / ‐type f ‐size +100000k # 下面通过pid可以查看到对应的进程信息 [root@localhost.com ~]# ps ‐ef | grep 32707 | grep ‐v grep robin 32707 32095 0 09:48 pts/1 00:00:00 find / ‐type f ‐size +100000 [root@localhost.com ~]# tty # 当前终端的信息为pts/1 /dev/pts/1 # 打开另外一个终端 [root@localhost.com ~]# tty /dev/pts/3 [root@localhost.com ~]# jobs # 此时可以看到jobs命令无任何返回 [root@localhost.com ~]# ps ‐ef | grep 32707 | grep ‐v grep # 仅仅根据进程id可以找到对应的作业 robin 32707 32095 0 09:48 pts/1 00:00:00 find / ‐type f ‐size +100000 # 由上可知，对于当前 Shell 下的 jobs，仅当前 Shell (终端)可见 # 将后台作业切换到前台(fg命令) [root@localhost.com ~]# fg # 省略 Job number 的情形，则将缺省的 job 切换到前台 find / ‐type f ‐size +100000k /u02/database/old/BK/undo/undotbsBK.dbf ...... [ctrl]+z [root@localhost.com ~]# fg %1 tar ‐czvf temp2.tar.gz localhost.tar.gz [root@localhost.com ~]# jobs [2]‐ Stopped find /u02 ‐type f ‐size +100000k [3]+ Stopped find / ‐type f ‐size +100000k # 运行后台中暂停的作业(bg命令) # 前面有2个job处于stopped状态，现在我们让其在后台运行,直接输入bg命令则缺省的job继续运行，否则输入job编 号，运行指定的job [root@localhost.com ~]# bg 2 # 输入bg 2之后，可以看到原来的命令后被追加了\u0026amp; [2]‐ find /u02 ‐type f ‐size +100000k \u0026amp; [root@localhost.com ~]# jobs [2]‐ Running find /u02 ‐type f ‐size +100000k \u0026amp; [3]+ Stopped find / ‐type f ‐size +100000k # 移除指定的作业n(kill) [root@localhost.com ~]# jobs [3]+ Stopped find / ‐type f ‐size +100000k [root@localhost.com ~]# kill ‐9 %3 # 强制终止job 3，注意，此处的%不可省略 [root@localhost.com ~]# jobs [3]+ Killed find / ‐type f ‐size +100000k [root@localhost.com ~]# jobs # kill ‐9 表明强制终止指定的Job，‐15则表明是正常终止指定的job。 kill ‐l 则列出kill能够使用的所有信号 # 对于上述命令的详细帮助,使用 man command来获取帮助信息 # 带参Shell脚本的后台处理 # 下面是一个测试用的Shell脚本 [root@localhost.com ~]#more echo_time.sh #!/bin/bash time=$(date) echo $time # 直接执行带参的Shell脚本 [root@localhost.com ~]#./echo_time.sh Fri Feb 14 19:18:40 CST 2019 [1]+ Stopped ./echo_time.sh #按下[ctrl]+z将其切换到后台 [root@localhost.com ~]#jobs [1]+ Stopped ./echo_time.sh [root@localhost.com ~]#kill ‐9 %1 #强制终止该job [1]+ Stopped ./echo_time.sh [root@localhost.com ~]#jobs #此时该job已经被标记为killed [1]+ Killed ./echo_time.sh [root@localhost.com ~]#./echo_time.sh \u0026amp; #将Shell脚本参数之后跟 \u0026amp;符号即将job放入到后台 [1] 2233 [root@localhost.com ~]# #此时依旧可以看到有输出，但可以继续后续操作 TODAY ‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐ 2019‐05‐03 11:08:25 [root@localhost.com ~]# jobs [1]+ Running ./echo_time.sh \u0026amp; [root@localhost.com ~]# ./echo_time.sh \u0026gt;temp.log 2\u0026gt;\u0026amp;1 \u0026amp; #最佳的办法是直接将其输出到日志文件 [2] 2256 [root@localhost.com ~]# jobs [1]‐ Running ./echo_time.sh \u0026amp; [2]+ Running ./echo_time.sh \u0026gt;temp.log 2\u0026gt;\u0026amp;1 \u0026amp; # 下面来查看日志，日志中的两次查询正好相差5分钟 [root@localhost.com ~]# more temp.log Fri Feb 14 19:18:40 CST 2019 4、作业脱机管理 将作业(进程)切换到后台可以避免由于误操作如[ctrl]+c等导致的job被异常中断的情形，而脱机管理主要是针 对终端异常断开的情形。 通常使用nohup命令来使得脱机或注销之后，Job依旧可以继续运行。也就是说nohup忽略所有挂断(SIGHUP) 信号。 如果该方式命令之后未指定\u0026amp;符号，则job位于前台，指定\u0026amp;符号，则job位于后台。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #下面是使用nohup的示例，可以省略日志的输出，因为原job的输出会自动被nohup重定向到缺省的nohup.out日志文件 [root@localhost.com ~]# nohup ./echo_time.sh nohup: appending output to `nohup.out\u0026#39; # 直接断开终端，并重新连接一个新的终端窗口 [root@localhost.com ~]# jobs # 由于是一个新的终端，所以jobs无法看到任何作业 [root@localhost.com ~]# ps ‐ef | grep echo_time.sh robin 2623 1 0 11:26 ? 00:00:00 /bin/bash ./echo_time.sh [root@localhost.com ~]# more nohup.out # 其输出的日志可以看到job被成功完成 Fri Feb 14 19:18:40 CST 2019 #下面使用 nohup方式且将 Job 放入后台处理，同时指定了日志文件，则nohup使用指定的日志文件，而不会输出到缺 省的nohup.out [root@localhost.com ~]# nohup ./echo_time.sh \u0026gt;temp2.log 2\u0026gt;\u0026amp;1 \u0026amp; [1] 3019 [root@localhost.com ~]# jobs [1]+ Running nohup ./echo_time.sh \u0026gt;temp2.log 2\u0026gt;\u0026amp;1 \u0026amp; 5、screen 命令使用 1、简介 Screen 是一款由GNU计划开发的用于命令行终端切换的自由软件。用户可以通过该软件同时连接多个本地或远程的命令行会话，并在其间自由切换。GNU Screen可以看作是窗口管理器的命令行界面版本。它提供了统一的管理多个会话的界面和相应的功能。\n1、会话恢复 只要Screen本身没有终止，在其内部运行的会话都可以恢复。这一点对于远程登录的用户特别有用——即使网络连接中断，用户也不会失去对已经打开的命令行会话的控制。只要再次登录到主机上执行screen -r就可以恢复会话的运行。同样在暂时离开的时候，也可以执行分离命令detach，在保证里面的程序正常运行的情况下让Screen挂起（切换到后台）。这一点和图形界面下的VNC很相似。 2、多窗口 在Screen环境下，所有的会话都独立的运行，并拥有各自的编号、输入、输出和窗口缓存。用户可以通过快捷键在不同的窗口下切换，并可以自由的重定向各个窗口的输入和输出。Screen实现了基本的文本操作，如复制粘贴等；还提供了类似滚动条的功能，可以查看窗口状况的历史记录。窗口还可以被分区和命名，还可以监视后台窗口的活动。 会话共享 Screen可以让一个或多个用户从不同终端多次登录一个会话，并共享会话的所有特性（比如可以看到完全相同的输出）。它同时提供了窗口访问权限的机制，可以对窗口进行密码保护。 2、安装 screen 流行的Linux发行版（例如Red Hat Enterprise Linux）通常会自带screen实用程序，如果没有的话，可以从GNU screen的官方网站下载。\n1 2 3 [root@qfdeu ~]# yum install screen [root@qfdeu ~]# rpm ‐qa|grep screen screen‐4.0.3‐4.el5 3、语法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@localhost ~]# screen [‐AmRvx ‐ls ‐wipe][‐d \u0026lt;作业名称\u0026gt;][‐h \u0026lt;行数\u0026gt;][‐r \u0026lt;作业名称\u0026gt;][‐s ][‐S \u0026lt;作业名 称\u0026gt;] ‐A 将所有的视窗都调整为目前终端机的大小。 ‐d \u0026lt;作业名称\u0026gt; 将指定的screen作业离线。 ‐h \u0026lt;行数\u0026gt; 指定视窗的缓冲区行数。 ‐m 即使目前已在作业中的screen作业，仍强制建立新的screen作业。 ‐r \u0026lt;作业名称\u0026gt; 恢复离线的screen作业。 ‐R 先试图恢复离线的作业。若找不到离线的作业，即建立新的screen作业。 ‐s 指定建立新视窗时，所要执行的Shell。 ‐S \u0026lt;作业名称\u0026gt; 指定screen作业的名称。 ‐v 显示版本信息。 ‐x 恢复之前离线的screen作业。 ‐ls或‐‐list 显示目前所有的screen作业。 ‐wipe 检查目前所有的screen作业，并删除已经无法使用的screen作业。 4、常用screen参数 1 2 3 4 5 [root@qfdeu ~]# screen ‐S yourname ‐\u0026gt; 新建一个叫yourname的session [root@qfdeu ~]# screen ‐ls ‐\u0026gt; 列出当前所有的session [root@qfdeu ~]# screen ‐r yourname ‐\u0026gt; 回到yourname这个session [root@qfdeu ~]# screen ‐d yourname ‐\u0026gt; 远程detach某个session [root@qfdeu ~]# screen ‐d ‐r yourname ‐\u0026gt; 结束当前session并回到yourname这个session 5、在 Session下，使用ctrl+a(C-a) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 C‐a ? ‐\u0026gt; 显示所有键绑定信息 C‐a c ‐\u0026gt; 创建一个新的运行Shell的窗口并切换到该窗口 C‐a n ‐\u0026gt; Next，切换到下一个 window C‐a p ‐\u0026gt; Previous，切换到前一个 window C‐a 0..9 ‐\u0026gt; 切换到第 0..9 个 window Ctrl+a [Space] ‐\u0026gt; 由视窗0循序切换到视窗9 C‐a C‐a ‐\u0026gt; 在两个最近使用的 window 间切换 C‐a x ‐\u0026gt; 锁住当前的 window，需用用户密码解锁 C‐a d ‐\u0026gt; detach，暂时离开当前session，将目前的 screen session (可能含有多个 windows) 丢到后台执行，并 会回到还没进 screen 时的状态，此时在 screen session 里，每个 window 内运行的 process (无论是前台/后 台)都在继续执行，即使 logout 也不影响。 C‐a z ‐\u0026gt; 把当前session放到后台执行，用 Shell 的 fg 命令则可回去。 C‐a w ‐\u0026gt; 显示所有窗口列表 C‐a t ‐\u0026gt; time，显示当前时间，和系统的 load C‐a k ‐\u0026gt; kill window，强行关闭当前的 window C‐a [ ‐\u0026gt; 进入 copy mode，在 copy mode 下可以回滚、搜索、复制就像用使用 vi 一样 C‐b Backward，PageUp C‐f Forward，PageDown H(大写) High，将光标移至左上角 L Low，将光标移至左下角 0 移到行首 $ 行末 w forward one word，以字为单位往前移 b backward one word，以字为单位往后移 Space 第一次按为标记区起点，第二次按为终点 Esc 结束 copy mode C‐a ] ‐\u0026gt; paste，把刚刚在 copy mode 选定的内容贴上 6、常用操作 1、创建会话（-m 强制）\n1 [root@qfdeu ~]# screen ‐dmS session_name # session_name session名称 2、关闭会话\n1 [root@qfdeu ~]# screen ‐X ‐S [session # you want to kill] quit 3、查看所有会话\n1 [root@qfdeu ~]# screen ‐ls 4、进入会话\n1 [root@qfdeu ~]# screen ‐r session_name 6、清除dead 会话\n如果由于某种原因其中一个会话死掉了（例如人为杀掉该会话），这时screen -list会显示该会话为dead状态。使用screen -wipe命令清除该会话： 7、关闭或杀死窗口\n正常情况下，当你退出一个窗口中最后一个程序（通常是bash）后，这个窗口就关闭了。另一个关闭窗口的方法是使用C-a k，这个快捷键杀死当前的窗口，同时也将杀死这个窗口中正在运行的进程。 如果一个Screen会话中最后一个窗口被关闭了，那么整个Screen会话也就退出了，screen进程会被终止。 除了依次退出/杀死当前Screen会话中所有窗口这种方法之外，还可以使用快捷键C-a :，然后输入quit命令退出Screen会话。需要注意的是，这样退出会杀死所有窗口并退出其中运行的所有程序。其实C-a :这个快捷键允许用户直接输入的命令有很多，包括分屏可以输入split等，这也是实现Screen功能的一个途径，不过个人认为还是快捷键比较方便些。 6、输入输出重定向 一般情况下，计算机从键盘读取用户输入的数据，然后再把数据拿到程序（C语言程序、Shell 脚本程序等）中使用；这就是标准的输入方向，也就是从键盘到程序。 程序中产生数据直接呈现到显示器上，这就是标准的输出方向，也就是从程序到显示器。输入输出方向就是数据的流动方向：\n输入方向就是数据从哪里流向程序。数据默认从键盘流向程序，如果改变了它的方向，数据就从其它地方流入，这就是输入重定向。 输出方向就是数据从程序流向哪里。数据默认从程序流向显示器，如果改变了它的方向，数据就流向其它地方，这就是输出重定向。 1、硬件设备和文件描述符\n计算机的硬件设备有很多，常见的输入设备有键盘、鼠标等，输出设备有显示器、投影仪、打印机等。不过，在 Linux中，标准输入设备指的是键盘，标准输出设备指的是显示器。 Linux 中一切皆文件，包括标准输入设备（键盘）和标准输出设备（显示器）在内的所有计算机硬件都是文件。 为了表示和区分已经打开的文件，Linux 会给每个文件分配一个 ID，这个 ID 就是一个整数，被称为文件描述符（File Descriptor）。 文件描述符 文件名 类型 硬件 0 stdin 标准输入文件 键盘 1 stdout 标准输出文件 显示器 2 stderr 标准错误输出文件 显示器 Linux 程序在执行任何形式的 I/O 操作时，都是在读取或者写入一个文件描述符。一个文件描述符只是一个和打开的文件相关联的整数，它的背后可能是一个硬盘上的普通文件、FIFO、管道、终端、键盘、显示器，甚至是一个网络连接。 stdin、stdout、stderr 默认都是打开的，在重定向的过程中，0、1、2 这三个文件描述符可以直接使用。 2、Shell 输出重定向\n输出重定向是指命令的结果不再输出到显示器上，而是输出到其它地方，一般是文件中。这样做的最大好处就是把命令的结果保存起来，当我们需要的时候可以随时查询。Bash 支持的输出重定向符号如下表所示。 类 型 符 号 作 用 标准输出重定向 command \u0026gt;file 以覆盖的方式，把 command 的正确输出结果输出到 file 文件中。 command\u0026raquo;file 以追加的方式，把 command 的正确输出结果输出到 file 文件中。 标准错误输出重定向 command 2\u0026gt;file 以覆盖的方式，把 command 的错误信息输出到 file 文件中。 command2\u0026raquo;file 以追加的方式，把 command 的错误信息输出到file 文件中。 正确输出和错误信息同时保存 command \u0026gt;file 2\u0026gt;\u0026amp;1 以覆盖的方式，把正确输出和错误信息同时保存到同一个文件（file）中。 command\u0026raquo;file 2\u0026gt;\u0026amp;1 以追加的方式，把正确输出和错误信息同时保存到同一个文件（file）中。 command\u0026gt;file12\u0026gt;file2 以覆盖的方式，把正确的输出结果输出到 file1 文件中，把错误信息输出到 file2 文件中。 command\u0026raquo;file12\u0026raquo;file2 以追加的方式，把正确的输出结果输出到 file1 文件中，把错误信息输出到 file2 文件中。 command\u0026gt;file 2\u0026gt;file 【不推荐】这两种写法会导致 file 被打开两次，引起资源竞争，所以 stdout 和 stderr 会互相覆盖， command\u0026raquo;file2\u0026raquo;file 注意： 输出重定向中，\u0026gt; 代表的是覆盖，\u0026raquo; 代表的是追加。 输出重定向的完整写法其实是 fd\u0026gt;file 或者 fd\u0026raquo;file ，其中 fd 表示文件描述符，如果不写，默认为 1，也就是标准输出文件。 当文件描述符为 1 时，一般都省略不写，如上表所示；当然，如果你愿意，也可以将 command \u0026gt;file 写作command 1\u0026gt;file，但这样做是多此一举。 当文件描述符为大于 1 的值时，比如 2，就必须写上。 需要重点说明的是，fd 和 \u0026gt;之间不能有空格，否则 Shell 会解析失败；\u0026gt; 和 file 之间的空格可有可无。为了保持一致，习惯在 \u0026gt; 两边都不加空格。 下面的语句是一个反面教材：\n1 [root@qfdeu ~]# echo \u0026#34;c.biancheng.net\u0026#34; 1 \u0026gt;log.txt 注意 1 和 \u0026gt; 之间的空格。echo 命令的输出结果是 c.biancheng.net，初衷是将输出结果重定向到 log.txt，打开log.txt 文件后，发现文件的内容为 c.biancheng.net 1，这就是多余的空格导致的解析错误。也就是说，Shell 将该条语句理解成了下面的形式：\n1 [root@qfdeu ~]# echo \u0026#34;c.biancheng.net\u0026#34; 1 1\u0026gt;log.txt ","permalink":"https://ktzxy.top/posts/2iol6gdhkw/","summary":"Shell学习","title":"Shell学习"},{"content":"Kubernetes核心技术Pod Pod概述 Pod是K8S系统中可以创建和管理的最小单元，是资源对象模型中由用户创建或部署的最小资源对象模型，也是在K8S上运行容器化应用的资源对象，其它的资源对象都是用来支撑或者扩展Pod对象功能的，比如控制器对象是用来管控Pod对象的，Service或者Ingress资源对象是用来暴露Pod引用对象的，PersistentVolume资源对象是用来为Pod提供存储等等，K8S不会直接处理容器，而是Pod，Pod是由一个或多个container组成。\nPod是Kubernetes的最重要概念，每一个Pod都有一个特殊的被称为 “根容器”的Pause容器。Pause容器对应的镜像属于Kubernetes平台的一部分，除了Pause容器，每个Pod还包含一个或多个紧密相关的用户业务容器。\nPod基本概念 最小部署的单元 Pod里面是由一个或多个容器组成【一组容器的集合】 一个pod中的容器是共享网络命名空间 Pod是短暂的 每个Pod包含一个或多个紧密相关的用户业务容器 Pod存在的意义 创建容器使用docker，一个docker对应一个容器，一个容器运行一个应用进程 Pod是多进程设计，运用多个应用程序，也就是一个Pod里面有多个容器，而一个容器里面运行一个应用程序 Pod的存在是为了亲密性应用 两个应用之间进行交互 网络之间的调用【通过127.0.0.1 或 socket】 两个应用之间需要频繁调用 Pod是在K8S集群中运行部署应用或服务的最小单元，它是可以支持多容器的。Pod的设计理念是支持多个容器在一个Pod中共享网络地址和文件系统，可以通过进程间通信和文件共享这种简单高效的方式组合完成服务。同时Pod对多容器的支持是K8S中最基础的设计理念。在生产环境中，通常是由不同的团队各自开发构建自己的容器镜像，在部署的时候组合成一个微服务对外提供服务。\nPod是K8S集群中所有业务类型的基础，可以把Pod看作运行在K8S集群上的小机器人，不同类型的业务就需要不同类型的小机器人去执行。目前K8S的业务主要可以分为以下几种\n长期伺服型：long-running 批处理型：batch 节点后台支撑型：node-daemon 有状态应用型：stateful application 上述的几种类型，分别对应的小机器人控制器为：Deployment、Job、DaemonSet 和 StatefulSet (后面将介绍控制器)\nPod实现机制 主要有以下两大机制\n共享网络 共享存储 共享网络 容器本身之间相互隔离的，一般是通过 namespace 和 group 进行隔离，那么Pod里面的容器如何实现通信？\n首先需要满足前提条件，也就是容器都在同一个namespace之间 关于Pod实现原理，首先会在Pod会创建一个根容器： pause容器，然后我们在创建业务容器 【nginx，redis 等】，在我们创建业务容器的时候，会把它添加到 info容器 中\n而在 info容器 中会独立出 ip地址，mac地址，port 等信息，然后实现网络的共享\n完整步骤如下\n通过 Pause 容器，把其它业务容器加入到Pause容器里，让所有业务容器在同一个名称空间中，可以实现网络共享 共享存储 Pod持久化数据，专门存储到某个地方中\n使用 Volumn数据卷进行共享存储，案例如下所示\nPod镜像拉取策略 我们以具体实例来说，拉取策略就是 imagePullPolicy\n拉取策略主要分为了以下几种\nIfNotPresent：默认值，镜像在宿主机上不存在才拉取 Always：每次创建Pod都会重新拉取一次镜像 Never：Pod永远不会主动拉取这个镜像 Pod资源限制 也就是我们Pod在进行调度的时候，可以对调度的资源进行限制，例如我们限制 Pod调度是使用的资源是 2C4G，那么在调度对应的node节点时，只会占用对应的资源，对于不满足资源的节点，将不会进行调度\n示例 我们在下面的地方进行资源的限制\n这里分了两个部分\nrequest：表示调度所需的资源 limits：表示最大所占用的资源 Pod重启机制 因为Pod中包含了很多个容器，假设某个容器出现问题了，那么就会触发Pod重启机制\n重启策略主要分为以下三种\nAlways：当容器终止退出后，总是重启容器，默认策略 【nginx等，需要不断提供服务】 OnFailure：当容器异常退出（退出状态码非0）时，才重启容器。 Never：当容器终止退出，从不重启容器 【批量任务】 Pod健康检查 通过容器检查，原来我们使用下面的命令来检查\n1 kubectl get pod 但是有的时候，程序可能出现了 Java 堆内存溢出，程序还在运行，但是不能对外提供服务了，这个时候就不能通过 容器检查来判断服务是否可用了\n这个时候就可以使用应用层面的检查\n1 2 3 4 5 # 存活检查，如果检查失败，将杀死容器，根据Pod的restartPolicy【重启策略】来操作 livenessProbe # 就绪检查，如果检查失败，Kubernetes会把Pod从Service endpoints中剔除 readinessProbe Probe支持以下三种检查方式\nhttp Get：发送HTTP请求，返回200 - 400 范围状态码为成功 exec：执行Shell命令返回状态码是0为成功 tcpSocket：发起TCP Socket建立成功 Pod调度策略 创建Pod流程 首先创建一个pod，然后创建一个API Server 和 Etcd【把创建出来的信息存储在etcd中】 然后创建 Scheduler，监控API Server是否有新的Pod，如果有的话，会通过调度算法，把pod调度某个node上 在node节点，会通过 kubelet -- apiserver 读取etcd 拿到分配在当前node节点上的pod，然后通过docker创建容器 影响Pod调度的属性 Pod资源限制对Pod的调度会有影响\n根据request找到足够node节点进行调度 节点选择器标签影响Pod调度 关于节点选择器，其实就是有两个环境，然后环境之间所用的资源配置不同\n我们可以通过以下命令，给我们的节点新增标签，然后节点选择器就会进行调度了\n1 kubectl label node node1 env_role=prod 节点亲和性 节点亲和性 nodeAffinity 和 之前nodeSelector 基本一样的，根据节点上标签约束来决定Pod调度到哪些节点上\n硬亲和性：约束条件必须满足 软亲和性：尝试满足，不保证 支持常用操作符：in、NotIn、Exists、Gt、Lt、DoesNotExists\n反亲和性：就是和亲和性刚刚相反，如 NotIn、DoesNotExists等\n污点和污点容忍 概述 nodeSelector 和 NodeAffinity，都是Prod调度到某些节点上，属于Pod的属性，是在调度的时候实现的。\nTaint 污点：节点不做普通分配调度，是节点属性\n场景 专用节点【限制ip】 配置特定硬件的节点【固态硬盘】 基于Taint驱逐【在node1不放，在node2放】 查看污点情况 1 kubectl describe node k8smaster | grep Taint 污点值有三个\nNoSchedule：一定不被调度 PreferNoSchedule：尽量不被调度【也有被调度的几率】 NoExecute：不会调度，并且还会驱逐Node已有Pod 未节点添加污点 1 kubectl taint node [node] key=value:污点的三个值 举例：\n1 kubectl taint node k8snode1 env_role=yes:NoSchedule 删除污点 1 kubectl taint node k8snode1 env_role:NoSchedule- 演示 我们现在创建多个Pod，查看最后分配到Node上的情况\n首先我们创建一个 nginx 的pod\n1 kubectl create deployment web --image=nginx 然后使用命令查看\n1 kubectl get pods -o wide 我们可以非常明显的看到，这个Pod已经被分配到 k8snode1 节点上了\n下面我们把pod复制5份，在查看情况pod情况\n1 kubectl scale deployment web --replicas=5 我们可以发现，因为master节点存在污点的情况，所以节点都被分配到了 node1 和 node2节点上\n我们可以使用下面命令，把刚刚我们创建的pod都删除\n1 kubectl delete deployment web 现在给了更好的演示污点的用法，我们现在给 node1节点打上污点\n1 kubectl taint node k8snode1 env_role=yes:NoSchedule 然后我们查看污点是否成功添加\n1 kubectl describe node k8snode1 | grep Taint 然后我们在创建一个 pod\n1 2 3 4 # 创建nginx pod kubectl create deployment web --image=nginx # 复制五次 kubectl scale deployment web --replicas=5 然后我们在进行查看\n1 kubectl get pods -o wide 我们能够看到现在所有的pod都被分配到了 k8snode2上，因为刚刚我们给node1节点设置了污点\n最后我们可以删除刚刚添加的污点\n1 kubectl taint node k8snode1 env_role:NoSchedule- 污点容忍 污点容忍就是某个节点可能被调度，也可能不被调度\n","permalink":"https://ktzxy.top/posts/fsaxhwyq5z/","summary":"8 Kubernetes核心技术Pod","title":"8 Kubernetes核心技术Pod"},{"content":"使用kubeadm-ha脚本一键安装K8S 前情提示 以前安装 K8S 集群的时候使用的是 k8s 官网的教程 使用的镜像源都是国外的 速度慢就不说了 还有一些根本就下载不动 导致安装失败 最后在群里小伙伴(蘑菇博客交流群/@你钉钉响了) 的建议下使用一个开源的一键安装k8s的脚本就好了起来了\nGithub地址：https://github.com/TimeBye/kubeadm-ha\n环境准备 官网的安装说明也很简单但是还有些细节还是没有提到，所以我自己照着官网的教程 补充了一些细节\n硬件系统要求 Master节点：2C4G + Worker节点：2C4G + 使用centos7.7安装请按上面配置准备好3台centos,1台作为Master节点,2台Worker节点\n本方式为1主2worker的配置\n这是我的各个节点的配置\n主机名 ip 配置 k8s-master 192.168.177.130 2C4G k8s-node1 192.168.177.131 2C2G k8s-node2 192.168.177.132 2C2G centos准备 在安装之前需要准备一些基础的软件环境用于下载一键安装k8s的脚本和编辑配置\ncentos网络准备 安装时需要连接互联网下载各种软件 所以需要保证每个节点都可以访问外网\n1 ping baidu.com 建议关闭 CentOS 的防火墙\n1 systemctl stop firewalld \u0026amp;\u0026amp; systemctl disable firewalld \u0026amp;\u0026amp; systemctl status firewalld 同时需要保证各个节点间可以相互ping通\n1 ping 其他节点ip CentOS软件准备 用 ssh 连接到 Master 节点上安装 Git\n1 yum install git -y 部署k8s前配置 下载部署脚本 在Master节点clone安装脚本 脚本地址\n1 git clone --depth 1 https://github.com/TimeBye/kubeadm-ha 进入到下载的部署脚本的目录\n1 cd kubeadm-ha 安装 Ansible 运行环境 在master节点安装Ansible环境\n1 sudo ./install-ansible.sh 修改安装的配置文件 由于我是一个master两个node的方式构建的centos所以我们需要修改example/hosts.s-master.ip.ini 文件\n1 vi example/hosts.s-master.ip.ini 具体要修改的就是 ip 和密码 其他的保持默认\n我的hosts.s-master.ip.ini 文件预览\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 ; 将所有节点信息在这里填写 ; 第一个字段 为远程服务器内网IP ; 第二个字段 ansible_port 为节点 sshd 监听端口 ; 第三个字段 ansible_user 为节点远程登录用户名 ; 第四个字段 ansible_ssh_pass 为节点远程登录用户密码 [all] 192.168.177.130 ansible_port=22 ansible_user=\u0026#34;root\u0026#34; ansible_ssh_pass=\u0026#34;moxi\u0026#34; 192.168.177.131 ansible_port=22 ansible_user=\u0026#34;root\u0026#34; ansible_ssh_pass=\u0026#34;moxi\u0026#34; 192.168.177.132 ansible_port=22 ansible_user=\u0026#34;root\u0026#34; ansible_ssh_pass=\u0026#34;moxi\u0026#34; ; 单 master 节点不需要进行负载均衡，lb节点组留空。 [lb] ; 注意etcd集群必须是1,3,5,7...奇数个节点 [etcd] 192.168.177.130 192.168.177.131 192.168.177.132 [kube-master] 192.168.177.130 [kube-worker] 192.168.177.130 192.168.177.131 192.168.177.132 ; 预留组，后续添加master节点使用 [new-master] ; 预留组，后续添加worker节点使用 [new-worker] ; 预留组，后续添加etcd节点使用 [new-etcd] ; 预留组，后续删除worker角色使用 [del-worker] ; 预留组，后续删除master角色使用 [del-master] ; 预留组，后续删除etcd角色使用 [del-etcd] ; 预留组，后续删除节点使用 [del-node] ;-------------------------------------- 以下为基础信息配置 ------------------------------------; [all:vars] ; 是否跳过节点物理资源校验，Master节点要求2c2g以上，Worker节点要求2c4g以上 skip_verify_node=true ; kubernetes版本 kube_version=\u0026#34;1.18.14\u0026#34; ; 负载均衡器 ; 有 nginx、openresty、haproxy、envoy 和 slb 可选，默认使用 nginx ; 为什么单 master 集群 apiserver 也使用了负载均衡请参与此讨论： https://github.com/TimeBye/kubeadm-ha/issues/8 lb_mode=\u0026#34;nginx\u0026#34; ; 使用负载均衡后集群 apiserver ip，设置 lb_kube_apiserver_ip 变量，则启用负载均衡器 + keepalived ; lb_kube_apiserver_ip=\u0026#34;192.168.56.15\u0026#34; ; 使用负载均衡后集群 apiserver port lb_kube_apiserver_port=\u0026#34;8443\u0026#34; ; 网段选择：pod 和 service 的网段不能与服务器网段重叠， ; 若有重叠请配置 `kube_pod_subnet` 和 `kube_service_subnet` 变量设置 pod 和 service 的网段，示例参考： ; 如果服务器网段为：10.0.0.1/8 ; pod 网段可设置为：192.168.0.0/18 ; service 网段可设置为 192.168.64.0/18 ; 如果服务器网段为：172.16.0.1/12 ; pod 网段可设置为：10.244.0.0/18 ; service 网段可设置为 10.244.64.0/18 ; 如果服务器网段为：192.168.0.1/16 ; pod 网段可设置为：10.244.0.0/18 ; service 网段可设置为 10.244.64.0/18 ; 集群pod ip段，默认掩码位 18 即 16384 个ip kube_pod_subnet=\u0026#34;10.244.0.0/18\u0026#34; ; 集群service ip段 kube_service_subnet=\u0026#34;10.244.64.0/18\u0026#34; ; 分配给节点的 pod 子网掩码位，默认为 24 即 256 个ip，故使用这些默认值可以纳管 16384/256=64 个节点。 kube_network_node_prefix=\u0026#34;24\u0026#34; ; node节点最大 pod 数。数量与分配给节点的 pod 子网有关，ip 数应大于 pod 数。 ; https://cloud.google.com/kubernetes-engine/docs/how-to/flexible-pod-cidr kube_max_pods=\u0026#34;110\u0026#34; ; 集群网络插件，目前支持flannel,calico network_plugin=\u0026#34;calico\u0026#34; ; 若服务器磁盘分为系统盘与数据盘，请修改以下路径至数据盘自定义的目录。 ; Kubelet 根目录 kubelet_root_dir=\u0026#34;/var/lib/kubelet\u0026#34; ; docker容器存储目录 docker_storage_dir=\u0026#34;/var/lib/docker\u0026#34; ; Etcd 数据根目录 etcd_data_dir=\u0026#34;/var/lib/etcd\u0026#34; 升级内核 修改完配置文件后建议升级内核\n1 ansible-playbook -i example/hosts.s-master.ip.ini 00-kernel.yml 内核升级完毕后重启所有节点 在master node1 node2上执行\n1 reboot 开始部署k8s 等待所有的节点重启完成后进入脚本目录\n1 cd kubeadm-ha 执行一键部署命令 1 ansible-playbook -i example/hosts.s-master.ip.ini 90-init-cluster.yml 查看节点运行情况 1 kubectl get nodes 等待所有节点ready 即为创建成功\n1 2 3 4 NAME STATUS ROLES AGE VERSION 192.168.28.128 Ready etcd,worker 2m57s v1.18.14 192.168.28.80 Ready etcd,master,worker 3m29s v1.18.14 192.168.28.89 Ready etcd,worker 2m57s v1.18.14 集群重置 如果部署失败了，想要重置整个集群【包括数据】，执行下面脚本\n1 ansible-playbook -i example/hosts.s-master.ip.ini 99-reset-cluster.yml 部署kuboard 安装Docker 因为我们需要拉取镜像，所以需要在服务器提前安装好Docker，首先配置一下Docker的阿里yum源\n1 2 3 4 5 6 7 8 cat \u0026gt;/etc/yum.repos.d/docker.repo\u0026lt;\u0026lt;EOF [docker-ce-edge] name=Docker CE Edge - \\$basearch baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/7/\\$basearch/edge enabled=1 gpgcheck=1 gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg EOF 然后yum方式安装docker\n1 2 3 4 5 6 7 8 # yum安装 yum -y install docker-ce # 查看docker版本 docker --version # 开机自启 systemctl enable docker # 启动docker systemctl start docker 配置docker的镜像源\n1 2 3 4 5 cat \u0026gt;\u0026gt; /etc/docker/daemon.json \u0026lt;\u0026lt; EOF { \u0026#34;registry-mirrors\u0026#34;: [\u0026#34;https://b9pmyelo.mirror.aliyuncs.com\u0026#34;] } EOF 然后重启docker\n1 systemctl restart docker 安装Kuboard【可选】 简介 Kuboard 是一款免费的 Kubernetes 图形化管理工具，力图帮助用户快速在 Kubernetes 上落地微服务。\nKuboard文档：https://kuboard.cn/\n安装 在master节点执行\n1 2 kubectl apply -f https://kuboard.cn/install-script/kuboard.yaml kubectl apply -f https://addons.kuboard.cn/metrics-server/0.3.7/metrics-server.yaml 查看 Kuboard 运行状态\n1 kubectl get pods -l k8s.kuboard.cn/name=kuboard -n kube-system 输出结果如下所示。注意：如果是 ContainerCreating 那么需要等待一会\n1 2 NAME READY STATUS RESTARTS AGE kuboard-74c645f5df-cmrbc 1/1 Running 0 80s 访问Kuboard Kuboard Service 使用了 NodePort 的方式暴露服务，NodePort 为 32567；您可以按如下方式访问 Kuboard。\n1 2 3 4 5 # 格式 http://任意一个Worker节点的IP地址:32567/ # 例如，我的访问地址如下所示 http://192.168.177.130:32567/ 页面如下所示：\n第一次访问需要输入token 我们获取一下 token， 在master节点执行\n1 echo $(kubectl -n kube-system get secret $(kubectl -n kube-system get secret | grep kuboard-user | awk \u0026#39;{print $1}\u0026#39;) -o go-template=\u0026#39;{{.data.token}}\u0026#39; | base64 -d) 获取到的 token，然后粘贴到框中，我的 token 格式如下：\n1 eyJhbGciOiJSUzI1NiIsImtpZCI6ImY1eUZlc0RwUlZha0E3LWZhWXUzUGljNDM3SE0zU0Q4dzd5R3JTdXM2WEUifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJrdWJvYXJkLXVzZXItdG9rZW4tMmJsamsiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoia3Vib2FyZC11c2VyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiYzhlZDRmNDktNzM0Zi00MjU1LTljODUtMWI5MGI4MzU4ZWMzIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Omt1YmUtc3lzdGVtOmt1Ym9hcmQtdXNlciJ9.MujbwGnkL_qa3H14oKDT1zZ5Fzt16pWoaY52nT7fV5B2nNIRsB3Esd18S8ztHUJZLRGxAhBwu-utToi2YBb8pH9RfIeSXMezFZ6QhBbp0n5xYWeYETQYKJmes2FRcW-6jrbpvXlfUuPXqsbRX8qrnmSVEbcAms22CSSVhUbTz1kz8C7b1C4lpSGGuvdpNxgslNFZTFrcImpelpGSaIGEMUk1qdjKMROw8bV83pga4Y41Y6rJYE3hdnCkUA8w2SZOYuF2kT1DuZuKq3A53iLsvJ6Ps-gpli2HcoiB0NkeI_fJORXmYfcj5N2Csw6uGUDiBOr1T4Dto-i8SaApqmdcXg 最后即可进入 kuboard 的 dashboard 界面\n卸载Kuboard 当我们 kuboard 不想使用的时候，我们就可以直接卸载\n1 2 kubectl delete -f https://kuboard.cn/install-script/kuboard.yaml kubectl delete -f https://addons.kuboard.cn/metrics-server/0.3.7/metrics-server.yaml Rancher部署【可选】 kuboard和rancher建议部署其中一个\nhelm安装 使用helm部署rancher会方便很多，所以需要安装helm\n1 2 3 curl -O http://rancher-mirror.cnrancher.com/helm/v3.2.4/helm-v3.2.4-linux-amd64.tar.gz tar -zxvf helm-v3.2.4-linux-amd64.tar.gz mv linux-amd64/helm /usr/local/bin 验证 1 helm version 输入以下内容说明helm安装成功\n1 version.BuildInfo{Version:\u0026#34;v3.2.4\u0026#34;, GitCommit:\u0026#34;0ad800ef43d3b826f31a5ad8dfbb4fe05d143688\u0026#34;, GitTreeState:\u0026#34;clean\u0026#34;, GoVersion:\u0026#34;go1.13.12\u0026#34;} 添加rancher chart仓库 1 2 helm repo add rancher-stable http://rancher-mirror.oss-cn-beijing.aliyuncs.com/server-charts/stable helm repo update 安装rancher 1 2 3 4 helm install rancher rancher-stable/rancher \\ --create-namespace\t\\ --namespace cattle-system \\ --set hostname=rancher.local.com 等待 Rancher 运行： 1 kubectl -n cattle-system rollout status deploy/rancher 输出信息：\n1 2 Waiting for deployment \u0026#34;rancher\u0026#34; rollout to finish: 0 of 3 updated replicas are available... deployment \u0026#34;rancher\u0026#34; successfully rolled out ","permalink":"https://ktzxy.top/posts/fmsgcgkhw5/","summary":"30 使用kubeadm ha脚本一键安装K8S","title":"30 使用kubeadm ha脚本一键安装K8S"},{"content":"1. 索引概述 1.1. 什么是索引 MySQL 官方对索引的定义为：索引（index）是帮助 MySQL 高效获取数据的数据结构（有序）。在数据之外，数据库系统还维护者满足特定查找算法的数据结构，这些数据结构以某种方式引用（指向）数据，这样就可以在这些数据结构上实现高级查找算法，这种数据结构就是索引。如下面的示意图所示：\n索引的本质是：一种数据结构 索引的作用是：高效获取数据 左边是数据表，一共有两列七条记录，最左边的是数据记录的物理地址（注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的）。为了加快Col2的查找，可以维护一个右边所示的二叉查找树，每个节点分别包含索引键值和一个指向对应数据记录物理地址的指针，这样就可以运用二叉查找快速获取到相应数据。\n一般来说索引本身也很大，不可能全部存储在内存中，因此索引往往以索引文件的形式存储在磁盘上。索引是数据库中用来提高性能的最常用的工具。\n1.2. 索引的优缺点 1.2.1. 索引的优势 类似于书籍的目录索引，提高数据检索的效率，降低数据库的IO成本。 通过索引列对数据进行排序，降低数据排序的成本，降低CPU的消耗。 1.2.2. 索引的代价（劣势） 空间上的代价 每建立一个索引都要为它建立一棵 B+ 树，每一棵 B+ 树的每一个节点都是一个数据页，一个页默认会占用 16KB 的存储空间，一棵很大的 B+ 树由许多数据页组成会占据很多的存储空间。\n实际上索引也是一张表，该表中保存了主键与索引字段，并指向实体类的记录，所以索引列也是要占用空间的。\n时间上的代价 虽然索引大大提高了查询效率，同时却也降低更新表的速度，如对表进行INSERT、UPDATE、DELETE等操作。因为更新表时，MySQL 不仅要保存数据，还要保存索引文件因每次更新添加了索引列的字段，都会调整因为更新所带来的键值变化后的索引信息，即需要修改各个 B+ 树索引。\nB+ 树每层节点都是按照索引列的值从小到大的顺序排序而组成了双向链表。不论是叶子节点中的记录，还是非叶子内节点中的记录都是按照索引列的值从小到大的顺序而形成了一个单向链表。而增、删、改操作可能会对节点和记录的排序造成破坏，所以存储引擎需要额外的时间进行一些记录移位，页面分裂、页面回收的操作来维护好节点和记录的排序。如果我们建了许多索引，每个索引对应的 B+ 树都要进行相关的维护操作，这必然会对性能造成影响。\n2. 索引的基础语法 InnoDB 和 MyISAM 会自动为主键或者声明为UNIQUE的列去自动建立B+树索引。如要给表中其他列创建索引则需要通过sql语句去指定\n2.1. 查看索引 1 SHOW INDEX FROM table_name; 2.2. 创建/修改索引 2.2.1. 语法 建表同时创建索引 1 2 3 4 CREATE TALBE 表名 ( 各种列的信息 ··· , [KEY|INDEX] 索引名 (需要被索引的单个列或多个列) ) 单独创建索引 1 CREATE [UNIQUE] INDEX 索引名 ON 表名(字段名(length)); 创建/修改索引 1 2 3 ALTER TABLE 表名 ADD [UNIQUE] INDEX [索引名] (字段名(length)); -- 可以同时创建多个索引 ALTER TABLE 表名 ADD [UNIQUE] INDEX [索引名] (字段名(length)), ADD [UNIQUE] INDEX [索引名] (字段名(length)), ...; 2.2.2. 示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 -- 普通索引方式1-创建表的时候直接指定 create table student( sid int primary key, card_id varchar(20), name varchar(20), gender varchar(20), age int, birth date, phone_num varchar(20), score double, index index_name(name) -- 给name列创建索引 ); -- 普通索引方式2-直接创建 create index index_gender on student(gender); -- 普通索引方式3-修改表结构(添加索引) alter table student add index index_age(age); -- 唯一索引方式1-创建表的时候直接指定 create table student2( sid int primary key, card_id varchar(20), name varchar(20), gender varchar(20), age int, birth date, phone_num varchar(20), score double, unique index_card_id(card_id) -- 给card_id列创建索引 ); -- 唯一索引方式2-直接创建 create unique index index_card_id on student2(card_id); -- 唯一索引方式3-修改表结构(添加索引) alter table student2 add unique index_phone_num(phone_num) -- 创建索引的基本语法 create index indexname on table_name(column1(length),column2(length)); 2.2.3. 两种创建索引方式的区别 创建(修改) 索引有两种方式，分别使用 CREATE 与 ALTER 关键字。两种索引的区别：\nALTER 创建索引时可以省略索引名称，数据库会默认根据第一个索引列作为索引的名称；CREATE 创建索引时必须指定索引名称。 CREATE 不能用于创建 Primary Key 索引。 ALTER 方式允许一条语句同时创建多个索引；CREATE 方式一次只能创建一个索引。 2.3. 删除索引 1 2 3 4 5 -- 方式一 DROP INDEX [索引名] ON 表名; -- 方式二 ALTER TABLE 表名 DROP [INDEX|KEY] 索引名; 值得注意的是：若删除表中涉及索引的某列，索引会受到影响。对于多列组合索引，如果删除其中的某一列，则该列会从对应的索引中被删除（即删除列，不会删除索引）；如果删除组成索引的所有列，则索引将被删除（即不仅删除列，还删除相应的索引）。\n2.4. 大表添加索引的优化方案 如果一张表数据量级是千万级别以上，给表添加索引的时候，是会对表加锁的。如果不谨慎操作，有可能出现生产事故的。可以参考以下添加索引的方法进行优化：\n先创建一张跟原表 A 数据结构相同的新表 B。 在新表 B 添加需要加上的新索引。 把原表 A 数据导到新表 B。 rename 新表 B 为原表的表名 A，原表 A 换别的表名 3. 索引结构（从数据结构维度划分） 索引是在 MySQL 的存储引擎层中实现的，而不是在服务器层实现的。所以每种存储引擎的索引都不一定完全相同，也不是所有的存储引擎都支持所有的索引类型的。MySQL 目前提供了以下4种索引：\nB-TREE 索引：最常见的索引类型，大部分索引都支持 B 树索引。 HASH 索引：只有 Memory 引擎支持，使用场景简单。 R-tree 索引（空间索引）：空间索引是 MyISAM 引擎的一个特殊索引类型，主要用于地理空间数据类型，通常使用较少，了解即可。 Full-text（全文索引）：全文索引也是 MyISAM 的一个特殊索引类型，主要用于全文索引，InnoDB 从 MySQL 5.6 版本开始支持全文索引。 索引 InnoDB引擎 MyISAM引擎 Memory引擎 BTREE索引 支持 支持 支持 HASH 索引 不支持 不支持 支持 R-tree 索引 不支持 支持 不支持 Full-text 5.6 版本之后支持 支持 不支持 注：平常所说的索引，如果没有特别指明，都是指B+树（多路搜索树，并不一定是二叉的）结构组织的索引。其中聚集索引、复合索引、前缀索引、唯一索引默认都是使用 B+tree 索引，统称为索引\n3.1. BTREE 结构 BTree又叫多路平衡搜索树，一颗m叉的BTree特性如下：\n树中每个节点最多包含m个孩子。 除根节点与叶子节点外，每个节点至少有[ceil(m/2)]个孩子。 若根节点不是叶子节点，则至少有两个孩子。 所有的叶子节点都在同一层。 每个非叶子节点由n个key与n+1个指针组成，其中[ceil(m/2)-1] \u0026lt;= n \u0026lt;= m-1 以5叉BTree为例，由key的数量（即是key=5）根据公式[ceil(m/2)-1] \u0026lt;= n \u0026lt;= m-1推导可得知，所以2 \u0026lt;= n \u0026lt;=4。当n\u0026gt;4时，中间节点分裂到父节点，两边节点分裂。\nTips: 树的度数指的是一个节点的子节点个数。\n插入 C N G A H E K Q M F W L T Z D P R X Y S 数据为例，其演变过程如下：\n插入前4个字母 C N G A 插入H，n\u0026gt;4，中间元素G字母向上分裂到新的节点 插入E，K，Q不需要分裂 插入M，中间元素M字母向上分裂到父节点G 插入F，W，L，T不需要分裂 插入Z，中间元素T向上分裂到父节点中 插入D，中间元素D向上分裂到父节点中。然后插入P，R，X，Y不需要分裂 最后插入S，NPQR节点n\u0026gt;5，中间节点Q向上分裂，但分裂后父节点DGMT的n\u0026gt;5，中间节点M向上分裂 到此，该BTREE树就已经构建完成了，BTREE树和二叉树相比，查询数据的效率更高，因为对于相同的数据量来说，BTREE的层级结构比二叉树小，因此搜索速度快。\nTips: 可通过数据结构可视化的网站来体验 BTREE 的数据结构演变过程。 https://www.cs.usfca.edu/~galles/visualization/BTree.html\n3.2. B+TREE 结构 3.2.1. B+TREE 简介 B+Tree 为 BTree 的变种，B+Tree 与 BTree 的区别为：\nn 叉 B+Tree 最多含有 n 个 key，而 BTree 最多含有 n-1 个 key B+Tree 的叶子节点保存所有的 key 信息，依 key 大小顺序排列 所有的非叶子节点都可以看作是 key 的索引部分，不存储数据。只存储索引(冗余)是为了可以放更多的索引 叶子节点包含所有索引字段 所有叶子节点使用指针连接，提高区间访问的性能（MySQL 增加的特性） 由于 B+Tree 只有叶子节点保存 key 信息，查询任何 key 都要从 root 节点走到叶子节点。所以 B+Tree 的查询效率更加稳定\nTips: MySQL 在高版本里，会优化将索引都常驻内存中，提升查询的速度\n3.2.2. MySQL 中的 B+Tree MySql 索引数据结构对经典的 B+Tree 进行了优化。在原 B+Tree 的基础上，增加一个指向相邻叶子节点的链表指针，就形成了带有顺序指针的 B+Tree，提高区间访问的性能。MySQL 中的 B+Tree 索引结构示意图如下：\nNotes:\n特别注意，一个索引对应一个B+Tree 底层以上的部分，是索引部分，仅仅起到索引数据的作用，不存储数据。 最底层部分是数据存储部分，在其叶子节点中要存储具体的数据。 可通过数据结构可视化的网站来体验 BTREE 的数据结构演变过程。 https://www.cs.usfca.edu/~galles/visualization/BPlusTree.html 3.3. 磁盘和B+树的关系 关系型数据库都选择了B+树数据结构，这个和磁盘的特性有着非常大的关系。\n3.3.1. 磁盘结构 盘片被划分成一系列同心环，圆心是盘片中心，每个同心环叫做一个磁道，所有半径相同的磁道组成一个柱面。磁道被沿半径线划分成一个个小的段，每个段叫做一个扇区，每个扇区是磁盘的最小存储单元也是最小读写单元。现在磁盘扇区一般是 512 个字节~4k 个字节。\n磁盘上数据必须用一个三维地址唯一标示：柱面号、盘面号、扇区号。读/写磁盘上某一指定数据需要下面步骤：\n首先移动臂根据柱面号使磁头移动到所需要的柱面上，这一过程被称为定位或查找。 所有磁头都定位到磁道上后，这时根据盘面号来确定指定盘面上的具体磁道。 盘面确定以后，盘片开始旋转，将指定块号的磁道段移动至磁头下。 经过上面步骤，指定数据的存储位置就被找到。这时就可以开始读/写操作。磁盘读取依靠的是机械运动，分为寻道时间、旋转延迟、传输时间三个部分，这三个部分耗时相加就是一次磁盘 IO 的时间，一般大概 9ms 左右。\n为了提高效率，要尽量减少磁盘 I/O。为此，磁盘往往不是严格按需读取，而是每次都会预读，即使只需要一个字节，磁盘也会从这个位置开始，顺序向后读取一定长度的数据放入内存，这个称之为预读。这样做的理论依据是计算机科学中著名的局部性原理：当一个数据被用到时，其附近的数据也通常会马上被使用。程序运行期间所需要的数据通常比较集中。\n磁盘顺序读取的效率很高（不需要寻道时间，只需很少的旋转时间）。\n机械磁盘的顺序读的效率是随机读的 40 到 400 倍。顺序写是随机写的 10 到 100 倍 SSD 盘顺序读写的效率是随机读写的 7 到 10 倍 3.3.2. B+树结构在磁盘的存储 预读的长度一般为页（page）的整倍数。页是计算机管理存储器的逻辑块，硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块，每个存储块称为一页，页大小通常为 4k 当然也有 16K 的，主存和磁盘以页为单位交换数据。即可以理解为，一页就是一个磁盘块，代表一次磁 I/O。\n按照磁盘的这种性质，如果是一个页存放一个 B+树的节点，自然是可以存很多的数据的，比如 InnoDB 默认定义的 B+树的节点大小是 16KB，这就是说，假如一个 Key 是 8 个字节，那么一个节点可以存放大约 1000 个 Key，意味着 B+数可以有 1000 个分叉。同时 InnoDB 每一次磁盘 I/O，读取的都是 16KB 的整数倍的数据。也就是说 InnoDB 在节点的读写上是可以充分利用磁盘顺序 IO 的高速读写特性。\n按照 B+树逻辑结构来说，在叶子节点一层，所有记录的主键按照从小到大的顺序排列，并且形成了一个双向链表。同一层的非叶子节点也互相串联，形成了一个双向链表。那么在实际读写的时候，很大的概率相邻的节点会放在相邻的页上，又可以充分利用磁盘顺序 I/O 的高速读写特性。所以对 MySQL 优化的一大方向就是尽可能的多让数据顺序读写，少让数据随机读写。\n3.4. Hash 结构 MySQL 中除了支持 B+Tree 索引，还支持一种索引类型 - Hash 索引。\n3.4.1. 结构 哈希索引就是采用一定的 hash 算法，将键值换算成新的 hash 值，映射到对应的槽位上，然后存储在 hash 表中。如果两个(或多个)键值，映射到一个相同的槽位上，他们就产生了 hash 冲突（也称为 hash 碰撞），可以通过链表来解决。\n若数据产生较少的 hash 冲突，那么查找一个数据的时间复杂度就是O(1)，因此一般多用于精确查找。\n3.4.2. Hash 索引和B+树优劣比较 Hash 索引只能用于对等比较(=，in)，不支持范围查询（between，\u0026gt;，\u0026lt; ，\u0026hellip;） Hash 索引无法利用索引完成排序操作 Hash 索引不支持模糊查询以及多列索引的最左前缀匹配。原理也是因为 hash 函数的不可预测。 hash 索引结构不会存放行数据，因此任何时候都避免不了回表查询数据，而B+树在符合某些条件(聚簇索引，覆盖索引等)的时候可以只通过索引完成查询。 Hash 索引查询效率高，通常(不存在hash冲突的情况)只需要一次检索就可以了，效率通常要高于 B+tree 索引。但性能不可预测，当某个键值存在大量重复的时候，发生hash碰撞，此时效率可能极差。而B+树的查询效率比较稳定，对于所有的查询都是从根节点到叶子节点，且树的高度较低。 因此在大多数情况下，直接选择 B+树索引可以获得稳定且较好的查询速度，而不需要使用hash索引。\n3.4.3. 存储引擎支持 在 MySQL 中，支持 hash 索引的是 Memory 存储引擎。而 InnoDB 中具有自适应 hash 功能，hash 索引是 InnoDB 存储引擎根据 B+Tree 索引在指定条件下自动构建的。\n3.5. B+树作用与索引总结 在块设备上，通过 B+树可以有效的存储数据； 所有记录都存储在叶子节点上，非叶子(non-leaf)存储索引(keys)信息；而且记录按照索引列的值由小到大排好了序。 B+树含有非常高的扇出（fanout），通常超过 100，在查找一个记录时，可以有效的减少 IO 操作； Tips:\n『扇出』是每个索引节点(Non-LeafPage)指向每个叶子节点(LeafPage)的指针； 扇出数 = 索引节点(Non-LeafPage)可存储的最大关键字个数 + 1 3.5.1. 为什么 InnoDB 存储引擎选择使用 B+tree 索引结构 相对于二叉树，B+tree 层级更少，搜索效率高。 由于 B+tree 的数据都存储在叶子结点中，叶子结点均为索引，只需要扫描一遍叶子结点即可；但是 BTree 因为其分支结点同样存储着数据，在查找具体的数据时，需要进行一次中序遍历按序来扫。所以 B+tree 更加适合在区间查询的情况，而在数据库中基于范围的查询是非常频繁的，所以通常 B+tree 更适用于数据库索引。 对于 BTree，无论是叶子节点还是非叶子节点，都会保存数据，这样导致一页中存储的键值减少，指针跟着减少，要同样保存大量数据，只能增加树的高度，导致性能降低；而 B+tree 的节点只存储索引 key 值，具体信息的地址存在于叶子节点的地址中。这就使以页为单位的索引中可以存放更多的节点，减少更多的I/O支出。 B+tree 的查询效率更加稳定，任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同，让每一个数据的查询效率相当。 相对 Hash 索引，B+tree 支持范围匹配及排序操作 4. 索引分类（从应用维度划分） 普通索引 唯一索引 主键索引，默认自动创建，一张表只能存在一个。 复合（组合、联合）索引 前缀索引 全文索引 空间索引 MySQL 8.x 中实现的索引新特性：\n隐藏索引：也称为不可见索引，不会被优化器使用，但是仍然需要维护，通常会软删除和灰度发布的场景中使用。主键不能设置为隐藏（包括显式设置或隐式设置）。 降序索引：之前的版本就支持通过 desc 来指定索引为降序，但实际上创建的仍然是常规的升序索引。直到 MySQL 8.x 版本才开始真正支持降序索引。另外，在 MySQL 8.x 版本中，不再对 GROUP BY 语句进行隐式排序。 函数索引：从 MySQL 8.0.13 版本开始支持在索引中使用函数或者表达式的值，也就是在索引中可以包含函数或者表达式。 4.1. 普通索引 MySQL 中基本索引类型，没有什么限制，一张表允许创建多个普通索引，并且允许在定义索引的列中插入重复值和空值，唯一作用就是为了快速查询数据。\n4.2. 唯一索引 唯一索引与普通索引类似，不同点是：索引列的值必须唯一，但允许有空值（Null）。\n唯一索引也是一种约束。唯一索引的索引列不能出现重复的数据，但是允许数据为空值（NULL），一张表允许创建多个唯一索引。如果是组合唯一索引，则列值的组合必须唯一。\n建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性，而不是为了查询效率。\n4.3. 主键索引 在 MySQL 的 InnoDB 的表中，每张表都会有主键。在创建表时，如果没有显示的指定表的主键时，InnoDB 会自动先检查表中是否有唯一索引且不允许存在 null 值的字段，如果有，则选择该字段为默认的主键，否则 InnoDB 将会自动创建一个 6Byte 的自增主键。而 MySQL 会自动在主键列上建立一个索引，这就是主键索引。\n主键是具有唯一性并且不允许为 NULL，所以主键索引是一种特殊的唯一索引。\n4.3.1. 复合主键/联合主键 复合主键（联合主键）是指表的主键含有一个以上的字段的组合，不使用无业务含义的自增id作为主键。复合（联合）主键的意义：用多个字段来确定一条记录，但这些字段都不是唯一的，可以分别重复。但多个字段联合的主键是唯一的。\n举个例子，在表中创建了一个ID字段，自动增长，并设为主键，这个是没有问题的，因为“主键是唯一的索引”，ID自动增长保证了唯一性，所以可以。此时，再创建一个字段name，类型为varchar，也设置为主键，就会发现，在表的多行中是可以填写相同的name值的\n总结：当表中只有一个主键时，它是唯一的索引；当表中有多个主键时，称为复合主键，复合主键联合保证唯一索引。某几个主键字段值可以分别出现重复，只要不是有多条记录的所有主键值完全一样，就不算重复。\n4.3.2. 主键索引与唯一索引的区别 主键索引是不允许为 Null 值并且唯一。 唯一约束的列可以在允许为 null 值（因为 MySQL 定义所有的 Null 均不是同一个值），唯一索引主要是用来防止数据重复插入。 4.4. 组合（复合、联合）索引 组合索引也叫复合索引，指的是在建立索引的时候使用多个字段（例如同时使用身份证和手机号建立索引），同样的可以建立为普通索引或者是唯一索引。\nTips: 使用组合索引时需遵循最左前缀原则。任何标准表最多可以创建 16 个索引列。（待验证是否正确）\n4.5. 前缀索引 前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引，因为只取前几个字符，所以相比普通索引建立的数据更小，。\n4.6. 全文索引 4.6.1. 概述 全文索引的关键字是 fulltext。全文索引主要用来查找文本中的关键字，而不是直接与索引中的值相比较，它更像是一个搜索引擎，基于相似度的查询，而不是简单的where语句的参数匹配。\n用 like + % 就可以实现模糊匹配了，为什么还要全文索引？like + % 在文本比较少时是合适的，但是对于大量的文本数据检索，是不可想象的。全文索引在大量的数据面前，能比 like + % 快 N 倍，速度不是一个数量级，但是全文索引可能存在精度问题。\n4.6.2. 注意事项 MySQL 5.6 以前的版本，只有 MyISAM 存储引擎支持全文索引 MySQL 5.6 及以后的版本，MyISAM 和 InnoDB 存储引擎均支持全文索引 只有字段的数据类型为 char、varchar、text 及其系列才可以建全文索引 在数据量较大时候，现将数据放入一个没有全局索引的表中，然后再用 create index 创建 fulltext 索引，要比先为一张表建立 fulltext 然后再将数据写入的速度快很多； 4.6.3. 最小搜索长度和最大搜索长度 MySQL 中的全文索引，有两个变量，最小搜索长度和最大搜索长度，对于长度小于最小搜索长度和大于最大搜索长度的词语，都不会被索引。通俗点就是说，想对一个词语使用全文索引搜索，那么这个词语的长度必须在以上两个变量的区间内。这两个的默认值可以使用以下命令查看:\n1 show variables like \u0026#39;%ft%\u0026#39;; 部分参数解析\n参数名称 默认值 最小值 最大值 作用 ft_min_word_len 4 1 3600 MyISAM 引擎表全文索引包含的最小词长度 ft_query_expansion_limit 20 0 1000 MyISAM引擎表使用 with query expansion 进行全文搜索的最大匹配数 innodb_ft_min_token_size 3 0 16 InnoDB 引擎表全文索引包含的最小词长度 innodb_ft_max_token_size 84 10 84 InnoDB 引擎表全文索引包含的最大词长度 4.6.4. 全文索引创建语法 语法格式：\n1 2 3 4 5 6 7 8 9 10 11 -- 创建表的时候添加全文索引 create table 表名 ( ... fulltext (字段名) -- 创建全文检索 ); -- 修改表结构添加全文索引 alter table 表名 add fulltext index_content(字段名); -- 直接添加全文索引 create fulltext index index_content on 表名(字段名); 示例：\n1 2 3 4 5 6 7 8 9 10 11 create table t_article ( id int primary key auto_increment , title varchar(255) , content varchar(1000) , writing_date date, fulltext (content) -- 创建全文检索 ); alter table t_article add fulltext index_content(content); create fulltext index index_content on t_article(content); 4.6.5. 使用全文索引 使用全文索引和常用的模糊匹配使用 like + % 不同，全文索引有自己的语法格式，使用 match 和 against 关键字，格式:\n1 match (col1,col2,...) against(expr [search_modifier]) 示例：\n1 2 select * from t_article where match(content) against(\u0026#39;yo\u0026#39;); -- 没有结果 单词数需要大于等于3 select * from t_article where match(content) against(\u0026#39;you\u0026#39;); -- 有结果 4.7. 空间索引(少用，了解) MySQL 在 5.7 之后的版本支持了空间索引，而且支持 OpenGIS 几何数据模型。空间索引是对空间数据类型的字段建立的索引，MYSQL 中的空间数据类型有4种，分别是 GEOMETRY、POINT、LINESTRING、POLYGON。\n类型 含义 说明 Geometry 空间数据 任何一种空间类型 Point 点 坐标值 LineString 线 有一系列点连接而成 Polygon 多边形 由多条线组成 MYSQL 使用 SPATIAL 关键字进行扩展，使得能够用于创建正规索引类型的语法创建空间索引。\n语法：\n1 2 3 4 create table 表名 ( ... spatial key geom_index(列名) ); Notes: 创建空间索引的列，必须将其声明为 NOT NULL。\n5. InnoDB 中的索引（按照底层存储方式划分） MySQL 的 InnoDB 引擎中的索引也是按照 B+树来组织的。最终会在磁盘中保存为 .ibd 格式的文件，里面包含了该表的索引和数据。以物理存储维度主要分为两种：聚集索引和二级索引（辅助索引、非聚簇索引）。\n5.1. 聚集索引/聚簇索引（Clustered Index） 5.1.1. 聚簇索引介绍 聚簇索引（Clustered Index）是以主键创建的索引，在叶子节点存储的是表中的数据。即索引结构和数据一起存放的索引，并不是一种单独的索引类型。如果表没有定义主键，MySQL 会选择一个不允许为 NULL 的唯一性索引建立聚集索引。如果也没有合适的唯一性索引，MySQL 也会创建一个隐含列 RowID 来做主键（此隐藏的主键长度为6个字节，值会随着数据的插入自增），然后用这个主键来建立聚集索引。\nInnoDB 中的主键索引就属于聚簇索引。MySQL 会将表的主键用来构造一棵 B+树，每个非叶子节点存储索引（主键），并且将整张表的行记录数据存放在该 B+树的叶子节点（叶子节点就是数据页，数据页上存放的是完整的每行记录）中。\n聚簇索引特点是：索引即数据，数据即索引。由于聚集索引是利用表的主键构建的，因此每张表只能拥有一个聚集索引。\n5.1.2. 聚簇索引的优缺点 优点：\n查询速度快：因为整个 B+树本身就是一颗多叉平衡树，叶子节点也都是有序的，定位到索引的节点，就相当于定位到了数据。因为聚集索引能获取完整的整行数据，相比于非聚簇索引，聚簇索引少了一次读取数据的 IO 操作。 对排序查找和范围查找优化：对于主键的排序查找和范围查找速度非常快。因为聚集索引叶子节点的存储是逻辑上连续的，使用双向链表连接，叶子节点按照主键的顺序排序。 缺点：\n依赖于有序的数据：因为 B+树是多路平衡树，如果索引的数据不是有序的，那么就需要在插入时排序，如果数据是整型还好，否则类似于字符串或 UUID 这种又长又难比较的数据，插入或查找的速度会比较慢。 更新代价大：如果对索引列的数据被修改时，那么对应的索引也将会被修改，而且聚簇索引的叶子节点还存放着数据，修改代价肯定是较大的，所以对于主键索引来说，主键一般都是不可被修改的。 5.2. 非聚簇索引 5.2.1. 非聚簇索引简介 非聚集索引就是以非主键创建的索引，在叶子节点存储的是主键和索引列。\n5.2.2. 非聚簇索引的优缺点 优点：\n更新代价比聚簇索引要小：非聚簇索引的更新代价就没有聚簇索引那么大了，非聚簇索引的叶子节点是不存放数据的 缺点：\n依赖于有序的数据：跟聚簇索引一样，非聚簇索引也依赖于有序的数据 可能会二次查询(回表)：这应该是非聚簇索引最大的缺点了。当查到索引对应的指针或主键后，可能还需要根据指针或主键再到数据文件或表中查询。 5.2.3. 聚集索引和非聚集索引的结构对比 聚集索引的叶子节点下挂的是这一行的数据 。 二级索引的叶子节点下挂的是该字段值对应的主键值。 5.2.4. 回表 当通过辅助索引来寻找数据时，InnoDB 存储引擎会遍历辅助索引并通过叶级别的指针获得指向主键索引的主键，然后再通过主键索引（聚集索引）来找到一个完整的行记录。这个过程也被称为回表。\n根据辅助索引的值查询一条完整的用户记录需要使用到 2 棵 B+树 -\u0026gt; 一次辅助索引，一次聚集索引。\n辅助索引叶子节点不会像聚集索引一样，不会将完整的行记录放到叶子节点中。虽然如果将完整行记录放到叶子节点是可以不用回表，但相当于每建立一棵 B+树都需要把所有的记录再都拷贝一份，太过占用存储空间。而且每次对数据的变化要在所有包含数据的索引中全部都修改一次，性能也非常低下。\n回表查询会增加额外的磁盘访问开销和数据传输开销，降低查询的性能。回表的记录越少，性能提升就越高，需要回表的记录越多，使用二级索引的性能就越低，甚至让某些查询宁愿使用全表扫描也不使用二级索引。\n至于采取何种扫描方式是由查询优化器决定。查询优化器会事先对表中的记录计算一些统计数据，然后再利用这些统计数据根据查询的条件来计算一下需要回表的记录数，需要回表的记录数越多，就越倾向于使用全表扫描，反之倾向于使用二级索引+回表的方式。\n下面分析一下，执行如下的SQL语句时，具体的查找的过程：\n由于是根据 name 字段进行查询，所以先根据 name='Arm' 到 name 字段的二级索引中进行匹配查找。但是在二级索引中只能查找到 Arm 对应的主键值 10。 由于查询返回的是整行的数据，因此还需要根据主键值10，到聚集索引中查找10对应的记录，最终找到10对应的行row。 最终拿到这一行的数据，直接返回即可。 5.2.5. 辅助索引/二级索引（Secondary Index） 聚簇索引只能在搜索条件是主键值时才能发挥作用，因为B+树中的数据都是按照主键进行排序的。如果以别的列作为搜索条件时，通常会给这些列建立索引，这些索引被称为辅助索引/二级索引。唯一索引，普通索引，前缀索引等索引均属于二级索引。\n辅助索引(Secondary Index，也称二级索引、非聚集索引)，叶子节点并不包含行记录的全部数据。叶子节点除了包含键值以外，每个叶子节点中的索引行中还包含了一个书签(bookmark)。该书签用来告诉 InnoDB 存储引擎哪里可以找到与索引相对应的行数据。因此 InnoDB 存储引擎的辅助索引的书签就是相应行数据的聚集索引键。\n辅助索引的存在并不影响数据在聚集索引中的组织，因此每张表上可以有多个辅助索引。\n5.2.6. 联合索引/复合索引 构建索引可以包含多个字段，将表上的多个列组合起来进行索引，称之为联合索引或者复合索引。\n值得注意的是：联合索引只会建立1棵B+树；而对多个列分别建立索引（辅助索引）则会分别以每个列则建立B+树，有几个列就有几个B+树。\n联合索引的结构是按定义时的列的顺序，逐个排序建立索引。如index(note,b)在索引构建如下：\n先把各个记录按照 note 列进行排序 在记录的 note 列相同的情况下，采用 b 列进行排序 最终叶子节点存储聚集索引的主键列 5.3. 覆盖索引/索引覆盖 InnoDB 存储引擎支持覆盖索引(covering index，或称索引覆盖)，即从辅助索引中字段包含了需要查询的字段，那可以直接得到查询的记录，而不需要回表查询聚集索引中的记录。使用覆盖索引的一个好处是辅助索引不包含整行记录的所有信息，故其大小要远小于聚集索引，因此可以减少大量的 IO 操作。\n注意：覆盖索引并不是索引类型的一种。\n不是所有类型的索引都可以成为覆盖索引。覆盖索引要存储索引列的值，而哈希索引、全文索引不存储索引列的值，所以 MySQL 使用 b+树索引做覆盖索引。\n5.4. 自适应哈希索引 B+树的查找次数，取决于 B+树的高度，在生产环境中，B+树的高度一般为 34 层，故需要 34 次的 IO 查询。\nInnoDB 存储引擎内部自己去监控索引表，如果监控到某个索引经常用，那么就认为是热数据，然后内部自己创建一个 hash 索引，称之为自适应哈希索引( Adaptive Hash Index,AHI)，创建以后，如果下次又查询到这个索引，那么直接通过 hash 算法推导出记录的地址，直接一次就能查到数据，比重复去B+tree 索引中查询三四次节点的效率高了不少。\nInnoDB 存储引擎使用的哈希函数采用除法散列方式，其冲突机制采用链表方式。注意，对于自适应哈希索引仅是数据库自身创建并使用的，用户并不能对其进行干预。查询当前自适应哈希索引的使用状况的命令如下：\n1 show engine innodb status\\G; 哈希索引只能用来搜索等值的查询，如SELECT* FROM table WHERE indexcol=xxx。而对于其他查找类型，如范围查找，是不能使用哈希索引。\n由于 AHI 是由 InnoDB 存储引擎控制的，因此这里的信息只供我们参考。不过我们可以通过观察 SHOW ENGINE INNODB STATUS 的结果及参数 innodb_adaptive_hash_index 来考虑是禁用或启动此特性，默认 AHI 为开启状态。\n什么时候需要禁用呢？如果发现监视索引查找和维护哈希索引结构的额外开销远远超过了自适应哈希索引带来的性能提升就需要关闭这个功能。\n同时在 MySQL 5.7 中，自适应哈希索引搜索系统被分区。每个索引都绑定到一个特定的分区，每个分区都由一个单独的 latch 锁保护。分区由innodb_adaptive_hash_index_parts 配置选项控制 。在早期版本中，自适应哈希索引搜索系统受到单个 latch 锁的保护，这可能成为繁重工作负载下的争用点。innodb_adaptive_hash_index_parts 默认情况下，该 选项设置为 8。最大设置为 512。禁用或启动此特性和调整分区个数是 DBA 的工作，我们了解即可。\n5.5. 全文检索之倒排索引 全文检索（Full-Text Search）：是将存储于数据库中的整本书或整篇文章中的任意内容信息查找出来的技术。它可以根据需要获得全文中有关章、节、段、句、词等信息，也可以进行各种统计和分析。比较熟知的如 Elasticsearch、Solr 等就是全文检索引擎，底层都是基于 Apache Lucene 的。\n倒排索引就是，将文档中包含的关键字全部提取处理，然后再将关键字和文档之间的对应关系保存起来，最后再对关键字本身做索引排序。用户在检索某一个关键字是，先对关键字的索引进行查找，再通过关键字与文档的对应关系找到所在文档。\n注：具体如何使用 InnoDB 存储引擎的全文检索，查阅相关官方文档或者书籍\n5.6. InnoDB 索引的限制 参考官方文档：https://dev.mysql.com/doc/refman/8.0/en/innodb-limits.html\n一个表最多可包含 1017 列。虚拟生成的列也包含在此限制内。 一个表最多可包含 64 个二级索引。 索引键前缀长度限制为 3072 字节。尝试使用超过限制的索引键前缀长度会返回错误。 对于使用 REDUNDANT 或 COMPACT 行格式的 InnoDB 表，索引键前缀长度限制为 767 字节。假设使用 utf8mb4 字符集，每个字符最多 4 字节，那么 TEXT 或 VARCHAR 列的列前缀索引长度超过 191 个字符时，就可能会达到此限制。 如果在创建 MySQL 实例时通过指定 innodb_page_size 选项将 InnoDB 页面大小减小到 8KB 或 4KB，那么索引键的最大长度会根据 16KB 页面大小的 3072 字节限制按比例降低。也就是说，当页面大小为 8KB 时，索引键的最大长度为 1536 字节；当页面大小为 4KB 时，索引键的最大长度为 768 字节。 多列索引最多允许 16 列。超过限制将返回错误。 1 ERROR 1070 (42000): Too many key parts specified; max 16 parts allowed 6. 索引在查询中使用的原理 一个索引对应一个B+树，索引让查询可以快速定位和扫描到需要的数据记录上，加快查询的速度。 一个select查询语句在执行过程中一般最多能使用一个二级索引，即使在where条件中用了多个二级索引。 6.1. 扫描区间 对于某个查询来说，最简单粗暴的执行方案就是扫描表中的所有记录，判断每一条记录是否符合搜索条件。如果符合，就将其发送到客户端，否则就跳过该记录。这就是全表扫描。\n对于使用 InnoDB 存储引擎的表来说，全表扫描意味着从聚簇索引第一个叶子节点的第一条记录开始，沿着记录所在的单向链表向后扫描，直到最后一个叶子节点的最后一条记录。虽然全表扫描是一种很笨的执行方案，但却是一种万能的执行方案，所有的查询都可以使用这种方案来执行，只是效率不高。\n有了索引，利用 B+树查找索引列值等于某个值的记录，这样可以明显减少需要扫描的记录数量。由于 B+树叶子节点中的记录是按照索引列值由小到大的顺序排序的，所以即使只扫描某个区间或者某些区间中的记录也可以明显减少需要扫描的记录数量。\n1 SELECT * FROM order_exp WHERE id \u0026gt;= 3 AND id\u0026lt;= 99; 以上语句是查找 id 值在[3,99]区间中的所有聚簇索引记录。以通过聚簇索引对应的 B+树快速地定位到 id 值为 3 的那条聚簇索引记录，然后沿着记录所在的单向链表向后扫描,直到某条聚簇索引记录的 id 值不在[3,99]区间中为止。\n与全表扫描相比，扫描 id 值在[3,99]区间中的记录已经很大程度地减少了需要扫描的记录数量，所以提升了查询效率。其实所谓的全表扫描，可以理解为扫描的区间是[负无穷，正无穷]或者[第一条记录，最后一条记录]。\n1 SELECT * FROM order_exp WHERE order_no \u0026lt; \u0026#39;DD00_10S\u0026#39; AND expire_time \u0026gt; \u0026#39;2021-03-22 18:28:28\u0026#39; AND order_note \u0026gt; \u0026#39;7 排\u0026#39;; 上述语句中，order_no 和 expire_time 都有索引，order_note 没有索引。这个不会出现两个扫描区间。一个select查询语句在执行过程中一般最多能使用一个二级索引。\n无论用哪个索引执行查询，都需要获取到索引中的记录后，进行回表，获取到完整的用户记录后再根据判定条件判断这条记录是否满足 SQL 语句的要求。\n6.2. 范围区间扫描 对于B+树索引来说，只要索引列和常数使用=、\u0026lt;=\u0026gt;、IN、NOT IN、IS NULL、IS NOT NULL、\u0026gt;、\u0026lt;、\u0026gt;=、\u0026lt;=、BETWEEN、!=（不等于也可以写成\u0026lt;\u0026gt;）或者 LIKE 操作符连接起来，就可以产生一个区间。\nIN 操作符的效果和若干个等值匹配操作符=之间用OR连接起来是一样的，也就是说会产生多个单点区间，比如下边这两个语句的效果是一样的： 1 2 SELECT * FROM order_exp WHERE insert_time IN (2021-03-22 18:23:42, yyyy); SELECT * FROM order_exp WHERE insert_time= 2021-03-22 18:23:42 OR insert_time = yyyy; != 一般会产生的两个扫描区间，即[第一条记录, 不等于表达式的值]和[不等于表达式的值, 最后一条记录] 1 SELECT * FROM order_exp WHERE order_no != \u0026#39;DD00_9S\u0026#39;; LIKE 操作符比较特殊，只有在匹配完整的字符串或者匹配字符串前缀时才产生合适的扫描区间。 对于某个索引列来说，字符串前缀相同的记录在由记录组成的单向链表中肯定是相邻的。比如有一个搜索条件是 note LIKE 'b%'，对于二级索引 idx_note 来说，所有字符串前缀为'b'的二级索引记录肯定是相邻的。这也就意味着只要定位到 idx_note 值的字符串前缀为'b'的第一条记录，就可以沿着记录所在的单向链表向后扫描，直到某条二级索引记录的字符串前缀不为'b'为止。\n6.3. 所有搜索条件都可以使用某个索引的情况 每个搜索条件都可以使用到某个索引\n1 2 3 4 -- AND 的方式连接 SELECT * FROM order_exp WHERE order_no \u0026gt; \u0026#39;DD00_6S\u0026#39; AND order_no \u0026gt; \u0026#39;DD00_9S\u0026#39;; -- OR 的方式连接 SELECT * FROM order_exp WHERE order_no \u0026gt; \u0026#39;DD00_6S\u0026#39; OR order_no \u0026gt; \u0026#39;DD00_9S\u0026#39;; AND连接搜索条件时，取交集；OR连接搜索条件时，取并集；\n6.4. 有的搜索条件无法使用索引的情况 1 SELECT * FROM order_exp WHERE expire_time\u0026gt; \u0026#39;2021-03-22 18:35:09\u0026#39; AND order_note = \u0026#39;abc\u0026#39;; 上面示例这个查询语句中能利用的索引只有 idx_expire_time 一个，而 idx_expire_time 这个二级索引的记录中又不包含 order_note 这个字段，所以在使用二级索引 idx_expire_time 定位记录的阶段用不到 order_note = 'abc'这个条件，而范围区间是为了到索引中取记录中提出的概念，所以在确定范围区间的时候不需要考虑 order_note = 'abc'这个条件。即可以将语句简化成：\n1 SELECT * FROM order_exp WHERE expire_time \u0026gt; \u0026#39;2021-03-22 18:35:09\u0026#39;; 如果使用or连接搜索条件：\n1 SELECT * FROM order_exp WHERE expire_time \u0026gt; \u0026#39;2021-03-22 18:35:09\u0026#39; OR order_note = \u0026#39;abc\u0026#39;; 一个使用到索引的搜索条件和没有使用该索引的搜索条件使用OR连接起来后是无法使用该索引的。因为索引expire_time列不包含非索引的order_note字段，所以无法判断非索引的order_note字段是否满足搜索条件，又因为是OR条件连接，所以必须要在主键索引中从第一条记录到最后一条记录逐条判定非索引的order_note字段是否满足条件。即可以将语句简化成：\n1 SELECT * FROM order_exp ; 6.5. 复杂搜索条件下找出范围匹配的区间 当搜索条件可能特别复杂时。比如：\n1 2 3 4 SELECT * FROM order_exp WHERE (order_no \u0026gt; \u0026#39;DD00_9S\u0026#39; AND expire_time = \u0026#39;2021-03-22 18:35:09\u0026#39; ) OR (order_no \u0026lt; \u0026#39;DD00_6S\u0026#39; AND order_no \u0026gt; \u0026#39;DD00_9S\u0026#39;) OR (order_no LIKE \u0026#39;%0S\u0026#39; AND order_no \u0026gt; \u0026#39;DD00_12S\u0026#39; AND (expire_time \u0026lt; \u0026#39;2021-03-22 18:28:28\u0026#39; OR order_note = \u0026#39;abc\u0026#39;)) ; 首先查看 WHERE 子句中的搜索条件都涉及到了哪些列，哪些列可能使用到索引。这个查询的搜索条件涉及到了 order_no、expire_time、order_note 这 3 个列，然后 order_no 列有二级索引 idx_order_no，expire_time 列有二级索引idx_expire_time。对于那些可能用到的索引，分析它们的范围区间。\n使用 idx_order_no 执行查询 分析时可以先那些用不到该索引的搜索条件暂时移除掉。把所有用不到的搜索条件视为True来进行中间替换，\n1 2 3 4 5 6 7 8 9 10 11 12 -- 使用true代替无使用索引的搜索条件 (order_no \u0026gt; \u0026#39;DD00_9S\u0026#39; AND TRUE ) OR (order_no \u0026lt; \u0026#39;DD00_6S\u0026#39; AND order_no \u0026gt; \u0026#39;DD00_9S\u0026#39;) OR (TRUE AND order_no \u0026gt; \u0026#39;DD00_12S\u0026#39; AND (TRUE OR TRUE)) -- 再次化简 (order_no \u0026gt; \u0026#39;DD00_9S\u0026#39;) OR (order_no \u0026lt; \u0026#39;DD00_6S\u0026#39; AND order_no \u0026gt; \u0026#39;DD00_9S\u0026#39;) OR (order_no \u0026gt; \u0026#39;DD00_12S\u0026#39;) -- 替换掉永远为 TRUE 或 FALSE 的条件 (order_no \u0026gt; \u0026#39;DD00_9S\u0026#39;) OR (order_no \u0026gt; \u0026#39;DD00_12S\u0026#39;) 最后搜索条件是使用OR操作符连接起来的，意味着要取并集，所以最终的结果化简的到的区间就是：order_no \u0026gt; 'DD00_12S'。也就是说：上边那个复杂搜索条件的查询语句如果使用 idx_order_no 索引执行查询的话，需要把满足order_no \u0026gt; 'DD00_12S'的二级索引记录都取出来，然后拿着这些记录的 id 再进行回表，得到完整的用户记录之后再使用其他的搜索条件进行过滤。记住，说的是如果使用 idx_order_no 索引执行查询，不代表 MySQL 一定会使用，因为MySQL 需要做整体评估，才能确定是否使用这个索引还是别的索引，或者是干脆全表扫描。\n使用 idx_expire_time 执行查询 把那些用不到该索引的搜索条件暂时使用 TRUE 条件替换掉，其中有关 order_no 和 order_note 的搜索条件都需要被替换掉，替换结果就是：\n1 2 3 4 5 6 7 8 9 (TRUE AND expire_time = \u0026#39;2021-03-22 18:35:09\u0026#39; ) OR (TRUE AND TRUE) OR (TRUE AND TRUE AND (expire_time \u0026lt; \u0026#39;2021-03-22 18:28:28\u0026#39; OR TRUE)) -- 替换掉永远为 TRUE 或 FALSE 的条件 expire_time = \u0026#39;2021-03-22 18:35:09\u0026#39; OR TRUE -- 最终结果 TRUE 这个结果也就意味着如果我们要使用 idx_expire_time 索引执行查询语句的话，需要扫描 idx_expire_time 二级索引的所有记录，然后再回表，这种情况下为啥 MySQL 不直接全表扫描呢？所以一定不会使用 idx_expire_time 索引的。\n6.6. 使用联合索引执行查询时对应的扫描区间 联合索引的索引列包含多个列，B+树每一层页面以及每个页面中的记录采用的排序规则较为复杂，以 order_exp 表的 u_idx_day_status 联合索引为例，它采用的排序规则如下所示：\n先按照 insert_time 列的值进行排序 在 insert_time 列的值相同的情况下，再按照 order_status 列的值进行排序 在 insert_time 和 order_status 列的值都相同的情况下，再按照 expire_time 列的值进行排序 以下用几个查询示例来说明：\n1 SELECT * FROM order_exp WHERE insert_time = \u0026#39;2021-03-22 18:34:55\u0026#39;; 由于二级索引记录是先按照 insert_time 列的值进行排序的，所以所有符合insert_time = '2021-03-22 18:34:55'条件的记录肯定是相邻的，可以定位到第一条符合insert_time = '2021-03-22 18:34:55'条件的记录，然后沿着记录所在的单向链表向后扫描，直到某条记录不符合insert_time = '2021-03-22 18:34:55'条件为止(当然，对于获取到的每一条二级索引记录都要执行回表操作)。\n扫描区间就是['2021-03-22 18:34:55', '2021-03-22 18:34:55']，形成这个扫描区间的条件就是 insert_time = '2021-03-22 18:34:55'。\n1 SELECT * FROM order_exp WHERE insert_time = \u0026#39;2021-03-22 18:34:55\u0026#39; AND order_status = 0; 由于二级索引记录是先按照 insert_time 列的值进行排序的；在 insert_time 列的值相等的情况下，再按照 order_status 列进行排序。所以符合 insert_time = '2021-03-22 18:34:55' AND order_status = 0 条件的二级索引记录肯定是相邻的，我们可以定位到第一条符合 insert_time='2021-03-22 18:34:55' AND order_status=0 条件的记录，然后沿着记录所在的链表向后扫描，直到某条记录不符合insert_time='2021-03-22 18:34:55'条件或者order_status=0条件为止。\n形成扫描区间[('2021-03-22 18:34:55', 0), ('2021-03-22 18:34:55', 0)]，形成这个扫描区间的条件就是 insert_time = '2021-03-22 18:34:55' AND order_status = 0。\n1 SELECT * FROM order_exp WHERE insert_time = \u0026#39;2021-03-22 18:34:55\u0026#39; AND order_status = 0 AND expire_time = \u0026#39;2021-03-22 18:35:13\u0026#39;; 由于二级索引记录是先按照 insert_time 列的值进行排序的；在 insert_time 列的值相等的情况下，再按照 order_status 列进行排序；在 insert_time 和order_status 列的值都相等的情况下，再按照 expire_time 列进行排序。所以符合insert_time = \u0026lsquo;2021-03-22 18:34:55\u0026rsquo; AND order_status = 0 AND expire_time = \u0026lsquo;2021-03-22 18:35:13\u0026rsquo;条件的二级索引记录肯定是相邻的，我们可以定位到第一条符合 insert_time=\u0026lsquo;2021-03-22 18:34:55\u0026rsquo; AND order_status=0 AND expire_time=\u0026lsquo;2021-03-22 18:35:13\u0026rsquo;条件的记录，然后沿着记录所在的链表向后扫描，直到某条记录不符合 insert_time=\u0026lsquo;2021-03-22 18:34:55\u0026rsquo;条件或者order_status=0 条件或者 expire_time=\u0026lsquo;2021-03-22 18:35:13\u0026rsquo;条件为止。\n如果使用 u_idx_day_status 索引执行查询时，可以形成扫描区间[('2021-03-22 18:34:55', 0, '2021-03-22 18:35:13'), ('2021-03-22 18:34:55', 0, '2021-03-22 18:35:13')]，形成这个扫描区间的条件就是insert_time = '2021-03-22 18:34:55' AND order_status = 0 AND expire_time = '2021-03-22 18:35:13'。\n1 SELECT * FROM order_exp WHERE insert_time \u0026lt; \u0026#39;2021-03-22 18:34:55\u0026#39;; 由于二级索引记录是先按照 insert_time 列的值进行排序的，所以所有符合insert_time \u0026lt; '2021-03-22 18:34:55'条件的记录肯定是相邻的，我们可以定位到第一条符合insert_time \u0026lt; '2021-03-22 18:34:55'条件的记录(其实就是u_idx_day_status 索引第一个叶子节点的第一条记录)，然后沿着记录所在的链表向前扫描，直到某条记录不符合 insert_time \u0026lt; '2021-03-22 18:34:55'为止。\n使用 u_idx_day_status 索引执行查询时，形成扫描区间(第一条记录, '2021-03-22 18:34:55')，形成这个扫描区间的条件就是insert_time \u0026lt; '2021-03-22 18:34:55'。\n1 SELECT * FROM order_exp WHERE insert_time = \u0026#39;2021-03-22 18:34:55\u0026#39; AND order_status \u0026gt;= 0; 由于二级索引记录是先按照 insert_time 列的值进行排序的；在 insert_time 列的值相等的情况下，再按照 order_status 列进行排序。也就是说在符合 insert_time = \u0026lsquo;2021-03-22 18:34:55\u0026rsquo;条件的二级索引记录中，是按照 order_status 列的值进行排序的，那么此时符合 insert_time = '2021-03-22 18:34:55' AND order_status \u0026gt;= 0;条件的二级索引记录肯定是相邻的。我们可以定位到第一条符合 insert_time = '2021-03-22 18:34:55' AND order_status \u0026gt;= 0;条件的记录，然后沿着记录所在的链表向后扫描，直到某条记录不符合 insert_time='2021-03-22 18:34:55'条件或者order_status \u0026gt;= 0条件为止。\n使用 u_idx_day_status 索引执行查询时，形成扫描区间，条件就是insert_time = '2021-03-22 18:34:55' AND order_status \u0026gt;= 0;\n1 SELECT * FROM order_exp WHERE order_status = 1; 由于二级索引记录不是直接按照 order_status 列的值排序的，所以符合order_status = 1的二级索引记录可能并不相邻，也就意味着不能通过这个order_status = 1搜索条件来减少需要扫描的记录数量。在这种情况下，是不会使用 u_idx_day_status 索引执行查询的。\n1 SELECT * FROM order_exp WHERE insert_time = \u0026#39;2021-03-22 18:34:55\u0026#39; AND expire_time = \u0026#39;2021-03-22 18:35:12\u0026#39;; 由于二级索引记录是先按照 insert_time 列的值进行排序的，所以符合insert_time = '2021-03-22 18:34:55'条件的二级索引记录肯定是相邻的，但是对于符合 insert_time = '2021-03-22 18:34:55'条件的二级索引记录来说，并不是直接按照 expire_time 列进行排序的，也就是说我们不能根据搜索条件expire_time = '2021-03-22 18:35:12'来进一步减少需要扫描的记录数量。那么如果使用u_idx_day_status索引执行查询的话，可以定位到第一条符合insert_time='2021-03-22 18:34:55'条件的记录，然后沿着记录所在的单向链表向后扫描，直到某条记录不符合insert_time = '2021-03-22 18:34:55'条件为止。\n使用 u_idx_day_status 索引执行查询时，对应的扫描区间其实是['2021-03-22 18:34:55', '2021-03-22 18:34:55']，形成该扫描区间的搜索条件是insert_time = '2021-03-22 18:34:55'，与expire_time = '2021-03-22 18:35:12'无关。\n1 SELECT * FROM order_exp WHERE insert_time \u0026lt; \u0026#39;2021-03-22 18:34:57\u0026#39; AND order_status = 1; 由于二级索引记录是先按照 insert_time 列的值进行排序的，所以符合insert_time \u0026lt; '2021-03-22 18:34:57'条件的二级索引记录肯定是相邻的，但是对于符合insert_time \u0026lt; '2021-03-22 18:34:57'条件的二级索引记录来说，并不是直接按照order_status列进行排序的，也就是说我们不能根据搜索条件order_status = 0来进一步减少需要扫描的记录数量。\n使用 u_idx_day_status 索引执行查询的话，可以定位到第一条符合 insert_time 的记录，其实就是u_idx_day_status索引第一个叶子节点的第一条记录，所以在使用u_idx_day_status索引执行查询的过程中，对应的扫描区间其实是[第一条记录,'2021-03-22 18:34:57')。\n7. MyISAM 中的索引（了解） 7.1. 简介 MyISAM 存储引擎中的索引方案，虽然也使用树形结构，但是却将索引和数据分开存储的。\nMyISAM 将表中的记录按照记录的插入顺序单独存储在一个文件中，称之为数据文件。这个文件并不划分为若干个数据页，有多少记录就往这个文件中存多少条记录。可以通过行号而快速访问到一条记录。\n由于在插入数据的时候并没有刻意按照主键大小排序，所以并不能在这些数据上使用二分法进行查找。\n使用 MyISAM 存储引擎的表会把索引信息另外存储到一个称为索引文件的另一个文件中。MyISAM 会单独为表的主键创建一个索引，只不过在索引的叶子节点中存储的不是完整的用户记录，而是主键值+行号的组合。也就是先通过索引找到对应的行号，再通过行号去找对应的记录。这一点和 InnoDB 是完全不相同的，在 InnoDB 存储引擎中，只需要根据主键值对聚簇索引进行一次查找就能找到对应的记录，而在 MyISAM 中却需要进行一次回表操作，意味着 MyISAM 中建立的索引相当于全部都是二级索引。\n如有需要，也可以对其它的列分别建立索引或者建立联合索引，原理和 InnoDB 中的索引差不多，不过在叶子节点处存储的是相应的列+行号。这些索引也全部都是二级索引。\n7.2. MyISAM 索引与 InnoDB 索引的区别 InnoDB 索引是聚簇索引；MyISAM 索引是非聚簇索引。 InnoDB 的主键索引的叶子节点存储着行数据，因此主键索引非常高效；MyISAM 索引的叶子节点存储的是行数据地址，需要再寻址一次才能得到数据。即在 InnoDB 存储引擎中，根据主键值对聚簇索引进行一次查找就能找到对应的记录，而在 MyISAM 则需要进行一次回表操作，意味着 MyISAM 中建立的索引相当于全部都是二级索引（非聚簇索引）。 InnoDB 的非聚簇索引的叶子节点存储的是主键和其他带索引的列数据；而 MyISAM 索引记录的是地址 。换句话说，InnoDB 的所有非聚簇索引都引用主键作为 data 域，因此查询时做到覆盖索引会非常高效。 InnoDB 的数据文件本身就是索引文件；而 MyISAM 索引文件和数据文件是分离的 ，索引文件仅保存数据记录的地址。 MyISAM 的表在磁盘上存储的文件中：*.sdi（描述表结构）、*.MYD（数据），*.MYI（索引） InnoDB 的表在磁盘上存储的文件中：.ibd（表结构、索引和数据都存在一起） MyISAM 的回表操作十分快速，因为是通过地址偏移量直接到文件中取数据的；而 InnoDB 是通过获取主键之后再去聚簇索引里找记录，速度比不上直接用地址去访问。 InnoDB 要求表必须有主键。如果没有显式指定，则 MySQL 系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列，则 MySQL 自动为 InnoDB 表生成一个隐含字段作为主键，这个字段长度为6个字节，类型为长整型；而 MyISAM 可以没有主键。 ","permalink":"https://ktzxy.top/posts/60zrj4638z/","summary":"MySQL 索引","title":"MySQL 索引"},{"content":"Kubeadm和二进制方式对比 Kubeadm方式搭建K8S集群 安装虚拟机，在虚拟机安装Linux操作系统【3台虚拟机】\n对操作系统初始化操作\n所有节点安装Docker、kubeadm、kubelet、kubectl【包含master和slave节点】\n安装docker、使用yum，不指定版本默认安装最新的docker版本 修改docker仓库地址，yum源地址，改为阿里云地址 安装kubeadm，kubelet 和 kubectl k8s已经发布最新的1.19版本，可以指定版本安装，不指定安装最新版本 yum install -y kubelet kubeadm kubectl 在master节点执行初始化命令操作\nkubeadm init 默认拉取镜像地址 K8s.gcr.io国内地址，需要使用国内地址 安装网络插件(CNI)\nkubectl apply -f kube-flannel.yml 在所有的node节点上，使用join命令，把node添加到master节点上\n测试kubernetes集群\n二进制方式搭建K8S集群 安装虚拟机和操作系统，对操作系统进行初始化操作 生成cfssl 自签证书 ca-key.pem、ca.pem server-key.pem、server.pem 部署Etcd集群 部署的本质，就是把etcd集群交给 systemd 管理 把生成的证书复制过来，启动，设置开机启动 为apiserver自签证书，生成过程和etcd类似 部署master组件，主要包含以下组件 apiserver controller-manager scheduler 交给systemd管理，并设置开机启动 如果要安装最新的1.19版本，下载二进制文件进行安装 部署node组件 docker kubelet kube-proxy【需要批准kubelet证书申请加入集群】 交给systemd管理组件- 组件启动，设置开机启动 批准kubelet证书申请 并加入集群 部署CNI网络插件 测试Kubernets集群【安装nginx测试】 ","permalink":"https://ktzxy.top/posts/f6fxremm4l/","summary":"5 Kubeadm和二进制方式对比","title":"5 Kubeadm和二进制方式对比"},{"content":"Kubernetes容器交付介绍 如何在k8s集群中部署Java项目\n容器交付流程 开发代码阶段 编写代码 编写Dockerfile【打镜像做准备】 持续交付/集成 代码编译打包 制作镜像 上传镜像仓库 应用部署 环境准备 Pod Service Ingress 运维 监控 故障排查 应用升级 k8s部署Java项目流程 制作镜像【Dockerfile】 上传到镜像仓库【Dockerhub、阿里云、网易】 控制器部署镜像【Deployment】 对外暴露应用【Service、Ingress】 运维【监控、升级】 k8s部署Java项目 准备Java项目 第一步，准备java项目，把java进行打包【jar包或者war包】\n依赖环境 在打包java项目的时候，我们首先需要两个环境\njava环境【JDK】 maven环境 然后把java项目打包成jar包\n1 mvn clean install 编写Dockerfile文件 Dockerfile 内容如下所示\n1 2 3 4 FROM openjdk:8-jdk-alpine VOLUME /tmp ADD ./target/demojenkins.jar demojenkins.jar ENTRYPOINT [\u0026#34;java\u0026#34;,\u0026#34;-jar\u0026#34;,\u0026#34;/demojenkins.jar\u0026#34;, \u0026#34;\u0026amp;\u0026#34;] 制作镜像 在我们创建好Dockerfile文件后，我们就可以制作镜像了\n我们首先将我们的项目，放到我们的服务器上\n然后执行下面命令打包镜像\n1 docker build -t java-demo-01:latest . 等待一段后，即可制作完成我们的镜像\n最后通过下面命令，即可查看我们的镜像了\n1 docker images; 启动镜像 在我们制作完成镜像后，我们就可以启动我们的镜像了\n1 docker run -d -p 8111:8111 java-demo-01:latest -t 启动完成后，我们通过浏览器进行访问，即可看到我们的java程序\n1 http://192.168.177.130:8111/user 推送镜像 下面我们需要将我们制作好的镜像，上传到镜像服务器中【阿里云、DockerHub】\n首先我们需要到 阿里云 容器镜像服务，然后开始创建镜像仓库\n然后选择本地仓库\n我们点击我们刚刚创建的镜像仓库，就能看到以下的信息\n登录镜像服务器 使用命令登录\n1 docker login --username=XXXXXXX@163.com registry.cn-shenzhen.aliyuncs.com 然后输入刚刚我们开放时候的注册的密码\n镜像添加版本号 下面为我们的镜像添加版本号\n1 2 3 4 5 # 实例 docker tag [ImageId] registry.cn-shenzhen.aliyuncs.com/mogublog/java-project-01:[镜像版本号] # 举例 docker tag 33f11349c27d registry.cn-shenzhen.aliyuncs.com/mogublog/java-project-01:1.0.0 操作完成后\n推送镜像 在我们添加版本号信息后，我们就可以推送我们的镜像到阿里云了\n1 docker push registry.cn-shenzhen.aliyuncs.com/mogublog/java-project-01:1.0.0 操作完成后，我们在我们的阿里云镜像服务，就能看到推送上来的镜像了\n控制器部署镜像 在我们推送镜像到服务器后，就可以通过控制器部署镜像了\n首先我们需要根据刚刚的镜像，导出yaml\n1 2 3 # 导出yaml kubectl create deployment javademo1 --image=registry.cn- shenzhen.aliyuncs.com/mogublog/java-project-01:1.0.0 --dry-run -o yaml \u0026gt; javademo1.yaml 导出后的 javademo1.yaml 如下所示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 apiVersion: apps/v1 kind: Deployment metadata: creationTimestamp: null labels: app: javademo1 name: javademo1 spec: replicas: 1 selector: matchLabels: app: javademo1 strategy: {} template: metadata: creationTimestamp: null labels: app: javademo1 spec: containers: - image: registry.cn-shenzhen.aliyuncs.com/mogublog/java-project-01:1.0.0 name: java-project-01 resources: {} status: {} 然后通过下面命令，通过yaml创建我们的deployment\n1 2 3 # 创建 kubectl apply -f javademo1.yaml # 查看 pods 或者我们可以进行扩容，多创建几个副本\n1 kubectl scale deployment javademo1 --replicas=3 然后我们还需要对外暴露端口【通过service 或者 Ingress】\n1 2 3 4 # 对外暴露端口 kubectl expose deployment javademo1 --port=8111 --target-port=8111 --type=NodePort # 查看对外端口号 kubectl get svc 然后通过下面的地址访问\n1 2 3 4 # 对内访问 curl http://10.106.103.242:8111/user # 对外访问 http://192.168.177.130:32190/user 运维 \u0026hellip;.\n","permalink":"https://ktzxy.top/posts/bnvd9fajoz/","summary":"19 Kubernetes容器交付介绍","title":"19 Kubernetes容器交付介绍"},{"content":"Kubernetes控制器Controller详解 Statefulset Statefulset主要是用来部署有状态应用\n对于StatefulSet中的Pod，每个Pod挂载自己独立的存储，如果一个Pod出现故障，从其他节点启动一个同样名字的Pod，要挂载上原来Pod的存储继续以它的状态提供服务。\n无状态应用 我们原来使用 deployment，部署的都是无状态的应用，那什么是无状态应用？\n认为Pod都是一样的 没有顺序要求 不考虑应用在哪个node上运行 能够进行随意伸缩和扩展 有状态应用 上述的因素都需要考虑到\n让每个Pod独立的 让每个Pod独立的，保持Pod启动顺序和唯一性 唯一的网络标识符，持久存储 有序，比如mysql中的主从 适合StatefulSet的业务包括数据库服务MySQL 和 PostgreSQL，集群化管理服务Zookeeper、etcd等有状态服务\nStatefulSet的另一种典型应用场景是作为一种比普通容器更稳定可靠的模拟虚拟机的机制。传统的虚拟机正是一种有状态的宠物，运维人员需要不断地维护它，容器刚开始流行时，我们用容器来模拟虚拟机使用，所有状态都保存在容器里，而这已被证明是非常不安全、不可靠的。\n使用StatefulSet，Pod仍然可以通过漂移到不同节点提供高可用，而存储也可以通过外挂的存储来提供 高可靠性，StatefulSet做的只是将确定的Pod与确定的存储关联起来保证状态的连续性。\n部署有状态应用 无头service， ClusterIp：none\n这里就需要使用 StatefulSet部署有状态应用\n然后通过查看pod，能否发现每个pod都有唯一的名称\n然后我们在查看service，发现是无头的service\n这里有状态的约定，肯定不是简简单单通过名称来进行约定，而是更加复杂的操作\ndeployment：是有身份的，有唯一标识 statefulset：根据主机名 + 按照一定规则生成域名 每个pod有唯一的主机名，并且有唯一的域名\n格式：主机名称.service名称.名称空间.svc.cluster.local 举例：nginx-statefulset-0.default.svc.cluster.local DaemonSet DaemonSet 即后台支撑型服务，主要是用来部署守护进程\n长期伺服型和批处理型的核心在业务应用，可能有些节点运行多个同类业务的Pod，有些节点上又没有这类的Pod运行；而后台支撑型服务的核心关注点在K8S集群中的节点(物理机或虚拟机)，要保证每个节点上都有一个此类Pod运行。节点可能是所有集群节点，也可能是通过 nodeSelector选定的一些特定节点。典型的后台支撑型服务包括：存储、日志和监控等。在每个节点上支撑K8S集群运行的服务。\n守护进程在我们每个节点上，运行的是同一个pod，新加入的节点也同样运行在同一个pod里面\n例子：在每个node节点安装数据采集工具 这里是不是一个FileBeat镜像，主要是为了做日志采集工作\n进入某个 Pod里面，进入\n1 kubectl exec -it ds-test-cbk6v bash 通过该命令后，我们就能看到我们内部收集的日志信息了\nJob和CronJob 一次性任务 和 定时任务\n一次性任务：一次性执行完就结束 定时任务：周期性执行 Job是K8S中用来控制批处理型任务的API对象。批处理业务与长期伺服业务的主要区别就是批处理业务的运行有头有尾，而长期伺服业务在用户不停止的情况下永远运行。Job管理的Pod根据用户的设置把任务成功完成就自动退出了。成功完成的标志根据不同的 spec.completions 策略而不同：单Pod型任务有一个Pod成功就标志完成；定数成功行任务保证有N个任务全部成功；工作队列性任务根据应用确定的全局成功而标志成功。\nJob Job也即一次性任务\n使用下面命令，能够看到目前已经存在的Job\n1 kubectl get jobs 在计算完成后，通过命令查看，能够发现该任务已经完成\n我们可以通过查看日志，查看到一次性任务的结果\n1 kubectl logs pi-qpqff CronJob 定时任务，cronjob.yaml如下所示\n这里面的命令就是每个一段时间，这里是通过 cron 表达式配置的，通过 schedule字段\n然后下面命令就是每个一段时间输出\n我们首先用上述的配置文件，创建一个定时任务\n1 kubectl apply -f cronjob.yaml 创建完成后，我们就可以通过下面命令查看定时任务\n1 kubectl get cronjobs 我们可以通过日志进行查看\n1 kubectl logs hello-1599100140-wkn79 然后每次执行，就会多出一个 pod\n删除svc 和 statefulset 使用下面命令，可以删除我们添加的svc 和 statefulset\n1 2 3 kubectl delete svc web kubectl delete statefulset --all Replication Controller Replication Controller 简称 RC，是K8S中的复制控制器。RC是K8S集群中最早的保证Pod高可用的API对象。通过监控运行中的Pod来保证集群中运行指定数目的Pod副本。指定的数目可以是多个也可以是1个；少于指定数目，RC就会启动新的Pod副本；多于指定数目，RC就会杀死多余的Pod副本。\n即使在指定数目为1的情况下，通过RC运行Pod也比直接运行Pod更明智，因为RC也可以发挥它高可用的能力，保证永远有一个Pod在运行。RC是K8S中较早期的技术概念，只适用于长期伺服型的业务类型，比如控制Pod提供高可用的Web服务。\nReplica Set Replica Set 检查 RS，也就是副本集。RS是新一代的RC，提供同样高可用能力，区别主要在于RS后来居上，能够支持更多种类的匹配模式。副本集对象一般不单独使用，而是作为Deployment的理想状态参数来使用\n","permalink":"https://ktzxy.top/posts/2uqd5az8zj/","summary":"11 Kubernetes控制器Controller详解","title":"11 Kubernetes控制器Controller详解"},{"content":"概述 Linux是一种自由和开放源码的类UNIX操作系统。该操作系统的内核由林纳斯·托瓦兹在1991年10月5日首次发布在加上用户空间的应用程序之后，成为Linux操作系统。\n安装与下载 VMware虚拟机下载安装Linux(CentOS7)系统_虚拟机linux系统下载_\n目录结构 Linux 的一切资源都挂载在 / 节点下。\n/bin： Binary的缩写。存放系统命令，普通用户和 root 都可以执行。放在 /bin 下的命令在单用户模式下也可以执行。\n/boot： 这里存放的是启动 Linux 时使用的一些核心文件，包括一些连接文件以及镜像文件。\n/dev： Device的缩写。该目录下存放的是 Linux 的外部设备，在 Linux 中访问设备的方式和访问文件的方式是相同的。\n/etc： Etcetera的缩写。这个目录用来存放所有的系统管理所需要的配置文件和子目录。\n/home： 用户的主目录，在 Linux 中，每个用户都有一个自己的目录，一般该目录名是以用户的账号命名的。\n/lib： Library的缩写。这个目录里存放着系统最基本的动态连接共享库，其作用类似于 Windows 里的 DLL 文件。几乎所有的应用程序都需要用到这些共享库。\n/lib64： 64位相关的库会放在这。\n/media： linux 系统会自动识别一些设备，例如U盘、光驱等等，当识别后，Linux 会把识别的设备挂载到这个目录下。\n/mnt： 系统提供该目录是为了让用户临时挂载别的文件系统的，我们可以将光驱挂载在 /mnt/ 上，然后进入该目录就可以查看光驱里的内容了。\n/opt： optional的缩写。这是给主机额外安装软件所摆放的目录。\n/proc： Processes的缩写。/proc 是一种伪文件系统（也即虚拟文件系统），存储的是当前内核运行状态的一系列特殊文件，这个目录是一个虚拟的目录，它是系统内存的映射，我们可以通过直接访问这个目录来获取系统信息。这个目录的内容不在硬盘上而是在内存里\n/root： 该目录为系统管理员，也称作超级权限者的用户主目录。\n/run： 运行目录\n/sbin： s 就是 Super User 的意思，是 Superuser Binaries (超级用户的二进制文件) 的缩写，这里存放的是系统管理员使用的系统管理程序。\n/srv： 该目录存放一些服务启动之后需要提取的数据。\n/sys： 虚拟文件系统。和 /proc/ 目录相似，该目录中的数据都保存在内存中，主要保存与内核相关的信息\n/tmp： temporary的缩写这个目录是用来存放一些临时文件的。\n/usr： unix system resources缩写。用于存储系统软件资源。\n/var： 用于存储动态数据，例如缓存、日志文件、软件运行过程中产生的文件等\n常用命令 Linux命令大全(手册) – 真正好用的Linux命令在线查询网站 (linuxcool.com)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 # 查看cpu信息 lscpu # 查看内存大小 free # 查看硬盘和分区情况 lsblk # 查看内核版本 uname -r # 查看操作系统发行版本 cat /etc/redhat-release cat /etc/os-release # 显示日历 cal –y # 关机 halt poweroff # 重启 reboot # 用户登录信息查看命令 whoami: 显示当前登录有效用户 who: 系统当前所有的登录会话 w: 系统当前所有的登录会话及所做的操作 # 显示当前工作目录 pwd # 列出当前目录的内容或指定目录 ls 常用选项 -a 包含隐藏文件 -l 显示额外的信息 -R 目录递归 -ld 目录和符号链接信息 -1 文件分行显示 -S 按从大到小排序 -t 按mtime排序 -u 配合-t选项，显示并按atime从新到旧排序 -U 按目录存放顺序显示 -X 按文件后缀排序 路径 绝对路径 以正斜杠/ 即根目录开始 完整的文件的位置路径 可用于任何想指定一个文件名的时候 相对路径名 不以斜线开始 一般情况下，是指相对于当前工作目录的路径，特殊场景下，是相对于某目录的位置 可以作为一个简短的形式指定一个文件名 学习网址 http://www.yunweipai.com/\n文件管理 创建文件 1 touch 文件名 创建目录 1 mkdir 路径和目录名 复制 1 cp 源文件路径 目标文件夹 移动 1 mv 源文件路径 目标文件路径 删除 1 rm -rf 文件或目录的路径 查看 1 2 3 4 5 6 7 cat全部 more翻页 head头部 tail尾部 grep过滤关键字 语法：grep 关键字 文件名 # grep \u0026#39;abc\u0026#39; /root/file1.txt 修改 图像文件编辑器 gedit\n文本编辑器 vi vim\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 # 光标定位 hjkL //上下左右 0 $ //行首行尾 gg G //页首页尾 3G 进入第三行 /string (n N 可以循环的) //查找字符，按n键选下一个（重要） #文本编辑 文本编辑 yy 复制 dd 删除 p 粘贴 u undo撤销 # 进入其他模式 进入其它模式 a 进入插入模式 i 进入插入模式 o 进入插入模式 A 进入插入模式 : 进入末行模式（扩展命令模式） v 进入可视模式 ESC 返回命令模式 # 扩展命令模式 保存退出 :w 保存 :q 退出 :wq 保存并退出 查找替换 :范围 s/原内容/新内容/全局 :1,5 s/root/qianfeng/g 从1－5行的root 替换为qianfeng 另存为 :w file9.txt 另存为 file9.txt :set nu 设置行号 :set nonu 取消设置行号 :set list 显示控制字符 文件类型 1 2 3 4 5 6 7 - 普通文件（文本文件，二进制文件，压缩文件，电影，图片。。。） d 目录文件（蓝色） b 设备文件（块设备）存储设备硬盘，U盘 /dev/sda, /dev/sda1 c 设备文件（字符设备）打印机，终端 /dev/tty1 l 链接文件（淡蓝色） s 套接字文件 p 管道文件 用户与用户组 1 2 3 4 5 6 7 8 9 10 11 12 [root@localhost ~]# head -1 /etc/passwd root:x:0:0:root:/root:/bin/bash 用户名：x：uid：gid：描述：HOME：shell 系统约定：RHEL7 uid：0 特权用户 uid：1~499 系统用户 uid：1000+ 普通用户 用户基本信息文件\t/etc/passwd 用户密码信息文件\t/etc/shadow 组信息文件\t/etc/group 用户管理 1、创建用户 useradd [选项] 用户名\n1 2 3 4 5 6 7 8 9 10 选项： -d\t指定用户登录时的目录 -e\t指定用户的有效期限 -f\t缓冲天数，密码过期时在指定天数后关闭该账号 -g\t指定用户所属组 -G\t指定用户的附加组 -m\t自动创建用户的登录目录 -r\t创建系统账号 -s\t指定用户的登录shell -u\t指定用户的用户ID 2、查看用户的属性信息cat /etc/passwd\n3、设置用户密码 passwd [选项] 用户名\n修改密码也使用同样的语法格式\n1 2 3 4 5 选项： -l\t锁定密码，锁定后密码失效，无法登录 -d\t删除密码，仅系统管理员可用 -S\t列出密码的相关信息，仅管理员可使用 -f\t强制执行 查看用户的密码信息（已加密）cat /etc/shadow\n4、删除用户 userdel [选项] 用户名\n1 2 -f\t强制删除，即使是当前用户 -r\t删除用户的同时，删除与用户相关的所有文件 5、修改用户信息 usermod [选项] 参数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 usermod命令用与修改用户属性信息，包括用户ID、主目录、用户组、账号有效信息 使用usermod命令时，必须先明确该用户没有在电脑上执行任何程序 -d\t修改用户的的登录目录 -e\t修改账号的有效期限 -f\t修改缓冲天数 -g\t修改指定用户所属组 -G\t修改指定用户的附加组 -l\t修改用户账号名称 -L\t锁定密码，锁定后密码失效，无法登录 -U\t解除密码锁定 -s\t修改指定用户的登录shell -u\t修改指定用户的用户ID 用法： # usermod -u 678 user1 用户组管理 1、新增用户 groupadd [选项] 参数\n1 2 3 4 5 6 -g\t指定新建用户组的组ID -r\t创建系统用户组，组ID的取值范围为1~499 -o\t允许创建组ID已存在的用户 用法： # groupadd -g 550 group1 Linux系统将用户组信息储存在/etc/group文件中，新用户组创建成功后，该文件将多一条与该用户组相关的记录。\n2、删除用户组 groupdel 组名\n3、修改用户组属性 groupmod [选项] 参数\n1 2 3 4 5 6 7 8 选项： -g\t为用户组指定新的组ID -n\t修改用户组的组名 -o\t允许用户创建组ID已存在的用户组 用法： # groupmod -n \u0026lt;旧组名\u0026gt; \u0026lt;新组名\u0026gt; # groupmod -o \u0026lt;组名\u0026gt; -g \u0026lt;已经存在的组ID\u0026gt; 4、用户组切换 newgrp 用户组\n1 用户组分为基本组和附加组，将用户添加附加组后，用户可拥有对应组的权限。用户的基本组唯一，但附加组可以不唯一。用户可从附加组中移除，但不能从基本组中移除。 5、用户组管理\ngpasswd命令用于管理用户组 gpasswd [选项] 参数\n1 2 3 4 5 6 7 -a\t添加用户到用户组 -d\t从用户组中删除用户 -r\t删除密码 -R\t限制用户登入组，只有组中的成员才可以用newgrp加入用户组 用法： # gpasswd -a user1 group1\t将用户user1添加到group1 用户切换 1、使用su命令切换用户，可以任意用户切换 su [选项] 用户名\n若选项和用户名缺省（默认），则表示切换到root用户，但此时保留原来用户的工作环境，若使用“su -”，则表示从当前用户切换到root用户，并切换到root用户的工作目录。\n1 2 3 4 5 选项： -c\t执行完指定的指令后，切换到原来的用户 -l\t切换用户的同时，切换到对应用户的工作目录，环境变量也会随之改变。 -m,-p\t切换用户，不改变环境目录 -s\t指定要执行的shell 2、sudo命令的格式sudo [选项] [参数]\nsudo可使当前用户以其他身份来执行命令，若不指定用户名，则默认以root身份执行。在使用sudo命令时，用户需要输入自己的密码，密码验证在之后的5分钟内有效，若超过则需要重新验证。\n使用sudo命令之前，需要先在etc目录下的sudoers文件中对可执行sudo指令的用户进行设置。sudoers文件内容须遵循语法规范，visudo命令可防止其他用户同时修改sudoers文件。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # visudo ## Allow root to run any commands anywhere root ALL=(ALL) ALL # 第二条语句是对root用户权限设置，作用：使root用户能够在任何情景下执行任何命令， 格式： 账户名\t主机名称=(可切换的身份)\t可用的命令 说明： 账户名：只有账户名被写入sudoers文件时，该用户才能使用sudo命令 主机名称：该参数决定此条语句中账户名对应的用户可以从哪些网络主机连接当前Linux主机，root用户默认可以\t来自任何一台网络主机。 可用的命令：该参数指数此条语句中的用户可以执行哪些命令。注意，命令的路径为绝对路径。 以上的语句ALL分别代表任何主机，任何身份和任何命令。以用户user1为例，若要使用户user1能以root用户执行/bin/more命令，则应在sudoers文件中添加如下内容。 user1\tALL=(root)\t/bin/more 保存退出后，切换到用户user1，使用命令`sudo -l`可查看该用户可以使用的命令。 当需要操作的用户较多时，如此操作显然相对麻烦，Linux系统支持为用户组内的整组用户统一设置权限。\n1 2 3 4 5 6 7 8 ## Allows people in group wheel to run all commands %wheel ALL=(ALL) ALL \u0026#34;%\u0026#34;声明之后的字符串是一个用户组，该语句表示任何加入用户组wheel的用户，都能通过任意主机连接、任何身份执行全部命令。若想提升某些用户的权限为ALL，将它们添加到用户组wheel即可。 例子 %group1 ALL=(root)\t/bin/more 禁用 %group1 ALL=(root)\t!/bin/more 用户的权限 基本权限UGO 权限 对应字符 文件 目录 读 r 4 可查看文件内容 可以列出目录中的内容 写 w 2 可修改文件内容 可以在目录中创建、删除文件 执行 x 1 可执行该文件 可以进入目录 常用的权限管理命令有chmod、chown、chgrp等，默认情况下，普通用户不能使用权限。\n设置权限：\n1、chmod更改权限，其功能为变更文件或目录权限（使用字符，使用数字） 语法：\n1 2 3 4 5 chmod 对象(u/g/o)赋值符(+/-/=)权限类型(rwx) 文件/目录 选项: -f\t不显示错误信息 -v\t显示指令执行过程 -R\t递归处理，处理指定目录及其中所有的文件与子目录 2、chown其功能为更改文件或目录的所有者。默认情况下文件的所有者为创建该文件的用户，或在文件被创建时通过命令指定用户，需要时可使用chown对文件的所有者进行修改。\n命令格式 chown [选项] [用户] [文件或目录]\n1 2 3 4 5 6 7 8 选项: -f\t不显示错误信息 -v\t显示指令执行过程 -R\t递归处理，处理指定目录及其中所有的文件与子目录 chown:设置文件属于谁，属组 语法：\tchown\t用户名.组名\t文件 chown\tuser01.hr\t/tmp/file1 3、chgrp用于更改文件或目录的所属组，一般情况下，文件或目录与创建该文件的用户属于同一组，或在被创建时通过选项指定所属组，但在需要时，可通过chgrp命令更改文件的所属组。\n1 # chgrp 组名 文件或目录 小结：chmod、chown、chgrp都可以对目录进行改权、改主、改组，但底下文件权限不会变，若要改变，使用-R递归操作\n基本权限ACL access contral list 限制用户对文件的访问 ACL是UGO的补充，或者说是加强版 命令：setfacl -m g:hr:rwx /home/file1\n设置文件 -设置 对象：对象名：权限\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 [root@localhost tmp]# useradd alice [root@localhost tmp]# useradd jack [root@localhost tmp]# setfacl -m u:alice:rw /home/test.txt [root@localhost tmp]# setfacl -m u:jack:- /home/test.txt [root@localhost tmp]# getfacl /home/test.txt getfacl: Removing leading \u0026#39;/\u0026#39; from absolute path names # file: home/test.txt # owner: root # group: root user::rw- user:alice:rw- user:jack:--- group::r-- mask::rw- other::r-- #删除alice对test.txt文件的权限 [root@localhost tmp]# setfacl -x u:alice /home/test.txt [root@localhost tmp]# getfacl /home/test.txt getfacl: Removing leading \u0026#39;/\u0026#39; from absolute path names # file: home/test.txt # owner: root # group: root user::rw- user:jack:--- group::r-- mask::r-- other::r-- 特殊权限（了解） 1、特殊位suid：高级权限类型（suid,(sgid)针对文件/程序时，具备临时获得属主权限） suid是针对文件所设置的一个特别的权限。 功能：使调用文件的用户临时具备属主的能力 普通用户不能查看root目录下的文件，但赋予/usr/bin/cat一个s权限后，普通用户可以cat，root文件\n1 2 3 4 5 6 7 [root@localhost ~]# cat /root/file1.txt cat: /root/file1.txt: 权限不够 [root@localhost ~]# chmod u+s /usr/bin/cat [root@localhost ~]# ll /usr/bin/cat -rwsr-xr-x. 1 root root 54080 8月 20 2019 /usr/bin/cat [root@localhost ~]# cat /root/file1.txt 123 2、文件属性chattr i:在文件上启用这个属性时，我们不能更改、重命名或者删除这文件 a:允许在文件中追加操作\n1 2 3 4 5 6 7 8 9 10 #chattr改变文件属性 [root@localhost ~]# chattr +i ./file1.txt #lsattr列出文件属性 [root@localhost ~]# lsattr ./file1.txt ----i----------- ./file1.txt #不允许修改、删除文件 [root@localhost ~]# rm -rf file1.txt rm: 无法删除\u0026#34;file1.txt\u0026#34;: 不允许的操作 #去除i属性 [root@localhost ~]# chattr -i ./file1.txt 3、进程掩码umask\n进程管理 进程存在于计算机内存中，计算机内存中可同时存在多个进程，每个CPU上同时只会执行一个进程，但计算机上似乎能够同时运行多个进程。实际上这是计算机采用了“多道程序设计”技术，即计算机允许多个相互独立的程序同时进入内存，在内核的管理控制下，相互之间穿插运行。\n进程状态 初始态、就绪态、运行态、睡眠态和终止态。\n进程管理 1、ps 在命令行输入ps后按回车键就能查看当前系统中正在运行的进程。\n1 2 3 4 5 6 选项： a\t显示当前终端机下的所有进程，包括其他用户启动的进程 u\t以用户的形式，显示系统中的进程 x\t忽略终端机，显示所有进程 e\t显示每个进程使用的环境变量 r\t只列出当前终端机中正在执行的进程 1 2 3 4 5 -a\t显示所有终端机中除阶段作业领导进程(拥有子进程的进程)之外的进程 -e\t显示所有进程 -f\t除默认项外，显示UUID、PPID、C、STIME项 -o\t指定显示哪些字段，字段名可以使用长格式，也可以使用“%字符”的短格式指定，多个字段名使用逗号分割 -l\t使用详细的格式显示进程信息 进程是已启动的可执行程序的运行实例，进程有一下组成部分： 1、一个文件： 2、被分配内存空间： 3、有权限控制； 4、程序代码的一个或多个副本（也叫执行线程）； 5、拥有状态\nps -ef 显示含义（查看进程的父子关系）\n列的含义\n字段名 说明 UID 该进程执行的用户id PID 进程id PPID 该进程的父进程id，如果一个程序的父级进程找不到，该程序的进程被称之为僵尸进程 C cpu的占用率，其形式是百分数 STIME 进行的启动时间 TTY 终端设备，发起该进程的设备识别符号，如果显示“?”则表示该进程不是由终端设备发起的 TIME 进程的执行时间 CMD 该进程的名称或者对应的路径 案例（100%使用的命令）在ps的结果中过滤出想要查看的进程状态\n1 2 3 #ps -ef |grep 进程名称 自定义显示内容 ps axo user,pid,ppid,%mem | head -3 ps aux命令展示的进程信息\nuser 用户 PID 进程的编号 %CPU 占用CPU时间的百分比 %MEM 占用内存空间的百分 VSZ RSS 虚拟内存和实际内存占用大小 TTY 终端类型 STAT 运行，睡眠，停止，退出，僵死 START 进程启动时间 TIME 占用cpu的时间 COMMAND 程序的路径和名称 2、top命令可以实时观察系统的整体运行情况，默认时间间隔为3s，即每3秒更新一次界面，是一个很实用的系统性能检测工具。\ntop [选项]\n1 2 top -d 1 //每1秒刷新 top -d 1 -p 进程号\t//查看指定进程的动态信息 3、pstree\n一个新进程由已存在的进程创建，创建新进程的进程与新创建的进程为父子进程，一个父进程可以创建多个子进程，由同一个进程创建的多个子进程又称为兄弟进程。\npstree命令可以以树状的形式显示系统中的进程，直接观察进程之间的派生关系\npstree [选项]\n1 2 3 4 5 6 选项： -a\t显示每个进程的完整命令 -c\t不使用精简标识法 -h\t列出树状图，特别标明当前正在执行的进程 -u\t显示用户名称 -n\t使用程序识别码排序 4、pgrep\n命令根据进程名从进程队列中查找进程，查找成功后默认显示进程的pid\n5、nice\n进程的优先级会影响进程执行的顺序，在linux系统中，可通过改变进程的nice值来更改进程的优先级\nnice [选项] [参数]\n如修改bash的优先级为5 nice -n 5 bash\n6、jobs\n使用jobs命令可以查看当前内存中的作业列表\n7、bg和fg\n使用快捷键Ctrl+Z也能将进程调入后台，但调入后台的进程会被暂时停止。若要将后台的命令调回前台继续执行，可以使用fg命令\nfg 作业号\n8、kill\nkill命令一般用于管理进程，它的工作原理是发送某个信号给指定进程，以改变进程的状态，命令格式\nkill 选项 [参数]\n1 2 kill 序号(一般为9) 进程ID killall 服务名 1 2 3 4 5 6 7 8 1) SIGHUP\t重新加载配置 2) SIGINT\t键盘中断Ctrl+C 3) SIGQUIT\t键盘退出Ctrl+\\，类似SIGINT 9) SIGKILL\t强制终止，无条件 15) SIGTERM\t终止（正常结束），缺省信号 18) SIGCONT\t继续 19) SIGSTOP\t暂停 20) SIGTSTP\t键盘暂停Ctrl+C 管道和重定向 重定向和管道符 FD,文件描述符，文件句柄进程使用文件描述符来管理打开的文件 作用：FD给文件一个描述符（软链接，数字范围0-255），进程调用文件时就不用使用长路径，直接使用文件描述符就可以调用文件进程了。 0号，标准输入（一个文件代表键盘，按键盘数字先进入文件中，然后通过0号FD进入程序中去），文件结果通过1号描述符输出在显示器文件，然后在显示器/终端显示出来，2号描述符，标准错误输出，输出在显示器文件，然后在显示器/终端显示出来。\n输出重定向 可分为两种：正确输出和错误输出\n正确输出：1\u0026gt; 等价于 \u0026gt; 1\u0026raquo; 等价于 \u0026raquo; 错误输出：2\u0026gt; 没有简写 2\u0026raquo; 没有简写 将正确输出和错误输出重定向到一个文件里\n输入重定向发送邮件 默认邮件发送的过程： 向系统其他用户发送邮件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@localhost tmp]# mail -s \u0026#39;ssss\u0026#39; xulei\t\u0026#39;ssss\u0026#39; 为标题 你好呀！hello,world\t内容 .\t按键 . 表示输入完成 EOT\t[root@localhost ~]# su - xulei\t查看mail消息 上一次登录：三 6月 23 13:23:16 CST 2021pts/0 上 [xulei@localhost ~]$ mail\t回车后按键1，再回车 #使用重定向快速创建邮件 [xulei@localhost ~]$ vim test01.txt [xulei@localhost ~]$ mail -s \u0026#39;标题\u0026#39; admin \u0026lt; test01.txt\t将内容以文本编辑发送 [xulei@localhost ~]$ su - admin 密码： 上一次登录：五 6月 25 17:46:20 CST 2021:0 上 [admin@localhost ~]$ mail 管道符 进程管道 1、管道命令可以将多条命令组合起来，一次性完成复杂的处理任务\n1 2 3 4 5 6 7 8 9 [root@localhost ~]# cat /etc/passwd | grep \u0026#39;root\u0026#39; root:x:0:0:root:/root:/bin/bash operator:x:11:0:operator:/root:/sbin/nologin [root@localhost ~]# cat /etc/passwd | grep \u0026#39;root\u0026#39; |tail -1 operator:x:11:0:operator:/root:/sbin/nologin [root@localhost tmp]# cat /etc/passwd |grep ntp ntp:x:38:38::/etc/ntp:/sbin/nologin [root@localhost tmp]# cat /etc/passwd |grep ntp |cut -d: -f3 #cut命令，-d：（以冒号作为分隔符）-f3（切割第三列） 38 tee管道 三通管道，既交给另一个程序处理，又保存一份副本。\n1 2 passwd内容放到file1.txt，并且显示第一行在终端（类似水管的三通管） [root@localhost tmp]# cat /etc/passwd |tee file1.txt |head -1 参数传递Xargs cp、rm一些特殊命令就是不服其他程序\n1 2 3 4 5 6 7 8 [root@localhost tmp]# touch file{1..5}\t创建1-5的文件 [root@localhost tmp]# cat files.txt\t以下是文本内容 /tmp/file1 /tmp/file2 [root@localhost tmp]# cat files.txt |rm -rvf\t将文本的内容传到rm执行，结果失败 [root@localhost tmp]# cat files.txt |xargs rm -rvf\t使用xargs参数，执行成功，v参数：显示信息 已删除\u0026#34;/tmp/file1\u0026#34; 已删除\u0026#34;/tmp/file2\u0026#34; 使用\u0026laquo;EOF EOF指令\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #写一个多行的代码文本 [root@localhost dir1]# vim js.sh 以下是脚本的内容 cat \u0026gt; /tmp/dir/tt.txt \u0026lt;\u0026lt;EOF\t\u0026lt;\u0026lt;EOF EOF用法（cat内容，将内容定向到tt.txt文中 11111111111--第1行\t22222222222--第2行\t具体内容 33333333333--第3行\tEOF [root@localhost dir1]# chmod +x js.sh [root@localhost dir1]# ./js.sh [root@localhost dir1]# cat tt.txt 11111111111--第1行 22222222222--第2行 33333333333--第3行 磁盘管理 存储管理 需要掌握的知识点： 1、磁盘长什么样？ 2、磁盘有哪些？ 3、什么是好磁盘，什么是次磁盘 4、把磁盘的软件操作（分区格式化） 5、写入输入数据到磁盘\n磁盘类型：\n​\t机械硬盘 由：盘片、马达、磁头、磁臂等组成的机器结构存储器\n​\t固态硬盘：由芯片和集成电路组成。尺寸：2.5英寸、3.5英寸\n转速：每分钟旋转的速度 5400转、7200转、15000转\n接口：早期使用IDE，现在使用SATA\n厂商：西部数据、希捷、三星、日立、金士顿\n磁盘命名方式 kernel对不同接口硬盘的命名方式：如RHEL7/CentOs\n（略）IDE（并口）\nSATA（串口）：/dev/sda sda是一个文件，s代表sata就是串口，d代表磁盘，a代表第一块\n磁盘分区方式 MBR：主引导记录（MBR，Master Boot Record）是 硬盘 支持最大磁盘容量是\u0026lt;2TB，设计时分配四个分区。如果超过4个分区，需放弃主分区，改为扩展分区和逻辑分区。\nGPT： 全局唯一标识分区表（GUID Partition Table，缩写：GPT）是一个实体磁盘的分区表的结构布局的标准。 支持最大磁盘容量是\u0026gt;2TB，分配128个分区\n使用MBR方式创建分区，可通过fdisk命令进行管理 fdisk [选项] [磁盘]\n1 2 3 4 选项： -l\t详细显示磁盘及其信息 -s\t显示磁盘分区容量（单位为block） -b\t设置扇区大小（扇区大小取值为512、1024、2048或4096，单位为MB）\t管理磁盘 详细解析 CentOS 7 磁盘挂载\n三部曲：分区（隔间）、格式化（放家具/打造柜子格）、挂载（加个门/目录，才能访问）\n1、创建和查看磁盘\n1 2 3 4 5 6 7 # 查看 # fdisk -l /dev/sdb\t# lsblk # ll /dev/sd* # fdisk /dev/sdb\t创建 # partprobe /dev/sdb\t刷新 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [root@localhost ~]# fdisk /dev/sdb Welcome to fdisk (util-linux 2.23.2). Changes will remain in memory only, until you decide to write them. Be careful before using the write command. Device does not contain a recognized partition table Building a new DOS disklabel with disk identifier 0x4dc36548. Command (m for help): n Partition type: p primary (0 primary, 0 extended, 4 free) e extended Select (default p): p\t//请选择主分区，或扩展分区 Partition number (1-4, default 1):\t//选择分区号 First sector (2048-2097151, default 2048):\t//选择磁盘开始的扇区 Using default value 2048 Last sector, +sectors or +size{K,M,G} (2048-2097151, default 2097151): +200M Partition 1 of type Linux and of size 200 MiB is set\t//选择磁盘分区结束的扇区，即分区大小，实际环境根据磁盘划分 Command (m for help): w\t//输入w保存分区信息，自动退出 The partition table has been altered! Calling ioctl() to re-read partition table. Syncing disks. 2、创建挂载信息\n1 2 3 4 [root@localhost ~]# mkfs.ext4 /dev/sdb1\t#使用ext4格式，格式化sdb1的磁盘 创建一个文件系统 [root@localhost ~]# mkdir /mnt/disk1\t#创建一个挂载目录 [root@localhost ~]# mount -t ext4 /dev/sdb1 /mnt/disk1\t#将磁盘挂载在目录上 [root@localhost ~]# df -hT\t#查看文件系统信息 补充：系统目录结构是统一的（根下面有/mnt目录，在mnt/disk1里写入数据，并不是使用sda的内存空间，而是使用sdb1的空间），实际的内存存储在硬盘上。磁盘一旦被挂载，数据写入到新挂载的磁盘，而不是sda的内存。\n3、MBR主引导记录是一个文件，共64个字节，每16个字节记录一个主分区，所以一块硬盘只有4个分区，如果想要超过4个分区，则需要一个主分区分出来成为扩展分区，扩展分区可以继续分为N个逻辑分区\n交换分区管理Swap 作用：“提升”内存的容量，防止OOM（out of memory） 实际生产：大于4GB而小于16GB，最小需要4GB交换空间 大于16GB而小于64GB，最小需要8GB交换空间 大于64GB而小于256GB，最小需要16GB交换空间\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 [root@localhost ~]# free -m\t查看挂载信息 total used free shared buff/cache available Mem: 972 428 201 10 342 391 Swap: 3271 0 3271 ---------------------------------------------------------------------------------- 增加交换分区内存空间（以磁盘sde为例） [root@localhost ~]# fdisk /dev/sde\t#加一个扩展分区为1号 e 在扩展分区上加一个100M的逻辑分区l 分区为2号 [root@localhost ~]# mkswap /dev/sde2 [root@localhost ~]# swapon /dev/sde2 [root@localhost ~]# free -m total used free shared buff/cache available Mem: 972 428 201 10 342 391 Swap: 3371 0 3371 由上可见，swap交换分区多了100M 补充：在挂载的磁盘上/mnt/disk1写入数据(创建1-5的文件夹，数据指向/dev/sdb1)，然后卸载磁盘/dev/sdb1,进入/mnt/disk1并没有1-5的文件夹(此时数据指向sda)，此时再创建6-10的文件夹，挂载/dev/sdb1后，/mnt/disk1并没有6-10的文件夹，除非将挂载点转到/mnt/disk2。\n逻辑卷LVM 目的：管理磁盘的一种方式，性质与基本磁盘无异 特点：随意扩张大小 PV：物理卷 VG：卷组 LV：逻辑卷\n1、pvcreat命令用于将磁盘分区初始化为物理卷，命令格式 pvcreate [选项] 参数\n1 2 3 4 5 选项： -f\t强制创建物理卷，不需要用户确认 -u\t指定设备的UUID（通用唯一识别码） -y\t所有的问题都用yes回答 -z\t是否使用前四个扇区（y/n） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 [root@localhost ~]# pvcreate /dev/sdd\t//将物理磁盘转换成物理卷-PV Physical volume \u0026#34;/dev/sdd\u0026#34; successfully created. [root@localhost ~]# vgcreate vg1 /dev/sdd\t//创建卷组VG Volume group \u0026#34;vg1\u0026#34; successfully created [root@localhost ~]# lvcreate -L 800M -n lv1 vg1\t//指定大小，创建逻辑卷 -L大小 -n卷名 vg1组名 Logical volume \u0026#34;lv1\u0026#34; created. [root@localhost ~]# mkfs.ext4 /dev/vg1/lv1\t//创建文件系统并挂载 /dev/卷组名/逻辑卷名 mke2fs 1.42.9 (28-Dec-2013) Filesystem label= OS type: Linux Block size=4096 (log=2) Fragment size=4096 (log=2) Stride=0 blocks, Stripe width=0 blocks 51296 inodes, 204800 blocks 10240 blocks (5.00%) reserved for the super user First data block=0 Maximum filesystem blocks=209715200 7 block groups 32768 blocks per group, 32768 fragments per group 7328 inodes per group Superblock backups stored on blocks: 32768, 98304, 163840 Allocating group tables: done Writing inode tables: done Creating journal (4096 blocks): done Writing superblocks and filesystem accounting information: done [root@localhost ~]# mkdir /mnt/lv1 [root@localhost ~]# mount /dev/vg1/lv1 /mnt/lv1 [root@localhost ~]# df -hT Filesystem Type Size Used Avail Use% Mounted on /dev/mapper/centos-root xfs 17G 1.3G 16G 8% / devtmpfs devtmpfs 478M 0 478M 0% /dev tmpfs tmpfs 489M 0 489M 0% /dev/shm tmpfs tmpfs 489M 6.8M 482M 2% /run tmpfs tmpfs 489M 0 489M 0% /sys/fs/cgroup /dev/sda1 xfs 1014M 125M 890M 13% /boot tmpfs tmpfs 98M 0 98M 0% /run/user/0 /dev/mapper/vg1-lv1 ext4 772M 1.6M 714M 1% /mnt/lv1 [root@localhost ~]# dd if=/dev/zero of=/mnt/lv1/1.txt bs=1M count=800\t//向目标分区写入大量数据，填满 dd: error writing ‘/mnt/lv1/1.txt’: No space left on device 754+0 records in 753+0 records out 790433792 bytes (790 MB) copied, 6.09358 s, 130 MB/s [root@localhost ~]# df -hT Filesystem Type Size Used Avail Use% Mounted on /dev/mapper/centos-root xfs 17G 1.3G 16G 8% / devtmpfs devtmpfs 478M 0 478M 0% /dev tmpfs tmpfs 489M 0 489M 0% /dev/shm tmpfs tmpfs 489M 6.8M 482M 2% /run tmpfs tmpfs 489M 0 489M 0% /sys/fs/cgroup /dev/sda1 xfs 1014M 125M 890M 13% /boot tmpfs tmpfs 98M 0 98M 0% /run/user/0 /dev/mapper/vg1-lv1 ext4 772M 756M 0 100% /mnt/lv1 [root@localhost ~]# pvcreate /dev/sde //创建PV。将PV增加到VG中 Physical volume \u0026#34;/dev/sde\u0026#34; successfully created. [root@localhost ~]# pvs PV VG Fmt Attr PSize PFree /dev/sda2 centos lvm2 a-- \u0026lt;19.00g 0 /dev/sdd vg1 lvm2 a-- 1020.00m 220.00m /dev/sde lvm2 --- 1.00g 1.00g [root@localhost ~]# vgextend vg1 /dev/sde\t//扩容VG Volume group \u0026#34;vg1\u0026#34; successfully extended [root@localhost ~]# pvs PV VG Fmt Attr PSize PFree /dev/sda2 centos lvm2 a-- \u0026lt;19.00g 0 /dev/sdd vg1 lvm2 a-- 1020.00m 220.00m /dev/sde vg1 lvm2 a-- 1020.00m 1020.00m [root@localhost ~]# vgs VG #PV #LV #SN Attr VSize VFree centos 1 2 0 wz--n- \u0026lt;19.00g 0 vg1 2 1 0 wz--n- 1.99g 1.21g [root@localhost ~]# lvextend -L +200M /dev/vg1/lv1\t//扩容LV Size of logical volume vg1/lv1 changed from 800.00 MiB (200 extents) to 1000.00 MiB (250 extents). Logical volume vg1/lv1 successfully resized. [root@localhost ~]# df -hT Filesystem Type Size Used Avail Use% Mounted on /dev/mapper/centos-root xfs 17G 1.3G 16G 8% / devtmpfs devtmpfs 478M 0 478M 0% /dev tmpfs tmpfs 489M 0 489M 0% /dev/shm tmpfs tmpfs 489M 6.8M 482M 2% /run tmpfs tmpfs 489M 0 489M 0% /sys/fs/cgroup /dev/sda1 xfs 1014M 125M 890M 13% /boot tmpfs tmpfs 98M 0 98M 0% /run/user/0 /dev/mapper/vg1-lv1 ext4 772M 756M 0 100% /mnt/lv1 [root@localhost ~]# resize2fs /dev/vg1/lv1 resize2fs 1.42.9 (28-Dec-2013) Filesystem at /dev/vg1/lv1 is mounted on /mnt/lv1; on-line resizing required old_desc_blocks = 1, new_desc_blocks = 1 The filesystem on /dev/vg1/lv1 is now 256000 blocks long. [root@localhost ~]# df -hT Filesystem Type Size Used Avail Use% Mounted on /dev/mapper/centos-root xfs 17G 1.3G 16G 8% / devtmpfs devtmpfs 478M 0 478M 0% /dev tmpfs tmpfs 489M 0 489M 0% /dev/shm tmpfs tmpfs 489M 6.8M 482M 2% /run tmpfs tmpfs 489M 0 489M 0% /sys/fs/cgroup /dev/sda1 xfs 1014M 125M 890M 13% /boot tmpfs tmpfs 98M 0 98M 0% /run/user/0 /dev/mapper/vg1-lv1 ext4 970M 756M 154M 84% /mnt/lv1 2、vgcreat命令用于将物理卷整合为卷组，语法格式 vgcreate [选项] 卷组名称 物理卷路径1 物理卷路径2 ……\n1 2 3 4 5 6 7 8 9 10 11 12 选项： -l\t设置卷组上允许创建的最大逻辑卷数 -p\t设置卷组上允许添加的最大物理卷数 -s\t设置卷组上的最小存储单元（PE） 用法： # 将物理卷/dev/sdc2、/dev/sdc4整合为卷组，并命名为vg1 vgcreate vg1 /dev/sdc2 /dev/sdc4 或者vgcreate vg1 /dev/sdc{2,4} # 将物理卷/dev/sdc2、/dev/sdc4整合为卷组，设置最小存储单位为8MB，并命名为vg2 vgcreate -s 8M vg2 /dev/sdc2 /dev/sdc4 3、lvcreate命令的功能是在已经存在的卷组中创建逻辑卷，命令格式\nlvcreate [选项] 卷组名/路径 物理卷路径\n1 -n\t逻辑卷名称 4、vgdisplay用于显示LVM卷组信息，命令格式 vgdisplay [选项] 卷组\n1 2 3 选项： -s\t使用短格式输出信息 -A\t仅显示活动卷组的属性 5、lvextend\n当逻辑卷的可用空间不足时，需要为其拓展存储空间。使用LVM机制管理磁盘时可通过lvextend命令动态地调整分区大小，命令格式\nlvextend [选项] 逻辑卷\n1 2 3 选项： -l\t以PE为单位指定逻辑卷容量 -L\t指定逻辑卷的容量，单位B/S/K/M/G/T/P/E 6、lvremove命令用于删除指定的LVM逻辑卷，若逻辑卷已经被挂载到系统中，则应先使用unmount命令将其卸载，再进行删除，命令格式 lvremove [选项] 逻辑卷\nlvremove命令的常用选项为-f，其功能为强制删除指定逻辑卷\n删除卷组、物理卷分别使用vgremove、pvremove，删除是应逆向删除，即先删除逻辑分区、卷组，最后删除物理卷。\n文件系统 蓝色的小方块在文件系统中称为块（block），每一块可以存放4096个字节（4K），如果一个文件有5K,它会占用2个块。如果file1占用1,2,5，file2占用3,4，产生文件碎片，使用inode文件记录file文件的碎片的存放位置。之所以一个500G的硬盘，实际没用500G显示，是因为它inode和每个块之间有\nblock：存储文件的实际数据，实际存储文件的内容，若文件较大，会占用多个 block，block大小默认为4K。\ninode（索引节点）：每产生一个小片，就是一个inode，每个小片对应的就是块的位置，我们想调用块的时候就找索引。\n记录文件的属性（文件的元数据metadata，元数据，文件的属性，大小，权限，属主，属组，连接数，块数量，块的编号），一个文件占用一个inode，同时记录此文件所在的block number。inode大小为128bytes。\n（略）superblock：通俗易懂说，就是一堆inode和block的统称。\n每使用一次mkfs.ext4，就是创建一个文件系统.\n使用df -i 查看sdb2可以存放多少个文件\n1 2 3 4 5 6 7 8 9 10 11 [root@localhost disk1]# df -i |grep sdb2 /dev/sdb2 51200 11 51189 1% /mnt/disk2 总创建量 已用量\t剩余 [root@localhost disk2]# ls lost+found [root@localhost disk2]# touch file{1..51189}\t#创建51189个文件 [root@localhost disk2]# touch aaa.txt\t大于51189则无法创建 touch: 无法创建\u0026#34;aaa.txt\u0026#34;: 设备上没有空间 [root@localhost disk2]# df -i |grep sdb2 /dev/sdb2 51200 51200 0 100% /mnt/disk2 #表示inode已经用完了，不能继续创建文件夹，但可以在其中的文件夹中写入数据。 结论：磁盘空间的限制根据inode和block两方面，请清理掉填满的分区，避免不必要的报错。\n文件链接 符号链接 1、创建一个文件，并输入内容\n1 2 3 4 5 6 7 8 9 10 [root@localhost disk4]# echo 123 \u0026gt; /file00 [root@localhost disk4]# cat /file00 123\t硬\t软 [root@localhost disk4]# ln -s /file00 /tmp/file11 #创建软连接 [root@localhost disk4]# cat /tmp/file11 123 [root@localhost disk4]# rm -rf /file00 删除硬连接，软连接不复存在 [root@localhost disk4]# cat /tmp/file11 cat: /tmp/file11: 没有那个文件或目录 #可以对源文件进行追加，软链接文件也有追加内容 硬链接(可以理解为文件备份) 1 2 3 4 5 6 7 8 9 10 [root@localhost disk4]# echo 222 \u0026gt;/file2 [root@localhost disk4]# cat /file2 222 [root@localhost disk4]# ln /file2 /tmp/file-h1 [root@localhost disk4]# cat /tmp/file-h1 222 [root@localhost disk4]# rm -rf /file2 [root@localhost disk4]# cat /tmp/file-h1 222 #把源文件删除，硬链接文件还存在原有数据 RAID磁盘列阵(了解) RAID 0 将N块硬盘上选择合理的带区来创建带区集。其原理是将类似于显示器隔行扫描，将数据分割成不同条带(Stripe)分散写入到所有的硬盘中同时进行读写。多块硬盘的并行操作使同一时间内磁盘读写的速度提升N倍。\nRAID 1 称为磁盘镜像，原理是把一个磁盘的数据镜像到另一个磁盘上，也就是说数据在写入一块磁盘的同时，会在另一块闲置的磁盘上生成镜像文件，在不影响性能情况下最大限度的保证系统的可靠性和可修复性上，只要系统中任何一对镜像盘中至少有一块磁盘可以使用，甚至可以在一半数量的硬盘出现问题时系统都可以正常运行，当一块硬盘失效时，系统会忽略该硬盘，转而使用剩余的镜像盘读写数据，具备很好的磁盘冗余能力。虽然这样对数据来讲绝对安全，但是成本也会明显增加，磁盘利用率为50%。\nRAID5（分布式奇偶校验的独立磁盘结构）。 从它的示意图上可以看到，它的奇偶校验码存在于所有磁盘上，其中的p0代表第0带区的奇偶校验值，其它的意思也相同。RAID5的读出效率很高，写入效率一般，块式的集体访问效率不错，RAID5至少需要3块磁盘。\n缺点：\n1、RAID0没有冗余功能，如果一个磁盘（物理）损坏，则所有的数据都无法使用。 2、RAID1磁盘的利用率最高只能达到50%(使用两块盘的情况下)，是所有RAID级别 中最低的。 3、RAID0+1以理解为是RAID 0和RAID 1的折中方案。RAID 0+1可以为系统提供数据安全保障，但保障程度要比 Mirror低而磁盘空间利用率要比Mirror高。\n创建RAID\nLinux系统中使用mdadm命令创建和管理RAID，语法格式 mdadm [模式] \u0026lt;RAID设备名\u0026gt; [选项] \u0026lt;组件设备名\u0026gt;\n1 2 3 4 5 6 7 8 模式： -A/--accemble\t组合，组装一个预先存在的阵列 -B/--build\t构建，构建一个不需要超级块的阵列，阵列中的每个设备都没有超级块 -C/--create\t创建，创建一个新阵列 -F/--follow/--monitor\t监视，监视一个或多个阵列的状态 -G/--grow\t增长，更改RAID的容量或阵列中的设备数目 --auto-detect\t自动侦测，请求内核启动任何自动检测的阵列 -I/--incremental\t增加，向阵列中添加单个设备，或从阵列中删除单个设备 在-C模式下选项\n1 2 3 4 5 -l\t指定RAID级别 -n\t指定设备数量 -a{yes/no}\t是否自动为其创建设备文件 -c\t指定数据块大小 -x\t指定空闲盘个数，空闲盘可自动顶替损坏的工作盘 实验：准备四块硬盘\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [root@localhost ~]#ls /dev/sd* -l\t//查看 #创建RAID [root@localhost ~]#yum -y install mdadm //确保mdadm命令可用 [root@localhost ~]# mdadm -C /dev/md0 -l5 -n3 -x1 /dev/sd{h,i,j,k} -C创建RAID RAID的名称\t-l5 RAID5 数据盘3 热备盘数量1 磁盘 mdadm: Defaulting to version 1.2 metadata mdadm: array /dev/md0 started. # 格式化，挂载 [root@localhost ~]# mkfs.ext4 /dev/md0\t格式化 [root@localhost ~]# mkdir /mnt/raid5\t创建挂载文件 [root@localhost ~]# mount /dev/md0 /mnt/raid5\t挂载 [root@localhost ~]# cp -rf /etc/ /mnt/raid5/etc\t使用raid5 [root@localhost ~]# df -hT |tail -1 /dev/md0 ext4 9.8G 126M 9.1G 2% /mnt/raid5 #添加了4块5G的盘，显示了10G。因为两块盘是数据盘，第三块的校验盘，第四块的热备盘。 [root@localhost ~]# mdadm -D /dev/md0\t-D 查看raid5详细信息 破坏一块磁盘\n1 2 3 4 [root@localhost ~]# watch -n0.5 \u0026#39;mdadm -D /dev/md0 |tail -10\u0026#39; #实时查看信息 [root@localhost ~]# mdadm /dev/md0 -f /dev/sde -r /dev/sde #模拟损坏的数据盘 查找和压缩 文件查找 which ：命令查找\nalias：起别名(可以输入别名就可以执行对应的命令)，语法：alias 别名=‘ls -l’\nlocate：文件查找，语法：locate 文件名\nfind：任意文件查找，语法：find 路径 选项 文件名（文件名记不全可用代替，如hos 就会查找以hos字样的文件）\n1 2 3 4 5 6 7 8 9 选项： -name ：按照文件名区分大小写 -iname：不区分大小写 -size：按文件大小查找（如后接 +5M） -maxdepth：按照目录级查找（如find / maxdepth 4 -a -name \u0026#34;文件字眼\u0026#34; ） 从根开始向下查找，包括根目录，总共4级 -user/group: 根据用户/组查找（如 find /home -user alice） -type：按照文件类型查找（find /dev/ -type b）(b:块设备，f：普通文件) -perm：按照文件权限查找 find后接命令： find ………… -delete （表示将查到的内容删除） find ………… -ok cp -rvf {} 目标目录 ; （-ok表示连接符，{}表示引用查找的内容后的文件，;表示结束符）\n1 [root@localhost ~]# find /etc/ -name \u0026#39;ifcfg*\u0026#39; -ok cp -rvf {} /tmp \\; 文件打包及压缩 打包 tar命令本是用于备份文件的命令，该命令可以打包多个文件或目录，亦可将被打包的文件与目录中包中还原，\n命令格式 tar 选项 包名 [参数]\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 选项： -c\t创建新的备份文件 -x\t从备份文件中还原文件 -v\t显示命令执行过程 -f\t指定备份文件 -z\t通打包完成后使用gzip命令将包压缩 -j\t打包完成后使用bzip2命令将包压缩 -p\t保留包中文件原来的属性 用法： # 将目录test下的文件打包 $ tar -cvf test.tar ./test # 将目录test下的文件打包,并以gzip命令将包压缩 $ tar -zcvf test.tar.gz ./test # 将目录test下的文件打包,并以bzip2命令将包压缩 $ tar -jcvf test.tar.bz2 ./test #从包test.tar.bz2中还原文件 $ tar -xvf test.tar.bz2 压缩和解压 1、zip/unzip命令\n使用zip命令压缩文件时压缩包一般命名为\u0026quot;文件名.zip\u0026quot; zip [选项] 压缩包名 参数\n1 2 3 4 5 6 7 8 选项： -j\t只保留文件名称及其内容，不存放任何目录名称 -m\t文件压缩完成后，删除原始文件 -o\t以压缩文件内拥有最新更改时间的文件为准，更新压缩文件的更改时间 -r\t当参数为目录时，递归处理目录下的所有文件和子目录 $ zip -r dir1.zip dir1 注意：不能在当前目录压缩该目录 2、使用tar命令压缩 tar [选项] [压缩包名] [源文件]\n1 2 3 4 5 6 7 8 9 10 11 压缩类型分析（了解） [root@localhost ~]# tar -cf etc.tar /etc [root@localhost ~]# tar -czf etc.gz /etc [root@localhost ~]# tar -cjf etc.bz /etc [root@localhost ~]# tar -cJf etc.xz /etc 压缩时间由上到下越来越长，压缩程度越来越狠 [root@localhost ~]# ls -lh |grep etc -rw-r--r--. 1 root root 11M 6月 28 00:18 etc.bz -rw-r--r--. 1 root root 12M 6月 28 00:17 etc.gz -rw-r--r--. 1 root root 38M 6月 28 00:17 etc.tar -rw-r--r--. 1 root root 8.3M 6月 28 00:19 etc.xz 软件包管理 Linux提供了软件包的集中管理机制，该机制将软件以包的形式存储在仓库中，方便用户搜索、安装和管理软件包。\n红帽系操作系统软件管理分类：yum、rpm、source、bin。\nRPM软件包管理 软件名称 版本号(主版本、次版本、修订号) 操作系统 cpu平台\n操作系统:el6 el5 fedora suse debin ubuntu\ncpu平台:i386 486 586 686 表示32位软件\n​\tx86_64 表示64为软件\n​\tnoarch 表示32,64通用\n一、RPM软件包分为两种：二进制包和源码包。\n1、二进制包中封装的是编译后生成的可执行文件，类似于Windows系统的.exe文件，可使用rpm命令直接安装。\n2、源码包中封装的是源代码，在安装之前需先安装源码包以生成源码，再对源码进行编译生成后缀名为.rpm的RPM包，之后才能安装软件本身。\n3、后缀.rpm表示二进制包，后缀.src.rpm表示源码包 RPM工具\n1 2 [root@localhost Packages]#rpm -ivh wget-1.14-18.el7_6.1.x86_64.rpm #使用-i:install -v:显示安装信息 -h:百分比 使用rpm安装软件包： 安装软件包：rpm -ivh 软件包 查询是否安装完成：rpm -q 软件包 查询软件安装路径：rpm -ql 软件名称 查询软件名称：rpm -qa |grep ftp 查询软件信息：rpm -qi 软件名称\n删除软件包：rpm -evh wget-1.14-18.el7_6.1.x86_64（卸载的是软件，不用加.rpm后缀） 强制删除软件包（不安装依赖）：rpm -ivh 软件包 \u0026ndash;force 删除软件包（不检查依赖）：rpm -ivh 软件包 \u0026ndash;nodeps\n查询某一个文件是哪个软件产生的：rpm -qf /etc/passwd\n软件卸载：rpm -e 软件名称\n查询软件的配置文件：rpm -qc 软件名称\n​\t\u0026ndash;force 在安装的时候用(强制安装)\n​\t\u0026ndash;nodeps 在卸载的时候用(卸载的时候不检查依赖关系)\n无法自动安装依赖性软件包\n出现下载错误信息：可能出现的原因，/etc/yum.repos.d 文件配置错误\nYUM工具 基于RPM包管理，能够从指定的服务器自动下载RPM包并且安装，可以自动处理依赖性关系，并且依次安装所有依赖的软件包，无须繁琐地一次次下载、安装。\n1 2 3 1、安装\tinstall 2、查询\tlist和info。 yum list用于列出一个或一组软件包；yum info用于显示关于软件包或组的详细信息 使用yum安装软件包\n1 2 3 4 5 6 7 语法：yum -y install 软件包 重新安装：yum -y reinstall 软件包 更新：yum -y update 软件包 查询软件包：yum list 软件包 卸载软件包：yum -y remove 软件包 查看文件属于哪个软件：yum\tprovides vim\t使用install把对应的软件包下载下来，即可以使用vim 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 清理Yum缓存: [root@localhost ~]# yum clean all 缓存软件包信息: 提高搜索/安装软件的速度 [root@localhost ~]# yum makecache 查询yum源信息: [root@localhost ~]# yum repolist 查找软件: [root@localhost ~]# yum search mysql 此命令会搜索到系统已经安装和yum源里没有安装的软件信息,可以用他简单测试yum是否好用 查看软件依赖性关系: [root@localhost ~]# yum deplist 查看文件属于哪个软件 [root@localhost ~]# yum provides 想要安装的服务名 查看系统已经安装好的软件和没有安装的软件: [root@localhost ~]# yum list 查看系统已经安装好的软件组和没有安装的软件组: [root@localhost ~]# yum grouplist 查看软件组包含的具体软件： [root@localhost ~]# yum groupinfo 安装软件组: [root@localhost ~]# yum groupinstall ‘软件组名称’ 如果软件或者软件组名称内有空格，要给空格转义或者加引号 安装软件: [root@localhost ~]# yum install 软件名称 [root@localhost ~]# yum install mysql mysql-server -y -y跳过确认提示直接安装 重装： [root@localhost ~]# yum reinstall 软件名 卸载软件: [root@localhost ~]# yum erase mysql-server [root@localhost ~]# yum remove mysql-server 配置本地YUM源 介绍：基于RPM包管理，能够从指定的服务器自动下载RPM包并且安装，可以自动依赖处理依赖性关系，并且一次安装所有依赖的软件包，无须繁琐的一次次下载，安装。\nyum 源的配置与更新\n（1）在etc/yum/repos.d目录下，找到CentOS6-Base.repo文件替换（即替换CentOS6-Base.repo文件中的所有内容）。\n或者直接下载替换即可：\n1 2 3 #配置阿里云源 [root@localhost ~]#wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo centos-vault安装包下载_开源镜像站-阿里云 (aliyun.com)\n清华大学开源软件镜像站 | Tsinghua Open Source Mirror\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 # CentOS-Base.repo # # The mirror system uses the connecting IP address of the client and the # update status of each mirror to pick mirrors that are updated to and # geographically close to the client. You should use this for CentOS updates # unless you are manually picking other mirrors. # # If the mirrorlist= does not work for you, as a fall back you can try the # remarked out baseurl= line instead. # # [base] name=CentOS-$releasever - Base #mirrorlist=http://mirrorlist.centos.org/?release=$releasever\u0026amp;arch=$basearch\u0026amp;repo=os\u0026amp;infra=$infra #baseurl=https://mirrorlist.centos.org/centos/$releasever/os/$basearch/ baseurl=https://mirrors.aliyun.com/centos-vault/7.4.1708/os/$basearch/ gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 #released updates [updates] name=CentOS-$releasever - Updates #mirrorlist=http://mirrorlist.centos.org/?release=$releasever\u0026amp;arch=$basearch\u0026amp;repo=updates\u0026amp;infra=$infra #baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos-vault/$releasever/updates/$basearch/ baseurl=https://mirrors.aliyun.com/centos-vault/7.4.1708/updates/$basearch/ gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 #additional packages that may be useful [extras] name=CentOS-$releasever - Extras #mirrorlist=http://mirrorlist.centos.org/?release=$releasever\u0026amp;arch=$basearch\u0026amp;repo=extras\u0026amp;infra=$infra #baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos-vault/$releasever/extras/$basearch/ baseurl=https://mirrors.aliyun.com/centos-vault/7.4.1708/extras/$basearch/ gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 #additional packages that extend functionality of existing packages [centosplus] name=CentOS-$releasever - Plus #mirrorlist=http://mirrorlist.centos.org/?release=$releasever\u0026amp;arch=$basearch\u0026amp;repo=centosplus\u0026amp;infra=$infra #baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos-vault/$releasever/centosplus/$basearch/ baseurl=https://mirrors.aliyun.com/centos-vault/7.4.1708/centosplus/$basearch/ gpgcheck=1 enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 #contrib - packages by Centos Users [contrib] name=CentOS-$releasever - Contrib #mirrorlist=http://mirrorlist.centos.org/?release=$releasever\u0026amp;arch=$basearch\u0026amp;repo=contrib\u0026amp;infra=$infra #baseurl=https://mirrors.tuna.tsinghua.edu.cn/centos-vault/$releasever/contrib/$basearch/ baseurl=https://mirrors.aliyun.com/centos-vault/7.4.1708/contrib/$basearch/ gpgcheck=1 enabled=0 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7 连续执行以下命令\n1 2 3 yum install all yum makecache yum -y update 源码包管理 缺点：配置复杂\n1 2 3 4 1.解压缩 tar -xf 2.配置\t./configure 用户\t组\t功能模块 3.编译\tmake 4.安装\tmake install 获取源码包 来源：官方网站（如：Apache:www.apache.org Nginx:www.nginx.org Tengine:The Tengine Web Server (taobao.org)）\n在宿主机的源码包上传到VMare机器\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #查找rz命令需要什么软件支持 #rz：将window的文件拷贝到Linux\tsz：将Linux的文件拷贝到Windows [root@localhost ~]# yum provides rz\t[root@localhost ~]# yum install -y lrzsz-0.12.20-36.el7.x86_64 #使用rz命令，打开在弹窗的文件包，即上传到当下目录 [root@localhost ~]# rz [root@localhost ~]# yum clean all [root@localhost ~]# yum makecache [root@localhost ~]# yum -y install gcc make zlib-devel pcre pcre-devel openssl-devel [root@localhost ~]# useradd www [root@localhost ~]# tar xvf tengine-3.0.0.tar.gz [root@localhost ~]# cd tengine-3.0.0/ [root@localhost tengine-3.0.0]# ./configure --user=www --group=www --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_sub_module --with-http_ssl_module --with-pcre [root@localhost tengine-3.0.0]# make [root@localhost tengine-3.0.0]# make install [root@localhost tengine-3.0.0]# lsof -i:80 [root@localhost tengine-3.0.0]# yum provides lsof [root@localhost tengine-3.0.0]# yum install lsof-4.87-6.el7.x86_64 [root@localhost tengine-3.0.0]# lsof -i:80 [root@localhost tengine-3.0.0]# /usr/local/nginx/sbin/nginx [root@localhost tengine-3.0.0]# lsof -i:80 任务计划 一次性调度执行 at 语法：at\n案例：now +5min teatime tomorrow(teatime is 16:00) noon +4 days 5pm august 3 2029 4:00 2021-11-27\n1 2 3 4 5 6 [root@localhost ~]# at now+1min at\u0026gt; useradd aaa at\u0026gt; \u0026lt;EOT\u0026gt; job 3 at Sun Jul 4 11:41:00 2021 [root@localhost ~]# id aaa uid=1006(aaa) gid=1008(aaa) 组=1008(aaa) 循环调度执行cron 用于设置周期性被执行的指令，该命令从标准输入设备读取指令，并将其存放于“crontab”文件中，以供以后读取和执行。想让计算机干什么，就往里面写指令，不执行的就删掉。\n1、程序不执行问题，查看进程状态\n1 2 [root@localhost ~]# systemctl status crond.service [root@localhost ~]# ps aux |grep crond 计划任务存储位置 /var/spool/cron\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 安装软件 [root@localhost ~]# yum -y install crontabs 启动服务 rhel5/6: [root@localhost ~]# /etc/init.d/crond status [root@localhost ~]# /etc/init.d/crond start rhel7: [root@localhost ~]# systemctl start crond.service [root@localhost ~]# systemctl status crond.service [root@localhost ~]# systemctl enable crond.service 开机启动(rhel5/6) [root@localhost ~]# chkconfig crond on 创建计划任务：用户级别的计划任务 [root@localhost ~]# crontab -u 用户 -e -u 指定用户 默认不写就是root [root@localhost ~]# crontab -e 配置分两部分 拿空格分开 第一部分:时间 分钟 小时 日 月 周 范围 0-59 0-23 1-31 1-12 0-7 上面的时间范围可以查看man手册: [root@localhost ~]# man 5 crontab 各种时间写法： 5 10 * * * 5 10 8 * * 1 5 7 * 5 1,5,9 * * * * 8-12 * * * * 5-20,40 * * * * 8-12,20-25 * * * * */5 * * * * ps: * 表示每... , 取不同的时间点 - 表示范围 */5 每5分钟 第二部分:动作 把上面规定的时间要执行的命令写在这里，当然包括脚本(最常用)，命令最好要写绝对路径 查看计划任务:两种方法 1)[root@localhost ~]# crontab -l -u 用户名 查看某一个账户的计划任务 2)[root@localhost ~]# cat /var/spool/cron/root 计划任务删除:两种方法 1)[root@localhost ~]# crontab -r -u wing -r 删除 -u 指定用户 [root@localhost ~]# crontab -e -u tom 2)[root@localhost ~]# rm -f /var/spool/cron/root 计划任务的权限控制 [root@localhost ~]# cat /etc/cron.deny 如果这个文件存在，凡是写到这个文件里面的账户不允许执行crontab命令 [root@localhost ~]# cat /etc/cron.allow 如果这个文件存在，没有写到这个文件里面的账户不允许执行crontab命令 如果有allow文件，那不管deny是否存在，都是只允许allow文件里面的用户 日志管理 路由 1 2 3 4 5 6 7 8 9 10 11 12 [root@localhost ~]# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.101.2 0.0.0.0 UG 100 0 0 ens33 192.168.101.0 0.0.0.0 255.255.255.0 U 100 0 0 ens33 [root@localhost ~]# ip r\t//查看路由表 default via 192.168.101.2 dev ens33 proto dhcp metric 100 192.168.101.0/24 dev ens33 proto kernel scope link src 192.168.101.128 metric 100 [root@localhost ~]# ip r d default\t//删除默认网关 [root@localhost ~]# ip r del 192.168.101.0/24\t//删除静态路由 [root@localhost ~]# ip r add default via 192.168.101.2 dev ens33\t//添加默认网关 [root@localhost ~]# ip r add 192.168.101.0/24 via 192.168.101.2 dev ens33\t//添加静态路由 常见系统日志 1 2 3 4 5 6 /var/log/message:记录Linux操作系统常见的系统和服务错误信息 /var/log/boot.log:记录了系统在引导过程中发生的事件，就是Linux系统开机自检过程显示的信息 /var/log/lastlog :记录最后一次用户成功登陆的时间、登陆IP等信息(一般通过命令lastlog查看) /var/log/secure : Linux系统安全日志,记录用户和工作组变坏情况、用户登陆认证情况 /var/log/btmp :记录Linux登陆失败的用户、时间以及远程IP地址 /var/log/wtmp:该日志文件永久记录每个用户登录、注销及系统的启动、停机的事件，使用1ast命令查看 常用是1、4点，遇到错误提示时，可以查看系统日志，上图的第一点使用 在/root目录下，tailf /var/log/messages # 实时查看系统日志\n日志类型 auth pam 产生的日志\nauthpriv ssh，ftp 等登录信息的验证信息\ncron 时间任务相关\nkern 内核\nlpr 打印\nmail 邮件\nmark(syslog)-rsyslog 服务内部的信息，时间标识\nnews 新闻组\nuser 用户程序产生的相关信息\n日志的优先级 日志的优先级分为7种，级别代号0~7： 0 debug 有调试信息的，日志信息最多 1 info 一般信息的日志，最常用 2 notice 最具有重要性的普通条件的信息 3 warning 警告级别 4 err 错误级别，阻止某个功能或者模块不能正常工作的信息 5 crit 严重级别，阻止整个系统或者整个软件不能工作的信息 6 alert 需要立即修改的信息 7 emerg 内核崩溃等严重信息 none 什么都不记录\n自定义日志 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [root@localhost ~]#vim /etc/rsyslog.conf 日志对象(设备):你要对什么东东做日志 日志级别:级别越低，信息越多 日志文件:存储日志的文件 日志对象.日志级别 日志文件 . 大于或者等于后面指定的日志级别 .= 等于后面指定的日志级别 .! 非 例： *.* /var/log/mylog kern.err /var/log/kernel.log *.info;mail.none /var/log/big.log mail.info /var/log/mail.log cron.info;cron.!err /var/log/newcron cron.info /var/log/newcron 重启日志服务： systemctl restart rsyslog 日志进程rsyslog 系统专职日志程序，处理大部分日志记录，操作系统有关的信息，如登录信息，程序启动关闭信息，错误信息。\n任务一：rsyslog系统日志管理 关心的问题：哪类程序，产生什么的日志，放在什么地方\n查看日志进程\n1 2 3 [root@localhost ~]# ps aux | grep rsyslogd root 20730 0.0 0.4 214460 4644 ? Ssl 15:48 0:00 /usr/sbin/rsyslogd -n root 69210 0.0 0.0 112812 976 pts/1 R+ 17:02 0:00 grep --color=auto rsyslogd 动态查看日志文件的尾部：tail -f /var/log/messages 开启两个终端窗口\n查看各种日志 /var/log\nrsyslog配置文件 1 2 3 4 [root@localhost log]# rpm -qc rsyslog #查看配置文件 /etc/logrotate.d/syslog\t#rsyslogd相关文件，定义级别（了解） /etc/rsyslog.conf\t#rsyslogd的主配置文件(告诉rsyslog进程什么日志，应该存放在哪) /etc/sysconfig/rsyslog\t#和日志办轮转（切割）相关 日志轮转logrotate 任务二：将大量的日志，分割管理，删除旧日志\n简介 记录程序运行时的各种信息，通过日志可以分析用户的行为，记录运行轨迹，查找程序问题，但磁盘空间有限，记录的信息再重要也只能记录最后一段时间发生的事。为了节省空间和整理方便，日志文件需要经常按时间或大小等维度分成多份，删除时间久远\n工作原理 按照配置进行轮转，配置文件有主配置文件和子配置文件夹（自定义，便于管理） 主文件：/etc/ logrotate.conf （决定每个日志文件如何轮转） 子文件夹：/etc/logrotate.d/*\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [root@localhost log]# vi /etc/logrotate.conf =================全局设置====================== weekly\t//轮转的周期，一周轮转 rotate 4\t//保留4份，当产生第五份的时候，自动删除最旧的一份 create\t//轮转后创建新文件 dateext\t//使用日期作为后缀 # compress\t//是否压缩 include /etc/logrotate.d\t//包含该目录下的子配置文件，程序运行，在读主\t配置文件时，到子配置文件查看，读取 /var/log/wtmp {\t//对某日志文件设置轮转的方法，优先级最高 monthly\t//一个月轮转一次 minsize 1M\t//最小达到1M才轮转，monthly and minsize create 0664 root utmp\t//轮转后创建文件并设置权限 rotate 1\t//保留一份 } /var/log/btmp { missingok\t//丢失不提示 monthly\t//每月轮转一次 create 0600 root utmp\t//轮转后创建新文件，并设置权限 rotate 1\t//保留一份 } 1 2 3 4 5 6 7 8 9 10 [root@localhost log]# vim /etc/logrotate.d/yum\t#子配置文件下的yum /var/log/yum.log { missingok #notifempty #maxsize 30k #yearly daily\t#自定义每天轮转一次 rotate 3\t#自定义保留三份 create 0600 root root } 1 2 3 4 5 6 7 8 #测试 [root@localhost log]# date\t#查看当下时间 2021年 07月 09日 星期五 23:20:49 CST [root@localhost log]# date 07092322\t#修改系统时间（月日时分） 2021年 07月 09日 星期五 23:22:00 CST [root@localhost log]# logrotate /etc/logrotate.conf\t#手动轮转日志 [root@localhost log]# ls /var/log/yum.*\t#查看日志生成 /var/log/yum.log /var/log/yum.log-20210709 日志安全 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 [root@localhost log]# vim /etc/logrotate.d/syslog /etc/logrotate.d/messages 建议测试时先把/etc/logrotate.d/syslog中messages删除 #设置messages轮转 /var/log/messages { prerotate\t#设置轮转前的动作 chattr -a /var/log/messages endscript\t#动作结束 daily create 0777 root root missingok rotate 3 postrotate\t#轮转之后的动作 chattr +a /var/log/messages endscript } [root@localhost log]# logrotate /etc/logrotate.conf\t#手动轮转测试配置文件无误。 #为多个日志文件配置日志轮转 [root@localhost ~]#vim /etc/logrotate.d/syslog /var/log/cron /var/log/maillog /var/log/secure /var/log/spooler { missingok sharedscripts postrotate /bin/kill -HUP `cat /var/run/syslogd.pid 2\u0026gt; /dev/null` 2\u0026gt; /dev/null || true endscript } 网络管理 ip地址的划分 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 A类地址:范围从0-127，0是保留的并且表示所有IP地址，而127也是保留的地址，并且是用于测试环回用 的。因此A类地址的范围其实是从1-126之间。 如：10.0.0.1，第一段号码为网络号码，剩下的三段号码为本地计算机的号码。转换为2进制来说，一个A 类IP地址由1字节的网络地址和3字节主机地址组成，网络地址的最高位必须是“0”， 地址范围从0.0.0.1 到126.0.0.0。可用的A类网络有126个，每个网络能容纳1亿多个主机（2的24次方的-2主机数目）。 以子网掩码来进行区别：：255.0.0.0 127.0.0.0到127.255.255.255是保留地址，用做循环测试用的 B类地址：范围从128-191，如172.168.1.1，第一和第二段号码为网络号码，剩下的2段号码为本地计算 机的号码。转换为2进制来说，一个B类IP地址由2个字节的网络地址和2个字节的主机地址组成，网络地址 的最高位必须是“10”，地址范围从128.0.0.0到191.255.255.255。可用的B类网络有16382个，每个网 络能容纳6万多个主机 。(2的16次方-2) 以子网掩码来进行区别：255.255.0.0 169.254.0.0到169.254.255.255是保留地址。如果你的IP地址是自动获取IP地址，而你在网络上又没 有找到可用的DHCP服务器，这时你将会从169.254.0.0到169.254.255.255中临时获得一个IP地址。 C类地址：范围从192-223，如192.168.1.1，第一，第二，第三段号码为网络号码，剩下的最后一段号码 为本地计算机的号码。转换为2进制来说，一个C类IP地址由3字节的网络地址和1字节的主机地址组成，网 络地址的最高位必须是“110”。范围从192.0.0.0到223.255.255.255。C类网络可达209万余个，每个 网络能容纳254个主机。(2的8次方-2) 以子网掩码来进行区别： 255.255.255.0 D类地址：范围从224-239，D类IP地址第一个字节以“1110”开始，它是一个专门保留的地址。它并不指向 特定的网络，目前这一类地址被用在多点广播（Multicast）中。多点广播地址用来一次寻址一组计算 机，它标识共享同一协议的一组计算机。 224.0.0.0-239.255.255.255 组播地址 E类地址：范围从240-254，以“11110”开始，为将来使用保留。 全零（“0．0．0．0”）地址对应于当前 主机。全“1”的IP地址（“255．255．255．255”）是当前子网的广播地 址。 240.0.0.0-255.255.255.254 保留地址 子网掩码 就是为了区分ip地址的中的网络号和主机号的\n例1：\n1 2 3 4 5 6 7 8 ip地址: 202.197.119.110 若掩码为:255.255.255.0 求网络号和主机号 ip转换为2进制 1100 1010. 1100 0101. 0111 0111. 0110 1110 子网掩码2进制 1111 1111. 1111 1111. 1111 1111. 0000 0000 相与运算 1100 1010. 1100 0101. 0111 0111. 0000 0000 网络号 ip转换为2进制 1100 1010. 1100 0101. 0111 0111. 0110 1110 子网掩码取反 0000 0000. 0000 0000. 0000 0000. 1111 1111 相与运算 0000 0000. 0000 0000. 0000 0000. 0110 1110 主机号 1 2 3 4 ip 202.197.118.110 是否与上一个ip在同一网段? 求网络号，相同则同一网段 ip转换为2进制 1100 1010. 1100 0101. 0111 0110.0110 1110 求得网络号 1100 1010.1100 0101.0111 0110.0000 0000 网络号 不同，所以不再同一网络中 1 2 3 4 5 6 ip地址: 202.197.119.110 若掩码为:255.255.128.0 求网络号和主机号 ip转换为2进制 1100 1010. 1100 0101. 0111 0111. 0110 1110 子网掩码2进制 1111 1111. 1111 1111. 1000 0000. 0000 0000 相与运算 1100 1010. 1100 0101. 0000 0000. 0000 0000 网络号 主机号 0000 0000. 0000 0000. 0111 0111. 0110 1110 主机号 1 2 3 4 5 ip 202.197.118.110 是否与上一个ip再统一网段? ip转换为2进制 1100 1010. 1100 0101. 0111 0110. 0110 1110 求得网络号 1100 1010.1100 0101.0000 0000. 0000 0000 同上一个ip在同一个网络中 所以判断两个ip是否在同一网络要看子网掩码的设置 网络接口名称规则 1 2 3 4 5 6 7 8 9 10 ifconfig eth0 //单独查看eth0 ifconfig //查看所有网卡 ip a //查看所有网卡 ip a s ens33\t//单独查看ens33 ifconfig ens33 192.168.2.250/24 //会覆盖旧的IP ifconfig ens33:0 192.168.2.251/24 //子网掩码可以不写 ifconfig ens33 up //启动网卡 ifup ens33\t//启动网卡 ifconfig ens33 down\t//关闭网卡 ifdown ens33\t//关闭网卡 1、认识网卡；2、找到网卡文件；3、学会修改文件；4，多台服务器互通\n1 2 3 4 5 6 [root@localhost ~]#/etc/sysconfig/network-scripts/\t#查看网卡文件 [root@localhost ~]#systemctl status NetworkManager\t#查看网络管理程序状态 [root@localhost ~]#systemctl status network\t#重启网络 [root@localhost ~]#vi /etc/sysconfig/network-scripts/ifcfg-ens33 [root@localhost ~]# ip neigh #查路由，网关 [root@localhost ~]# ss -tnl\t#查端口服务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 DEVICE=eth0 NAME=\u0026#34;System eth0\u0026#34; //名称 可以不存在 BOOTPROTO=none //（none或static 静态获取 ）（dhcp 动态获取IP） NM_CONTROLLED=no //关闭NetworkManager ONBOOT=yes //开机启动 TYPE=Ethernet // 以太网类型 HWADDR=00:0c:29:8e:a5:d3 //MAC地址 IPADDR=172.16.80.252 //IP地址 NETMASK=255.255.0.0 //子网掩码 PREFIX=24 //子网掩码 NETWORK=172.16.0.0 //网络 GATEWAY=172.16.0.1 //网关 DNS1=172.16.0.1 //domain name server域名服务器 DNS2=114.114.114.114 //第2台DNS服务器 重启网络服务: 配置文件修改后必须重起网络服务\n1 2 3 systemctl restart network //rhel7 /etc/init.d/network restart //rhel5/6 service network restart //rhel5/6 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 最小化安装 1.为你的服务器配置root密码 2.配置IP地址 3.配置YUM源 4.关防火墙 systemctl stop firewalld systemctl disable firewalld systemctl status firewalld 5.selinux 查看selinux getenforce 临时关闭\tsetenforce 0 永久关闭\tvi /etc/sysconfig/selinux SELINUX=disabled 6.安装常用程序 上传下载工具 系统状态 字符浏览器 下载工具 网络工具 自动补全 yum install -y lrzsz sysstat elinks wget net-tools bash-completion vim 7.关机快照 网络测试工具 使用ping命令\n用来测试主机之间网络的连通性。执行ping指令会使用ICMP传输协议，发出要求回应的信息，若 远端主机的网络功能没有问题，就会回应该信息，因而得知该主机运作正常。\n（linux）-c数字：表示发送多少个包 （windows）-n数字：表示发送多少个包 -t：一直发送数据包\nping www.baidu.com -i 0.01 # ping百度，间隔时间为0.01s\n常用：ping -c1000 -i 0.01 www.baidu.com\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 ping 参数 目标主机 参数 详解 -a Audible ping. -A 自适应ping，根据ping包往返时间确定ping的速度； -b 允许ping一个广播地址； -B 不允许ping改变包头的源地址； -c count ping指定次数后停止ping； -d 使用Socket的SO_DEBUG功能； -F flow_label 为ping回显请求分配一个20位的“flow label”，如果未设置，内核会为ping随机分 配； -f 极限检测，快速连续ping一台主机，ping的速度达到100次每秒； -i interval 设定间隔几秒发送一个ping包，默认一秒ping一次； -I interface 指定网卡接口、或指定的本机地址送出数据包； -l preload 设置在送出要求信息之前，先行发出的数据包； 千锋云计算学院 -L 抑制组播报文回送，只适用于ping的目标为一个组播地址 -n 不要将ip地址转换成主机名； -p pattern 指定填充ping数据包的十六进制内容，在诊断与数据有关的网络错误时这个选项就非 常有用，如：“-p ff”； -q 不显示任何传送封包的信息，只显示最后的结果 -Q tos 设置Qos(Quality of Service)，它是ICMP数据报相关位；可以是十进制或十六进制数，详 见rfc1349和rfc2474文档； -R 记录ping的路由过程(IPv4 only)； 注意：由于IP头的限制，最多只能记录9个路由，其他会被忽略； -r 忽略正常的路由表，直接将数据包送到远端主机上，通常是查看本机的网络接口是否有问题； 如果主机不直接连接的网络上，则返回一个错误。 -S sndbuf Set socket sndbuf. If not specified, it is selected to buffer not more than one packet. -s packetsize 指定每次ping发送的数据字节数，默认为“56字节”+“28字节”的ICMP头，一共是84 字节； 包头+内容不能大于65535，所以最大值为65507（linux:65507, windows:65500）； -t ttl 设置TTL(Time To Live)为指定的值。该字段指定IP包被路由器丢弃之前允许通过的最大网段 数； -T timestamp_option 设置IP timestamp选项,可以是下面的任何一个： \u0026#39;tsonly\u0026#39; (only timestamps) \u0026#39;tsandaddr\u0026#39; (timestamps and addresses) \u0026#39;tsprespec host1 [host2 [host3]]\u0026#39; (timestamp prespecified hops). -M hint 设置MTU（最大传输单元）分片策略。 可设置为： \u0026#39;do\u0026#39;：禁止分片，即使包被丢弃； \u0026#39;want\u0026#39;：当包过大时分片； \u0026#39;dont\u0026#39;：不设置分片标志（DF flag）； -m mark 设置mark； -v 使ping处于verbose方式，它要ping命令除了打印ECHO-RESPONSE数据包之外，还打印其它 所有返回的ICMP数据包； -U Print full user-to-user latency (the old behaviour). Normally ping prints network round trip time, which can be different f.e. due to DNS failures. -W timeout 以毫秒为单位设置ping的超时时间； -w deadline deadline； 使用traceroute 命令\n通过traceroute我们可以知道信息从你的计算机到互联网另一端的主机是走的什么路径。当然每 次数据包由某一同样的出发点（source）到达某一同样的目的地(destination)走的路径可能会不 一样，但基本上来说大部分时候所走的路由是相同的。linux系统中，我们称之为traceroute,在 MS Windows中为tracert。 traceroute通过发送小的数据包到目的设备直到其返回，来测量其需 要多长时间。一条路径上的每个设备traceroute要测3次。输出结果中包括每次测试的时间(ms)和 设备的名称（如有的话）及其IP地址。\n1 2 3 4 5 6 traceroute www.baidu.com # 测试ping百度经过多少个网关 traceroute -m 10 www.baidu.com # 设置ping百度10个网关 traceroute -n www.baidu.com # 只显示ip地址 # 探测包使用基本UDP端口设置6888 traceroute -p 6888 www.baidu.com #对外探测包响应时间为3秒 traceroute -w 3 www.baidu.com 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 traceroute [参数] [主机] traceroute指令让你追踪网络数据包的路由途径，预设数据包大小是40Bytes，用户可另行设置 #traceroute [-dFlnrvx][-f\u0026lt;存活数值\u0026gt;][-g\u0026lt;网关\u0026gt;...][-i\u0026lt;网络界面\u0026gt;][- m\u0026lt;存活数值\u0026gt;][-p\u0026lt;通信端口\u0026gt;][-s\u0026lt;来源地址\u0026gt;][-t\u0026lt;服务类型\u0026gt;][-w\u0026lt;超时秒数\u0026gt;][主机名称或IP地址] [数据包大小] -d 使用Socket层级的排错功能。 -f 设置第一个检测数据包的存活数值TTL的大小。 -F 设置勿离断位。 -g 设置来源路由网关，最多可设置8个。 -i 使用指定的网络界面送出数据包。 -I 使用ICMP回应取代UDP资料信息。 -m 设置检测数据包的最大存活数值TTL的大小。 -n 直接使用IP地址而非主机名称。 -p 设置UDP传输协议的通信端口。 -r 忽略普通的Routing Table，直接将数据包送到远端主机上。 -s 设置本地主机送出数据包的IP地址。 -t 设置检测数据包的TOS数值。 -v 详细显示指令的执行过程。 -w 设置等待远端主机回报的时间。 -x 开启或关闭数据包的正确性检验。 Traceroute的工作原理\nTraceroute程序的设计是利用ICMP及IP header的TTL（Time To Live）栏位（field）。首先， traceroute送出一个TTL是1的IP datagram（其实，每次送出的为3个40字节的包，包括源地址， 目的地址和包发出的时间标签）到目的地，当路径上的第一个路由器（router）收到这个 datagram时，它将TTL减1。此时，TTL变为0了，所以该路由器会将此datagram丢掉，并送回一 个「ICMP time exceeded」消息（包括发IP包的源地址，IP包的所有内容及路由器的IP地址）， traceroute 收到这个消息后，便知道这个路由器存在于这个路径上，接着traceroute 再送出另一 个TTL是2 的datagram，发现第2 个路由器\u0026hellip;\u0026hellip; traceroute 每次将送出的datagram的TTL 加1来 发现另一个路由器，这个重复的动作一直持续到某个datagram 抵达目的地。当datagram到达目 的地后，该主机并不会送回ICMP time exceeded消息，因为它已是目的地了，那么traceroute如 何得知目的地到达了呢？ Traceroute在送出UDP datagrams到目的地时，它所选择送达的port number 是一个一般应用程 序都不会用的号码（30000 以上），所以当此UDP datagram 到达目的地后该主机会送回一个 「ICMP port unreachable」的消息，而当traceroute 收到这个消息时，便知道目的地已经到达 了。所以traceroute 在Server端也是没有所谓的Daemon 程式。 Traceroute提取发 ICMP TTL到期消息设备的IP地址并作域名解析。每次 ，Traceroute都打印出 一系列数据,包括所经过的路由设备的域名及 IP地址,三个包每次来回所花时间。\n文件服务 Ftp 介绍 文件传输协议（File Transfer Protocol，FTP），基于该协议FTP客户端与服务端可以实现共享文 件、上传文件、下载文件。 FTP 基于TCP协议生成一个虚拟的连接，主要用于控制FTP连接信息， 同时再生成一个单独的TCP连接用于FTP数据传输。用户可以通过客户端向FTP服务器端上传、下 载、删除文件，FTP服务器端可以同时提供给多人共享使用。\nFTP服务是Client/Server（简称C/S）模式，基于FTP协议实现FTP文件对外共享及传输的软件称之 为FTP服务器源端，客户端程序基于FTP协议，则称之为FTP客户端，FTP客户端可以向FTP服务器 上传、下载文件。\n目前主流的FTP服务器端软件包括：Vsftpd、ProFTPD、PureFTPd、Wuftpd、Server-U FTP、 FileZilla Server等软件，其中Unix/Linux使用较为广泛的FTP服务器端软件为Vsftpd 。\nFtp 传输模式介绍 FTP基于C/S模式，FTP客户端与服务器端有两种传输模式，分别是FTP主动模式、FTP被动模式， 主被动模式均是以FTP服务器端为参照。\nFTP主动模式：客户端从一个任意的端口N（N\u0026gt;1024）连接到FTP服务器的port 21命令端口，客 户端开始监听端口N+1，并发送FTP命令“port N+1”到FTP服务器，FTP服务器以数据端口（20）连 接到客户端指定的数据端口（N+1）。\nFTP被动模式：客户端从一个任意的端口N（N\u0026gt;1024）连接到FTP服务器的port 21命令端口，客 户端开始监听端口N+1，客户端提交 PASV命令，服务器会开启一个任意的端口（P \u0026gt;1024），并 发送PORT P命令给客户端。客户端发起从本地端口N+1到服务器的端口P的连接用来传送数据。\n企业实际环境中，如果FTP客户端与FTP服务端均开放防火墙，FTP需以主动模式工作，这样只需 要在FTP服务器端防火墙规则中，开放20、21端口即可。\nVsftp 服务器简介 非常安全的FTP服务进程（Very Secure FTP daemon，Vsftpd），Vsftpd在Unix/Linux发行版中 最主流的FTP服务器程序，优点小巧轻快，安全易用、稳定高效、满足企业跨部门、多用户的使用 （1000用户）等。\nVsftpd基于GPL开源协议发布，在中小企业中得到广泛的应用，Vsftpd可以快速上手，基于Vsftpd 虚拟用户方式，访问验证更加安全。Vsftpd还可以基于MYSQL数据库做安全验证，多重安全防 护。\nVsftp 的登陆类型 VSFTP提供了系统用户、匿名用户、和虚拟用户三种不同的登陆方式。所有的虚拟用户会映射成一个系 统用户，访问时的文件目录是为此系统用户的家目录；匿名用户也是虚拟用户，映射的系统用户为ftp， 详细信息可以通过man vsftpd.conf查看\nVsftp 安装配置 1 2 3 4 5 6 7 #安装 epel 源 [root@localhost ~]# yum -y install epel-release # 安装 vsftpd 及相关依赖 [root@localhost ~]# yum -y install vsftpd* pam* db4* vsftpd： ftp软件 pam：认证模块 DB4：支持文件数据库 vsftpd 配置文件说明 配置文件 作用 /etc/vsftpd/vsftpd.conf vsftpd的核心配置文件 /etc/vsftpd/ftpusers 用于指定哪些用户不能访问FTP服务器 /etc/vsftpd/user_list 指定允许使用vsftpd的用户列表文件 /etc/vsftpd/vsftpd_conf_migrate.sh 是vsftpd操作的一些变量和设置脚本 /var/ftp/ 默认情况下匿名用户的根目录 vsftpd 修改配置前备份配置文件 1 2 3 4 5 6 [root@localhost ~]# cd /etc/vsftpd/ [root@localhost vsftpd]# ls ftpusers user_list vsftpd.conf vsftpd_conf_migrate.sh [root@localhost vsftpd]# cp vsftpd.conf{,.bak} [root@localhost vsftpd]# ls ftpusers user_list vsftpd.conf vsftpd.conf.bak vsftpd_conf_migrate.sh vsftpd 配置匿名用户 1、编辑配置文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [root@localhost vsftpd]# cat vsftpd.conf | grep -v ^# [root@localhost vsftpd]# vim vsftpd.conf #:%d删除所有内容 write_enable=YES anon_umask=022 anon_upload_enable=YES anon_mkdir_write_enable=YES anon_other_write_enable=YES dirmessage_enable=YES xferlog_enable=YES connect_from_port_20=YES xferlog_std_format=YES listen=YES pam_service_name=vsftpd userlist_enable=YES tcp_wrappers=YES 2、常用的匿名FTP配置项\n1 2 3 4 5 6 7 8 9 anonymous_enable=YES # 是否允许匿名用户访问 anon_umask=022 # 匿名用户所上传文件的权限掩码 anon_root=/var/ftp # 设置匿名用户的FTP根目录 anon_upload_enable=YES # 是否允许匿名用户上传文件 anon_mkdir_write_enable=YES # 是否允许匿名用户允许创建目录 anon_other_write_enable=YES # 是否允许匿名用户有其他写入权 （改名，删除，覆盖） anon_max_rate=0 # 限制最大传输速率（字节/秒）0为 无限制 开启 vsftp 服务\n1 2 [root@localhost vsftpd]# systemctl start vsftpd [root@localhost vsftpd]# netstat -lnpt |grep vsftpd 修改权限实现上传\n1 2 3 4 [root@localhost vsftpd]# cd /var/ftp/ [root@localhost ftp]# ls pub [root@localhost ftp]# chown -R ftp.ftp pub/ 重点：改变根目录的属主，如果不改变的话，只能访问，其他权限不能生效。因为我们是以ftp用 户的身份访问的，而pub默认的属主属组是root。\n注意： 修改完配置之后需要重启完服务才能生效 还需要从新从客户端登陆，否则修改后的配置看不到效果。\nvsftp 配置本地（系统）用户 1 2 3 4 [root@localhost ftp]# useradd zhangsan [root@localhost ftp]# useradd lisi [root@localhost ftp]# echo \u0026#34;123456\u0026#34; | passwd --stdin zhangsan [root@localhost ftp]# echo \u0026#34;123456\u0026#34; | passwd --stdin lisi 修改配置文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [root@localhost ftp]# vi /etc/vsftpd/vsftpd.conf local_enable=YES local_umask=077 chroot_local_user=YES allow_writeable_chroot=YES userlist_deny=NO write_enable=YES dirmessage_enable=YES xferlog_enable=YES connect_from_port_20=YES xferlog_std_format=YES listen=YES pam_service_name=vsftpd userlist_enable=YES tcp_wrappers=YES 常用的本地用户FTP配置项\n1 2 3 4 5 6 7 8 9 10 11 local_enable=YES # 是否允许本地系统用户访问 local_umask=022 # 本地用户所上传文件的权限掩码 local_root=/var/ftp # 设置本地用户的FTP根目录 chroot_list_enable=YES # 表示是否开启chroot的环境，默认 没有开启 chroot_list_file=/etc/vsftpd/chroot_list # 表示写 在/etc/vsftpd/chroot_list文件里面的用户是不可以出chroot环境的。默认是可以的。 Chroot_local_user=YES # 表示所有写 在/etc/vsftpd/chroot_list文件里面的用户是可以出chroot环境的，和上面的相反。 local_max_rate=0 # 限制最大传输速率（字节/秒）0为 无限制 添加用户到白名单\n1 2 3 [root@localhost ftp]# vi /etc/vsftpd/user_list zhangsan lisi 重启服务\n1 systemctl restart vsftpd vsftp 配置虚拟用户 建立虚拟 FTP 用户的帐号\n1 useradd -s /sbin/nologin vu 创建虚拟用户文件\n1 2 3 4 5 6 [root@localhost ~]# cd /etc/vsftpd/ [root@localhost vsftpd]# vi user wangwu 12345 maliu 12345 基数行代表用户名，偶数行代表密码 创建数据文件\n通过 db_load 工具创建出 Berkeley DB 格式的数据库文件 1 [root@localhost vsftpd]# db_load -T -t hash -f user user.db -f 指定数据原文件 -T 允许非Berkeley DB的应用程序使用文本格式转换的DB数据文件 -t hash 读取文件的基本方法 建立支持虚拟用户的PAM认证文件\n1 2 3 vi /etc/pam.d/vsftpd.vu auth required /lib64/security/pam_userdb.so db=/etc/vsftpd/user account required /lib64/security/pam_userdb.so db=/etc/vsftpd/user 对应刚才生成 user.db 的文件 修改配置文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [root@localhost vsftpd]# vi vsftpd.conf write_enable=YES dirmessage_enable=YES xferlog_enable=YES connect_from_port_20=YES xferlog_std_format=YES listen=YES userlist_enable=YES tcp_wrappers=YES allow_writeable_chroot=YES local_enable=YES local_umask=077 chroot_local_user=YES pam_service_name=vsftpd.vu guest_enable=YES guest_username=vu virtual_use_local_privs=YES user_config_dir=/etc/vsftpd/user_dir 常用的全局配置项\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 listen=YES # 是否以独立运行的方式监听服务 千锋云计算学院 7、为用户建立独立的配置目录及文件 8、创建虚拟用户数据存放目录 listen_address=192.168.4.1 # 设置监听FTP服务的IP地址 listen_port=21 # 设置监听FTP服务的端口号 write_enable=YES # 是否启用写入权限（上传，删除文 件） download_enable＝YES # 是否允许下载文件 dirmessage_enable=YES # 用户切换进入目录时显 示.message文件 xferlog_enable=YES # 启用日志文件，记录 到/var/log/xferlog xferlog_std_format=YES # 启用标准的xferlog日志格式，禁 用此项将使用vsftpd自己的格式 connect_from_port_20=YES # 允许服务器主动模式（从20端口建 立数据连接） pasv_enable=YES # 允许服务器被动模式 pasv_max_port=24600 # 设置被动模式服务器的最大端口号 pasv_min_port=24500 # 设置被动模式服务器的最小端口号 pam_service_name=vsftpd # 用户认证的PAM文件位置 （/etc/pam.d/vsftpd.vu） userlist_enable=YES # 是否启用user_list列表文件 userlist_deny=YES # 是否禁用user_list中的用户 max_clients=0 # 限制并发客户端连接数 max_per_ip=0 # 限制同一IP地址的并发连接数 tcp_wrappers=YES # 是否启用tcp_wrappers主机访问 控制 chown_username=root # 表示匿名用户上传的文件的拥有人 是root，默认关闭 ascii_upload_enable=YES # 表示是否允许用户可以上传一个二 进制文件，默认是不允许的 ascii_download_enable=YES # 这个是代表是否允许用户可以下载 一个二进制文件，默认是不允许的 nopriv_user=vsftpd # 设置支撑Vsftpd服务的宿主用户为 手动建立的Vsftpd用户 async_abor_enable=YES # 设定支持异步传输功能 ftpd_banner=Welcome to Awei FTP servers # 设定Vsftpd的登陆标语 guest_enable=YES # 设置启用虚拟用户功能 guest_username=ftpuser # 指定虚拟用户的宿主用户 virtual_use_local_privs=YES # 设定虚拟用户的权限符合他们的宿 主用户 user_config_dir=/etc/vsftpd/vconf # 设定虚拟用户个人Vsftp的配置文件存放路径 为用户建立独立的配置目录及文件\n1 2 3 4 [root@localhost vsftpd]# mkdir /etc/vsftpd/user_dir [root@localhost vsftpd]# cd /etc/vsftpd/user_dir [root@localhost user_dir]# vi wangwu local_root=/etc/vsftpd/data # 虚拟用户数据的存放路径 创建虚拟用户数据存放目录\n1 2 3 [root@localhost user_dir]# cd .. [root@localhost vsftpd]# mkdir data [root@localhost vsftpd]# chmod 777 data/ 重启服务\n1 systemctl restart vsftpd Ftp 客户端 lftp lftp 介绍 lftp命令 是一款优秀的文件客户端程序，它支持ftp、SETP、HTTP和FTPs等多种文件传输协议。 lftp支持tab自动补全，记不得命令双击tab键，就可以看到可能的选项了。\nlftp 语法 1 lftp(选项)(参数) lftp 选项 1 2 3 4 -f：指定lftp指令要执行的脚本文件； -c：执行指定的命令后退出； --help：显示帮助信息； --version：显示指令的版本号。 lftp 参数 站点：要访问的站点的 ip 地址或者域名。 lftp 使用实例 登录ftp 1 lftp 用户名:密码@ftp地址:传送端口（默认21） 也可以先不带用户名登录，然后在接口界面下用login命令来用指定账号登录，密码不显示。 查看文件与改变目录 1 2 ls cd # 对应ftp目录 下载 get当然是可以的，还可以： 1 2 3 4 mget -c *.pdf # 把所有的pdf文件以允许断点续传的方式下载。 mirror aaa/ # 将aaa目录整个的下载下来，子目录也会自动复制。 pget -c -n 10 file.dat # 以最多10个线程以允许断点续传的方式下载file.dat，可以通过 设置pget:default-n的值而使用默认值。 上传 同样的put、mput都是对文件的操作，和下载类似。 1 mirror -R 本地目录名 将本地目录以迭代（包括子目录）的方式反向上传到ftp site。 模式设置 1 set ftp:charset gbk 远程ftp site用gbk编码，对应的要设置为utf8,只要替换gbk为utf8即可。 1 set file:charset utf8 本地的charset设定为utf8,如果你是gbk，相应改掉。 1 set ftp:passive-mode 1 使用被动模式登录，有些site要求必须用被动模式或者主动模式才可以登录，这个开关就是设置这 个的。0代表不用被动模式。 书签 其实命令行也可以有书签，在lftp终端提示符下： 1 bookmark add ustc 就可以把当前正在浏览的ftp site用ustc作为标签储存起来。以后在shell终端下，直接 lftp ustc 就可以自动填好用户名和密码，进入对应的目录了。 1 bookmark edit 会调用编辑器手动修改书签。当然，也可以看到，这个书签其实就是个简单的文本文件。密码，用 户名都可以看到。 配置文件 1 vim /etc/lftp.conf 一般添加这几行：\n1 2 3 set ftp:charset gbk set file:charset utf8 set pget:default-n 5 这样，就不用每次进入都要打命令了。其他的 set 可以自己 tab 然后 help 来看 NAS 存储 NAS 介绍 NAS 指 Network Area Storage，网络连接存储(NAS)是连接到TCP/IP网络(通常是以太网)的文件级 数据存储设备。它通常使用网络文件系统(NFS)或CIFS协议，但也可以使用其他选项，如HTTP。它 一般是将本地的存储空间共享给其他主机使用，一般通过 C/S 架构实现通信。它实现的是文件级 别的共享，计算机通常将共享的设别识别为一个文件系统，其文件服务器会管理锁以实现并发访 问。常见的 NAS 有 NFS 和 CIFS。 NAS在操作系统中显示为共享文件夹。工作人员像访问网络上的其他文件一样访问NAS中的文件。 NAS依赖于局域网运行，如果局域网出现故障，那么NAS服务将中断。 NAS通常不像基于块存储的SAN速度那么快，但高速局域网可以克服大多数性能和延迟问题 SAN 介绍 SAN 指 Storage Area Network，它将传输网络模拟成 SCSI 总线来使用，每一个主机的网卡相当 于 SCSI 总线中的 initiator，服务器相当于一个或多个 target，它需要借助客户端和服务端的 SCSI 驱动，通过 FC 或 TCP/IP 协议封装 SCSI 报文。它实现的是块级别的共享，通常被识别为一个块设 备，但是需要借助专门的锁管理软件才能实现多主机并发访问。 SAN是用于整合块级存储的专用高性能网络。网络将存储设备、交换机和主机互连。高端企业存储 区域网络(SAN)还可能包括SAN导向器级交换机，以实现更高性能和更高效的容量使用 服务器使用主机总线适配器(HBA)连接到SAN结构。服务器将SAN标识为本地连接存储，因此多台 服务器可以共享一个存储池。 SAN不依赖局域网，并通过直接从连接的服务器卸载数据来减轻本地 网络的压力。 NAS 与 SAN 的区别 Fabric。NAS使用TCP/IP网络，最常见的是以太网。传统SAN通常运行在高速光纤通道网络上，尽 管更多的SAN采用基于IP的光纤架构，这是因为光纤通道的成本和复杂性。高性能仍然是SAN的要 求，基于闪存的光纤协议有助于缩小光纤通道速度和IP速度之间的差距。 数据处理。两种存储体系结构处理数据的方式不同：NAS处理基于文件的数据，而SAN处理块数 据。这个故事并不那么简单：NAS可以使用全局命名空间，SAN可以访问专门的SAN文件系统。全 局命名空间聚合多个NAS文件系统以呈现统一视图。SAN文件系统使服务器能够共享文件。在SAN 架构中，每台服务器都维护一个专用的非共享LAN。 SAN文件系统允许服务器通过对同一LAN上 的服务器提供文件级访问来安全共享数据。 协议。NAS通过电缆直接连接以太网到以太网的交换机。NAS可以使用多种协议与服务器连接，包 括NFS、SMB/CIFS、HTTP。在SAN方面，服务器使用SCSI协议与SAN磁盘驱动器设备进行通信。 网络是使用SAS/SATA结构或将映射层映射到其他协议(例如光纤通道协议(FCP)，通过光纤通道映射 SCSI或通过TCP/IP映射SCSI形成的。 性能。对于需要高速流量的环境，例如高交易数据库和电子商务网站，SAN的性能更高。由于NAS 速度较慢的文件系统层，NAS通常具有较低的吞吐量和较高的延迟，但高速网络可弥补NAS内部的 性能损失。 可扩展性。入门级和NAS设备的可扩展性不高，但高端NAS系统使用集群或横向扩展节点扩展到 PB级。相反，可扩展性是购买SAN的主要驱动因素。其网络架构使管理员能够在扩展或扩展配置 中扩展性能和容量。 价格。虽然高端NAS的价格会高于入门级SAN，但通常NAS的购买和维护成本较低。NAS设备与存 储区域网络相比，硬件和软件管理组件更少。行政费用也计入比较因素中。在复杂堆栈的基础上， 使用FC SAN管理SAN更为复杂。其经验法则是将购买成本的10到20倍的费用作为年度维护计算。 易于管理。在一对一的比较中，NAS赢得了管理竞赛的便利。该设备可轻松插入局域网并提供简化 的管理界面。SAN需要比NAS设备更多的管理时间。部署通常需要对数据中心进行物理更改，而持 续管理通常需要专门的管理人员。对于SAN来说，例外的是更多的NAS设备不共享公共管理控制 台。 NAS 和 SAN 使用案例 NAS：当需要整合、集中和共享时 文件存储和共享。这是NAS在中小企业和企业远程办公室的主要用例。单个NAS设备允许IT整合多 个文件服务器，简化管理，节省空间和能源。 活动档案。长期存档最好存储在成本比较低廉的存储介质上，如磁带或基于云计算的冷存储库。 NAS是搜索和访问活跃存档的理想选择，而高容量NAS可以替代大型磁带库进行存档。 大数据。企业对于大数据有多种选择：横向扩展NAS、分布式JBOD节点、全闪存阵列、基于对象 的存储。横向扩展NAS适用于处理大型文件、ETL(提取，转换，加载)、智能数据服务(如自动分层 和分析)。NAS也是大型非结构化数据(如视频监控和流媒体)以及后期制作存储的理想选择。 虚拟化。并非每个用户都在使用NAS来进行虚拟化网络销售，但用例正在增长，VMware和HyperV都支持NAS上的数据存储。当企业尚未拥有SAN时，这是新型或小型虚拟化环境的流行选择。 虚拟桌面界面(VDI)。中档和高端NAS系统提供支持VDI的本机数据管理功能，如快速桌面克隆和重 复数据删除。 千锋云计算学院 SAN：当需要加速，扩展和保护时 数据库和电子商务网站。通用文件服务或NAS可用于较小的数据库，但高速事务环境需要SAN的高 I/O处理速度和非常低的延迟。这使SAN非常适合企业数据库和高流量电子商务网站。 快速备份。服务器操作系统将SAN视为附加存储，从而实现对SAN的快速备份。由于服务器直接备 份到SAN，备份流量不会通过LAN传输。这可以在不增加以太网负载的情况下实现更快速的备份。 虚拟化。NAS支持虚拟化环境，但SAN更适合大规模和/或高性能部署。存储区域网络可以在虚拟 机和虚拟化主机之间快速传输多个I/O流，并且拥有高扩展性支持动态处理。 视频编辑。视频编辑应用程序需要非常低的延迟和非常高的数据传输速率。SAN提供这种高性能， 因为它直接连接到视频编辑桌面客户端，无需额外的服务器层。视频编辑环境需要第三方SAN分布 式文件系统和每节点负载均衡控制。 在NFS，FTP，SAMBA中，其中下载速度或者说性能最好的是NFS，其次FTP，最后SAMBA\nNFS 服务器和 samba 服务器介绍 NFS 介绍 NFS 全称是 Network FileSystem，NFS 和其他文件系统一样，是在 Linux 内核中实现的，因此 NFS 很难做到与 Windows 兼容。NFS 共享出的文件系统会被客户端识别为一个文件系统，客户端 可以直接挂载并使用。 NFS 的实现使用了 RPC（Remote Procedure Call） 的机制，远程过程调用使得客户端可以调用 服务端的函数。由于有 VFS 的存在，客户端可以像使用其他普通文件系统一样使用 NFS 文件系 统，由操作系统内核将 NFS 文件系统的调用请求通过 TCP/IP 发送至服务端的 NFS 服务，执行相 关的操作，之后服务端再讲操作结果返回客户端。 NFS 文件系统仅支持基于 IP 的用户访问控制，NFS 是在内核实现的，因此 NFS 服务由内核监听在 TCP 和 UDP 的 2049 端口，对于 NFS 服务的支持需要在内核编译时选择。它同时还使用了几个用 户空间进程用于访问控制，用户映射等服务，这些程序由 nfs-utils 程序包提供。 RPC 服务在 CentOS 6.5 之后改名为 portmapper，它监听在 TCP/UDP 的 111 端口，其他基于 RPC 的服务进程需要监听时，先像 RPC 服务注册，RPC 服务为其分配一个随机端口供其使用。客 户端在请求时，先向 RPC 服务请求对应服务监听的端口，然后再向改服务发出调用请求。 Samba 简介 Samba是在Linux和UNIX系统上实现SMB协议的一个免费软件，由服务器及客户端程序构成。 SMB（Server Messages Block，信息服务块）是一种在局域网上共享文件和打印机的一种通信协 议，它为局域网内的不同计算机之间提供文件及打印机等资源的共享服务。SMB协议是客户机/服 务器型协议，客户机通过该协议可以访问服务器上的共享文件系统、打印机及其他资源。通过设置 “NetBIOS over TCP/IP”使得Samba不但能与局域网络主机分享资源，还能与全世界的电脑分享资 源。\nSamba最大的功能就是可以用于Linux与windows系统直接的文件共享和打印共享，Samba既可 以用于windows与Linux之间的文件共享，也可以用于Linux与Linux之间的资源共享。\nSamba由两个主要程序组成，它们是 smbd 和 nmbd 。这两个守护进程在服务器启动到停止期间持 续运行，功能各异。 Smbd 和 nmbd 使用的全部配置信息全都保存在smb.conf文件中。Smb.conf向 smbd和nmbd两个守护进程说明输出什么以便共享，共享输出给谁及如何进行输出。\nSamba提供了基于CIFS的四个服务：文件和打印服务、授权与被授权、名称解析、浏览服务。前 两项服务由 smbd 提供，后两项服务则由 nmbd 提供。 简单地说， smbd 进程的作用是处理到来的 SMB软件包，为使用该软件包的资源与Linux进行协商， nmbd 进程使主机(或工作站)能浏览Linux 服务器。\nSMB是Samba 的核心启动服务，主要负责建立 Linux Samba服务器与Samba客户机之间的对 话， 验证用户身份并提供对文件和打印系统的访问，只有SMB服务启动，才能实现文件的共享， 监听139 TCP端口；而NMB服务是负责解析用的，类似与DNS实现的功能，NMB可以把Linux系统 共享的工作组名称与其IP对应起来，如果NMB服务没有启动，就只能通过IP来访问共享文件，监 听137和138 UDP端口。\nSamba服务器的IP地址为192.168.122.15，对应的工作组名称为 MYWORKGROUP，那么在 Windows的IE浏览器输入下面两条指令都可以访问共享文件。其实这就是Windows下查看Linux Samba服务器共享文件的方法。\n\\192.168.122.15\\共享目录名称\n\\MYWORKGROUP\\共享目录名称\nSamba服务器可实现如下功能：WINS和DNS服务； 网络浏览服务； Linux和Windows域之间的 认证和授权； UNICODE字符集和域名映射；满足CIFS协议的UNIX共享等。\n安装配置 NFS 服务 添加 hosts 解析 1 2 3 vim /etc/hosts [可选] 192.168.101.100 zhangsan 192.168.101.102 nas 安装 NFS 服务器 1 yum install -y nfs-utils 创建 NFS 存储目录 1 mkdir /data 配置 NFS 服务 1 2 vim /etc/exports /data 192.168.101.0/24(rw,sync,no_root_squash) 192.168.101.0/24 一个网络号的主机可以挂载 NFS服务器上的 /data 目录到自己的文件系统中\nNFS 定制参数说明 ro：目录只读 rw： 这个选项允许 NFS 客户机进行读/写访问。缺省选项是只读的。 secure： 这个选项是缺省选项，它使用了 1024 以下的 TCP/IP 端口实现 NFS 的连接。指定 insecure 可以禁用这个选项。 async： 这个选项可以改进性能，但是如果没有完全关闭 NFS 守护进程就重新启动了 NFS 服务 器，这也可能会造成数据丢失。 no_wdelay： 这个选项关闭写延时。如果设置了 async，那么 NFS 就会忽略这个选项。 nohide： 如果将一个目录挂载到另外一个目录之上，那么原来的目录通常就被隐藏起来或看起来 像空的一样。要禁用这种行为，需启用 hide 选项。 no_subtree_check： 这个选项关闭子树检查，子树检查会执行一些不想忽略的安全性检查。缺省 选项是启用子树检查。 no_auth_nlm： 这个选项也可以作为 insecure_locks 指定，它告诉 NFS 守护进程不要对加锁请求 进行认证。如果关心安全性问题，就要避免使用这个选项。缺省选项是 auth_nlm 或 secure_locks。 mp (mountpoint=path)： 通过显式地声明这个选项，NFS 要求挂载所导出的目录。 fsid=num： 这个选项通常都在 NFS 故障恢复的情况中使用。如果希望实现 NFS 的故障恢复，请 参考 NFS 文档。 NFS 用户映射的选项 在使用 NFS 挂载的文件系统上的文件时，用户的访问通常都会受到限制，这就是说用户都是以匿 名用户的身份来对文件进行访问的，这些用户缺省情况下对这些文件只有只读权限。如果用户希望 以 root 用户或锁定义的其他用户身份访问远程文件系统上的文件，NFS 允许指定访问远程文件的 用户——通过用户标识号（UID）和组标识号（GID）进行用户映射。 root_squash： 这个选项不允许 root 用户访问挂载上来的 NFS 卷。 no_root_squash： 这个选项允许 root 用户访问挂载上来的 NFS 卷。 all_squash： 这个选项对于公共访问的 NFS 卷来说非常有用，它会限制所有的 UID 和 GID，只使 用匿名用户。缺省设置是 no_all_squash。 anonuid 和 anongid： 这两个选项将匿名 UID 和 GID 修改成特定用户和组帐号。 设置 NFS 服务开机启动 1 2 3 4 systemctl enable rpcbind.service systemctl enable nfs-server.service systemctl start rpcbind.service systemctl start nfs-server.service 确认 NFS 服务器启动 1 exportfs -v rpcinfo -p：检查 NFS 服务器是否挂载我们想共享的目录 /data： exportfs -r： 使配置生效 挂载端安装 NFS 客户端 安装 NFS 软件包 1 yum -y install nfs-utils 设置 rpcbind 开机启动 1 systemctl enable rpcbind.service 启动 rpcbind 服务 1 systemctl start rpcbind.service 注意：客户端不需要启动 nfs 服务 查看 NFS 服务端共享 检查 NFS 服务器端是否有目录共享：showmount -e nfs服务器IP/nfs服务器主机名\n1 showmount -e nas 挂在使用 NFS 存储 1 2 3 4 5 6 mkdir /data\t#手动挂载 mount -t nfs nas:/data /data umount /data vim /etc/fstab\t#开机自动挂载 mount -a df\t#查看挂载 如果服务器端修改了 NFS 的配置，而又不想重启 NFS 服务（因为有客户端正在使用）可以使用 exportfs 命令重新载入 NFS 配置。\nexport -ar: 重新导出所有的文件系统\nexport -au: 关闭导出的所有文件系统\nexport -u FS: 关闭指定的导出的文件系\n1 rpcinfo -p #查看 RPC 服务列表 网站服务 Apache 静态站点 1 2 3 4 5 6 7 8 [root@localhost ~]# yum -y install httpd [root@localhost ~]# systemctl start httpd [root@localhost ~]# systemctl status httpd [root@localhost ~]# systemctl enable httpd [root@localhost ~]# systemctl stop firewalld [root@localhost ~]# setenforce 0 [root@localhost ~]# systemctl stop firewalld [root@localhost ~]# httpd -v LAMP 动态站点 部署论坛系统discuz 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #永久关闭selinux [root@apache ~]# sed -ri \u0026#39;/^SELINUX=/cSELINUX=disabled\u0026#39; /etc/selinux/config 临时关闭selinux [root@apache ~]# setenforce 0 [root@apache ~]# systemctl stop firewalld.service [root@apache ~]# systemctl disable firewalld.service #安装LAMP [root@apache ~]# yum -y install httpd mariadb-server mariadb php php-mysql gd php-gd [root@apache ~]# systemctl start httpd mariadb [root@apache ~]# systemctl enable httpd mariadb #安装discuz ##导入discuz网站源码 wget http://download.comsenz.com/DiscuzX/2.5/Discuz_X2.5_SC_UTF8.zip [root@apache ~]# mkdir -p /webroot/discuz [root@apache ~]# yum install -y unzip [root@apache ~]#unzip Discuz_X2.5_SC_UTF8.zip [root@apache ~]#cp -rf upload/* /webroot/discuz/ [root@apache ~]#chown -R apache.apache /webroot/discuz/ # Apache 配置虚拟主机 [root@apache ~]# vim /etc/httpd/conf.d/discuz.conf \u0026lt;VirtualHost *:80\u0026gt; ServerName www.discuz.com DocumentRoot /webroot/discuz \u0026lt;/VirtualHost\u0026gt; \u0026lt;Directory \u0026#34;/webroot/discuz\u0026#34;\u0026gt; Require all granted \u0026lt;/Directory\u0026gt; [root@apache ~]# systemctl restart httpd #准备数据库 [root@localhost discuz]# mysql MariaDB [(none)]\u0026gt; create database discuz ; 域名服务 hosts文件：实现名字解析，主要为本地主机名，集群节点提供快速解析。缺点是不便于查询，更新。\nDNS：（Domain Name System，域名系统）实现名字解析（例如将主机名解析为IP），分布式，层次性。\nFQDN：(Fully Qualified Domain Name)完全合格域名/全称域名。\n主机名.四级域.三级域.二级域.顶级域.（根域）\n命名空间：\nDNS解析流程 例如客户端解析 www.126.com\n客户端查询自己的缓存（包含hosts中的记录），如果没有将查询发送/etc/resolv.conf中的DNS服务器\n如果本地DNS服务器对于请求的信息具有权威性，会将（权威答案）发送到客户端。\n否则（不具有权威性），如果DNS服务器在其缓存中有请求信息，则将（非权威答案）发送到客户端\n如果缓存中没有该查询信息，DNS服务器将搜索权威DNS服务器以查找信息： a. 从根区域开始，按照DNS层次结构向下搜索，直至对于信息具有权威的名称服务器，为客户端获答案 DNS服务器将信息传递给客户端 ，并在自己的缓存中保留一个副本，以备以后查找。 b. 转发到其它DNS服务器\n正向解析/反向解析：\nDNS服务主要起到两个作用：\n1）可以把相对应的域名解析为对应的IP地址，这叫正向解析。\n2）可以把相对应的IP地址解析为对应的域名，这叫反向解析。（反垃圾邮件）\n远程管理 ssh服务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 安装软件： openssh-server 提供服务 openssh-clients 客户端 openssh [root@localhost ~]# yum install openssh* -y ssh 端口22 服务器端： 启动服务： [root@localhost ~]# systemctl start sshd 查看： [root@localhost ~]# lsof -i:22 关闭防火墙和selinux 客户端： 远程登陆管理： [root@localhost ~]# ssh -X tom@10.18.44.208 -p 2222 [root@localhost ~]# ssh 10.18.44.208 如登陆果账户没有密码，默认不能 无密码登陆(ssh密钥认证) client: 产生公钥和私钥： [root@localhost ~]# ssh-keygen //一路回车 拷贝公钥给对方： [root@localhost ~]# ssh-copy-id -i 10.18.44.208 直接执行远程命令： [root@localhost ~]# ssh 10.18.44.208 \u0026#34;reboot\u0026#34; 远程拷贝： 需要先安装客户端 [root@localhost ~]# cp 源文件 目标路径 谁是远程谁加IP [root@localhost ~]# scp /a.txt 192.168.2.108:/ [root@localhost ~]# scp 192.168.2.108:/a.txt ./ -P端口 拷贝目录加-r选项 [root@localhost ~]# scp 192.168.2.108:/a.txt 192.168.2.109:/ 修改端口号 [root@localhost ~]# vim /etc/ssh/sshd_config Port 22 ListenAddress 192.168.2.8 PermitRootLogin yes MaxSessions 10 最大并发量 PermitEmptyPasswords no rz sz命令 1 2 3 4 5 6 7 8 9 安装 root 账号登陆后执行以下命令： [root@localhost ~]#yum install -y lrzsz 使用 sz命令发送文件到本地： [root@localhost ~]# sz filename rz命令本地上传文件到服务器： [root@localhost ~]# rz 执行该命令后，在弹出框中选择要上传的文件即可。 文件服务器 Ftp 介绍 文件传输协议（File Transfer Protocol，FTP），基于该协议FTP客户端与服务端可以实现共享文件、上传文件、下载文件。 FTP 基于TCP协议生成一个虚拟的连接，主要用于控制FTP连接信息，\n同时再生成一个单独的TCP连接用于FTP数据传输。用户可以通过客户端向FTP服务器端上传、下载、删除文件，FTP服务器端可以同时提供给多人共享使用。\nFTP服务是Client/Server（简称C/S）模式，基于FTP协议实现FTP文件对外共享及传输的软件称之为FTP服务器源端，客户端程序基于FTP协议，则称之为FTP客户端，FTP客户端可以向FTP服务器上传、下载文件。\n目前主流的FTP服务器端软件包括：Vsftpd、ProFTPD、PureFTPd、Wuftpd、Server-U FTP、FileZilla Server等软件，其中Unix/Linux使用较为广泛的FTP服务器端软件为Vsftpd 。\nFtp 传输模式介绍 FTP基于C/S模式，FTP客户端与服务器端有两种传输模式，分别是FTP主动模式、FTP被动模式，主被动模式均是以FTP服务器端为参照。\nFTP主动模式：客户端从一个任意的端口N（N\u0026gt;1024）连接到FTP服务器的port 21命令端口，客户端开始监听端口N+1，并发送FTP命令“port N+1”到FTP服务器，FTP服务器以数据端口（20）连接到客户端指定的数据端口（N+1）。\nFTP被动模式：客户端从一个任意的端口N（N\u0026gt;1024）连接到FTP服务器的port 21命令端口，客户端开始监听端口N+1，客户端提交 PASV命令，服务器会开启一个任意的端口（P \u0026gt;1024），并发送PORT P命令给客户端。客户端发起从本地端口N+1到服务器的端口P的连接用来传送数据。\n企业实际环境中，如果FTP客户端与FTP服务端均开放防火墙，FTP需以主动模式工作，这样只需要在FTP服务器端防火墙规则中，开放20、21端口即可。\nVsftp 服务器简介 非常安全的FTP服务进程（Very Secure FTP daemon，Vsftpd），Vsftpd在Unix/Linux发行版中最主流的FTP服务器程序，优点小巧轻快，安全易用、稳定高效、满足企业跨部门、多用户的使用（1000用户）等。\nVsftpd基于GPL开源协议发布，在中小企业中得到广泛的应用，Vsftpd可以快速上手，基于Vsftpd虚拟用户方式，访问验证更加安全。Vsftpd还可以基于MYSQL数据库做安全验证，多重安全防护。\nVsftp 的登陆类型 VSFTP提供了系统用户、匿名用户、和虚拟用户三种不同的登陆方式。所有的虚拟用户会映射成一个系统用户，访问时的文件目录是为此系统用户的家目录；匿名用户也是虚拟用户，映射的系统用户为ftp，详细信息可以通过man vsftpd.conf查看\nVsftp 安装配置 安装 epel 源 1 yum -y install epel-release.noarch 安装 vsftpd 及相关依赖 1 yum -y install vsftpd* pam* db4* vsftpd： ftp软件 pam：认证模块 DB4：支持文件数据库 vsftpd 配置文件说明 vsftpd 配置详解 ","permalink":"https://ktzxy.top/posts/9ot8187agx/","summary":"Linux学习笔记","title":"Linux学习笔记"},{"content":"Go中的结构体 关于结构体 Golang中没有“类”的概念，Golang中的结构体和其他语言中的类有点相似。和其他面向对象语言中的类相比，Golang中的结构体具有更高的扩展性和灵活性。\nGolang中的基础数据类型可以装示一些事物的基本属性，但是当我们想表达一个事物的全部或部分属性时，这时候再用单一的基本数据类型就无法满足需求了，Golang提供了一种自定义数据类型，可以封装多个基本数据类型，这种数据类型叫结构体，英文名称struct。也就是我们可以通过struct来定义自己的类型了。\nType关键字 Golang中通过type关键词定义一个结构体，需要注意的是，数组和结构体都是值类型，在这个和Java是有区别的\n自定义类型 在Go语言中有一些基本的数据类型，如string、整型、浮点型、布尔等数据类型，Go语言中可以使用type关键字来定义自定义类型。\n1 type myInt int 上面代码表示：将mylnt定义为int类型，通过type关键字的定义，mylnt就是一种新的类型，它具有int的特性。\n示例：如下所示，我们定义了一个myInt类型\n1 2 3 4 5 type myInt int func main() { var a myInt = 10 fmt.Printf(\u0026#34;%v %T\u0026#34;, a, a) } 输出查看它的值以及类型，能够发现该类型就是myInt类型\n1 10 main.myInt 除此之外，我们还可以定义一个方法类型\n1 2 3 4 5 6 7 func fun(x int, y int)int { return x + y } func main() { var fn myFn = fun fmt.Println(fn(1, 2)) } 然后调用并输出\n1 3 类型别名 Golang1.9版本以后添加的新功能\n类型别名规定：TypeAlias只是Type的别名，本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有大名、小名、英文名，但这些名字都指的是他本人\n1 type TypeAlias = Type 我们之前见过的rune 和 byte 就是类型别名，他们的底层代码如下\n1 2 type byte = uint8 type rune = int32 结构体定义和初始化 结构体的定义 使用type 和 struct关键字来定义结构体，具体代码格式如下所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /** 定义一个人结构体 */ type Person struct { name string age int sex string } func main() { // 实例化结构体 var person Person person.name = \u0026#34;张三\u0026#34; person.age = 20 person.sex = \u0026#34;男\u0026#34; fmt.Printf(\u0026#34;%#v\u0026#34;, person) } 注意：结构体首字母可以大写也可以小写，大写表示这个结构体是公有的，在其它的包里面也可以使用，小写表示结构体属于私有的，在其它地方不能使用\n例如：\n1 2 3 4 5 type Person struct { Name string Age int Sex string } 实例化结构体 刚刚实例化结构体用到了：var person Person\n1 2 3 4 5 // 实例化结构体 var person Person person.name = \u0026#34;张三\u0026#34; person.age = 20 person.sex = \u0026#34;男\u0026#34; 实例化结构体2 我们下面使用另外一个方式来实例化结构体，通过new关键字来实例化结构体，得到的是结构体的地址，格式如下\n1 2 3 4 5 var person2 = new(Person) person2.name = \u0026#34;李四\u0026#34; person2.age = 30 person2.sex = \u0026#34;女\u0026#34; fmt.Printf(\u0026#34;%#v\u0026#34;, person2) 输出如下所示，从打印结果可以看出person2是一个结构体指针\n1 \u0026amp;main.Person{name:\u0026#34;李四\u0026#34;, age:30, sex:\u0026#34;女\u0026#34;} 需要注意：在Golang中支持对结构体指针直接使用，来访问结构体的成员\n1 2 3 person2.name = \u0026#34;李四\u0026#34; // 等价于 (*person2).name = \u0026#34;李四\u0026#34; 实例化结构体3 使用\u0026amp;对结构体进行取地址操作，相当于对该结构体类型进行了一次new实例化操作\n1 2 3 4 5 6 // 第三种方式实例化 var person3 = \u0026amp;Person{} person3.name = \u0026#34;赵四\u0026#34; person3.age = 28 person3.sex = \u0026#34;男\u0026#34; fmt.Printf(\u0026#34;%#v\u0026#34;, person3) 实例化结构体4 使用键值对的方式来实例化结构体，实例化的时候，可以直接指定对应的值\n1 2 3 4 5 6 7 // 第四种方式初始化 var person4 = Person{ name: \u0026#34;张三\u0026#34;, age: 10, sex: \u0026#34;女\u0026#34;, } fmt.Printf(\u0026#34;%#v\u0026#34;, person4) 实例化结构体5 第五种和第四种差不多，不过是用了取地址，然后返回的也是一个地址\n1 2 3 4 5 6 7 // 第五种方式初始化 var person5 = \u0026amp;Person{ name: \u0026#34;孙五\u0026#34;, age: 10, sex: \u0026#34;女\u0026#34;, } fmt.Printf(\u0026#34;%#v\u0026#34;, person5) 实例化结构体6 第六种方式是可以简写结构体里面的key\n1 2 3 4 5 6 var person6 = Person{ \u0026#34;张三\u0026#34;, 5, \u0026#34;女\u0026#34;, } fmt.Println(person6) 结构体方法和接收者 在go语言中，没有类的概念但是可以给类型（结构体，自定义类型）定义方法。所谓方法就是定义了接收者的函数。接收者的概念就类似于其他语言中的this 或者self。\n方法的定义格式如下：\n1 2 3 func (接收者变量 接收者类型) 方法名(参数列表)(返回参数) { 函数体 } 其中\n接收者变量：接收者中的参数变量名在命名时，官方建议使用接收者类型名的第一个小写字母，而不是self、this之类的命名。例如，Person类型的接收者变量应该命名为p，Connector类型的接收者变量应该命名为c等。、 接收者类型：接收者类型和参数类似，可以是指针类型和非指针类型。 非指针类型：表示不修改结构体的内容 指针类型：表示修改结构体中的内容 方法名、参数列表、返回参数：具体格式与函数定义相同 如果示例所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /** 定义一个人结构体 */ type Person struct { name string age int sex string } // 定义一个结构体方法 func (p Person) PrintInfo() { fmt.Print(\u0026#34; 姓名: \u0026#34;, p.name) fmt.Print(\u0026#34; 年龄: \u0026#34;, p.age) fmt.Print(\u0026#34; 性别: \u0026#34;, p.sex) fmt.Println() } func (p *Person) SetInfo(name string, age int, sex string) { p.name = name p.age = age p.sex = sex } func main() { var person = Person{ \u0026#34;张三\u0026#34;, 18, \u0026#34;女\u0026#34;, } person.PrintInfo() person.SetInfo(\u0026#34;李四\u0026#34;, 18, \u0026#34;男\u0026#34;) person.PrintInfo() } 运行结果为：\n1 2 姓名: 张三 年龄: 18 性别: 女 姓名: 李四 年龄: 18 性别: 男 注意，因为结构体是值类型，所以我们修改的时候，因为是传入的指针\n1 2 3 4 5 func (p *Person) SetInfo(name string, age int, sex string) { p.name = name p.age = age p.sex = sex } 给任意类型添加方法 在Go语言中，接收者的类型可以是任何类型，不仅仅是结构体，任何类型都可以拥有方法。\n举个例子，我们基于内置的int类型使用type关键字可以定义新的自定义类型，然后为我们的自定义类型添加方法。\n1 2 3 4 5 6 7 8 9 10 11 12 type myInt int func fun(x int, y int)int { return x + y } func (m myInt) PrintInfo() { fmt.Println(\u0026#34;我是自定义类型里面的自定义方法\u0026#34;) } func main() { var a myInt = 10 fmt.Printf(\u0026#34;%v %T \\n\u0026#34;, a, a) a.PrintInfo() } 结构体的匿名字段 结构体允许其成员字段在声明时没有字段名而只有类型，这种没有名字的字段就被称为匿名字段\n匿名字段默认采用类型名作为字段名，结构体要求字段名称必须唯一，因此一个结构体中同种类型的匿名字段只能一个\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /** 定义一个人结构体 */ type Person struct { string int } func main() { // 结构体的匿名字段 var person = Person{ \u0026#34;张三\u0026#34;, 18 } } 结构体的字段类型可以是：基本数据类型，也可以是切片、Map 以及结构体\n如果结构体的字段类似是：指针、slice、和 map 的零值都是nil，即还没有分配空间\n如果需要使用这样的字段，需要先make，才能使用\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /** 定义一个人结构体 */ type Person struct { name string age int hobby []string mapValue map[string]string } func main() { // 结构体的匿名字段 var person = Person{} person.name = \u0026#34;张三\u0026#34; person.age = 10 // 给切片申请内存空间 person.hobby = make([]string, 4, 4) person.hobby[0] = \u0026#34;睡觉\u0026#34; person.hobby[1] = \u0026#34;吃饭\u0026#34; person.hobby[2] = \u0026#34;打豆豆\u0026#34; // 给map申请存储空间 person.mapValue = make(map[string]string) person.mapValue[\u0026#34;address\u0026#34;] = \u0026#34;北京\u0026#34; person.mapValue[\u0026#34;phone\u0026#34;] = \u0026#34;123456789\u0026#34; // 加入#打印完整信息 fmt.Printf(\u0026#34;%#v\u0026#34;, person) } 同时我们还支持结构体的嵌套，如下所示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // 用户结构体 type User struct { userName string password string sex string age int address Address // User结构体嵌套Address结构体 } // 收货地址结构体 type Address struct { name string phone string city string } func main() { var u User u.userName = \u0026#34;moguBlog\u0026#34; u.password = \u0026#34;123456\u0026#34; u.sex = \u0026#34;男\u0026#34; u.age = 18 var address Address address.name = \u0026#34;张三\u0026#34; address.phone = \u0026#34;110\u0026#34; address.city = \u0026#34;北京\u0026#34; u.address = address fmt.Printf(\u0026#34;%#v\u0026#34;, u) } 嵌套结构体的字段名冲突 嵌套结构体内部可能存在相同的字段名，这个时候为了避免歧义，需要指定具体的内嵌结构体的字段。（例如，父结构体中的字段 和 子结构体中的字段相似）\n默认会从父结构体中寻找，如果找不到的话，再去子结构体中在找\n如果子类的结构体中，同时存在着两个相同的字段，那么这个时候就会报错了，因为程序不知道修改那个字段的为准。\n结构体的继承 结构体的继承，其实就类似于结构体的嵌套，如下所示，我们定义了两个结构体，分别是Animal 和 Dog，其中每个结构体都有各自的方法，然后通过Dog结构体 继承于 Animal结构体\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // 用户结构体 type Animal struct { name string } func (a Animal) run() { fmt.Printf(\u0026#34;%v 在运动 \\n\u0026#34;, a.name) } // 子结构体 type Dog struct { age int // 通过结构体嵌套，完成继承 Animal } func (dog Dog) wang() { fmt.Printf(\u0026#34;%v 在汪汪汪 \\n\u0026#34;, dog.name) } func main() { var dog = Dog{ age: 10, Animal: Animal{ name: \u0026#34;阿帕奇\u0026#34;, }, } dog.run(); dog.wang(); } 运行后，发现Dog拥有了父类的方法\n1 2 阿帕奇 在运动 阿帕奇 在汪汪汪 Go中的结构体和Json相互转换 JSON（JavaScript Object Notation）是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。RESTfull Api接口中返回的数据都是json数据。\n1 2 3 4 { \u0026#34;name\u0026#34;: \u0026#34;张三\u0026#34;, \u0026#34;age\u0026#34;: 15 } 比如我们Golang要给App或者小程序提供Api接口数据，这个时候就需要涉及到结构体和Json之间的相互转换 Golang JSON序列化是指把结构体数据转化成JSON格式的字符串，Golang JSON的反序列化是指把JSON数据转化成Golang中的结构体对象\nGolang中的序列化和反序列化主要通过“encoding/json”包中的 json.Marshal() 和 son.Unmarshal()\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 定义一个学生结构体，注意结构体的首字母必须大写，代表公有，否则将无法转换 type Student struct { ID string Gender string Name string Sno string } func main() { var s1 = Student{ ID: \u0026#34;12\u0026#34;, Gender: \u0026#34;男\u0026#34;, Name: \u0026#34;李四\u0026#34;, Sno: \u0026#34;s001\u0026#34;, } // 结构体转换成Json（返回的是byte类型的切片） jsonByte, _ := json.Marshal(s1) jsonStr := string(jsonByte) fmt.Printf(jsonStr) } 将字符串转换成结构体类型\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 定义一个学生结构体，注意结构体的首字母必须大写，代表公有，否则将无法转换 type Student struct { ID string Gender string Name string Sno string } func main() { // Json字符串转换成结构体 var str = `{\u0026#34;ID\u0026#34;:\u0026#34;12\u0026#34;,\u0026#34;Gender\u0026#34;:\u0026#34;男\u0026#34;,\u0026#34;Name\u0026#34;:\u0026#34;李四\u0026#34;,\u0026#34;Sno\u0026#34;:\u0026#34;s001\u0026#34;}` var s2 = Student{} // 第一个是需要传入byte类型的数据，第二参数需要传入转换的地址 err := json.Unmarshal([]byte(str), \u0026amp;s2) if err != nil { fmt.Printf(\u0026#34;转换失败 \\n\u0026#34;) } else { fmt.Printf(\u0026#34;%#v \\n\u0026#34;, s2) } } 注意 我们想要实现结构体转换成字符串，必须保证结构体中的字段是公有的，也就是首字母必须是大写的，这样才能够实现结构体 到 Json字符串的转换。\n结构体标签Tag Tag是结构体的元信息，可以在运行的时候通过反射的机制读取出来。Tag在结构体字段的后方定义，由一对反引号包裹起来，具体的格式如下：\n1 key1：\u0026#34;value1\u0026#34; key2：\u0026#34;value2\u0026#34; 结构体tag由一个或多个键值对组成。键与值使用冒号分隔，值用双引号括起来。同一个结构体字段可以设置多个键值对tag，不同的键值对之间使用空格分隔。\n注意事项：为结构体编写Tag时，必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差，一旦格式写错，编译和运行时都不会提示任何错误，通过反射也无法正确取值。例如不要在key和value之间添加空格。\n如下所示，我们通过tag标签，来转换字符串的key\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // 定义一个Student体，使用结构体标签 type Student2 struct { Id string `json:\u0026#34;id\u0026#34;` // 通过指定tag实现json序列化该字段的key Gender string `json:\u0026#34;gender\u0026#34;` Name string `json:\u0026#34;name\u0026#34;` Sno string `json:\u0026#34;sno\u0026#34;` } func main() { var s1 = Student2{ Id: \u0026#34;12\u0026#34;, Gender: \u0026#34;男\u0026#34;, Name: \u0026#34;李四\u0026#34;, Sno: \u0026#34;s001\u0026#34;, } // 结构体转换成Json jsonByte, _ := json.Marshal(s1) jsonStr := string(jsonByte) fmt.Println(jsonStr) // Json字符串转换成结构体 var str = `{\u0026#34;Id\u0026#34;:\u0026#34;12\u0026#34;,\u0026#34;Gender\u0026#34;:\u0026#34;男\u0026#34;,\u0026#34;Name\u0026#34;:\u0026#34;李四\u0026#34;,\u0026#34;Sno\u0026#34;:\u0026#34;s001\u0026#34;}` var s2 = Student2{} // 第一个是需要传入byte类型的数据，第二参数需要传入转换的地址 err := json.Unmarshal([]byte(str), \u0026amp;s2) if err != nil { fmt.Printf(\u0026#34;转换失败 \\n\u0026#34;) } else { fmt.Printf(\u0026#34;%#v \\n\u0026#34;, s2) } } 嵌套结构体和Json序列化反序列化 和刚刚类似，我们同样也是使用的是 json.Marshal()\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 // 嵌套结构体 到 Json的互相转换 // 定义一个Student结构体 type Student3 struct { Id int Gender string Name string } // 定义一个班级结构体 type Class struct { Title string Students []Student3 } func main() { var class = Class{ Title: \u0026#34;1班\u0026#34;, Students: make([]Student3, 0), } for i := 0; i \u0026lt; 10; i++ { s := Student3{ Id: i + 1, Gender: \u0026#34;男\u0026#34;, Name: fmt.Sprintf(\u0026#34;stu_%v\u0026#34;, i + 1), } class.Students = append(class.Students, s) } fmt.Printf(\u0026#34;%#v \\n\u0026#34;, class) // 转换成Json字符串 strByte, err := json.Marshal(class) if err != nil { fmt.Println(\u0026#34;打印失败\u0026#34;) } else { fmt.Println(string(strByte)) } } ","permalink":"https://ktzxy.top/posts/3xuqqj4311/","summary":"12 Go中的结构体","title":"12 Go中的结构体"},{"content":"1. JDBC（Java数据库连接）概述 JDBC（Java DataBase Connectivity, java数据库连接）是一种用于执行 SQL 语句的 Java API。JDBC 是 Java访问数据库的标准规范，作用是可以为不同的关系型数据库提供统一访问方式，它由一组用Java语言编写的接口和类组成。具体的实现类由各大数据库厂商来编写。\nJDBC 需要连接驱动，驱动是两个设备要进行通信，满足一定通信数据格式，数据格式由设备提供商规定，设备提供商为设备提供驱动软件，通过软件可以与该设备进行通信。\n1.1. MySQL 数据库 JDBC 相关资源 MySQL 驱动官网下载地址：https://dev.mysql.com/downloads/connector/j/\n相关文件夹说明：\nsrc 文件夹是源代码 docs 文件夹是 API 2. JDBC 开发 2.1. JDBC 执行流程 使用 JDBC 连接数据库的四个参数：\n用户名 密码 连接字符串：jdbc:mysql://localhost:3306/数据库?参数名=参数值 数据库驱动类，如 MySQL数据库驱动是 com.mysql.jdbc.Driver，8.0版本后驱动类是 com.mysql.cj.jdbc.Driver 2.2. JDBC 开发步骤（以 MySQL 为例） 加载并注册数据库驱动，即告知JVM使用的是哪一个数据库的驱动，通常使用反射技术 Class.forName() 方法将驱动类加入到内存中。 1 Class.forName(\u0026#34;com.mysql.jdbc.Driver\u0026#34;) 通过 DriverManager 连接数据库并获得连接对象 Connection 1 Connection conn = DriverManager.getConnection(url, user, password); 使用JDBC中的类，完成对MySQL数据库的连接\n获得语句执行平台，通过 Connection 对象的 createStatement() 方法获得 Statement 对象。Statement 对象就是对SQL语句的执行者对象。 调用 Statement 对象的 excuteXxx(String sql) 方法发送要执行的SQL语句，并获得执行后数据库返回结果。 处理结果，通过 ResultSet 的 next() 和 getXxx(索引/字段名) 方法获得数据库返回的结果集。 必须关闭释放数据库资源，使用 close() 方法。关闭原则：后开的先关(ResultSet -\u0026gt; Statement -\u0026gt; Connection) 注：一般开发会使用 PreparedStatement 来代替父类 Statement，使用效率和安全性都更高。\nJDBC 访问数据库的基础包，在 JavaSE 中的包。导包注意要选择java.sql包\n2.3. 项目（程序）引入驱动 jar 包（MySQL 的驱动） MySQL 相关jar包：mysql-connector-java-x.x.xx-bin.jar MySQL驱动相关文件夹mysql-connector-java-x.x.xx src文件夹是源代码，docs文件夹是API MySQL 官方驱动下载地址：https://dev.mysql.com/downloads/connector/j/ MySQL maven 依赖 1 2 3 4 5 6 \u0026lt;!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;x.x.xx\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 3. JDBC 的核心 API 使用 3.1. 核心 API 概述 Driver 接口：数据库驱动的接口，由数据库的厂商实现。 DriverManager 类：用来管理和注册数据库驱动的类 Connection 接口：数据库连接对象。 Statement/PrepareStatement 接口：操作数据库 SQL 语句的对象 ResultSet 接口：用来封装数据库返回符合查询条件的结果集 3.2. Driver 接口 每个驱动程序都应该提供一个实现 Driver 接口的类。DriverManager 会试着加载尽可能多的它可以找到的驱动程序，这意味着用户可以通过调用以下程序加载和注册一个驱动程序类对象。\n1 Class.forName(\u0026#34;foo.bah.Driver\u0026#34;); 3.3. DriverManager 类 3.3.1. 类作用 java.sql.DriverManager 类用于管理和注册一组 JDBC 驱动程序的基本服务。\nDriverManager 是一个工厂类，通过它来创建数据库连接。当 JDBC 的 Driver 类被加载进来时，它会自己注册到 DriverManager 类里面，然后会把数据库配置信息传成 DriverManager.getConnection() 方法，DriverManager 会使用注册到它里面的驱动来获取数据库连接，并返回给调用的程序。\n3.3.2. 注册驱动程序方法 1 public static void registerDriver(new Driver()); 注册数据库驱动的静态方法\n1 DriverManager.registerDriver(new com.mysql.jdbc.Driver()); 使用该方法注册驱动会触发驱动注册两次，一般不推荐使用\n正确的注册驱动使用下面的方式：一般使用 Class 类的 forName(\u0026quot;类全名\u0026quot;) 注册驱动，可以解决注册两次的问题\n1 Class.forName(\u0026#34;com.mysql.jdbc.Driver\u0026#34;); 注：从JDK1.6开始，JDBC的版本是4.0版本后，可以不用写注册驱动的代码了，但是为了兼容之前的版本，注册驱动的代码一般都会保留。\n3.3.3. 类常用方法 获取连接对象方式1：通过指定 url（需要连接的数据库地址），用户名和密码得到一个 Connection 接口的实现类。用于连接数据库并获得连接对象。\n1 public static Connection getConnection(String url, String user, String password) throws SQLException; 参数说明：\nurl: 连接数据库字符串 url user: 用户名，比如：root password: 密码，注册时候的密码。比如：123456 获取连接对象方式2：通过一个属性集和 URL，得到一个数据库的连接。实际开发中比较常用\n1 public static Connection getConnection(String url, Properties info) throws SQLException 参数说明：\nurl: 连接数据库字符串url info: 属性集合，用来存储用户名和密码 一般 properties 属性文件存放在 src 文件夹中，先获取当前类的 Class 对象，再使用 Class 类的 getResourceAsStream(\u0026quot;/文件名\u0026quot;) 得到输入流对象，再使用 load() 读取文件\nCode Demo: 使用 Properties 属性文件获取 Connection 对象\n1 2 3 4 5 6 // 创建Properties集合 Properties info = new Properties(); // 加载文件数据到info中 info.load(ConnectionDemo.class.getResourceAsStream(\u0026#34;/jdbc.properties\u0026#34;)); // 连接数据库获得连接对象 Connection conn = DriverManager.getConnection(\u0026#34;jdbc:mysql://10.211.55.3:3306/tempDb\u0026#34;, info); 3.3.4. 连接数据库的 URL 地址格式 URL用于标识数据库的位置，程序员通过URL地址告诉JDBC程序连接哪个数据库，URL的写法为：\n3.3.4.1. url地址格式 格式：\n1 jdbc协议名:子协议://数据库服务器地址或 IP 地址:端口号/数据库名 示例：\n1 jdbc:mysql: [] //localhost:3306/test? 参数名:参数值 1 2 3 4 5 // MySQL写法： jdbc:mysql://localhost:3306/temp // 简写格式（前提：数据库服务器地址是本机，端口号默认是3306） jdbc:mysql:///temp 3.3.4.2. 各类数据库连接字符串 JDBC的URL = 协议名 + 子协议名 + 数据源名\n协议名总是jdbc。 子协议名由JDBC驱动程序的编写者决定。 数据源名也可能包含用户与口令等信息；这些信息也可单独提供。 3.3.4.3. 几种常见的数据库连接 Oracle\n驱动：oracle.jdbc.driver.OracleDriver URL：jdbc:oracle:thin:@machine_name:port:dbname machine_name：数据库所在的机器的名称； port：端口号，默认是1521 MySQL\n驱动：com.mysql.jdbc.Driver URL：jdbc:mysql://machine_name:port/dbname machine_name：数据库所在的机器的名称； port：端口号，默认3306 SQL Server\n驱动：com.microsoft.jdbc.sqlserver.SQLServerDriver URL：jdbc:microsoft:sqlserver://\u0026lt;machine_name\u0026gt;\u0026lt;:port\u0026gt;;DatabaseName=\u0026lt;dbname\u0026gt; machine_name：数据库所在的机器的名称； port：端口号，默认是1433 DB2\n驱动：com.ibm.db2.jdbc.app.DB2Driver URL：jdbc:db2://\u0026lt;machine_name\u0026gt;\u0026lt;:port\u0026gt;/dbname machine_name：数据库所在的机器的名称； port：端口号，默认是5000 3.3.5. 解决JDBC无法连接MySQL数据库的问题 MySQL数据连接时只能使用localhost连接，但不能用IP连接问题的解决方案\n打开cmd窗口，进入MySQL安装的bin目录 执行命令登录数据库,之后会出现一行要你输入密码的 mysql -u root -p 执行以下命令分配新用户： 1 2 # (%) 表示所有ip grant all privileges on *.* to \u0026#39;root\u0026#39;@\u0026#39;%\u0026#39; identified by \u0026#39;root\u0026#39;; 执行完上述命令后用下面的命令刷新权限 1 flush privileges; 之后关闭mysql服务，然后启动mysql服务，大功告成 3.3.6. mysql 8.0+以上版本驱动连接失败 使用 mysql 8.0+ 版本后，一些项目连接数据会报以下的错误：\n1 java.sql.SQLNonTransientConnectionException:Public key Retrieval is not allowed… 此时需要修改数据库的连接，增加如下配置：\nuseSSL=false MySQL 8.0 以上版本不需要建立 SSL 连接的，需要显示关闭 allowPublicKeyRetrieval=true 允许客户端从服务器获取公钥。 serverTimezone=UTC 设置时区，mysql驱动8.0+也要指定时区，不然也会报一些错 1 2 3 4 5 6 7 spring: datasource: url: jdbc:mysql://localhost:3306/test_db?characterEncoding=utf-8\u0026amp;useSSL=false\u0026amp;useUnicode=true\u0026amp;allowPublicKeyRetrieval=true\u0026amp;serverTimezone=UTC driverClassName: com.mysql.cj.jdbc.Driver username: root password: 123456 type: com.alibaba.druid.pool.DruidDataSource # 设置使用的数据源类型 Notes: 如果使用的 mysql 是 8.0+，建议最好把以上三个参数设置下，避免发生一些莫名错误。\n3.4. Connection 接口 3.4.1. 接口作用 用来和数据库建立连接，和获取 Statement 对象\n3.4.2. 常用方法 1 Statement createStatement() throws SQLException; 创建一个 Statement 接口现实类对象来将 SQL 语句发送到数据库。 1 PreparedStatement prepareStatement(String sql); 创建一个 PreparedStatement 对象来将参数化的 SQL 语句发送到数据库。 sql：需要执行的SQL语句，获取 PreparedStatement 对象后直接使用方法 executeUpdate() 或 executeQuery() 进行操作。 1 void close() throws SQLException 立即释放此 Connection 对象的数据库和 JDBC 资源，建议最好在调用 close 方法之前，应用程序显式提交或回滚一个活动事务。 3.5. Statement 接口（操作数据库） 3.5.1. 接口概述 用来发送SQL语句给数据库执行，对数据库进行增删改查操作。通过 Connection 对象的 createStatement() 方法获取 Statement 对象。\n1 public interface Statement extends Wrapper, AutoCloseable CRUD(增删改查操作):\nC: Create R: Read U: Update D: Delete 3.5.2. 常用方法 1 2 3 4 boolean execute(String sql) throws SQLException; boolean execute(String sql, int autoGeneratedKeys) throws SQLException; boolean execute(String sql, int columnIndexes[]) throws SQLException; boolean execute(String sql, String columnNames[]) throws SQLException; 可以执行任意的 SQL 语句。如果查询的结果是一个 ResultSet 对象，此方法返回 true；如果结果不是 ResultSet（比如 insert 或者 update 等语句），此方法则会返回 false。可以通过接口的 getResultSet() 方法来获取 ResultSet 结结果对象，或者通过 getUpdateCount() 方法来获取更新的记录条数。 1 int executeUpdate(String sql) throws SQLException; 只能执行数据库的SQL语句中的 insert、delete、update、drop、create 用于数据库的增删改的操作等 DML、DDL 语句。方法参数与返回值解析如下： sql：表示语句，返回执行成功的数据表行数。 可以执行数据定义语言『DDL(Data Definition Language)』。用于创建，修改，删除数据库、表、列等。如：create，alter，drop等。使用DDL语句不影响行数，即返回值为0。 可以数据操作语言『DML(Data Manipulation Language)』。用来修改、删除、添加数据。如：insert、delete、update。使用DML语句会影响行数，会返回影响行数 建议：一般使用操作数据库的语句，先在图形化客户端运行判断是否有错，再写到 IDE\n1 ResultSet executeQuery(String sql) throws SQLException; 用于数据库SQL语句中的 select 查询操作，返回一个符合条件的结果集，是 ResultSet 接口的现实类。即使查询不到记录返回的 ResultSet 也不会为 null。如果方法传入参数是 insert 或者 update 语句的话，它会抛出错误信息为 “executeQuery method can not be used for update” 的 java.util.SQLException。 1 void close() throws SQLException; 立即释放此 Statement 对象的数据库和 JDBC 资源，而不是等待该对象自动关闭时发生此操作。一般来说，使用完后立即释放资源是一个好习惯，这样可以避免对数据库资源的占用。 注：关闭 Statement 对象时，还将同时关闭其当前的 ResultSet 对象（如果有）。\n3.6. ResultSet 接口（查询/处理结果） 3.6.1. 接口概述 1 public interface ResultSet extends Wrapper, AutoCloseable 通过 Statement 的 executeQuery() 方法查询数据库后会返回一个 ResultSet 对象。java.sql.ResultSet 是一个接口，其作用是用来封装符合查询条件的记录，提供用于从当前行获取列值的方法，可以指定列的索引编号或列的名称。\nResultSet 对象维护了一个游标，指向当前的数据行。可以将 ResultSet 理解成一根指针，开始的时候这个游标默认指向位置在执行查询记录的第一行之前，如果调用了 ResultSet 的 next() 方法游标会下移一行；如果没有更多的数据时，next() 方法会返回 false。因此可以在 for 循环中用它来遍历数据集。\n默认的 ResultSet 是不能更新的，游标也只能往下移，即只能从第一行到最后一行遍历一遍。不过也可以创建可以回滚或者可更新的 ResultSet。\n值得注意的是，当生成 ResultSet 的 Statement 对象要关闭或者重新执行或是获取下一个 ResultSet 的时候，之前的 ResultSet 对象也会自动关闭。\n3.6.2. 常用方法 1 boolean next() throws SQLException; ResultSet 光标最初位于第一行之前。第一次调用 next 方法使第一行成为当前行；第二次调用使第二行成为当前行，依此类推。如果新的当前行有效，则返回 true；如果不存在下一行，则返回 false。 1 Xxx getXxx(列索引号或列字段名称); 可以指定列的索引编号或列的名称，XXX 表示数据类型。列号默认从1开始。如：getString(1);、getInt(\u0026quot;name\u0026quot;); 1 void close() throws SQLException; 即释放此 ResultSet 对象的数据库和 JDBC 资源 3.6.3. getXxx 方法注意事项 如果指针在指向结果集第一行前面时调用 resultSet.getXX() 获取列值，会抛出异常：Before start of result set 如果指针在指向结果集最后一行后面时，调用 resultSet.getXX() 获取列值，会抛出异常：After end of result set 如果查询结果集为空，还调用 resultSet.getXX() 获取列值，会抛出异常：Illegal operation on empty result set 使用完毕以后要关闭结果集 ResultSet，再关闭 Statement，再关闭 Connection 3.6.4. 常用数据类型转换表 SQL 类型 JDBC 对应方法 返回类型 BIT(1) bit(n) getBoolean getBytes() Boolean byte[] TINYINT getByte() Byte SMALLINT getShort() Short Int getInt() Int BIGINT getLong() Long CHAR,VARCHAR,LONGVARCHAR getString() String Text(clob) Blob getClob getBlob() Clob Blob DATE getDate() java.sql.Date TIME getTime() java.sql.Time TIMESTAMP getTimestamp() java.sql.Timestamp 查询乱码的问题：如果汉字查询出现乱码，要将数据库的字符集设置成 UTF-8，因为多数 IDE 默认使用 utf-8 的编码\n3.7. PreparedStatement 接口 3.7.1. SQL 注入的概念 SQL 注入是指，用户输入的内容作为了 SQL 语法的一部分，改变了原有 SQL 语句的含义。\n3.7.2. 接口概述 用于 SQL 语句的发送，是继承 Statement 的子接口，拥有父类所有的功能。可以防止 SQL 注入的问题，比父类更安全。然后可以使用此对象多次高效地执行该语句。\n在使用 Connection 的方法得到 PreparedStatement 对象时，已经定义数据库操作的语句。\n实际开发时使用 PreparedStatement 比较多\n3.7.3. 常用方法 1 int executeUpdate() throws SQLException; 用于增删改的操作，返回影响的行数 在此 PreparedStatement 对象中执行 SQL 语句\n1 ResultSet executeQuery() throws SQLException; 用于查询，返回结果集。在此 PreparedStatement 对象中执行 SQL 查询，并返回该查询结果集封装成的 ResultSet 对象。\n1 void setXxx(int index, Xxx xxx); 将指定参数值 xxx 赋值给第 index 个占位符 ? 。再将此值发送到数据库时，驱动程序将它转换成一个 SQL Xxx类型值。\n3.7.4. 预编译 数据库在接收到 SQL 语句后，需要对词法和语义解析，优化 SQL 语句，制定执行计划等一系列处理，这需要花费一些时间。如果一条 SQL 语句需要反复执行，每次都进行语法检查和优化，显然会造成性能与时间的浪费。\nSQL 预编译，指的是数据库驱动在发送 SQL 语句和参数给 DBMS 之前对 SQL 语句进行编译，这样 DBMS 执行 SQL 时，就不需要重新编译，一次编译、多次运行，省去解析优化等过程。\nJDBC 中使用对象 PreparedStatement 来抽象预编译语句，使用预编译。具体做法是将 SQl 语句中的值用占位符替代，即SQL 语句模板化。预编译阶段可以优化 SQL 的执行，预编译之后的 SQL 多数情况下可以直接执行，DBMS 不需要再次编译，越复杂的 SQL，编译的复杂度将越大，预编译阶段可以合并多次操作为一个操作；同时预编译语句对象可以重复利用，把一个 SQL 预编译后产生的 PreparedStatement 对象缓存下来，下次对于同一个SQL，可以直接使用这个缓存的对象。\n预编译的作用小结：\n预编译阶段可以优化 sql 的执行。预编译之后的 sql 多数情况下可以直接执行，数据库服务器不需要再次编译，可以提升性能。 预编译语句对象可以重复利用。将一个 sql 预编译后产生的 PreparedStatement 对象缓存下来，下次对于同一个 sql，可以直接使用这个缓存的对象。 防止 SQL 注入。使用预编译，对注入的参数将不会再进行 SQL 编译。即后面注入进来的参数系统将不会认为它会是一条 SQL 语句，而默认其是一个参数。 3.7.5. Statement 和 PreparedStatement 的区别 安全性 PreparedStatement 可以防止 SQL 注入问题，安全 Statement 有 SQL注入 的隐患问题，不安全 预编译功能 PreparedStatement 有预编译的功能，在创建对象的时候就提供了 SQL 语句并存储在 PreparedStatement 对象中。在真正执行前，才把参数传递给 SQL 语句，并且可以反复执行。 Statement 没有预编译的功能，创建的时候没有 SQL 语句，执行的时候才提供 SQL 语句 缓存 PreparedStatement 有缓存功能，使用缓存可以提升运行速度。 Statement 没有缓存功能 效率 PreparedStatement 效率高，因为数据库系统对相同的 SQL 语句不会再次编译 Statement 效率低 3.7.6. PreparedStatement 使用步骤 准备要执行的SQL语句，使用 ? 临时代替真实的参数 调用 Connection 对象的 preparedStatement() 方法创建 PreparedStatement 对象，并传递SQL语句 调用 PreparedStatement 对象的 setXxx(int index, Xxx value) 给占位符 ? 使用真实的参数赋值，? 参数序号，从 1 开始 调用 PreparedStatement 对象的 executeUpdate()/executeQuery() 方法，执行SQL语句，不需要再次传递SQL语句。 注：要先使用 setXxx() 方法给占位符 ? 赋值后再调用 executeXxx() 方法\n3.8. 释放资源 需要释放的对象和顺序：ResultSet -\u0026gt; Statement -\u0026gt; Connection Connection 对象释放原则：尽可能晚的打开，尽可能早的释放。因为 Connection 资源比较占用内存的，而且使用频率高，是公共的，稀缺的资源。 释放资源的操作放在 finally 语句块中 3.9. API 示例 3.9.1. 使用 executeUpdate 执行 DML 操作数据库练习代码 Code Demo: 给数据库insert、update、delete create、drop等操作\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; /* * 3.1 使用 JDBC 的开发步骤： * 1. 注册并加载数据库驱动 Class.forName() * 2. 连接数据库，获取连接对象 Connection con = DriverManager.getConnection(); * 3. 创建 Statement对象 Statement stmt = conn.createStatement(); * 4. 使用 Statement对象发送 SQL * 5. 处理结果 * 6. 关闭连接，后开的先关。 */ public class MoonZero { public static void main(String[] args) throws Exception { // 注册数据库驱动 Class.forName(\u0026#34;com.mysql.jdbc.Driver\u0026#34;); // 准备连接数据库字符串 String url = \u0026#34;jdbc:mysql://localhost:3306/tempDb\u0026#34;; String uName = \u0026#34;root\u0026#34;; String pwd = \u0026#34;123456\u0026#34;; // 连接数据库，获取Connection对象 Connection con = DriverManager.getConnection(url, uName, pwd); // 获取Statement对象 Statement sta = con.createStatement(); // 使用Statement对象进行DDL和DML操作,定义一个sql操作语句字符串 String sql; // 创建表 // sql = \u0026#34;create table testss(id int primary key,tname varchar(20) not null);\u0026#34;; // 执行后line=0 // 删除数据表 // sql = \u0026#34;drop table testss;\u0026#34;; // 执行后line=0 // 进行insert操作 // sql = \u0026#34;insert into testss values(1,\u0026#39;试试\u0026#39;);\u0026#34;; // 执行后line=1 // sql = \u0026#34;insert into testss values(2,\u0026#39;再试试\u0026#39;), (3,\u0026#39;最后一插\u0026#39;);\u0026#34;; // 执行后line=2 // 进行delete操作 // sql = \u0026#34;delete from testss where id = 1;\u0026#34;; // 执行后line=1 // 进行update操作 sql = \u0026#34;update testss set tname=\u0026#39;用eclipse改一下\u0026#39; where id = 3;\u0026#34;; // 执行后line=1 operateTable(sta, sql); // 关闭资源，先开后关 sta.close(); con.close(); } // 测试创建数据表方法 public static void operateTable(Statement sta, String sql) throws SQLException { // 执行后line=0 int line = sta.executeUpdate(sql); System.out.println(line); } } 3.9.2. 解析 executeQuery 返回的数据库结果集练习代码 Code Demo: 解析 executeQuery 返回的数据库结果集(执行 DQL 操作)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class MoonZero { public static void main(String[] args) throws Exception { // 数据库注册 Class.forName(\u0026#34;com.mysql.jdbc.Driver\u0026#34;); // 获取数据库Connetion对象 String url = \u0026#34;jdbc:mysql://localhost:3306/tempDb\u0026#34;; String user = \u0026#34;root\u0026#34;; String pwd = \u0026#34;123456\u0026#34;; Connection conn = DriverManager.getConnection(url, user, pwd); // 获得语句执行平台，通过数据库连接对象，获取到SQL语句执行者对象 Statement stat = conn.createStatement(); // 准备sql语句 String sql = \u0026#34;select * from dept\u0026#34;; // 获取查询返回的select查询结果集 ResultSet rs = stat.executeQuery(sql); // 使用循环解析返回的所有内容 while (rs.next()) { System.out.print(rs.getInt(\u0026#34;id\u0026#34;) + \u0026#34; \u0026#34;); System.out.println(rs.getString(\u0026#34;name\u0026#34;)); } // 关闭资源 rs.close(); stat.close(); conn.close(); } } 3.9.3. 自定义数据库工具类练习 Code Demo: 自定义原生JDBC数据库工具类，用来创建连接和关闭资源，用户名和密码使用读取 properties 属性文件的方式\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; /** * 获取数据连接、关闭等操作的工具类 */ public class MooNJDBCUtils { // 数据库驱动字符串 private static final String DRIVER_CLASS = \u0026#34;com.mysql.jdbc.Driver\u0026#34;; // 使用静态代码块，用来加载类时就运行注册的动作 static { try { Class.forName(DRIVER_CLASS); } catch (ClassNotFoundException e) { e.printStackTrace(); } } /** * 获取数据库连接对象 * * @param url 需要连接的数据库地址url字符串 * @return 连接数据库对象 */ public static Connection getConnection(String url) { // 读取jdbc.properties配置文件，获取用户名和密码 Properties pro = new Properties(); try { pro.load(MooNJDBCUtils.class.getResourceAsStream(\u0026#34;/jdbc.properties\u0026#34;)); Connection conn = DriverManager.getConnection(url, pro); return conn; } catch (Exception e) { throw new RuntimeException(e); } } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } close(stmt, conn); } /** * 关闭数据库资源方法 * * @param stmt 数据库操作对象(子类PreparedStatement也可以关闭) * @param conn 数据库的连接对象 */ public static void close(Statement stmt, Connection conn) { if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } close(conn); } /** * 关闭数据库连接对象方法 * * @param conn 数据库的连接对象 */ public static void close(Connection conn) { if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } 保存用户名和密码的 properties 属性文件 jdbc.properties:\n1 2 user=root password=123456 4. JDBC 事务操作 4.1. JDBC 事务处理的分类 事务处理方式主要分为两种：自动处理和手动处理。\n自动处理：每条SQL语句执行后自动提交事务，无法通过回滚撤消操作。\n手动处理：\n先调用 setAutoCommit(false) 开启事务，取消自动提交 在 SQL 执行完后调用 commit() 提交事务；如果出现异常则调用 rollback() 回滚事务。 4.2. Connection 接口与事务处理相关的方法 1 void setAutoCommit(boolean autoCommit) throws SQLException; 将此连接的自动提交模式设置为给定状态。如果连接处于自动提交模式下，则它的所有 SQL 语句将被执行并作为单个事务提交。否则，它的 SQL 语句将聚集到事务中，直到调用 commit 方法或 rollback 方法为止。默认情况下，新连接处于自动提交模式。参数 boolean autoCommit 的取值说明如下： autoCommit 设置为 false，则代表开启事务，后面的 SQL 语句将算成一个事务，直到调用 commit 方法或 rollback 方法为止。 默认情况下，autoCommit 值为 true，是自动提交模式，代表后面每一个 SQL 语句算成一个事务，自动提交事务。 1 void commit() throws SQLException; 提交事务 1 void rollback() throws SQLException; 回滚事务 4.3. JDBC 事务操作基础示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import java.sql.Connection; import java.sql.PreparedStatement; import org.apache.commons.dbutils.DbUtils; import jdbc.C3P0Utils; public class MoonZero { public static void main(String[] args) { // 声明连接对象 Connection conn = null; // 声明操作数据库对象 PreparedStatement ps = null; try { // 定义操作sql语句 String sql = \u0026#34;update users set gender=\u0026#39;改\u0026#39; where id=2;\u0026#34;; // 使用工具类读取配置文件获取连接 conn = C3P0Utils.getConnection(); // 关闭事务自动提交 conn.setAutoCommit(false); // 获取操作对象,对数据库进行更新 ps = conn.prepareStatement(sql); ps.executeUpdate(); DbUtils.closeQuietly(ps); // 手动制造异常 // System.out.println(5 / 0); ps = conn.prepareStatement(\u0026#34;update users set gender=\u0026#39;改\u0026#39; where id=4;\u0026#34;); ps.executeUpdate(); System.out.println(\u0026#34;修改成功\u0026#34;); // 提交事务 DbUtils.commitAndCloseQuietly(conn); } catch (Exception e) { // 事务回滚 DbUtils.rollbackAndCloseQuietly(conn); System.out.println(\u0026#34;修改失败\u0026#34;); e.printStackTrace(); } finally { // 使用DbUtils工具类关闭资源 DbUtils.closeQuietly(ps); } } } 5. JDBC 连接池 5.1. 连接池概述 连接池是一个用来创建和管理数据连接对象的容器。\n连接池的核心思想：连接复用，为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接，当需要建立数据库连接时，只需从“缓冲池”中取出一个连接，使用完毕之后再放回池中。可以通过设定连接池最大连接数来防止系统无限制地与数据库连接，更为重要的是可以通过连接池的管理机制监视数据库的连接的数量、使用情况，为系统开发，测试及性能调整提供依据。\n5.1.1. JDBC 中连接数据的问题 获取连接对象需要消耗比较多的资源，而每次操作都要重新获取新的连接对象，执行一次操作就把连接关闭，这样连接对象的使用率低。而数据库创建连接通常需要消耗相对较多的资源，创建时间也较长。\n使用连接池技术可以避免频繁创建数据库连接对象和销毁连接对象带来的开销。\n5.1.2. 连接池的使用步骤 创建：程序启动时创建连接池(容器)并初始化连接对象。放在一块内存中，这块内存称为连接池。 获取(使用)：直接从连接池中获得一个已经创建好的连接对象来操作数据库 关闭：关闭的时候不是真正关闭连接，而是将连接对象再次放回到连接池中，等待复用。 5.2. DataSource 数据库连接池 API 5.2.1. 数据源(连接池)接口 javax.sql.DataSource 接口表示数据源。只要是实现类实现了该接口的类，就是一个连接池类。\n1 public interface DataSource extends CommonDataSource, Wrapper 5.2.2. 连接池接口常用方法 1 Connection getConnection() throws SQLException; 从数据源（连接池）中获取一个连接对象\n5.2.3. 连接池相关参数 初始连接数：一开始连接池中创建多少个连接对象。 最大连接数：连接池中最多可以有多少个连接对象。 最长等待时间：当一个会话要从连接池中得到连接对象的时候，最长等待多久。 最长空闲时间：当一个连接对象在指定时间内没有被使用时，则回收该连接对象。 5.3. 常用数据库连接池（第三方工具） C3P0 连接池 DBCP 连接池 5.4. C3P0 连接池技术 5.4.1. C3P0 连接池概述 C3P0 是一个开源的第三方 JDBC 连接池工具，它实现了数据源和 JNDI 绑定，支持 JDBC3 规范和 JDBC2 的标准扩展。目前使用它的开源项目有 Hibernate，Spring 等。\n官网下载地址：https://sourceforge.net/projects/c3p0/\n5.4.2. C3P0 连接池的特点 免费开源的连接池技术 很多主流的第三方框架都是使用该连接池技术。比如：Spring 和 Hibernate 框架，默认推荐使用 C3P0 作为连接池实现 5.4.3. c3p0 与 DBCP 区别 dbcp 没有自动回收空闲连接的功能 c3p0 有自动回收空闲连接功能 5.4.4. C3P0 的使用步骤 导入 jar 库 c3p0-0.9.5.2.jar 和 mchange-commons-java-0.2.11.jar。（注：数据库驱动mysql-connector-java-5.1.37-bin.jar也不能少） 创建连接池对象 ComboPooledDataSource 对象 1 ComboPooledDataSource ds = new ComboPooledDataSource(); 设置数据库连接参数（jdbcUrl，user，password，driverClass） 数据库连接url字符串：ds.setJdbcUrl(\u0026quot;jdbc:mysql://localhost:3306/tempDb\u0026quot;); 用户名：ds.setUser(\u0026quot;root\u0026quot;); 密码：ds.setPassword(\u0026quot;123456\u0026quot;); 驱动类全名：ds.setDriverClass(\u0026quot;com.mysql.jdbc.Driver\u0026quot;); 设置连接池参数 初始连接数：一开始获得多少个数据连接对象(默认值：3)，如果设置值少于3，也是默认是3。 1 setInitialPoolSize(int num) 最大连接数：连接池中最多有多少个连接对象可以使用(默认值：15) 1 setMaxPoolSize(int num) 最大等待时间：当某个纯种要获得连接对象时，如果没有可用的连接对象时需要等待的时间，如果超过时间还没有获得，则抛出异常。(默认值：0，就是无限等待) 1 setCheckoutTimeout(毫秒值) 最大空闲回收时间：连接对象空间时长，如果在指定的时长内没有被再次使用，会被真正关闭。(默认值：0，就是无限等待) 1 setMaxIdleTime(秒值) 调用 getConnection() 方法，获取连接对象。 1 Connection conn = ds.getConnection(); 关闭连接对象。不是真正关闭，而是将连接对象放回连接池中。 Code Demo: C3P0创建连接池步骤练习\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import java.sql.Connection; import com.mchange.v2.c3p0.ComboPooledDataSource; public class MoonZero { public static void main(String[] args) { // 创建连接池对象 ComboPooledDataSource ds = new ComboPooledDataSource(); try { // 设置连接参数 ds.setDriverClass(\u0026#34;com.mysql.jdbc.Driver\u0026#34;); ds.setJdbcUrl(\u0026#34;jdbc:mysql://localhost:3306/tempDb\u0026#34;); ds.setUser(\u0026#34;root\u0026#34;); ds.setPassword(\u0026#34;123456\u0026#34;); // 设置连接池参数 // 初始化连接数 ds.setInitialPoolSize(5); // 最大连接数 ds.setMaxPoolSize(10); // 最大等待时间 ds.setCheckoutTimeout(3000); // 最大空余等待时间 ds.setMaxIdleTime(3); // 获取连接 for (int i = 1; i \u0026lt;= 11; i++) { Connection conn = ds.getConnection(); System.out.println(conn); if (i == 5) { // 不是真正关闭，将连接对象放回连接池 conn.close(); } } } catch (Exception e) { e.printStackTrace(); } } } 5.4.5. C3P0 连接池(使用 xml 配置文件加载) 5.4.5.1. 使用配置文件的好处 配置信息和操作数据库代码分离，降低了程序的耦合性。 配置信息不是硬编码到 Java 源码中，后期维护更加方便。 可以使用不同的连接池参数。如：maxPoolSize=10,maxPoolSize=8 可以连接不同的数据库。如：db1,db2 可以连接不同厂商的数据库。如：Oracle 或 MySQL 5.4.5.2. 配置文件的要求 文件名命名要求：c3p0-config.xml 位置要求：放在类路径下，源代码即 src 目录下。 5.4.5.3. C3P0 配置文件的使用方式 方式1： 使用默认配置（default-config）\n1 ComboPooledDataSource ds = new ComboPooledDataSource(); 方式2： 使用命名配置（named-config：name=\u0026quot;配置名\u0026quot;）\n1 ComboPooledDataSource ds = new ComboPooledDataSource(\u0026#34;配置名\u0026#34;); 配置文件的各个属性名，都是以设置方法去掉set后的名字。\nCode Demo : C3P0 配置文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;c3p0-config\u0026gt; \u0026lt;!-- 默认配置 --\u0026gt; \u0026lt;default-config\u0026gt; \u0026lt;!-- 数据库连接信息 --\u0026gt; \u0026lt;property name=\u0026#34;driverClass\u0026#34;\u0026gt;com.mysql.jdbc.Driver\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;jdbcUrl\u0026#34;\u0026gt;jdbc:mysql://localhost:3306/tempDb\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;user\u0026#34;\u0026gt;root\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34;\u0026gt;123456\u0026lt;/property\u0026gt; \u0026lt;!-- 连接池的配置 --\u0026gt; \u0026lt;!-- 连接池初始化连接数 --\u0026gt; \u0026lt;property name=\u0026#34;initialPoolSize\u0026#34;\u0026gt;5\u0026lt;/property\u0026gt; \u0026lt;!-- 连接池最大连接数 --\u0026gt; \u0026lt;property name=\u0026#34;maxPoolSize\u0026#34;\u0026gt;10\u0026lt;/property\u0026gt; \u0026lt;!-- 连接池最大等待时间 --\u0026gt; \u0026lt;property name=\u0026#34;checkoutTimeout\u0026#34;\u0026gt;3000\u0026lt;/property\u0026gt; \u0026lt;!-- 连接池最大空闲时间 --\u0026gt; \u0026lt;property name=\u0026#34;maxIdleTime\u0026#34;\u0026gt;3\u0026lt;/property\u0026gt; \u0026lt;/default-config\u0026gt; \u0026lt;!-- 其它的命名配置 --\u0026gt; \u0026lt;named-config name=\u0026#34;MoonZero-config\u0026#34;\u0026gt; \u0026lt;!-- 数据库连接信息 --\u0026gt; \u0026lt;property name=\u0026#34;driverClass\u0026#34;\u0026gt;com.mysql.jdbc.Driver\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;jdbcUrl\u0026#34;\u0026gt;jdbc:mysql://127.0.0.1:3306/tempDb\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;user\u0026#34;\u0026gt;root\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34;\u0026gt;123456\u0026lt;/property\u0026gt; \u0026lt;!-- 连接池的配置 --\u0026gt; \u0026lt;!-- 连接池初始化连接数 --\u0026gt; \u0026lt;property name=\u0026#34;initialPoolSize\u0026#34;\u0026gt;5\u0026lt;/property\u0026gt; \u0026lt;!-- 连接池最大连接数 --\u0026gt; \u0026lt;property name=\u0026#34;maxPoolSize\u0026#34;\u0026gt;9\u0026lt;/property\u0026gt; \u0026lt;!-- 连接池最大等待时间 --\u0026gt; \u0026lt;property name=\u0026#34;checkoutTimeout\u0026#34;\u0026gt;3000\u0026lt;/property\u0026gt; \u0026lt;!-- 连接池最大空闲时间 --\u0026gt; \u0026lt;property name=\u0026#34;maxIdleTime\u0026#34;\u0026gt;3\u0026lt;/property\u0026gt; \u0026lt;/named-config\u0026gt; \u0026lt;/c3p0-config\u0026gt; Code Demo : C3P0 使用配置文件创建连接池\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import java.sql.Connection; import java.sql.SQLException; import com.mchange.v2.c3p0.ComboPooledDataSource; /* * 关卡1训练案例2 * 1.C3P0 连接池的使用 * 要求：连接池参数和初始化连接数通过配置文件配置。 * 使用 C3P0连接池获得 10个连接对象。 * 要求：分别使用默认配置和命名配置创建连接池对象。再通过连接池对象获得连接对象。 */ public class MoonZero { public static void main(String[] args) { // 使用配置文件创建连接池对象(方式1：使用默认配置(default-config)) // ComboPooledDataSource ds = new ComboPooledDataSource(); // 使用配置文件创建连接池对象(方式2：使用命名配置(named-config:MoonZero-config)) ComboPooledDataSource ds = new ComboPooledDataSource(\u0026#34;MoonZero-config\u0026#34;); // 使用循环获取连接对象 for (int i = 1; i \u0026lt;= 11; i++) { try { Connection conn = ds.getConnection(); System.out.println(i + \u0026#34; = \u0026#34; + conn); // 测试最大连接数，不是真正的关闭，只是放回去连接池 if (i == 5 || i == 9) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } } } 5.4.6. 自定义 C3P0 连接池工具类 Code Demo: 自定义C3P0工具类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import java.sql.Connection; import java.sql.SQLException; import javax.sql.DataSource; import com.mchange.v2.c3p0.ComboPooledDataSource; public class C3P0Utils { // 创建私有静态数据源(连接池对象)成员变量 // src文件夹下有xml配置文件，所以不需要进行数据库设置与连接池设置 private static DataSource ds = new ComboPooledDataSource(); // 创建公有的得到数据源(连接池对象)的方法 public static DataSource getDataSource() { return ds; } // 创建共有的得到连接对象的方法 public static Connection getConnection() { try { return ds.getConnection(); } catch (SQLException e) { throw new RuntimeException(e); } } } 5.5. DBCP 连接池技术 5.5.1. DBCP 连接池概述 DBCP: DataBase Connection Pool 数据库连接池。\nDBCP 是 Apache 旗下组织开发的一款产品，免费开源的，也是 Tomcat 服务器的默认使用连接池\n5.5.2. DBCP 使用步骤 导入 dbcp 相关的 jar 包 目前使用版本 commons-dbcp-1.4.jar\t核心包 commons-pool-1.6.jar\t支持包 注：DBCP 2 compiles and runs under Java 7 only (JDBC 4.1) DBCP 1.4 compiles and runs under Java 6 only (JDBC 4) 创建连接池对象 BasicDataSource 对象 1 BasicDataSource ds = new BasicDataSource(); 设置连接参数（url，username，password，driverClassName） 数据库连接url字符串：ds.setUrl(\u0026quot;jdbc:mysql://localhost:3306/tempDb\u0026quot;); 用户名：ds.setUsername(\u0026quot;root\u0026quot;); 密码：ds.setPassword(\u0026quot;xxx\u0026quot;); 驱动类全名：ds.setDriverClassName(\u0026quot;com.mysql.jdbc.Driver\u0026quot;); 注：与C3P0设置连接参数的方法名有不同\n设置连接池参数（初始连接数，最大连接数，最大等待时间，最大空闲数） 初始化连接数：setInitialSize(int num) 最大连接数：setMaxActive(int num) 超过最大连接数时，最大等待时间：setMaxWait(毫秒值) 最大空闲数：setMaxIdle(int num) 连接池中最多只有指定个数的连接对象空闲。 注：与C3P0设置连接池参数的方法名有不同\n调用 getConnection() 方法，获取连接对象 1 Connection conn = ds.getConnection(); 关闭连接对象。不是真正关闭，而是将连接对象放回连接池中 Code Demo: DBCP连接池技术\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import java.sql.Connection; import org.apache.commons.dbcp.BasicDataSource; public class MoonZero { public static void main(String[] args) { try { // 创建连接池对象 BasicDataSource ds = new BasicDataSource(); // 设置连接参数 ds.setDriverClassName(\u0026#34;com.mysql.jdbc.Driver\u0026#34;); ds.setUrl(\u0026#34;jdbc:mysql://localhost:3306/tempDb\u0026#34;); ds.setUsername(\u0026#34;root\u0026#34;); ds.setPassword(\u0026#34;123456\u0026#34;); // 设置连接池参数 // 初始化连接数 ds.setInitialSize(5); // 最大连接数 ds.setMaxActive(10); // 超过最大连接数时，最大等待时间 ds.setMaxWait(3000); // 最大空闲连接数 ds.setMaxIdle(3); // 获取连接 for (int i = 1; i \u0026lt;= 10; i++) { Connection conn = ds.getConnection(); // 因为 DBCP 返回的 Connection 对象已经重写了 toString()方法， // 为了看到不同的对象，输出 hashCode()方法。 System.out.println(i + \u0026#34; : \u0026#34; + conn.hashCode()); if (i == 5) { // 释放连接(不是真正的关闭连接对象，而是把连接对象放回连接池) conn.close(); } } } catch (Exception e) { e.printStackTrace(); } } } 1 2 3 4 5 6 7 8 9 10 11 输出结果： 1 : 1327763628 2 : 1915503092 3 : 1535128843 4 : 2027961269 5 : 458209687 6 : 458209687 7 : 254413710 8 : 1789447862 9 : 1688376486 10 : 445884362 5.5.3. 使用 Properties 配置文件加载 DBCP 连接池 5.5.3.1. DBCP配置文件要求 文件名命名要求：xxx.properties，一般使用 dbcp.properties 位置要求：放在类路径下，源代码即 src 目录下。 键名：配置文件的键与方法名去掉 set 相同，首字母小写。 注：写键值对时，在\u0026quot;=\u0026ldquo;左右尽量不要有空格\ndbcp.properties : DBCP配置文件示例\n1 2 3 4 5 6 7 8 9 10 # 数据库的连接信息 url=jdbc:mysql://localhost:3306/tempDb username=root password=123456 driverClassName=com.mysql.jdbc.Driver #连接池的配置信息 initailSize=5 maxActive=10 maxWait=3000 maxIdle=7 5.5.3.2. DBCP 使用配置文件加载连接池步骤 创建 Properties 属性文件，配置相关参数 通过类对象的 getResourceAsStream(\u0026quot;/dbcp.properties\u0026quot;) 方法，从类路径下加载文件，以字节流的方式加载。 通过 properties.load(InputStream in) 加载属性文件 通过 BasicDataSourceFactory.createDataSource(Properties prop)，得到 DataSource 连接池对象 通过 DataSource 类对象调用 getConnection() 方法得到连接对象 关闭连接对象 Code Demo: DBCP使用配置文件加载连接池\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import java.sql.Connection; import java.util.Properties; import org.apache.commons.dbcp.BasicDataSource; import org.apache.commons.dbcp.BasicDataSourceFactory; public class MoonZero { public static void main(String[] args) { try { // 创建Properties类对象读取属性文件 Properties pro = new Properties(); pro.load(MoonZero.class.getResourceAsStream(\u0026#34;/dbcp.properties\u0026#34;)); // 创建连接池对象 DataSource ds = BasicDataSourceFactory.createDataSource(pro); // 获取连接 for (int i = 1; i \u0026lt;= 11; i++) { Connection conn = ds.getConnection(); // 因为 DBCP 返回的 Connection 对象已经重写了 toString()方法， // 为了看到不同的对象，输出 hashCode()方法。 System.out.println(i + \u0026#34; : \u0026#34; + conn.hashCode()); if (i == 5) { // 释放连接(不是真正的关闭连接对象，而是把连接对象放回连接池) conn.close(); } } } catch (Exception e) { e.printStackTrace(); } } } 6. DBUtils 工具 6.1. DbUtils 工具概述 DbUtils 是 Apache 组织开发的一个开源 JDBC 工具类库。是一款方便操作数据库的工具。\n使用 DBUtils 能简化 JDBC 操作数据库复杂的代码，同时也不会影响程序的性能。使用需要导入 jar 包：\ncommons-dbutils-x.x.jar 核心包 commons-logging-x.x.x.jar 日志记录包 目前使用的是：commons-dbutils-1.7.jar\n6.2. DbUtils 工具的核心类 6.2.1. DbUtils 类 提供了装载 JDBC 驱动程序、关闭资源和处理事务的相关静态方法\n1 public static void close(…) throws java.sql.SQLException; DbUtils 类提供了三个重载的关闭方法。这些方法检查所提供的参数是不是 NULL，如果不是的话，它们就关闭 Connection、Statement 和 ResultSet。\n1 public static void closeQuietly(…); 这一类方法不仅能在 Connection、Statement 和 ResultSet 为 NULL 情况下避免关闭，还能隐藏一些在程序中抛出的 SQLException。\n6.2.2. QueryRunner 类 用来对数据库执行CRUD(增删改查)操作\n6.2.2.1. QueryRunner 增删改操作方式1：传入数据源对象 构造方法\n1 QueryRunner(DataSource ds); 根据数据源创建查询器对象，方法参数：\nds: 连接池对象，数据源 增删改的方法\n1 2 3 int update(String sql); int update(String sql, Object param); int update(String sql, Object...params); 执行增删改操作，返回影响的行数。\n方法参数：\nsql：需要执行的sql语句 params：实际参数(真实参数)，给sql语句中的占位符赋值 注：以上方法在内部都有释放资源的代码，所以无需关闭连接等操作\n6.2.2.2. QueryRunner 增删改操作方式2：没有传入任何对象 构造方法\n1 QueryRunner(); 创建查询器对象\n增删改的方法，在方法中指定 Connection 对象\n1 2 int update(Connection conn, String sql); int update(Connection conn, String sql, Object...params); 参数说明：\nconn: 数据库连接对象 sql: 需要执行的sql语句 params: 实际参数(真实参数)，给sql语句中的占位符赋值 注：以上方法没有释放资源的代码，需要操作者手动关闭连接\n6.2.2.3. QueryRunner 查询操作方式1：没有连接对象 构造方法\n1 QueryRunner(DataSource ds); 根据数据源创建查询器对象。\n参数说明：\nds: 连接池对象，数据源 1 2 Object query(String sql, ResultSetHandler rsh) Object query(String sql, ResultSetHandler rsh, Object... params) 注：以上方法在内部都有释放资源的代码，所以无需关闭连接等操作\n6.2.2.4. QueryRunner 查询操作方式2：有连接对象，需要手动关闭资源 构造方法\n1 QueryRunner(); 创建查询器对象\n1 2 Object query(Connection conn, String sql, ResultSetHandler rsh) Object query(Connection conn, String sql, ResultSetHandler rsh, Object... params) 注：以上方法没有释放资源的代码，需要操作者手动关闭连接\n6.2.2.5. QureyRunner 的操作多个数据方法 1 int[] batch(String sql, Object[][] params) 用于同一个sql语句进行执行多次的方法。\n参数说明：\nparams：二维数组 一维：sql语句要执行多次 二维：就是每条sql语句中?存储的占位符的参数，二维长度是?参数的个数 注：批量处理，是访问数据库一次，一次性执行重复多个sql语句处理，这样可以减少数据库访问次数的压力(如果使用逐条删除的方式，每次删除都访问数据库一次)。\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /** * 批量删除商品 * * @param pids 商品id数组 */ public void delProductBatch(String[] pids) { // 删除数据的sql语句，只需要将删除单个数据的语句执行多次，每次根据不同的id删除即可 String sql = \u0026#34;delete from product where pid=?;\u0026#34;; // DBUtils.runner.batch(sql, params),用于同一个sql语句进行执行多次的方法 // params 二维数组 // 一维：sql语句要执行多少次 // 二维：就是每个sql语句中？存储的占位符的值 // 创建二维数组,长度是传入数组的长度，每个元素数组长度是1 Object[][] params = new Object[pids.length][1]; // 使用循环给二维数组赋值 for (int i = 0; i \u0026lt; params.length; i++) { params[i][0] = pids[i]; } // 使用查询器方法操作多次操作 try { qr.batch(sql, params); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException(e); } } 6.2.3. ResultSetHandler 接口 用来定义如何封装查询结果集\n6.2.3.1. 接口的方法 1 Object handle(ResultSet rs); 方法调用时机：调用query(String sql, ResultSetHandler rsh);方法查询到结果之后会触发结果集处理器对象的该方法，按需要重写方法返回自定义的结果。\n方法内如何处理由操作者定义。当DbUtils提供的常用实现类不能满足要求的时，再定义匿名内部类重写该方法\nCode Demo: 实现 ResultSetHandler 接口，重写 handle 方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import javax.sql.DataSource; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.ResultSetHandler; import jdbc.C3P0Utils; /* * 关卡1训练案例10 * 查询用户表的所有用户数据，要求如下： * 1.只查询用户名和性别两个字段信息。 * 2.查询结果是一个集合，集合中存放所有的用户对象。 * 操作步骤 * 1. 通过 C3P0Utils 工具类获得数据源对象 * 2. 根据数据源对象创建 QueryRunner 对象 * 3. 编写查询的 SQL 语句。 * 4. 调用 QueryRunner 的对象的 query 方法进行查询 * 5. 获得查询结果。 */ public class QueryRunnerTest { public static void main(String[] args) { // 使用c3p0工具类获取数据源对象 DataSource ds = C3P0Utils.getDataSource(); // 获取QueryRunner对象 QueryRunner qr = new QueryRunner(ds); // 准备sql语句 String sql = \u0026#34;select name,gender from users;\u0026#34;; // 调用query文件进行查询，重写handle方法，返回一个对象集合 try { List\u0026lt;User\u0026gt; list = qr.query(sql, new ResultSetHandler\u0026lt;List\u0026lt;User\u0026gt;\u0026gt;() { @Override public List\u0026lt;User\u0026gt; handle(ResultSet rs) throws SQLException { // 创建集合用来存放对象 List\u0026lt;User\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); // 使用循环记取数据库返回的结果集 while (rs.next()) { // 创建用户对象 User u = new User(); u.setName(rs.getString(\u0026#34;name\u0026#34;)); u.setGender(rs.getString(\u0026#34;gender\u0026#34;)); list.add(u); } return list; } }); // 遍历集合 for (User u : list) { System.out.println(u); } } catch (SQLException e) { e.printStackTrace(); } } } 6.2.4. 常用的 ResultSetHandler 接口的实现类 6.2.4.1. 封装成 JavaBean (BeanHandler / BeanListHandler) 前提：表的列名与 JavaBean 属性名要相同\n1 T BeanHandler\u0026lt;T\u0026gt;(Class clazz); 把结果集的一行数据封装成 JavaBean。常用于查询一条记录的情况。如果SQL语句是查询多个记录，则返回查询到的第一行记录。\n1 List\u0026lt;T\u0026gt; BeanListHandler\u0026lt;T\u0026gt;(Class clazz); 把结果集的每一行数据封装成 JavaBean，把这个 JavaBean 放入 List 中返回\nCode Demo:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 import java.sql.SQLException; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanHandler; import day25.level01.User; import jdbc.DBCPUtils; /* * 关卡2训练案例3 * 查询用户表中的第一条用户记录并将该记录封装成一个 JavaBean 对象。 * 注意事项：JavaBean 属性名和用户表的列名要相同。 */ public class Test02_03 { public static void main(String[] args) throws SQLException { // 使用工具类得到数据源对象,并创建QueryRunner对象 QueryRunner qr = new QueryRunner(DBCPUtils.getDataSource()); // 准备sql语句 String sql = \u0026#34;select * from users\u0026#34;; // 准备好bean对象，属性名与用户表列相同，调用query方法封装JavaBean对象 User u = qr.query(sql, new BeanHandler\u0026lt;\u0026gt;(User.class)); // 输入bean对象 System.out.println(u); } } import java.sql.Connection; import java.sql.SQLException; import java.util.List; import org.apache.commons.dbutils.DbUtils; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanListHandler; import day25.level01.User; import jdbc.DBCPUtils; /* * 关卡2训练案例4 * 1.查询用户表的所有用户记录并将每一条记录封装成 JavaBean 对象存放到集合中。 */ public class Test02_04 { public static void main(String[] args) throws SQLException { // 创建无参QueryRunner对象 QueryRunner qr = new QueryRunner(); // 准备sql语句 查询用户表全部记录 String sql = \u0026#34;select * from users;\u0026#34;; // 使用工具类获取连接对象，创建BeanListHandler对象，执行sql语句 Connection conn = DBCPUtils.getConnection(); List\u0026lt;User\u0026gt; list = qr.query(conn, sql, new BeanListHandler\u0026lt;\u0026gt;(User.class)); // 遍历集合将Bean对象输出 for (User u : list) { System.out.println(u); } // 使用DbUtils工具类关闭资源 DbUtils.closeQuietly(conn); } } 6.2.4.2. 封装成 Map (MapHandler / MapListHandler) 可用于表连接查询的时候\n1 Map\u0026lt;String, Object\u0026gt; MapHandler(); 将结果集中的第一行数据封装到一个 Map 里，key 是列名，value 就是对应的值。\n1 List\u0026lt;Map\u0026lt;String, Object\u0026gt;\u0026gt; MapListHandler(); 将结果集中的每一行数据都封装到一个 Map 里，然后再存放到 List\nCode Demo:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import java.sql.Connection; import java.sql.SQLException; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.dbutils.DbUtils; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.MapHandler; import org.apache.commons.dbutils.handlers.MapListHandler; import jdbc.DBCPUtils; /* * 关卡2训练案例6 * 1.定义一个方法：查询用户表获取第一条用户记录并封装成 Map 集合(key 是字段名称，value是字段值)。 * 2.定义一个方法：查询用户表获取所有用户记录并返回一个集合，集合中存放的都是 Map 对象， * 一个 Map 对象封装对应一个用户记录。 */ public class Test02_06 { public static void main(String[] args) throws SQLException { // 创建无参的QueryRunner对象 QueryRunner qr = new QueryRunner(); // 定义执行的sql操作语句 String sql = \u0026#34;select * from users\u0026#34;; // 查询用户表第一个用户记录并封装成map集合 testMapHandler(qr, sql); System.out.println(\u0026#34;**********************\u0026#34;); // 查询用户表获取所有用户记录并返回一个集合，集合中存放的都是 Map对象 testMapListHandler(qr, sql); } public static void testMapListHandler(QueryRunner qr, String sql) throws SQLException { // 使用工具类获取Connection对象 Connection conn = DBCPUtils.getConnection(); // 创建MapHandler对象，执行sql语句 List\u0026lt;Map\u0026lt;String, Object\u0026gt;\u0026gt; list = qr.query(conn, sql, new MapListHandler()); // 遍历List集合 for (Map\u0026lt;String, Object\u0026gt; map : list) { Set\u0026lt;String\u0026gt; key = map.keySet(); for (String k : key) { System.out.println(k + \u0026#34; = \u0026#34; + map.get(k)); } System.out.println(\u0026#34;==========\u0026#34;); } // 使用DbUtils工具类关闭资源 DbUtils.closeQuietly(conn); } public static void testMapHandler(QueryRunner qr, String sql) throws SQLException { // 使用工具类获取Connection对象 Connection conn = DBCPUtils.getConnection(); // 创建MapHandler对象，执行sql语句 Map\u0026lt;String, Object\u0026gt; map = qr.query(conn, sql, new MapHandler()); // 遍历Map集合 Set\u0026lt;String\u0026gt; keySet = map.keySet(); for (String key : keySet) { System.out.println(key + \u0026#34; = \u0026#34; + map.get(key)); } // 使用DbUtils工具类关闭资源 DbUtils.closeQuietly(conn); } } 6.2.4.3. 封装成数组(ArrayHandler / ArrayListHandler) 1 Object[] ArrayHandler(); 把结果集的第一行数据封装成对象数组。(常用于只有一条记录的情况)\n1 List\u0026lt;Object[]\u0026gt; ArrayListHandler(); 把结果集的每一行数据封装对象数组，把这个对象数组放入 List 中\nCode Demo:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 import java.sql.Connection; import java.sql.SQLException; import java.util.Arrays; import java.util.List; import org.apache.commons.dbutils.DbUtils; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.ArrayHandler; import org.apache.commons.dbutils.handlers.ArrayListHandler; import jdbc.DBCPUtils; /* * 关卡2训练案例1 * 查询用户表中的第一条数据。并将数据封装成对象数组 * 操作步骤 * 1.通过 C3P0Utils 工具类获得数据源对象 * 2.创建 QueryRunner 对象 * 3.编写 SQL 语句 * 4.调用 QueryRunner 对象的 query 方法传入 SQL 语句和 ArrayHandler 对象 * 5.接收方法返回值即对象数组。 */ public class Test02_01 { public static void main(String[] args) throws SQLException { // 准备sql语句 String sql = \u0026#34;select * from users\u0026#34;; // 使用DBCP工具类获取数据源，并创建QueryRunner对象 QueryRunner qr = new QueryRunner(DBCPUtils.getDataSource()); // 使用ArrayHandler获取第一行数据并封装成对象数组 Object[] arr = qr.query(sql, new ArrayHandler()); // 直接输出数组 System.out.println(Arrays.toString(arr)); System.out.println(\u0026#34;================\u0026#34;); // 使用无参构造方法创建QueryRunner对象 QueryRunner qr2 = new QueryRunner(); // 使用工具类获取Connection对象，传入query方法执行sql语句, Connection conn = DBCPUtils.getConnection(); // 使用ArrayListHandler返回一个对象数组集合 List\u0026lt;Object[]\u0026gt; arr2 = qr2.query(conn, sql, new ArrayListHandler()); // 使用DbUtils方法关闭资源 DbUtils.closeQuietly(conn); // 遍历对象数组集合 for (Object[] objs : arr2) { System.out.println(Arrays.toString(objs)); } } } 6.2.4.4. 封装单行单列数据 (ScalarHandler) 1 T ScalarHandler\u0026lt;T\u0026gt;(); 把结果集的第一行第一列取出。通常用于只有单行单列的聚合函数查询查询结果集。\n注：用来统计数量是时返回的数据类型是long\nCode Demo:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import java.sql.Connection; import java.sql.SQLException; import org.apache.commons.dbutils.DbUtils; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.ScalarHandler; import jdbc.DBCPUtils; /* * 关卡2训练案例4 * 查询用户表中用户记录的数量。 */ public class Test02_04 { public static void main(String[] args) throws SQLException { // 创建无参QueryRunner对象 QueryRunner qr = new QueryRunner(); // 准备sql语句 查询用户表全部记录 String sql = \u0026#34;select COUNT(*) from users;\u0026#34;; // 使用工具类获取连接对象，创建ScalarHandler对象统计用户数量，执行sql语句 Connection conn = DBCPUtils.getConnection(); long count = qr.query(conn, sql, new ScalarHandler\u0026lt;Long\u0026gt;()); System.out.println(\u0026#34;用户数量是：\u0026#34; + count); // 使用DbUtils工具类关闭资源 DbUtils.closeQuietly(conn); } } 6.2.4.5. 封装多行单列数据 (ColumnListHandler) 1 List\u0026lt;T\u0026gt; ColumnListHandler\u0026lt;T\u0026gt;(); 只封装一列的时候，将这一列的数据封装成 List 集合，集合中的元素类型与列的类型相同。其中 new ColumnListHandler\u0026lt;String\u0026gt;(\u0026quot;列名\u0026quot;)。通常用于多行单列的查询结果集。\n如果查询多列的话，只默认返回第一列\nCode Demo:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import java.sql.Connection; import java.sql.SQLException; import java.util.List; import org.apache.commons.dbutils.DbUtils; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.ColumnListHandler; import jdbc.C3P0Utils; /* * 关卡2训练案例5 * 1.定义一个方法，查询用户表，获得所有用户的名字存放到集合中。(ColumnListHandler) */ public class Test02_05 { public static void main(String[] args) throws SQLException { // 创建QueryRunner无参对象 QueryRunner qr = new QueryRunner(); // 准备sql语句 // 查询用户表中所有用户名 String sql = \u0026#34;select name from users;\u0026#34;; // 使用工具类获取连接对象，创建ColumnListHandler，获取所有用户名集合 Connection conn = C3P0Utils.getConnection(); List\u0026lt;String\u0026gt; list = qr.query(conn, sql, new ColumnListHandler\u0026lt;String\u0026gt;()); // 遍历集合 for (String s : list) { System.out.println(s); } // 使用DbUtils工具类关闭资源 DbUtils.closeQuietly(conn); } } 6.2.4.6. KeyedHandler 1 Map\u0026lt;String, Map\u0026lt;String, Object\u0026gt;\u0026gt; KeyedHandler\u0026lt;K\u0026gt;(String s); 将多条记录封装成一个 Map，取其中的一列做为键，记录本身做为值，这个值是 Map 集合，封装这一条记录。即：Map\u0026lt;某列类型,Map\u0026lt;字段名,字段值\u0026gt;\u0026gt;，其中 KeyedHandler 指定为\u0026lt;某列的类型\u0026gt;(列名)。\n不指定，默认以第1列的值做为键。一般指定唯一的值的列做为列\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import java.sql.Connection; import java.sql.SQLException; import java.util.Map; import java.util.Set; import org.apache.commons.dbutils.DbUtils; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.KeyedHandler; import jdbc.C3P0Utils; /* * 关卡2训练案例5 * 2.定义一个方法，查询用户表，获得所有用户的所有信息，返回一个 Map 集合(Map 集合的 * key 是用户 id，value 是每一个用户的信息，也是一个 Map 集合。) */ public class Test02_05 { public static void main(String[] args) throws SQLException { // 创建QueryRunner无参对象 QueryRunner qr = new QueryRunner(); // 准备sql语句 // 查询用户表中所有用户,封装成一个Map集合 String sql = \u0026#34;select * from users;\u0026#34;; // 使用工具类获取连接对象，创建KeyedHandler，获取所有用户Map集合 Connection conn = C3P0Utils.getConnection(); Map\u0026lt;Integer, Map\u0026lt;String, Object\u0026gt;\u0026gt; map = qr.query(conn, sql, new KeyedHandler\u0026lt;Integer\u0026gt;(\u0026#34;id\u0026#34;)); // 遍历Map集合 Set\u0026lt;Integer\u0026gt; set = map.keySet(); for (Integer key : set) { System.out.println(key + \u0026#34; = \u0026#34; + map.get(key)); } // 使用DbUtils工具类关闭资源 DbUtils.closeQuietly(conn); } } 6.3. DbUtils 事务操作 6.3.1. DbUtils 事务处理方式 自动提交：每条SQL语句执行后自动提交事务。无法通过回滚撤消操作。\n手动提交：\n创建 Connection 对象和 QueryRunner 对象，QueryRunner 对象不能使用数据源。 先调用 conn.setAutoCommit(false); 开启事务，取消自动提交。 在 SQL 执行完后调用 commitAndCloseQuietly(conn); 提交事务，如果出现异常则调用 rollbackAndCloseQuietly(conn); 回滚事务。 6.3.2. 与事务处理相关的方法（待修改完善） 1 conn.setAutoCommit(false); 禁止自动提交事务，开启事务 1 new QueryRunner(); 创建核心类，不传数据源(手动管理连接) 1 2 query(conn , sql , handler, params); update(conn, sql , params); 手动传递连接执行查询或更新的操作 1 2 DbUtils.commitAndClose(conn); DbUtils.commitAndCloseQuietly(conn); 提交并关闭连接 1 2 DbUtils.rollbackAndClose(conn); DbUtils.rollbackAndCloseQuietly(conn); 回滚并关闭连接 7. JPA（Java Persistence API） 7.1. 概述 JPA 是 Java Persistence API 的简称，中文名 Java 持久层 API，是 JDK 5.0 注解或 XML 描述对象－关系表的映射关系，并将运行期的实体对象持久化到数据库中。\n7.2. Persistence 类 Persistence 类主要是用于读取配置文件，获得实体管理工厂。常用方法如下所示：\n1 public static EntityManagerFactory createEntityManagerFactory(String persistenceUnitName) 获得实体管理工厂。参数 String persistenceUnitName：配置文件中的 persistenceUnitName 示例：\n1 EntityManagerFactory emf = Persistence.createEntityManagerFactory(\u0026#34;crm\u0026#34;); 7.3. EntityManagerFactory 接口 用于管理数据库的连接，获得操作对象实体管理类 EntityManager。EntityManagerFactory 是一个线程安全的对象，并且其创建极其浪费资源，所以编程的时候要保持它是单例的。常用方法如下所示：\n1 public EntityManager createEntityManager(); 获取操作对象实体管理类 示例：\n1 2 EntityManagerFactory emf = Persistence.createEntityManagerFactory(\u0026#34;crm\u0026#34;); EntityManager entityManager = emf.createEntityManager(); 7.4. EntityManager 接口 在 JPA 规范中，EntityManager 实体管理类是操作数据库的重要 API，它是线程不安全的，需要保持线程独有。常用方法如下所示：\n1 public EntityTransaction getTransaction(); 获取事务对象，但没有开启事务 1 public void persist(Object entity); 插入数据 1 public void remove(Object entity); 根据对象的主键id删除数据。注意：使用JPA删除数据也必须使用持久化对象，即要先查询数据，再删除 1 public \u0026lt;T\u0026gt; T merge(T entity); 如果数据库没有记录就保存，如果有记录就更新，重要的判断是 OID 是否相同，OID（Object ID）就是在配置文件配置为 \u0026lt;id\u0026gt; 属性。 1 public \u0026lt;T\u0026gt; T find(Class\u0026lt;T\u0026gt; entityClass, Object primaryKey); 用于通过OID，获得一条记录，有延迟。相当于get() 1 public \u0026lt;T\u0026gt; T getReference(Class\u0026lt;T\u0026gt; entityClass, Object primaryKey); 用于通过OID，获得一条记录，无延迟。相当于load() 1 public Query createQuery(String qlString); 获取 JPQL 操作对象，参数是操作 HQL 语句，用于删除和更新 1 public \u0026lt;T\u0026gt; TypedQuery\u0026lt;T\u0026gt; createQuery(String qlString, Class\u0026lt;T\u0026gt; resultClass); 获取 JPQL 操作对象，用于查询操作 7.5. EntityTransaction 接口 EntityTransaction 接口用于管理事务（开始，提交，回滚）。获取事务（没有开启事务）：\n7.5.1. 获取实例 1 2 3 EntityManagerFactory emf = Persistence.createEntityManagerFactory(\u0026#34;crm\u0026#34;); EntityManager em = emf.createEntityManager(); EntityTransaction transaction = em.getTransaction(); 7.5.2. 常用方法 1 public void begin(); 开启事务 1 public void commit(); 提交事务 1 public void rollback(); 回滚事务 7.6. TypedQuery 接口 TypedQuery 接口继承 Query 接口。用于操作 JPQL 的查询的。JPQL 和 HQL 一样。为什么 JPA 的标准，查询需要指定类型，目的就是为了让返回的数据没有没有警告。\n7.6.1. 获取实例 1 2 EntityManager em = xxx; TypedQuery\u0026lt;Xxx\u0026gt; query = em.createQuery(\u0026#34;xxx\u0026#34;, Xxx.class); 7.6.2. 常用方法 1 int executeUpdate(); 执行查询操作（好像不用）。此方法继承于 Query 接口 1 TypedQuery\u0026lt;X\u0026gt; setParameter(int position, Object value); 设置 ? 参数的值。 int position：是 ? 占位符后面指定的下标 Object value：占位符的值 1 TypedQuery\u0026lt;X\u0026gt; setParameter(String name, Object value); 设置命名参数的值。 String name：命名参数的名字，不带 : 号 Object value：命名参数的值 1 TypedQuery\u0026lt;X\u0026gt; setFirstResult(int startPosition); 设置分页查询的起始数 int startPosition：查询语句 limit 的起始数 1 TypedQuery\u0026lt;X\u0026gt; setMaxResults(int maxResult); 设置分页查询的数量 int maxResult：分页查询每页大小 1 List\u0026lt;X\u0026gt; getResultList(); 返回查询的结果List集合（查询所有的数据） 1 X getSingleResult(); 返回查询的结果是一条数据，常用聚合函数 count()，相当于 uniqueResult() 7.7. Query 接口 用于操作SQL的查询接口，执行没有返回数据的JPQL（增删改），用于删除和更新\n7.7.1. 获取实例 1 2 EntityManager em = xxx; Query query = em.createQuery(\u0026#34;xxx\u0026#34;); 7.7.2. 常用方法 1 int executeUpdate(); 执行删除和修改的操作 1 Query setParameter(int position, Object value); 设置 ? 参数的值。 int position：是 ? 占位符后面指定的下标 Object value：占位符的值 1 Query setParameter(String name, Object value); 设置命名参数的值。 String name：命名参数的名字，不带 : 号 Object value：命名参数的值 7.8. CriteriaBuilder 接口 用户使用标准查询接口 Criteria 查询接口\n","permalink":"https://ktzxy.top/posts/3qgconana2/","summary":"Java基础 数据库编程","title":"Java基础 数据库编程"},{"content":"日志收集项目架构设计及Kafka介绍 项目背景 每个业务系统都有日志，当系统出现问题时，需要通过日志信息来定位和解决问题。当系统机器比较少时，登陆到服务器上查看即可满足当系统机器规模巨大，登陆到机器上查看几乎不现实（分布式的系统，一个系统部署在十几台机器上）\n解决方案 把机器上的日志实时收集，统一存储到中心系统。再对这些日志建立索引，通过搜索即可快速找到对应的日志记录。通过提供一个界面友好的web页面实现日志展示与检索。\n面临的问题 实时日志量非常大，每天处理几十亿条。日志准实时收集，延迟控制在分钟级别。系统的架构设计能够支持水平扩展。\n业界方案 ELK AppServer：跑业务的服务器 Logstash Agent： Elastic Search Cluster Kibana Server：数据可视化 Browser：浏览器 ELK方案的问题 运维成本高，每增加一个日志收集项，都需要手动修改配置 使用etcd来管理被收集的日志项 监控缺失，无法精准获取logstash的状态 无法做到定制化开发与维护 日志收集系统架构设计 架构设计 通过etcd做一个配置中心的概念，它是用go写的，是可以用来替代Zookeeper的\nLogAgent收集日志，然后将其发送到Kafka中，Kafka既可以作为消息队列，也可以做到消息的存储组件\n然后Log transfer就将Kafka中的日志记录取出来，进行处理，然后写入到ElasticSearch中，然后将对应的日志\n最后通过Kibana进行可视化展示，SysAgent是用来采集系统的日志信息（或者使用 普罗米修斯）\n组件介绍 LogAgent：日志收集客户端，用来收集服务器上的日志 Kafka：高吞吐量的分布式队列（Linkin开发，Apache顶级开源项目） ElasticSearch：开源的搜索引擎，提供基于HTTP RESTful的web接口 Kibana：开源的ES数据分析和可视化工具 Hadoop：分布式计算框架，能够对大量数据进行分布式处理的平台 Storm：一个免费并开源的分布式实时计算框架 将学到的技能 服务端agent开发 后端服务组件开发 Kafka和Zookeeper的使用 ES和Kibana使用 etcd的使用（配置中心，配置共享） 消息队列的通信模型 点对点模式 queue 消息生产者发送到queue中，然后消息消费者从queue中取出并消费信息，一条消息被消费以后，queue中就没有了，不存在重复消费的问题\n发布/订阅 topic 消息生产者（发布）将消息发布到topic中，同时有多个消息消费者（订阅）消费该消息。和点对点方式不同，发布到topic的消息会被所有的订阅者消费（类似于关注了微信公众号的人都能收到推送的文章）。\n补充：发布订阅模式下，当发布者消息量很大时，显然单个订阅者的处理能力是不足的。实际上现实场景中多个订阅者节点组成一个订阅组负载均衡消费topic消息即分组订阅，这样订阅者很容易实现消费能力线扩展。可以看成是一个topic下有多个Queue，每个Queue是点对点的方式，Queue之间是发布订阅方式。\nKafka Apache Kafka由著名职业社交公司Linkedin开发，最初是被设计用来解决LinkedIn公司内部海量日志传输问题，Kafka使用Scala语言编写，于2011年开源并进入Apache孵化器，2012年10月正式毕业，现在为Apache顶级项目\n介绍 Kafka是一个分布式数据流平台，可以运行在单台服务器上，也可以在多台服务器上部署形成集群。它提供了发布和订阅功能，使用这可以发送数据到Kafka中，也可以从Kafka中读取数据（以便进行后续处理）。Kafka具有高吞吐、低延迟、高容错等特点。\nKafka的架构图 Producer:Producer即生产者，消息的产生者，是消息的入口。 kafka cluster:kafka集群，一台或多台服务器组成 Broker:Broker是指部署了Kafka实例的服务器节点。每个服务器上有一个或多个kafka的实例，我们姑且认为每个broker对应一台服务器。每个kafka集群内的broker都有一个不重复的编号，如图中的broker-0、broker-1等… Topic：消息的主题，可以理解为消息的分类，kafka的数据就保存在topic。在每个broker上都可以创建多个topic。实际应用中通常是一个业务线建一个topic。 Partition:Topic的分区，每个topic可以有多个分区，分区的作用是做负载，提高kafka的吞吐量。同一个topic在不同的分区的数据是不重复的，partition的表现形式就是一个一个的文件夹！ Replication：每一个分区都有多个副本，副本的作用是做备胎。当主分区（Leader）故障的时候会选择一个备胎（Follower）上位，成为Leader。在kafka中默认副本的最大数量是10个，且副本的数量不能大于Broker的数量，follower和leader绝对是在不同的机器，同一机器对同一个分区也只可能存放一个副本（包括自己）。 Consumer：消费者，即消息的消费方，是消息的出口。 工作流程 我们看上面的架构图中，produce就是生产者，是数据的入口。Producer在写入数据的时候会把数据写入到Leader中，不会直接将数据写入follower！那leader怎么找呢？写入流程又是怎么样的呢？我们看下图\n生产者从Kafka集群获取分区leader信息 生产者将消息发送给leader leader将消息写入本地磁盘 follower从leader拉取消息数据 follower将消息写入本地磁盘后向leader发送ACK leader收到所有的follower的ACK之后向生产者发送ACK 选择partition的原则 那在kafka中，如果某低opic有多个partition，producer又怎么知道该将数据发往哪个partition呢？ kafka中有几个原则：\npartition在写入的时候可以指定需要写入的partition，如果有指定，则写入对应的partition。 如果没有指定partition，但是设置了数据的key，则会根据key的值hash出一个partition。 如果既没指定partition，又没有设置key，则会采用轮询方式，即每次取一小段时间的数据写入某个partition，下一小段的时间写入下一个partition。 ACK应答机制 producer在向kafka写入消息的时候，可以设置参数来确定是否确认kafka接收到数据，这个参数可设置的值为0、1、all。\n0：代表producer往集群发送数据不需要等到集群的返回，不确保消息发送成功。安全性最低但是效I率最高。 1：代表producer往集群发送数据只要leader应答就可以发送下一条，只确保leader发送成功。 all：代表producer往集群发送数据需要所有的follower都完成从leader的同步才会发送下一条，确保leader发送成功和所有的副本都完成备份。安全性最高，但是效率最低。 最后要注意的是，如果往不存在的topic写数据，kafka会自动创建topic，partition和replication的数量默认配置都是1。\nTopic和数据日志 topic是同一类别的消息记录（record）的集合。在kafka中，一个主题通常有多个订阅者。对于每个主题、Kafka集群维护了一个分区数据日志文件结构如下：\n每个partition都是一个有序并且不可变的消息记录集合。当新的数据写入时，就被追加到partition的末尾。在每个partition中，每条消息都会被分配一个顺序的唯一标识，这个标识被称为offset，即偏移量。注意，kafka只保证在他同一个partition内部消息是有序的，在不同partition之间，并不能保证消息有序。\nKafka可以配置一个保留期限，用来标识日志会在Kafka集群中保留多长时间。Kafka集群会保留在保留期限内所有发布的消息，不管这些消息是否被消费过，比如保留期限设置为两天，那么数据被发布到Kafka集群的两天以内，所有的这些数据都可以被消费，当超过两天，这些数据将会被清空。以便为后续的数据腾出空间，由于Kafka会将数据进行持久化存储（即写入到硬盘上），所以保留的数据大小可以设置为一个比较大的值。\nPartition结构 Partition在服务器上的表现形式就是一个一个的文件夹，每个partition的文件夹下面会有多组segment文件，每组segment文件又包含.index文件、.log文件、.timeindex文件三个文件，其中.log文件就是实际存储message的地方，而.index和.timeindex文件为索引文件，用于检索消息。\n消费数据 多个消费者实例可以组成一个消费者组，并用一个标签来标识这个消费者组。一个消费者组中的不同消费者实例可以运行在不同的进程甚至不同的服务器上。\n如果所有的消费者实例都在同一个消费者组中，那么消息记录会被很好的均衡的发送到每个消费者实例。\n如果所有的消费者实例都在不同的消费者组，那么每一条消息记录会被广播到每一个消费者实例。\n上面是kafka集群，下面就是消费者组\n举个例子，如上图所示一个两个节点的Kafka集群上拥有一个四个partition（PO-P3）的topic。有两个消费者组都在消费这个topic中的数据，消费者组A有两个消费者实例，消费者组B有四个消费者实例。\n从图中我们可以看到，在同一个消费者组中，每个消费者实例可以消费多个分区，但是每个分区最多只能被消费者组中的一个实例消费。也就是说，如果有一个4个分区的主题，那么消费者组中最多只能有4个消费者实例去消费，多出来的都不会被分配到分区。其实这也很好理解，如果允许两个消费者实例同时消费同一个分区，那么就无法记录这个分区被这个消费者组消费的offset了。如果在消费者组中动态的上线或下线消费者，那么Kafka集群会自动调整分区与消费者实例间的对应关系。\n使用场景 上面介绍了Kafka的一些基本概念和原理，那么Kafka可以做什么呢？目前主流使用场景基本如下：\n消息队列（MQ） 在系统架构设计中，经常会使用消息队列（Message Queue）——MQ。MQ是一种跨进程的通信机制，用于上下游的消息传递，使用MQ可以使上下游解耦，消息发送上游只需要依赖MQ，逻辑上和物理上都不需要依赖其他下游服务。MQ的常见使用场景如流量削峰、数据驱动的任务依赖等等。在MQ领域，除了Kafka外还有传统的消息队列如ActiveMQ和RabbitMQ等。\n追踪网站活动 Kafka最出就是被设计用来进行网站活动（比如PV、UV、搜索记录等）的追踪。可以将不同的活动放入不同的主题，供后续的实时计算、实时监控等程序使用，也可以将数据导入到数据仓库中进行后续的离线处理和生成报表等。\nMetrics Kafka 经常被用来传输监控数据。主要用来聚合分布式应用程序的统计数据，将数据集中后进行统一的分析和展示等。\n日志聚合 很多人使用Kafka作为日志聚合的解决方案。日志聚合通常指将不同服务器上的日志收集起来并放入一个日志中心，比如一台文件服务器或者HDFS中的一个目录，供后续进行分析处理。相比于Flume和Scribe等日志聚合工具，Kafka具有更出色的性能。\nKafka安装和启动 下载 下载地址：https://www.apache.org/dyn/closer.cgi?path=/kafka/2.6.0/kafka_2.12-2.6.0.tgz\n安装 将下载好的压缩包解压到本地，注意Kafka需要先安装JDK环境\n修改配置 我们首先找到Kafka的配置文件 server.properties\n然后修改日志存放位置\n1 2 3 4 ############################# Log Basics ############################# # A comma separated list of directories under which to store log files log.dirs=D:/tmp/kafka-logs 启动 修改完成后，我们即可启动Kafka了，kafka默认端口号是9092\n然后到 bin\\windows目录下，输入下面的脚本\n1 .\\kafka-server-start.bat ..\\..\\config\\server.properties Kafka启动成功\nZookeeper启动 注意，我们安装的Kafka里面，就包含了所需的Zookeeper配置文件，因此我们只需要修改配置即可\n找到Zookeeper.properties配置文件，修改数据目录\n修改完成后，我们到 bin\\windows目录下\n启动脚本\n1 .\\zookeeper-server-start.bat ..\\..\\config\\zookeeper.properties 启动完成后，将占用 2181端口号\nLogAgent的工作流程 读日志 往kafka中写日志 首先我们需要下载tailf，使用下面的命令\n1 go get github.com/hpcloud/tail 然后编写测试样例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/hpcloud/tail\u0026#34; \u0026#34;time\u0026#34; ) // tailf的用法 func main() { // 需要记录的日志文件 fileName := \u0026#34;./my.log\u0026#34; // config := tail.Config{ ReOpen: true, // 重新打开，日志文件到了一定大小，就会分裂 Follow: true, // 是否跟随 Location: \u0026amp;tail.SeekInfo{Offset: 0, Whence: 2}, // 从文件的哪个位置开始读 MustExist: false, // 是否必须存在，如果不存在是否报错 Poll: true, // } tails, err := tail.TailFile(fileName, config) if err != nil { fmt.Println(\u0026#34;tail file failed, err:\u0026#34;, err) return } var( line *tail.Line ok bool ) for { // 从tails中一行一行的读取 line, ok = \u0026lt;- tails.Lines if !ok { fmt.Println(\u0026#34;tail file close reopen, filename:%s\\n\u0026#34;, tails.Filename) time.Sleep(time.Second) continue } fmt.Println(\u0026#34;line\u0026#34;, line.Text) } } 但是我们在启动的时候，又出错了\n1 cannot find module providing package gopkg.in/fsnotify.v1 我们定位到源码目录，因为 opkg.in/fsnotify.v1的包改名了，所以我们需要修改两个文件，inotify.go 和 inotify_tracker.go\n将里面出错的文件，替换成下面的这个文件即可\nlog agent开发 下载安装 1 go get github.com/Shopify/sarama sarama V1.20之后的版本加入了zstd压缩算法，需要用到cgo，在Windows平台编译时会提示类似如下错误：\n1 exec: \u0026#34;gcc\u0026#34;:executable file not found in %PATH% 所以在Windows平台请使用v1.19版本的sarama，因此我们需要使用下面命令创建一个go.mod文件\n1 go mod init kafkaDemo 然后在文件里面配置一下版本号 v1.19.0\n1 2 3 4 5 6 go 1.14 require ( github.com/Shopify/sarama v1.19.0 ) 然后在项目的目录下，下载依赖\n1 go mod download 下载完成后，我们就可以写测试代码了\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/Shopify/sarama\u0026#34; ) // 基于sarama第三方库开发的Kafka client func main() { config := sarama.NewConfig() // tailf包使用，发送完数据需要 leader 和 follow都确定 config.Producer.RequiredAcks = sarama.WaitForAll // 新选出一个partition config.Producer.Partitioner = sarama.NewRandomPartitioner // 成功交付的消息将在 success channel返回 config.Producer.Return.Successes = true msg := \u0026amp;sarama.ProducerMessage{} msg.Topic = \u0026#34;web_log\u0026#34; msg.Value = sarama.StringEncoder(\u0026#34;this is a test log\u0026#34;) // 连接kafka，可以连接一个集群 client, err := sarama.NewSyncProducer([]string{\u0026#34;127.0.0.1:9092\u0026#34;}, config) if err != nil { fmt.Println(\u0026#34;producer closed, err: \u0026#34;, err) } fmt.Println(\u0026#34;连接kafka成功！\u0026#34;) // 定义延迟关闭 defer client.Close() // 发送消息 pid, offset, err := client.SendMessage(msg) if err != nil { fmt.Println(\u0026#34;send msg failed, err:\u0026#34;, err) return } fmt.Printf(\u0026#34;pid:%v offset:%v \\n\u0026#34;, pid, offset) } LogAgent编码 首先我们需要创建一个 kafka.go 用来初始化kafka和发送消息到kafka\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package kafka import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/Shopify/sarama\u0026#34; ) // 专门往kafka写日志的模块 var ( // 声明一个全局连接kafka的生产者 client sarama.SyncProducer ) // 初始化client func Init()(err error) { config := sarama.NewConfig() // tailf包使用，发送完数据需要 leader 和 follow都确定 config.Producer.RequiredAcks = sarama.WaitForAll // 新选出一个partition config.Producer.Partitioner = sarama.NewRandomPartitioner // 成功交付的消息将在 success channel返回 config.Producer.Return.Successes = true msg := \u0026amp;sarama.ProducerMessage{} msg.Topic = \u0026#34;web_log\u0026#34; msg.Value = sarama.StringEncoder(\u0026#34;this is a test log\u0026#34;) // 连接kafka，可以连接一个集群 client, err = sarama.NewSyncProducer([]string{\u0026#34;127.0.0.1:9092\u0026#34;}, config) if err != nil { fmt.Println(\u0026#34;producer closed, err: \u0026#34;, err) } fmt.Println(\u0026#34;Kafka初始化成功\u0026#34;) return err } // 发送消息到Kafka func SendToKafka(topic, data string) { msg := \u0026amp;sarama.ProducerMessage{} msg.Topic = topic msg.Value = sarama.StringEncoder(data) // 发送到kafka pid, offset, err := client.SendMessage(msg) if err != nil { fmt.Println(\u0026#34;send msg failed, err:\u0026#34;, err) return } fmt.Println(\u0026#34;发送消息：\u0026#34;, data) fmt.Printf(\u0026#34;发送成功~ pid:%v offset:%v \\n\u0026#34;, pid, offset) } 然后我们在创建一个 taillog.go文件，用来记录日志\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package taillog import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/hpcloud/tail\u0026#34; ) // 定义全局对象 var ( // 声明一个全局连接kafka的生产者 tailObj *tail.Tail ) // 专门从日志文件收集日志的模块 func Init(fileName string)(err error ){ // 定义配置文件 config := tail.Config{ ReOpen: true, // 重新打开，日志文件到了一定大小，就会分裂 Follow: true, // 是否跟随 Location: \u0026amp;tail.SeekInfo{Offset: 0, Whence: 2}, // 从文件的哪个位置开始读 MustExist: false, // 是否必须存在，如果不存在是否报错 Poll: true, // } tailObj, err = tail.TailFile(fileName, config) if err != nil { fmt.Println(\u0026#34;tail file failed, err:\u0026#34;, err) return } return err } // 读取日志，返回一个只读的chan func ReadChan() \u0026lt;-chan *tail.Line { return tailObj.Lines } 最后我们创建main.go作为启动类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package main import ( \u0026#34;LogDemo/kafka\u0026#34; \u0026#34;LogDemo/taillog\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;time\u0026#34; ) // logAgent入口程序 func run() { // 1.读取日志 for { select { case line := \u0026lt;-taillog.ReadChan(): { // 2.发送到kafka kafka.SendToKafka(\u0026#34;web_log\u0026#34;, line.Text) } default: time.Sleep(1 * time.Second) } } } func main() { // 1. 初始化kafka连接 err := kafka.Init() if err != nil { fmt.Printf(\u0026#34;init Kafka failed, err:%v \\n\u0026#34;, err) return } // 2. 打开日志文件，准备收集 err = taillog.Init(\u0026#34;./my.log\u0026#34;) if err != nil { fmt.Printf(\u0026#34;Init taillog failed, err: %v \\n\u0026#34;, err) return } // 3.执行业务逻辑 run() } 最后我们启动main.go，然后往文件里面插入内容，然后就会将日志记录发送到kafka中\n最后通过下面的脚本，来进行kafka的消息的消费\n1 .\\kafka-console-consumer.bat --bootstrap-server=127.0.0.1:9092 --topic=web_log --from-beginning 然后就开始消费kafka中的消息\n存在的问题 上述的代码还存在硬编码的问题，我们将通过配置文件将一些信息配置出来，这样能够提高代码的扩展性，这里我们用的是ini文件，创建一个 config.ini文件，填入配置信息\n1 2 3 4 5 6 [kafka] address=127.0.0.1:9092 topic=web_log [taillog] path:=./my.log 引入依赖 我们需要使用go-ini的依赖，来对我们的配置文件进行解析 ，go-ini官网\n首先下载依赖\n1 go get gopkg.in/ini.v1 然后通过下面的方式进行读取\n1 2 3 4 5 6 7 8 9 10 11 // 0. 加载配置文件 cfg, err := ini.Load(\u0026#34;./conf/config.ini\u0026#34;) if err != nil { fmt.Printf(\u0026#34;Fail to read file: %v\u0026#34;, err) os.Exit(1) } // 典型读取操作，默认分区可以使用空字符串表示 fmt.Println(\u0026#34;kafka address:\u0026#34;, cfg.Section(\u0026#34;kafka\u0026#34;).Key(\u0026#34;address\u0026#34;).String()) fmt.Println(\u0026#34;kafka topic:\u0026#34;, cfg.Section(\u0026#34;kafka\u0026#34;).Key(\u0026#34;topic\u0026#34;).String()) fmt.Println(\u0026#34;taillog path:\u0026#34;, cfg.Section(\u0026#34;taillog\u0026#34;).Key(\u0026#34;path\u0026#34;).String()) 优化后的main.go如下所示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package main import ( \u0026#34;LogDemo/kafka\u0026#34; \u0026#34;LogDemo/taillog\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; \u0026#34;os\u0026#34; \u0026#34;time\u0026#34; ) // logAgent入口程序 func run() { // 1.读取日志 for { select { case line := \u0026lt;-taillog.ReadChan(): { // 2.发送到kafka kafka.SendToKafka(\u0026#34;web_log\u0026#34;, line.Text) } default: time.Sleep(1 * time.Second) } } } func main() { // 0. 加载配置文件 cfg, err := ini.Load(\u0026#34;./conf/config.ini\u0026#34;) if err != nil { fmt.Printf(\u0026#34;Fail to read file: %v\u0026#34;, err) os.Exit(1) } // 典型读取操作，默认分区可以使用空字符串表示 fmt.Println(\u0026#34;kafka address:\u0026#34;, cfg.Section(\u0026#34;kafka\u0026#34;).Key(\u0026#34;address\u0026#34;).String()) fmt.Println(\u0026#34;kafka topic:\u0026#34;, cfg.Section(\u0026#34;kafka\u0026#34;).Key(\u0026#34;topic\u0026#34;).String()) fmt.Println(\u0026#34;taillog path:\u0026#34;, cfg.Section(\u0026#34;taillog\u0026#34;).Key(\u0026#34;path\u0026#34;).String()) // 1. 初始化kafka连接 address := []string{cfg.Section(\u0026#34;kafka\u0026#34;).Key(\u0026#34;address\u0026#34;).String()} topic := cfg.Section(\u0026#34;taillog\u0026#34;).Key(\u0026#34;path\u0026#34;).String() err = kafka.Init(address, topic) if err != nil { fmt.Printf(\u0026#34;init Kafka failed, err:%v \\n\u0026#34;, err) return } // 2. 打开日志文件，准备收集 err = taillog.Init(cfg.Section(\u0026#34;taillog\u0026#34;).Key(\u0026#34;path\u0026#34;).String()) if err != nil { fmt.Printf(\u0026#34;Init taillog failed, err: %v \\n\u0026#34;, err) return } // 3.执行业务逻辑 run() } 最终版本 上述的方式，还是存在一些问题，就是配置信息不能传递，只能在main方法里面，那么我们可以在定义一个结构体，conf.go\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 package conf type AppConf struct { KafkaConf `ini:\u0026#34;kafka\u0026#34;` TaillogConf `ini:\u0026#34;taillog\u0026#34;` } type KafkaConf struct { Address string `ini:\u0026#34;address\u0026#34;` Topic string `ini:\u0026#34;topic\u0026#34;` } type TaillogConf struct { FileName string `ini:\u0026#34;filename\u0026#34;` } 然后原来的main.go改为\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package main import ( \u0026#34;LogDemo/conf\u0026#34; \u0026#34;LogDemo/kafka\u0026#34; \u0026#34;LogDemo/taillog\u0026#34; \u0026#34;fmt\u0026#34; \u0026#34;gopkg.in/ini.v1\u0026#34; \u0026#34;time\u0026#34; ) var ( cfg = new(conf.AppConf) ) // logAgent入口程序 func run() { // 1.读取日志 for { select { case line := \u0026lt;-taillog.ReadChan(): { // 2.发送到kafka kafka.SendToKafka(cfg.Topic, line.Text) } default: time.Sleep(1 * time.Second) } } } func main() { // 0. 加载配置文件 // 方式2 err := ini.MapTo(\u0026amp;cfg, \u0026#34;./conf/config.ini\u0026#34;) if err != nil { fmt.Printf(\u0026#34;load ini failed, err: %v \\n\u0026#34;, err) return } fmt.Println(\u0026#34;读取到的配置信息\u0026#34;, cfg) // 1. 初始化kafka连接 address := []string{cfg.Address} topic := cfg.Topic err = kafka.Init(address, topic) if err != nil { fmt.Printf(\u0026#34;init Kafka failed, err:%v \\n\u0026#34;, err) return } // 2. 打开日志文件，准备收集 err = taillog.Init(cfg.FileName) if err != nil { fmt.Printf(\u0026#34;Init taillog failed, err: %v \\n\u0026#34;, err) return } // 3.执行业务逻辑 run() } ","permalink":"https://ktzxy.top/posts/euc6d2lvw7/","summary":"日志收集项目架构设计及Kafka介绍","title":"日志收集项目架构设计及Kafka介绍"},{"content":"MySQL limit 子句可以实现分页查询，比如limit 100,10 取偏移100条记录之后的10条记录。limit 子句在提供便捷分页功能的同时，也带来了性能问题，当表数据量非常大，分页数非常多，查询比较靠后的页时，SQL执行性能非常差。\n优化方法一，改写SQL 看一个例子，一张表，有64w多条记录。\n1 2 3 4 5 6 7 mysql\u0026gt; select count(*) from sbtest2; +----------+ | count(*) | +----------+ | 640133 | +----------+ 1 row in set (2.67 sec) 查询50w之后的10条记录，耗时5.06秒，如下：\n1 2 3 mysql\u0026gt; select * from sbtest2 order by id limit 500000,10; ... 10 rows in set (5.06 sec) 换一种写法，耗时3.81秒，明显优于前一种写法，如下：\n1 2 3 mysql\u0026gt; select * from sbtest2 t1, (select id from sbtest2 order by id limit 500000,10) t2 where t1.id=t2.id; ... 10 rows in set (3.81 sec) 换一种写法，使用了子查询，子查询中通过id所在索引（主键或辅助索引）扫描，将需要的10条记录的id检索出来，然后再进行表关联，查出10条记录的完整信息，避免了50w条数据由InnoDB存储引擎传递给MySQL Server层的IO消耗，从而提升性能。\noffset越大，检索字段越多，这种写法的性能提升越明显。\n优化方法二，记录最大最小id 程序记录每个分页的最大和最小id，翻页时根据上一页的最大，最小id来检索记录，可以避免大量无效的数据检索消耗，如下所示：\nselect * from sbtest2 where id \u0026gt; $max_id order by id limit 10;\nselect * from sbtest2 where id \u0026lt; $mim_id order by id desc limit 10;\n这种方式，由于能够按id走索引，SQL执行性能非常快。\n","permalink":"https://ktzxy.top/posts/9iky5e06ti/","summary":"MySQL limit 分页优化","title":"MySQL limit 分页优化"},{"content":"﻿### 我们以 INSERT 触发器的创建为例，讲解触发器的创建和使用。首先创建测试数据表：\n1 2 3 4 5 6 7 --创建学生表 create table student( stu_id int identity(1,1) primary key, stu_name varchar(10), stu_gender char(2), stu_age int ) 为 student 表创建 INSERT 触发器： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 --创建insert触发器 create trigger trig_insert on student after insert as begin if object_id(N\u0026#39;student_sum\u0026#39;,N\u0026#39;U\u0026#39;) is null--判断student_sum表是否存在 create table student_sum(stuCount int default(0));--创建存储学生人数的student_sum表 declare @stuNumber int; select @stuNumber = count(*)from student; if not exists (select * from student_sum)--判断表中是否有记录 insert into student_sum values(0); update student_sum set stuCount =@stuNumber; --把更新后总的学生数插入到student_sum表中 end 1 2 3 4 5 6 7 8 --测试触发器trig_insert--\u0026gt;功能是向student插入数据的同时级联插入到student_sum表中，更新stuCount --因为是后触发器，所以先插入数据后，才触发触发器trig_insert; insert into student(stu_name,stu_gender,stu_age)values(\u0026#39;吕布\u0026#39;,\u0026#39;男\u0026#39;,30); select stuCount 学生总人数 from student_sum; insert into student(stu_name,stu_gender,stu_age)values(\u0026#39;貂蝉\u0026#39;,\u0026#39;女\u0026#39;,30); select stuCount 学生总人数 from student_sum; insert into student(stu_name,stu_gender,stu_age)values(\u0026#39;曹阿瞒\u0026#39;,\u0026#39;男\u0026#39;,40); select stuCount 学生总人数 from student_sum; 另外，因为定义学生总数表 student_sum ，是向 student 表中插入数据后，才计算的学生总数。所以，学生总数表应该禁止用户，向其中插入数据。 1 2 3 4 5 6 7 8 9 --创建insert_forbidden,禁止用户向student_sum表中插入数据 create trigger insert_forbidden on student_sum after insert as begin RAISERROR(\u0026#39;禁止直接向该表中插入记录，操作被禁止\u0026#39;,1,1)--raiserror 是用于抛出一个错误 rollback transaction end 实验部分 向 score 表建立一个插入触发器。保证向 score 表中插入的学生信息的学号，必须在 student 表中存在 1 2 3 4 5 6 7 8 9 10 11 create trigger trigger_insert_score on score after insert as Begin if not exists (select * from student where sno in(select sno from inserted)) Begin rollback transaction Begin transaction End End 1 2 3 4 5 6 7 insert into score values(\u0026#39;1001\u0026#39;,\u0026#39;2001\u0026#39;,\u0026#39;89.5\u0026#39;) go insert into score values(\u0026#39;1002\u0026#39;,\u0026#39;2001\u0026#39;,\u0026#39;95\u0026#39;) go insert into score values(\u0026#39;1011\u0026#39;,\u0026#39;2001\u0026#39;,\u0026#39;88\u0026#39;) go select * from score 向 student 表插入删除触发器，实现 student 表和 score 表的级联删除； 1 2 3 4 5 6 7 create trigger trigger_delete_student on student for delete as Begin delete score where sno in (select sno from deleted) End 1 2 3 delete from student where sno=\u0026#39;1001\u0026#39; go select * from student 向 score 表建立触发器，使 grade 列不能手工修改 1 2 3 4 5 6 7 8 create trigger trigger_protect_grade on score for update as Begin if update(grade) raiserror(\u0026#39;cannot modify the grade\u0026#39;,16,1) End 1 2 3 4 5 delete from score where sno=\u0026#39;1001\u0026#39; go select * from score go select * from student ","permalink":"https://ktzxy.top/posts/upev5z3tk7/","summary":"笔记一：触发器部分","title":"笔记一：触发器部分"},{"content":" 索引条件下推，Index Condition Pushdown，简称ICP，是MySQL内部通过索引查询数据的一种优化方法，简单来说就是将原本需要在Server层对数据进行过滤的条件下推到了引擎层去做，在引擎层过滤更多的数据，这样从引擎层发送到Server层的数据就会显著减少，从而优化性能。\n原文地址：\nhttps://mytecdb.com/blogDetail.php?id=97\n1. ICP索引下推原理 举一个例子，有一个索引如下： idx_all(a,b,c)\n查询语句： select d from t where a=\u0026lsquo;xx\u0026rsquo; and b like \u0026lsquo;%xx%\u0026rsquo; and c like \u0026lsquo;%xx%\u0026rsquo;\n查询走索引idx_all，但是只能使用前缀a的部分。\n这样一个查询，在没有使用ICP时，存储引擎根据索引条件a=\u0026lsquo;xx\u0026rsquo;，获取记录，将这些记录返回给Server层，Server层再根据条件 b like \u0026lsquo;%xx%\u0026rsquo; 和 c like \u0026lsquo;%xx%\u0026rsquo; 来进一步过滤记录，最后回表拿到d字段数据返回给用户。\n使用ICP时，存储引擎根据索引条件a=\u0026lsquo;xx\u0026rsquo;，获取记录，并在引擎层根据条件 b like \u0026lsquo;%xx%\u0026rsquo; 和 c like \u0026lsquo;%xx%\u0026rsquo; 来过滤数据，然后引擎返回记录到Server层，显然，使用ICP时，返回给Server层的记录数量会显著减少，Server层不需要再过滤，直接回表查询，整体效率将会提高很多。\n总的来说，ICP主要在引擎层增加了条件过滤能力，减少了引擎层向Server层传输的数据量。\n2. ICP索引下推的适用条件 查询走索引，explain中的访问方式为range，ref，eq_ref，ref_or_null，并且需要回表查询。 ICP可用于InnoDB、MyISAM及分区表。 对于InnoDB表，ICP只适用于走二级索引的查询。 ICP不支持在虚拟列上创建的二级索引。 如果where条件涉及子查询，则不能使用ICP。 如果where条件使用函数，不能使用ICP。 触发器条件也不能使用ICP。 如果一个SQL使用了ICP优化，那么在Explain的输出中，其Extra列会显示 Using index condition。\n3. ICP索引下推举例 表结构：\n1 2 3 4 5 6 7 CREATE TABLE `people` ( `zipcode` varchar(50) DEFAULT NULL, `lastname` varchar(50) DEFAULT NULL, `address` varchar(50) DEFAULT NULL, `age` int(11) DEFAULT \u0026#39;0\u0026#39;, KEY `idx_all` (`zipcode`,`lastname`,`address`) ); select age from people where zipcode=\u0026lsquo;0000005\u0026rsquo; and lastname like \u0026lsquo;%1%\u0026rsquo; and address like \u0026lsquo;%1%\u0026rsquo;;\n这个SQL走了二级索引 idx_all，并且需要回表查询，所以满足ICP优化的条件，从explain的结果来看，Extra字段也确实是Using index condition，如下：\n1 2 3 4 5 6 7 mysql\u0026gt; explain select age from people where zipcode=\u0026#39;0000005\u0026#39; and lastname like \u0026#39;%xx%\u0026#39; and address like \u0026#39;%xx%\u0026#39;; +----+------+---------------+---------+---------+-------+------+----------+-----------------------+ | id | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+------+---------------+---------+---------+-------+------+----------+-----------------------+ | 1 | ref | idx_all | idx_all | 153 | const | 1 | 50.00 | Using index condition | +----+------+---------------+---------+---------+-------+------+----------+-----------------------+ 1 row in set, 1 warning (0.00 sec) ICP优化效果：\n表中记录总数：393216 满足条件【zipcode=\u0026lsquo;0000005\u0026rsquo;】的记录数：32768 满足条件【zipcode=\u0026lsquo;0000005\u0026rsquo; and lastname like \u0026lsquo;%1%\u0026rsquo; and address like \u0026lsquo;%1%\u0026rsquo;】的记录数：15000 执行耗时：\n未使用ICP：1.07秒 使用ICP：0.62秒 从以上执行耗时来看，ICP优化效果还是不错的，当然不同的数据分布对优化效果也会有一定的影响，有兴趣的可以自己测试一下。\n","permalink":"https://ktzxy.top/posts/m74onpz1c7/","summary":"MySQL性能优化 索引条件下推ICP(Index Condition Pushdown)","title":"MySQL性能优化 索引条件下推ICP(Index Condition Pushdown)"},{"content":"Kubernetes核心技术Ingress 前言 原来我们需要将端口号对外暴露，通过 ip + 端口号就可以进行访问\n原来是使用Service中的NodePort来实现\n在每个节点上都会启动端口 在访问的时候通过任何节点，通过ip + 端口号就能实现访问 但是NodePort还存在一些缺陷\n因为端口不能重复，所以每个端口只能使用一次，一个端口对应一个应用 实际访问中都是用域名，根据不同域名跳转到不同端口服务中 Ingress和Pod关系 pod 和 ingress 是通过service进行关联的，而ingress作为统一入口，由service关联一组pod中\n首先service就是关联我们的pod 然后ingress作为入口，首先需要到service，然后发现一组pod 发现pod后，就可以做负载均衡等操作 Ingress工作流程 在实际的访问中，我们都是需要维护很多域名， a.com 和 b.com\n然后不同的域名对应的不同的Service，然后service管理不同的pod\n需要注意，ingress不是内置的组件，需要我们单独的安装\n使用Ingress 步骤如下所示\n部署ingress Controller【需要下载官方的】 创建ingress规则【对哪个Pod、名称空间配置规则】 创建Nginx Pod 创建一个nginx应用，然后对外暴露端口\n1 2 3 4 # 创建pod kubectl create deployment web --image=nginx # 查看 kubectl get pods 对外暴露端口\n1 kubectl expose deployment web --port=80 --target-port=80 --type:NodePort 部署 ingress controller 下面我们来通过yaml的方式，部署我们的ingress，配置文件如下所示\n这个文件里面，需要注意的是 hostNetwork: true，改成ture是为了让后面访问到\n1 kubectl apply -f ingress-con.yaml 通过这种方式，其实我们在外面就能访问，这里还需要在外面添加一层\n1 kubectl apply -f ingress-con.yaml 最后通过下面命令，查看是否成功部署 ingress\n1 kubectl get pods -n ingress-nginx 创建ingress规则文件 创建ingress规则文件，ingress-h.yaml\n添加域名访问规则 在windows 的 hosts文件，添加域名访问规则【因为我们没有域名解析，所以只能这样做】\n最后通过域名就能访问\n","permalink":"https://ktzxy.top/posts/7kw46ku3pk/","summary":"14 Kubernetes核心技术Ingress","title":"14 Kubernetes核心技术Ingress"},{"content":" prometheus 监控docker 一、概述 cAdvisor（Container Advisor）用于收集正在运行的容器资源使用和性能信息。\n使用Prometheus监控cAdvisor\ncAdvisor将容器统计信息公开为Prometheus指标。\n默认情况下，这些指标在/metrics HTTP端点下提供。\n可以通过设置-prometheus_endpoint命令行标志来自定义此端点。\n要使用Prometheus监控cAdvisor，只需在Prometheus中配置一个或多个作业，这些作业会在该指标端点处刮取相关的cAdvisor流程。\n使用文档：https://github.com/google/cadvisor 图表模板：https://grafana.com/dashboards/193 二、运行cAdvisor 启动cAdvisor容器 运行单个cAdvisor来监控整个Docker主机，被监控端安装完Docker后，添加启动cAdvisor容器\n1 2 3 4 5 6 7 8 9 10 11 docker run \\ --volume=/:/rootfs:ro \\ --volume=/var/run:/var/run:ro \\ --volume=/sys:/sys:ro \\ --volume=/var/lib/docker/:/var/lib/docker:ro \\ --volume=/dev/disk/:/dev/disk:ro \\ --publish=8080:8080 \\ --detach=true \\ --name=cadvisor \\ --restart=always \\ google/cadvisor:latest 配置Promethus 修改配置文件prometheus.yml，最后一行添加\n1 2 3 4 5 - job_name: \u0026#39;docker\u0026#39; static_configs: - targets: [\u0026#39;192.168.31.138:8080\u0026#39;] labels: instance: docker测试 修改配置文件后，重启prometheus\n访问prometheus targets，确保是up状态\n三、Granfana 导入 Docker 监控图表 推荐图标ID：https://grafana.com/dashboards/193\n输入导入图标ID等待3秒弹出如下，修改后保存\n查看图标监控仪表盘\n但是这个模板，无法选择根据主机选择。推荐另外一个模板，它是可以选择主机的。\nhttps://grafana.com/grafana/dashboards/10566\n本文参考链接：\nhttps://www.cnblogs.com/xiangsikai/p/11289518.html\n","permalink":"https://ktzxy.top/posts/ifjzju1a1p/","summary":"Prometheus监控docker","title":"Prometheus监控docker"},{"content":"Yesterday is history, tomorrow is a mystery and today is a gift, that\u0026rsquo;s why they call it the present.\nsqlserver数据库优化 一、数据库瓶颈-IO瓶颈\n1.磁盘读IO瓶颈：\n热点数据太多，数据库缓存放不下，每次查询会产生大量的IO，降低查询速度-\u0026gt;分库和垂直分表\n2.网络IO瓶颈：\n请求的数据太多，网络带宽不够\n二、数据库瓶颈-CPU瓶颈\n1.SQL问题：\n如SQL中包含join，group by，order by，非索引字段条件查询等，增加CPU运算的操作-\u0026gt;SQL优化，建立合适的索引，在业务Service层进行业务计算（不建议在数据库中对数据进行操作）。\n2.单表数据量太大：\n查询时扫描的行太多，SQL效率低，增加CPU运算的操作。\n三、数据库压力过大\n1.数据库性能降低，增加数据库服务延迟-\u0026gt;Web服务器卡顿\n2.数据库服务器宕机，造成数据库数据丢失，数据错乱，web服务瘫痪\n常见的优化方案\n1.加入索引\u0026mdash;针对于查询\n2.配置读写分离。让查询分摊出去。\n​\t【二八原则】一个主库：负责数据库中20%的写入-增删改 多个从库：应对80%的操作；共同负责查询动作\n3.分库分表-化整为零操作数据库。\n==高性能+高可用==\n1.高性能\u0026ndash;数据库服务响应快，加入索引，配置读写分离。让查询分摊出去。\n2.高可用\u0026ndash;保证数据库绝不会故障，数据库服务器一定不发生故障？做不到。如果要保证数据库无论如何都能够提供服务，只能采用==替补==模式。\nSqlServer AlwaysOn高可用组\n从SqlServer2012开始提供的一项数据库高可用解决方案。\n核心价值：\n在Windows故障转移群集基础上完成部署 读写分离，支持负载均衡 2019最多可以有5个写入节点实现故障转移，5个数据实时同步节点 SqlServer2012支持1+4部署 SqlServer2016支持1+8部署 环境准备\n基于Vmware搭建了四个windowsServer虚拟机\n1.域控制DC主机 主机名：win-dc ip：192.168.1.120\n2.SqlServer机-1 主机名：win-node01 ip：192.168.1.170\n3.SqlServer机-2 主机名：win-node02 ip：192.168.1.180\n4.SqlServer机-3 主机名：win-node03 ip：192.168.1.190\n开始做域控\n1.安装Activer Directory\n​\t通过【添加角色和功能】安装Activer Directory域服务和DNS服务器。\n​\t点击AD DS，然后点击进行配置，将此服务器提升为域控制器，然后根据Activer Directory域服务配置向导，添加新林，填写根域名，\n2.安装DNS域名解析服务器\n3.DC升级做域服务器\n4.需要群集的节点加入域\n​\t系统属性，选择域，填写域名；使用固定ip地址，DNS服务器ip地址填域控制DC主机ip地址\n最终结果：通过域账户登录，可以通过域名ping通\n配置共享磁盘\n1.windows故障转移群集，需要共享磁盘\n​\t安装iSCSI，【添加角色和功能】，勾选文件服务器\n2.在DC机器配置网络磁盘\n​\t新建iSCSI虚拟磁盘，访问服务器 添加节点ip地址\n3.需要故障转移群集的节点去联机共享磁盘\n​\t通过【工具】【iSCSI发起 程序】进行连接，然后可以初始化磁盘，新建卷\n在节点服务器安装【故障转移群集】功能；添加网络适配器，其一是桥接模式，其二是仅主机模式；创建群集。\n开始AlwaysOn高可用组\n1.配置SqlServer登陆账号为域账号\n2.开启AlwaysOn功能\n​\tSqlServer配置管理器中，sqlserver服务右键\n3.开始新建AlwaysOn高可用组。\n​\t添加可用性组；可用性侦听器\n4.测试\nSqlServer性能优化 解决查询问题\n把查询独立出去，就查询数据库的服务\u0026ndash;让更多的服务器来支持 读写分离\u0026ndash;分为主从数据库，一主多从 增删改汇聚到主数据库 主数据库同步数据到从库数据库 查询操作占数据库操作的80%，查询就可以让更多的服务器来支撑查询。更多的从数据库共同承载80%的查询行为； 矛盾：写入主库，查询从库，数据怎么能查询的到？\n数据库的数据复制\u0026ndash;价值\n数据复制：多种\u0026ndash;快照，数据库的日志；\n特点：每个数据库中的数据包含的表中的数据都是完全一致的；\n数据复制有延迟，延迟可以解决，不会太高。\n读写分离四种模式 快照发布\n发布服务器按预定的时间间隔向订阅服务器发送已发布数据的快照\n特点：最低要延迟10s钟；数据的同步是批量性。同步表结构的变化；选定表范围内的变化，可以同步的；\n对于一些数据库中增删改相对频繁的操作可以考虑快照；\n事务发布\u0026ndash;最推荐-延迟最小\n在订阅服务器收到已发布数据的初始快照后，发布服务器将事务流式传输到订阅服务器。\n适用于增删改少-查询多的情况\n特点：表必须有主键；发布方修改表结构，表结构不能同步；只能同步数据库；增加表，也是不能同步的。\n延迟比较低，事务流，及时性高。\n对等发布\n对等发布支持多主复制。发布服务器将事务流式传输到拓扑中的所有对等方。所有对等节点可以读取和写入更改，且所有更改将传播到拓扑中的所有节点。\n合并发布\n在订阅服务器收到已发布数据的初始快照后，发布服务器和订阅服务器可以独立更新已发布数据。更改会定期合并。Microsoft SQL Server Compact Edition只能订阅合并发布。\n1.发布服务器：主要节点-主数据库\n2.分发服务器：配置数据需要复制的，数据要复制的文件保存到分发服务器\n3.订阅服务器：从数据库\n什么是索引 索引是对数据表中一列或多列的值进行排序的一种结构，使用索引可快速访问数据库表中的特定信息。让我们在做数据查询的过程中，提升查询的效率。\n索引的合理使用 1.添加索引的时候，数据库引擎需要合成索引，在合成的过程中，需要有大量的数据指向的调整；\n2.索引的生成——会影响增加删改的性能，尤其是增加数据和删除数据之后、增加、删除后。重新维护新的数据库的索引结构；\n性能指标：响应时间，吞吐量，可扩展性\n平衡：\n最小化每个sql的响应时间\n合理增加吞吐量\n减少网络延时\n优化磁盘IO、CPU\n能够协调、平衡的运作-\u0026gt;合理的响应外部的请求-\u0026gt;实现资源利用最大化\n常见因素的影响： 数据库结构的设计\n性能优化-\u0026gt;贯穿整个生命周期\n了解业务-\u0026gt;系统为了满足业务需求\n优先考虑第三范式设计：字段冗余-\u0026gt;避免多表关联的手段\n查询-\u0026gt;默认加载查询-\u0026gt;10万次\n更新-\u0026gt;每天甚至几天才会更新一次\n表关联尽可能少，使用简单的sql语句，避免过多的表关联\n坚持最小原则：尽可能的字段类型\n适当的使用约束：安全方面\nsql语句的编写\n数据文件的位置\nTempDB系统库-\u0026gt;全局资源-\u0026gt;放在独立的磁盘、减少分配争用\nModel系统\u0026ndash;Raid 0\n数据拆分\u0026ndash;\u0026gt;与CPU物理线程个数相同\n用户系统库和日志文件隔离存放\n数据文件-\u0026gt;随机读写\n日志文件-\u0026gt;顺序读写\n主文件组仅供系统使用\n硬件资源\n扫描运算符：表扫描-\u0026gt;堆表、聚集索引扫描、非聚集索引扫描（包含在定义好的索引中，索引中已经包含数据，覆盖索引）\n扫描-\u0026gt;性能的最大杀手\n缓存数据会频繁交替-\u0026gt;性能瓶颈\n查找运算符-\u0026gt;索引\n通过索引去定位数据\n聚集索引查找\n非聚集索引查找-\u0026gt;非聚集索引字段进行where过滤\n键查找-\u0026gt;标签查找（使用了非聚集索引的语句中，用于查找不包含在当前索引中的字段）\nOrder by-\u0026gt;参与排序操作的数据量的大小\n避免对大批数据进行排序操作\n中间数据-\u0026gt;TempDB数据库-\u0026gt;存储查询过程中产生的中间数据\n排序过程-\u0026gt;工作区内存-\u0026gt;TempDB数据库-\u0026gt;IO的开销\nGroup by + distinct\nwhere 子句\n是否有合适的索引\n字段上是否存在函数计算\n结果集是否过大\n是否仅查询出需要的字段\n合理索引的选择-\u0026gt;查找的方式-\u0026gt;去查询某个或若干个覆盖索引\n覆盖索引-包含了当前查询语句所使用的左右字段信息\nSARG-\u0026gt;可以高效使用索引的写法\n非SARG\nwhere条件符号左边出现标量函数\nwhere upper(列名) = ‘A’ \u0026ndash; \u0026gt; where (列名 = \u0026lsquo;A\u0026rsquo; or 列名 = \u0026lsquo;a\u0026rsquo;)\nwhere 列-1 = 某个值 \u0026mdash;\u0026gt;where 列 = 某个值 + 1\nleft(Column，3) = ‘ABC’ \u0026mdash; \u0026gt;Column like \u0026lsquo;ABC%\u0026rsquo;\nDATEADD(day,7,列名) \u0026gt; GETDATE() \u0026mdash; 》 列名 \u0026gt; DATEADD(day,-7,GETDATE())\n隐式类型转换\n子查询 - \u0026gt; 嵌套在查询语句里的查询语句 - \u0026gt; 出现在where 子句、Select 语句某个字段的表达式\n尽量出现在where子句\n子查询的数量不超过3个，整个涉及的表不超过5个\n子查询\u0026ndash;\u0026gt;常用的连接操作\n无法转化的情况\u0026ndash;\u0026gt;优先执行\u0026ndash;\u0026gt;结果集作为下一个操作的输入部分\n避免在子查询中对大数据集进行汇总或排序操作\n尽量缩小子查询中可能返回的结果集\n尽量使用确定性的判断符、=、in、exists，避免使用any、some、all\nIn exists \u0026ndash;》inner join\n","permalink":"https://ktzxy.top/posts/s9j0hwrw17/","summary":"sqlserver数据库优化","title":"sqlserver数据库优化"},{"content":"对象存储MinIO入门介绍 常见的对象存储方式对比 直接将图片保存到服务的硬盘\n优点：开发便捷，成本低 缺点：扩容困难 使用分布式文件系统进行存储\n优点：容易实现扩容 缺点：开发复杂度稍大（尤其是开发复杂的功能） 使用nfs做存储\n优点：开发较为便捷 缺点：需要有一定的运维知识进行部署和维护 使用第三方的存储服务\n优点：开发简单，拥有强大功能，免维护 缺点：付费 为何不采取FastDFs进行文件存储 我们前面使用分布式文件系统FastDFS简直不要太爽，但是有几个问题不知道大家发现没有\n第一个就是FastDFS没有一个完善的官方文档，各种第三方文档满天飞。 第二个就是创建容器比较麻烦，要创建存储服务与跟踪服务. 第三个就是安全性问题 对象存储MinIO MinIO是世界上最快的对象存储服务器，在标准硬件上，读写速度分贝为183GB/s 和 171GB/s，对象存储可以作为主要存储层，用于Spark，Presto，TensorFlow，H20.ai 以及替代产品等各种工作负载用于Hadoop HDFS\nMinIO是一种高性能的分布式对象存储系统，它是软件定义的，可在行业标准硬件上运行，并且在Apache 2.0许可下，百分百开放源代码。\n文档地址：https://docs.min.io/cn/\n下载 我们使用的是Docker的方式安装MinIO，首先拉取对应的镜像\n1 docker pull minio/minio 然后我们需要创建两个目录，用于保存我们的文件和配置\n1 2 mkdir -p /home/minio/data mkdir -p /home/minio/config 启动容器 然后我们启动我们的容器，后面有个目录，就是我们需要挂载的硬盘目录\n1 2 3 4 5 6 7 docker run -p 9000:9000 --name minio \\ -e \u0026#34;MINIO_ACCESS_KEY=mogu2018\u0026#34; \\ --privileged=true \\ -e \u0026#34;MINIO_SECRET_KEY=mogu2018\u0026#34; \\ -v /home/minio/data:/data \\ -v /home/minio/config:/root/.minio \\ minio/minio server /data 上面的配置中，包含两个重要的信息【以后登录时会用到，可以修改成自己的】\nMINIO_ACCESS_KEY：公钥 MINIO_SECRET_KEY：密钥 运行成功后，我们就能看到我们下面的提示信息\n如果需要后台运行，使用这条语句\n1 2 3 4 5 6 7 docker run --privileged -d -it -p 9000:9000 --name minio \\ -e \u0026#34;MINIO_ACCESS_KEY=mogu2018\u0026#34; \\ --privileged=true \\ -e \u0026#34;MINIO_SECRET_KEY=mogu2018\u0026#34; \\ -v /home/minio/data:/data \\ -v /home/minio/config:/root/.minio \\ minio/minio server /data 访问 我们只需要访问上面提到的ip地址，就能够进入到我们的页面了【部署云服务器，需要自行开启安全组！】\n1 http://192.168.1.101:9000 会有一个不错的登录页面，我们输入刚刚配置的账号和密码 mogu2018 mogu2018 即可进入\n创建bucket 我们首先需要创建一个桶，可以当成是一个目录，点击我们的右下角 加号 按钮，选择 create bucket进行创建\n我们创建一个叫 mogublog 的桶，创建完成后，在侧边栏就能够看到我们刚刚创建的了\n上传文件 然后我们选中我们的桶，在点击加号，选择 upload file进行文件上传\n上传成功后，即可看到我们刚刚上传的文件列表了~\nSpringBoot整合MinIO 参考文档：Java Client API文档\n修改权限 如果要使用SDK，比如Java客户端来操作我们的minio的话，那么我们还需要修改一下我们的bucket权限\n首先点击我们的mogublog的右边区域，点击Edit policy，然后添加我们的权限为 可读可写，保存即可\n添加依赖 1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.minio\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;minio\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;7.0.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 添加application.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 server: port: 9001 spring: application: name: minio-application main: allow-bean-definition-overriding: true minio: endpoint: http://192.168.1.101:9000 accessKey: mogu2018 secretKey: mogu2018 bucketImageName: mogublog 添加配置文件 然后我们需要编写配置文件，用于初始化配置 MinioClient装载到spring容器中\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Configuration public class MinIoConfig { @Value(\u0026#34;${minio.endpoint}\u0026#34;) private String endpoint; @Value(\u0026#34;${minio.accessKey}\u0026#34;) private String accessKey; @Value(\u0026#34;${minio.secretKey}\u0026#34;) private String secretKey; @Bean public MinioClient minioClient() throws Exception{ // 使用MinIO服务的URL，端口，Access key和Secret key创建一个MinioClient对象 MinioClient minioClient = new MinioClient(endpoint, accessKey, secretKey); return minioClient; } } 编写Controller 然后在写一个前端控制器\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @RestController public class MinIoController { @Autowired MinioClient minioClient; @PostMapping(\u0026#34;/upload\u0026#34;) public String upload(@RequestParam(\u0026#34;data\u0026#34;) MultipartFile data) throws Exception{ String fileName = data.getOriginalFilename(); InputStream inputStram = data.getInputStream(); minioClient.putObject( PutObjectArgs.builder().bucket(\u0026#34;mogublog\u0026#34;).object(fileName).stream( inputStram, data.getSize(), -1) .contentType(data.getContentType()) .build()); return \u0026#34;上传成功\u0026#34;; } @PostMapping(\u0026#34;/download\u0026#34;) public String download(@RequestParam(\u0026#34;fileName\u0026#34;)String fileName) throws Exception{ String url = minioClient.presignedGetObject(\u0026#34;mogublog\u0026#34;, fileName, 60*60*24*7); return url; } } 测试图片上传 下面我们就需要进行测试了，我们运行我们的项目，然后使用postman进行上传测试\n首先我们在postman中添加我们的上传接口，然后在修改请求头中添加Content-Type\n1 Content-Type multipart/form-data 然后在选择我们的图片上传\n最后在刷新MinIO，就能够看到我们刚刚上传的文件了\n我们可以通过下面的地址直接访问我们的图片\n1 http://192.168.1.101:9000/mogublog/1578926382309.jpg ","permalink":"https://ktzxy.top/posts/b9mqlkeyf4/","summary":"对象存储MinIO入门简介","title":"对象存储MinIO入门简介"},{"content":"sqlserver链接服务器到mysql SQL Server数据库如何添加mysql链接服务器（Windows系统）_sqlserver链接服务器mysql-CSDN博客\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 USE [master]; GO -- ======【修改以下参数】====== DECLARE @LinkedServerName NVARCHAR(128) = N\u0026#39;MYSQLCON\u0026#39;; --链接服务器名 DECLARE @ODBC_DSN NVARCHAR(128) = N\u0026#39;mysql_odbc\u0026#39;; --mysqlodbc 名，必须保持一致 DECLARE @RemoteUser NVARCHAR(128) = N\u0026#39;admin\u0026#39;; --账号 DECLARE @RemotePassword NVARCHAR(128) = N\u0026#39;admin123\u0026#39;; --密码 -- ============================ -- 删除已存在的链接服务器 IF EXISTS (SELECT 1 FROM sys.servers WHERE name = @LinkedServerName) EXEC sp_dropserver @LinkedServerName, \u0026#39;droplogins\u0026#39;; -- 创建链接服务器（MSDASQL + ODBC DSN） EXEC sp_addlinkedserver @server = @LinkedServerName, @provider = N\u0026#39;MSDASQL\u0026#39;, @srvproduct = N\u0026#39;MySQL\u0026#39;, @datasrc = @ODBC_DSN; -- 设置登录凭据 EXEC sp_addlinkedsrvlogin @rmtsrvname = @LinkedServerName, @useself = N\u0026#39;False\u0026#39;, @locallogin = NULL, @rmtuser = @RemoteUser, @rmtpassword = @RemotePassword; -- 【可选】仅保留关键选项（防中文乱码） EXEC sp_serveroption @server = @LinkedServerName, @optname = N\u0026#39;use remote collation\u0026#39;, @optvalue = N\u0026#39;true\u0026#39;; sqlserver链接服务器到mysql 无dsn 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 USE [master]; GO -- ======【修改以下参数】====== DECLARE @LinkedServerName NVARCHAR(128) = N\u0026#39;MYSQLCON\u0026#39;; -- 链接服务器名称 DECLARE @MySQL_Host NVARCHAR(128) = N\u0026#39;127.0.0.1\u0026#39;; -- MySQL 服务器 IP 或主机名 DECLARE @MySQL_Port INT = 3306; -- MySQL 端口 DECLARE @MySQL_Database NVARCHAR(128) = N\u0026#39;gmsabdrmsdc\u0026#39;; -- 数据库名（可选，查询时可指定） DECLARE @RemoteUser NVARCHAR(128) = N\u0026#39;admin\u0026#39;; -- MySQL 用户名 DECLARE @RemotePassword NVARCHAR(128) = N\u0026#39;admin123\u0026#39;; -- MySQL 密码 -- 注意：驱动名称必须与系统中安装的 MySQL ODBC 驱动完全一致！ DECLARE @DriverName NVARCHAR(256) = N\u0026#39;MySQL ODBC 8.1 Unicode Driver\u0026#39;; -- 常见驱动名还有： -- \u0026#39;MySQL ODBC 8.0 ANSI Driver\u0026#39; -- \u0026#39;MySQL ODBC 5.3 Unicode Driver\u0026#39; -- 可通过 ODBC 数据源管理器查看 -- ============================ -- 删除已存在的链接服务器 IF EXISTS (SELECT 1 FROM sys.servers WHERE name = @LinkedServerName) EXEC sp_dropserver @LinkedServerName, \u0026#39;droplogins\u0026#39;; -- 构造连接字符串（关键！） DECLARE @ProvStr NVARCHAR(1024); SET @ProvStr = N\u0026#39;DRIVER={\u0026#39; + @DriverName + N\u0026#39;};\u0026#39; + N\u0026#39;SERVER=\u0026#39; + @MySQL_Host + N\u0026#39;;\u0026#39; + N\u0026#39;PORT=\u0026#39; + CAST(@MySQL_Port AS NVARCHAR(10)) + N\u0026#39;;\u0026#39; + CASE WHEN @MySQL_Database \u0026lt;\u0026gt; N\u0026#39;\u0026#39; THEN N\u0026#39;DATABASE=\u0026#39; + @MySQL_Database + N\u0026#39;;\u0026#39; ELSE N\u0026#39;\u0026#39; END + N\u0026#39;UID=\u0026#39; + @RemoteUser + N\u0026#39;;\u0026#39; + N\u0026#39;PWD=\u0026#39; + @RemotePassword + N\u0026#39;;\u0026#39; + N\u0026#39;OPTION=3;\u0026#39;; -- OPTION=3 表示启用 ANSI 引用符等，可选但推荐 -- 创建链接服务器（无 DSN） EXEC sp_addlinkedserver @server = @LinkedServerName, @srvproduct = N\u0026#39;MySQL\u0026#39;, @provider = N\u0026#39;MSDASQL\u0026#39;, @provstr = @ProvStr; -- 【重要】对于 MSDASQL 无 DSN，通常不需要再调用 sp_addlinkedsrvlogin！ -- 因为用户名/密码已在连接字符串中提供。 -- 但如果仍提示登录失败，可尝试添加（但一般不建议重复）： -- EXEC sp_addlinkedsrvlogin -- @rmtsrvname = @LinkedServerName, -- @useself = N\u0026#39;False\u0026#39;, -- @locallogin = NULL, -- @rmtuser = @RemoteUser, -- @rmtpassword = @RemotePassword; -- 设置关键选项 EXEC sp_serveroption @server = @LinkedServerName, @optname = N\u0026#39;use remote collation\u0026#39;, @optvalue = N\u0026#39;true\u0026#39;; EXEC sp_serveroption @server = @LinkedServerName, @optname = N\u0026#39;data access\u0026#39;, @optvalue = N\u0026#39;true\u0026#39;; 查询\n1 SELECT * FROM OPENQUERY(MYSQLCON, \u0026#39;SELECT * from 表名\u0026#39;); sqlserver 链接服务器到sqlserver 无dsn 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 USE [master]; GO -- 参数化（方便修改） DECLARE @LinkedServerName NVARCHAR(128) = N\u0026#39;fkt\u0026#39;; DECLARE @RemoteHost NVARCHAR(128) = N\u0026#39;171.111.192.34,2431\u0026#39;; -- 替换为实际 IP,端口 DECLARE @RemoteUser NVARCHAR(128) = N\u0026#39;sa\u0026#39;; DECLARE @RemotePassword NVARCHAR(128) = N\u0026#39;@fkt123..\u0026#39;; -- 删除已存在的（可选，避免报错） IF EXISTS (SELECT * FROM sys.servers WHERE name = @LinkedServerName) EXEC sp_dropserver @LinkedServerName, \u0026#39;droplogins\u0026#39;; -- 创建链接服务器 EXEC sp_addlinkedserver @server = @LinkedServerName, @provider = N\u0026#39;SQLNCLI\u0026#39;, @srvproduct = N\u0026#39;SqlServer\u0026#39;, @datasrc = @RemoteHost; -- 设置登录凭据 EXEC sp_addlinkedsrvlogin @rmtsrvname = @LinkedServerName, @useself = N\u0026#39;False\u0026#39;, @locallogin = NULL, @rmtuser = @RemoteUser, @rmtpassword = @RemotePassword; sqlserver 链接服务器 oracle\nInstant Client for Microsoft Windows (x64) 64-bit\n选择：\nInstant Client Package - Basic Instant Client Package - OLE DB（关键！） 版本需与 Oracle 服务器兼容（如 19c、21c） 注册 OLE DB 驱动（通常安装后自动注册） 驱动名称为：==OraOLEDB.Oracle==\n配置 tnsnames.ora（可选但推荐） 文件路径示例：C:\\oracle\\instantclient_19_20\\network\\admin\\tnsnames.ora\n1 2 3 4 5 6 7 8 ORCL = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = 192.168.1.50)(PORT = 1521)) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = orcl) ) ) 方式一：使用 TNS 别名（推荐，更清晰）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 USE [master]; GO -- ======【修改参数】====== DECLARE @LinkedServerName NVARCHAR(128) = N\u0026#39;ORACLE_LS\u0026#39;; DECLARE @TNS_Alias NVARCHAR(128) = N\u0026#39;ORCL\u0026#39;; -- tnsnames.ora 中的别名 DECLARE @OracleUser NVARCHAR(128) = N\u0026#39;scott\u0026#39;; DECLARE @OraclePassword NVARCHAR(128) = N\u0026#39;tiger\u0026#39;; -- ======================== -- 删除已存在的 IF EXISTS (SELECT 1 FROM sys.servers WHERE name = @LinkedServerName) EXEC sp_dropserver @LinkedServerName, \u0026#39;droplogins\u0026#39;; -- 创建链接服务器（使用 TNS 别名） EXEC sp_addlinkedserver @server = @LinkedServerName, @srvproduct = N\u0026#39;Oracle\u0026#39;, @provider = N\u0026#39;OraOLEDB.Oracle\u0026#39;, @datasrc = @TNS_Alias; -- 对应 tnsnames.ora 中的别名 -- 设置登录凭据 EXEC sp_addlinkedsrvlogin @rmtsrvname = @LinkedServerName, @useself = N\u0026#39;False\u0026#39;, @locallogin = NULL, @rmtuser = @OracleUser, @rmtpassword = @OraclePassword; -- 设置关键选项 EXEC sp_serveroption @server = @LinkedServerName, @optname = N\u0026#39;data access\u0026#39;, @optvalue = N\u0026#39;true\u0026#39;; EXEC sp_serveroption @server = @LinkedServerName, @optname = N\u0026#39;rpc out\u0026#39;, @optvalue = N\u0026#39;true\u0026#39;; EXEC sp_serveroption @server = @LinkedServerName, @optname = N\u0026#39;collation compatible\u0026#39;,@optvalue = N\u0026#39;false\u0026#39;; EXEC sp_serveroption @server = @LinkedServerName, @optname = N\u0026#39;use remote collation\u0026#39;,@optvalue = N\u0026#39;true\u0026#39;; 方式二：直接写连接字符串（无需 tnsnames.ora）\n1 2 3 4 5 6 7 8 -- 构造数据源（格式：//host:port/service_name） DECLARE @DataSource NVARCHAR(256) = N\u0026#39;//192.168.1.50:1521/orcl\u0026#39;; EXEC sp_addlinkedserver @server = N\u0026#39;ORACLE_LS\u0026#39;, @srvproduct = N\u0026#39;Oracle\u0026#39;, @provider = N\u0026#39;OraOLEDB.Oracle\u0026#39;, @datasrc = @DataSource; 查询\n1 2 3 4 5 -- 测试查询（注意 Oracle 表名大写！） SELECT * FROM OPENQUERY(ORACLE_LS, \u0026#39;SELECT * FROM SCOTT.EMP WHERE ROWNUM \u0026lt;= 5\u0026#39;); -- 或使用四部分命名（不推荐，可能有元数据问题） -- SELECT * FROM ORACLE_LS..SCOTT.EMP; sqlserver链接oracle数据库 ODBC + DSN-less 需要instantclient_21_19 包含odbc_install.exe\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 USE [master]; GO DECLARE @ProvStr NVARCHAR(1024); SET @ProvStr = N\u0026#39;DRIVER={Oracle in instantclient_21_19};\u0026#39; + N\u0026#39;DBQ=153.0.165.91:16161/orcl;\u0026#39; + -- ← 关键！使用 DBQ + EZ Connect N\u0026#39;UID=dherp_bzk;\u0026#39; + N\u0026#39;PWD=qwer1234;\u0026#39;; -- 删除已存在的链接服务器 IF EXISTS (SELECT 1 FROM sys.servers WHERE name = N\u0026#39;ORACLE_LS\u0026#39;) EXEC sp_dropserver N\u0026#39;ORACLE_LS\u0026#39;, \u0026#39;droplogins\u0026#39;; -- 创建链接服务器 EXEC sp_addlinkedserver @server = N\u0026#39;ORACLE_LS\u0026#39;, @srvproduct = N\u0026#39;Oracle\u0026#39;, @provider = N\u0026#39;MSDASQL\u0026#39;, @provstr = @ProvStr; -- 启用数据访问 EXEC sp_serveroption @server = N\u0026#39;ORACLE_LS\u0026#39;, @optname = N\u0026#39;data access\u0026#39;, @optvalue = N\u0026#39;true\u0026#39;; ","permalink":"https://ktzxy.top/posts/7is1offwg1/","summary":"数据库相关链接","title":"数据库相关链接"},{"content":"1. 反射的概述 1.1. 动态语言的概念 动态语言指程序在运行时可以改变其结构的语言，比如新的属性或方法的添加、删除等结构上的变化。JavaScript、Ruby、Python 等都属于动态语言；C、C++ 不属于动态语言。从反射的角度来说，Java 属于半动态语言。\n1.2. 什么是反射 动态获取类和对象的信息，以及动态调用对象的方法的功能被称为 Java 语言的反射机制。\n在程序运行状态中，对于任意一个类，都能够获取类的所有属性和方法；对于任意一个对象，都能够调用其任意一个方法和属性。\n反射是一种机制，利用该机制可以在程序运行过程中对类进行解剖并操作类中所有成员。因为在 Java 中把一切的东西都抽象成对象，所以类本身也可以抽象成一个对象。类中的构造方法、方法、成员变量等也可以抽象成对象。反射就是通过类对象Class，得到类中有哪些构造方法Constructor、成员方法Method、成员变量（字段）Field 等对象。程序编译分为以下两种：\n静态编译：在编译时确定类型，绑定对象 动态编译：运行时确定类型，绑定对象 Notes: 反射的前提条件是，获得该类Class对象，就是字节码文件对象。\n1.3. 反射机制优缺点 优点：运行期类型的判断，动态加载类，提高代码灵活度。 缺点：性能瓶颈：反射相当于一系列解释操作，通知 JVM 要做的事情，性能比直接的java代码要慢很多 1.4. 反射的应用 Java中的对象有两种类型：编译时类型和运行时类型。\n编译时类型指在声明对象时所采用的类型 运行时类型指为对象赋值时所采用的类型 1 Person person = new Student(); 比如以上代码，编译时类型为Person，运行时类型是Student，因此无法在编译时获取在 Student 类中定义的方法。因为程序在编译期间无法预知该对象和类的真实信息，只能通过运行时信息来发现该对象和类的真实信息，而其真实信息（对象的属性和方法）通常通过反射机制来获取，这便是Java语言中反射机制的核心功能。\n实际开发中还有如下的应用场景：\n开发IDE（集成开发环境），比如:Eclipse 框架的学习或框架的开发（Spring, MyBatis、dubbo） 反射中对象的规律：有 Declared 的可以得到所有声明的方法，没有 Declared 的只能得到公共的方法 伪泛型：在编译时期的进行限制，但利用反射可以在运行时候进行操作。 动态代理设计模式也采用了反射机制 2. Class 类 2.1. 获取 Class 对象 JDK 中有4种方式获取 Class 对象。值得注意的是，4 种方式得到的类对象，是同一个对象，因为 Class 文件在 JVM 中只存在一份\n方式1: 通过对象实例继承 Object 类中的getClass()方法 1 2 Person p = new Person(); Class c = p.getClass(); 方式2: 通过 类名.class 获取到字节码文件对象。任意数据类型都具备一个 class 静态属性，该类要和当前类在同一个项目中 1 Class c2 = Person.class; 方式3: 通过Class类中的静态方法 forName(String str)。将类名作为字符串传递给 Class 类中的静态方法 forName 即可。而类名必须全名：包名.类名 (在同一个项目下的类全名，不能跨项目的) 1 Class c3 = Class.forName(\u0026#34;com.moon.Person\u0026#34;); Notes: 第三种和前两种的区别前两种你必须明确Person类型。后面是指定这种类型的字符串即可，这种扩展更强，我不需要知道你的类，我只提供字符串，按照配置文件加载就可以了\n方式4：通过类加载器传入类路径获取 1 ClassLoader.getSystemClassLoader().loadClass(\u0026#34;com.moon.TargetObject\u0026#34;); Notes: 通过类加载器获取 Class 对象不会进行初始化，意味着不进行包括初始化等一系列步骤，静态代码块和静态对象不会得到执行\n2.1.1. 番外：通过 IDE 获取类全限定名 eclipse 中获取类全名的方法：右键类名 -\u0026gt; 选择 Copy Qualified Name\nidea 中获取类全名的方法：右键类文件或者类名 -\u0026gt; 选择 Copy Reference。快捷键：Ctrl+Shift+Alt+C\n2.2. Class 类常用方法 1 public String getName(); 得到类的完全限定类名。如：java.util.Date API文档说明: 以 String 的形式返回此 Class 对象所表示的实体（类、接口、数组类、基本类型或 void）名称。\n1 public String getSimpleName(); 得到类名。 如：Date API文档说明: 返回源代码中给出的底层类的简称。如果底层类是匿名的则返回一个空字符串。\n1 public T newInstance() 通过无参数构造方法创建对象。使用此方法获取一个空参构造的对象前提如下： 被反射的类，必须具有空参数构造方法 构造方法权限必须是 public 示例： 1 2 3 4 5 6 Class c = Class.forName(xx.xxx.xxx.类名); // (xx.xxx.xxx是包全名) Object obj = c.newInstance(); // 使用Class类方法创建被反射类的对象 // 或使用泛型指定Class的类型： Class\u0026lt;Student\u0026gt; c = (Class\u0026lt;Student\u0026gt;) Class.forName(xx.Student); Student s = c.newInstance(); API文档说明: 创建此 Class 对象所表示的类的一个新实例。如同用一个带有一个空参数列表的 new 表达式实例化该类。如果该类尚未初始化，则初始化这个类。\n1 public boolean isAssignableFrom(Class\u0026lt;?\u0026gt; cls) 判定此 Class 对象所表示的类或接口与方法参数 Class\u0026lt;?\u0026gt; cls 所表示的类或接口是否相同，或者是否为 cls 的超类或超接口。如果是则返回 true；否则返回 false。如果该 Class 表示一个基本类型，且指定的 Class 参数正是该 Class 对象，则该方法返回 true；否则返回 false。\n1 public URL getResource(String name); 如果资源文件是在src文件夹下，资源文件路径: /文件名，代表从bin目录指定名称的资源文件。 如果资源文件和当前类在同一个文件夹下时，资源文件路径可以省略 /，直接给文件名。 返回的URL对象（统一资源定位符，不能包含中文字符，开发时注意） Tips: URL类的成员方法 String getPath(); 获得资源文件的绝对路径字符串。\n1 public InputStream getResourceAsStream(String name); 如果资源文件是在src文件夹下，资源文件路径:“/文件名”，代表从bin目录指定名称的资源文件。 如果资源文件和当前类在同一个文件夹下时，资源文件路径可以省略/，直接给文件名。 返回与资源文件关联的字节输入流对象（返回的对象：BufferedInputStream）。 2.3. 综合示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class Demo02 { public static void main(String[] args) throws IOException { // 获得当前类的Class对象 InputStream in = Demo02.class.getResourceAsStream(\u0026#34;a.txt\u0026#34;); System.out.println(in); // 获得资源文件对饮的URL对象 URL url = Demo02.class.getResource(\u0026#34;/a.txt\u0026#34;); // System.out.println(url.getPath()); // 创建字节输入流 FileInputStream fis = new FileInputStream(url.getPath()); System.out.println(fis.read()); // 关闭流 fis.close(); } } 3. AccessibleObject 类 3.1. 概述 对于公共成员、默认（打包）访问成员、受保护成员和私有成员，在分别使用 Field、Method 或 Constructor 对象来设置或获取字段、调用方法，或者创建和初始化类的新实例的时候，会执行访问检查。\nAccessibleObject 类是 Field、Method 和 Constructor 的父类。它提供了将反射的对象标记为在使用时取消默认 Java 语言访问控制检查的能力。\n3.2. 常用方法 1 public void setAccessible(boolean flag) throws SecurityException 设置是否取消权限检查（暴力反射） 参数值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。参数值为 false 则指示反射的对象应该实施 Java 语言访问检查。 示例： 1 反射获取的(Field、Method 或 Constructor)对象.setAccesible(true); 通过 Field、Method 或 Constructor 实例调用该方法后，才能调用或者操作相应的私有对象的方法或者属性。\nNotes: 一般不推荐访问私有，因为破坏了程序的封装性，安全性\n4. Constructor 类（构造方法） 在反射机制中，把类中的成员（构造方法、成员方法、成员变量）都封装成了对应的类进行表示。其中，构造方法使用 Constructor 类表示。每一个构造方法都是一个 Constructor 类的对象\n4.1. 获取构造方法实例 可通过 Class 类中提供的方法，获取一个或者多个 Constructor 构造方法对象\n1 public Constructor\u0026lt;T\u0026gt; getConstructor(Class\u0026lt;?\u0026gt;... parameterTypes); 获取 public 修饰，指定参数类型所对应的构造方法对象 1 public Constructor\u0026lt;T\u0026gt; getDeclaredConstructor(Class\u0026lt;?\u0026gt;... parameterTypes); 获取指定参数类型所对应的构造方法对象(包含 private 修饰) 示例： 1 Constructor\u0026lt;Student\u0026gt; con = c.getConstructor(xxx.class, xxx.class, xxx.class, xxx.class); 参数列表是该类的构造方法对应的参数列表的数据类型.class，个数也要和需要反射得到的构造方法一致。\n1 public Constructor\u0026lt;?\u0026gt;[] getConstructors(); 获取类中所有的 public 修饰的构造方法，返回一个 Constructor 类型数组 1 public Constructor\u0026lt;?\u0026gt;[] getDeclaredConstructors(); 获取类中所有的构造方法(包含private修饰)，返回一个 Constructor 类型数组 注：基本类型与引用类型类对象：在Java 中int.class 和 Integer.class 是2 种不同的类型。所以如果参数类型不匹配，也无法得到相应的构造方法，会出现如下异常：java.lang.NoSuchMethodException\n4.2. Constructor 类 newInstance 方法 反射方式获取构造方法后，创建对象使用到 Constructor 类的方法\n1 public T newInstance(Object... initargs); 指定构造方法的参数值(0~n)，创建一个 T 对象 4.3. 反射调用构造方法创建对象的步骤 通过反射方式，获取构造方法(私有)，创建对象，步骤如下：\n获取到 Class 对象; 获取指定的构造方法; 暴力访问, 通过 setAccessible(boolean flag) 方法（如果是私有） 通过构造方法类 Constructor 中的方法，创建对象; 4.4. 反射创建对象案例 反射获取构造方法创建对象(包含私有构造方法)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import java.lang.reflect.Constructor; /* *\t通过反射方式，获取构造方法(包含私有构造方法)，创建对象 */ public static void main(String[] args) throws Exception { // 获取Student的Class对象 // Class\u0026lt;Student\u0026gt; c = (Class\u0026lt;Student\u0026gt;) Class.forName(\u0026#34;com.exercise.Student\u0026#34;); // 或者直接类名.class; Class\u0026lt;Student\u0026gt; c = Student.class; // 获取Student的空参构造 Constructor\u0026lt;Student\u0026gt; con1 = c.getConstructor(); System.out.println(con1); // 创建无参对象 Student s1 = con1.newInstance(); System.out.println(s1); // 获取Student的有参构造 Constructor\u0026lt;Student\u0026gt; con2 = c.getConstructor(String.class, String.class, int.class, double.class); System.out.println(con2); // 创建有参对象 Student s2 = con2.newInstance(\u0026#34;001\u0026#34;, \u0026#34;凌月\u0026#34;, 23, 90); System.out.println(s2); // 获取Student的private有参构造方法 Constructor\u0026lt;Student\u0026gt; con3 = c.getDeclaredConstructor(String.class, String.class, int.class); System.out.println(con3); // 暴力反射 con3.setAccessible(true); // 创建prviate有参构造方法 Student s3 = con3.newInstance(\u0026#34;002\u0026#34;, \u0026#34;傷月\u0026#34;, 24); System.out.println(s3); } 5. Field 类（成员属性） 在反射机制中，把类中的成员变量使用类Field表示。可通过Class类中提供的方法获取成员变量：\n5.1. 获取类成员属性实例 通过 Class 类方法，返回一个成员变量\n1 public Field getField(String name); 获取指定的 public修饰的变量 1 public Field getDeclaredField(String name); 获取指定的任意变量 5.2. Class 类方法，返回多个成员变量 1 public Field[] getFields(); 获取所有public 修饰的成员变量 1 public Field[] getDeclaredFields(); 获取所有的成员变量(包含private修饰) 5.3. Field 类常用方法 反射方式获取成员属性 Field 实例后，可以使用以下方法对属性值进行操作\n1 public void set(Object obj, Object value) 给指定对象 Obj 的成员变量赋值为指定的值 value 1 public Object get(Object obj) 获得指定对象 obj 的当前成员变量的值 1 public String getName() 返回此 Field 对象表示的字段的名称(即成员变量的变量名称) 1 public Class\u0026lt;?\u0026gt; getType() 返回一个 Class 对象，它标识了此 Field 对象所表示字段的声明类型。 1 public boolean isAnnotationPresent(Class\u0026lt;? extends Annotation\u0026gt; annotationClass) 继承自 java.lang.reflect.AccessibleObject 类的方法，判断当前字段对象上是否标识某个注解。是则返回 true，否则返回 false 5.4. 反射操作类属性的步骤 通过反射，创建对象，获取指定的成员变量，进行赋值与获取值操作。步骤如下：\n获取 Class 对象 获取构造方法 通过构造方法，创建对象 获取指定的成员变量（私有成员变量，通过 setAccessible(boolean flag) 方法暴力访问和修改） 通过方法，给指定对象的指定成员变量赋值或者获取值 5.5. 反射获取类属性案例 通过反射方式，获取成员变量(私有成员变量)，并修改\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import java.lang.reflect.Constructor; import java.lang.reflect.Field; /* *\t通过反射方式，获取成员变量(私有成员变量)，并修改 */ public static void main(String[] args) throws Exception { // 获取Student的Class对象，直接类名.class; Class\u0026lt;Student\u0026gt; c = Student.class; // 通过反射方式获取Student的public有参构造 Constructor\u0026lt;Student\u0026gt; con = c.getConstructor(String.class, String.class, int.class, double.class); // 使用Constructor类的方法获取一个Student对象 Student s = con.newInstance(\u0026#34;001\u0026#34;, \u0026#34;敌法师\u0026#34;, 23, 90); // 输出Student对象[id=001, name=敌法师, age=23, score=90.0] System.out.println(s); // 获取public修饰的成员变量 Field f1 = c.getField(\u0026#34;name\u0026#34;); // [public java.lang.String com.exercise.Student.name] System.out.println(f1); // 获取成员变量的值[敌法师] System.out.println(f1.get(s)); // 修改成员变量的值 f1.set(s, \u0026#34;改改\u0026#34;); // 获取修改后成员变量的值[改改] System.out.println(f1.get(s)); // [id=001, name=改改, age=23, score=90.0] System.out.println(s); // 获取private修饰的成员变量 Field f2 = c.getDeclaredField(\u0026#34;id\u0026#34;); // private java.lang.String com.exercise.Student.id System.out.println(f2); // 强行反射修改成员变量 f2.setAccessible(true); // 获取私有的成员变量[001] System.out.println(f2.get(s)); // 修改私有的成员变量 f2.set(s, \u0026#34;00x\u0026#34;); // [id=00x, name=改改, age=23, score=90.0] System.out.println(s); // 获取该成员变量的名称,返回String类型，[name] System.out.println(f1.getName()); //获取该成员变量的类型，返回Class类型，[class java.lang.String] System.out.println(f1.getType()); } 6. Method 类（成员方法） 在反射机制中，把类中的成员方法使用 Method 类表示。\n6.1. 获取 Method 实例 可通过 Class 类中提供的方法获取成员方法\n1 public Method getMethod(String name, Class\u0026lt;?\u0026gt;... parameterTypes); 获取指定参数 public 修饰的方法 1 public Method getDeclaredMethod(String name, Class\u0026lt;?\u0026gt;... parameterTypes); 获取任意指定参数的方法，包含私有的 name 参数: 要查找的方法名称 parameterTypes 参数: 该方法的参数类型 示例： 1 Method\u0026lt;Student\u0026gt; met = c.getDeclaredMethod(\u0026#34;方法名\u0026#34;, xxx.class, xxx.class, xxx.class, xxx.class); 参数列表是需要获取的方法名;后面是该类的方法对应的参数列表的数据类型.class，个数也要和需要反射得到的方法一致。如果该方法没有参数，则只写方法名。\n1 public Method[] getMethods(); 获取本类与父类（接口）中所有public 修饰的方法（包括继承的所有方法）\n1 public Method[] getDeclaredMethods(); 获取本类中所有的方法(包含私有的，但不包括继承的方法)\n6.2. Method 类 invoke 方法 通过 Method 类的 invoke方法，可以实现动态调用类或接口上某个方法及访问该方法的信息。比如可以动态传入参数及将方法参数化。\n1 public Object invoke(Object obj, Object... args) 示例 1 2 3 4 5 // 调用对象方法。如果方法是无参，则可以不用写 method对象.invoke(该方法所在类对象, 方法的参数值); // 调用静态方法。 method对象.invoke(null(或该方法的所在的类对象), 方法的参数值); 执行指定对象obj中，当前Method对象所代表的方法，方法要传入的参数通过args指定。返回值为当前调用的方法的返回值 6.2.1. 通过反射调用指定方法的步骤（包括private） 获取成员方法（包括私有），步骤如下：\n获取Class对象 获取构造方法 通过构造方法，创建对象 获取指定的方法 开启暴力访问 执行找到的方法 6.2.2. 方法反射调用的案例 通过反射方式获取成员方法(私有成员变量)，并调用\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import java.lang.reflect.Constructor; import java.lang.reflect.Method; /* *\t通过反射方式，获取成员方法(私有成员变量)，并调用 */ public static void main(String[] args) throws Exception { // 获取Student的Class对象,直接类名.class Class\u0026lt;Student\u0026gt; c = Student.class; // 通过反射，获取Student公共无参构造方法，直接用Class类的newInstance方法获取 Student s = c.newInstance(); // 或者通过Constructor类获取 Constructor\u0026lt;Student\u0026gt; con = c.getConstructor(); Student s2 = con.newInstance(); // 获取公共方法 Method m1 = c.getMethod(\u0026#34;eat\u0026#34;); // 调用该方法 m1.invoke(s); // 获取私有方法 Method m2 = c.getDeclaredMethod(\u0026#34;sleep\u0026#34;); // 强制执行 m2.setAccessible(true); // 调用私有方法 m2.invoke(s); // 获取静态方法 Method m3 = c.getMethod(\u0026#34;play\u0026#34;); m3.invoke(s); // 或者 m3.invoke(null); } 上面三例使用到的Student类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public class Student { private String id; public String name; private int age; private double score; public Student() { super(); } public Student(String id, String name, int age, double score) { this.id = id; this.name = name; this.age = age; this.score = score; } @SuppressWarnings(\u0026#34;unused\u0026#34;) private Student(String id, String name, int age) { super(); this.id = id; this.name = name; this.age = age; } @Override public String toString() { return \u0026#34;Student [id=\u0026#34; + id + \u0026#34;, name=\u0026#34; + name + \u0026#34;, age=\u0026#34; + age + \u0026#34;, score=\u0026#34; + score + \u0026#34;]\u0026#34;; } // 私有方法 @SuppressWarnings(\u0026#34;unused\u0026#34;) private void sleep() { System.out.println(\u0026#34;正在睡觉\u0026#34;); } // 公共方法 public void eat() { System.out.println(\u0026#34;正在吃饭\u0026#34;); } // 静态方法 public static void play() { System.out.println(\u0026#34;正在玩耍\u0026#34;); } } 7. 反射获取超类 7.1. Class 类获取超类的方法 1 public Type getGenericSuperclass() 上面 Class 类对象的方法，用于获取此 Class 所表示的实体（类、接口、基本类型或 void）的直接超类的 Type。如果超类是参数化类型（泛型），则返回的 Type 对象能准确反映源代码中所使用的实际类型参数。如果以前未曾创建表示超类的参数化类型，则创建这个类型。有关参数化类型创建过程的语义，请参阅 ParameterizedType 声明。\n如果此 Class 对象是 Object 类、接口、基本类型或 void，则返回 null。如果此对象是一个数组类，则返回表示 Object 类的 Class 对象。\n7.2. 反射获取泛型参数示例 假设某个类继承带泛型的类\n1 2 class StudentDao extends BaseDao\u0026lt;Student\u0026gt; { } 通过该字节码对象的 getGenericSuperclass 方法获取其父类，然后判断返回的 Type 类型是否为 ParameterizedType，再通过 ParameterizedType 类的 getActualTypeArguments 获取父类中的实际类型参数的 Type 对象的数组。\n1 2 3 4 5 6 Type type = StudentDao.class.getGenericSuperclass(); System.out.println(type); if (type instanceof ParameterizedType ) { System.out.println(((ParameterizedType)type).getActualTypeArguments()[0]); // 因为示例只是一个泛型，所以直接获取第一个元素 } 8. Reflections 反射框架(待整理) 9. 反射与Properties案例 9.1. 案例1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 import java.io.File; import java.io.FileInputStream; import java.lang.reflect.Field; import java.util.Properties; import java.util.Set; /* *\t2.1需求 *\t1)有属性内容如下： *\t注意：文件放在项目根目录下 *\tstudent.properties *\tid=1 *\tname=Sandy *\tgender=\\u7537 *\tscore=100 *\t2)有一个 Student 类的属性：Student(String id, String name, String gender, String score)， *\t这里将所有的属性设置成了 String 类型。 *\t3)通过 Properties 类读取 student.properties 文件 *\t4)使用反射的方式把属性文件中读取的数据赋值给一个实例化好的 Student 对象， *\tStudent 类中的属性名与student.properties 要对应。 *\t5)重写 Student 的 toString()方法，输出对象的属性值 。 */ public class MoonZero { public static void main(String[] args) throws Exception { // 创建配置文件的路径对象 File file = new File(\u0026#34;student.properties\u0026#34;); // 创建字节输入流对象 FileInputStream fis = new FileInputStream(file); // 创建Properties集合并读取配置文件 Properties props = new Properties(); props.load(fis); // 关闭流资源 fis.close(); String className = \u0026#34;com.exercise.Student\u0026#34;; // 调用方法获取实例化的对象（第一种方法容易出现异常，使用第二种） Student s = (Student) createObject(className, props); Student s1 = (Student) createObject1(className, props); // 输出结果:Student [id=1, name=Sandy, gender=男, score=100] System.out.println(s); System.out.println(s1); } /** * 读取Properties配置文件，使用反射实例化一个Student对象 * 这种方法有缺陷，如果以集合的键去获取成员变量对象，如果键不与对象的名字一致， * 则会出现java.lang.NoSuchFieldException 找不到字段的异常 * @param className * 需要实例化的对象 * @param props * Properties配置文件 * @return 实例化的对象 * @throws Exception */ @SuppressWarnings(\u0026#34;rawtypes\u0026#34;) public static Object createObject(String className, Properties props) throws Exception { // 获取类的Class对象 Class c = Class.forName(className); // 获取一个无参的Studnet对象 Object obj = c.newInstance(); // 获取Properties集合的键Set集合 Set\u0026lt;String\u0026gt; set = props.stringPropertyNames(); // 遍历集合，使用反射给对象赋值 for (String s : set) { // 获取成员变量对象 Field f = c.getDeclaredField(s); // 暴力反射给成员变量赋值 f.setAccessible(true); f.set(obj, props.getProperty(s)); } return obj; } /** * 读取Properties配置文件，使用反射实例化一个Student对象（第2种） * * @param className * 需要实例化的对象 * @param props * Properties配置文件 * @return 实例化的对象 * @throws Exception */ @SuppressWarnings(\u0026#34;rawtypes\u0026#34;) public static Object createObject1(String className, Properties props) throws Exception { // 获取类的Class对象 Class c = Class.forName(className); // 获取一个无参的Studnet对象 Object obj = c.newInstance(); // 获取成员变量对象数组 Field[] arr = c.getDeclaredFields(); // 遍历数组使用反射给成员变量赋值 for (Field f : arr) { // 获取成员变量名称 String key = f.getName(); // Properties对应的值 String value = props.getProperty(key); // 暴力反射 f.setAccessible(true); f.set(obj, value); } return obj; } } 10. 代理模式 10.1. 代理模式的概述 代理模式的作用：拦截对真实对象的直接访问，并增加一些功能。\n10.2. 代理模式的分类 代理模式分成静态代理和动态代理\n区别：静态代理字节码文件已经生成；动态代理的字节码文件随用随加载。\n10.2.1. 静态代理模式 静态代理模式的优点：\n不需要修改目标对象就实现了目标对象功能的增加 缺点：\n一个真实对象必须对应一个代理对象，如果大量使用会导致类的数量急速增长。 如果抽象对象中存在很多方法，则代理对象也要同样实现相应数量的方法。 10.2.2. 动态代理模式 动态代理模式特点：\n动态生成代理对象，不用手动编写代理对象 不需要编写目标对象中所有同名的方法 11. JDK 动态代理 11.1. 概述 JDK 动态代理的核心主要涉及到 java.lang.reflect 包中的两个类：Proxy 和 InvocationHandler。\nInvocationHandler 是一个接口，通过实现该接口来定义代理后的处理逻辑，并可以通过反射机制来调用目标类的原代码，动态将代理逻辑和原逻辑编制在一起。 Proxy 利用 InvocationHandler 动态创建一个符合某一接口的实例，生成目标类的代理对象。 11.2. Proxy 类创建代理对象 创建 JDK 的动态代码对象，需要使用 Proxy 类的 newProxyInstance 方法。\n1 public static Object newProxyInstance(ClassLoader loader, Class\u0026lt;?\u0026gt;[] interfaces, InvocationHandler h) 参数ClassLoader loader：类加载器对象，和被代理对象使用相同的类加载器 参数Class\u0026lt;?\u0026gt;[] interfaces：真实对象所现实的所有接口的class对象数组，即和被代理对象具有相同的行为，实现相同的接口。 参数InvocationHandler h：回调处理对象，具体的代理操作，InvocationHandler是一个接口，需要传入一个实现了此接口的实现类。（可以使用匿名内部类来现实） 回调处理对象注意事项：不要在invoke方法中通过proxy对象调用方法，因为会产生死循环\n真实对象与代理对象的是实现了共同接口，所以返回的Object代理对象需要转成接口类型\n引用网络资料的解释：为什么jdk动态代理的对象必须实现一个统一的接口，其实可以大致理解为，代理类本身已经 extends 了 Proxy，如果传入的是父类，很可能出现这种情况：“public class $Proxy1 extends Proxy extends 传入的父类”；这个明显在 java 中是不允许的，Java 只支持单继承，但是实现接口是完全可以的。\n11.3. InvocationHandler 接口 InvocationHandler的invoke方法，在这方法中实现对真实方法的增强或拦截\n该方法使用了一个设计模式：策略模式\n参与者明确 目标明确 中间实现的过程就是策略 1 2 3 public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } invoke 方法的作用：每当通过代理对象调用方法时，都会被该方法拦截。方法参数说明如下：\n参数Object proxy：代理对象本身（不一定每次都用得到）。即方法 newProxyInstance()方法返回的代理对象，该对象一般不要在 invoke 方法中使用，容易出现递归调用。 参数Method method：代理对象调用的方法（即被拦截真实对象的方法），是真实对象的方法对象，会进行多次调用，每次调用 method 对象都不同。 参数Object[] args：代理对象调用方法时传递的参数，该参数会传递给真实对象的方法。 返回值 Object：一般返回真实对象方法执行后的结果。 11.4. Class 类 getInterfaces 方法 1 public Class\u0026lt;?\u0026gt;[] getInterfaces() 作用：返回调用对象的类所有的接口数组。如果此对象表示一个类，则返回值是一个数组，它包含了表示该类所实现的所有接口的对象。 示例（伪代码）\n1 2 3 public class Demo implements A, B, C, …… { } Class[] arr = Demo.class.getInterfaces(); 11.5. 动态代理模式的开发步骤(案例) 先明确要被代理的功能(方法)是什么 然后将需要被代理的(功能)方法定义的接口中 真实对象实现接口重写方法 创建真实对象，但不通过真实对象直接调用方法 利用 Proxy 类创建代理对象 真实对象的类加载器 真实对象现实的所有接口的 Class 类型数组 回调处理对象，拦截对代理方法调用 通过代理对象调用相关方法，方法就会被回调处理对象拦截。其实是调用 InvocationHandler 接口中的 invoke() 方法，值得注意的是，接口中每个方法的调用都会触发 InvocationHandler.invoke 方法，可以在拦截的方法中执行相关的判断。 11.5.1. 示例 1 注：定义了个有参构造方法，传入被代理对象。也可以使用直接使用final修饰被代理的成员变量。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 package level01.test04; import java.lang.reflect.Proxy; /* * 关卡1训练案例4 * 一、按以下要求编写代码： * 1. 定义一个接口：Person，包含以下抽象方法：work() * 1) 定义一个类 Student，实现 Person 接口，实现 work()方法，打印输出：”我做 Java 项目”； * 2. 定义一个 MyHandler，实现 InvocationHandler 接口，有如下要求： * 1) 定义成员属性--被代理对象： * 2) 定义构造方法，为被代理对象赋值； * 3) 定义一个方法 before()，打印输出：”项目设计”； * 4) 定义一个方法 after()，打印输出：”项目总结”； * 5) 重写 invoke()方法，要求在调用方法前执行 before()方法，在调用方法后执行 after()方法。 * 3. 定义一个测试类：Test，包含 main()方法。要求用动态代理获取 Student 类的代理对象，并执行 work()方法。 */ public class Test01_04 { public static void main(String[] args) { // 创建学生类对象 Student stu = new Student(); // 创建MyHandler对象 MyHandler h = new MyHandler(stu); // 创建代理对象 Person proxy = (Person) Proxy.newProxyInstance(stu.getClass().getClassLoader(), stu.getClass().getInterfaces(), h); // 使用代理对象调用学生类的方法 proxy.work(); } } package level01.test04; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; // 定义MyHandler实现InvocationHandler接口 public class MyHandler implements InvocationHandler { private Object target; public MyHandler(Object target) { this.target = target; } // 重写invoke方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object obj = method.invoke(target, args); after(); return obj; } public static void before() { System.out.println(\u0026#34;项目设计\u0026#34;); } public static void after() { System.out.println(\u0026#34;项目总结\u0026#34;); } } package level01.test04; public class Student implements Person{ @Override public void work() { System.out.println(\u0026#34;我做 Java 项目\u0026#34;); } } package level01.test04; public interface Person { public void work(); } 11.5.2. 示例 2 或者这样玩，在InvocationHandler接口的实现类中直接使用Proxy的newProxyInstance方法，返回一个代理对象。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package level01.test04; public class Test01_04 { public static void main(String[] args) { // 创建学生类对象 Student stu = new Student(); // 创建MyHandler对象 MyHandler h = new MyHandler(); // 调用MyHandler方法直接获取代理对象 Person proxy = (Person) h.getProxy(stu); // 使用代理对象调用学生类的方法 proxy.work(); } } import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class MyHandler implements InvocationHandler { private Object target; public Object getProxy(Object target) { this.target = target; return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } // 重写invoke方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object obj = method.invoke(target, args); after(); return obj; } public static void before() { System.out.println(\u0026#34;项目设计\u0026#34;); } public static void after() { System.out.println(\u0026#34;项目总结\u0026#34;); } } 总结这种方式的好处：\nProxy类的代码量被固定下来，不会因为业务的逐渐庞大而庞大； 可以实现AOP编程(面向切面编程)，实际上静态代理也可以实现，总的来说，AOP可以算作是代理模式的一个典型应用； 解耦，通过参数就可以判断真实类，不需要事先实例化，更加灵活多变。 11.5.3. 框架学习阶段时案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 public interface IActor { /** * 基本表演 */ void basicAct(float money); /** * 精彩表演 */ void wonderfulAct(float money); } public class ActorImpl implements IActor { @Override public void basicAct(float money) { System.out.println(\u0026#34;拿到 \u0026#34; + money + \u0026#34; 元，开始基本的表演!!\u0026#34;); } @Override public void wonderfulAct(float money) { System.out.println(\u0026#34;拿到 \u0026#34; + money + \u0026#34; 元，开始精彩的表演!!\u0026#34;); } } /** * 动态代理测试 */ public class Client { public static void main(String[] args) { // 获取接口实现类 IActor actor = new ActorImpl(); System.out.println(\u0026#34;=============没有使用动态代理模式前=============\u0026#34;); actor.basicAct(108.89F); actor.wonderfulAct(3000.1F); System.out.println(\u0026#34;=============没有使用动态代理模式后=============\u0026#34;); // 获取代理 IActor proxy = (IActor) Proxy.newProxyInstance(ActorImpl.class.getClassLoader(), ActorImpl.class.getInterfaces(), new InvocationHandler() { // 重写拦截的方法 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 获取调用方法的名字 String mothodName = method.getName(); // 获取调用方法的参数 float money = (float) args[0]; // 开始判断 if (\u0026#34;basicAct\u0026#34;.equals(mothodName)) { // 对象调用了basicAct方法 if (money \u0026gt; 2000) { // 满足条件才执行方法 proxy = method.invoke(actor, money / 2); } } else if (\u0026#34;wonderfulAct\u0026#34;.equals(mothodName)) { // 对象调用了wonderfulAct方法 if (money \u0026gt; 5000) { // 满足条件才执行方法 proxy = method.invoke(actor, money / 2); } } return proxy; } }); // 使用代理调用方法 proxy.basicAct(1003F); proxy.wonderfulAct(6234F); } } 11.6. Proxy 类 newProxyInstance 生成代理对象的实现原理 11.6.1. 模拟 JDK 的动态代理实现示例 定义一个接口\n1 2 3 4 5 public interface DemoInterface { void foo(); int bar(); } 定义用于测试的被代理类\n1 2 3 4 5 6 7 8 9 10 11 12 public class DemoTarget implements DemoInterface { @Override public void foo() { System.out.println(\u0026#34;被代理类 DemoTarget.foo() 方法执行了...\u0026#34;); } @Override public int bar() { System.out.println(\u0026#34;被代理类 DemoTarget.bar() 方法执行了...\u0026#34;); return 99; } } 重点：定义模拟 JDK 动态代理生成的代理类 $Proxy0\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 public class $Proxy0 extends Proxy implements DemoInterface { static Method foo; static Method bar; static { try { // 反射获取被代理类的中方法对象 foo = DemoTarget.class.getMethod(\u0026#34;foo\u0026#34;); bar = DemoTarget.class.getMethod(\u0026#34;bar\u0026#34;); } catch (NoSuchMethodException e) { e.printStackTrace(); throw new NoSuchMethodError(e.getMessage()); } } // 继承的 Proxy 类中，有一个 InvocationHandler 类型的属性，调用父类构造，给属性设值 public $Proxy0(InvocationHandler h) { super(h); } @Override public void foo() { try { // 方法无参数 h.invoke(this, foo, new Object[0]); } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } @Override public int bar() { try { // 获取方法调用的返回值，并返回 Object result = h.invoke(this, bar, new Object[0]); return (int) result; } catch (RuntimeException | Error e) { throw e; } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } } 测试代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void testCustomProxy() { // 创建自己实现的代理对象 DemoInterface demo = new $Proxy0(new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 模拟功能增强 System.out.println(\u0026#34;代理功能增强...\u0026#34;); // 调用目标的方法，并返回 return method.invoke(new DemoTarget(), args); } }); // 调用接口的方法，由代理来实现 demo.foo(); demo.bar(); } 测试运行结果\n1 2 3 4 代理功能增强... 被代理类 DemoTarget.foo() 方法执行了... 代理功能增强... 被代理类 DemoTarget.bar() 方法执行了... 11.6.2. InvocationHandler 接口的 invoke 方法的调用 Proxy.newProxyInstance(ClassLoader loader, Class\u0026lt;?\u0026gt;[] interfaces, InvocationHandler h)方法，会生成一个代理对象，此代理对象名称叫$Proxy0@xxx。此代理对象会拥有一个属性h，该属性就是实现了InvocationHandler接口的实例（编写增强逻辑的类）。接口实现h有一个属性，就是待增强的类的实例。\n通过一个工具方法，获取生成的代理实例的字节码数组，然后通过文件流的方式生成.class文件\n再通过反编译去查询生成的文件，可以发现，其实在调用待增强的方法时，该方法里就只做了一个事情，就是调用h实例中的invoke方法。所以就是当通过Proxy.newProxyInstance()方法生成的代理对象，执行与待增强类实现的接口方法时，就会调用到invoke方法。\n11.6.3. newProxyInstance 方法的执行过程 首先会生成一个代理类，通过拼凑字符串的方法生成一个叫$Proxy0的类，以.java结尾 使用文件流将拼凑好的字符串生成$Proxy0.java文件到本地磁盘中 在运行时，将$Proxy0.java文件编译成$Proxy0.class文件 使用类加载器，将$Proxy0.class文件加载到JVM内存中 在内存中执行，并返回代理实例 11.7. JDK 动态代理注意事项 JDK 的代理类是由 JDK 直接生成字节码文件，可以用 arthas 的 jad 工具反编译代理类查看源码 代理类会继承 Proxy 类，该父类中有一个 InvocationHandler h 属性，通过接口回调的方式来实现代理增强的逻辑 目标类必须有实现的接口。如果某个类没有实现接口，那么这个类就不能用 JDK 动态代理。 在代理实现的接口方法中，通过反射调用相应的目标方法 代理增强是借助多态来实现，因此成员变量、静态方法、final 方法均不能通过代理实现 扩展知识：JDK 的动态代理对反射调用目标对象的方法做了优化。 前 16 次都是使用反射调用，性能较低 第 17 次调用会生成代理类，优化为非反射调用 可使用 arthas 的 jad 工具反编译第 17 次调用生成的代理类，查看源码 注意：运行测试程序时须添加 --add-opens java.base/java.lang.reflect=ALL-UNNAMED --add-opens java.base/jdk.internal.reflect=ALL-UNNAMED\n12. CGlib 动态代理 12.1. 概述 CGLIB(Code Generation Library) 是通过继承的方式做的动态代理。如果某个类被标记为 final，那么它是无法使用 CGLIB 生成动态代理。\n优点：与 JDK 动态代理相比，目标类不需要实现特定的接口，更加灵活。\nTips: 在 Spring AOP 中，如果目标类没有实现接口，则会选择使用 CGLIB 来生成动态代理\n12.2. CGlib 基础使用 定义接口与实现类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface DemoInterface { void foo(); int bar(); } public class DemoTarget implements DemoInterface { @Override public void foo() { System.out.println(\u0026#34;被代理类 DemoTarget.foo() 方法执行了...\u0026#34;); } @Override public int bar() { System.out.println(\u0026#34;被代理类 DemoTarget.bar() 方法执行了...\u0026#34;); return 99; } } 使用 CGlib 生成动态代理。与 JDK 的动态代理用法十分接近，主要区别有以下两点：\n增强逻辑的回调是定义在 net.sf.cglib.proxy.MethodInterceptor 接口中 用于增强的拦截方法形参不一样，分别提供了 Method 方法对象与 MethodProxy 对象用于进行目标方法的调用 method.invoke 是与 jdk 动态代理一样，通过反射调用，必须调用到足够次数才会进行优化 methodProxy.invoke 不通过反射调用，它会正常（间接）调用目标对象的方法（Spring 框架底层采用） methodProxy.invokeSuper 也是不通过反射调用，它会正常（间接）调用代理对象的方法，可以省略目标对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Test public void testCglibBasic() { // 创建被代理对象 DemoTarget demoTarget = new DemoTarget(); // 创建代理 DemoInterface demoInterface = (DemoInterface) Enhancer.create(DemoTarget.class, new MethodInterceptor() { @Override public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { // 模拟增强逻辑 System.out.println(\u0026#34;CGlib 动态代理的 MethodProxy signature: \u0026#34; + methodProxy.getSignature()); // 使用反射，调用原始目标方法 // return method.invoke(demoTarget, args); // 内部没有用反射, 需要目标对象，spring 框架采用这种方式 return methodProxy.invoke(demoTarget, args); // 内部没有用反射, 需要代理对象 // return methodProxy.invokeSuper(proxy, args); } }); demoInterface.foo(); demoInterface.bar(); } 注意事项：通过 MethodProxy 调用目标方法，在 jdk \u0026gt;= 9 的情况下，在调用 Object 的方法会有问题，启动程序时需要设置：--add-opens java.base/java.lang=ALL-UNNAMED\n12.3. 模拟 CGlib 代理基础实现 定义用于测试的被代理类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Target { public void save() { System.out.println(\u0026#34;被代理类 Target.save() 方法执行...\u0026#34;); } public void save(int i) { System.out.println(\u0026#34;被代理类 Target.save(int) 方法执行，参数：\u0026#34; + i); } public void save(long j) { System.out.println(\u0026#34;被代理类 Target.save(long) 方法执行，参数：\u0026#34; + j); } } CGlib 动态代理是继承被代理类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 // CGlib 代理是继承被代理类 public class CglibProxyMock extends Target { // 定义用于增强的回调接口的属性 private MethodInterceptor methodInterceptor; // 通过构造函数或者 setter 方法来接收 MethodInterceptor 回调接口 public CglibProxyMock(MethodInterceptor methodInterceptor) { this.methodInterceptor = methodInterceptor; } // 定义方法反射调用的 Method 属性 static Method save0; static Method save1; static Method save2; // 定义不使用反射调用的 MethodProxy 属性 static MethodProxy save0Proxy; static MethodProxy save1Proxy; static MethodProxy save2Proxy; static { try { save0 = Target.class.getMethod(\u0026#34;save\u0026#34;); save1 = Target.class.getMethod(\u0026#34;save\u0026#34;, int.class); save2 = Target.class.getMethod(\u0026#34;save\u0026#34;, long.class); /* * 创建 MethodProxy 对象 * 参数1：被代理类字节码对象 * 参数2：代理类字节码对象 * 参数3：代理方法的返回值与形参的表达式 * 参数4：增强后的方法名称 * 参数5：原始方法的名称 */ save0Proxy = MethodProxy.create(Target.class, CglibProxyMock.class, \u0026#34;()V\u0026#34;, \u0026#34;save\u0026#34;, \u0026#34;saveSuper\u0026#34;); save1Proxy = MethodProxy.create(Target.class, CglibProxyMock.class, \u0026#34;(I)V\u0026#34;, \u0026#34;save\u0026#34;, \u0026#34;saveSuper\u0026#34;); save2Proxy = MethodProxy.create(Target.class, CglibProxyMock.class, \u0026#34;(J)V\u0026#34;, \u0026#34;save\u0026#34;, \u0026#34;saveSuper\u0026#34;); } catch (NoSuchMethodException e) { throw new NoSuchMethodError(e.getMessage()); } } // ***** 带原始功能的方法，直接调用父类方法 ***** public void saveSuper() { super.save(); } public void saveSuper(int i) { super.save(i); } public void saveSuper(long j) { super.save(j); } // ***** 带增强功能的方法，通过 MethodInterceptor.intercept 回调方法执行功能增强 ***** @Override public void save() { try { methodInterceptor.intercept(this, save0, new Object[0], save0Proxy); } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } @Override public void save(int i) { try { methodInterceptor.intercept(this, save1, new Object[]{i}, save1Proxy); } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } @Override public void save(long j) { try { methodInterceptor.intercept(this, save2, new Object[]{j}, save2Proxy); } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } } 12.4. MethodProxy 的实现原理 上面提及 MethodProxy 类可以避免反射调用目标方法，原理是当调用 MethodProxy 的 invoke 或 invokeSuper 方法时，会动态生成两个类，都会继承 net.sf.cglib.reflect.FastClass 抽象类\n其中一个类是配合代理对象一起使用，避免反射 另外一个类是配合目标对象一起使用，避免反射 (Spring 框架底层使用此方式) 12.4.1. 配合目标对象的 FastClass 实现 当第一次在 MethodInterceptor 实现中调用 methodProxy.invoke(target, args) 方法，cglib 就动态生成一个类，继承 net.sf.cglib.reflect.FastClass 抽象类。在初始创建时就记录了被代理的目标对象中方法与编号的对应关系，其中 getIndex 方法用于获取调用的方法的编号，invoke 方法则用于根据方法编号通过被代理的目标对象调用相应的方法\n模拟实现如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 /** * 模拟 MethodProxy 为避免反射调用目标方法时生成的 FastClass 实现类。 * 即 methodProxy.invoke(target, args) 方法的实现原理，配置目标对象使用。 * 注：只抽取两个比较重点的方法实现做测试 */ class TargetFastClass extends FastClass { // FastClass 要求必须要调用其有参构造方法 public TargetFastClass(Class clazz) { super(clazz); } // 初始化方法相应的签名对象 Signature s0 = new Signature(\u0026#34;save\u0026#34;, \u0026#34;()V\u0026#34;); Signature s1 = new Signature(\u0026#34;save\u0026#34;, \u0026#34;(I)V\u0026#34;); Signature s2 = new Signature(\u0026#34;save\u0026#34;, \u0026#34;(J)V\u0026#34;); /** * 获取目标方法的编号 * * @param signature 被调用的方法签名，包括方法名字、参数返回值 * @return */ @Override public int getIndex(Signature signature) { /* 判断方法签名，返回相应的方法编号。示例假设方法编号如下： save() 0 save(int) 1 save(long) 2 */ if (s0.equals(signature)) { return 0; } else if (s1.equals(signature)) { return 1; } else if (s2.equals(signature)) { return 2; } return -1; } /** * 根据方法的编号，正常调用目标对象中的方法 * * @param index 方法的编号 * @param target 目标对象 * @param args 被调用的方法的形参 * @return * @throws InvocationTargetException */ @Override public Object invoke(int index, Object target, Object[] args) throws InvocationTargetException { // 根据方法编号，调用相应的对象中的方法 if (index == 0) { ((Target) target).save(); return null; } else if (index == 1) { ((Target) target).save((int) args[0]); return null; } else if (index == 2) { ((Target) target).save((long) args[0]); return null; } else { throw new RuntimeException(\u0026#34;无此方法\u0026#34;); } } // ...省略其他实现方法 } 模拟测试 methodProxy.invoke(target, args) 方法，配合目标对象进行方法的调用实现流程\n1 2 3 4 5 6 7 8 9 10 @Test public void testTargetFastClass() throws InvocationTargetException { // MethodProxy.create 方法调用时，就相当于创建了 FastClass 实现类，创建过程就会记录了方法签名信息 TargetFastClass fastClass = new TargetFastClass(Target.class); // 调用时，根据方法签名获取方法的编号 int index = fastClass.getIndex(new Signature(\u0026#34;save\u0026#34;, \u0026#34;(I)V\u0026#34;)); System.out.println(index); // 根据方法编号，调用相应目标的方法 fastClass.invoke(index, new Target(), new Object[]{100}); } 注：Target 是前面定义的用来测试的被代理类\n12.4.2. 配合代理对象的 FastClass 实现 当第一次在 MethodInterceptor 实现中调用 methodProxy.invokeSuper(proxy, args) 方法，cglib 就动态生成一个类，继承 net.sf.cglib.reflect.FastClass 抽象类。在初始创建时就记录了cglib 的代理对象中调用原目标方法与编号的对应关系，其中 getIndex 方法用于获取调用的方法的编号，invoke 方法则用于根据方法编号通过代理对象调用相应原目标的方法\n模拟实现如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 /** * 模拟 MethodProxy 为避免反射调用目标方法时生成的 FastClass 实现类。 * 即 methodProxy.invokeSuper(proxy, args) 方法的实现原理，配置 cglib 代理对象使用。 * 注：只抽取两个比较重点的方法实现做测试 */ class ProxyFastClass extends FastClass { // FastClass 要求必须要调用其有参构造方法 public ProxyFastClass(Class clazz) { super(clazz); } // 初始化代理中方法相应的签名对象 Signature s0 = new Signature(\u0026#34;saveSuper\u0026#34;, \u0026#34;()V\u0026#34;); Signature s1 = new Signature(\u0026#34;saveSuper\u0026#34;, \u0026#34;(I)V\u0026#34;); Signature s2 = new Signature(\u0026#34;saveSuper\u0026#34;, \u0026#34;(J)V\u0026#34;); /** * 获取代理中目标方法的编号 * * @param signature 被调用的方法签名，包括方法名字、参数返回值 * @return */ @Override public int getIndex(Signature signature) { /* 判断方法签名，返回相应的方法编号。示例假设代理中调用原目标方法编号如下： saveSuper() 0 saveSuper(int) 1 saveSuper(long) 2 */ if (s0.equals(signature)) { return 0; } else if (s1.equals(signature)) { return 1; } else if (s2.equals(signature)) { return 2; } return -1; } /** * 根据方法的编号，调用代理类中定义的目标对象方法 * * @param index 方法的编号 * @param proxy 代理对象 * @param args 被调用的方法的形参 * @return * @throws InvocationTargetException */ @Override public Object invoke(int index, Object proxy, Object[] args) throws InvocationTargetException { // 根据方法编号，调用代理对象中的相应方法 if (index == 0) { ((CglibProxyMock) proxy).saveSuper(); return null; } else if (index == 1) { ((CglibProxyMock) proxy).saveSuper((int) args[0]); return null; } else if (index == 2) { ((CglibProxyMock) proxy).saveSuper((long) args[0]); return null; } else { throw new RuntimeException(\u0026#34;无此方法\u0026#34;); } } // ...省略其他实现方法 } 模拟测试 methodProxy.invokeSuper(proxy, args) 方法，配合代理对象进行方法的调用实现流程\n1 2 3 4 5 6 7 8 9 10 @Test public void testProxyFastClass() throws InvocationTargetException { // MethodProxy.create 方法调用时，就相当于创建了 FastClass 实现类，创建过程就会记录了方法签名信息 ProxyFastClass fastClass = new ProxyFastClass(Target.class); // 调用时，根据方法签名获取方法的编号 int index = fastClass.getIndex(new Signature(\u0026#34;saveSuper\u0026#34;, \u0026#34;(I)V\u0026#34;)); System.out.println(index); // 根据方法编号，调用相应目标的方法 fastClass.invoke(index, new CglibProxyMock((obj, method, args, proxy) -\u0026gt; proxy.invokeSuper(obj, args)), new Object[]{100}); } 注：\n此测试使用 CglibProxyMock 是前面自定义的模拟 cglib 代理实现，非 cglib 原生 上面模拟使用代理对象调用原被代理目标的方法，记得要调用自定义代理中的非增强的方法。如果调用增强的方法，该方法中又会回调 MethodInterceptor 的 intercept 方法，就会出现无限的循环调用 12.5. CGlib 与 JDK 动态代理的区别 JDK 的动态代理通过 Proxy 类使用反射技术来实现，不需要导入其他依依赖。值得注意的是，当方法被调用到一定的次数后，才会生成不通过反射调用的代理。\n而 CGlib 需要引入 asm.jar 相关依赖，它是使用字节码增强技术来实现。在加载时动态生成两个类，分别是使用目标对象与代理对象来调用原方法，从而避免反射，提高性能。但代价是一个代理类会搭配生成两个 FastClass 实现类，代理类中还得增加仅调用原目标对象的 super 的相关方法。\n使用编号处理方法对应关系比较省内存，另外，最初获得方法顺序是不确定的，这个过程没法固定死。\n13. Java SPI 机制 13.1. SPI 简述 SPI 全称 Service Provider Interface，是 Java 提供的一套用来被第三方实现或者扩展的 API，它可以用来启用框架扩展和替换组件\n从上图可以看到，SPI 的本质其实是帮助程序，为某个特定的接口寻找它的实现类。而且哪些实现类的会加载，是个动态过程（不是提前预定好的）\n有点类似 IOC 的思想，就是将装配的控制权移到程序之外，在模块化设计中这个机制尤其重要。所以 SPI 的核心思想就是解耦。常见的例子：\n数据库驱动加载接口实现类的加载。JDBC 加载不同类型数据库的驱动 日志门面接口实现类加载，SLF4J 加载不同提供商的日志实现类 Spring 中大量使用了 SPI，比如：对 servlet3.0 规范对 ServletContainerInitializer 的实现、自动类型转换 Type Conversion SPI(Converter SPI、Formatter SPI) 等 13.2. Java SPI 约定 要使用 Java SPI，需要遵循如下约定：\n当服务提供者提供了接口的一种具体实现后，在 jar 包的META-INF/services目录下创建一个以『接口全限定名』为命名的文件，内容为实现类的全限定名。 接口实现类所在的 jar 包放在主程序的 classpath 中。 程序通过 java.util.ServiceLoder 动态装载实现模块，它通过扫描 META-INF/services 目录下的配置文件找到实现类的全限定名，把类加载到 JVM。 SPI 的实现类必须有无参构造方法。 13.3. 基础使用示例 详情示例参考 dubbo-thought 示例工程（待迁移至 java api 工程）\n先定义一个接口 1 2 3 public interface InfoService { Object sayHello(String name); } 定义多个接口的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class InfoServiceAImpl implements InfoService { @Override public Object sayHello(String name) { System.out.println(name + \u0026#34;,你好，调通了A实现！\u0026#34;); return name + \u0026#34;,你好，调通了A实现！\u0026#34;; } } public class InfoServiceBImpl implements InfoService { @Override public Object sayHello(String name) { System.out.println(name + \u0026#34;,你好，调通了B实现！\u0026#34;); return name + \u0026#34;,你好，调通了B实现！\u0026#34;; } } 在 classpath 下新增目录 META-INF/services，创建接口的名称为接口全限定名的文件com.moon.service.InfoService，内容为该接口的实现类全限定名。 1 2 com.moon.service.impl.info.InfoServiceAImpl com.moon.service.impl.info.InfoServiceBImpl 测试 1 2 3 4 5 6 7 8 9 @Test public void testBaseSpi() { // 服务加载器，加载实现类 ServiceLoader\u0026lt;InfoService\u0026gt; serviceLoader = ServiceLoader.load(InfoService.class); // ServiceLoader是实现了Iterable的迭代器，直接遍历实现类 for (InfoService service : serviceLoader) { Object result = service.sayHello(\u0026#34;Moon\u0026#34;); // 依次调用文件中配置的所有实现类 } } 13.4. 核心功能类 需要指出的是，java 之所以能够顺利根据配置加载这个实现类，完全依赖于 jdk 内的一个核心类：java.util.ServiceLoader\n","permalink":"https://ktzxy.top/posts/hem77hafic/","summary":"Java基础 反射","title":"Java基础 反射"},{"content":"\n巴菲特投资语录 1、投资需要的一是要学会估值，二是要学会面对涨跌!\n2、一生能够积累多少财富，不取决于你能够赚多少钱，而取决于你如何投资理财，钱找人胜过人找钱， 要懂得钱为你工作，而不是你为钱工作。\n3、那些最好的买卖，刚开始的时候，从数字上看，几乎都会告诉你不要买。\n4、我在11 岁时做了人生第一笔投资，我认为我之前是虚度了光阴。起步越早，我们越能通过复利魔力给我们带来滚滚财富。如果我们挑选了正确的股票，耐心持有伴随公司成长则是关键。\n5、如果想人生多彩多姿，就试着学所有有兴趣的事。\n6、感觉迷失时不是你迷路了，而是找到正途前的必然之路。\n7、对志向的渴望可以引领我们完成引以为傲的成就。\n8、审视自己的内心是最好的投资。\n9、愿望未能实现时，思考什么是自己真想要。\n10、考虑到人可以从错误中学习，那么，最好的事情就是从别人的错误中学习。\n11、你一定要敢于行动!非你莫属!\n12、利用市场的愚蠢，进行有规律的投资。\n13、我是个现实主义者，我喜欢目前自己所从事的一切，并对此始终深信不疑。作为一个彻底的实用 现实主义者，我只对现实感兴趣，从不抱任何幻想，尤其是对自己。\n14、如果你打了半小时牌，仍然不知道谁是菜鸟，那么你就是。\n15、为你最崇拜的人工作，这样除了获得薪水外还会让你早上很想起床。\n16、如何决定一家企业的价值呢?做许多阅读：我阅读所注意的公司的年度报告，同时我也阅读它的竞争对手的年度报告。\n17、竞争并不是推动人类前进的动力，嫉妒才是。\n18、你人生的起点并不是那么重要，重要的是你最后抵达了哪里。\n19、当大浪退去时，我们才知道谁在裸泳。\n20、当有人逼迫你去突破自己，你要感恩他。他是你生命中的贵人,也许你会因此而改变和蜕变。当没有人逼迫你，请自己逼迫自己，因为真正的改变是自己想改变。蜕变的过程是很痛苦的，但每一次的蜕变都会有成长的惊喜。\n21、无论你多有天分，也无论你多么努力，有些事情就是需要时间。让九个女人同时怀孕，小孩也不可能在一个月内出生。\n22、你们到了我这个年纪的时候就会发现，衡量自己成功的标准就是有多少人在真正关心你、爱你。\n23、永远不要问理发师你是否需要理发。\n24、想要彻底改变自己，不完全取决于你花了多长时间，更重要的是在于你是否用了心并找对方法。\n25、我只做我完全明白的事。\n26、如何定义朋友呢：他们会向你隐瞒什么?\n27、形成自己的宏观观点，或是听别人对宏观或市场进行预测，都是在浪费时间。事实上这是危险的，因为这可能会模糊你的视野，让你看不清真正重要的事实。\n28、伴侣是人生最大的投资。在选择伴侣上，如果你错了，将让你损失很多，不仅仅是金钱方面。\n29、如果你不愿意拥有一只股票十年，那就不要考虑拥有它十分钟。\n30、我们之所以取得目前的成就，是因为我们关心的是寻找那些我们可以跨越的一英尺障碍，而不是去拥有什么能飞越七英尺的能力。\n31、当别人贪婪时，你就该恐惧。当别人恐惧时，你就该贪婪。\n32、风险来自于你不知道自己在做什么。\n33、评价一个人时，应重点考察四项特征：善良、正直、聪明、能干。如果不具备前两项，那后面两项会害了你。里根也曾说：如果你正直，这比什么都重要;如果你不善良，什么也都不重要了。直面自心，做一个善良正直胸怀坦荡的人!\n34、就算美联储主席格林斯潘偷偷告诉我他未来二年的货币政策，我也不会改变我的任何一个作为。\n35、成功有两个方法，一、去接近成功人士，让他们的想法影响你;二、走出去学习，让精彩的世界影响你; 世间没有贫穷的口袋，只有贫穷的脑袋!\n36、想成为最优秀的人，就要向最优秀的人学习。\n37、在我看来，能做好事情的人并不是“马力最大的”而是效率最高的。\n38、我们知道的是我们不知道。\n39、如果盖茨卖的不是软件而是汉堡，他也会成为世界汉堡大王。\n40、在短期内，市场是一台投票机，但在长期内，它是一台称重机。\n41、当适当的气质与适当的智力结构相结合时，你就会得到理性的行为。\n42、要学会以40 分钱买1 元的东西。\n43、金钱多少对于你我没有什么大的区别。我们不会改变什么，只不过是我们的妻子会生活得好一些。\n44、人性中总是有喜欢把简单的事情复杂化的不良成分。\n45、成功的投资在本质上是内在的独立自主的结果。\n46、用我的想法和你们的钱，我们会做得很好。\n47、你不得不自己动脑。我总是吃惊于那么多高智商的人也会没有头脑的模仿。在别人的交谈中，没有得到任何好的想法。\n48、在拖拉机问世的时候做一匹马，或在汽车问世的时候做一名铁匠，都不是一件有趣的事。\n49、任何不能永远前进的事物都将停滞。\n50、人不是天生就具有这种才能的，即始终能知道一切。但是那些努力工作的人有这样的才能。他们寻找和精选世界上被错误定价的赌注。当世界提供这种机会时，聪明人会敏锐地看到这种赌注。当他们有机会时，他们就投下大赌注。其余时间不下注。事情就这么简单。\n51、反动把智商当做对良好投资关键，强调要有判断力，原则性和耐心。\n52、我喜欢简单的东西。\n53、要量力而行。你要发现你生活与投资的优势所在。每当偶尔的机会降临，即你的这种优势有充分的把握，你就全力以赴，孤注一掷。\n54、别人赞成你也罢，反动你也罢，都不应该成为你做对事或做错事的因素。\n55、我们不因大人物，或大多数人的赞同而心安理得，也不因他们的反对而担心。\n56、如果你发现了一个你明了的局势，其中各种关系你都一清二楚，那你就行动，不管这种行动是符合常规，还是反常的，也不管别人赞成还是反动。\n57、所有的男人的不幸出自同一个原因，即他们都不能安份地呆在一个房间里。\n58、高等院校喜欢奖赏复杂行为，而不是简单行为，而简单的行为更有效。\n59、时间是杰出(快乐)人的朋友，平庸人(痛苦)的敌人。\n60、我与富有情感的人在一起工作(生活)。\n61、当我发现自己处在一个洞穴之中时，最重要的事情就是停止挖掘。\n62、我很理性，许多人有更高的智商，许多人工作更长的时间，但是我能理性地处理事物。你们必须能控制自己，别让你的感情影响你的思维。\n63、有两种信息：你可以知道的信息和重要的信息。而你可以知道且又重要的信息在整个已知的信息中只占极小的百分比!\n64、如果你在错误的路上，奔跑也没有用。\n65、把你的鸡蛋放在一个篮子里，然后看好它。\n66、投资的秘诀、不是评估某一行业对社会的影响有多大、或它的发展前景有多好、而是一间公司有多强的竞争优势、这优势可以维持多久。产品和服务的优越性持久而深厚、才能给投资者带来优厚的回报。\n67、财务顾问就是那些比你更需要你的钱的人。\n68、当那些好的企业突然受困于市场逆转、股价不合理的下跌、这就是大好的投资机会来临了。\n69、我最喜欢的持股时间是……永远!\n70、金钱并不是很重要的东西、至少它买不到健康与友情。\n71、如果你认为你可以经常进出股市而致富的话、我不愿意和你合伙做生意、但我希望成为你的股票经纪。\n72、在股票市场中、唯一能让您被三振出局的是- 不断的抢高杀低、耗损资金。\n73、世界上最笨的事情就是-看到股价上扬就决定要投资一档股票。\n74、不要以价格决定是否购买股票、而是要取决于这个企业的价值。\n75、投资股票的第一原则：不要赔钱。\n76、把每支投资的股票都当作一桩生意来做。\n77、自认对市场震荡起伏敏感而杀进杀出的投资人、反而不如以不变应万变的投资人容易赚钱。\n78、假如观念会过时、那就不足以成为观念。\n79、如果股市可以用理论去有效分析、我早就变成路边的流浪汉了。\n80、如果股市投资人只需相信理论上的投资效益、就好比较一个打桥牌、却告诉他看各家的牌对于桥局没有任何帮助一样。\n81、买股票时、应该假设明天开始股市要休市3-5年。\n82、如果你不断的跟着风向转、那你就不可能会发财。\n83、判断一家企业的价值、一半是靠科学研究、一半是靠艺术上的灵感。\n84、不需要等到股价跌到谷底才进场买股票、反正你买到的股价一定比它真正的价值还低。\n85、买股票的时机应该与大多数的投资人想法相反。\n86、真正适合投资股票的时机是趁大多数人还未注意股市动态时。\n87、理智是投资股票最基本的要求。\n88、假如这家公司表现得很好、它的股价绝对不会寂寞的。\n89、绩优企业受制于市场逆转、股价不合理而下跌时、大好的投资机会及将来临。\n90、假如我在一个五兆的市场中还不能赚钱、要我相信跑到几千里外的另一个市场能赚到钱、这种想法未免太天真了。\n91、一个过热的股市就像是一名企业的扒手。\n92、耐得住时间考验才最真实、能让你赚到大钱的不是你的判断、而是耐心等待的功夫我们所做的事、不超出任何人的能力范围、多做额外的工作、不尽然就能得到与别人不同的结果。\n93、思想枯竭、则巧言生焉!\n94、吸引我从事工作的原因之一是、它可以让你过你自己想过的生活。你没有必要为成功而打扮。\n95、投资对于我来说、既是一种运动、也是一种娱乐。他喜欢通过寻找好的猎物来“捕获稀有的快速移动的大象。”\n96、我工作时不思考其他任何东西。\n97、如果发生了坏事情、请忽略这件事。\n98、要赢得好的声誉需要20 年、而要毁掉它、5 分钟就够。如果明白了这一点、你做起事来就会不同了。\n99、如果你能从根本上把问题所在弄清楚并思考它、你永远也不会把事情搞得一团遭!\n100、我从来不曾有过自我怀疑。我从来不曾灰心过。\n101、我买的不是股票，而是一个公司的未来。\n102、因为没有人愿意慢慢变富。\n103、买股票从来不投机，而是做价值投资。\n复利计算器 复利计算器 - 在线复利计算器 - 利率复利计算器\n闲隙笔记 通过3种方式让自己赚到更多的钱 第 1 种：让自己的单位时间更加值钱。通过提升自己的工作技能，提升自己的能力来做到\n第 2 种：把你的一份时间出售很多次\n第 3 种：用钱来买别人的时间，用别人的时间为你自己赚钱。\n三大成本 机会成本，时间成本，试错成本\n复利公式 资产的类型笔记 1.资产分为三类：生钱资产、耗钱资产、其他资产\n（1）生钱资产：持续产生净现金流\n（2）耗钱资产：持续产生负现金流\n（3）其他资产：不产生现金流，价差不确定\n2.当生钱资产带来的非工资收入超过日常开销或者大于耗钱资产的开支时，就实现了基本的财务自由。\n3.富人思维中，生钱资产比例高达80%、好支出比例高达80%。\n正确系统的投资技能包括： 理财的底层逻辑、富人思维 股票的海选 精选出好公司 财务报表分析 企业分析 计算好价格 制定买进标准 指定持有标准 制定卖出标准等 企业年度报告 巨潮资讯网\n🌟根据统计： 中国的GDP大概6.5%以上； 中国所有上市公司的平均年化收益率大概12%以上 中国好公司的平均年化收益率大概24%以上\n【投资股票正确的方法】可以分为两大步： 第一步：选出内在价值高的盈利的好企业 第二步：在合理的价格把握时机，及时买入\n1、好公司的股 票暴跌收益会更高，要选择持续盈利的优质好公司。\n2、好公司股 票赚钱，一个是赚公司持续的现金分红，另外一个是股 票价格上涨带来的收益。\n3、要掌握理 财技能，从而能够更精准的找到优质高收益的好公司。\n4、好收益=好公司+好价格，挑选到好公司的股 票，要以所有者的心态长期持有，不必时刻关注它的涨涨跌跌\n挑选基金 原则 秘诀一：基金规模小于1亿元不要选\n秘诀二：挑选费用较低的基金\n指数基金的费用主要是：申购费、赎回费、管理费、托管费。 费率低了，长期来看可以提高0.3%~0.5%的年化收益率，非常有效地增加我们的收益。 秘诀三：选择追踪误差较小的基金\n第一，基金份额净值增长率与业绩比较基准收益率的差值。正的好。 第二，基金份额净值增长率标准差与业绩比较基准收益率标准差的差值。如果标准差的差距小，就说明基金在每个时间段的追踪情况都比较贴近于指数。 估值 市盈率：市值/一定时期的净利润 市净率：市值/净资产 股息率：现金分红/市值 长期投资时，可以选A类。短期投资时，可以选C类。\n指数基金在熊市进入低估区域的时间长度平均为1~2年，历史最长也不过3年。\n常见的宽基指数 A股：沪深300、中证500和创业板指数 沪深300：国内最具代表性的指数 中证指数开发， 沪+深，规模最大+流动性最好的300只 金融、能源，行业占比高，波动大 代码：沪000300、深399300 挑选思路： 费用最低、误差最小 有特色的增强型（有一定风险） 开始时间：2004.12.31，1000点 中证500指数：最容易获得超额收益的指数 中证指数公司， 将沪深300的公司排除， 将最近一年日均总市值排名前300的排除 剩下的，日均总市值排名前500 开始时间：2004.12.31，1000点 代码：沪000905，深399905 挑选思路： 费用低，规模大，历史久，误差小 有一定超额收益的增强型 创业板指数：整体盈利比较低，不稳定 构成：创业板上市公司中，规模最大+流动性最好的100只 开始时间：2010.5.31，1000点 港股：恒生指数、H股指数、香港中小股指数 恒生 指数：港股最具有代表性的指数 指数代码：HSI 恒生指数公司开发 港股的蓝筹股，所有公司里排名前50， 单只最高比例是15% 每季度重新选一次 基金代码含有：QDII H股指数：恒生国企指数，价值洼地 公司在内地注册，却在香港上市，用港币交易的股票 从2017年扩容，现有50只股票 2000点起步 香港中小指数：港股中小盘股的代表 投资的是H股和红筹股，主营业务在内地的公司 平均市值是300亿，放在A股属于大盘股 150+成分股，单只上限5% 代码：501021 美股：纳斯达克100指数、标普500指数 纳斯达克100指数 纳斯达克前100（苹果、微软） 代码：NDX 开始时间：1985，100点 国内通过QDII指数基金 标普500指数 蓝筹股指数，500只成份股 不依照市值，大公司90%、中型公司10% 入选公司必须是：行业领导者 消费、医药、科技，弱周期性，超过50% 开始时间：1941年，10点，9% 策略加权指数基金 在A股，大约有十几种不同类型的策略指数是有对应的基金产品的。这些指数包括基本面、红利、价值、低波动、央视50、AH优选、行业龙头等。每一个指数，对应的指数基金可能只有一两只，数量并不多。\n在定投指数基金的时候，通常策略加权指数基金的收益会更好一些。\n红利指数：持股收息的最优选择 上证红利指数 由上海证券交易所过去两年平均现金股息率最高的50只股票组成的， 指数代码为000015。 开始时间：2004年12月31日，1000点 中证红利指数 同时从上海证券交易所和深圳证券交易所挑选过去两年平均现金股息率最高的100只股票。 代码是000922/399922。 它是从2004年12月31日的1000点开始的。 深证红利指数 专门投资深圳证券交易所的高现金股息率的股票，不过成份股只有40只。 深证红利指数是红利系列指数中最早推出的一只， 于2002年12月31日推出，也是从1000点开始的， 代码为399324。 红利机会指数 标普公司围绕A股开发 代码为CSPSADRP 条件：红利机会指数把符合以下3条要求的股票按股息率排名，再从中筛选出股息率最高的100只作为成份股。 过去3年盈利为正 过去12个月，净利润为正 每只股票权重不超过3%，单个行业不超过33%。 基本面指数 截至2019年4月，规模比较合适的基本面指数主要是基本面50、深证基本面60/120这三个 基本面50是从上海和深圳证券交易所一起挑选50只，而深证基本面60/120，则是只从深圳证券交易所挑选60只和120只。 ","permalink":"https://ktzxy.top/posts/niabm5hgsz/","summary":"\u003cp\u003e\u003cimg alt=\"202310241510425\" loading=\"lazy\" src=\"https://fastly.jsdelivr.net/gh/ktzxy/blog-img@main/2026/20260304111550063.webp\"\u003e\u003c/p\u003e\n\u003ch1 id=\"巴菲特投资语录\"\u003e巴菲特投资语录\u003c/h1\u003e\n\u003cp\u003e1、投资需要的一是要学会估值，二是要学会面对涨跌!\u003cbr\u003e\n2、一生能够积累多少财富，不取决于你能够赚多少钱，而取决于你如何投资理财，钱找人胜过人找钱，\n要懂得钱为你工作，而不是你为钱工作。\u003cbr\u003e\n3、那些最好的买卖，刚开始的时候，从数字上看，几乎都会告诉你不要买。\u003cbr\u003e\n4、我在11 岁时做了人生第一笔投资，我认为我之前是虚度了光阴。起步越早，我们越能通过复利魔力给我们带来滚滚财富。如果我们挑选了正确的股票，耐心持有伴随公司成长则是关键。\u003cbr\u003e\n5、如果想人生多彩多姿，就试着学所有有兴趣的事。\u003cbr\u003e\n6、感觉迷失时不是你迷路了，而是找到正途前的必然之路。\u003cbr\u003e\n7、对志向的渴望可以引领我们完成引以为傲的成就。\u003cbr\u003e\n8、审视自己的内心是最好的投资。\u003cbr\u003e\n9、愿望未能实现时，思考什么是自己真想要。\u003cbr\u003e\n10、考虑到人可以从错误中学习，那么，最好的事情就是从别人的错误中学习。\u003cbr\u003e\n11、你一定要敢于行动!非你莫属!\u003cbr\u003e\n12、利用市场的愚蠢，进行有规律的投资。\u003cbr\u003e\n13、我是个现实主义者，我喜欢目前自己所从事的一切，并对此始终深信不疑。作为一个彻底的实用\n现实主义者，我只对现实感兴趣，从不抱任何幻想，尤其是对自己。\u003cbr\u003e\n14、如果你打了半小时牌，仍然不知道谁是菜鸟，那么你就是。\u003cbr\u003e\n15、为你最崇拜的人工作，这样除了获得薪水外还会让你早上很想起床。\u003cbr\u003e\n16、如何决定一家企业的价值呢?做许多阅读：我阅读所注意的公司的年度报告，同时我也阅读它的竞争对手的年度报告。\u003cbr\u003e\n17、竞争并不是推动人类前进的动力，嫉妒才是。\u003cbr\u003e\n18、你人生的起点并不是那么重要，重要的是你最后抵达了哪里。\u003cbr\u003e\n19、当大浪退去时，我们才知道谁在裸泳。\u003cbr\u003e\n20、当有人逼迫你去突破自己，你要感恩他。他是你生命中的贵人,也许你会因此而改变和蜕变。当没有人逼迫你，请自己逼迫自己，因为真正的改变是自己想改变。蜕变的过程是很痛苦的，但每一次的蜕变都会有成长的惊喜。\u003cbr\u003e\n21、无论你多有天分，也无论你多么努力，有些事情就是需要时间。让九个女人同时怀孕，小孩也不可能在一个月内出生。\u003cbr\u003e\n22、你们到了我这个年纪的时候就会发现，衡量自己成功的标准就是有多少人在真正关心你、爱你。\u003cbr\u003e\n23、永远不要问理发师你是否需要理发。\u003cbr\u003e\n24、想要彻底改变自己，不完全取决于你花了多长时间，更重要的是在于你是否用了心并找对方法。\u003cbr\u003e\n25、我只做我完全明白的事。\u003cbr\u003e\n26、如何定义朋友呢：他们会向你隐瞒什么?\u003cbr\u003e\n27、形成自己的宏观观点，或是听别人对宏观或市场进行预测，都是在浪费时间。事实上这是危险的，因为这可能会模糊你的视野，让你看不清真正重要的事实。\u003cbr\u003e\n28、伴侣是人生最大的投资。在选择伴侣上，如果你错了，将让你损失很多，不仅仅是金钱方面。\u003cbr\u003e\n29、如果你不愿意拥有一只股票十年，那就不要考虑拥有它十分钟。\u003cbr\u003e\n30、我们之所以取得目前的成就，是因为我们关心的是寻找那些我们可以跨越的一英尺障碍，而不是去拥有什么能飞越七英尺的能力。\u003cbr\u003e\n31、当别人贪婪时，你就该恐惧。当别人恐惧时，你就该贪婪。\u003cbr\u003e\n32、风险来自于你不知道自己在做什么。\u003cbr\u003e\n33、评价一个人时，应重点考察四项特征：善良、正直、聪明、能干。如果不具备前两项，那后面两项会害了你。里根也曾说：如果你正直，这比什么都重要;如果你不善良，什么也都不重要了。直面自心，做一个善良正直胸怀坦荡的人!\u003cbr\u003e\n34、就算美联储主席格林斯潘偷偷告诉我他未来二年的货币政策，我也不会改变我的任何一个作为。\u003cbr\u003e\n35、成功有两个方法，一、去接近成功人士，让他们的想法影响你;二、走出去学习，让精彩的世界影响你; 世间没有贫穷的口袋，只有贫穷的脑袋!\u003cbr\u003e\n36、想成为最优秀的人，就要向最优秀的人学习。\u003cbr\u003e\n37、在我看来，能做好事情的人并不是“马力最大的”而是效率最高的。\u003cbr\u003e\n38、我们知道的是我们不知道。\u003cbr\u003e\n39、如果盖茨卖的不是软件而是汉堡，他也会成为世界汉堡大王。\u003cbr\u003e\n40、在短期内，市场是一台投票机，但在长期内，它是一台称重机。\u003cbr\u003e\n41、当适当的气质与适当的智力结构相结合时，你就会得到理性的行为。\u003cbr\u003e\n42、要学会以40 分钱买1 元的东西。\u003cbr\u003e\n43、金钱多少对于你我没有什么大的区别。我们不会改变什么，只不过是我们的妻子会生活得好一些。\u003cbr\u003e\n44、人性中总是有喜欢把简单的事情复杂化的不良成分。\u003cbr\u003e\n45、成功的投资在本质上是内在的独立自主的结果。\u003cbr\u003e\n46、用我的想法和你们的钱，我们会做得很好。\u003cbr\u003e\n47、你不得不自己动脑。我总是吃惊于那么多高智商的人也会没有头脑的模仿。在别人的交谈中，没有得到任何好的想法。\u003cbr\u003e\n48、在拖拉机问世的时候做一匹马，或在汽车问世的时候做一名铁匠，都不是一件有趣的事。\u003cbr\u003e\n49、任何不能永远前进的事物都将停滞。\u003cbr\u003e\n50、人不是天生就具有这种才能的，即始终能知道一切。但是那些努力工作的人有这样的才能。他们寻找和精选世界上被错误定价的赌注。当世界提供这种机会时，聪明人会敏锐地看到这种赌注。当他们有机会时，他们就投下大赌注。其余时间不下注。事情就这么简单。\u003cbr\u003e\n51、反动把智商当做对良好投资关键，强调要有判断力，原则性和耐心。\u003cbr\u003e\n52、我喜欢简单的东西。\u003cbr\u003e\n53、要量力而行。你要发现你生活与投资的优势所在。每当偶尔的机会降临，即你的这种优势有充分的把握，你就全力以赴，孤注一掷。\u003cbr\u003e\n54、别人赞成你也罢，反动你也罢，都不应该成为你做对事或做错事的因素。\u003cbr\u003e\n55、我们不因大人物，或大多数人的赞同而心安理得，也不因他们的反对而担心。\u003cbr\u003e\n56、如果你发现了一个你明了的局势，其中各种关系你都一清二楚，那你就行动，不管这种行动是符合常规，还是反常的，也不管别人赞成还是反动。\u003cbr\u003e\n57、所有的男人的不幸出自同一个原因，即他们都不能安份地呆在一个房间里。\u003cbr\u003e\n58、高等院校喜欢奖赏复杂行为，而不是简单行为，而简单的行为更有效。\u003cbr\u003e\n59、时间是杰出(快乐)人的朋友，平庸人(痛苦)的敌人。\u003cbr\u003e\n60、我与富有情感的人在一起工作(生活)。\u003cbr\u003e\n61、当我发现自己处在一个洞穴之中时，最重要的事情就是停止挖掘。\u003cbr\u003e\n62、我很理性，许多人有更高的智商，许多人工作更长的时间，但是我能理性地处理事物。你们必须能控制自己，别让你的感情影响你的思维。\u003cbr\u003e\n63、有两种信息：你可以知道的信息和重要的信息。而你可以知道且又重要的信息在整个已知的信息中只占极小的百分比!\u003cbr\u003e\n64、如果你在错误的路上，奔跑也没有用。\u003cbr\u003e\n65、把你的鸡蛋放在一个篮子里，然后看好它。\u003cbr\u003e\n66、投资的秘诀、不是评估某一行业对社会的影响有多大、或它的发展前景有多好、而是一间公司有多强的竞争优势、这优势可以维持多久。产品和服务的优越性持久而深厚、才能给投资者带来优厚的回报。\u003cbr\u003e\n67、财务顾问就是那些比你更需要你的钱的人。\u003cbr\u003e\n68、当那些好的企业突然受困于市场逆转、股价不合理的下跌、这就是大好的投资机会来临了。\u003cbr\u003e\n69、我最喜欢的持股时间是……永远!\u003cbr\u003e\n70、金钱并不是很重要的东西、至少它买不到健康与友情。\u003cbr\u003e\n71、如果你认为你可以经常进出股市而致富的话、我不愿意和你合伙做生意、但我希望成为你的股票经纪。\u003cbr\u003e\n72、在股票市场中、唯一能让您被三振出局的是- 不断的抢高杀低、耗损资金。\u003cbr\u003e\n73、世界上最笨的事情就是-看到股价上扬就决定要投资一档股票。\u003cbr\u003e\n74、不要以价格决定是否购买股票、而是要取决于这个企业的价值。\u003cbr\u003e\n75、投资股票的第一原则：不要赔钱。\u003cbr\u003e\n76、把每支投资的股票都当作一桩生意来做。\u003cbr\u003e\n77、自认对市场震荡起伏敏感而杀进杀出的投资人、反而不如以不变应万变的投资人容易赚钱。\u003cbr\u003e\n78、假如观念会过时、那就不足以成为观念。\u003cbr\u003e\n79、如果股市可以用理论去有效分析、我早就变成路边的流浪汉了。\u003cbr\u003e\n80、如果股市投资人只需相信理论上的投资效益、就好比较一个打桥牌、却告诉他看各家的牌对于桥局没有任何帮助一样。\u003cbr\u003e\n81、买股票时、应该假设明天开始股市要休市3-5年。\u003cbr\u003e\n82、如果你不断的跟着风向转、那你就不可能会发财。\u003cbr\u003e\n83、判断一家企业的价值、一半是靠科学研究、一半是靠艺术上的灵感。\u003cbr\u003e\n84、不需要等到股价跌到谷底才进场买股票、反正你买到的股价一定比它真正的价值还低。\u003cbr\u003e\n85、买股票的时机应该与大多数的投资人想法相反。\u003cbr\u003e\n86、真正适合投资股票的时机是趁大多数人还未注意股市动态时。\u003cbr\u003e\n87、理智是投资股票最基本的要求。\u003cbr\u003e\n88、假如这家公司表现得很好、它的股价绝对不会寂寞的。\u003cbr\u003e\n89、绩优企业受制于市场逆转、股价不合理而下跌时、大好的投资机会及将来临。\u003cbr\u003e\n90、假如我在一个五兆的市场中还不能赚钱、要我相信跑到几千里外的另一个市场能赚到钱、这种想法未免太天真了。\u003cbr\u003e\n91、一个过热的股市就像是一名企业的扒手。\u003cbr\u003e\n92、耐得住时间考验才最真实、能让你赚到大钱的不是你的判断、而是耐心等待的功夫我们所做的事、不超出任何人的能力范围、多做额外的工作、不尽然就能得到与别人不同的结果。\u003cbr\u003e\n93、思想枯竭、则巧言生焉!\u003cbr\u003e\n94、吸引我从事工作的原因之一是、它可以让你过你自己想过的生活。你没有必要为成功而打扮。\u003cbr\u003e\n95、投资对于我来说、既是一种运动、也是一种娱乐。他喜欢通过寻找好的猎物来“捕获稀有的快速移动的大象。”\u003cbr\u003e\n96、我工作时不思考其他任何东西。\u003cbr\u003e\n97、如果发生了坏事情、请忽略这件事。\u003cbr\u003e\n98、要赢得好的声誉需要20 年、而要毁掉它、5 分钟就够。如果明白了这一点、你做起事来就会不同了。\u003cbr\u003e\n99、如果你能从根本上把问题所在弄清楚并思考它、你永远也不会把事情搞得一团遭!\u003cbr\u003e\n100、我从来不曾有过自我怀疑。我从来不曾灰心过。\u003cbr\u003e\n101、我买的不是股票，而是一个公司的未来。\u003cbr\u003e\n102、因为没有人愿意慢慢变富。\u003cbr\u003e\n103、买股票从来不投机，而是做价值投资。\u003c/p\u003e","title":"理财课程笔录"},{"content":" MySQL中插入数据，如果插入的数据在表中已经存在（主键或者唯一键已存在），使用insert ignore 语法可以忽略插入重复的数据。\n1、insert ignore 语法 insert ignore into table_name values\u0026hellip;\n使用insert ignore语法插入数据时，如果发生主键或者唯一键冲突，则忽略这条插入的数据。\n满足以下条件之一：\n主键重复 唯一键重复 2、insert ignore 案例 先看一张表，表名table_name，主键id，唯一键name，具体表结构及表中数据如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 CREATE TABLE table_name( id int(11) NOT NULL, name varchar(50) DEFAULT NULL, age int(11) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; mysql\u0026gt; select * from table_name; +----+------+------+ | id | name | age | +----+------+------+ | 1 | Tom | 20 | +----+------+------+ 2.1 主键冲突 插入一条记录，id为1，如果不加 ignore ，报主键冲突的错误，如下： mysql\u0026gt; insert into table_name values(1,\u0026lsquo;Bill\u0026rsquo;, 21); ERROR 1062 (23000): Duplicate entry \u0026lsquo;1\u0026rsquo; for key \u0026lsquo;PRIMARY\u0026rsquo;\n加上ignore之后，不会报错，但有一个warning警告，如下： mysql\u0026gt; insert ignore into table_name values(1,\u0026lsquo;Bill\u0026rsquo;, 21); Query OK, 0 rows affected, 1 warning (0.00 sec) mysql\u0026gt; show warnings; +\u0026mdash;\u0026mdash;\u0026mdash;+\u0026mdash;\u0026mdash;+\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;+ | Level | Code | Message | +\u0026mdash;\u0026mdash;\u0026mdash;+\u0026mdash;\u0026mdash;+\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;+ | Warning | 1062 | Duplicate entry \u0026lsquo;1\u0026rsquo; for key \u0026lsquo;PRIMARY\u0026rsquo; | +\u0026mdash;\u0026mdash;\u0026mdash;+\u0026mdash;\u0026mdash;+\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;+\n查询表，发现插入的数据被忽略了。 mysql\u0026gt; select * from table_name; +\u0026mdash;-+\u0026mdash;\u0026mdash;+\u0026mdash;\u0026mdash;+ | id | name | age | +\u0026mdash;-+\u0026mdash;\u0026mdash;+\u0026mdash;\u0026mdash;+ | 1 | Tom | 20 | +\u0026mdash;-+\u0026mdash;\u0026mdash;+\u0026mdash;\u0026mdash;+\n2.2 唯一键冲突 同样，插入唯一键冲突的数据也会忽略，如下所示： mysql\u0026gt; insert into table_name values(2,\u0026lsquo;Tom\u0026rsquo;,21); ERROR 1062 (23000): Duplicate entry \u0026lsquo;Tom\u0026rsquo; for key \u0026lsquo;uk_name\u0026rsquo;\nmysql\u0026gt; insert ignore into table_name values(2,\u0026lsquo;Tom\u0026rsquo;,21); Query OK, 0 rows affected, 1 warning (0.00 sec)\n如果业务逻辑需要插入重复数据时自动忽略，不妨试试MySQL 的 insert ignore 功能。\n","permalink":"https://ktzxy.top/posts/16gffynatx/","summary":"MySQL插入数据insert ignore","title":"MySQL插入数据insert ignore"},{"content":"1. 行构造表达式示例 SELECT * FROM t1 WHERE (column1,column2) = (1,1);\n其中 (column1,column2) 与 (1,1)就是行构造表达式。\n(column1,column2) = (1,1) 这个表达式，在逻辑上，等同于：\ncolumn1 = 1 AND column2 = 1\n这个也比较好理解，但是，如果把\n(column1,column2) \u0026gt; (1,1) 理解为 column1 \u0026gt; 1 AND column2 \u0026gt; 1，那就错了。实际上应该是\ncolumn1 \u0026gt; 1 OR ((column1 = 1) AND (column2 \u0026gt; 1))\n如果不相信的话，看下面这个案例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 mysql\u0026gt; select * from t_test; +------+------+------+ | c1 | c2 | c3 | +------+------+------+ | 1 | 1 | 1 | | 1 | 1 | 0 | | 1 | 2 | 2 | | 1 | 2 | -1 | +------+------+------+ 4 rows in set (0.00 sec) mysql\u0026gt; select * from t_test where c1=1 AND (c2,c3) \u0026gt; (1,1); +------+------+------+ | c1 | c2 | c3 | +------+------+------+ | 1 | 2 | 2 | | 1 | 2 | -1 | +------+------+------+ 2 rows in set (0.00 sec) mysql\u0026gt; select * from t_test where c1 = 1 AND (c2 \u0026gt; 1 OR ((c2 = 1) AND (c3 \u0026gt; 1))); +------+------+------+ | c1 | c2 | c3 | +------+------+------+ | 1 | 2 | 2 | | 1 | 2 | -1 | +------+------+------+ 2 rows in set (0.00 sec) (c2,c3) \u0026gt; (1,1)，结果把c3为-1的记录也查出来了，是不是与期望有点落差，但事实就是这样。它的逻辑是先比较字段c2，如果c2 \u0026gt; 1，直接返回true，不再比较c3。如果c2=1，再比较字段c3。\n2. 行构造表达式优化 关于行构造表达式的优化，主要还是转换成多个单独的条件表达式，由 or and 这类逻辑运算符连接，这样才能最大可能用上索引。\n","permalink":"https://ktzxy.top/posts/ehj3wyi14c/","summary":"MySQL行构造器表达式优化(Row Constructor Expression)","title":"MySQL行构造器表达式优化(Row Constructor Expression)"},{"content":" Prometheus基于Alertmanager实现钉钉告警 一、安装prometheus-webhook-dingtalk插件 1 2 3 wget https://github.com/timonwong/prometheus-webhook-dingtalk/releases/download/v0.3.0/prometheus-webhook-dingtalk-0.3.0.linux-amd64.tar.gz tar -zxf prometheus-webhook-dingtalk-0.3.0.linux-amd64.tar.gz -C /opt/ mv /opt/prometheus-webhook-dingtalk-0.3.0.linux-amd64 /opt/prometheus-webhook-dingtalk 二、钉钉创建机器人自定义告警关键词并获取token 1 2 3 4 5 6 选择群组—\u0026gt;群设置–\u0026gt;添加智能群助手–\u0026gt;添加机器人 注意：选择过程中会有三种安全设置（这里我们只用第一种） 1.第一个自定义关键字是说你在以后发送的文字中必须要有这个关键字，否则发送不成功。 2.加签是一种特殊的加密方式，第一步，把timestamp+\u0026#34;\\n\u0026#34;+密钥当做签名字符串，使用HmacSHA256算法计算签名，然后进行Base64 encode，最后再把签名参数再进行urlEncode，得到最终的签名（需要使用UTF-8字符集）。 3.IP地址就是说你在发送时会获取你的IP地址，如果不匹配就发送不成功。这个加密的方式可以自己选择，我们选择加签。如果你想使用IP的话，可以访问https://ip.cn/ 三、启动钉钉插件dingtalk 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 vim /etc/systemd/system/prometheus-webhook-dingtalk.service #添加如下内容 [Unit] Description=prometheus-webhook-dingtalk After=network-online.target [Service] Restart=on-failure ExecStart=/opt/prometheus-webhook-dingtalk/prometheus-webhook-dingtalk --ding.profile=ops_dingding=自己钉钉机器人的Webhook地址 [Install] WantedBy=multi-user.target #命令行启动 systemctl daemon-reload systemctl start prometheus-webhook-dingtalk ss -tnl | grep 8060 #测试 curl -H \u0026#34;Content-Type: application/json\u0026#34; -d \u0026#39;{ \u0026#34;version\u0026#34;: \u0026#34;4\u0026#34;, \u0026#34;status\u0026#34;: \u0026#34;firing\u0026#34;, \u0026#34;description\u0026#34;:\u0026#34;description_content\u0026#34;}\u0026#39; http://localhost:8060/dingtalk/ops_dingding/send 四、配置Alertmanager 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 cat /opt/alertmanager/alertmanager.yml global: #每一分钟检查一次是否恢复 resolve_timeout: 1m route: #设置默认接收人 receiver: \u0026#39;webhook\u0026#39; #组告警等待时间。也就是告警产生后等待10s，如果有同组告警一起发出 group_wait: 10s #两组告警的间隔时间 group_interval: 10s #重复告警的间隔时间，减少相同微信告警的发送频率 repeat_interval: 1h #采用哪个标签来作为分组依据 group_by: [alertname] routes: - receiver: webhook group_wait: 10s match: team: node receivers: - name: \u0026#39;webhook\u0026#39; webhook_configs: - url: http://localhost:8060/dingtalk/ops_dingding/send #警报被解决之后是否通知 send_resolved: true #命令行启动 cd /opt/alertmanager/ ./alertmanager --config.file=alertmanager.yml \u0026amp; netstat -anput | grep 9093 五、关联Prometheus并配置报警规则 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 cat /opt/prometheus/rules/node_down.yml groups: - name: Node_Down rules: - alert: Node实例宕机 expr: up == 0 for: 10s labels: user: prometheus severity: Warning annotations: summary: \u0026#34;{{ $labels.instance }} 服务宕机\u0026#34; description: \u0026#34;{{ $labels.instance }} of job {{ $labels.job }} has been Down.\u0026#34; 修改Prometheus配置文件 cat /opt/prometheus/prometheus.yml # 修改以下内容 # Alertmanager configuration alerting: alertmanagers: - static_configs: - targets: [\u0026#34;localhost:9093\u0026#34;] rule_files: - \u0026#34;/opt/prometheus/rules/node_down.yml\u0026#34; # 实例存活报警规则文件 #重启 systemctl restart prometheus ","permalink":"https://ktzxy.top/posts/ojaef5l90k/","summary":"Prometheus基于Alertmanager实现钉钉告警","title":"Prometheus基于Alertmanager实现钉钉告警"},{"content":"﻿### 在超市管理数据库SuperMarket的基础上进行实验。\n1、编写一个事务处理：某学生买5袋薯片，如中间出现故障则回滚事务。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 --定义变量，用于累计事务执行过程中的错误 declare @ErrorSum int --初始化为0，即无错误 set @ErrorSum = 0 --编写事务 begin transaction --更新语句 update Goods set Number = Number + 5 where CategoryNO in ( select CategoryNO from Category where CategoryName like \u0026#39;饼干\u0026#39;) --累计是否有错误 set @ErrorSum=@ErrorSum+@@ERROR --判断执行过程中是否出现故障，是的话，回滚事务，否则提交事务 if(@ErrorSum \u0026gt; 0) rollback transaction else commit transaction 2、编写一个事务，当学生购买商品时，插入购买明细到SaleBill中，并修改Goods表以保持数据一致性，如中间出现故障则回滚事务。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 create trigger tri_SaleBill_Instered on SaleBill for insert as begin begin tran declare @ErrorSum int set @ErrorSum = 0 declare @number int,@goodsno varchar(10) select @number = Number,@goodsno = GoodsNO from inserted update Goods set Number = Number + @number where GoodsNO = @goodsno set @ErrorSum = @ErrorSum + @@ERROR if (@ErrorSum \u0026lt;\u0026gt; 0) rollback tran else commit tran end --测试 insert into SaleBill values(\u0026#39;GN0011\u0026#39;,\u0026#39;S01\u0026#39;,\u0026#39;2018-06-09 00:00:00.000\u0026#39;,5) 3、编写一个事务，当撤销某个学生购买明细时，删除SaleBill 中的记录，然后修改Goods表以保持数据一致性，如中间出现故障则回滚事务。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 create trigger tri_SaleBill_Delete on SaleBill for delete as begin begin tran declare @ErrorSum int set @ErrorSum = 0 declare @Number int select @Number = Number from deleted update Goods set Number = Number - @Number set @ErrorSum = @ErrorSum + @@ERROR if(@ErrorSum \u0026lt;\u0026gt; 0) rollback tran else commit tran end delete from SaleBill where GoodsNO = \u0026#39;GN0011\u0026#39; and SNO = \u0026#39;S07\u0026#39;; ","permalink":"https://ktzxy.top/posts/xl1qr9l77w/","summary":"实验10 数据库的并发控制","title":"实验10 数据库的并发控制"},{"content":"在进行sqlserver数据库事务复制 禁止分发和发布时，遇到如下错误 SQLServer 无法禁用“DELL”上的发布和分发。 X其他信息： 执行 Transact-SQL语句或批处理时发生了异常。（Mi crosoft.SqlServer.ConnectionInfo) 无法作为数据库主体执行，因为主体“db。”不存在、无法模拟这种类型的主体，或您没 有所需的权限 已将数据库上下文更改为“master”。（MicrosoftSQLServer，错误：15517)\n‌SQL Server 禁用发布和分发错误 15517 解决方案‌ 根据错误描述（错误：15517）及提示信息，问题核心在于 ‌数据库主体权限异常‌ 或 ‌复制元数据残留‌。以下是具体解决步骤：\n‌1. 清理复制元数据‌ 在 ‌发布数据库‌ 和 ‌订阅数据库‌ 中分别执行系统存储过程 sp_removedbreplication，强制清除复制相关的元数据残留24：\n1 2 3 4 5 sqlCopy CodeUSE [发布数据库名]; EXEC sp_removedbreplication; USE [订阅数据库名]; EXEC sp_removedbreplication; ‌作用‌：删除与复制相关的系统表记录，解决因元数据不一致导致的禁用失败问题。\n‌**2. 修复数据库所有者（dbo）**‌ 错误 15517 通常由数据库所有者（dbo）无效或权限丢失引发。按以下步骤修复：\n‌确认当前所有者\n1 2 sqlCopy CodeUSE [数据库名]; SELECT name AS OwnerName FROM sys.database_principals WHERE principal_id = 1; ‌修改为有效登录名（如sa)\n1 2 sqlCopy CodeUSE [数据库名]; ALTER AUTHORIZATION ON DATABASE::[数据库名] TO sa; ‌注意‌：需确保目标账户（如 sa）具有 sysadmin 角色权限47。\n‌3. 验证用户权限‌ 执行禁用操作的用户需满足以下条件：\n属于 ‌**sysadmin 固定服务器角色**‌。 对 ‌分发数据库‌ 和 ‌发布数据库‌ 拥有完全控制权限。 可通过以下命令检查权限： 1 2 sqlCopy Code-- 检查用户角色 SELECT IS_SRVROLEMEMBER(\u0026#39;sysadmin\u0026#39;, CURRENT_USER) AS IsSysAdmin; ‌4. 重新执行禁用操作‌ 完成上述步骤后，通过 SSMS 或 T-SQL 重新尝试禁用发布和分发：\n1 2 sqlCopy CodeUSE master; EXEC sp_dropdistributor @no_checks = 1, @ignore_distributor = 1; ‌参数说明‌：\n@no_checks = 1：跳过依赖项检查。 @ignore_distributor = 1：强制删除分发服务器配置23。 ‌5. 其他注意事项‌ ‌网络与连接‌：确保 SQL Server 实例间的网络连通性，避免因连接超时导致操作中断7。 ‌日志分析‌：检查 SQL Server 错误日志（ERRORLOG 文件），定位潜在的系统级错误35。 ‌总结‌ ‌步骤‌ ‌关键操作‌ ‌引用来源‌ 清理复制元数据 执行 sp_removedbreplication 24 修复数据库所有者 使用 ALTER AUTHORIZATION 重置权限 47 验证权限并重试禁用操作 强制删除分发配置 (sp_dropdistributor) 23 通过上述流程，可系统性解决因权限异常或元数据残留导致的发布和分发禁用失败问题\n","permalink":"https://ktzxy.top/posts/p3706qjruc/","summary":"‌SQL Server 禁用发布和分发错误 15517 解决方案‌","title":"‌SQL Server 禁用发布和分发错误 15517 解决方案‌"},{"content":"﻿\nDay-03-java流程控制 Scanner对象 java.util.Scanner是Java5的新特性，我们可以通过Scanner类来获取用户的输入。\n基本语法\n1 Scanner s = new Scanner(System.in); 通过Scanner类的next()与nextLine()方法获取输入的字符串，在读取前我们一般需要使用hasNext() 与hasNextLine()判断是否还有输入的数据。\n1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main(String[] args) { //创建一个扫描对象，用于接受键盘数据 Scanner scanner = new Scanner(System.in); System.out.println(\u0026#34;使用next方式接受：\u0026#34;); //判断用户有没有输入字符串 if (scanner.hasNext()){ //使用next方式接受 String str = scanner.next(); System.out.println(\u0026#34;输出的内容为：\u0026#34;+str); //凡是属于IO流的类如果不关闭会一直占用资源 scanner.close(); } } 1 2 3 4 5 6 7 8 9 10 11 public static void main(String[] args) { //从键盘接收数据 Scanner scanner = new Scanner(System.in); System.out.println(\u0026#34;使用nextLine方式接受\u0026#34;); //判断是否还有输入 if (scanner.hasNextLine()){ String str = scanner.nextLine(); System.out.println(\u0026#34;输出的内容是：\u0026#34;+str); scanner.close(); } } next(): 1、一定要读取到有效字符后才可以结束输入。 2、对输入有效字符之前遇到的空白，next()方法会自动将其去掉。\n3、只有输入有效字符后才将其后面输入的空白作为分隔符或者结束符。\n4、next()不能得到带有空格的字符串。 nextLine(): 1、以Enter为结束符,也就是说nextLine()方法返回的是输入回车之前的所有字符。\n2、可以获得空白。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int i = 0; float f = 0.0f; System.out.println(\u0026#34;请输入整数：\u0026#34;); if (scanner.hasNextInt()){ i = scanner.nextInt(); System.out.println(\u0026#34;整数数据：\u0026#34;+i); }else { System.out.println(\u0026#34;输入的不是整数数据！\u0026#34;); } System.out.println(\u0026#34;请输入小数：\u0026#34;); if (scanner.hasNextFloat()){ f = scanner.nextFloat(); System.out.println(\u0026#34;小数数据：\u0026#34;+f); }else { System.out.println(\u0026#34;输入的不是小数数据！\u0026#34;); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void main(String[] args) { //我们可以输入多个数字，并求其总和与平均数，每输入一个数字用回车确认，通过输入非数字来结束输入并输出执行结果: Scanner scanner= new Scanner(System.in); //和 double sum = 0; //计算输入了多少个数字 int m =0; System.out.println(\u0026#34;请输入数据：\u0026#34;); //通过循环判断是否还有输入，并在里面对每一次进行求和和统计 while (scanner.hasNextDouble()){ double x = scanner.nextDouble(); //m = m + 1; m++; sum = sum + x; System.out.println(\u0026#34;你输入了第\u0026#34;+m+\u0026#34;个数据，然后当前结果sum：\u0026#34;+sum); } System.out.println(m+\u0026#34;个数的和为：\u0026#34;+sum); System.out.println(m + \u0026#34;个数的平均数为：\u0026#34;+(sum/m)); scanner.close(); } 顺序结构 JAVA的基本结构就是顺序结构，除非特别指明，否则就按照顺序一句一句执行。顺序结构是最简单的算法结构。\n语句与语句之间，框与框之间是按从上到下的顺序进行的，它是由若干个依次执行的处理步骤组成的，它是任何一个算法都离不开的一种基本算法结构。\n选择结构 if单选择结构\nif双选择结构\nif多选择结构\n嵌套的if结构 switch多选择结构\nif单选择结构 我们很多时候需要去判断(个东西是否可行，然后我们才去执行，这样一个过程在程序中用if语句来表示\n语法：\n1 2 3 if(布尔表达式){ //如果布尔表达式为true将执行的语句 } 1 2 3 4 5 6 7 8 9 10 public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println(\u0026#34;请输入内容：\u0026#34;); String s = scanner.nextLine(); if (s.equals(\u0026#34;Hello\u0026#34;)){ System.out.println(s); } System.out.println(\u0026#34;End\u0026#34;); scanner.close(); } if 双选择结构 语法：\n1 2 3 4 5 if(布尔表达式){ //如果布尔表达式的值为true }else{ //如果布尔表达式的值为false } 1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main(String[] args) { //如果分数大于60就是及格，小于60就是不及格 Scanner scanner = new Scanner(System.in); System.out.println(\u0026#34;请输入你的成绩：\u0026#34;); int score = scanner.nextInt(); if (score\u0026lt;60){ System.out.println(\u0026#34;不及格\u0026#34;); }else { System.out.println(\u0026#34;及格\u0026#34;); } scanner.close(); } if 多选择结构 语法：\n1 2 3 4 5 6 7 8 9 if(布尔表达式1){ //如果布尔表达式1的值为true执行代码 }else if(布尔表达式2){ //如果布尔表达式2的值为true执行代码 }else if(布尔表达式3){ //如果布尔表达式3的值为true执行代码 }else { //如果以上布尔表达式都不为true执行代码 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println(\u0026#34;请输入你的成绩：\u0026#34;); int score = scanner.nextInt(); if (score==100){ System.out.println(\u0026#34;恭喜满分！\u0026#34;); }else if (score\u0026lt;60 \u0026amp;\u0026amp; score\u0026gt;=0){ System.out.println(\u0026#34;不及格\u0026#34;); }else if (score\u0026lt;70 \u0026amp;\u0026amp; score\u0026gt;=60){ System.out.println(\u0026#34;及格\u0026#34;); }else if (score\u0026lt;80 \u0026amp;\u0026amp; score\u0026gt;=70){ System.out.println(\u0026#34;差\u0026#34;); }else if (score\u0026lt;90 \u0026amp;\u0026amp; score\u0026gt;=80){ System.out.println(\u0026#34;良\u0026#34;); }else if (score\u0026lt;100 \u0026amp;\u0026amp; score\u0026gt;=90){ System.out.println(\u0026#34;优秀\u0026#34;); }else { System.out.println(\u0026#34;输入不合法！\u0026#34;); } scanner.close(); } 嵌套的if结构 使用嵌套的if\u0026hellip;else语句是合法的。也就是说你可以在另一个if或者else if语句中使用if或者else if 语句。你可以像if 语句一样嵌套else if\u0026hellip;else。 语法:\n1 2 3 4 5 6 if(布尔表达式1){ //如果布尔表达式1的值为true执行代码 if(布尔表达式2){ //如果布尔表达式2的值为true执行代码 } } 嵌套练习 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class demo2 { public static void main(String[] args) { //1 给出积分 //键盘接受数据 Scanner sc = new Scanner(System.in); System.out.println(\u0026#34;请输入会员积分：\u0026#34;); //先判断键盘输入的数据是不是整型 if (sc.hasNextInt()==true){ int score = sc.nextInt(); //判断这个整数是否为正数 if (score\u0026gt;=0){ String discount = \u0026#34;\u0026#34;; //2 根据积分判断折扣 if (score\u0026gt;=8000){ discount = \u0026#34;0.6\u0026#34;; }else if (score\u0026gt;=4000){ discount = \u0026#34;0.7\u0026#34;; }else if (score\u0026gt;=2000){ discount = \u0026#34;0.8\u0026#34;; }else{ discount = \u0026#34;0.9\u0026#34;; } System.out.println(\u0026#34;该会员享受的折扣为：\u0026#34;+discount); }else { System.out.println(\u0026#34;你输入的数据是负数，不符合要求\u0026#34;); } }else { System.out.println(\u0026#34;你输入的数据不是整型\u0026#34;); } } } switch多选择结构 多选择结构还有一个实现方式就是switch case语句。\nswitch case语句判断一个变量与一系列值中某个值是否相等，每个值称为一个分支。\n为了防止代码的“穿透”效果：在每个分支后面加上一个关键字break，遇到break这个分支就结束了\n类似else的“兜底”“备胎”的分支：default分支\ndefault分支可以写在任意的位置上，但是如果没有在最后一行，后面必须加上break关键字\n相邻分支逻辑是一样的，那么就可以只保留最后一个分支，上面的都可以省去不写了\nswitch分支和if分支区别：\n表达式是等值判断的话——if，switch都可以\n如果表达式时区间判断的情况——if最好\nswitch应用场合：就是等值判断，等值的情况比较少的情况下\n1 2 3 4 5 6 7 8 9 10 11 switch(expression){ case value : //语句 break;//可选 case value : //语句 break; //可选 //你可以有任意数量的case语句 default ://可选 //语句 } switch语句中的变量类型可以是:\nbyte、short、int或者char，String，枚举。\n同时case标签必须为字符串常量或字面量。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static void main(String[] args) { //case 穿透 //switch 匹配一个具体的值 char grade = \u0026#39;F\u0026#39;; switch (grade){ case \u0026#39;A\u0026#39;: System.out.println(\u0026#34;优秀\u0026#34;); break; case \u0026#39;B\u0026#39;: System.out.println(\u0026#34;良好\u0026#34;); break; case \u0026#39;C\u0026#39;: System.out.println(\u0026#34;及格\u0026#34;); break; case \u0026#39;D\u0026#39;: System.out.println(\u0026#34;再接再厉\u0026#34;); break; default: System.out.println(\u0026#34;未知等级\u0026#34;); } } 从Java SE7开始\nswitch支持字符串String类型了\n1 2 3 4 5 6 7 8 9 10 11 12 13 public static void main(String[] args) { String name = \u0026#34;赵云\u0026#34;; switch (name){ case \u0026#34;英雄\u0026#34;: System.out.println(\u0026#34;英雄\u0026#34;); break; case \u0026#34;五虎上将\u0026#34;: System.out.println(\u0026#34;五虎上将\u0026#34;); break; default: System.out.println(\u0026#34;不知道\u0026#34;); } } 循环结构 while循环\ndo\u0026hellip;while\n循环for循环\n在Java5中引入了一种主要用于数组的增强型for循环。\nwhile循环 while是最基本的循环，它的结构为:\n1 2 3 while(布尔表达式 ){ /循环内容 } 只要布尔表达式为true，循环就会一直执行下去。 我们大多数情况是会让循环停止下来的，我们需要一个让表达式失效的方式来结束循环。\n少部分情况需要循环一直执行，比如服务器的请求响应监听等。 循环条件一直为true就会造成无限循环【死循环】，我们正常的业务编程中应该尽量避免死循环。会影响程序性能或者造成程序卡死奔溃!\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public static void main(String[] args) { //计算1+2+...+100? int i =1; //条件初始化 int sum = 0; while (i\u0026lt;=100){ //条件判断 sum+=i; //循环体 i++; //迭代 } System.out.println(sum); //计算2+4+...+998+1000 int i =2; int sum = 0; while (i\u0026lt;=1000){ sum += i; i = i +2; } System.out.println(sum); //计算1*3*5*7*9*11*13 int i = 1; int result = 1; while (i\u0026lt;=13){ result *= i; i += 2; } System.out.println(result); } do\u0026hellip;while循环 对于while语句而言，如果不满足条件，则不能进入循环。但有时候我们需要即使不满足条件,也至少执行一次。 do\u0026hellip;while循环和while循环相似，不同的是，do\u0026hellip;while循环至少会执行一次。\n1 2 3 do { //代码语句 }while(布尔表达式); 1 2 3 4 5 6 7 8 9 public static void main(String[] args) { int i = 0; int sum = 0; do { sum+=i; i++; }while (i\u0026lt;=100); System.out.println(sum); } While和do-While的区别: while：先判断，后执行。\n​\tdowhile：先执行，后判断! 至少被执行一次，第二次才开始判断！\n​\tDo..while总是保证循环体会被至少执行一次!这是他们的主要差别。\n1 2 3 4 5 6 7 8 9 10 11 12 public static void main(String[] args) { int a =0; while (a\u0026lt;0){ System.out.println(a); //不输出 a++; } System.out.println(\u0026#34;===============\u0026#34;); do { System.out.println(a); //输出0 a++; }while (a\u0026lt;0); } For循环 虽然所有循环结构都可以用while或者do\u0026hellip;while表示，但Java提供了另一种语句——for循环，使一些循环结构变得更加简单。\nfor循环语句是支持迭代的一种通用结构，是最有效、最灵活的循环结构。\nfor循环执行的次数是在执行前就确定的。\n语法格式如下:\n1 2 3 for(初始化;条件判断;迭代){ //代码语句 } 关于for循环有以下几点说明:\n​\t最先执行初始化步骤。可以声明一种类型，但可初始化一个或多个循环控制变量，也可以是空语句。\n​\t然后，检测布尔表达式的值。如果为 true，循环体被执行。如果为false，循环终止，开始执行循环体后面的语句。\n​\t执行一次循环后，更新循环控制变量(迭代因子控制循环变量的增减)。\n​\t再次检测布尔表达式。循环执行上面的过程。\n​\ti的作用域：作用范围：离变量最近{}\n​\t循环分为两大类：\n​\t第一类：当型 while(){} for(){}\n​\t第二类：直到型 do{}while();\n1 2 3 4 5 6 7 8 9 10 //死循环 for (;;){ } while(true){ } do{ }while(true); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main(String[] args) { //练习一：计算0到100之间的奇数和偶数的和 int oddSum = 0; int evenSum = 0; for (int i = 0; i \u0026lt;= 100; i++) { if (i%2!=0){ oddSum+=i; }else { evenSum+=i; } } System.out.println(\u0026#34;奇数的和：\u0026#34;+oddSum); //2500 System.out.println(\u0026#34;偶数的和：\u0026#34;+evenSum); //2550 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main(String[] args) { //练习二：用while或for循环输出1——1000之间能被5整除的数，并且每行输出3个 for (int i = 0; i \u0026lt;= 1000; i++) { if (i%5==0){ System.out.print(i+\u0026#34;\\t\u0026#34;); } if (i%(3*5)==0){ //每行输出3个 System.out.println(); //System.out.println(\u0026#34;\\n\u0026#34;); } } //println 输出完会换行 //print 输出完不会换行 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void main(String[] args) { /*打印九九乘法表 1.先打印出第一列 2.再用一个循环包起来 3.去掉重复项 i \u0026lt;= j 4.调整样式 */ for (int i = 1; i \u0026lt;=9 ; i++) { for (int j = 1; j \u0026lt;=i ; j++) { System.out.print(i + \u0026#34;*\u0026#34; + j + \u0026#34;=\u0026#34; + (i*j) + \u0026#34;\\t\u0026#34;); } System.out.println(); } System.out.println(\u0026#34;===============================\u0026#34;); for (int i = 9; i \u0026gt;=1 ; i--) { for (int j = 1; j \u0026lt;=i ; j++) { System.out.print(i + \u0026#34;*\u0026#34; + j + \u0026#34;=\u0026#34; + (i*j) + \u0026#34;\\t\u0026#34;); } System.out.println(); } } 增强for循环 数组重点使用\nJava5引入了一种主要用于数组或集合的增强型for循环。\nJava 增强for循环语法格式如下:\n1 2 3 4 for(声明语句∶表达式) { //代码句子 } 声明语句:声明新的局部变量，该变量的类型必须和数组元素的类型匹配。其作用域限定在循环语句块，其值与此时数组元素的值相等。\n表达式:表达式是要访问的数组名，或者是返回值为数组的方法。\n1 2 3 4 5 6 7 8 9 10 11 12 public static void main(String[] args) { int [] numbers = {10,20,30,40,50}; //定义了一个数组 for (int i =0;i\u0026lt;5;i++){ System.out.println(numbers[i]); } System.out.println(\u0026#34;=====================\u0026#34;); //遍历数组的元素 for (int x:numbers){ System.out.println(x); } } break continue break在任何循环语句的主体部分，均可用break控制循环的流程。break用于强行退出循环，不执行循环中剩余的语句。(break语句也在switch语句中使用)\ncontinue语句用在循环语句体中，用于终止某次循环过程，即跳过循环体中尚未执行的语句，接着进行下一次是否执行循环的判定。\nreturn的作用，结束当前所在方法的执行。\n1 2 3 4 5 6 7 8 9 break的作用：停止最近的循环 public static void main(String[] args) { for (int i = 1; i \u0026lt;=100 ; i++) { System.out.println(i); while (i==36){ break; //break停止的是里面的while循环，而不是外面的for循环 } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /* 功能：输出1——100中被6整除的数： 方式一： for (int i = 1; i \u0026lt;=100 ; i++) { if (i%6==0){ System.out.println(i); } } */ /* 方式二 for (int i = 1; i \u0026lt;=100 ; i++) { if (i%6!=0){ continue; //停止本次循环，继续下一次循环 } System.out.println(i); } */ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static void main(String[] args) { int i =0; while (i\u0026lt;100){ i++; System.out.println(i); if (i==30){ break; } } //continue int d = 0; while (d\u0026lt;100){ d++; if (d%10==0){ System.out.println(); continue; } System.out.print(d); } /* 1.break在任何循环语句的主体部分，均可用break控制循环的流程。 2.break用于强行退出得环，不执行循环中剩余的语句。(break 语句也在switch语句中使用) 3.continue 语句用在循环语句体中,用于终s止t某次循环过程，即跳过循环体中尚未执行的语句，接着进行下一 次是否执行循环的判定。 */ } 关于goto关键字\n​\tgoto关键字很早就在程序设计语言中出现。尽管goto仍是Java的一个保留字，但并未在语言中得到正式使用;Java没有goto。然而，在break和continue这两个关键字的身上，我们仍然能看出一些goto的影子\u0026mdash;带标签的break和continue。 ​\t“标签”是指后面跟一个冒号的标识符，例如:label ​\t对Java来说唯一用到标签的地方是在循环语句之前。而在循环之前设置标签的唯一理由是:我们希望在其中嵌套另一个循环，由于break和continue关键字通常只中断当前循环，但若随同标签使用，它们就会中断到存在标签的地方。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main(String[] args) { //打印101-150之间所有的质数 //质数是指大于1的自然数中，除了1和它本身以外不再有其他因数的自然数 //不建议使用 int count = 0; outer:for (int i =101;i\u0026lt;150;i++){ for (int j = 2; j\u0026lt;i/2;j++){ if (i % j ==0){ continue outer; } } System.out.print(i+\u0026#34; \u0026#34;); } } 练习一： 1 2 3 4 5 6 7 8 9 10 11 //输出输出1——100被5整除的数，每行输出6个 int sount = 0; //引入一个计数器，初始值为0 for (int i = 1; i \u0026lt;=100 ; i++) { if (i%5==0){ //被5整除的数 System.out.print(i+\u0026#34;\\t\u0026#34;); sount ++; //每在控制台输出一个数，count就加1操作 if (sount%6==0){ System.out.println(); //换行 } } } 练习二 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 /* 功能： 1.请输入10个整数，当输入的数是666的时候，退出程序 2.判断其中录入正数的个数并输出 3.判断系统的退出状态：是正常退出还是被迫退出 */ int count = 0; //引入一个计数器 boolean flag = true; //引入一个布尔类型的变量 --》理解为一个“开关”，默认情况下开关是开着的 Scanner sc = new Scanner(System.in); for (int i = 1; i \u0026lt;=10 ; i++) { //i：循环次数 System.out.println(\u0026#34;请录入第\u0026#34;+i+\u0026#34;个数：\u0026#34;); int num = sc.nextInt(); if (num\u0026gt;0){ //录入的正数 count++; } if (num==666){ flag=false; //当遇到666的时候，“开关”被关上 //退出循环 break; } } System.out.println(\u0026#34;你录入的正数个数为\u0026#34;+count); if (flag){ //flag=true System.out.println(\u0026#34;正常退出\u0026#34;); }else { //flag = false System.out.println(\u0026#34;被迫退出\u0026#34;); } 练习三 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public static void main(String[] args) { //打印三角形 5行 for (int i = 1; i \u0026lt;= 5; i++){ for (int j = 5;j \u0026gt;=i ; j--){ System.out.print(\u0026#34; \u0026#34;); } for (int j =1;j \u0026lt;=i ; j++){ System.out.print(\u0026#34;*\u0026#34;); } for (int j =1;j \u0026lt; i ; j++){ System.out.print(\u0026#34;*\u0026#34;); } System.out.println(); } } 练习四 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 //前面带有空隙的长方形 for (int j = 1; j \u0026lt;=4 ; j++) { //控制行数 for (int i = 1; i \u0026lt;=5 ; i++) { //控制空格的个数 System.out.print(\u0026#34; \u0026#34;); } for (int i = 1; i \u0026lt;=9 ; i++) { //控制*的个数 System.out.print(\u0026#34;*\u0026#34;); } System.out.println(); } //平行四边形 for (int j = 1; j \u0026lt;=4 ; j++) { //控制行数 for (int i = 1; i \u0026lt;=(9-j) ; i++) { //控制空格的个数 System.out.print(\u0026#34; \u0026#34;); } for (int i = 1; i \u0026lt;=9 ; i++) { //控制*的个数 System.out.print(\u0026#34;*\u0026#34;); } System.out.println(); } //实心菱形一 //上面三角形 for (int j = 1; j \u0026lt;=4 ; j++) { //控制行数 for (int i = 1; i \u0026lt;=(9-j) ; i++) { //控制空格的个数 System.out.print(\u0026#34; \u0026#34;); } for (int i = 1; i \u0026lt;=(2*j-1) ; i++) { //控制*的个数 System.out.print(\u0026#34;*\u0026#34;); } System.out.println(); } //下面三角形 for (int j = 1; j \u0026lt;=3 ; j++) { //控制行数 for (int i = 1; i \u0026lt;=(j+5) ; i++) { //控制空格的个数 System.out.print(\u0026#34; \u0026#34;); } for (int i = 1; i \u0026lt;=(7-2*j) ; i++) { //控制*的个数 System.out.print(\u0026#34;*\u0026#34;); } System.out.println(); } //实心菱形二 int size = 15; int startNum = size/2+1; //起始列号 int endNum = size/2+1; //结束列号 boolean flag = true; for (int j = 1; j \u0026lt;=size ; j++) { for (int i = 1; i \u0026lt;=size ; i++) { if (i\u0026gt;=startNum \u0026amp;\u0026amp; i\u0026lt;=endNum){ System.out.print(\u0026#34;*\u0026#34;); }else { System.out.print(\u0026#34; \u0026#34;); } } //换行 System.out.println(); if (endNum==size){ flag = false; } if (flag){ startNum--; endNum++; }else { startNum++; endNum--; } } //空心菱形一 //上面三角形 for (int j = 1; j \u0026lt;=4 ; j++) { //控制行数 for (int i = 1; i \u0026lt;=(9-j) ; i++) { //控制空格的个数 System.out.print(\u0026#34; \u0026#34;); } for (int i = 1; i \u0026lt;=(2*j-1) ; i++) { //控制*的个数 if (i==1 || i == (2*j-1)){ System.out.print(\u0026#34;*\u0026#34;); }else { System.out.print(\u0026#34; \u0026#34;); } } System.out.println(); } //下面三角形 for (int j = 1; j \u0026lt;=3 ; j++) { //控制行数 for (int i = 1; i \u0026lt;=(j+5) ; i++) { //控制空格的个数 System.out.print(\u0026#34; \u0026#34;); } for (int i = 1; i \u0026lt;=(7-2*j) ; i++) { //控制*的个数 if (i==1 || i == (7-2*j)){ System.out.print(\u0026#34;*\u0026#34;); }else { System.out.print(\u0026#34; \u0026#34;); } } System.out.println(); } //空心菱形二 int size = 15; int startNum = size/2+1; //起始列号 int endNum = size/2+1; //结束列号 boolean flag = true; for (int j = 1; j \u0026lt;=size ; j++) { for (int i = 1; i \u0026lt;=size ; i++) { if (i==startNum || i==endNum){ System.out.print(\u0026#34;*\u0026#34;); }else { System.out.print(\u0026#34; \u0026#34;); } } //换行 System.out.println(); if (endNum==size){ flag = false; } if (flag){ startNum--; endNum++; }else { startNum++; endNum--; } } ","permalink":"https://ktzxy.top/posts/f8ec3i652s/","summary":"Day 03 java流程控制","title":"Day 03 java流程控制"},{"content":"﻿### 1、使用教材P48页上的SQL语句，在数据库sjkDB上创建教材的表3-3和表3-4所示的员工表和薪资表，使用SQL语句或SQL Server Management Studio完成教材例3-16~例3-24的操作。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 alter table staff add constraint pk_yg primary key(id) alter table salary add constraint U_xinzname unique(Saname) alter table salary add constraint DF_jichu default 1200 for bsalary alter table salary add constraint CK_shifa check (Psalary\u0026lt;Sapayabl) alter table staff add constraint CK_phone check (phone like\u0026#39;[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]\u0026#39;) alter table salary with nocheck add constraint CK_nock check (Psalary\u0026lt;Sapayabl) alter table staff add constraint FK_xinji foreign key(salaryid) references salary (salaryid) alter table staff add constraint FK_xinji foreign key(salaryid) references salary (salaryid) on delete cascade on update cascade 2、在students数据库上，使用SQL语句完成下列操作： （1）为Sc表中的Sno添加外码约束，引用Student表的Sno；为Sc表添加外码约束，引用Course表的Cno。\n1 2 3 4 alter table Sc add constraint FK_sc foreign key(Sno) references Student(Sno), foreign key(Cno) references Course(Cno) （2）为Student表中的Sname列添加唯一约束，使其值不重复。\n1 2 alter table Student add constraint U_sname unique(Sname) （3）为Sc表中的Grade列添加CHECK约束，使其值为0~100。\n1 2 alter table Sc add constraint CK_grade check (Grade between 1 and 100) （4）为Student表中的Sage列添加DEFAULT约束，使其默认值为19。\n1 2 alter table Student add constraint DF_sage default 19 for Sage （5）删除第（4）题中的DEFAULT约束。\n1 alter table Student drop DF_sage ","permalink":"https://ktzxy.top/posts/f7acb2jik0/","summary":"实验3 数据完整性","title":"实验3 数据完整性"},{"content":"[toc]\n1. Mysql 索引 1.1. Mysql如何实现的索引机制？ MySQL中索引分三类：B+树索引、Hash索引、全文索引\n1.1.1. InnoDB索引与MyISAM索引实现的区别是什么？ MyISAM的索引方式都是非聚簇的，与InnoDB包含1个聚簇索引是不同的。 在InnoDB存储引擎中，我们只需要根据主键值对聚簇索引进行一次查找就能找到对应的记录，而在MyISAM中却需要进行一次回表操作，意味着MyISAM中建立的索引相当于全部都是二级索引。 InnoDB的数据文件本身就是索引文件，而MyISAM索引文件和数据文件是分离的 ，索引文件仅保存数据记录的地址。 MyISAM的表在磁盘上存储在以下文件中： *.sdi（描述表结构）、*.MYD（数据），*.MYI（索引） InnoDB的表在磁盘上存储在以下文件中： .ibd（表结构、索引和数据都存在一起） InnoDB的非聚簇索引data域存储相应记录主键的值 ，而MyISAM索引记录的是地址 。换句话说，InnoDB的所有非聚簇索引都引用主键作为data域。 MyISAM的回表操作是十分快速的，因为是拿着地址偏移量直接到文件中取数据的，反观InnoDB是通过获取主键之后再去聚簇索引里找记录，虽然说也不慢，但还是比不上直接用地址去访问。 InnoDB要求表必须有主键 （ MyISAM可以没有 ）。如果没有显式指定，则MySQL系统会自动选择一个可以非空且唯一标识数据记录的列作为主键。如果不存在这种列，则MySQL自动为InnoDB表生成一个隐含字段作为主键，这个字段长度为6个字节，类型为长整型。 1.1.2. 一个表中如果没有创建索引，那么还会创建B+树吗？ 会\n如果有主键会创建聚簇索引 如果没有主键会生成rowid作为隐式主键 1.2. 说一下B+树索引实现原理（数据结构） 1.2.1. 讲义 假设有一个表index_demo，表中有2个INT类型的列，1个CHAR(1)类型的列，c1列为主键：\n1 CREATE TABLE index_demo(c1 INT,c2 INT,c3 CHAR(1),PRIMARY KEY(c1)) ; index_demo表的简化的行格式示意图如下：\n我们只在示意图里展示记录的这几个部分：\nrecord_type：表示记录的类型， 0是普通记录、 2是最小记录、 3 是最大记录、1是B+树非叶子节点记录。 next_record：表示下一条记录的相对位置，我们用箭头来表明下一条记录。 各个列的值：这里只记录在 index_demo 表中的三个列，分别是 c1 、 c2 和 c3 。 其他信息：除了上述3种信息以外的所有信息，包括其他隐藏列的值以及记录的额外信息。 将其他信息项暂时去掉并把它竖起来的效果就是这样：\n把一些记录放到页里的示意图就是（这里一页就是一个磁盘块，代表一次IO）：\nname age sex\nMySQL InnoDB的默认的页大小是16KB，因此数据存储在磁盘中，可能会占用多个数据页。如果各个页中的记录没有规律，我们就不得不依次遍历所有的数据页。如果我们想快速的定位到需要查找的记录在哪些数据页中，我们可以这样做 ：\n下一个数据页中用户记录的主键值必须大于上一个页中用户记录的主键值 给所有的页建立目录项 以页28为例，它对应目录项2 ，这个目录项中包含着该页的页号28以及该页中用户记录的最小主键值 5。我们只需要把几个目录项在物理存储器上连续存储（比如：数组），就可以实现根据主键值快速查找某条记录的功能了。比如：查找主键值为 20 的记录，具体查找过程分两步：\n先从目录项中根据二分法快速确定出主键值为20的记录在目录项3中（因为 12 ≤ 20 \u0026lt; 209 ），对应页9。 再到页9中根据二分法快速定位到主键值为 20 的用户记录。 至此，针对数据页做的简易目录就搞定了。这个目录有一个别名，称为索引 。\n1.2.2. InnoDB中的索引方案 我们新分配一个编号为30的页来专门存储目录项记录，页10、28、9、20专门存储用户记录：\n目录项记录和普通的用户记录的不同点：\n目录项记录 的 record_type 值是1，而 普通用户记录 的 record_type 值是0。 目录项记录只有主键值和页的编号两个列，而普通的用户记录的列是用户自己定义的，包含很多列，另外还有InnoDB自己添加的隐藏列。 现在查找主键值为 20 的记录，具体查找过程分两步：\n先到页30中通过二分法快速定位到对应目录项，因为 12 ≤ 20 \u0026lt; 209 ，就是页9。 再到页9中根据二分法快速定位到主键值为 20 的用户记录。 更复杂的情况如下：\n我们生成了一个存储更高级目录项的 页33 ，这个页中的两条记录分别代表页30和页32，如果用户记录的主键值在 [1, 320) 之间，则到页30中查找更详细的目录项记录，如果主键值 不小于320 的话，就到页32中查找更详细的目录项记录。这个数据结构，它的名称是 B+树 。\n1.2.3. 聚簇索引与非聚簇索引b+树实现有什么区别？ 1.2.3.1. 聚簇索引 特点：\n索引和数据保存在同一个B+树中\n页内的记录是按照主键的大小顺序排成一个单向链表 。\n页和页之间也是根据页中记录的主键的大小顺序排成一个双向链表 。\n非叶子节点存储的是记录的主键+页号。\n叶子节点存储的是完整的用户记录。\n优点：\n数据访问更快 ，因为索引和数据保存在同一个B+树中，因此从聚簇索引中获取数据比非聚簇索引更快。 聚簇索引对于主键的排序查找和范围查找速度非常快。 按照聚簇索引排列顺序，查询显示一定范围数据的时候，由于数据都是紧密相连，数据库可以从更少的数据块中提取数据，节省了大量的IO操作 。 缺点：\n插入速度严重依赖于插入顺序 ，按照主键的顺序插入是最快的方式，否则将会出现页分裂，严重影响性能。因此，对于InnoDB表，我们一般都会定义一个自增的ID列为主键。 更新主键的代价很高 ，因为将会导致被更新的行移动。因此，对于InnoDB表，我们一般定义主键为不可更新。 限制：\n只有InnoDB引擎支持聚簇索引，MyISAM不支持聚簇索引。 由于数据的物理存储排序方式只能有一种，所以每个MySQL的表只能有一个聚簇索引。 如果没有为表定义主键，InnoDB会选择非空的唯一索引列代替。如果没有这样的列，InnoDB会隐式的定义一个主键作为聚簇索引。 为了充分利用聚簇索引的聚簇特性，InnoDB中表的主键应选择有序的id，不建议使用无序的id，比如UUID、MD5、HASH、字符串作为主键，无法保证数据的顺序增长。 1.2.3.2. 非聚簇索引 （二级索引、辅助索引）\n聚簇索引，只能在搜索条件是主键值时才发挥作用，因为B+树中的数据都是按照主键进行排序的，如果我们想以别的列作为搜索条件，那么需要创建非聚簇索引。\n例如，以c2列作为搜索条件，那么需要使用c2列创建一棵B+树，如下所示：\n这个B+树与聚簇索引有几处不同：\n页内的记录是按照从c2列的大小顺序排成一个单向链表 。\n页和页之间也是根据页中记录的c2列的大小顺序排成一个双向链表 。\n非叶子节点存储的是记录的c2列+页号。\n叶子节点存储的并不是完整的用户记录，而只是c2列+主键这两个列的值。\n一张表可以有多个非聚簇索引：\n1.2.4. 说一下B+树中聚簇索引的查找（匹配）逻辑 1.2.5. 说一下B+树中非聚簇索引的查找（匹配）逻辑 **例如：**根据c2列的值查找c2=4的记录，查找过程如下：\n根据根页面44定位到页42（因为2 ≤ 4 \u0026lt; 9） 由于c2列没有唯一性约束，所以c2=4的记录可能分布在多个数据页中，又因为 2 ≤ 4 ≤ 4，所以确定实际存储用户记录的页在页34和页35中。 在页34和35中定位到具体的记录。 但是这个B+树的叶子节点只存储了c2和c1（主键）两个列，所以我们必须再根据主键值去聚簇索引中再查找一遍完整的用户记录。 like 张% 1.2.6. 平衡二叉树，红黑树，B树和B+树的区别是什么？都有哪些应用场景？ 平衡二叉树\n基础数据结构 左右平衡 高度差大于1会自旋 每个节点记录一个数据 平衡二叉树（AVL）\nAVL树全称G.M. Adelson-Velsky和E.M. Landis，这是两个人的人名。\n平衡二叉树也叫平衡二叉搜索树（Self-balancing binary search tree）又被称为AVL树， 可以保证查询效率较高。\n具有以下特点：\n它是一棵空树或它的左右两个子树的高度差的绝对值不超过1 并且左右两个子树都是一棵平衡二叉树。 AVL的生成演示：https://www.cs.usfca.edu/~galles/visualization/AVLtree.html\nAVL的问题\n众所周知，IO操作的效率很低，在大量数据存储中，查询时我们不能一下子将所有数据加载到内存中，只能逐节点加载（一个节点一次IO）。如果我们利用二叉树作为索引结构，那么磁盘的IO次数和索引树的高度是相关的。平衡二叉树由于树深度过大而造成磁盘IO读写过于频繁，进而导致效率低下。\n为了提高查询效率，就需要 减少磁盘IO数 。为了减少磁盘IO的次数，就需要尽量降低树的高度 ，需要把原来“瘦高”的树结构变的“矮胖”，树的每层的分叉越多越好。针对同样的数据，如果我们把二叉树改成 三叉树：\n上面的例子中，我们将二叉树变成了三叉树，降低了树的高度。如果能够在一个节点中存放更多的数据，我们还可以进一步减少节点的数量，从而进一步降低树的高度。这就是多叉树。\n普通树的问题\n左子树全部为空，从形式上看，更像一个单链表，不能发挥BST的优势。 解决方案：平衡二叉树(AVL) 红黑树\nhashmap存储 两次旋转达到平衡 分为红黑节点 在这个棵严格的平台树上又进化为“红黑树”{是一个非严格的平衡树 左子树与右子树的高度差不能超过1}，红黑树的长子树只要不超过短子树的两倍即可！\n当再次插入7的时候，这棵树就会发生旋转\nB+ 树和 B 树的差异：\nB+树中非叶子节点的关键字也会同时存在在子节点中，并且是在子节点中所有关键字的最大值（或最小）。 B+树中非叶子节点仅用于索引，不保存数据记录，跟记录有关的信息都放在叶子节点中。而B树中，非叶子节点既保存索引，也保存数据记录。 B+树中所有关键字都在叶子节点出现，叶子节点构成一个有序链表，而且叶子节点本身按照关键字的大小从小到大顺序链接。 1.2.7. 一个b+树中大概能存放多少条索引记录？ 真实环境中一个页存放的记录数量是非常大的（默认16KB），假设指针与键值忽略不计（或看做10个字节），数据占 1 kb 的空间： 如果B+树只有1层，也就是只有1个用于存放用户记录的节点，最多能存放 16 条记录。 如果B+树有2层，最多能存放 1600×16=25600 条记录。 如果B+树有3层，最多能存放 1600×1600×16=40960000 条记录。 如果存储千万级别的数据，只需要三层就够了 B+树的非叶子节点不存储用户记录，只存储目录记录，相对B树每个节点可以存储更多的记录，树的高度会更矮胖，IO次数也会更少。\n1.2.8. 使用B+树存储的索引crud执行效率如何？ c 新增\nO(lognN)\nN = 高度\n1.2.9. 什么是自适应哈希索引？ 自适应哈希索引是Innodb引擎的一个特殊功能，当它注意到某些索引值被使用的非常频繁时，会在内存中基于B-Tree所有之上再创建一个哈希索引，这就让B-Tree索引也具有哈希索引的一些优点，比如快速哈希查找。这是一个完全自动的内部行为，用户无法控制或配置\n使用命令\n1 SHOW ENGINE INNODB STATUS \\G ; 查看INSERT BUFFER AND ADAPTIVE HASH INDEX\n1.2.10. 什么是2-3树 2-3-4树？ 多叉树（multiway tree）允许每个节点可以有更多的数据项和更多的子节点。2-3树，2-3-4树就是多叉树，多叉树通过重新组织节点，减少节点数量，增加分叉，减少树的高度，能对二叉树进行优化。\n2-3树\n下面2-3树就是一颗多叉树\n2-3树具有如下特点：\n2-3树的所有叶子节点都在同一层。 有两个子节点的节点叫二节点，二节点要么没有子节点，要么有两个子节点。 有三个子节点的节点叫三节点，三节点要么没有子节点，要么有三个子节点。 2-3树是由二节点和三节点构成的树。 对于三节点的子树的值大小仍然遵守 BST 二叉排序树的规则。 2-3-4树\n1.3. 为什么官方建议使用自增长主键作为索引？（说一下自增主键和字符串类型主键的区别和影响） 自增主键能够维持底层数据顺序写入 读取可以由b+树的二分查找定位 支持范围查找，范围数据自带顺序 字符串无法完成以上操作\n1.3.1. 使用int自增主键后 最大id是10，删除id 10和9，再添加一条记录，最后添加的id是几？删除后重启mysql然后添加一条记录最后id是几？ 删除之后\n如果重启，会从最大的id开始递增 如果没重启，会延续删除之前最大的id开始递增 1.4. 索引的优缺点是什么？ 优点\n聚簇（主键）索引：\n顺序读写 范围快速查找 范围查找自带顺序 非聚簇索引：\n条件查询避免全表扫描scan 范围，排序，分组查询返回行id，排序分组后，再回表查询完整数据，有可能利用顺序读写 覆盖索引不需要回表操作 索引的代价\n索引是个好东西，可不能乱建，它在空间和时间上都会有消耗：\n空间上的代价 每建立一个索引都要为它建立一棵B+树，每一棵B+树的每一个节点都是一个数据页，一个页默认会占用 16KB 的存储空间，一棵很大的B+树由许多数据页组成，那就是很大的一片存储空间。\n时间上的代价 每次对表中的数据进行 增、删、改 操作时，都需要去修改各个B+树索引。而增、删、改操作可能会对节点和记录的排序造成破坏，所以存储引擎需要额外的时间进行一些记录移位、页面分裂、页面回收等操作来维护好节点和记录的排序。如果我们建了许多索引，每个索引对应的B+树都要进行相关的维护操作，会给性能拖后腿。\nB 树和 B+ 树都可以作为索引的数据结构，在 MySQL 中采用的是 B+ 树。\n但B树和B+树各有自己的应用场景，不能说B+树完全比B树好，反之亦然。\n1.4.1. 使用索引一定能提升效率吗？ 不一定\n少量数据全表扫描也很快，可以直接获取到全量数据 唯一索引会影响插入速度，但建议使用 索引过多会影响更新，插入，删除数据速度 1.4.2. 如果是大段文本内容，如何创建（优化）索引？ B 树和 B+ 树都可以作为索引的数据结构，在 MySQL 中采用的是 B+ 树。\n第一种方式是分表存储，然后创建索引\n第二是使用es为大文本创建索引\n1.5. 什么是聚簇索引？ 聚簇索引数据和索引存放在一起组成一个b+树\n参考005题\n1.5.1. 一个表中可以有多个（非）聚簇索引吗？ 聚簇索引只能有一个\n非聚簇索引可以有多个\n1.5.2. 聚簇索引与非聚集索引的特点是什么？ 参考005题\n1.5.3. CRUD时聚簇索引与非聚簇索引的区别是什么？ 聚簇索引插入新值比采用非聚簇索引插入新值的速度要慢很多，因为插入要保证主键不能重复 聚簇索引范围，排序查找效率高，因为是有序的 非聚簇索引访问需要两次索引查找，第一次找到主键值，第二次根据主键值找到行数据 1.5.4. 非聚簇索引为什么不存数据地址值而存储主键？ 因为聚簇索引中有时会引发分页操作、重排操作数据有可能会移动\n1.6. 什么是回表操作？ id age name sex\nage -\u0026gt; index\nselect * from user where age \u0026gt;20 ;\n第一次 取回id，第二次（回表）根据id拿到完整数据\nselect * from user where age \u0026gt;20 ;\n1.6.1. 什么是覆盖索引？ id age name sex\nage -\u0026gt; index\nselect * from user where age \u0026gt;20 ;\n第一次 取回id，第二次（回表）根据id拿到完整数据\nage,name -\u0026gt; index\nselect age from user where age \u0026gt;20 and name like\u0026quot;张%\u0026quot; ;\n覆盖索引不会回表查询，查询效率也是比较高的\n1.6.2. 非聚集索引一定回表查询吗? 不一定，只要b+树中包含的字段（创建索引的字段），覆盖（包含）想要select 的字段，那么就不会回表查询了。\n1.6.3. 为什么要回表查询？直接存储数据不可以吗？ 为了控制非聚簇索引的大小\n1.6.4. 如果把一个 InnoDB 表的主键删掉，是不是就没有主键，就没办法进行回表查询了？ 不是，InnoDB会生成rowid辅助回表查询\n1.7. 什么是联合索引，组合索引，复合索引？ 为c2和c3列建立联合索引，如下所示：\nc2，c3 - \u0026gt; index\nc3,c2 -\u0026gt; index\nwhere c3=?\n全职匹配\n最左前缀\n1.7.1. 复合索引创建时字段顺序不一样使用效果一样吗？ 我们也可以同时以多个列的大小作为排序规则，也就是同时为多个列建立索引，比方说我们想让B+树按照 c2和c3列 的大小进行排序，这个包含两层含义：\n先把各个记录和页按照c2列进行排序。 在记录的c2列相同的情况下，采用c3列进行排序 B+树叶子节点处的记录由c2列、c3列和主键c1列组成 本质上也是二级索引 create index idx_c2_c3 on user (c2,c3); 1.8. 什么是唯一索引？ 随表一起创建索引： 1 2 3 4 5 6 7 8 9 10 11 CREATE TABLE customer ( id INT UNSIGNED AUTO_INCREMENT, customer_no VARCHAR(200), customer_name VARCHAR(200), PRIMARY KEY(id), -- 主键索引：列设定为主键后会自动建立索引，唯一且不能为空。 UNIQUE INDEX uk_no (customer_no), -- 唯一索引：索引列值必须唯一，允许有NULL值，且NULL可能会出现多次。 KEY idx_name (customer_name), -- 普通索引：既不是主键，列值也不需要唯一，单纯的为了提高查询速度而创建。 KEY idx_no_name (customer_no,customer_name) -- 复合索引：即一个索引包含多个列。 ); 单独建创索引： 1 2 3 4 5 6 7 8 9 10 CREATE TABLE customer1 ( id INT UNSIGNED, customer_no VARCHAR(200), customer_name VARCHAR(200) ); ALTER TABLE customer1 ADD PRIMARY KEY customer1(id); -- 主键索引 CREATE UNIQUE INDEX uk_no ON customer1(customer_no); -- 唯一索引 CREATE INDEX idx_name ON customer1(customer_name); -- 普通索引 CREATE INDEX idx_no_name ON customer1(customer_no,customer_name); -- 复合索引 1.8.1. 唯一索引是否影响性能？ 是\n1.8.2. 什么时候使用唯一索引？ 业务需求唯一字段的时候，一般不考虑性能问题\n. 【强制】业务上具有唯一特性的字段，即使是多个字段的组合，也必须建成唯一索引。 说明：不要以为唯一索引影响了 insert 速度，这个速度损耗可以忽略，但提高查找速度是明 显的；另外，即使在应用层做了非常完善的校验控制，只要没有唯一索引，根据墨菲定律，必 然有脏数据产生。\n1.9. 什么时候适合创建索引，什么时候不适合创建索引？ 适合创建索引\n频繁作为where条件语句查询字段\n关联字段需要建立索引\n排序字段可以建立索引\n分组字段可以建立索引(因为分组前提是排序)\n统计字段可以建立索引（如.count(),max()）\n不适合创建索引\n频繁更新的字段不适合建立索引\nwhere，分组，排序中用不到的字段不必要建立索引\n可以确定表数据非常少不需要建立索引\n参与mysql函数计算的列不适合建索引\n创建索引时避免有如下极端误解：\n1）宁滥勿缺。认为一个查询就需要建一个索引。\n2）宁缺勿滥。认为索引会消耗空间、严重拖慢更新和新增速度。\n3）抵制惟一索引。认为业务的惟一性一律需要在应用层通过“先查后插”方式解决。\n1.10. 什么是索引下推？ 5.6之前的版本是没有索引下推这个优化的\n**Using index condition：**叫作 Index Condition Pushdown Optimization （索引下推优化）\n如果没有索引下推（ICP），那么MySQL在存储引擎层找到满足content1 \u0026gt; 'z'条件的第一条二级索引记录。主键值进行回表，返回完整的记录给server层，server层再判断其他的搜索条件是否成立。如果成立则保留该记录，否则跳过该记录，然后向存储引擎层要下一条记录。 如果使用了索引下推（ICP），那么MySQL在存储引擎层找到满足content1 \u0026gt; 'z'条件的第一条二级索引记录。不着急执行回表，而是在这条记录上先判断一下所有关于idx_content1索引中包含的条件是否成立，也就是content1 \u0026gt; 'z' AND content1 LIKE '%a'是否成立。如果这些条件不成立，则直接跳过该二级索引记录，去找下一条二级索引记录；如果这些条件成立，则执行回表操作，返回完整的记录给server层。 总结：\n未开启索引下推：\n根据筛选条件在索引树中筛选第一个条件 获得结果集后回表操作 进行其他条件筛选 再次回表查询 开启索引下推：在条件查询时，当前索引树如果满足全部筛选条件，可以在当前树中完成全部筛选过滤，得到比较小的结果集再进行回表操作\n1.11. 有哪些情况会导致索引失效？ 计算、函数导致索引失效 1 2 3 -- 显示查询分析 EXPLAIN SELECT * FROM emp WHERE emp.name LIKE \u0026#39;abc%\u0026#39;; EXPLAIN SELECT * FROM emp WHERE LEFT(emp.name,3) = \u0026#39;abc\u0026#39;; --索引失效 LIKE以%，_ 开头索引失效 拓展：Alibaba《Java开发手册》\n【强制】页面搜索严禁左模糊或者全模糊，如果需要请走搜索引擎来解决。\n1 EXPLAIN SELECT * FROM emp WHERE name LIKE \u0026#39;%ab%\u0026#39;; --索引失效 不等于(!= 或者\u0026lt;\u0026gt;)索引失效 1 2 EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.name = \u0026#39;abc\u0026#39; ; EXPLAIN SELECT SQL_NO_CACHE * FROM emp WHERE emp.name \u0026lt;\u0026gt; \u0026#39;abc\u0026#39; ; --索引失效 IS NOT NULL 失效 和 IS NULL 1 2 EXPLAIN SELECT * FROM emp WHERE emp.name IS NULL; EXPLAIN SELECT * FROM emp WHERE emp.name IS NOT NULL; --索引失效 **注意：**当数据库中的数据的索引列的NULL值达到比较高的比例的时候，即使在IS NOT NULL 的情况下 MySQL的查询优化器会选择使用索引，此时type的值是range（范围查询）\n1 2 3 4 5 6 -- 将 id\u0026gt;20000 的数据的 name 值改为 NULL UPDATE emp SET `name` = NULL WHERE `id` \u0026gt; 20000; -- 执行查询分析，可以发现 IS NOT NULL 使用了索引 -- 具体多少条记录的值为NULL可以使索引在IS NOT NULL的情况下生效，由查询优化器的算法决定 EXPLAIN SELECT * FROM emp WHERE emp.name IS NOT NULL 类型转换导致索引失效 1 2 EXPLAIN SELECT * FROM emp WHERE name=\u0026#39;123\u0026#39;; EXPLAIN SELECT * FROM emp WHERE name= 123; --索引失效 复合索引未用左列字段失效 如果mysql觉得全表扫描更快时（数据少）; 1.11.1. 为什么LIKE以%开头索引会失效？ id,name,age\nname 创建索引\nselect * from user where name like \u0026lsquo;%明\u0026rsquo;\ntype=all\nselect name,id from user where name like \u0026lsquo;%明\u0026rsquo;\ntype=index\n张明\n(name,age)\n其实并不会完全失效，覆盖索引下会出现type=index，表示遍历了索引树，再回表查询，\n覆盖索引没有生效的时会直接type=all\n没有高效使用索引是因为字符串索引会逐个转换成accii码，生成b+树时按首个字符串顺序排序，类似复合索引未用左列字段失效一样，跳过开始部分也就无法使用生成的b+树了\n1.12. 一个表有多个索引的时候，能否手动选择使用哪个索引？ 不可用手动直接干预，只能通过mysql优化器自动选择\n1.12.1. 如何查看一个表的索引？ 1 2 show index from t_emp; // 显示表上的索引 explain select * from t_emp where id=1; // 显示可能会用到的索引及最终使用的索引 1.12.2. 能否查看到索引选择的逻辑？是否使用过optimizer_trace？ 1 2 3 set session optimizer_trace=\u0026#34;enabled=on\u0026#34;,end_markers_in_json=on; SELECT * FROM information_schema.OPTIMIZER_TRACE; set session optimizer_trace=\u0026#34;enabled=off\u0026#34;; 1.12.3. 多个索引优先级是如何匹配的？ 主键（唯一索引）匹配 全值匹配（单值匹配） 最左前缀匹配 范围匹配 索引扫描 全表扫描 一般性建议\nØ 对于单键索引，尽量选择过滤性更好的索引（例如：手机号，邮件，身份证）\nØ 在选择组合索引的时候，过滤性最好的字段在索引字段顺序中，位置越靠前越好。\nØ 选择组合索引时，尽量包含where中更多字段的索引\nØ 组合索引出现范围查询时，尽量把这个字段放在索引次序的最后面\nØ 尽量避免造成索引失效的情况\n1.13. 使用Order By时能否通过索引排序？ 没有过滤条件不走索引\n1.13.1. 通过索引排序内部流程是什么？ select name,id from user where name like \u0026lsquo;%明\u0026rsquo; order by name；\nselect name,id，age from user where name like \u0026lsquo;%明\u0026rsquo;\n关键配置：\nsort_buffer可供排序的内存缓冲区大小 max_length_for_sort_data 单行所有字段总和限制，超过这个大小启动双路排序 通过索引检过滤筛选条件索到需要排序的字段+其他字段（如果是符合索引） 判断索引内容是否覆盖select的字段 如果覆盖索引，select的字段和排序都在索引上，那么在内存中进行排序，排序后输出结果 如果索引没有覆盖查询字段，接下来计算select的字段是否超过max_length_for_sort_data限制，如果超过，启动双路排序，否则使用单路 1.13.2. 什么是双路排序和单路排序 单路排序：一次取出所有字段进行排序，内存不够用的时候会使用磁盘\n双路排序：取出排序字段进行排序，排序完成后再次回表查询所需要的其他字段\n如果不在索引列上，filesort有两种算法： mysql就要启动双路排序和单路排序\n双路排序（慢）\nSelect id,age,name from stu order by name;\nMySQL 4.1之前是使用双路排序，字面意思就是两次扫描磁盘，最终得到数据， 读取行指针和order by列，对他们进行排序，然后扫描已经排序好的列表，按照列表中的值重新从列表中读取对应的数据输出 从磁盘取排序字段，在buffer进行排序，再从磁盘取其他字段。 取一批数据，要对磁盘进行两次扫描，众所周知，I\\O是很耗时的，所以在mysql4.1之后，出现了第二种改进的算法，就是单路排序。 单路排序（快）\n从磁盘读取查询需要的所有列，按照order by列在buffer对它们进行排序，然后扫描排序后的列表进行输出， 它的效率更快一些，避免了第二次读取数据。并且把随机IO变成了顺序IO，但是它会使用更多的空间， 因为它把每一行都保存在内存中了。\n结论及引申出的问题\n但是用单路有问题\n在sort_buffer中，单路比多路要多占用很多空间，因为单路是把所有字段都取出, 所以有可能取出的数据的总大小超出了sort_buffer的容量，导致每次只能取sort_buffer容量大小的数据，进行排序（创建tmp文件，多路合并），排完再取sort_buffer容量大小，再排……从而多次I/O。\n单路本来想省一次I/O操作，反而导致了大量的I/O操作，反而得不偿失。\n优化策略\n增大sort_buffer_size参数的设置 增大max_length_for_sort_data参数的设置 减少select 后面的查询的字段。 禁止使用select * 提高Order By的速度\nOrder by时select * 是一个大忌。只Query需要的字段， 这点非常重要。在这里的影响是： 当Query的字段大小总和小于max_length_for_sort_data 而且排序字段不是 TEXT|BLOB 类型时，会用改进后的算法——单路排序， 否则用老算法——多路排序。 两种算法的数据都有可能超出sort_buffer的容量，超出之后，会创建tmp文件进行合并排序，导致多次I/O，但是用单路排序算法的风险会更大一些，所以要提高sort_buffer_size。 尝试提高 sort_buffer_size 不管用哪种算法，提高这个参数都会提高效率，当然，要根据系统的能力去提高，因为这个参数是针对每个进程（connection）的 1M-8M之间调整。 MySQL5.7和8.0，InnoDB存储引擎默认值是1048576字节，1MB。 1 SHOW VARIABLES LIKE \u0026#39;%sort_buffer_size%\u0026#39;; 尝试提高 max_length_for_sort_data 提高这个参数， 会增加用改进算法的概率。 1 SHOW VARIABLES LIKE \u0026#39;%max_length_for_sort_data%\u0026#39;; 5.7默认1024字节 8.0默认4096字节 但是如果设的太高，数据总容量超出sort_buffer_size的概率就增大，明显症状是高的磁盘I/O活动和低的处理器使用率。如果需要返回的列的总长度大于max_length_for_sort_data，使用双路算法，否则使用单路算法。1024-8192字节之间调整\n1.13.3. group by 分组和order by在索引使用上有什么区别？ group by 使用索引的原则几乎跟order by一致 ，唯一区别：\ngroup by 先排序再分组，遵照索引建的最佳左前缀法则 group by没有过滤条件，也可以用上索引。Order By 必须有过滤条件才能使用上索引。 1.14. 如果表中有字段为null，又被经常查询该不该给这个字段创建索引？ 应该创建索引，使用的时候尽量使用is null判断。\nIS NOT NULL 失效 和 IS NULL 1 2 EXPLAIN SELECT * FROM emp WHERE emp.name IS NULL; EXPLAIN SELECT * FROM emp WHERE emp.name IS NOT NULL; --索引失效 **注意：**当数据库中的数据的索引列的NULL值达到比较高的比例的时候，即使在IS NOT NULL 的情况下 MySQL的查询优化器会选择使用索引，此时type的值是range（范围查询）\n1 2 3 4 5 6 -- 将 id\u0026gt;20000 的数据的 name 值改为 NULL UPDATE emp SET `name` = NULL WHERE `id` \u0026gt; 20000; -- 执行查询分析，可以发现 IS NOT NULL 使用了索引 -- 具体多少条记录的值为NULL可以使索引在IS NOT NULL的情况下生效，由查询优化器的算法决定 EXPLAIN SELECT * FROM emp WHERE emp.name IS NOT NULL 1.14.1. 有字段为null索引是否会失效？ 不一定会失效，每一条sql具体有没有使用索引 可以通过trace追踪一下\n最好还是给上默认值\n数字类型的给0，字符串给个空串“”，\n参考上一题\n2. 二 MySQL 内部技术架构 2.1. Mysql内部支持缓存查询吗？ 当MySQL接收到客户端的查询SQL之后，仅仅只需要对其进行相应的权限验证之后，就会通过Query Cache来查找结果，甚至都不需要经过Optimizer模块进行执行计划的分析优化，更不需要发生任何存储引擎的交互\nmysql5.7支持内部缓存，8.0之后就废弃掉了\n2.1.1. mysql8为何废弃掉查询缓存？ 缓存的意义在于快速查询提升系统性能，可以灵活控制缓存的一致性\nmysql缓存的限制\nmysql基本没有手段灵活的管理缓存失效和生效，尤其对于频繁更新的表 SQL必须完全一致才会导致cache命中 为了节省内存空间，太大的result set不会被cache (\u0026lt; query_cache_limit)； MySQL缓存在分库分表环境下是不起作用的； 执行SQL里有触发器,自定义函数时，MySQL缓存也是不起作用的； 在表的结构或数据发生改变时，基于该表相关cache立即全部失效。 2.1.2. 替代方案是什么？ 应用层组织缓存，最简单的是使用redis，ehcached等\n2.2. Mysql内部有哪些核心模块组成，作用是什么？ Connectors（客户端）\nMySQL服务器之外的客户端程序，与具体的语言相关，例如Java中的JDBC，图形用户界面SQLyog等。本质上都是在TCP连接上通过MySQL协议和MySQL服务器进行通信。\nMySQL Server（服务器）\n第1层：连接层\n系统（客户端）访问 MySQL 服务器前，做的第一件事就是建立 TCP 连接。 经过三次握手建立连接成功后， MySQL 服务器对 TCP 传输过来的账号密码做身份认证、权限获取。 用户名或密码不对，会收到一个Access denied for user错误，客户端程序结束执行 用户名密码认证通过，会从权限表查出账号拥有的权限与连接关联，之后的权限判断逻辑，都将依赖于此时读到的权限 TCP 连接收到请求后，必须要分配给一个线程专门与这个客户端的交互。所以还会有个线程池，去走后面的流程。每一个连接从线程池中获取线程，省去了创建和销毁线程的开销。 第2层：服务层\nManagement Serveices \u0026amp; Utilities： 系统管理和控制工具\nSQL Interface：SQL接口：\n接收用户的SQL命令，并且返回用户需要查询的结果。比如SELECT \u0026hellip; FROM就是调用SQL Interface MySQL支持DML（数据操作语言）、DDL（数据定义语言）、存储过程、视图、触发器、自定义函数等多种SQL语言接口 Parser：解析器：\n在SQL命令传递到解析器的时候会被解析器验证和解析。解析器中SQL 语句进行语法分析、语法解析，并为其创建语法树。 语法分析\n语法分析主要是把输入转化成若干个tokens，包含key和非key。\n在分析之后，会得到4个Token，其中有2个key，它们分别是SELECT、FROM。\nkey 非key key 非key SELECT age FROM user 典型的解析树如下： Optimizer：查询优化器：\nSQL语句在语法解析后、查询前会使用查询优化器对查询进行优化，确定SQL语句的执行路径，生成一个执行计划。 Caches \u0026amp; Buffers： 查询缓存组件：\nMySQL内部维持着一些Cache和Buffer，比如Query Cache用来缓存一条SELECT语句的执行结果，如果能够在其中找到对应的查询结果，那么就不必再进行查询解析、查询优化和执行的整个过程了，直接将结果反馈给客户端。 这个缓存机制是由一系列小缓存组成的。比如表缓存，记录缓存，key缓存，权限缓存等 。 这个查询缓存可以在不同客户端之间共享 。 第3层：引擎层\n插件式存储引擎层（ Storage Engines），负责MySQL中数据的存储和提取，对物理服务器级别维护的底层数据执行操作，服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同，管理的表有不同的存储结构，采用的存取算法也不同，这样我们可以根据自己的实际需要进行选取。例如MyISAM引擎和InnoDB引擎。\n存储层\n所有的数据、数据库、表的定义、表的每一行的内容、索引，都是存在文件系统 上，以文件的方式存在，并完成与存储引擎的交互。\n2.3. 一条sql发送给mysql后，内部是如何执行的？（说一下 MySQL 执行一条查询语句的内部执行过程？） 1.5、查询流程说明\n首先，MySQL客户端通过协议与MySQL服务器建连接，通过SQL接口发送SQL语句，先检查查询缓存，如果命中，直接返回结果，否则进行语句解析。也就是说，在解析查询之前，服务器会先访问查询缓存，如果某个查询结果已经位于缓存中，服务器就不会再对查询进行解析、优化、以及执行。它仅仅将缓存中的结果返回给用户即可，这将大大提高系统的性能。\n接下来，MySQL解析器通过关键字将SQL语句进行解析，并生成一棵对应的解析树，解析器使用MySQL语法规则验证和解析SQL语句。例如，它将验证是否使用了错误的关键字，或者使用关键字的顺序是否正确，引号能否前后匹配等；预处理器则根据MySQL规则进一步检查解析树是否合法，例如，这里将检查数据表和数据列是否存在，还会解析名字和别名，看是否有歧义等。然后预处理器会进行查询重写，生成一棵新解析树。\n接下来，查询优化器将解析树转化成执行计划。MySQL优化程序会对我们的语句做一些优化，如子查询转换为连接、表达式简化等等。优化的结果就是生成一个执行计划，这个执行计划表明了应该使用哪些索引执行查询，以及表之间的连接顺序是啥样，等等。我们可以使用EXPLAIN语句来查看某个语句的执行计划。\n最后，进入执行器阶段。完成查询优化后，查询执行引擎会按照生成的执行计划调用存储引擎提供的接口执行SQL查询并将结果返回给客户端。在MySQL8以下的版本，如果设置了查询缓存，这时会将查询结果进行缓存，再返回给客户端。\n2.3.1. MySQL 提示“不存在此列”是执行到哪个节点报出的？ 是在Parser：解析器 分析sql语法的时候检查的列。\n2.4. 如果一张表创建了多个索引，在哪个阶段或模块进行的索引选择？ 在优化器阶段Optimizer：查询优化器：\n2.5. MySQL 支持哪些存储引擎？默认使用哪个？ 查看MySQL提供什么存储引擎\n1 SHOW ENGINES; 下面的结果表示MySQL中默认使用的存储引擎是InnoDB，支持事务，行锁，外键，支持分布式事务(XA)，支持保存点(回滚)\n也可以通过以下语句查看默认的存储引擎：\n1 SHOW VARIABLES LIKE \u0026#39;%default_storage_engine%\u0026#39;; 2.6. Mysql8.0自带哪些存储引擎？分别是做什么的？ 1. InnoDB存储引擎\nInnoDB是MySQL的默认事务型引擎，它被设计用来处理大量的短期(short-lived)事务。可以确保事务的完整提交(Commit)和回滚(Rollback)。\n除非有非常特别的原因需要使用其他的存储引擎，否则应该优先考虑InnoDB引擎。\n数据文件结构：\n表名.frm 存储表结构（MySQL8.0时，合并在表名.ibd中）\n表名.ibd 存储数据和索引\nInnoDB不仅缓存索引还要缓存真实数据， 对内存要求较 高 ，而且内存大小对性能有决定性的影响。\n2. MyISAM存储引擎\nMyISAM提供了大量的特性，包括全文索引、压缩、空间函数(GIS)等，但MyISAM不支持事务和行级锁，有一个毫无疑问的缺陷就是崩溃后无法安全恢复。\n优势是访问的 速度快 ，对事务完整性没有要求或者以SELECT、INSERT为主的应用。\n数据文件结构：\n表名.frm 存储表结构\n表名.MYD 存储数据\n表名.MYI 存储索引\nMyISAM只缓存索引，不缓存真实数据。\n3. Archive引擎\nArchive档案存储引擎只支持INSERT和SELECT操作。 Archive表适合日志和数据采集（档案）类应用。 根据英文的测试结论来看，Archive表比MyISAM表要小大约75%，比支持事务处理的InnoDB表小大约83%。 4. Blackhole引擎\nBlackhole引擎没有实现任何存储机制，它会丢弃所有插入的数据，不做任何保存。 但服务器会记录Blackhole表的日志，所以可以用于复制数据到备库，或者简单地记录到日志。但这种应用方式会碰到很多问题，因此并不推荐。 5. CSV引擎\nCSV引擎可以将普通的CSV文件作为MySQL的表来处理，但不支持索引。 CSV引擎可以作为一种数据交换的机制，非常有用。 CSV存储的数据直接可以在操作系统里，用文本编辑器，或者excel读取。 6. Memory引擎\n如果需要快速地访问数据，并且这些数据不会被修改，重启以后丢失也没有关系，那么使用Memory表是非常有用。 Memory表至少比MyISAM表要快一个数量级。 7. Federated引擎\nFederated引擎是访问其他MySQL服务器的一个代理（跨库关联查询），尽管该引擎看起来提供了一种很好的跨服务器的灵活性，但也经常带来问题，因此默认是禁用的。 2.7. MySQL 存储引擎架构了解吗？ https://dev.mysql.com/doc/refman/5.7/en/innodb-architecture.html\n下面是官方的InnoDB引擎结构图，主要分为内存结构和磁盘结构两大部分。\n内存区域\nBuffer Pool:在InnoDB访问表记录和索引时会在Buffer Pool的页中缓存，以后使用可以减少磁盘IO操作，提升效率。主要用来缓存热的数据页和索引页。\nLog Buffer：用来缓存redolog\nAdaptive Hash Index：自适应哈希索引\nChange Buffer:它是一种应用在非唯一普通索引页（non-unique secondary index page）不在缓冲池中，对页进行了写操作，并不会立刻将磁盘页加载到缓冲池，而仅仅记录缓冲变更（Buffer Changes），等未来数据被读取时，再将数据合并（Merge）恢复到缓冲池中的技术。写缓冲的目的是降低写操作的磁盘IO，提升数据库性能。\n磁盘区域\n磁盘中的结构分为两大类：表空间和重做日志。\n表空间：分为系统表空间(MySQL 目录的 ibdata1 文件)，临时表空间，常规表空间，Undo 表空间以及 file-per-table 表空间(MySQL5.7默认打开file_per_table 配置）。系统表空间又包括了InnoDB数据字典，双写缓冲区(Doublewrite Buffer)，修改缓存(Change Buffer），Undo日志等。 Redo日志：存储的就是 Log Buffer 刷到磁盘的数据。 官方文档：\nhttps://dev.mysql.com/doc/refman/8.0/en/innodb-storage-engine.html\n2.7.1. 能否单独为一张表设置存储引擎？ 方法1：\n设置默认存储引擎：\n1 SET DEFAULT_STORAGE_ENGINE=MyISAM; 方法2：\n或者修改 my.cnf 文件：vim /etc/my.cnf 新增一行：default-storage-engine=MyISAM 重启MySQL：systemctl restart mysqld\n方法3：\n我们可以为 不同的表设置不同的存储引擎\n1 2 CREATE TABLE 表名( 建表语句; ) ENGINE = 存储引擎名称; ALTER TABLE 表名 ENGINE = 存储引擎名称; 2.7.2. 阿里、京东等大厂都有自研的存储引擎，如何开发一套自己的？ 开发存储引擎并不难，难的是开发出来高效的有意义的存储引擎。\n简单例子可以看一下官方源码中的示例，可以实现一个什么也没做的存储引擎。\n有兴趣可以参考官方文档：https://dev.mysql.com/doc/dev/mysql-server/latest/\n2.8. MyISAM 和 InnoDB 的区别是什么？ 外键 事务 锁\n对比项 MyISAM InnoDB 外键 不支持 支持 事务 不支持 支持 行表锁 表锁，即使操作一条记录也会锁住整个表，不适合高并发的操作 行锁，操作时只锁某一行，不对其它行有影响，适合高并发的操作 缓存 只缓存索引，不缓存真实数据 不仅缓存索引还要缓存真实数据，对内存要求较高，而且内存大小对性能有决定性的影响 关注点 并发查询，节省资源、消耗少、简单业务 并发写、事务、多表关系、更大资源 默认安装 Y Y 默认使用 N Y 自带系统表使用 Y N 2.8.1. 具体说一下如何做技术选型 除非几乎没有写操作全部都是高频的读操作可以选择MyISAM作为表的存储引擎，其他业务可以一律使用InnoDB。\n3. 三 mysql 事务 3.1. 什么是数据库事务？事务的特性是什么？ 事务：\n是数据库操作的最小工作单元，是作为单个逻辑工作单元执行的一系列操作；\n这些操作作为一个整体一起向系统提交，要么都执行、要么都不执行；\n事务是一组不可再分割的操作集合（工作逻辑单元）\n事务都有 ACID 特性\n3.1.1. 什么是ACID？ 1 、原子性 atomicity\n过程的保证\n只做一个步骤\n1 给钱\n2 去买\n3 交回来\n事务是数据库的逻辑工作单位，事务中包含的各操作要么都做，要么都不做\n2 、一致性 consistency\n结果的保证\n保证要吃完 刚张嘴挂了，失去一致性\n事 务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时，就说数据库处于一致性状态。如果数据库系统 运行中发生故障，有些事务尚未完成就被迫中断，这些未完成事务对数据库所做的修改有一部分已写入物理数据库，这时数据库就处于一种不正确的状态，或者说是 不一致的状态。 3 、隔离性 isolation\n并发事务互相干扰\n不被干扰 刚张嘴别人塞了东西\n一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的，并发执行的各个事务之间不能互相干扰。 4 、持续性 永久性 durability\n保存 吃到肚子里\n也称永久性，指一个事务一旦提交，它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。\n3.2. 并发事务会有哪些问题？ 多个事务并发执行一定会产生相互争夺资源的问题\n3.2.1. 什么是脏读 丢失修改 不可重复读 幻读 脏读（Dirty read）\n是一个事务在处理过程中读取了另外一个事务未提交的数据\n当一个事务正在访问数据并且对其进行了修改，但是还没提交事务，这时另外一个事务也访问了这个数据，然后使用了这个数据，因为这个数据的修改还没提交到数据库，所以另外一个事务读取的数据就是“脏数据”，这种行为就是“脏读”，依据“脏数据”所做的操作可能是会出现问题的。\n修改丢失（Lost of modify）\n*是指一个事务读取一个数据时，另外一个数据也访问了该数据，那么在第一个事务修改了这个数据之后，第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失，这种情况就被称为**修改丢失\n不可重复读（Unrepeatableread）\n指在一个事务内多次读取同一数据**，在这个事务还没结束时，另外一个事务也访问了这个数据并对这个数据进行了修改，那么就可能造成第一个事务两次读取的数据不一致，这种情况就被称为**不可重复读。\n幻读（Phantom read）\n是指同一个事务内多次查询返回的结果集总数不一样（比如增加了或者减少了行记录）。\n幻读与不可重复读类似，幻读是指一个事务读取了几行数据，这个事务还没结束，接着另外一个事务插入了一些数据，在随后的查询中，第一个事务读取到的数据就会比原本读取到的多，就好像发生了幻觉一样，所以称为幻读。\n3.2.2. 不可重复读和幻读有什么区别？ 不可重复读 针对的是一份数据的修改\n幻读 针对的是行数修改\n3.3. Mysql是如何避免事务并发问题的？ 避免事务并发问题是需要付出性能代价的，此时和分布式系统设计一样（CAP定理及base理论），为了保证一致性就一定会牺牲性能，要做取舍\n在mysql内部通过加锁的方式实现好了解决方案可供选择，就是配置事务隔离级别\n3.3.1. 什么是事务隔离级别？ 1 2 3 4 5 事务隔离级别 脏读 不可重复读(被修改) 幻读（删减） 读未提交（read-uncommitted） 是 是 是 不可重复读（read-committed） 否 是 是 可重复读（repeatable-read） 否 否 是 串行化（serializable） 否 否 否 3.3.2. 默认的级别是什么？ MySQL InnoDB存储引擎默认的事务隔离级别是可重复读（REPEATABLE-READ）\n1 2 MySQL 5.7 SELECT @@tx_isolation; MySQL 8.0 SELECT @@transaction_isolation; 3.3.3. 如何选择事务隔离级别？ 隔离级别越低，事务请求的锁越少相应性能也就越高，如没有特殊要求或有错误发生，使用默认的隔离级别即可，如果系统中有高频读写并且对一致性要求高那么就需要比较高的事务隔离级别甚至串行化。\n3.3.4. 靠缓存可以提升高事务隔离级别的性能吗？ 提升事务级别的目的本质是提供更高的数据一致性，如果前置有缓存，那么缓存只能提供高效读并不能保证数据及时一致性，相反的我们还需要对缓存管理有额外的开销。\n3.4. Mysql事务隔离是如何实现的？ 隔离的实现主要是读写锁和MVCC\n3.4.1. 什么是一致性非锁定读和锁定读？ 锁定读\n使用到了读写锁\n读写锁是最简单直接的的事务隔离实现方式\n每次读操作需要获取一个共享(读)锁，每次写操作需要获取一个写锁。 共享锁之间不会产生互斥，共享锁和写锁之间、以及写锁与写锁之间会产生互斥。 当产生锁竞争时，需要等待其中一个操作释放锁后，另一个操作才能获取到锁。 锁机制，解决的就是多个事务同时更新数据，此时必须要有一个加锁的机制\n行锁（记录锁）：解决的就是多个事务同时更新一行数据 间隙锁：解决的就是多个事务同时更新多行数据 下列操作属于锁定读\n1 2 3 select ... lock in share mode select ... for update insert、update、delete 非锁定读\nv10 -\u0026gt; age=18\nv11 -\u0026gt;age=19\nv12 -\u0026gt;age=15\n使用mvcc 多版本控制实现\n3.4.2. 说一下MVCC内部细节 https://dev.mysql.com/doc/refman/5.7/en/innodb-multi-versioning.html\nMulti-Version Concurrency Control 多版本并发控制，MVCC 是一种并发控制的方法，一般在数据库管理系统中，实现对数据库的并发访问\nInnoDB是一个多版本的存储引擎。它保存有关已更改行的旧版本的信息，以支持并发和回滚等事务特性。这些信息存储在一个称为回滚段的数据结构中的系统表空间或undo表空间中。参见第14.6.3.4节“撤消表空间”。InnoDB使用回滚段中的信息来执行事务回滚所需的撤消操作。它还使用这些信息构建行的早期版本，以实现一致的读取\nMVCC 的实现依赖于：隐藏字段、Read View、undo log\n隐藏字段\nA 6-byte DB_TRX_ID 用来标识最近一次对本行记录做修改 (insert 、update) 的事务的标识符 ，即最后一次修改本行记录的事务 id。 如果是 delete 操作， 在 InnoDB 存储引擎内部也属于一次 update 操作，即更新行中的一个特殊位 ，将行标识为己删除，并非真正删除。 A 7-byte DB_ROLL_PTR 回滚指针，指向该行的 undo log 。如果该行未被更新，则为空. A 6-byte DB_ROW_ID 如果没有设置主键且该表没有唯一非空索引时，InnoDB 会使用该 id 来生成聚簇索引. Read View\n不同的事务隔离级别中，当有事物在执行过程中修改了数据（更新版本号），在并发事务时需要判断一下版本链中的哪个版本是当前事务可见的。为此InnoDB有了ReadView的概念，使用ReadView来记录和隔离不同事务并发时此记录的哪些版本是对当前访问事物可见的。\nundo log\n除了用来回滚数据，还可以读取可见版本的数据。以此实现非锁定读\n3.4.3. Mysql事务一致性，原子性是如何实现的？ 首先是通过锁和mvcc实现了执行过程中的一致性和原子性\n其次是在灾备方面通过Redo log实现，Redo log会把事务在执行过程中对数据库所做的所有修改都记录下来，在之后系统崩溃重启后可以把事务所做的任何修改都恢复出来。\n3.4.4. Mysql事务的持久性是如何实现的？ 使用Redo log保证了事务的持久性。当事务提交时，必须先将事务的所有日志写入日志文件进行持久化，就是我们常说的WAL(write ahead log)机制，如果出现断电重启便可以从redolog中恢复，如果redolog写入失败那么也就意味着修改失败整个事务也就直接回滚了。\n3.4.5. 表级锁和行级锁有什么区别？ 表级锁：串行化（serializable）时，整表加锁，事务访问表数据时需要申请锁，虽然可分为读锁和写锁，但毕竟是锁住整张表，会导致并发能力下降，一般是做ddl处理时使用\n行级锁：除了串行化（serializable）时 InnoDB使用的都是行级锁，只锁一行数据，其他行数据不影响，并发能力强。\n3.4.6. 什么是行级锁？Mysql如何完成的？ 行级锁实现比较复杂不是单纯锁住一行数据，是由mvcc完成的。\n3.4.7. 什么是共享锁（读锁）？ 共享锁或S锁，其它事务可以继续加共享锁，但不能加排它锁\n3.4.8. 什么是排它锁（写锁/独占锁）？ 排它锁或X锁，在进行写操作之前要申请并获得，其它事务不能再获得任何锁。\n3.4.9. 什么是意向锁？ 它分为意向共享锁（IS）和意向排他锁（IX）\n一个事务对一张表的某行添加共享锁前，必须获得对该表一个IS锁或者优先级更高的锁。 一个事务对一张表的某行添加排他锁之前，它必须对该表获取一个IX锁。\n意向锁属于表锁，它不与innodb中的行锁冲突，任意两个意向锁之间也不会产生冲突，但是会与表锁（S锁和X锁）产生冲突\n3.4.10. InnoDB支持哪几种锁？ 表锁，行锁，间隙锁，Next-Key锁等\n在Serializable中读加共享锁，写加排他锁，读写互斥\n两段锁协议，将事务分成两个阶段，加锁阶段和解锁阶段（所以叫两段锁）\n3.4.11. 当前读和快照读分别是什么？ 当前读 ：在锁定读（使用锁隔离事物）的时候读到的是最新版本的数据\n快照读：可重复读（repeatable-read）下 mvcc生效读取的是数据的快照，并不是最新版本的数据（未提交事物的数据）\n3.5. 什么是XA协议？ https://dev.mysql.com/doc/refman/8.0/en/xa.html\n涉及的角色：\nAP（Application Program）：应用程序，定义事务边界（定义事务开始和结束）并访问事务边界内的资源。 RM（Resource Manger）资源管理器: 管理共享资源并提供外部访问接口。供外部程序来访问数据库等共享资源。此外，RM还具有事务的回滚能力。 TM（Transaction Manager）事务管理器：TM是分布式事务的协调者，TM与每个RM进行通信，负责管理全局事务，分配事务唯一标识，监控事务的执行进度，并负责事务的提交、回滚、失败恢复等。 具体处理流程：\n应用程序AP向事务管理器TM发起事务请求 TM调用xa_open()建立同资源管理器的会话 TM调用xa_start()标记一个事务分支的开头 AP访问资源管理器RM并定义操作，比如插入记录操作 TM调用xa_end()标记事务分支的结束 TM调用xa_prepare()通知RM做好事务分支的提交准备工作。其实就是二阶段提交的提交请求阶段。 TM调用xa_commit()通知RM提交事务分支，也就是二阶段提交的提交执行阶段。 TM调用xa_close管理与RM的会话。 这些接口一定要按顺序执行，比如xa_start接口一定要在xa_end之前。此外，这里千万要注意的是事务管理器只是标记事务分支并不执行事务，事务操作最终是由应用程序通知资源管理器完成的。另外，我们来总结下XA的接口 xa_start:负责开启或者恢复一个事务分支，并且管理XID到调用线程 xa_end:负责取消当前线程与事务分支的关系 xa_prepare:负责询问RM 是否准备好了提交事务分支 xa_commit:通知RM提交事务分支 xa_rollback:通知RM回滚事务分支 3.5.1. 什么是mysql xa事务？ mysql的xa事务分为两部分：\nInnoDB内部本地普通事务操作协调数据写入与log写入两阶段提交 外部分布式事务 1 2 5.7 SHOW VARIABLES LIKE \u0026#39;%innodb_support_xa%\u0026#39;; 8.0 默认开启无法关闭 XA 事务语法示例如下：\n1 2 3 4 5 6 7 XA START \u0026#39;自定义事务id\u0026#39;; SQL语句... XA END \u0026#39;自定义事务id\u0026#39;; XA PREPARE \u0026#39;自定义事务id\u0026#39;; XA COMMIT\\ROLLBACK \u0026#39;自定义事务id\u0026#39;; XA PREPARE 执行成功后，事务信息将被持久化。即使会话终止甚至应用服务宕机，只要我们将【自定义事务id】记录下来，后续仍然可以使用它对事务进行 rollback 或者 commit。\n3.5.2. xa事务与普通事务区别是什么？ xa事务可以跨库或跨服务器，属于分布式事务，同时xa事务还支撑了InnoDB内部日志两阶段记录\n普通事务只能在单库中执行\n3.5.3. 什么是2pc 3pc？ 两阶段提交协议与3阶段提交协议，额外增加了参与的角色保证分布式事务完成更完善\n3.6. 是否使用过select for update？会产生哪些操作？ 1 2 3 查询库存 = 100 0 扣减库存 = -1 99 记录日志 = log 提交 commit select本身是一个查询语句，查询语句是不会产生冲突的一种行为，一般情况下是没有锁的，用select for update 会让select语句产生一个排它锁(X), 这个锁和update的效果一样，会使两个事务无法同时更新一条记录。\nhttps://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html\nhttps://dev.mysql.com/doc/refman/8.0/en/select.html\nfor update仅适用于InnoDB，且必须在事务块(BEGIN/COMMIT)中才能生效。\n在进行事务操作时，通过“for update”语句，MySQL会对查询结果集中每行数据都添加排他锁，其他线程对该记录的更新与删除操作都会阻塞。排他锁包含行锁、表锁。\nInnoDB默认是行级别的锁，在筛选条件中当有明确指定主键或唯一索引列的时候，是行级锁。否则是表级别。\n示例\n1 2 3 4 5 SELECT … FOR UPDATE [OF column_list][WAIT n|NOWAIT][SKIP LOCKED]; select * from t for update 会等待行锁释放之后，返回查询结果。 select * from t for update nowait 不等待行锁释放，提示锁冲突，不返回结果 select * from t for update wait 5 等待5秒，若行锁仍未释放，则提示锁冲突，不返回结果 select * from t for update skip locked 查询返回查询结果，但忽略有行锁的记录 3.7. 说一下mysql死锁的原因和处理方法 1 2 3 4 5 6 7 8 9 10 事务 a 表 t id=100 更新 加行锁 表 t id=200 更新 已加锁 事务 b 表 t id=200 更新 加行锁 表 t id=100 更新 已加锁 死锁与锁等待是两个概念 如未开启事务，多个客户端执行的insert操作 当多个事务同时持有和请求同一资源上的锁而产生循环依赖的时候就产生了死锁。 排查：\n正在运行的任务 show full processlist; 找到卡主的进程 解开死锁 UNLOCK TABLES ； 查看当前运行的事务 SELECT * FROM information_schema.INNODB_TRX; 当前出现的锁 SELECT * FROM information_schema.INNODB_LOCKS; 观察错误日志 查看InnoDB锁状态 show status like \u0026quot;innodb_row_lock%\u0026quot;; lnnodb_row_lock_current_waits:当前正在等待锁定的数量; lnnodb_row_lock_time :从系统启动到现在锁定的总时间长度，单位ms; Innodb_row_lock_time_avg :每次等待所花平均时间; Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间; lnnodb_row_lock_waits :从系统启动到现在总共等待的次数。\nkill id 杀死进程 解决：\n死锁无法避免，上线前要进行严格的压力测试\n快速失败\ninnodb_lock_wait_timeout 行锁超时时间 拆分sql，严禁大事务\n充分利用索引，优化索引，尽量把有风险的事务sql使用上覆盖索，优化where条件前缀匹配，提升查询速度，引减少表锁\n无法避免时：\n操作多张表时，尽量以相同的顺序来访问避免形成等待环路 单张表时先排序再操作 使用排它锁 比如 for update 3.8. Mysql会产生几种日志？ 错误日志（error log） error log主要记录MySQL在启动、关闭或者运行过程中的错误信息，在MySQL的配置文件my.cnf中，可以通过log-error=/var/log/mysqld.log 执行mysql错误日志的位置。\n慢查询日志（slow query log） 0.1秒\nMySQL的慢查询日志是MySQL提供的一种日志记录，它用来记录在MySQL中响应时间超过阀值的语句，具体指运行时间超过long_query_time值的SQL，则会被记录到慢查询日志中。 long_query_time的默认值为10，意思是运行10秒以上的语句。 由他来查看哪些SQL超出了我们的最大忍耐时间值，比如一条sql执行超过5秒钟，我们就算慢SQL，希望能收集超过5秒的sql，结合之前explain进行全面分析。 默认情况下，MySQL数据库没有开启慢查询日志，需要我们手动来设置这个参数。 当然，如果不是调优需要的话，一般不建议启动该参数，因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件。 在生产环境中，如果要手工分析日志，查找、分析SQL，显然是个体力活，MySQL提供了日志分析工具mysqldumpslow。\n一般查询日志（general log） general log 记录了客户端连接信息以及执行的SQL语句信息，通过MySQL的命令\n重写日志（redo log） 回滚日志（undo log） 二进制日志（bin log） 3.8.1. bin log作用是什么？ MySQL的bin log日志是用来记录MySQL中增删改时的记录日志。\n当你的一条sql操作对数据库中的内容进行了更新，就会增加一条bin log日志。查询操作不会记录到bin log中。\nbin log最大的用处就是进行主从复制，以及数据库的恢复。\n3.8.2. redo log作用是什么？ redo log是一种基于磁盘的数据结构，用来在MySQL宕机情况下将不完整的事务执行数据纠正，redo日志记录事务执行后的状态。\n当事务开始后，redo log就开始产生，并且随着事务的执行不断写入redo log file中。redo log file中记录了xxx页做了xx修改的信息，我们都知道数据库的更新操作会在内存中先执行，最后刷入磁盘。\nredo log就是为了恢复更新了内存但是由于宕机等原因没有刷入磁盘中的那部分数据。\n3.8.3. undo log作用是什么？ undo log主要用来回滚到某一个版本，是一种逻辑日志。\nundo log记录的是修改之前的数据，比如：当delete一条记录时，undolog中会记录一条对应的insert记录，从而保证能恢复到数据修改之前。在执行事务回滚的时候，就可以通过undo log中的记录内容并以此进行回滚。\nundo log还可以提供多版本并发控制下的读取（MVCC）。\n3.9. Mysql日志是否实时写入磁盘？ 097 bin log刷盘机制是如何实现的？098 redo log刷盘机制是如何实现的？ 099 undo log刷盘机制是如何实现的？ 磁盘写入固然是比较慢的。\n参数：sync_binlog\nbinlog 写入策略：\n1、sync_binlog=0 的时候，表示每次提交事务binlog不会马上写入到磁盘，而是先写到page cache,相对于磁盘写入来说写page cache要快得多,不过在Mysql 崩溃的时候会有丢失日志的风险。\n2、sync_binlog=1 的时候，表示每次提交事务都会执行 fsync 写入到磁盘 ；\n3、sync_binlog的值大于1 的时候，表示每次提交事务都 先写到page cach，只有等到积累了N个事务之后才fsync 写入到磁盘，同样在此设置下Mysql 崩溃的时候会有丢失N个事务日志的风险。\n很显然三种模式下，sync_binlog=1 是强一致的选择，选择0或者N的情况下在极端情况下就会有丢失日志的风险，具体选择什么模式还是得看系统对于一致性的要求。\ninnodb_flush_log_at_trx_commit\n1 2 3 取值0：每秒（一秒钟内提交的事务）写入磁盘 每秒触发一次缓存日志回写磁盘操作，并调用操作系统fsync刷新IO缓存。 取值1：有事务提交就立即刷盘 每次提交事务都立即调用操作系统fsync刷新IO缓存。 取值2：每次事务提交 都写给操作系统 由系统接管什么时候写入磁盘 每次都把redo log写到系统的page cache中，由系统接管什么时候写入磁盘 时机顺序：\n1 开启事务 2 查询数据库中需要更新的字段，加载到内存中 形成数据脏页 3 记录undo log到内存缓冲区（用于回滚和mvcc）并关联redo log -\u0026gt; 可刷盘 4 记录 redo log到内存缓冲区 （用于失败重放）准备提交事务 -\u0026gt; 可刷盘 5 修改内存中的脏页数据 6 提交事务触发redolog刷盘 7 undo log 和脏页 刷盘 8 事务成功 redo log 与 binlog 的两阶段提交\nredo log 的写入拆成了两个步骤：prepare 和 commit\nprepare：redolog写入log buffer，并fsync持久化到磁盘，在redolog事务中记录2PC的XID，在redolog事务打上prepare标识 commit：binlog写入log buffer，并fsync持久化到磁盘，在binlog事务中记录2PC的XID，同时在redolog事务打上commit标识 3.10. MySQL的binlog有有几种录入格式？分别有什么区别？ logbin格式：\nbinlog_format=STATEMENT（默认）：数据操作的时间，同步时不一致 每一条会修改数据的sql语句会记录到binlog中。优点是并不需要记录每一 条sql语句和每一行的 数据变化，减少了binlog日志量，节约IO，提高性能。缺点是在某些情况下会导致 master-slave 中的数据不一致( 如sleep()函数， last_insert_id()，以及user-defined functions(udf)等会 出\t现 问题) binlog_format=ROW：批量数据操作时，效率低 不记录每条sql语句的上下文信息，仅需记录哪条数据被修改了，修改成什么样 了。而且不会出 现某些特定情况下的存储过程、或function、或trigger的调用和触发无法被正确复制的 问题。缺 点是会产生大量的日志，尤其是alter table的时候会让日志暴涨。 binlog_format=MIXED：是以上两种level的混合使用，有函数用ROW，没函数用STATEMENT，但是无法识别系统变量 3.11. Mysql集群同步时为什么使用binlog？优缺点是什么？ binlog是mysql提供的日志，所有存储引擎都可用。 支持增量同步 binlog还可以供其他中间件读取，比如同步到hdfs中 如果复制表数据： 不支持某个阶段回放 直接复制数据过程中一旦中断复制（比如断网），很难确定复制的offset 4. 四 Mysql开发 4.1. 可以使用MySQL直接存储文件吗？ 可以使用 BLOB (binary large object)，用来存储二进制大对象的字段类型。\nTinyBlob 255 值的长度加上用于记录长度的1个字节(8位) Blob 65K值的长度加上用于记录长度的2个字节(16位) MediumBlob 16M值的长度加上用于记录长度的3个字节(24位) LongBlob 4G 值的长度加上用于记录长度的4个字节(32位)。\n4.1.1. 什么时候存，什么时候不存？ 存：需要高效查询并且文件很小的时候\n不存：文件比较大，数据量多或变更频繁的时候\n4.1.2. 存储的时候有遇到过什么问题吗？ 上传数据过大sql执行失败 调整max_allowed_packet 主从同步数据时比较慢 应用线程阻塞 占用网络带宽 高频访问的图片无法使用浏览器缓存 4.1.3. Emoji乱码怎么办？ 使用utf8mb4\nMySQL在5.5.3之后增加了这个utf8mb4的编码，mb4就是most bytes 4的意思，专门用来兼容四字节的unicode。好在utf8mb4是utf8的超集，除了将编码改为utf8mb4外不需要做其他转换。当然，一般情况下使用utf8也就够了。\n4.2. 如何存储ip地址？ 使用字符串 使用无符号整型 4个字节即解决问题 可以支持范围查询 INET_ATON() 和 INET_NTOA() ipv6 使用 INET6_ATON() 和 INET6_NTOA() 4.3. 长文本如何存储？ 可以使用Text存储\nTINYTEXT(255长度)\nTEXT(65535)\nMEDIUMTEXT（int最大值16M）\nLONGTEXT(long最大值4G)\n4.3.1. 大段文本如何设计表结构？ 或将大段文本同时存储到搜索引擎 分表存储 分表后多段存储 4.3.2. 大段文本查找时如何建立索引？ 全文检索，模糊匹配最好存储到搜索引擎中 指定索引长度 分段存储后创建索引 4.3.3. 有没有在开发中使用过TEXT,BLOB 数据类型 BLOB 之前做ERP的时候使用过，互联网项目一般不用BLOB\nTEXT 文献，文章，小说类，新闻，会议内容 等\n4.4. 日期，时间如何存取？ 使用 TIMESTAMP，DATETIME 使用字符串 4.4.1. TIMESTAMP，DATETIME 的区别是什么？ 跨时区的业务使用 TIMESTAMP，TIMESTAMP会有时区转换\n1、两者的存储方式不一样:\n对于TIMESTAMP，它把客户端插入的时间从当前时区转化为UTC（世界标准时间）进行存储。查询时，将其又转化为客户端当前时区进行返回。 而对于DATETIME，不做任何改变，基本上是原样输入和输出。 2、存储字节大小不同\n数据类型 MySQL 5.6.4之前需要存储 MySQL 5.6.4之后需要存储 DATETIME 8 bytes 5 bytes + 小数秒存储 TIMESTAMP 4 bytes 4 bytes + 小数秒存储 分秒数精度 存储字节大小 0 0 bytes 1,2 1 bytes 3,4 2 bytes 5,6 3 bytes 3、两者所能存储的时间范围不一样:\ntimestamp 所能存储的时间范围为：\u0026lsquo;1970-01-01 00:00:01.000000\u0026rsquo; 到 \u0026lsquo;2038-01-19 03:14:07.999999\u0026rsquo;。 datetime 所能存储的时间范围为：\u0026lsquo;1000-01-01 00:00:00.000000\u0026rsquo; 到 \u0026lsquo;9999-12-31 23:59:59.999999\u0026rsquo;。 4.4.2. 为什么不使用字符串存储日期？ 字符串无法完成数据库内部的范围筛选\n在大数据量存储优化索引时，查询必须加上时间范围\n4.4.3. 如果需要使用时间戳 timestamp和int该如何选择？ int 存储空间小，运算查询效率高，不受时区影响，精度低\ntimestamp 存储空间小，可以使用数据库内部时间函数比如更新，精度高，需要注意时区转换，timestamp更易读\n一般选择timestamp，两者性能差异不明显，本质上存储都是使用的int\n4.5. char与varchar的区别？如何选择？ 1.char的优点是存储空间固定（最大255），没有碎片，尤其更新比较频繁的时候，方便数据文件指针的操作，所以存储读取速度快。缺点是空间冗余，对于数据量大的表，非固定长度属性使用char字段，空间浪费。\n2.varchar字段，存储的空间根据存储的内容变化，空间长度为L+size，存储内容长度加描述存储内容长度信息，优点就是空间节约，缺点就是读取和存储时候，需要读取信息计算下标，才能获取完整内容。\n4.6. 财务计算有没有出现过错乱？ 第一类：锁包括多线程，数据库，UI展示后超时提交等\n第二类：应用与数据库浮点运算精度丢失\n应用开发问题：多线程共享数据读写， 之前有过丢失精度的问题，使用decimal解决 使用乘法替换除法 使用事务保证acid特性 更新时使用悲观锁 SELECT … FOR UPDATE 数据只有标记删除 记录详细日志方便溯源 4.6.1. 117 decimal与float,double的区别是什么？ float：浮点型，4字节，32bit。\ndouble：双精度实型，8字节，64位\ndecimal：数字型，128bit，不存在精度损失\n对于声明语法DECIMAL(M,D)，自变量的值范围如下：\nM是最大位数（精度），范围是1到65。可不指定，默认值是10。 D是小数点右边的位数（小数位）。范围是0到30，并且不能大于M，可不指定，默认值是0。 例如字段 salary DECIMAL(5,2)，能够存储具有五位数字和两位小数的任何值，因此可以存储在salary列中的值的范围是从-999.99到999.99。\n4.6.2. 118 浮点类型如何选型？为什么？ 需要不丢失精度的计算使用DECIMAL\n仅用于展示没有计算的小数存储可以使用字符串存储\n低价值数据允许计算后丢失精度可以使用float double\n整型记录不会出现小数的不要使用浮点类型\n4.7. 119 预编译sql是什么？ 完整解释：\nhttps://dev.mysql.com/doc/refman/8.0/en/prepare.html\nPreparedStatement\n4.7.1. 120 预编译sql有什么好处？ 预编译sql会被mysql缓存下来 作用域是每个session，对其他session无效，重新连接也会失效 提高安全性防止 sql 注入 select * from user where id =? \u0026ldquo;1;delete from user where id = 1\u0026rdquo;; 编译语句有可能被重复调用，也就是说sql相同参数不同在同一session中重复查询执行效率明显比较高 mysql 5,8 支持服务器端的预编译 4.8. 121\t子查询与join哪个效率高？ 子查询虽然很灵活，但是执行效率并不高。\n4.8.1. 122 为什么子查询效率低？ 在执行子查询的时候，MYSQL创建了临时表，查询完毕后再删除这些临时表\n子查询的速度慢的原因是多了一个创建和销毁临时表的过程。 而join 则不需要创建临时表 所以会比子查询快一点\n4.8.2. 123 join查询可以无限叠加吗？Mysql对join查询有什么限制吗？ 建议join不超过3张表关联，mysql对内存敏感，关联过多会占用更多内存空间，使性能下降\nToo many tables; MySQL can only use 61 tables in a join；\n系统限制最多关联61个表\n4.8.3. 124 join 查询算法了解吗？ Simple Nested-Loop Join：SNLJ，简单嵌套循环连接 Index Nested-Loop Join：INLJ，索引嵌套循环连接 Block Nested-Loop Join：BNLJ，缓存块嵌套循环连接 4.8.4. 125 如何优化过多join查询关联？ 适当使用冗余字段减少多表关联查询 驱动表和被驱动表（小表join大表） 业务允许的话 尽量使用inner join 让系统帮忙自动选择驱动表 关联字段一定创建索引 调整JOIN BUFFER大小 4.9. 126\t是否有过mysql调优经验？ 调优：\nsql调优 表（结构）设计调优 索引调优 慢查询调优 操作系统调优 数据库参数调优 4.9.1. 127 开发中使用过哪些调优工具？ 官方自带：\nEXPLAIN mysqldumpslow show profiles 时间 optimizer_trace 第三方：性能诊断工具，参数扫描提供建议，参数辅助优化\n4.9.2. 128 如何监控线上环境中执行比较慢的sql？ 129 如何分析一条慢sql？ 开启慢查询日志，收集sql\n默认情况下，MySQL数据库没有开启慢查询日志，需要我们手动来设置这个参数。 当然，如果不是调优需要的话，一般不建议启动该参数，因为开启慢查询日志会或多或少带来一定的性能影响。慢查询日志支持将日志记录写入文件。 查看及开启\n默认关闭 SHOW VARIABLES LIKE '%slow_query_log%'; 默认情况下slow_query_log的值为OFF，表示慢查询日志是禁用的，\n​\n开启：set global slow_query_log=1; 只对窗口生效，重启服务失效\n慢查询日志记录long_query_time时间\n1 2 3 SHOW VARIABLES LIKE \u0026#39;%long_query_time%\u0026#39;; SHOW GLOBAL VARIABLES LIKE \u0026#39;long_query_time\u0026#39;; l 全局变量设置，对所有客户端有效。但，必须是设置后进行登录的客户端。\nSET GLOBAL long_query_time=0.1;\nl 对当前会话连接立即生效，对其他客户端无效。\nSET SESSION long_query_time=0.1; #session可省略\n假如运行时间正好等于long_query_time的情况，并不会被记录下来。也就是说，\n在mysql源码里是判断大于long_query_time，而非大于等于。\n永久生效\n修改配置文件my.cnf（其它系统变量也是如此） [mysqld]下增加或修改参数 slow_query_log 和slow_query_log_file后，然后重启MySQL服务器。也即将如下两行配置进my.cnf文件 slow_query_log =1\nslow_query_log_file=/var/lib/mysql/localhost-slow.log\nlong_query_time=3\nlog_output=FILE\n关于慢查询的参数slow_query_log_file，它指定慢查询日志文件的存放路径，如果不设置，系统默认文件：[host_name]-slow.log case\n记录慢SQL并后续分析 SELECT * FROM emp;\nSELECT * FROM emp WHERE deptid \u0026gt; 1;\n查询当前系统中有多少条慢查询记录或者直接看慢查询日志 /var/lib/mysql/localhost-slow.log\nSHOW GLOBAL STATUS LIKE \u0026lsquo;%Slow_queries%\u0026rsquo;;\n日志分析工具mysqldumpslow\n在生产环境中，如果要手工分析日志，查找、分析SQL，显然是个体力活，MySQL提供了日志分析工具mysqldumpslow。\n查看mysqldumpslow的帮助信息\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 a) mysqldumpslow --help · -a: 将数字抽象成N，字符串抽象成S · -s: 是表示按照何种方式排序； c: 访问次数 l: 锁定时间 r: 返回记录 **t:** **查询时间** al:平均锁定时间 ar:平均返回记录数 at:平均查询时间 · -t: 即为返回前面多少条的数据； · -g: 后边搭配一个正则匹配模式，大小写不敏感的； 得到返回记录集最多的10个SQL mysqldumpslow -s r -t 10 /var/lib/mysql/localhost-slow.log 得到访问次数最多的10个SQL mysqldumpslow -s c -t 10 /var/lib/mysql/localhost-slow.log 得到按照时间排序的前10条里面含有左连接的查询语句 mysqldumpslow -s t -t 10 -g \u0026#34;left join\u0026#34; /var/lib/mysql/localhost-slow.log 另外建议在使用这些命令时结合 | 和more 使用 ，否则有可能出现爆屏情况 mysqldumpslow -s r -t 10 /var/lib/mysql/localhost-slow.log | more 4.9.3. 130 如何查看当前sql使用了哪个索引？ 可以使用EXPLAIN，选择索引过程可以使用 optimizer_trace\n4.9.4. 131 索引如何进行分析和调优？ 4.9.5. 132 EXPLAIN关键字中的重要指标有哪些？ 4.10. EXPLAIN是什么 使用EXPLAIN关键字可以模拟优化器执行SQL查询语句，从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是表结构的性能瓶颈。\n4.11. EXPLAIN的用法 用法：\n1 EXPLAIN + SQL语句 数据准备：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 USE atguigudb; CREATE TABLE t1(id INT(10) AUTO_INCREMENT, content VARCHAR(100) NULL, PRIMARY KEY (id)); CREATE TABLE t2(id INT(10) AUTO_INCREMENT, content VARCHAR(100) NULL, PRIMARY KEY (id)); CREATE TABLE t3(id INT(10) AUTO_INCREMENT, content VARCHAR(100) NULL, PRIMARY KEY (id)); CREATE TABLE t4(id INT(10) AUTO_INCREMENT, content1 VARCHAR(100) NULL, content2 VARCHAR(100) NULL, PRIMARY KEY (id)); CREATE INDEX idx_content1 ON t4(content1); -- 普通索引 # 以下新增sql多执行几次，以便演示 INSERT INTO t1(content) VALUES(CONCAT(\u0026#39;t1_\u0026#39;,FLOOR(1+RAND()*1000))); INSERT INTO t2(content) VALUES(CONCAT(\u0026#39;t2_\u0026#39;,FLOOR(1+RAND()*1000))); INSERT INTO t3(content) VALUES(CONCAT(\u0026#39;t3_\u0026#39;,FLOOR(1+RAND()*1000))); INSERT INTO t4(content1, content2) VALUES(CONCAT(\u0026#39;t4_\u0026#39;,FLOOR(1+RAND()*1000)), CONCAT(\u0026#39;t4_\u0026#39;,FLOOR(1+RAND()*1000))); 4.12. 各字段解释 4.12.1. table **单表：**显示这一行的数据是关于哪张表的 1 EXPLAIN SELECT * FROM t1; **多表关联：**t1为驱动表，t2为被驱动表。 注意：内连接时，MySQL性能优化器会自动判断哪个表是驱动表，哪个表示被驱动表，和书写的顺序无关\n1 EXPLAIN SELECT * FROM t1 INNER JOIN t2; 4.12.2. id 表示查询中执行select子句或操作表的顺序\n**id相同：**执行顺序由上至下 1 EXPLAIN SELECT * FROM t1, t2, t3; **id不同：**如果是子查询，id的序号会递增，id值越大优先级越高，越先被执行 1 2 3 4 5 EXPLAIN SELECT t1.id FROM t1 WHERE t1.id =( SELECT t2.id FROM t2 WHERE t2.id =( SELECT t3.id FROM t3 WHERE t3.content = \u0026#39;t3_434\u0026#39; ) ); 注意：查询优化器可能对涉及子查询的语句进行优化，转为连接查询\n1 EXPLAIN SELECT * FROM t1 WHERE content IN (SELECT content FROM t2 WHERE content = \u0026#39;a\u0026#39;); **id为NULL：**最后执行 1 EXPLAIN SELECT * FROM t1 UNION SELECT * FROM t2; 小结：\nid如果相同，可以认为是一组，从上往下顺序执行 在所有组中，id值越大，优先级越高，越先执行 关注点：id号每个号码，表示一趟独立的查询, 一个sql的查询趟数越少越好 4.12.3. select_type 查询的类型，主要是用于区别普通查询、联合查询、子查询等的复杂查询。\n**SIMPLE：**简单查询。查询中不包含子查询或者UNION。 1 EXPLAIN SELECT * FROM t1; **PRIMARY：**主查询。查询中若包含子查询，则最外层查询被标记为PRIMARY。 **SUBQUERY：**子查询。在SELECT或WHERE列表中包含了子查询。 1 EXPLAIN SELECT * FROM t3 WHERE id = ( SELECT id FROM t2 WHERE content= \u0026#39;a\u0026#39;); **DEPENDENT SUBQUREY：**如果包含了子查询，并且查询语句不能被优化器转换为连接查询，并且子查询是相关子查询（子查询基于外部数据列），则子查询就是DEPENDENT SUBQUREY。 1 EXPLAIN SELECT * FROM t3 WHERE id = ( SELECT id FROM t2 WHERE content = t3.content); **UNCACHEABLE SUBQUREY：**表示这个subquery的查询要受到外部系统变量的影响 1 2 EXPLAIN SELECT * FROM t3 WHERE id = ( SELECT id FROM t2 WHERE content = @@character_set_server); **UNION：**对于包含UNION或者UNION ALL的查询语句，除了最左边的查询是PRIMARY，其余的查询都是UNION。 **UNION RESULT：**UNION会对查询结果进行查询去重，MYSQL会使用临时表来完成UNION查询的去重工作，针对这个临时表的查询就是\u0026quot;UNION RESULT\u0026quot;。 1 2 3 4 EXPLAIN SELECT * FROM t3 WHERE id = 1 UNION SELECT * FROM t2 WHERE id = 1; **DEPENDENT UNION：**子查询中的UNION或者UNION ALL，除了最左边的查询是DEPENDENT SUBQUREY，其余的查询都是DEPENDENT UNION。 1 2 3 4 5 6 EXPLAIN SELECT * FROM t1 WHERE content IN ( SELECT content FROM t2 UNION SELECT content FROM t3 ); **DERIVED：**在包含派生表（子查询在from子句中）的查询中，MySQL会递归执行这些子查询，把结果放在临时表里。 1 2 3 EXPLAIN SELECT * FROM ( SELECT content, COUNT(*) AS c FROM t1 GROUP BY content ) AS derived_t1 WHERE c \u0026gt; 1; 这里的\u0026lt;derived2\u0026gt;就是在id为2的查询中产生的派生表。\n**补充：**MySQL在处理带有派生表的语句时，优先尝试把派生表和外层查询进行合并，如果不行，再把派生表物化掉（执行子查询，并把结果放入临时表），然后执行查询。下面的例子就是就是将派生表和外层查询进行合并的例子：\n1 EXPLAIN SELECT * FROM (SELECT * FROM t1 WHERE content = \u0026#39;t1_832\u0026#39;) AS derived_t1; **MATERIALIZED：**优化器对于包含子查询的语句，如果选择将子查询物化后再与外层查询连接查询，该子查询的类型就是MATERIALIZED。如下的例子中，查询优化器先将子查询转换成物化表，然后将t1和物化表进行连接查询。 1 EXPLAIN SELECT * FROM t1 WHERE content IN (SELECT content FROM t2); 4.12.4. partitions 代表分区表中的命中情况，非分区表，该项为NULL\n4.12.5. type ☆ 说明：\n结果值从最好到最坏依次是：\nsystem \u0026gt; const \u0026gt; eq_ref \u0026gt; ref \u0026gt; fulltext \u0026gt; ref_or_null \u0026gt; index_merge \u0026gt; unique_subquery \u0026gt; index_subquery \u0026gt; range \u0026gt; index \u0026gt; ALL\n比较重要的包含：system、const 、eq_ref 、ref、range \u0026gt; index \u0026gt; ALL\nSQL 性能优化的目标：至少要达到 range 级别，要求是 ref 级别，最好是 consts级别。（阿里巴巴 开发手册要求）\n**ALL：**全表扫描。Full Table Scan，将遍历全表以找到匹配的行 1 EXPLAIN SELECT * FROM t1; **index：**当使用覆盖索引，但需要扫描全部的索引记录时 覆盖索引：如果能通过读取索引就可以得到想要的数据，那就不需要读取用户记录，或者不用再做回表操作了。一个索引包含了满足查询结果的数据就叫做覆盖索引。\n1 2 -- 只需要读取聚簇索引部分的非叶子节点，就可以得到id的值，不需要查询叶子节点 EXPLAIN SELECT id FROM t1; 1 2 -- 只需要读取二级索引，就可以在二级索引中获取到想要的数据，不需要再根据叶子节点中的id做回表操作 EXPLAIN SELECT id, deptId FROM t_emp; **range：**只检索给定范围的行，使用一个索引来选择行。key 列显示使用了哪个索引，一般就是在你的where语句中出现了between、\u0026lt;、\u0026gt;、in等的查询。这种范围扫描索引扫描比全表扫描要好，因为它只需要开始于索引的某一点，而结束于另一点，不用扫描全部索引。 1 EXPLAIN SELECT * FROM t1 WHERE id IN (1, 2, 3); **ref：**通过普通二级索引列与常量进行等值匹配时 1 EXPLAIN SELECT * FROM t_emp WHERE deptId = 1; **eq_ref：**连接查询时通过主键或不允许NULL值的唯一二级索引列进行等值匹配时 1 EXPLAIN SELECT * FROM t1, t2 WHERE t1.id = t2.id; **const：**根据主键或者唯一二级索引列与常数进行匹配时 1 EXPLAIN SELECT * FROM t1 WHERE id = 1; **system：**MyISAM引擎中，当表中只有一条记录时。（这是所有type的值中性能最高的场景） 1 2 3 CREATE TABLE t(i int) Engine=MyISAM; INSERT INTO t VALUES(1); EXPLAIN SELECT * FROM t; 其他不太常见的类型（了解）：\nindex_subquery：利用普通索引来关联子查询，针对包含有IN子查询的查询语句。content1是普通索引字段 1 EXPLAIN SELECT * FROM t1 WHERE content IN (SELECT content1 FROM t4 WHERE t1.content = t4.content2) OR content = \u0026#39;a\u0026#39;; unique_subquery：类似于index_subquery，利用唯一索引来关联子查询。t2的id是主键，也可以理解为唯一的索引字段 1 EXPLAIN SELECT * FROM t1 WHERE id IN (SELECT id FROM t2 WHERE t1.content = t2.content) OR content = \u0026#39;a\u0026#39;; index_merge：在查询过程中需要多个索引组合使用，通常出现在有 or 的关键字的sql中。 1 EXPLAIN SELECT * FROM t_emp WHERE deptId = 1 OR id = 1; ref_or_null：当对普通二级索引进行等值匹配，且该索引列的值也可以是NULL值时。 1 EXPLAIN SELECT * FROM t_emp WHERE deptId = 1 OR deptId IS NULL; **fulltext：**全文索引。一般通过搜索引擎实现，这里我们不展开。 4.12.6. possible_keys 和 keys ☆ possible_keys表示执行查询时可能用到的索引，一个或多个。 查询涉及到的字段上若存在索引，则该索引将被列出，但不一定被查询实际使用。\nkeys表示实际使用的索引。如果为NULL，则没有使用索引。\n1 EXPLAIN SELECT id FROM t1 WHERE id = 1; 4.12.7. key_len ☆ 表示索引使用的字节数，根据这个值可以判断索引的使用情况，检查是否充分利用了索引，针对联合索引值越大越好。\n如何计算：\n先看索引上字段的类型+长度。比如：int=4 ; varchar(20) =20 ; char(20) =20 如果是varchar或者char这种字符串字段，视字符集要乘不同的值，比如utf8要乘 3，如果是utf8mb4要乘4，GBK要乘2 varchar这种动态字符串要加2个字节 允许为空的字段要加1个字节 1 2 3 4 5 6 -- 创建索引 CREATE INDEX idx_age_name ON t_emp(age, `name`); -- 测试1 EXPLAIN SELECT * FROM t_emp WHERE age = 30 AND `name` = \u0026#39;ab%\u0026#39;; -- 测试2 EXPLAIN SELECT * FROM t_emp WHERE age = 30; 4.12.8. ref 显示与key中的索引进行比较的列或常量。\n1 2 3 4 5 -- ref=atguigudb.t1.id 关联查询时出现，t2表和t1表的哪一列进行关联 EXPLAIN SELECT * FROM t1, t2 WHERE t1.id = t2.id; -- ref=const 与索引列进行等值比较的东西是啥，const表示一个常数 EXPLAIN SELECT * FROM t_emp WHERE age = 30; 4.12.9. rows ☆ MySQL认为它执行查询时必须检查的行数。值越小越好。\n1 2 3 4 5 -- 如果是全表扫描，rows的值就是表中数据的估计行数 EXPLAIN SELECT * FROM t_emp WHERE empno = \u0026#39;10001\u0026#39;; -- 如果是使用索引查询，rows的值就是预计扫描索引记录行数 EXPLAIN SELECT * FROM t_emp WHERE deptId = 1; 4.12.10. filtered 最后查询出来的数据占所有服务器端检查行数（rows）的百分比。值越大越好。\n1 2 3 4 5 6 -- 先根据二级索引deptId找到数据的主键，有3条记录满足条件， -- 再根据主键进行回表，最终找到3条记录，有100%的记录满足条件 EXPLAIN SELECT * FROM t_emp WHERE deptId = 1; -- 这个例子如果name列是索引列则 filtered = 100 否则filtered = 10(全表扫描) EXPLAIN SELECT * FROM t_emp WHERE `name` = \u0026#39;风清扬\u0026#39;; 4.12.11. Extra ☆ 包含不适合在其他列中显示但十分重要的额外信息。通过这些额外信息来理解MySQL到底将如何执行当前的查询语句。MySQL提供的额外信息有好几十个，这里只挑介绍比较重要的介绍。\nImpossible WHERE：where子句的值总是false 1 EXPLAIN SELECT * FROM t_emp WHERE 1 != 1; **Using where：**使用了where，但在where上有字段没有创建索引 1 EXPLAIN SELECT * FROM t_emp WHERE `name` = \u0026#39;风清扬\u0026#39;; **Using temporary：**使了用临时表保存中间结果 1 EXPLAIN SELECT DISTINCT content FROM t1; Using filesort： 在对查询结果中的记录进行排序时，是可以使用索引的，如下所示：\n1 EXPLAIN SELECT * FROM t1 ORDER BY id; 如果排序操作无法使用到索引，只能在内存中（记录较少时）或者磁盘中（记录较多时）进行排序（filesort），如下所示：\n1 EXPLAIN SELECT * FROM t1 ORDER BY content; **Using index：**使用了覆盖索引，表示直接访问索引就足够获取到所需要的数据，不需要通过索引回表 1 EXPLAIN SELECT id, content1 FROM t4; 1 EXPLAIN SELECT id FROM t1; **Using index condition：**叫作 Index Condition Pushdown Optimization （索引下推优化） 如果没有索引下推（ICP），那么MySQL在存储引擎层找到满足content1 \u0026gt; 'z'条件的第一条二级索引记录。主键值进行回表，返回完整的记录给server层，server层再判断其他的搜索条件是否成立。如果成立则保留该记录，否则跳过该记录，然后向存储引擎层要下一条记录。 如果使用了索引下推（ICP），那么MySQL在存储引擎层找到满足content1 \u0026gt; 'z'条件的第一条二级索引记录。不着急执行回表，而是在这条记录上先判断一下所有关于idx_content1索引中包含的条件是否成立，也就是content1 \u0026gt; 'z' AND content1 LIKE '%a'是否成立。如果这些条件不成立，则直接跳过该二级索引记录，去找下一条二级索引记录；如果这些条件成立，则执行回表操作，返回完整的记录给server层。 1 2 -- content1列上有索引idx_content1 EXPLAIN SELECT * FROM t4 WHERE content1 \u0026gt; \u0026#39;z\u0026#39; AND content1 LIKE \u0026#39;%a\u0026#39;; **注意：**如果这里的查询条件只有content1 \u0026gt; 'z'，那么找到满足条件的索引后也会进行一次索引下推的操作，判断content1 \u0026gt; \u0026lsquo;z\u0026rsquo;是否成立（这是源码中为了编程方便做的冗余判断）\n**Using join buffer：**在连接查询时，当被驱动表不能有效的利用索引时，MySQL会为其分配一块名为连接缓冲区（join buffer）的内存来加快查询速度 1 EXPLAIN SELECT * FROM t1, t2 WHERE t1.content = t2.content; 下面这个例子就是被驱动表使用了索引：\n1 EXPLAIN SELECT * FROM t_emp, t_dept WHERE t_dept.id = t_emp.deptId; 4.13. 133 MySQL数据库cpu飙升的话你会如何分析 重点是定位问题。\n先\n1 使用top观察mysqld的cpu利用率\n切换到常用的数据库\n使用show full processlist;查看会话\n观察是哪些sql消耗了资源，其中重点观察state指标\n定位到具体sql\n2 pidstat\n定位到线程 在PERFORMANCE_SCHEMA.THREADS中记录了thread_os_id 找到线程执行的sql 根据操作系统id可以到processlist表找到对应的会话 在会话中即可定位到问题sql 3 使用show profile观察sql各个阶段耗时\n4 服务器上是否运行了其他程序\n5 检查一下是否有慢查询\n6 pref top\n使用pref 工具分析哪些函数引发的cpu过高来追踪定位\n4.14. 134 有没有进行过分库分表？ 4.14.1. 135 什么是分库分表？ 垂直分库\n一个数据库由很多表的构成，每个表对应着不同的业务，垂直切分是指按照业务将表进行分类，分布到不同 的数据库上面，这样也就将数据或者说压力分担到不同的库上面，如下图：\n系统被切分成了，用户，订单交易，支付几个模块。\n水平分表\n把一张表里的内容按照不同的规则 写到不同的库里\n相对于垂直拆分，水平拆分不是将表做分类，而是按照某个字段的某种规则来分散到多个库之中，每个表中包含一部分数据。简单来说，我们可以将数据的水平切分理解为是按照数据行的切分，就是将表中的某些行切分 到一个数据库，而另外的某些行又切分到其他的数据库中，如图：\n4.14.2. 136 什么时候进行分库分表？有没有配合es使用经验？ 能不分就不分 单机性能下降明显的时候 增加缓存（通常查询量比较大），细分业务 首先尝试主被集群，读写分离 尝试分库 尝试分表 -\u0026gt; 冷热数据分离 大数据量下可以配合es完成高效查询\n4.14.3. 说一下实现分库分表工具的实现思路 伪装成mysql服务器，代理用户请求转发到真实服务器 基于本地aop实现，拦截sql，改写，路由和结果归集处理。 4.14.4. 用过哪些分库分表工具？ 4.14.5. 分库分表后可能会有哪些问题？ 经典的问题\n执行效率明显降低 表结构很难再次调整 引发分布式id问题 产生跨库join 代理类中间件网络io成为瓶颈 4.14.6. 说一下读写分离常见方案？ 4.15. 为什么要使用视图？ 什么是视图？ 视图定义： 1、视图是一个虚表，是从一个或几个基本表（或视图）导出的表。 2、只存放视图的定义，不存放视图对应的数据。 3、基表中的数据发生变化，从视图中查询出的数据也随之改变。 视图的作用： 1、视图能够简化用户的操作 2、视图使用户能以多种角度看待同一数据 3、视图对重构数据库提供了一定程度的逻辑独立性 4、视图能够对机密数据提供安全保护 5、适当的利用视图可以更清晰的表达查询\n4.16. 什么是存储过程？有没有使用过？ 项目中禁止使用存储过程，存储过程难以调试和扩展，更没有移植性\n4.17. 有没有使用过外键？有什么需要注意的地方？ 不得使用外键与级联，一切外键概念必须在应用层解决。\n说明：以学生和成绩的关系为例，学生表中的 student_id是主键，那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id，同时触发成绩表中的 student_id 更新，即为 级联更新。外键与级联更新适用于单机低并发，不适合分布式、高并发集群；级联更新是强阻 塞，存在数据库更新风暴的风险；外键影响数据库的插入速度。\n4.18. 用过processlist吗？ 关键的就是state列，mysql列出的状态主要有以下几种：\nChecking table 正在检查数据表（这是自动的）。 Closing tables 正在将表中修改的数据刷新到磁盘中，同时正在关闭已经用完的表。这是一个很快的操作，如果不是这样的话，就应该确认磁盘空间是否已经满了或者磁盘是否正处于重负中。 Connect Out 复制从服务器正在连接主服务器。 Copying to tmp table on disk 由于临时结果集大于tmp_table_size，正在将临时表从内存存储转为磁盘存储以此节省内存。 Creating tmp table 正在创建临时表以存放部分查询结果。 deleting from main table 服务器正在执行多表删除中的第一部分，刚删除第一个表。 deleting from reference tables 服务器正在执行多表删除中的第二部分，正在删除其他表的记录。 Flushing tables 正在执行FLUSH TABLES，等待其他线程关闭数据表。 Killed 发送了一个kill请求给某线程，那么这个线程将会检查kill标志位，同时会放弃下一个kill请求。MySQL会在每次的主循环中检查kill标志位，不过有些情况下该线程可能会过一小段才能死掉。如果该线程程被其他线程锁住了，那么kill请求会在锁释放时马上生效。 Locked 被其他查询锁住了。 Sending data 正在处理Select查询的记录，同时正在把结果发送给客户端。Sending data”状态的含义，原来这个状态的名称很具有误导性，所谓的“Sending data”并不是单纯的发送数据，而是包括“收集 + 发送 数据”。 Sorting for group 正在为GROUP BY做排序。 Sorting for order 正在为ORDER BY做排序。 Opening tables 这个过程应该会很快，除非受到其他因素的干扰。例如，在执Alter TABLE或LOCK TABLE语句行完以前，数据表无法被其他线程打开。正尝试打开一个表。 Removing duplicates 正在执行一个Select DISTINCT方式的查询，但是MySQL无法在前一个阶段优化掉那些重复的记录。因此，MySQL需要再次去掉重复的记录，然后再把结果发送给客户端。 Reopen table 获得了对一个表的锁，但是必须在表结构修改之后才能获得这个锁。已经释放锁，关闭数据表，正尝试重新打开数据表。 Repair by sorting 修复指令正在排序以创建索引。 Repair with keycache 修复指令正在利用索引缓存一个一个地创建新索引。它会比Repair by sorting慢些。 Searching rows for update 正在讲符合条件的记录找出来以备更新。它必须在Update要修改相关的记录之前就完成了。 Sleeping 正在等待客户端发送新请求. System lock 正在等待取得一个外部的系统锁。如果当前没有运行多个mysqld服务器同时请求同一个表，那么可以通过增加\u0026ndash;skip-external-locking参数来禁止外部系统锁。 Upgrading lock Insert DELAYED正在尝试取得一个锁表以插入新记录。= Updating 正在搜索匹配的记录，并且修改它们。 User Lock 正在等待GET_LOCK()。 Waiting for tables 该线程得到通知，数据表结构已经被修改了，需要重新打开数据表以取得新的结构。然后，为了能的重新打开数据表，必须等到所有其他线程关闭这个表。以下几种情况下会产生这个通知：FLUSH TABLES tbl_name, Alter TABLE, RENAME TABLE, REPAIR TABLE, ANALYZE TABLE,或OPTIMIZE TABLE。 waiting for handler insert Insert DELAYED已经处理完了所有待处理的插入操作，正在等待新的请求。 4.19. 145 某个表有数千万数据，查询比较慢，如何优化？说一下思路 前端优化 减少查询 合并请求:多个请求需要的数据尽量一条sql拿出来 会话保存：和用户会话相关的数据尽量一次取出重复使用 避免无效刷新 多级缓存 不要触及到数据库 应用层热点数据高速查询缓存（低一致性缓存） 高频查询大数据量镜像缓存（双写高一致性缓存） 入口层缓存（几乎不变的系统常量） 使用合适的字段类型，比如varchar换成char 一定要高效使用索引。 使用explain 深入观察索引使用情况 检查select 字段最好满足索引覆盖 复合索引注意观察key_len索引使用情况 有分组，排序，注意file sort，合理配置相应的buffer大小 检查查询是否可以分段查询，避免一次拿出过多无效数据 多表关联查询是否可以设置冗余字段，是否可以简化多表查询或分批查询 分而治之：把服务拆分成更小力度的微服务 冷热数据分库存储 读写分离，主被集群 然后再考虑分库分表 等 4.20. count(列名)和 count(*)有什么区别？ count()是 SQL92 定义的 标准统计行数的语法，跟数据库无关，跟 NULL 和非 NULL 无关。 说明：count()会统计值为 NULL 的行，而 count(列名)不会统计此列为 NULL 值的行。\n4.21. 如果有超大分页改怎么处理？ select name from user limit 10000,10;在 使用的时候并不是跳过 offset 行，而是取 offset+N 行，然后返回放弃前 offset 行，返回 N 行\n通过索引优化的方案：\n如果主键自增可以 select name from user where id \u0026gt; 10000 limit 10; 延迟关联 需要order by时 一定注意增加筛选条件，避免全表排序 where -》 order by -》 limit 减少select字段 优化相关参数避免filesort 一般大分页情况比较少（很少有人跳转到几百万页去查看数据），实际互联网业务中多数还是按顺序翻页，可以使用缓存提升前几页的查询效率，实际上大多数知名互联网项目也都是这么做的\n在阿里巴巴《Java开发手册》中的建议：\n【推荐】利用延迟关联或者子查询优化超多分页场景。 说明：MySQL 并不是跳过 offset 行，而是取 offset+N 行，然后返回放弃前 offset 行，返回 N 行，那当 offset 特别大的时候，效率就非常的低下，要么控制返回的总页数，要么对超过 特定阈值的页数进行 SQL 改写。 正例：先快速定位需要获取的 id 段，然后再关联： SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id\n4.22. mysql服务器毫无规律的异常重启如何排查问题？ 首先是查看mysql和系统日志来定位错误\n最常见的是关闭swap分区后OOM问题：\nmysql 分为应用进程和守护进程\n当应用进程内存占用过高的时候操作系统可能会kill掉进程，此时守护进程又帮我们重启了应用进程，运行一段时间后又出现OOM如此反复\n可以排查以下几个关键点\n运行时内存占用率 mysql buffer相关参数 mysql 网络连接相关参数 异常关机或kill -9 mysql 后导致表文件损坏\n直接使用备份 配置 innodb_force_recovery 跳过启动恢复过程 4.22.1. mysql 线上修改表结构有哪些风险? 针对ddl命令，有以下几种方式\ncopy table 锁原表，创建临时表并拷贝数据\ninplace 针对索引修改删除的优化，不需要拷贝所有数据\nOnline DDL 细分DDL命令来决定是否锁表\n可能会锁表，导致无法读写\nORM中的映射失效\n索引失效\n建议：建个新表，导入数据后重命名\n4.23. 什么是mysql多实例部署？ 指的是在一台主机上部署多个实例\n主要目的是压榨服务器性能\n缺点是互相影响\n","permalink":"https://ktzxy.top/posts/8nv3v2qdlr/","summary":"MySQL数据库150道高频面试题","title":"MySQL数据库150道高频面试题"},{"content":" MySQL一直以来提供show profile命令来获取某一条SQL执行过程中的资源使用与耗时情况，这个命令对于分析具体SQL的性能瓶颈有非常大的帮助，但是这个功能在MySQL新的版本里将会被废弃，取而代之的是使用Performance Schema来提供同样的功能。本文将介绍如何使用Performance Schema来实现show profile SQL性能分析的功能。\n1. 测试环境配置 测试版本：MySQL 5.7.19 配置参数：performance_schema=ON，该参数配置在my.cnf文件中，生效需要重启MySQL。 2. 配置performance_schema 2.1 配置表setup_actors 默认情况下，performance_schema功能打开后，将会收集所有用户的SQL执行历史事件，因为收集的信息太多，对数据库整体性能有一定影响，而且也不利于排查指定SQL的性能问题，因此需要修改setup_actors表的配置，只收集特定用户的历史事件信息。setup_actors表配置如下：\n1 2 3 4 5 6 7 8 mysql\u0026gt; select * from performance_schema.setup_actors; +-----------+------+------+---------+---------+ | HOST | USER | ROLE | ENABLED | HISTORY | +-----------+------+------+---------+---------+ | % | % | % | NO | NO | | localhost | root | % | YES | YES | +-----------+------+------+---------+---------+ 2 rows in set (0.00 sec) 只收集本地root用户的SQL执行历史事件。\n2.2 配置表setup_instruments 启用statement和stage监视器。\n1 2 3 UPDATE performance_schema.setup_instruments SET ENABLED = \u0026#39;YES\u0026#39;, TIMED = \u0026#39;YES\u0026#39; WHERE NAME LIKE \u0026#39;%statement/%\u0026#39;; UPDATE performance_schema.setup_instruments SET ENABLED = \u0026#39;YES\u0026#39;, TIMED = \u0026#39;YES\u0026#39; WHERE NAME LIKE \u0026#39;%stage/%\u0026#39;; 2.3 配置表setup_consumers 启用events_statements_，events_stages_ 开头的事件类型消费。\n1 2 3 UPDATE performance_schema.setup_consumers SET ENABLED = \u0026#39;YES\u0026#39; WHERE NAME LIKE \u0026#39;%events_statements_%\u0026#39;; UPDATE performance_schema.setup_consumers SET ENABLED = \u0026#39;YES\u0026#39; WHERE NAME LIKE \u0026#39;%events_stages_%\u0026#39;; 3. 收集具体SQL的性能分析 3.1 执行业务SQL 在上述配置完成之后，执行一个需要分析的业务SQL，比如： select * from blog;\n3.2 获取业务SQL的事件ID 通过以下SQL先查询事件ID。\n1 2 3 4 5 6 7 SELECT EVENT_ID, TRUNCATE(TIMER_WAIT/1000000000000,6) as Duration, SQL_TEXT FROM performance_schema.events_statements_history_long WHERE SQL_TEXT like \u0026#39;%blog%\u0026#39;; +----------+----------+-------------------------+ | EVENT_ID | Duration | SQL_TEXT | +----------+----------+-------------------------+ | 243 | 0.002698 | select * from blog.blog | +----------+----------+-------------------------+ 1 row in set (0.00 sec) 3.3 根据事件ID，获取各阶段执行耗时 根据上一步获取的事件ID(EVENT_ID)，查询该SQL各个阶段的耗时情况。如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 SELECT event_name AS Stage, TRUNCATE(TIMER_WAIT/1000000000000,6) AS Duration FROM performance_schema.events_stages_history_long WHERE NESTING_EVENT_ID=243; +--------------------------------+----------+ | Stage | Duration | +--------------------------------+----------+ | stage/sql/starting | 0.000117 | | stage/sql/checking permissions | 0.000010 | | stage/sql/Opening tables | 0.000031 | | stage/sql/init | 0.000055 | | stage/sql/System lock | 0.000024 | | stage/sql/optimizing | 0.000003 | | stage/sql/statistics | 0.000020 | | stage/sql/preparing | 0.000015 | | stage/sql/executing | 0.000001 | | stage/sql/Sending data | 0.002321 | | stage/sql/end | 0.000004 | | stage/sql/query end | 0.000019 | | stage/sql/closing tables | 0.000018 | | stage/sql/freeing items | 0.000048 | | stage/sql/cleaning up | 0.000001 | +--------------------------------+----------+ 15 rows in set (0.00 sec) 上述结果与show profile的输出结果类似，能够看到每个阶段的耗时情况。相对于show profile来说，似乎更加繁琐，不过performance schema是MySQL未来性能分析的趋势，提供了非常丰富的性能诊断工具，熟悉performance schema的使用将有助于更好的优化MySQL。\n","permalink":"https://ktzxy.top/posts/ruspmrw6g4/","summary":"MySQL SQL性能分析 Profiling Using Performance Schema","title":"MySQL SQL性能分析 Profiling Using Performance Schema"},{"content":"Go语言发展简史 开发文档 https://studygolang.com/pkgdoc\nGo语言核心开发团队 Ken Thompson（肯·汤普森）：1983年图灵奖（Turing Award）和1998年美国国家技术奖（National Medal of Technology）得主。他与Dennis Ritchie是Unix的原创者。Thompson也发明了后来衍生出C语言的B程序语言，同时也是C语言的主要发明人。\nRob Pike（罗布-派克）：曾是贝尔实验室（Bell Labs）的Unix团队，和Plan 9操作系统计划的成员。 他与Thompson共事多年，并共创出广泛使用的UTF-8字元编码。\nRobert Griesemer：曾协助制作Java的HotSpot编译器，和Chrome浏览器的JavaScript引擎V8。\nGoogle为什么要创建Go 计算机硬件技术更新频繁，性能提高很快。目前主流的编程语言发展明显落后于硬件，不能合理利用多核多CPU的优势提升软件系统性能。 软件系统复杂度越来越高，维护成本越来越高，目前缺乏一个足够简洁高效的编程语言。 现有编程语言存在：风格不统一、计算能力不够、处理大并发不够好 企业运行维护很多c/c++的项目，c/c++程序运行速度虽然很快，但是编译速度确很慢，同时还存在内存泄漏的一系列的困扰需要解决。 Go语言发展历史 2007年，谷歌工程师Rob Pike，Ken Thompson和Robert Griesemer开始设计一门全新的语言，这是Go语言的最初原型。 2009年11月10日，Google将Go语言以开放源代码的方式向全球发布。 2015年8月19日，Go1.5版发布，本次更新中移除了”最后残余的c代码” 2017年2月17日，Go语言Go1.8版发布。 2017年8月24日，Go语言Go1.9版发布。 2018年2月16日，Go语言Go1.10版发布。 Go语言的特点 Go语言保证了既能到达静态编译语言的安全和性能，又达到了动态语言开发维护的高效率，使用一个表达式来形容Go语言：Go=C+Python，说明Go语言既有C静态语言程序的运行速度，又能达到Python动态语言的快速开发。\n从c语言中继承了很多理念，包括表达式语法，控制结构，基础数据类型，调用参数传值，指针等等，也保留了和C语言一样的编译执行方式及弱化的指针。 1 2 3 4 // go语言的指针使用特点 func testPtr(num *int) { *num = 20 } 引入包的概念，用于组织程序结构，Go语言的一个文件都要归属于一个包，而不能单独存在。 垃圾回收机制，内存自动回收，不需开发人员管理 【稍微不注意就会出现内存泄漏】 天然并发【重要特点】 从语言层面支持并发，实现简单 goroutine，轻量级线程，可实现大并发处理，高效利用多核。 基于CPS并发模型（Communicating Sequential Processes）实现 吸收了管道通信机制，形成go语言特有的管道channel，通过管道channel，可以实现不同的goroute之间的相互通信 函数返回多个值（实例代码） 新的创新：比如切片slice，延时执行defer等 Hello Go 我们写一个最简单的入门代码，在控制台输出hello go！\n1 2 3 4 5 6 package main // fmt包中提供格式化，输入和输出的函数 import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;hello go!\u0026#34;) } Golang执行流程分析 我们可以通过以下命令来进行操作\ngo build hello.go -\u0026gt; hello.exe go run hello.go 两种执行流程分析 如果我们先编译生成了可执行文件，那么我们可以将该可执行文件拷贝到没有go开发环境的机器上，然可以运行 如果我们是直接go run go源代码，那么如果要在另外一个机器上运行，也需要go开发环境，否则无法执行。 在编译时，编译器会将程序运行依赖的库文件包含在可执行文件中，所以，可执行文件变大了很多。 什么是编译 有了go源文件，通过编译器将其编译成机器可以识别的二进制码文件。 在该源文件目录下，通过go build 对hello.go文件进行编译。可以指定生成的可执行文件名，在windows下必须是.exe后缀。 如果程序没有错误，没有任何提示，会在当前目录下会出现一个可执行文件（windows下是.exe Linux下是一个可执行文件），该文件是二进制码文件，也是可以执行的程序。 如果程序有错误，编译时，会在错误的那行报错。 Go语言开发注意事项 Go源文件以“go”为扩展名 Go应用程序的执行入口是main()方法 Go语言严格区分大小写。 Go方法由一条条语句构成，每个语句后不需要分号（Go语言会在每行后自动加分号），这也体现出Golang的简洁性。 Go编译器是一行行进行编译的，因此我们一行就写一条语句，不能把多条语句写在同一个，否则报错 Go语言定义的变量或者import的包如果没有使用到，代码不能编译通过 大括号都是成对出现的，缺一不可。 Go语言中的转义字符 GoLang常用的转义字符（escape char）\n\\t：一个制表位，实现对齐的功能 \\n：换行符 \\：一个\\ \\r：一个回车 ","permalink":"https://ktzxy.top/posts/r84hta9o0r/","summary":"1 Go语言发展简史","title":"1 Go语言发展简史"},{"content":"Spring面试的连环炮 1、请谈谈你对SpringIOC的理解 IOC：控制反转，原来我们使用对象的时候是由使用者控制者控制的，有了Spring以后，可以将整个对象交给容器来帮我们进行管理。（IOC其实是一种理论思想），谈到IOC又不是不提及到它的实现方式DI（依赖注入），Spring中所有的bean都是通过反射生成的。\n用一句话形容就是==你不要打给我了，我会主动打给你==\nDI：依赖注入，将对应的属性注入到具体的对象中，注入类型有两种，一种是byType，一种是byName，在我们经常使用的注解有@Autowired是通过byType来实现注入的，需要注入相同类型的的对象的话，还要使用@Qualifier来区分，当@Resource不设置任何值时，isDefaultName会为true，当对应字段名称的bean或者BeanDefinition已存在时会走byName的形式，否则走byType的形式；populateBean（是对对象属性的一个填充）方法来完成属性注入。\n容器：存储对象，使用Map结构存储对象， 在Sping中储存对象的时候一般有三级缓存，singletonObjects存放完整的对象（一级缓存）；earlySingletonObjects存放半成品对象（二级缓存），singletonFactories用来存放lambda表达式和对象名称的映射（三级缓存），整个bean的生命周期，从创建到使用到销毁，各个环节都是由容器来帮我们控制的。\n2、简单描述下Spring Bean的生命周期 Spring容器帮助我们去管理对象，从对象的产生到销毁的环节都由容器来控制，其中主要包含实例化和初始化两个关键节点，当然在整个过程中会有一些扩展的存点。 下面来详细描述各个环节和步骤：\n1、实例化bean对象，通过反射的方式来生成，在源码里有一个createBeanInstance方法是专门生成对象的。(先去获取构造器反射clazz.getDeclaredConstructor()，然后通过构造器的反射对象去ctor.newInstance())\n2、当bean对象创建完成之后，对象的属性值都是默认值，所以要给bean填充属性，通过populateBean方法来完成对象属性的填充，==中间会设计到循环依赖的问题==。后面详说。\n3、向bean对象中设置容器属性，会调用invokeAwareMethods方法来将容器对象设置到具体的bean对象中\n4、调用BeanPostProcessor中的前置处理方法来进行bean对象的扩展工作，比如：ApplicationContextPostProcessor，EmbeddValueResolver等对象\n5、调用invokeInitMethods方法来完成初始化方法的调用，在此方法处理过程中，需要判断当前bean对象是否实现了InitializingBean接口，如果实现了调用afterPropertiesSet方法来最后设置bean对象。\n6、调用BeanPostProcessor的后置处理方法，完成对Bean对象的后置处理工作，AOP就是在此处实现的，实现的接口实现名字叫做AbstractAutoProxyCeator\n7、获取到完整对象，通过getBean的方式进行对象的获取和使用\n8、当对象使用完成后，容器在关闭的时候，会销毁对象，首先会判断是否实现了DispoableBean接口，然后去调用destoryMethod方法。\n3、BeanFactory和FactoryBean的区别 1、BeanFactory和FactoryBean都可以用来创建对象，只不过创建的方式和流程不同。\n2、当使用BeanFactory的时候，必须要严格的遵守Bean的生命周期，经过一系列繁杂的步骤之后才可以创建出单例对象，是流水线式的创建过程。\n3、而FactroyBean式用户可以自定义bean对象的创建过程，不需要按照bean的生命周期来创建，在此接口中包含了三个方法：\nisSingleton：判断是否式单例对象 getObjectType：获取对象的类型 getObject：在此方法中可以自己创建对象，使用new的方式或者使用代理的方式都可以，用户可以按照自己的需要去随意创建对象，在很多框架继承的时候都会实现FactoryBean接口，比如feign。 4、Sping中用到哪些设计模式 ==单例模式==：spring中bean都是单例\n==工厂模式==：BeanFactory\n模板方法：PostProcessorBeanFactory，onRefresh\n观察者模式：listener，event，multicast\n适配器模式：Adapter\n装饰器模式：BeanWrapper\n责任链模式：使用aop的时候会有一个责任链模式\n==代理模式==：aop动态代理\n建造者模式：builder\n==策略模式==：XmlBeanDefinitionReader，PropertiesBeanDefinitionReader\n5、applicationContext和BeanFactory的区别 BeanFactory式访问Spring容器的根接口，里面只是提供了某些基本方法的约束和规范，\n为了满足更多的需求，applicationContext实现了此接口，并在此接口的基础上做了某些扩展的功能，提供了更加丰富的api调用。\n一般我们使用的时候用applicationContext更多\n6、谈谈你对Spring循环依赖的理解 什么是循环依赖? 简单来说就是A依赖B，B依赖A\nSpring中bean对象的创建都要经历实例化和初始化（属性填充）的过程，通过将对象的状态分开，存在半成品和成品对象的方式，来分别进行初始化和实例化，成品和半成品在存储的时候需要分不同的缓存进行存储。\n==当持有了某个对象的引用之后，能否在后续的某个步骤中对该对象进行赋值操作？==\n**可以。**利用这点，我们可以将半成品对象放入容器，然后再逆向的去赋值。\n只有一级缓存行不行？ 不行，会把成品状态的bean对象和半成品对象的bean对象放到一起，而半成品对象是无法暴露给外部使用的，所以要将成品和半成品分开，一级缓存放成品对象，二级缓存放半成品对象。\n只有二级缓存行不行？ 如果整个应用程序中不涉及aop的存在，那么二级缓存足以解决循环依赖的问题，如果aop中存在了循环依赖，那么就必须使用三级缓存才能解决。\n为什么需要三级缓存？ 三级缓存的value类型是ObjectFactory，是一个函数式接口，不能直接进行调用，只有在调用getObject方法的时候才回去调用里面存储的lambda表达式，存在的意义就是保证整个容器在允许过程中同名的bean对象只能有一个。\n如果一个对象被代理，或者说需要生成代理对象，那么要不要先生成一个原始对象？ 是需要的\n当创建出代理对象之后，会同时存在代理对象和普通对象，此时我该用哪一个?\n程序是死的，他怎么知道先用谁后用谁呢？\n当需要代理对象的时候，或者说代理对象生成的时候，必须要覆盖原始对象，也就是说整个容器中，有且仅有一个同名的bean对象。\n在实际调用过程中，是没有办法来确定什么时候对象需要被调用，因此当某一个对象被调用的时候，优先判断当前对象是否需要被代理，类似于回调机制，当获取对象之后，根据传入的lambda表达式来确认返回的是哪一个确定的对象，如果条件符合，返回代理对象，如何不符合，返回原始对象。\n7、SpringAOP的底层原理 总：AOP叫面向切面编程，应用场景主要有日志，事务。底层原理是采用动态代理来实现的。\n分：bean的创建过程中有一个步骤可以对bean进行扩展实现，aop本身就是一个扩展功能，所以在BeanPostProcessor的后置处理方法中来实现\n1、代理对象创建过程（advice，切面，切点）\n2、通过jdk或者chlib的方式来生成代理对象\n3、在执行方法调用的时候，会调用到生成的字节码文件中，直到回找到DynamicAdvicerdInterceptor类中的intercept方法，从此方法开始执行。\n4、根据之前定义好的通知来生成拦截器链\n5、从拦截器链中依次获取每一个通知开始进行执行，在执行过程中，为了找到下一个通知是哪个，会有cglibMethodInvocation的对象，找的时候是从-1开始找并且执行的。\n8、Spring的事务是如何回滚的 这个问题等同于问你\nSpring的事务管理是如何实现的？\n总：spring的事务是由aop实现的，首先要生成具体的代理对象，然后按照aop的整套流程执行具体的操作逻辑，正常情况下要通过通知来完成核心功能，但是事务不是通过通知来实现的，而是通过一个TransactionInterceptor来实现的，然后调用invoke方法来实现具体的逻辑\n分：\n1、先做准备工作，解析各个方法上事务相关的属性，根据具体的属性来判断是否开启新事务\n2、当需要开启的时候，获取数据库连接，关闭自动提交功能，开启事务\n3、执行具体的sql逻辑操作\n4、在操作过程中，如果执行失败了，那么会通过completeTransactionAfterThrowing来完成事务的回滚操作，回滚的具体逻辑是通过doRollBack方法实现的，实现的时候也是要先获取连接对象，通过连接对象来回滚\n5、如果执行过程中，没有任何意外情况发生，那么通过commitTranscationAfterReturning来完成事务的提交操作，提交的具体逻辑是通过doCommit方法来实现的，实现的时候也要获取连接，通过连接对象来提交。\n6、当事务执行完毕后，需要清理相关事务的信息cleanupTransactionInfo来实现。\n9、谈一下Spring事务传播特性 传播特性有几种？ 在TransactionDefinition类中，Spring提供了7种传播属性\nREQUIRED : 如果当前已经存在事务，那么加入该事务，如果不存在事务，创建一个事务，这是默认的传播属性值。\nSUPPORTS：如果当前已经存在事务，那么加入该事务，否则创建一个所谓的空事务（可以认为无事务执行）\nMANDATORY：当前必须存在一个事务，否则抛出异常\nREQUIRES_NEW：如果当前存在事务，先把当前事务相关内容封装到一个实体，然后重新创建一个新事务，接受这个实体为参数，用于事务的恢复。\n更直白的说法就是暂停当前事务(当前无事务则不需要)，创建一个新事务。 针对这种情况，两个事务没有依赖关系，可以实现新事务回滚了，但外部事务继续执行\nNOT_SUPPORTED：如果当前存在事务，挂起当前事务，然后新的方法在没有事务的环境中执行，没有spring事务的环境下，sql的提交完全依赖于 defaultAutoCommit属性值\nNEVER：如果当前存在事务，则抛出异常，否则在无事务环境上执行代码\nNESTED：如果当前存在事务，则使用 SavePoint 技术把当前事务状态进行保存，然后底层共用一个连接，当NESTED内部出错的时候，自行回滚到 SavePoint这个状态，只要外部捕获到了异常，就可以继续进行外部的事务提交，而不会受到内嵌业务的干扰，但是，如果外部事务抛出了异常，整个大事务都会回滚。\n事务传播特性不会单问，只会出场景题。\n比如：某一个事务嵌套另一个事务的时候怎么办？\nA方法调用B方法，AB方法都有事务，并且传播特性不相同，那么A如果有异常，B怎么办，B如果有异常，A怎么办？\n总：事务的传播特性指的是不同方法的嵌套过程中，事务应该如何处理，是用同一个事务还是不同的事务，当出现异常的时候会回滚还是会提交，两个方法之间的相互影响，在日常工作中，使用的比较多的还是 ==required，required_new，nested==\n分：\n1、先说事务的不同分类，可以分为三类： 支持当前事务required，不支持当前事务required_new，嵌套事务nested\n2、如果外层方法是required , 内层方法是 required ，required_new , nested\n3、如果外层方法是required_new , 内层方法是 required ，required_new , nested\n4、如果外层方法是nested, 内层方法是 required ，required_new , nested\n==核心处理逻辑非常简单：==\n判断内外方法是否是同一个事务：\n​\t是：异常统一在外层方法处理\n​\t不是：内层方法有可能影响到外层方法，但是外层方法不会影响到内层方法。\n","permalink":"https://ktzxy.top/posts/bl5ldglzlu/","summary":"Spring面试的连环炮","title":"Spring面试的连环炮"},{"content":"﻿# 什么是Maven\n前无论使用IDEA还是Eclipse等其他IDE，使用里面ANT工具。ANT工具帮助我们进行编译，打包运行等工作。\nApache基于ANT进行了升级，研发出了全新的自动化构建工具Maven。\nMaven是Apache的一款开源的项目管理工具。\n以后无论是普通javase项目还是javaee项目，我们都创建的是Maven项目。\nMaven使用项目对象模型(POM-Project Object Model,项目对象模型)的概念,可以通过一小段描述信息来管理项目的构建，报告和文档的软件项目管理工具。在Maven中每个项目都相当于是一个对象，对象（项目）和对象（项目）之间是有关系的。关系包含了：依赖、继承、聚合，实现Maven项目可以更加方便的实现导jar包、拆分项目等效果。\nMaven下载安装 下载地址： http://maven.apache.org/\n目录结构：\nbin：存放的是执行文件，命令\n在IDEA中可以直接集成Maven:\nconf目录：下面有一个非常重要的配置文件\u0026ndash;》settings.xml\u0026mdash;》maven的核心配置文件/全局配置文件。\n如果没有.m2目录 ，自己手动执行mvn命令： mvn help:system\nMaven仓库 Maven仓库是基于简单文件系统存储的，集中化管理Java API资源（构件）的一个服务。\n仓库中的任何一个构件都有其唯一的坐标，根据这个坐标可以定义其在仓库中的唯一存储路径。得益于 Maven 的坐标机制，任何 Maven项目使用任何一个构件的方式都是完全相同的。\nMaven 可以在某个位置统一存储所有的 Maven 项目共享的构件，这个统一的位置就是仓库，项目构建完毕后生成的构件也可以安装或者部署到仓库中，供其它项目使用。 对于Maven来说，仓库分为两类：本地仓库和远程仓库。\n远程仓库 不在本机中的一切仓库，都是远程仓库：分为中央仓库 和 本地私服仓库 远程仓库指通过各种协议如file://和http://访问的其它类型的仓库。这些仓库可能是第三方搭建的真实的远程仓库，用来提供他们的构件下载（例如repo.maven.apache.org和uk.maven.org是Maven的中央仓库）。其它“远程”仓库可能是你的公司拥有的建立在文件或HTTP服务器上的内部仓库(不是Apache的那个中央仓库，而是你们公司的私服，你们自己在局域网搭建的maven仓库)，用来在开发团队间共享私有构件和管理发布的。\n默认的远程仓库使用的Apache提供的中央仓库： https://mvnrepository.com/\n本地仓库 本地仓库指本机的一份拷贝，用来缓存远程下载，包含你尚未发布的临时构件。\n仓库配置 本地仓库配置\n本地仓库是开发者本地电脑中的一个目录，用于缓存从远程仓库下载的构件。默认的本地仓库是${user.home}/.m2/repository。用户可使用settings.xml文件修改本地仓库。\n镜像仓库配置\n如果仓库A可以提供仓库B存储的所有内容，那么就可以认为A是B的一个镜像。例如：在国内直接连接中央仓库下载依赖，由于一些特殊原因下载速度非常慢。这时，我们可以使用阿里云提供的镜像http://maven.aliyun.com/nexus/content/groups/public/来替换中央仓库http://repol.maven.org/maven2/。修改maven的setting.xml文件\n1 2 3 4 5 6 7 8 9 10 \u0026lt;mirror\u0026gt; \u0026lt;!-- 指定镜像ID（可自己改名） --\u0026gt; \u0026lt;id\u0026gt;nexus-aliyun\u0026lt;/id\u0026gt; \u0026lt;!-- 匹配中央仓库（阿里云的仓库名称，不可以自己起名，必须这么写）--\u0026gt; \u0026lt;mirrorOf\u0026gt;central\u0026lt;/mirrorOf\u0026gt; \u0026lt;!-- 指定镜像名称（可自己改名） --\u0026gt; \u0026lt;name\u0026gt;Nexus aliyun\u0026lt;/name\u0026gt; \u0026lt;!-- 指定镜像路径（镜像地址） --\u0026gt; \u0026lt;url\u0026gt;http://maven.aliyun.com/nexus/content/groups/public\u0026lt;/url\u0026gt; \u0026lt;/mirror\u0026gt; 仓库优先级别 JDK配置 当你的idea中有多个jdk的时候，就需要指定你编译和运行的jdk： 在settings.xml中配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;profile\u0026gt; \u0026lt;!-- settings.xml中的id不能随便起的 --\u0026gt; \u0026lt;!-- 告诉maven我们用jdk1.8 --\u0026gt; \u0026lt;id\u0026gt;jdk-1.8\u0026lt;/id\u0026gt; \u0026lt;!-- 开启JDK的使用 --\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;activeByDefault\u0026gt;true\u0026lt;/activeByDefault\u0026gt; \u0026lt;jdk\u0026gt;1.8\u0026lt;/jdk\u0026gt; \u0026lt;/activation\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;!-- 配置编译器信息 --\u0026gt; \u0026lt;maven.compiler.source\u0026gt;1.8\u0026lt;/maven.compiler.source\u0026gt; \u0026lt;maven.compiler.target\u0026gt;1.8\u0026lt;/maven.compiler.target\u0026gt; \u0026lt;maven.compiler.compilerVersion\u0026gt;1.8\u0026lt;/maven.compiler.compilerVersion\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;/profile\u0026gt; Maven工程类型 【1】POM工程 POM工程是逻辑工程。用在父级工程或聚合工程中。用来做jar包的版本控制。\n【2】JAR工程 将会打包成jar，用作jar包使用。即常见的本地工程 \u0026mdash;\u0026gt; Java Project。\n【3】WAR工程 将会打包成war，发布在服务器上的工程。\n创建工程 标准目录结构\n❀src/main/java\n这个目录下储存java源代码\n❀src/main/resources 储存主要的资源文件。比如xml配置文件和properties文件\n❀src/test/java 储存测试用的类，比如JUNIT的测试一般就放在这个目录下面 因为测试类本身实际是不属于项目的，所以放在任何一个包下都显得很尴尬，所以maven专门创建了一个测试包 用于存放测试的类\n❀src/test/resources 可以自己创建，储存测试环境用的资源文件\n❀src 包含了项目所有的源代码和资源文件，以及其他项目相关的文件。\n❀target 编译后内容放置的文件夹\n❀pom.xml 是Maven的基础配置文件。配置项目和项目之间关系，包括配置依赖关系等等\n结构图： \u0026ndash;MavenDemo 项目名 \u0026ndash;.idea 项目的配置，自动生成的，无需关注。 \u0026ndash;src \u0026ndash; main 实际开发内容 \u0026ndash;java 写包和java代码，此文件默认只编译.java文件 \u0026ndash;resource 所有配置文件。最终编译把配置文件放入到classpath中。 \u0026ndash; test 测试时使用，自己写测试类或junit工具等 \u0026ndash;java 储存测试用的类 pom.xml 整个maven项目所有配置内容。\n注意：目录名字不可以随便改，因为maven进行编译或者jar包生成操作的时候，是根据这个目录结构来找的，你若轻易动，那么久找不到了。\n工程关系 依赖 【1】依赖关系： 即A工程开发或运行过程中需要B工程提供支持，则代表A工程依赖B工程。\n在这种情况下，需要在A项目的pom.xml文件中增加下属配置定义依赖关系。\n通俗理解：就是导jar包。\nB工程可以是自己的项目打包后的jar包，也可以是中央仓库的jar包。\n【2】如何注入依赖呢？\n在pom.xml文件 根元素project下的 dependencies标签中，配置依赖信息，内可以包含多个 dependence元素，以声明多个依赖。每个依赖dependence标签都应该包含以下元素：groupId, artifactId, version : 依赖的基本坐标， 对于任何一个依赖来说，基本坐标是最重要的， Maven根据坐标才能找到需要的依赖。\n【3】依赖的好处：\n省去了程序员手动添加jar包的操作，省事！！\n可以帮我们解决jar包冲突问题。\n【4】依赖的传递性\n传递性依赖是Maven2.0的新特性。假设你的项目依赖于一个库，而这个库又依赖于其他库。你不必自己去找出所有这些依赖，你只需要加上你直接依赖的库，Maven会隐式的把这些库间接依赖的库也加入到你的项目中。这个特性是靠解析从远程仓库中获取的依赖库的项目文件实现的。一般的，这些项目的所有依赖都会加入到项目中，或者从父项目继承，或者通过传递性依赖。\n如果A依赖了B，那么C依赖A时会自动把A和B都导入进来。\n创建A项目后，选择IDEA最右侧Maven面板lifecycle，双击install后就会把项目安装到本地仓库中，其他项目就可以通过坐标引用此项目。\n【5】原则\n第一原则：最短路径优先原则 “最短路径优先”意味着项目依赖关系树中路径最短的版本会被使用。\n例如，假设A、B、C之间的依赖关系是A-\u0026gt;B-\u0026gt;C-\u0026gt;D(2.0) 和A-\u0026gt;E-\u0026gt;(D1.0)，那么D(1.0)会被使用，因为A通过E到D的路径更短。\n第二原则：最先声明原则\n依赖路径长度是一样的的时候，第一原则不能解决所有问题，比如这样的依赖关系：A–\u0026gt;B–\u0026gt;Y(1.0)，A–\u0026gt;C–\u0026gt;Y(2.0)，Y(1.0)和Y(2.0)的依赖路径长度是一样的，都为2。那么到底谁会被解析使用呢？在maven2.0.8及之前的版本中，这是不确定的，但是maven2.0.9开始，为了尽可能避免构建的不确定性，maven定义了依赖调解的第二原则：第一声明者优先。在依赖路径长度相等的前提下，在POM中依赖声明的顺序决定了谁会被解析使用。顺序最靠前的那个依赖优胜。\n【6】排除依赖\nexclusions： 用来排除传递性依赖 其中可配置多个exclusion标签，每个exclusion标签里面对应的有groupId, artifactId, version三项基本元素。注意：不用写版本号。\n【7】依赖范围\n依赖范围就决定了你依赖的坐标 在什么情况下有效，什么情况下无效： ❀compile 这是默认范围。如果没有指定，就会使用该依赖范围。表示该依赖在编译和运行时都生效。\n❀provided 已提供依赖范围。使用此依赖范围的Maven依赖。典型的例子是servlet-api，编译和测试项目的时候需要该依赖，但在运行项目的时候，由于容器已经提供，就不需要Maven重复地引入一遍(如：servlet-api)\n❀runtime runtime范围表明编译时不需要生效，而只在运行时生效。典型的例子是JDBC驱动实现，项目主代码的编译只需要JDK提供的JDBC接口，只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC驱动。\n❀system 系统范围与provided类似，不过你必须显式指定一个本地系统路径的JAR，此类依赖应该一直有效，Maven也不会去仓库中寻找它。但是，使用system范围依赖时必须通过systemPath元素显式地指定依赖文件的路径。\n❀test test范围表明使用此依赖范围的依赖，只在编译测试代码和运行测试的时候需要，应用的正常运行不需要此类依赖。典型的例子就是JUnit，它只有在编译测试代码及运行测试的时候才需要。Junit的jar包就在测试阶段用就行了，你导出项目的时候没有必要把junit的东西到处去了就，所在在junit坐标下加入scope-test\n❀Import import范围只适用于pom文件中的部分。表明指定的POM必须使用部分的依赖。 注意：import只能用在dependencyManagement的scope里。\n如果父工程中加入score-import 相当于强制的指定了版本号：\n继承 【1】继承关系： 如果A工程继承B工程，则代表A工程默认依赖B工程依赖的所有资源，且可以应用B工程中定义的所有资源信息。\n被继承的工程（B工程）只能是POM工程。\n注意：在父项目中放在中的内容时不被子项目继承，不可以直接使用\n放在中的内容主要目的是进行版本管理。里面的内容在子项目中依赖时坐标只需要填写\n和即可。（注意：如果子项目不希望使用父项目的版本，可以明确配置version）。\n父工程是一个POM工程：\n创建子工程：\n本质上：POM文件的继承\n聚合 当我们开发的工程拥有2个以上模块的时候，每个模块都是一个独立的功能集合。比如某大学系统中拥有搜索平台，学习平台，考试平台等。开发的时候每个平台都可以独立编译，测试，运行。这个时候我们就需要一个聚合工程。\n在创建聚合工程的过程中，总的工程必须是一个POM工程（Maven Project）（聚合项目必须是一个pom类型的项目，jar项目war项目是没有办法做聚合工程的），各子模块可以是任意类型模块（Maven Module）。\n前提：继承。\n聚合包含了继承的特性。\n聚合时多个项目的本质还是一个项目。这些项目被一个大的父项目包含。且这时父项目类型为pom类型。同时在父项目的pom.xml中出现表示包含的所有子模块。\n总项目：POM工程\n具体模块：\n常用插件 编译器插件 通过编译器插件，我们可以配置使用的JDK或者说编译器的版本：\n配置编译器插件：pom.xml配置片段\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;!-- 配置maven的编译插件 --\u0026gt; \u0026lt;build\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;!--JDK编译插件 --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;!--插件坐标 --\u0026gt; \u0026lt;groupId\u0026gt;org.apache.maven.plugins\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;maven-compiler-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.2\u0026lt;/version\u0026gt; \u0026lt;!-- --\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- 源代码使用JDK版本--\u0026gt; \u0026lt;source\u0026gt;1.7\u0026lt;/source\u0026gt; \u0026lt;!-- 源代码编译为class文件的版本，要保持跟上面版本一致--\u0026gt; \u0026lt;target\u0026gt;1.7\u0026lt;/target\u0026gt; \u0026lt;encoding\u0026gt;UTF-8\u0026lt;/encoding\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/build\u0026gt; 资源拷贝插件 Maven在打包时默认只将src/main/resources里的配置文件拷贝到项目中并做打包处理，而非resource目录下的配置文件在打包时不会添加到项目中。\n我们的配置文件，一般都放在：src/main/resources\n然后打包后配置文件就会在target的classes下面放着：\n一般打包只有放在resources目录下的配置文件才会被打包放入classes下面\n我现在想把非resources下面的文件也打包到classes下面：\n需要配置：\npom.xml配置片段：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;build\u0026gt; \u0026lt;resources\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;directory\u0026gt;src/main/java\u0026lt;/directory\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;include\u0026gt;**/*.xml\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;directory\u0026gt;src/main/resources\u0026lt;/directory\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;include\u0026gt;**/*.xml\u0026lt;/include\u0026gt; \u0026lt;include\u0026gt;**/*.properties\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;/resources\u0026gt; \u0026lt;/build\u0026gt; tomcat插件 我们如果创建war项目，必然要部署在服务器上，方式： （1）部署在远程服务器上\n（2）将IDEA和外部tomcat产生关联，然后将项目部署在外部tomcat上\n现在学习一个新的方式，不再依赖外部的tomcat，maven提供了tomcat插件，我们可以配置来使用。\n创建web项目：war项目：\n在index.jsp中写点东西用来测试tomcat：\n使用Tomcat插件发布部署并执行war工程的时候，需要使用启动命令，启动命令为： tomcat7:run。命令中的tomcat7是插件命名，由插件提供商决定。run为插件中的具体功能。\n具体pom.xml文件的配置如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;plugins\u0026gt; \u0026lt;!-- 配置Tomcat插件 --\u0026gt; \u0026lt;plugin\u0026gt; \u0026lt;groupId\u0026gt;org.apache.tomcat.maven\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;tomcat7-maven-plugin\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.2\u0026lt;/version\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- 配置Tomcat监听端口 --\u0026gt; \u0026lt;port\u0026gt;8080\u0026lt;/port\u0026gt; \u0026lt;!-- 配置项目的访问路径(Application Context) --\u0026gt; \u0026lt;path\u0026gt;/\u0026lt;/path\u0026gt; \u0026lt;/configuration\u0026gt; \u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; 执行命令：\n执行成功：\n访问：\n常用命令 ❀ install\n本地安装， 包含编译，打包，安装到本地仓库\n编译 - javac\n打包 - jar， 将java代码打包为jar文件\n安装到本地仓库 - 将打包的jar文件，保存到本地仓库目录中。\n❀ clean\n清除已编译信息。\n删除工程中的target目录。\n❀ compile\n只编译。 javac命令\n❀ package\n打包。 包含编译，打包两个功能。\ninstall和package的区别：\npackage命令完成了项目编译、单元测试、打包功能，但没有把打好的可执行jar包（war包或其它形式的包）布署到本地maven仓库和远程maven私服仓库\ninstall命令完成了项目编译、单元测试、打包功能，同时把打好的可执行jar包（war包或其它形式的包）布署到本地maven仓库，但没有布署到远程maven私服仓库\nTts-1613557550071)]\n常用命令 ❀ install\n本地安装， 包含编译，打包，安装到本地仓库\n编译 - javac\n打包 - jar， 将java代码打包为jar文件\n安装到本地仓库 - 将打包的jar文件，保存到本地仓库目录中。\n❀ clean\n清除已编译信息。\n删除工程中的target目录。\n❀ compile\n只编译。 javac命令\n❀ package\n打包。 包含编译，打包两个功能。\ninstall和package的区别：\npackage命令完成了项目编译、单元测试、打包功能，但没有把打好的可执行jar包（war包或其它形式的包）布署到本地maven仓库和远程maven私服仓库\ninstall命令完成了项目编译、单元测试、打包功能，同时把打好的可执行jar包（war包或其它形式的包）布署到本地maven仓库，但没有布署到远程maven私服仓库\n","permalink":"https://ktzxy.top/posts/9f8obz7stw/","summary":"Maven学习","title":"Maven学习"},{"content":"Oracle数据库速查知识文档 项目介绍 该项目记录了Oracle相关的速查知识汇总，主要涉及了oracle基础使用、SQL基础、oracle函数、oracle触发器、oracle高级查询、PL/SQL编程基础、PL/SQL存储过程等。若有新增，还将不断添加中。\nSQL基础部分 1.简介 Oracle Database，又名Oracle RDBMS，或简称Oracle，是甲骨文公司的一款关系数据库管理系统。本课程主要介绍Oracle的SQL基础，包括表空间的概念，如何登录Oracle数据库，如何管理表及表中的数据，以及约束的应用。为后续课程的学习打下一个良好的基础。\n2.安装好之后可以登录系统账户 打开sqlplus，输入用户名system或sys（后者有最高权限）和自己设置的口令就可以登录了。\n3.用户与表空间 系统用户有哪些？\nsys,system 前者高于后者，前者必须以管理员权限登录 sysman 操作企业管理的用户，也是管理员用户\nscott 普通用户\n前三者用户的密码是安装时设置的，scott的默认密码是tiger\n登录通用语句：\n1 [username/password][@server][as sysdba|sysoper] 如果是远程登录，则需要输入IP地址。\n也可以在前面加一个connect，比如connect as sysdba;\n4.数据字典 （1）数据字典介绍 数据字典是Oracle存放有关数据库信息的地方，其用途是用来描述数据的。比如一个表的创建者信息，创建时间信息，所属表空间信息，用户访问权限信息等。当用户在对数据库中的数据进行操作时遇到困难就可以访问数据字典来查看详细的信息。\ndba_开头的是管理员才能查看的数据字典，users_开头的是都能查看的数据字典。\n数据字典或表前加上desc可以查看他们的结构。\n比如desc dba_users查看数据字典的信息。\n1 select username from dba_users; 可以从数据字典里面查看用户的名字。\n（2）查看用户的数据字典dba_users dba_users、user_users用来查看不同权限级别的数据字典。使用示例如：\n1 select default_tablespace,temporary_tablespace from dba_users where username=\u0026#39;SYSTEM\u0026#39;; （3）数据字典dba_data_files，查看数据文件的 1 select file_name from dba_data_files where tablespace_name = \u0026#39;TEST1_TABLESPACE\u0026#39;; 得到结果：\n1 2 3 FILE_NAME -------------------------------------------------------------------------------- D:\\APP\\HP\\PRODUCT\\11.2.0\\DBHOME_1\\DATABASE\\TEST1FILE.DBF 类似的字典 dba_temp_files（查看临时表空间文件的）\n5.如何启用scott用户 默认情况下是锁定的。 启用语句：\n1 alter user username(可以替换) account unlock; 6.表空间 （1）表空间介绍 表空间就是在数据库中开辟的一个空间用来存储我们的数据库对象，一个数据库可以由多个表空间构成。\n表空间由一个或多个数据文件构成，大小可以由用户来定义。\n表空间的分类：\n永久表空间 临时表空间 UNDO表空间 永久表空间主要存储数据库中要永久存储的对象，表、视图、存储过程。临时表空间存储数据库操作当中中间执行的过程，执行结束之后自动释放掉，不进行永久性保存。UNDO表空间用于保存事务所修改数据的旧值，可以做回滚操作。\n（2）如何查看用户的表空间？ 数据字典：dba_tablespaces（针对管理员级别的用户）、user_tablespaces（针对普通用户的数据字典）\ndba的表空间中，system用于管理员，也叫系统表空间，sysaux为示例的辅助表空间，undotbs1用于存储撤销信息的，temp用于存储处理的表和索引信息的临时表空间，users用于存储用户创建的数据库对象，example表空间等。\n（3）如何设置用户的默认或者临时表空间 1 alter user username default|temporary tablespace tablespace_name; 1 alter user system default tablespace system; （4）创建表空间 1 create [temporary] tabalspace tablespace_name tempfile|datafile \u0026#39;xx.dbf\u0026#39; size xx; 1 create tablespace test1_tablespace datafile \u0026#39;test1file.dbf\u0026#39; size 10M; 1 create temporary tablespace temptest1_tablespace tempfile \u0026#39;tempfile1.dbf\u0026#39; size 10M; （5）修改表空间的状态 设置在线离线状态 1 alter tablespace tablespace_name online|offline; --脱机状态不能使用它 dba_tablespaces字典下面的status状态可以查看状态。\n1 select status from dba_tablespaces where tablespace_name = \u0026#39;TEST1_TABLESPACE\u0026#39;; 设置只读或可读写状态，一般是read write可读写的状态 1 alter tablespace tablespace_name read only|read write; --前提是表空间是一定是在线状态 （6）增加数据文件 1 alter tablespace tablespace_name add datafile \u0026#39;xx.dbf\u0026#39; size xx; 添加后使用dba_data_files来查询。\n1 select file_name from dba_data_files where tablespace_name=\u0026#39;TEST1_TABLESPACE\u0026#39;; （7）删除数据文件 1 alter tablespace tablespace_name drop datafile \u0026#39;xx.dbf\u0026#39;; --注意不能删除创建表空间时候的第一个文件，如果需要删除则必须要把表空间删掉。 （8）删除表空间 1 drop tablespace tablespace_name [including contents]; --如果需要把数据文件也删除则把后面加上。 7.数据表 （1）表的介绍 表是基本存储单位，要把数据都存在表中，oracle中的表都是二维结构。 一行也可以叫做记录，一列也可以叫做域或者字段。 约定：要求每一列需要有相同的数据类型。 列名要是唯一的。 每一行的数据是唯一的。\n（2）表中的数据类型 字符型 固定长度类型：char(n)（max2000）,nchar(n)（unicode格式，max1000，多数用来存储汉字） 可变长度类型：varchar2(n)（max4000）,nvarchar2(n)（unicode格式，max2000） 数值型 number(p,s) p为有效数字，s为小数点后面的位数，s可正可负 float(n) 用来存储二进制数据，二进制数据的1-126位，一般使用number 日期型 date 范围为公元前4712年1月1日到公元9999年12月31日，可以精确到秒 timestamp 可以精确到小数秒，一般用date类型 其他类型 blob 可存4G数据以二进制存放 clob 可存4G数据以字符串存放 （3）如何管理表 创建表： 1 2 3 create table table_name( --在同一个用户下表明要是唯一的。 column_name datatype,... ); 如\n1 2 3 4 5 6 create table userinfo (id number(6,0), username varchar2(20), userpwd varchar2(20), email varchar2(30), regdate date); 创建完之后如果需要查看字段的信息使用desc userinfo即可查看。\n（4）如何修改表的结构 添加字段 1 alter table table_name add column_name datatype; 如\n1 alter table userinfo add remarks varchar2(500); 更改字段数据类型 1 alter table table_name modify column_name datatype; --改长度或者更换数据类型 如\n1 alter table userinfo modify remarks varchar2(400); 删除字段 1 alter table table_name drop column column_name; 修改字段名 1 alter table table_name rename column column_name to new_column_name; 修改表名 1 rename table_name to new_table_name; （5）删除表 1 truncate table table_name; --删除表中的全部数据，没有删除表，比delete快很多。 1 drop table table_name; 数据和结构都删掉。 （6）操作表中的数据 添加数据 1 insert into table_name (column1,column2,...) values (value1,value2,...); --如果是所有字段都添加值，则表明后面的小括号可以省略。 如\n1 insert into userinfo values (1,\u0026#39;xxx\u0026#39;,\u0026#39;123\u0026#39;,\u0026#39;xxx@126.com\u0026#39;,sysdate); --sysdate可以获取当前系统日期 查询select * from userinfo; 又如\n1 insert into userinfo (id,username,userpwd) values (2,\u0026#39;yyy\u0026#39;,\u0026#39;123\u0026#39;); （7）设置某字段的默认值 创建表时添加 如\n1 create table userinfo1(id number(6,0),regdate date default sysdate); --使用default关键词，虽然指定了默认值，但是在添加的时候还是要指定字段名才行 在创建表以后添加默认值：\n1 alter table userinfo modify email default \u0026#39;无\u0026#39;; --如果在新加记录的时候不想要默认值了，则按正常的添加方式添加了就可以替换默认值了 （8）复制表数据 在建表时复制 1 create table table_new as select column1,...|* from table_old; --可以选择要复制的字段也可以复制所有 如：\n1 create table userinfo_new as select * from userinfo; --userinfo是被复制的表 部分字段复制如 1 create table userinfo_new1 as select id,username from userinfo; 在添加时复制 1 insert into table_new [(column1,...)] select column1,...|* from table_old; --顺序和数据类型要完全一致 如\n1 insert into userinfo_new select * from userinfo; 又如\n1 insert into userinfo_new (id,username) select id,username from userinfo; （9）修改表的数据 1 update tabel_name set column1=value1,...[where conditions]; 无条件更新： 1 update userinfo set userpwd = \u0026#39;111111\u0026#39;; 有条件的更新： 1 update userinfo set userpwd=\u0026#39;123456\u0026#39; where username =\u0026#39;xxx\u0026#39;; （10）删除数据 只能以行为单位来删除数据 1 2 delete from table_name; --删除全部数据，效率慢些 delete from table_name [where conditions]; 无条件删除 1 delete from testdel; 有条件的删除 1 delete from userinfo where username=\u0026#39;yyy\u0026#39;; 8.约束 （1）约束的介绍 约束的作用是定义规则（最重要），确保完整性。\n（2）约束的种类 非空约束 主键约束 外键约束 唯一约束 检查约束 （3）非空约束 在创建表时设置非空约束： 1 create table table_name(column_name datatype not null,...); 如：\n1 2 3 create table userinfo_1 (id number(6,0), username varchar2(20) not null, userpwd varchar2(20) not null); 在修改表时添加非空约束： 1 alter table tabel_name modify column_name datatype not null; 如\n1 alter table userinfo modify username varchar2(20) not null; --在修改之前表里面不要有任何数据 在修改表时去除非空约束： 1 alter table tabel_name modify column_name datatype null; （4）主键约束 必不可少，确定每一行数据的唯一性。\n一张表只能设计一个主键约束。\n主键约束可以由多个字段构成，称为联合主键或者复合主键。\n在创建表时设置主键约束： 1 create table table_name(column_name datatype primary key,...); 如\n1 2 3 create table userinfo_p(id number(6,0) primary key, username varchar2(20), userpwd varchar2(20)); 如果用约束的话：\n1 constraint constraint_name primary key (column_name1,...); --一般用来创建联合主键 如\n1 2 3 4 create table userinfo_p1(id number(6,0), username varchar2(20), userpwd varchar2(20), constraint pk_id_username primary key(id,username)); 如果忘记了约束的名字，可以到user_constraints数据字典查询constraint_name. 如\n1 select constraint_name from user_constraints where table_name=\u0026#39;USERINFO_P1\u0026#39;; 如果没有用约束来创建主键，则系统会自动命名约束的名称，可以看这个：\n1 select constraint_name from user_constraints where table_name=\u0026#39;USERINFO_P\u0026#39;; 结果为：\n1 2 3 CONSTRAINT_NAME ------------------------------ SYS_C0011189 在修改表时添加主键约束： 1 2 add constraint constraint_name primary key(column_name1,...); --主键名一般以pk_开头 alter table userinfo add constraint pk_id primary key(id); --设置约束之前，如果已经有值了，必须唯一，且不能为空。 更改约束的名称，可以修改任何约束的名字 1 alter table table_name rename constraint old_name to new_name; 删除主键约束： 1 alter table table_name disable|enable constraint constraint_name; --禁用|启用约束，不删除 查看状态：\n1 select constraint_name,status from user_constraints where table_name=\u0026#39;USERINFO\u0026#39;; 如果是完全删除： 1 alter table table_name drop constraint constraint_name; 还有一种方法： 1 drop primary key[cascade]; --删除主键约束，如果存在外键约束，填写cascade，可以把其他表中引用该主键约束的一起删掉 （5）外键约束 两个表之中字段关系的约束。\n在创建表的时候设置外键约束： 1 2 3 --分开创建时 create table table1(column_name datatype references table2(column_name),...); --table2为主表，table1为从表，也叫主从表。主表当中的字段必须是主表中的主键字段，主从表的字段要设置成同一个数据类型。在向设置了外键约束的表输入值的时候，从表中外键字段的值必须来自主表中的相应字段的值，或者为null值。 如创建主表： 1 2 create table typeinfo(typeid varchar2(10) primary key, typename varchar2(20)); 创建从表： 1 2 3 create table userinfo_f(id varchar2(10) primary key, username varchar2(20), typeid_new varchar2(10) references typeinfo(typeid)); 然后给主表插入数据： 1 insert into typeinfo values(1,1); 如果这样给从表插入数据： 1 insert into userinfo_f(id,typeid_new) values (1,2); 则2在主表中没有找到，会报错。需要填写\n1 insert into userinfo_f(id,typeid_new) values (1,1); 才可以，或者那个部分留空值：\n1 insert into userinfo_f(id,typeid_new) values (2,null); 在创建表的时候设置外键约束： 1 2 3 --定义完所有的字段之后设置的约束 constraint constraint_name foreign key(column_name) references table_name(column_name) [on delete cascade]; --后面的中括号是级联删除，表示主表当中的一条数据被删除的时候，从表当中使用了这条数据的字段所在的行也会被一起删除掉，这样确保了主从表数据的完整性。 如：\n1 2 3 4 create table userinfo_f2 (id varchar2(10) primary key, username varchar2(20), typeid_new varchar2(10), constraint fk_typeid_new foreign key (typeid_new) references typeinfo(typeid)); 如果添加级联删除： 1 2 3 4 create table userinfo_f3 (id varchar2(10) primary key, username varchar2(20), typeid_new varchar2(10), constraint fk_typeid_new1 foreign key (typeid_new) references typeinfo(typeid) on delete cascade); 在修改表时添加外键约束： 1 alter table tabel_name add constraint constraint_name foreign key(column_name) references table_name(column_name) [on dedelete cascade]; 删除外键约束 禁用外键约束： 1 disable|enable constraint constraint_name; 彻底删除外键约束： 1 drop constraint constraint_name; （6）唯一约束 作用是保证字段的唯一性，和主键约束的区别是，主键约束必须是非空的，而唯一约束允许有一个空值。主键约束在一张表中只能有一个，唯一约束可以有多个。\n在创建表时设置唯一约束： 1 create table tabel_name(column_name datatype unique,...); 在表级设置唯一约束： 1 constraint constraint_name unique(column_name); --如果需要设置多个字段为唯一约束，要写多个constraint子句。 在修改表时添加唯一约束： 1 add constraint constraint_name unique(column_name); 删除唯一约束： 禁用：\n1 disable|enable constraint constraint_name; 完全删除：\n1 drop constraint constraint_name; （7）检查约束 检查约束，让表当中的值更具有实际意义，能够满足一定的条件，具有实际意义。\n在创建表时设置检查约束： 1 create table tabel_name(column_name datatype check(expression),...); 如：\n1 2 3 create table userinfo_c (id varchar2(10) primary key, username varchar2(20), salary number(5,0) check(salary\u0026gt;0)); 比如输入insert into userinfo_c values(1,'aaa',-50);就会报错。\n在表级设置检查约束： 1 constraint constraint_name check(expression); 如：\n1 2 3 4 create table userinfo_c1(id varchar2(10) primary key, username varchar2(20), salary number(5,0), constraint ck_salary check(salary\u0026gt;0)); 在修改表时添加检查约束： 1 add constraint constraint_name check(expression); 删除检查约束： 禁用：\n1 disable|enable constraint constraint_name; 删除：\n1 drop constraint constraint_name; （8）总结五个约束 非空约束 主键约束：每张表只能有一个，可以由多个字段构成 外键约束：涉及两个表之间的关系 唯一约束 检查约束 在创建表时设置约束： 只有非空约束只能在列级设置约束，不能在表级设置约束，其他的都是两者都可以的。非空约束是没有名字的。\n在修改表时添加约束，也是只有非空约束不同，修改表时用的语句是\n1 alter table talbe_name modify column_name datatype not null; 更改约束的名称：数据字典（user_constraints查看名称）\n1 rename constraint old_name to new_name; 删除约束，非空约束较特殊\n1 alter table tabel_name modify column_name datatype null; 其他的如果是禁用的话使用\n1 disable|enable constraint constraint_name; 如果要永久删除可以用\n1 drop constraint constraint_name; 删除主键约束还能用\n1 drop primary key; 9.基本查询 （1）查询基本语句 1 select [distinct] column_name1,...|* from table_name [where conditions]; --distinct可以不显示重复的行。 （2）在SQL*PLUS中设置格式 1 column column_name heading new_name; 如\n1 2 col username heading 用户名; --执行成功的话不会有回显 --column可以简写成col，设置新的字段名（别名），使用select语句来查询的时候就可以看到变化了，但使用desc看结构还依然不变化。 设置结果显示的格式：\n1 column column_name format dataformat; 注意：字符类型只能设置它的长度。 \u0026ndash;字符格式用a开头，后面跟它要的长度。 如\n1 col username format a10; 如果是数值类型用，9表示一位数字，比如\n1 col salary format 9999.9; 可以保留4位数和一位小数。 如果\n1 col salary format 999.9; 但如果数据中有四位的数，超过这个长度的就用#####表示了，与excel一致。\n如果使用col salary format $9999.9;则数字前面加了美元符号。\n清除之前设置过的格式：\n1 column column_name clear; 如\n1 col salary clear; （3）查询表中的所有字段 1 select * from table_name; 查询指定的字段：比如\n1 select username,salary from users; （4）给字段设置别名 不会更改字段的名字，可以为多个字段设置别名\n1 select column_name as new_name,... from table_name; --其中as可以省略，但最好加上 如\n1 select id as 编号, username as 用户名, salary 工资 from users; 查看唯一值：\n1 select distinct username as 用户名 from users; （5）运算符和表达式 运算符大家都比较熟悉了，而表达式=操作数+运算符组成。\noracle中的操作数可以有变量、常量、字段。\n运算符有算术运算符（+、-、*、/），比较运算符（\u0026gt;,\u0026gt;=,\u0026lt;,\u0026lt;=,=,\u0026lt;\u0026gt;都是用在where条件里面的，两个数进行比较得到的结果是布尔类型的，真或者假），逻辑运算符（and,or,not）\n在select语句中使用运算符 在查询结果中，给每个员工的工资加上200元，但数据本身没变。 如\n1 select id,username,salary+200 from users; 使用比较运算符：\n查询工资高于800元的员工的姓名； 如\n1 select username from users where salary \u0026gt; 800; 使用逻辑运算符： 如\n1 2 select username from users where salary \u0026gt; 800 and salary \u0026lt;\u0026gt;1800.5; select username from users where salary \u0026gt; 800 or salary \u0026lt;\u0026gt;1800.5; （6）带条件的查询 如\n1 2 select salary from users where username=\u0026#39;aaa\u0026#39;; select username,salary from users where id=3; 多条件如\n1 select * from users where username=\u0026#39;aaa\u0026#39; or salary\u0026lt;=2000 and salary\u0026gt;800; 逻辑运算符的优先级顺序：not,and,or\n比较运算符优先级高于逻辑运算符\nnot的例子：\n1 select * from users where not(username=\u0026#39;aaa\u0026#39;); （7）模糊查询 like关键字，也可以归入比较运算符当中。\n通配符的使用（_表示一个字符，%表示0到多个任意字符） 如\n1 2 select * from users where username like \u0026#39;a%\u0026#39;; --以a开头的行 select username from users where username like \u0026#39;%a%\u0026#39;; --含有a的 （8）范围查询 between\u0026hellip;and \u0026ndash;表示从什么到什么之间。查询结果是含头又含尾的区间。\n如果不在这个之间的，在它们前面加上not 如\n1 select * from users where salary not between 800 and 2000; in/not in 后面跟着小括号，里面是一个列表的值，一个具体的值。 如\n1 2 select * from users where username in (\u0026#39;aaa\u0026#39;,\u0026#39;bbb\u0026#39;); select * from users where username not in (\u0026#39;aaa\u0026#39;,\u0026#39;bbb\u0026#39;); （9）对查询结果排序 1 select...from...[where...] order by column1 desc/asc,... --desc为降序排列，asc升序 （10）case\u0026hellip;when语句 1 case column_name when value1 then result1,...[else result] end; 如\n1 2 3 select username,case username when \u0026#39;aaa\u0026#39; then \u0026#39;计算机部门\u0026#39; when \u0026#39;bbb\u0026#39; then \u0026#39;市场部门\u0026#39; else \u0026#39;其他部门\u0026#39; end as 部门 from users; 另一种形式：\n1 case when column_name=value1 then result1,...[else result] end; 如\n1 2 3 select username,case when username=\u0026#39;aaa\u0026#39; then \u0026#39;计算机部门\u0026#39; when username=\u0026#39;bbb\u0026#39; then \u0026#39;市场部门\u0026#39; else \u0026#39;其他部门\u0026#39; end as 部门 from users; 如\n1 select username,case when salary\u0026lt;800 then \u0026#39;工资低\u0026#39; when salary\u0026gt;5000 then \u0026#39;工资高\u0026#39; end as 工资水平 from users; （11）decode函数的使用 1 decode(column_name,value1,result1,...,defaultvalue) 如\n1 select username,decode(username,\u0026#39;aaa\u0026#39;,\u0026#39;计算机部门\u0026#39;,\u0026#39;bbb\u0026#39;,\u0026#39;市场部门\u0026#39;,\u0026#39;其他\u0026#39;) as 部门 from users; 10.其他一些实用命令 可以使用host cls来清屏。 查看用户show user。 使用上下箭头可以选择历史输入记录来使用。 =====================================================================================\nOracle函数部分 1.函数的作用 方便数据的统计 处理查询结果 2.函数的分类 数值函数 字符函数 日期函数 转换函数 3.数值函数 四舍五入 1 round(n[,m]) 省略m：0 m\u0026gt;0:小数点后m位 m\u0026lt;0:小数点前m位 如\n1 select round(23.4),round(23.45,1),round(23.45,-1)from dual; 取整函数 1 2 3 ceil(n) --取最大值 floor(n) --取最小值 select ceil(23.45),floor(23.45) from dual; 结果：\n1 2 3 CEIL(23.45) FLOOR(23.45) ----------- ------------ 24 23 常用计算 1 abs(n) --取绝对值 如\n1 select abs(23.45),abs(-23),abs(0) from dual; =====\n1 mod(m,n) --取余数 如\n1 select mod(5,2) from dual; =====\n1 power(m,n) --取m的n次幂 如\n1 select power(2,3),power(null,2) from dual; =====\n1 sqrt(n) --求平方根 如\n1 select sqrt(16) from dual; =====\n三角函数 1 2 3 sin(n),asin(n) cos(n),acos(n) tan(n),atan(n) --提供弧度参数 如\n1 select sin(3.124) from dual; 4.字符函数 大小写转换函数 1 2 3 upper(char) --转换为大写 lower(char) --转换为小写 initcap(char) --首字母大写 如\n1 select upper(\u0026#39;abde\u0026#39;),lower(\u0026#39;ADe\u0026#39;),initcap(\u0026#39;asd\u0026#39;) from dual; 获取子字符 1 substr(char,[m[,n]]) --获取子字符，分别是从哪取，从哪个位置开始取以及取出多少位，n省略时，从m的位置截取到结束，m从1开始如果m写0也是从第一个字符开始。如果m为负数时，从字符串的尾部开始截取 如\n1 select substr(\u0026#39;abcde\u0026#39;,2,3),substr(\u0026#39;abcde\u0026#39;,2),substr(\u0026#39;abcde\u0026#39;,-2,1) from dual; 获取字符串长度函数 1 length(char) --会包含空格的长度 如\n1 select length(\u0026#39;acd \u0026#39;) from dual; 字符串连接函数 1 concat(char1,char2) --与||作用一样 如\n1 select concat(\u0026#39;ab\u0026#39;,\u0026#39;cd\u0026#39;) from dual; 去除子串函数 1 trim(c2 from c1) --代表从c1中去除c2字符串，就是子文本替换，要求c2中只能是一个字符 如\n1 select trim (\u0026#39;a\u0026#39; from \u0026#39;abcde\u0026#39;) from dual; =====\n1 ltrim(c1[,c2]) --从c1中去除c2，从左边开始去除，要求第一个就是要去除的字符，有多少个重复的该字符就会去除多少次 如\n1 select ltrim(\u0026#39;ababaa\u0026#39;,\u0026#39;a\u0026#39;) from dual; =====\n1 rtrim(c1[,c2]) --从c1中去除c2，要求右侧第一个就是要去除的字符，有多少个重复的该字符就会去除多少次 =====\n1 trim(c1) --代表去除首尾的空格，删首尾空，同理ltrim和rtrim只有一个参数时。 =====\n1 replace(char,s_string[,r_string]) --替换函数，省略第三个参数则用空白替换 如\n1 2 3 select replace(\u0026#39;abcde\u0026#39;,\u0026#39;a\u0026#39;,\u0026#39;A\u0026#39;) from dual; select replace(\u0026#39;abcde\u0026#39;,\u0026#39;c\u0026#39;) from dual; select replace(\u0026#39;abced\u0026#39;,\u0026#39;ab\u0026#39;,\u0026#39;A\u0026#39;) from dual; 5.日期函数 系统时间 sysdate 默认格式：DD-MON-RR 天-月-年\n日期操作 1 add_months(date,i) --用于添加指定的月份，返回在指定的日期上添加的月份，i可以是任意整数，如果i是负数，则是在原有的值上减去该月份了 如\n1 select add_months(sysdate,3),add_months(sysdate,-3) from dual; =====\n1 next_day(date,char) --第二个参数指定星期几，在中文环境下输入星期X即可，返回下一个周几是哪一天。 如\n1 select next_day(sysdate,\u0026#39;星期一\u0026#39;) from dual; =====\n1 last_day(date) --用于返回日期所在月的最后一天 如\n1 select last_day(sysdate) from dual; =====\n1 month_between(date1,date2) --计算两个日期之间间隔的月份，前者减后者 如\n1 select months_between(\u0026#39;20-5月-15\u0026#39;,\u0026#39;10-1月-15\u0026#39;) from dual; =====\n1 extract(date from datetime) --返回相应的日期部分 如\n1 2 select extract(year from sysdate) from dual; --可以改month或者day select extract(hour from timestamp \u0026#39;2015-10-1 17:25:13\u0026#39;) from dual; =====\n用于截取日期时间的trunc函数\n用法：trunc（字段名，精度）\n具体实例：\n在表table1中，有一个字段名为sysdate，该行id=123，日期显示：2016/10/28 15:11:58\n1、截取时间到年时，sql语句如下：\n1 select trunc(sysdate,\u0026#39;yyyy\u0026#39;) from table1 where id=123; --yyyy也可用year替换 显示：2016/1/1\n2、截取时间到月时，sql语句：\n1 select trunc(sysdate,\u0026#39;mm\u0026#39;) from table1 where id=123; 显示：2016/10/1\n3、截取时间到日时，sql语句：\n1 select trunc(sysdate,\u0026#39;dd\u0026#39;) from table1 where id=123; 显示：2016/10/28\n4、截取时间到小时时，sql语句：\n1 select trunc(sysdate,\u0026#39;hh\u0026#39;) from table1 where id=123; 显示：2016/10/28 15:00:00\n5、截取时间到分钟时，sql语句：\n1 select trunc(sysdate,\u0026#39;mi\u0026#39;) from table1 where id=123; 显示：2016/10/28 15:11:00\n6、截取时间到秒暂时不知道怎么操作\n7、不可直接用trunc(sysdate,\u0026lsquo;yyyy-mm-dd\u0026rsquo;)，会提示“精度说明符过多”\n8.如果不填写第二个参数，则默认到DD，包含年月日，不包含时分秒。\n6.转换函数 1 to_char(date[,fmt[,params]]) --date为需要转换的日期，fmt为转换的格式，params为转换的语言（通常默认会自动选择，可以省略，与安装语言一致） 默认格式：DD-MON-RR\n可以定义的格式：\nYY YYYY YEAR MM MONTH DD DAY HH24 HH12 MI SS 如 1 select to_char(sysdate,\u0026#39;yyyy-mm-dd hh24:mi:ss\u0026#39;) from dual; =====\n1 to_date(char[,fmt[,params]]) 如\n1 select to_date(\u0026#39;2015-05-22\u0026#39;,\u0026#39;yyyy-mm-dd\u0026#39;) from dual; --注意显示的时候仍然按照时间的默认格式来显示 =====\n1 to_char(number[,fmt]) fmt列表：\n9:显示数字并忽略前面的0 0：显示数字，位数不足，用0补齐 .或D：显示小数点 ,或G：显示千分位 $：美元符号 S：加正负号（前后都可以） 如 1 2 select to_char(12345.678,\u0026#39;$99,999.999\u0026#39;) from dual; select to_char(12345.678,\u0026#39;s99,999.999\u0026#39;) from dual; =====\n1 to_number(char[,fmt]) fmt是转换的格式，可以省略 如\n1 select to_number(\u0026#39;$1,000\u0026#39;,\u0026#39;$9999\u0026#39;) from dual; 7.一些课堂案例 在查询中使用函数 如\n在员工信息表中查询员工的生日 1 substr(char[,m[,n]]) 将员工信息表中的年龄字段与10取余数 取员工入职的年份 查询出5月份入职的员工信息 =====================================================================================\nOracle高级查询部分 1.简介 本部分需要有如下两个部分的基础\n《oracle数据库开发必备利器之SQL基础》 《oracle数据库开发利器之函数》 2.分组查询 （1）什么是分组函数 分组函数作用于一组数据，并对一组数据返回一个值。 结构：\n1 2 3 4 5 select [column,] group function(column),... from table [where condition] [group by column] [order by column]; （2）常见的分组函数 avg 求平均值 和 sum 求和 求出员工的平均工资和工资的总和 求出员工工资的最大值和最小值 求出员工的总人数 distinct关键字求出部门数 如 1 select count(distinct deptno) from emp; min 最小值 max 最大值 count 求个数 wm_concat 行转列 如\n1 select deptno 部门号,wm_concat(ename) 部门中员工的姓名 from emp group by deptno; （3）分组函数与空值 举例1：统计员工的平均工资 如 1 select sum(sal)/count(*) 一,sum(sal)/count(sal) 二,avg(sal) 三 from emp; 结果一样\n举例二：统计员工的平均奖金 如 1 select sum(comm)/count(*) 一,sum(comm)/count(comm) 二,avg(comm) 三 from emp; 二和三结果一样，一不一样，因为在奖金列里面含有空值，count的时候数数不一样\n所以分组函数会自动忽略空值，可以在分组函数中使用nvl函数来使分组函数无法忽略空值 如\n1 select count(*),count(nvl(comm,0)) from emp; （4）分组数据 group by 子句\n求出员工表中各个部门的平均工资 注意：在select列表中所有未包含在组函数（就是汇总计算xxx的列）中的列都应该包含在group by子句中，但包含在group by子句中的列不必包含在select列表中 如 1 select avg(sal) from emp group by deptno; 使用多个列分组 如\n1 select deptno,job,sum(sal) from emp group by deptno,job order by deptno; （5）非法使用组函数 要求所用包含于select列表中，而未包含于组函数中的列都必须包含于group by子句中。\n1 2 select deptno,count(ename) from emp; 这里的deptno没有包含在group by子句中，所以会报错。\n（6）过滤分组 求平均工资大于2000的部门，having子句的使用 在group by后加[having group_condition] 如 1 select deptno,avg(sal) from emp group by deptno having avg(sal) \u0026gt; 2000; 注意不能在where子句中使用组函数（注意）。\n可以在having子句中使用组函数。\n如果在能使用where的场景下，从SQL优化的角度来看，尽量使用where效率更高，因为having是在分组的基础上过滤分组的结果，而where是先过滤，再分组。要处理的记录数不同。所以where能使分组记录数大大降低，从而提高效率。\n（7）在分组查询中使用order by子句 示例：求每个部门的平均工资，要求显示：部门号，部门的平均工资，并且按照工资升序排列 可以按照：列、别名、表达式、序号进行排序 1 2 select deptno,avg(sal) from emp group by deptno order by avg(sal); select deptno,avg(sal) 平均工资 from emp group by deptno order by 2; --也可以填写序号 （8）分组函数的嵌套 示例：求部门平均工资的最大值 如 1 select max(avg(sal)) from emp group by deptno; （9）group by语句的增强 按部门、不同的职位显示工资的总额；同时按部门，统计工资总额；统计所有员工的工资总额。 如 1 select deptno,job,sum(sal) from emp group by rollup(deptno,job); 结果：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 DEPTNO JOB SUM(SAL) ------ --------- ---------- 10 CLERK 4541 10 MANAGER 6455 10 PRESIDENT 9711 10 20707 20 CLERK 8235 20 ANALYST 12360 20 MANAGER 7032.5 20 27627.5 30 CLERK 4117.5 30 MANAGER 6895 30 SALESMAN 18648 30 29660.5 77995 rollup就可以实现上述的效果。小计、总计的效果，可以用在报表里面。\n再次优化，先运行：\n1 break on deptno skip 2 再运行上面的代码即可。\n（10）sqlplus的报表功能 如\n1 2 3 4 5 ttitle col 15 \u0026#39;我的报表\u0026#39; col 35 sql.pno --15表示空15列，sql.pno表示报表页码 col deptno heading 部门号 --设置别名 col job heading 职位 col sum(sal) heading 工资总额 break on deptno skip 1\t--deptno只显示一次，部门间间隔一行 结果：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 我的报表 1 部门号 职位 工资总额 ---------- --------- ---------- 10 CLERK 4541 MANAGER 6455 PRESIDENT 9711 20707 20 CLERK 8235 ANALYST 12360 MANAGER 7032.5 27627.5 我的报表 2 部门号 职位 工资总额 ---------- --------- ---------- 30 CLERK 4117.5 MANAGER 6895 SALESMAN 18648 29660.5 77995 3.多表查询 （1）简介 按数据库设计原则，员工表中只有部门的编号信息，部门的详细信息会存放在部门表中。\n什么是多表查询：就是从多个表中获取数据。\n前提是有一个外键约束来表示员工是哪个部门的，有个一个部门号来联结。\n（2）笛卡尔集 有了它才有多表查询的存在。笛卡尔集的列数等于每张表列数的相加，行数等于每张表的行数相乘。比如emp*dept有六列六行。里面的每一条记录不一定都是对的。多表查询就是要从笛卡尔集中选择出正确的记录。需要一个连接条件，比如部门号相等。有了连接条件，就能避免使用笛卡尔全集。在实际运行环境下，应提供where连接条件，避免使用笛卡尔全集。连接条件至少有要连接表数-1个。\n创建笛卡尔集可以使用全连接： FULL JOIN\n（3）连接的类型 等值连接 不等值连接 外连接 自连接 （4）等值连接 如\n1 select e.empno,e.ename,e.sal,d.dname from emp e,dept d where e.deptno=d.deptno; （5）不等值连接 如\n1 SELECT e.empno,e.ename,e.sal,s.grade FROM emp e,salgrade s WHERE e.sal BETWEEN s.losal AND s.hisal; （6）外连接 如\n1 SELECT d.deptno 部门号,d.dname 部门名称,COUNT(e.empno) 人数 FROM emp e,dept d WHERE e.deptno=d.deptno GROUP BY d.deptno,d.dname; --漏了一个部门 核心：通过外连接，把对于连接条件不成立的记录，仍然包含在最后的结果中。\n左外连接（LEFT [OUTER] JOIN）：当连接条件不成立的时候，等号左边的表仍然被包含\n右外连接（RIGHT [OUTER] JOIN）：当连接条件不成立的时候，等号右边的表仍然被包含\n因此上述表达式改为：\n改为右外连接 方法是在相反的方向的等值连接结尾加上(+),比如右外连接就是加在左边的最后。\n1 SELECT d.deptno 部门号,d.dname 部门名称,COUNT(e.empno) 人数 FROM emp e,dept d WHERE e.deptno(+)=d.deptno GROUP BY d.deptno,d.dname; 或者写成：\n1 SELECT d.deptno 部门号,d.dname 部门名称,COUNT(e.empno) 人数 FROM emp e,dept d WHERE e.deptno right join d.deptno GROUP BY d.deptno,d.dname; 得到结果：\n1 2 3 4 5 6 部门号 部门名称 人数 ------ -------------- ---------- 10 ACCOUNTING 3 40 OPERATIONS 0 20 RESEARCH 5 30 SALES 6 （7）自连接 作用：通过别名，将同一张表视为多张表（核心） INNER JOIN 如\n1 SELECT e.ename 员工姓名,b.ename 老板姓名 FROM emp e,emp b WHERE e.mgr=b.empno; 自连接存在的问题 尽管是查询一张表，但本质上仍然是多表查询，会产生笛卡尔集。\n可以通过这个看笛卡尔集有多少条记录select count(*) from emp e,emp b;表越多，次方越多。比如员工表中有一亿条记录，如果看成三张表，就有一亿的立方的笛卡尔集，所以自连接不适合查询大表。\n所以要使用解决方法： 层次查询 （单表查询，只有在一张表时才不会查询笛卡尔集，在某些情况下可以取代自连接）。\n层次查询的原理：可以把前面的结果变成分level的一棵树。这棵树的根是没有上司的king，也就是mgr就是NULL。 如：\n1 SELECT level,empno,ename,sal,mgr FROM emp CONNECT BY PRIOR empno=mgr START WITH mgr IS NULL ORDER BY 1; 自连接的优点：结果直观。缺点：不适合操作大表。\n层次查询的优点：适合单表查询，不会产生笛卡尔集。缺点：并没有自连接那么直观。\n4.子查询 （1）子查询介绍 为什么要学习子查询：子查询可以解决不能一步求解的问题\n示例：查询工资比scott高的员工信息 子查询的语法：其实也就是select语句的嵌套\n1 2 3 4 5 select select_list from table where expr operator (select select_list from table); 如\n1 select * from emp where sal \u0026gt; (select sal from emp where ename=\u0026#39;SCOTT\u0026#39;); （2）子查询注意的十个问题 子查询语法中的小括号 语法中一定要有小括号，不然是错的。\n子查询的书写风格 该换行的换行，该缩进的索引，可以便于阅读。\n可以使用子查询的位置：where,select,having,from select后面使用，要求一定要只返回一条记录，要是单行子查询才行，多行子查询不行。 如\n1 2 SELECT empno,ename,sal,(SELECT job FROM emp WHERE empno=7839) 第四列 FROM emp; 在having后面使用： 如\n1 2 3 4 5 6 SELECT deptno,AVG(sal) FROM emp GROUP BY deptno HAVING AVG(sal) \u0026gt; (SELECT MAX(sal) FROM emp WHERE deptno=30); 在from后面放置：\n非常的重要，很多问题都是在from后面方式子查询来解决的 如\n1 SELECT * from(SELECT empno,ename,sal FROM emp); 不可以使用子查询的位置：group by 如\n1 2 3 SELECT AVG(sal) FROM emp GROUP BY (SELECT deptno FROM emp); --会报错，这里不允许出现子查询表达式 强调：from后面的子查询，比较特殊，比较重要 如\n1 2 SELECT * FROM (SELECT empno,ename,sal,sal*12 annsal FROM emp); 主查询和子查询可以不是同一张表 如\n1 2 3 4 SELECT * FROM emp WHERE deptno= (SELECT deptno FROM dept WHERE dname=\u0026#39;SALES\u0026#39;); 多表查询代码：\n1 2 3 SELECT e.*\tFROM emp e,dept d WHERE e.deptno=d.deptno AND d.dname=\u0026#39;SALES\u0026#39;; 哪种查询方式好呢？从理论上来讲，尽量使用多表查询比较好，因为子查询需要对数据库访问两次，而多表查询只需要对数据库访问一次。但实际情况下有可能不一样，因为多表查询的笛卡尔集可能很大所以慢了。\n一般不在子查询中，使用排序；但在Top-N分析问题中，必须对子查询排序 比如找到员工表中工资最高的前三名。\nrownum行号，是一个伪列，表上没有这一列，当做一些特殊操作的时候，oracle自动加上。行号需要注意的问题：行号永远按照默认的顺序生成；行号只能使用\u0026lt;，\u0026lt;=，不能使用\u0026gt;或者\u0026gt;=这样的符号。 如\n1 2 3 SELECT ROWNUM,empno,ename,sal FROM (SELECT * FROM emp ORDER BY sal DESC) WHERE ROWNUM\u0026lt;=3; 一般先执行子查询，再执行主查询；但相关子查询例外 相关子查询的表必须设定一个别名，然后把主查询的内容传入到子查询中进行查询。 如\n1 2 3 SELECT empno,ename,sal,(SELECT AVG(sal) FROM emp WHERE deptno=e.deptno) avgsal FROM emp e WHERE sal \u0026gt; (SELECT AVG(sal) FROM emp WHERE deptno=e.deptno); 这里就把主查询e表中的部门号传入子查询中进行查询了。\n单行子查询只能使用单行操作符；多行子查询只能使用多行操作符 单行操作符：=、\u0026gt;、\u0026gt;=、\u0026lt;、\u0026lt;=、\u0026lt;\u0026gt; 如\n1 2 3 SELECT * FROM emp WHERE job = (SELECT job FROM emp WHERE empno=7566) AND sal \u0026gt; (SELECT sal FROM emp WHERE empno=7782); 又如\n1 2 SELECT * FROM emp WHERE sal = (SELECT MIN(sal) FROM emp); 如\n1 2 3 4 5 6 SELECT deptno,MIN(sal) FROM emp GROUP BY deptno HAVING MIN(sal) \u0026gt; (SELECT MIN(sal) FROM emp WHERE deptno=20); 非法使用单行子查询： 如\n1 2 3 select empno,ename from emp where sal=(select min(sal) from emp group by deptno); --因为子查询返回了不止一行，所以是非法使用单行子查询。 多行操作符：in（等于列表中的任意一个）、any（和子查询返回的任意一个值比较）、all（和子查询返回的所有值比较） in： 如\n1 2 SELECT * FROM emp WHERE deptno IN (SELECT deptno FROM dept WHERE dname=\u0026#39;SALES\u0026#39; OR dname=\u0026#39;ACCOUNTING\u0026#39;); 又如\n1 2 SELECT e.* FROM emp e,dept d WHERE e.deptno=d.deptno AND (d.dname=\u0026#39;SALES\u0026#39; OR d.dname=\u0026#39;ACCOUNTING\u0026#39;); any： 如\n1 2 SELECT * FROM emp WHERE sal \u0026gt; ANY(SELECT sal FROM emp WHERE deptno=30); 等价于\n1 2 SELECT * FROM emp WHERE sal \u0026gt; (SELECT min(sal) FROM emp WHERE deptno=30); all： 如\n1 2 SELECT * FROM emp WHERE sal \u0026gt; ALL(SELECT sal FROM emp WHERE deptno=30); 等价于：\n1 2 SELECT * FROM emp WHERE sal \u0026gt; (SELECT MAX(sal) FROM emp WHERE deptno=30); 注意：子查询中是null值的问题 单行子查询中返回空值，要使用in之类的关键字，等于号的话永远为空。\n多行子查询中，如查询不是老板的员工 如：\n1 2 SELECT * FROM emp WHERE empno NOT IN (SELECT mgr FROM emp); --会不返回结果，因为当子查询中包含空值的时候，不能使用not in，因为not in等同于不等于所有（永远为假）。 所以修改为：\n1 2 3 SELECT * FROM emp WHERE empno NOT IN (SELECT mgr FROM emp WHERE mgr IS NOT NULL); 5.综合示例 （1）案例一 分页查询显示员工信息：显示员工号，姓名，月薪 每页显示四条记录 显示第二页的员工 按照月薪降序排列 注意：rownum只能使用\u0026lt;,\u0026lt;=不能使用\u0026gt;,\u0026gt;=，因为oracle数据库是一个行式数据库，取了第一行才能取第二行，所以行号永远从1开始，所以比如rownum\u0026gt;=5这样的条件永远为假。 所以分页查询：\n1 2 3 4 5 SELECT r,empno,ename,sal FROM(SELECT ROWNUM r,empno,ename,sal from(SELECT ROWNUM,empno,ename,sal FROM emp ORDER BY sal DESC) e1 WHERE ROWNUM\u0026lt;=8) e2 WHERE r\u0026gt;=5; （2）案例二 找到员工表中薪水大于本部门平均薪水的员工 如\n1 2 3 SELECT e.empno,e.ename,e.sal,d.avgsal FROM emp e,(SELECT deptno,AVG(sal) avgsal FROM emp GROUP BY deptno) d WHERE e.deptno=d.deptno AND e.sal\u0026gt;d.avgsal; --多表查询 如果需要查询执行计划看性能的话，则在语句前面加上EXPLAIN PLAN FOR\n执行一遍之后，运行select * from table(dbms_xplan.display);即可查看性能分析，看消耗的CPU的多少来判定性能的优劣。\n（3）案例三 按部门统计员工人数，按照规定格式输入，已知员工的入职年份在80,81,82,87年之中。 如 1 2 3 4 5 6 SELECT COUNT(*) Total, SUM(DECODE(to_char(hiredate,\u0026#39;yyyy\u0026#39;),\u0026#39;1980\u0026#39;,1,0)) \u0026#34;1980\u0026#34;, SUM(DECODE(to_char(hiredate,\u0026#39;yyyy\u0026#39;),\u0026#39;1981\u0026#39;,1,0)) \u0026#34;1981\u0026#34;, SUM(DECODE(to_char(hiredate,\u0026#39;yyyy\u0026#39;),\u0026#39;1982\u0026#39;,1,0)) \u0026#34;1982\u0026#34;, SUM(DECODE(to_char(hiredate,\u0026#39;yyyy\u0026#39;),\u0026#39;1987\u0026#39;,1,0)) \u0026#34;1987\u0026#34; FROM emp; 使用子查询方法：\n1 2 3 4 5 6 7 SELECT (SELECT COUNT(*) FROM emp) Total, (SELECT COUNT(*) FROM emp WHERE to_char(hiredate,\u0026#39;yyyy\u0026#39;)=\u0026#39;1980\u0026#39;) \u0026#34;1980\u0026#34;, (SELECT COUNT(*) FROM emp WHERE to_char(hiredate,\u0026#39;yyyy\u0026#39;)=\u0026#39;1981\u0026#39;) \u0026#34;1981\u0026#34;, (SELECT COUNT(*) FROM emp WHERE to_char(hiredate,\u0026#39;yyyy\u0026#39;)=\u0026#39;1982\u0026#39;) \u0026#34;1982\u0026#34;, (SELECT COUNT(*) FROM emp WHERE to_char(hiredate,\u0026#39;yyyy\u0026#39;)=\u0026#39;1987\u0026#39;) \u0026#34;1987\u0026#34; FROM dual; （4）练习 新建两个表，然后按要求查到相关的内容\n第一个表：\n1 2 3 4 CI_ID STU_IDS -------------------- -------------------------------------------------------------------------------- 1 1,2,3,4 2 1,4 表结构：\n1 2 3 4 Name Type Nullable Default Comments ------- ------------- -------- ------- -------- CI_ID VARCHAR2(20) STU_IDS VARCHAR2(100) Y 第二个表：\n1 2 3 4 5 6 STU_ID STU_NAME -------------------- -------------------- 1 张三 2 李四 3 王五 4 赵六 表结构：\n1 2 3 4 Name Type Nullable Default Comments -------- ------------ -------- ------- -------- STU_ID VARCHAR2(20) STU_NAME VARCHAR2(20) Y 提示：\n1.需要进行两个表的连接查询，为两个表都取别名\n2.使用instr(a,b)函数，该函数的含义为：如果字符串b在字符串a的里面，则返回的是b在a中的位置，及返回值大于0\n3.需要用到分组查询\n4.使用wm_concat(cols)函数对学生姓名用逗号进行拼接\n1 2 3 4 5 --结果查询语句 SELECT ci_id,wm_concat(stu_name) stu_name FROM pm_stu,pm_ci WHERE INSTR(stu_ids,stu_id)\u0026gt;0 GROUP BY ci_id; 得到正确结果：\n1 2 3 4 CI_ID STU_NAME -------------------- -------------------------------------------------------------------------------- 1 张三,赵六,王五,李四 2 张三,赵六 同时学会了一个，如果在oracle中，需要实现如果表已经存在则先删除表的操作，写法为：\n1 2 3 4 5 6 7 8 9 10 --如果已经存在表则先删除表 DECLARE k NUMBER; BEGIN select count(*) INTO k from all_tables where table_name=\u0026#39;PM_CI\u0026#39;; IF k=1 THEN execute IMMEDIATE \u0026#39;DROP TABLE pm_ci\u0026#39;; END IF; END; / 其中查询的表名和drop的表名变成你要检测的表名即可。\n=====================================================================================\nPL/SQL编程基础部分 1.为什么要学PL/SQL编程 使用PLSQL语言操作Oracle数据库的效率最高。\n之前用sql语句是命令式的语言，但如果案例是复杂的，比如需要分条件来做不同的事情的，就需要PL/SQL效率会更高，不需要用其他的编程语言。\nPL/SQL介绍 全称是Procedure Language/SQL，是oracle对sql语言的过程化扩展\n指在SQL命令语言中增加了过程处理语句（如分支、循环等），使SQL语言具有过程处理能力。\n（1）是sql的扩展，支持sql语句。\n（2）是面向过程的语言。\n2.语句块通用格式 1 2 3 4 5 6 7 8 9 declare --说明部分（变量说明、光标申明、例外说明） begin --程序体（DML语句） dbms_output.put_line(\u0026#39;Hello World\u0026#39;); exception --例外处理语句 end; / 1 / --这个正斜杠用来退出前面的代码编写并且执行语句 1 2 --查看程序包的结构 desc 程序包名字 3.打开输出开关 1 set serveroutput on 4.不同数据库中SQL扩展 Oracle：PL/SQL DB2：SQL/PL SQL Server：Transac-SQL(T-SQL) 5. PL/SQL的说明部分 定义基本变量 类型：char，varchar2，date，number，boolean，long 举例：名字在片面，变量是在后面，:=为赋值符号不是单=号\n1 2 3 var1 char(15); married bollean := true; psal number(7,2); --说明有两位小数 定义引用型变量 1 my_name emp.ename%type; --引用ename的类型，ename是啥类型变量就是啥类型 第二种赋值方式：\n1 select ename,sal into pename,psal from emp where empno=7839; 这里的into就可以赋值，是一一对应的。\n定义记录型变量 1 2 3 emp_rec emp%rowtype; --取表中一行的类型，作为变量的类型，可以理解为数组，如果需要取用到列里面的某一行，就像如下写法： emp_rec.ename := \u0026#39;ADAMS\u0026#39;; --ename是列的名字。 6. PL/SQL的流程控制语句 （1）if语句： 情况一\n1 2 3 if 条件 then 语句1; 语句2; end if; 情况二\n1 2 3 if 条件 then 语句序列1; else 语句序列2; end if; 情况三\n1 2 3 4 if 条件 then 语句; elsif 语句 then 语句; --特别注意下elsif else 语句; end if; （2）while循环： 1 2 3 4 while total \u0026lt;= 25000 loop ... total := total + salary; end loop; （3）loop循环： 1 2 3 4 5 --在控制光标的时候比较简单 loop exit[when 条件]; ...... end loop; （4）for循环： 1 2 3 for i in 1..3 loop --表示连续区间用这种写法 语句序列; end loop; 7.光标 （1）光标的引入背景 select如果返回的结果有多行的话就会出错，所以需要引入光标，光标cursor就是一个结果集。也叫游标。\n（2）光标的语法 1 cursor 光标名 [(参数名 数据类型[,参数名 数据类型]...)] is select 语句; 如\n1 cursor c1 is select ename from emp; 此外，光标是可以带参数的。\n（3）光标的一些操作 打开光标： 1 open c1;(打开光标执行查询) 关闭光标： 1 close c1;(关闭游标释放资源) 取一行光标的值： 1 fetch c1 into pename; (取一行到变量中) fetch的作用： （1）把当前指针指向的记录返回；\n（2）将指针指向下一条记录。\n（4）光标的属性 %found(光标能取到内容返回true，否则false) %notfound（与前者相反） %isopen：判断光标是否打开 %rowcount：影响的行数，不是总行数，比如光标取走了10行的数据，那么这个值就是10 （5）光标的限制 默认情况下，oracle数据库只允许在同一个会话中打开300个光标\n修改光标数的限制： 1 alter system set open_cursors=400 scope=both; scope是范围，取值有三个：both, memory（只更改当前实例）, spfile（只更改参数文件，数据库需要重启）\n8.例外 （1）例外的概念 例外是程序设计语言提供的一种功能，用来增强程序的健壮性和容错性。\n（2）例外的分类 系统例外 自定义例外 （3）系统例外 no_data_found 没有找到数据，select语句没有找到结果的时候 too_many_rows select\u0026hellip;into语句匹配多个行 zero_divide 被零除 value_error 算术或转换错误 timeout_on_resource 在等待资源时发生超时（分布式数据库的访问会用到） （4）自定义例外 定义变量，类型是exception\n使用raise关键字抛出自定义例外\n9.程序设计方法 瀑布模型：\n1 2 3 4 5 需求分析 设计1.概要设计2.详细设计 编码Coding 测试Testing 上线 以上步骤就像水流一下，最忌讳一上来就编码。\n想明白SQL语句、变量。\n变量：1.初始值是多少2.最终值如何得到\n10.其他小技巧： 使用||符号来连接文本字符串。 \u0026ndash;表示单行注释，/* */表示多行输入 单个等于号表示判断。 plsql中大小写不敏感。 then语句相当于一个大括号，后面的语句可以一起被处理，比如如下写法： 1 2 when zero_divide then dbms_output.put_line(\u0026#39;1:0不能做被除数\u0026#39;); dbms_output.put_line(\u0026#39;2:0不能做被除数\u0026#39;); 这里两句话都会被打印出来。\n把握一个原则：能不操作数据库就不操作数据库，比单单加减乘除的计算慢。 =====================================================================================\nOracle触发器部分 1.触发器的概念 触发器是一个特殊的存储过程。是一个与表相关联的、存储的PL/SQL程序。\n作用：每当一个特定的数据操作语句（insert、update、delete，注意没有select）在指定的表上发出时，oracle自动地执行触发器中定义的语句序列。\n2.触发器的类型 语句级的触发器 在指定的操作语句操作之前或之后执行一次，不管这条语句影响了多少行。\n语句级触发器针对表，只会触发一次\n行级的触发器 触发语句作用的每一条记录都被触发。在行级触发器中使用:old和:new伪记录变量，识别值的状态。如果有for each row就表示行级触发器。 如\n1 insert into emp10 select * from emp where deptno=10; 会查出3条记录。\n行级触发器针对行，有多少条记录就触发多少次。\n3.第一个触发器 每当成功插入新员工后，自动打印一句话，\u0026ldquo;成功插入新员工\u0026rdquo;。单词trigger\n例如：\n1 2 3 4 5 6 7 8 create trigger saynewemp after insert on emp declare begin dbms_output.put_line(\u0026#39;成功插入新员工\u0026#39;); end; / 4.触发器的具体应用场景 （1）复杂的安全性检查 \u0026ndash;比如周末不允许操作数据库\n（2）数据的确认 \u0026ndash;涨后的工资大于涨前的工资\n（3）数据库审计 \u0026ndash;跟踪表上所做的数据操作，什么时间什么人操作了什么数据，操作的人是什么。基于值的审计\n（4）数据的备份和同步 \u0026ndash;异地备份，把主数据的数据自动同步到备数据库中\n5.创建触发器的语法 1 2 3 4 5 6 create [or replace] trigger 触发器名 {before|after} {delete|insert|update [of 列名]} on 表名 [for each row [when 条件]] plsql块 6.触发器的案例 触发器案例一：禁止在非工作时间插入数据 触发器案例二：涨工资不能越涨越少 :old和:new的使用要注意。 触发器案例三：创建基于值的触发器 触发器案例四：数据库的备份和同步 \u0026ndash;利用触发器实现数据的同步备份，多用于异地分布式数据库 还能使用快照备份，快照备份是异步备份，而触发器是同步备份，没有延时的 ===================================================================================== Oracle存储过程部分 1.存储过程模板公式 1 2 3 create [or replace] PROCEDURE 过程名(参数列表) AS PLSQL子程序体; （关键字可以小写）（如果不传参，则参数列表的小括号也可以省略） 这个PLSQL子程序体;一般为：\n1 2 3 4 begin 代码... end; / 如果写的是存储函数，那么这里的PROCEDURE需要改成FUNCTION，而且必须在参数列表和AS之间添加一句：RETURN 函数值类型。而且需要在子程序体需要返回的时候写return 返回值。\n写好之后先编译，然后调用、运行。\nas后面跟的是说明部分，相当于declare。\nas也可以写成is。\n调用方式有两种：\n（1）exec 过程名();\n（2）\n1 2 3 4 begin 调用语句; end; / 2.入参 在参数列表中，如果是输入参数，可以写入eno in number，in是关键，in前面是变量名，后面是变量类型。\n3.出参 如果是输出参数，写eno out number，out是关键，out前面是变量名，后面是变量类型。\n存储过程和存储函数都可以有out参数。\n他们都可以有多个out参数。\n存储过程可以通过out参数来实现返回值。\n4.小例子 查询某个员工姓名、月薪和职位\n1 2 3 4 5 6 7 8 9 10 create or replace procedure queryempinform(eno in number, pename out varchar2, psal out number, pjob out varchar2) as begin --得到该员工的姓名、月薪和职位 select ename,sal,empjob into pename,psal,pjob from emp where empno=eno; end; / 5.在out参数中使用光标案例 案例：查询某个部门中所有员工的所有信息。\n包头（负责声明包中的结构）：\n1 2 3 4 CREATE OR REPLACE PACKAGE MYPACKAGE AS type empcursor is ref cursor; procedure queryEmpList(dno in number,empList out empcursor); END MYPACKAGE; 注意：包头中也可以不定义存储过程，只定义光标那一行。\n包体（负责写需要实现包头中声明的所有方法）：\n1 2 3 4 5 6 7 CREATE OR REPLACE PACKAGE BODY MYPACKAGE AS procedure queryEmpList(dno in number,empList out empcursor) AS BEGIN --打开光标类型（是一个集合，意味着可以返回许多信息的集合） open empList for select * from emp where deptno=dno; END queryEmpList; END MYPACKAGE; 注意：包体里面的存储过程也可以不写在包体内部也可以一样调用包头中定义的光标。\n6.其他小知识点 参数列表可以换行，也可以在关键字之间多加空格。 如果是没有参数的就是存储过程，如果有参数就是存储函数。存储函数可以有一个返回值，可以用return子句进行返回。 我们的原则是，如果只需要一个返回值，则用存储函数。如果没有返回值，用存储过程，如果需要有多个返回值，则使用存储过程，在参数中使用out参数。 单行注释使用“\u0026ndash;注释内容”，多行注释使用“/* 注释内容 */”。 调试的时候授予权限： 哪个权限没有就到sqlplus输入如下代码：\n1 grant 要调的权限（中间用逗号分隔） to 用户名; 如果在计算中可能会有空值的话需要使用nvl预空函数。 ===================================================================================== 其他独立知识点 一些不知道插进过去的哪些笔记的笔记就放在这里吧~\n简单EXISTS和 NOT EXISTS讲解和案例 以\n1 select * from A where exists(select * from B where A.a=B.a) 为例, exists表示,对于A中的每一个记录,如果,在表B中有记录,其属性a的值与表A这个记录的属性a的值相同,则表A的这个记录是符合条件的记录,\n如果是NOT exists,则表示如果表B中没有记录能与表A这个记录连接,则表A的这个记录是符合条件的记录。\n","permalink":"https://ktzxy.top/posts/ko1zbxsojy/","summary":"Oracle数据库速查知识文档","title":"Oracle数据库速查知识文档"},{"content":"linux磁盘空间占满问题快速定位并解决 常使用如下几个命令进行排查：df, lsof，du。\n通常的解决步骤如下：\n1. df -h 查看全部磁盘空间 1 2 3 4 5 6 [root@localhost ~]# df -h 文件系统 容量 已用 可用 已用% 挂载点 tmpfs 16G 530M 16G 4% /run /dev/mapper/cl-root 50G 34G 17G 67% / /dev/mapper/cl-home 492G 232G 261G 48% /home /dev/sda1 976M 278M 632M 31% /boot 2. 快速定位一下应用日志大小情况 进入使用率较高的挂载点，凭感觉定位可能过大的文件，比如 tomcat 日志，应用系统自己的日志等。\n3. 使用 du 命令定位 如果不能直观地排除出是某个日志多大的原因，就需要看一下指定目录下的文件和子目录大小情况，\n1 du -h --max-depth=1 path | sort -hr #查看目录大小并按照大小倒序展示 1 2 3 4 5 [root@test ~]# du -h --max-depth=1 /usr/local/ | sort -hr 2.6G /usr/local/ 1.1G /usr/local/mysql 358M /usr/local/jdk1.8.0_121 ...... 4. 删除或者清空文件 清空\n清空而不删除用 \u0026gt; 符号，例如清空 debug.log 文件：\n1 [root@localhost ~]# \u0026gt; logs/catalina.out 删除\n删除文件时注意：\n有时候用 rm 命令删除日志文件之后再 df -h 查看空间依然被占满，则该大文件可能在被某个进程占用而未释放。\n1 lsof 文件名 # 查看文件占用进程情况 如果删除的日志正在被某个进程占用，则必须重启或者 kill 掉进程。\n1 2 3 4 [root@test ~]# lsof /usr/local/apache-tomcat-7.0.54/logs/catalina.out COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME java 7053 root 1w REG 202,1 958123 1180888 /usr/local/apache-tomcat-7.0.54/logs/catalina.out java 7053 root 2w REG 202,1 958123 1180888 /usr/local/apache-tomcat-7.0.54/logs/catalina.out 【参考】\n每天一个 linux 命令（34）：du 命令 Linux 中删除文件，磁盘空间未释放问题追踪 linux 删除文件后，如何释放磁盘空间? ","permalink":"https://ktzxy.top/posts/vcurdewehu/","summary":"linux磁盘空间占满问题快速定位并解决","title":"linux磁盘空间占满问题快速定位并解决"},{"content":"Java基础 - 枚举(Enum) 1. 枚举的概述 枚举类型是在JDK1.5后的新特性。本质就是一个类，所有自定义的枚举类都默认是Enum的子类\n关于枚举，阿里巴巴开发手册有这样两条建议：\n枚举类名带上 Enum 后缀，枚举成员名称需要全大写，单词间用下划线隔开。 如果变量值仅在一个固定范围内变化用 enum 类型来定义。 1.1. JDK1.5之前实现类似枚举的功能（了解） JDK1.5之前的解决输入非法成员变量值的方法：重新定义一个成员变量的类，在定义成员变量的时候，使用这个相应的成员变量类。将成员变量的类的构造方法私有，不能让使用者new对象。\nJKD1.5之后的解决方法：定义一个枚举类，解决调用构造方法创建对象时输入非法成员变量的情况。\n1.2. Enum 类 1 public abstract class Enum\u0026lt;E extends Enum\u0026lt;E\u0026gt;\u0026gt; implements Comparable\u0026lt;E\u0026gt;, Serializable 这是所有 Java 语言枚举类型的公共基本类\n1.3. 枚举的作用 提高代码的可读性 控制某一数据类型的值不能乱写，避免产生垃圾值，保证数据有效性 1.4. 枚举的使用场景 当数据类型的值只能在给定的范围内进行选择时(数量不能太多的时候)。比如：性别、季节、月份、星期…\n2. 枚举的基础使用 2.1. 枚举类的定义格式 1 2 3 enum 枚举名称 { 成员名称1, 成员名称2, 成员名称3 } 枚举的底层实现，枚举的底层是一个类继承了Enum\n2.2. 枚举的使用步骤 定义枚举类 在成员变量类型上面使用枚举类型 设置枚举值(如WeekDay.FRI)，语法即枚举名称.成员 可以做枚举比较e.getResetDay() == WeekDay.STA 总结：枚举的作用是用来表示几个固定的值，可以使用枚举中成员\n3. 枚举常用方法 1 public final String name(); 获得枚举名，返回此枚举常量的名称 1 public static \u0026lt;T extends Enum\u0026lt;T\u0026gt;\u0026gt; T valueOf(Class\u0026lt;T\u0026gt; enumType, String name) 根据枚举名字符串获得枚举值对象。返回带指定名称的指定枚举类型的枚举常量。名称必须与在此类型中声明枚举常量所用的标识符完全匹配。（不允许使用额外的空白字符。）与通过\u0026quot;枚举类名.枚举项名称\u0026quot;去访问指定的枚举项得到相同的枚举对象 1 public final int ordinal() 返回此枚举常量的顺序（位置在枚举声明，在初始常数是零分序号）。不推荐使用，它被设计用于复杂的基于枚举的数据结构，比如 EnumSet 和 EnumMap 1 public final int compareTo(E o) 比较此枚举与指定对象的顺序(索引值)，返回索引值的差值。在该对象小于、等于或大于指定对象时，分别返回负整数、零或正整数。 1 public String toString() 返回枚举常量的名称 1 public static \u0026lt;T extends Enum\u0026lt;T\u0026gt;\u0026gt; T[] values(); 枚举中的一个特殊方法，可以将枚举类转变为一个该枚举类型的数组。此方法虽然在JDK文档中查找不到，但每个枚举类都具有该方法，它遍历枚举类的所有枚举值非常方便 4. 枚举的特点与综合示例 定义枚举类要用关键字 enum 所有枚举类都是 Enum 的子类（默认是Enum的子类，不需要（能）再写extends Enum） 每一个枚举项其实就是该枚举的一个对象，通过 枚举类名.枚举项名称 方式去访问指定的枚举项 枚举也是一个类，也可以去定义成员变量 枚举值必须是枚举类的第一行有效语句。多个枚举值必须要用逗号(,)分隔。最后一个枚举项后的分号是可以省略的，但是如果枚举类有其他的东西，这个分号就不能省略。建议不要省略 枚举类可以有构造方法，但必须是private修饰的，它默认的也是 private 的 枚举项的用法比较特殊：可以定义为枚举名称(\u0026quot;xxx\u0026quot;)，但定义构造方法 枚举类也可以有抽象方法，但是枚举项必须重写该方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class EnumDemo { // 普通枚举 enum ColorEnum { RED, GREEN, BLUE; } // 带属性的枚举，示例中的数字就是延伸信息，表示一年中的第几个季节。 enum SeasonEnum { SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4); private final int seq; SeasonEnum(int seq) { this.seq = seq; } public int getSeq() { return seq; } } // 带抽象方法枚举，示例中的构造方法为类型的中文名称，在定义枚举值时需要实现抽象方法 enum PayTypeEnum { WX_PAY(\u0026#34;微信支付\u0026#34;) { @Override public void doPay(BigDecimal money) { System.out.println(\u0026#34;微信支付: \u0026#34; + money); } }, ALI_PAY(\u0026#34;支付宝支付\u0026#34;) { @Override public void doPay(BigDecimal money) { System.out.println(\u0026#34;支付宝支付: \u0026#34; + money); } }; private final String name; PayTypeEnum(String name) { this.name = name; } public String getName() { return name; } // 定义抽象方法 public abstract void doPay(BigDecimal money); } } 5. 枚举底层实现 5.1. 枚举编译后的代码 创建一个ColorEnum的枚举类，通过编译，再反编译看看它发生了哪些变化。\n1 2 3 public enum ColorEnum { RED,GREEN,BULE; } 使用命令javac ColorEnum.java进行编译生成class文件，然后再用命令javap -p ColorEnum.class进行反编译。\n去掉包名，反编译后的内容如下：\n1 2 3 4 5 6 7 8 9 public final class ColorEnum extends Enum{ public static final ColorEnum GREEN; public static final ColorEnum BULE; private static final ColorEnum[] $VALUES; public static ColorEnum[] values(); public static ColorEnum valueOf(java.lang.String); private ColorEnum(); static {}; } 5.2. 枚举源码特点总结 枚举类被final修饰，因此枚举类不能被继承； 枚举类默认继承了Enum类，java不支持多继承，因此枚举类不能继承其他类； 枚举类的构造器是private修饰的，因此其他类不能通过构造器来获取对象； 枚举类的成员变量是static修饰的，可以用类名.变量来获取对象； values()方法是获取所有的枚举实例； valueOf(java.lang.String)是根据名称获取对应的实例； 6. 枚举的应用示例 6.1. 枚举使用案例 - 消除if/else 假如要写一套加密接口，分别给小程序、app和web端来使用，但是这三种客户端的加密方式不一样。一般情况下我们会传一个类型type来判断来源，然后调用对应的解密方法即可。代码如下：\n1 2 3 4 5 6 7 if (\u0026#34;WEIXIN\u0026#34;.equals(type)) { // dosomething } else if (\u0026#34;APP\u0026#34;.equals(type)) { // dosomething } else if (\u0026#34;WEB\u0026#34;.equals(type)) { // dosomething } 使用枚举来代替if/else。写一个加密用的接口，有加密和解密两个方法。然后用不同的算法去实现这个接口完成加解密。\n1 2 3 4 5 6 7 public interface Util { // 解密 String decrypt(); // 加密 String encrypt(); } 创建一个枚举类来实现这个接口\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public enum UtilEnum implements Util { WEIXIN { @Override public String decrypt() { return \u0026#34;微信解密\u0026#34;; } @Override public String encrypt() { return \u0026#34;微信加密\u0026#34;; } }, APP { @Override public String decrypt() { return \u0026#34;app解密\u0026#34;; } @Override public String encrypt() { return \u0026#34;app加密\u0026#34;; } }, WEB { @Override public String decrypt() { return \u0026#34;web解密\u0026#34;; } @Override public String encrypt() { return \u0026#34;web加密\u0026#34;; } }; } 最后，获取到type后，直接可以根据type调用解密方法即可\n1 String decryptMessage = UtilEnum.valueOf(type).decrypt(); 6.2. 枚举创建线程安全的单例模式（扩展应用） 1 2 3 4 5 6 7 8 public enum SingletonEnum { INSTANCE; public void doSomething(){ // dosomething... } } 这样一个单例模式就创建好了，通过SingletonEnum.INSTANCE来获取对象就可以了。\n6.2.1. 序列化造成单例模式不安全 一个类如果如果实现了序列化接口，则可能破坏单例。每次反序列化一个序列化的一个实例对象都会创建一个新的实例。\n枚举序列化是由JVM保证的，每一个枚举类型和定义的枚举变量在JVM中都是唯一的，在枚举类型的序列化和反序列化上，Java做了特殊的规定：在序列化时Java仅仅是将枚举对象的name属性输出到结果中，反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时，编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法，从而保证了枚举实例的唯一性。\n6.2.2. 反射造成单例模式不安全 通过反射强行调用私有构造器来生成实例对象，造成单例模式不安全。\n1 2 3 Class\u0026lt;?\u0026gt; aClass = Class.forName(\u0026#34;xx.xx.xx\u0026#34;); Constructor\u0026lt;?\u0026gt; constructor = aClass.getDeclaredConstructor(String.class); SingletonEnum singleton = (SingletonEnum) constructor.newInstance(\u0026#34;Demo\u0026#34;); 但是使用枚举创建的单例完全不用考虑这个问题，以下为newInstance的源码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class\u0026lt;?\u0026gt; caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers); } } // 如果是枚举类型，直接抛出异常，不让创建实例对象！ if ((clazz.getModifiers() \u0026amp; Modifier.ENUM) != 0) throw new IllegalArgumentException(\u0026#34;Cannot reflectively create enum objects\u0026#34;); ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings(\u0026#34;unchecked\u0026#34;) T inst = (T) ca.newInstance(initargs); return inst; } 如果是enum类型，则直接抛出异常Cannot reflectively create enum objects，无法通过反射创建实例对象！\n7. 枚举的治理 7.1. 概述 7.1.1. 为什么要使用枚举？ 表中某个字段标识了这条记录的状态，通常会使用一些code值来标识，例如01代表成功，00代表失败。多状态共性的东西可以常量保存，例如：\n1 2 3 4 class Constants{ public static final String success = \u0026#34;01\u0026#34;; public static final String failure= \u0026#34;00\u0026#34;; } 然而在一些大型项目中，表的数量极多，一些表中需要维护的状态也极多，如果都在一个常量类中维护，当需要添加或修改一个状态值时，就需要在庞大的类中找到对应的块，操作十分不便利。因此可以使用枚举，每个枚举类就只负责对一个状态做维护，这样即可方便增删改。例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public enum Payment { Payment_WX(\u0026#34;010000\u0026#34;, \u0026#34;微信支付\u0026#34;), Payment_ZFB(\u0026#34;010001\u0026#34;, \u0026#34;支付宝支付\u0026#34;), Payment_YL(\u0026#34;010002\u0026#34;, \u0026#34;银联支付\u0026#34;); public static final Map\u0026lt;String, String\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); static { Payment[] values = Payment.values(); if (values.length \u0026gt; 0) { for (Payment product : values) { map.put(product.getCode(), product.getName()); } } } Payment(String code, String name) { this.code = code; this.name = name; } private String code; private String name; // ...省略 getter/setter } 7.1.2. 为什么需要枚举治理？ 如果使用常量类，可以直接通过这个类的静态字段拿到值。当使用枚举时，尤其当枚举类逐渐增多时，此时不同的业务功能就可能需要获取不同的枚举类，然后再通过不同的枚举实例获取到不同的值。这种操作又会变得十分不便利。此时有如下的改进方法：\n改进一：以上面示例为例，把每个枚举类中实例存入到 map 集合中，把其 code 和 name 值映射进去，然后调用时通过静态 map 对象，把 code 值作为 key 传入，即可获取到对应的描述。 然而以上的改进后，依旧需要找到相应的枚举类，然后去使用它的静态 Map 集合的成员属性，能不能只通过一个类进行统一治理呢？\n改进二：通过一个类，把所有枚举都在该类中注册，然后通过该类直接获取到相应的枚举值及name描述。 7.2. 枚举治理的实现 7.2.1. 枚举的场景说明 通过枚举类中枚举名获取到枚举的code值，使用上面的枚举值定义为示例：{\u0026quot;Payment_WX\u0026quot;:\u0026quot;010000\u0026quot;,\u0026quot;Payment_YL\u0026quot;:\u0026quot;010002\u0026quot;,\u0026quot;Payment_ZFB\u0026quot;:\u0026quot;010001\u0026quot;} 1 if(param.equals(Payment.Payment_WX.getCode()){} 通过枚举类中枚举的code值获取到对应的name描述，使用上面的枚举值定义为示例：{\u0026quot;010002\u0026quot;:\u0026quot;银联支付\u0026quot;,\u0026quot;010001\u0026quot;:\u0026quot;支付宝支付\u0026quot;,\u0026quot;010000\u0026quot;:\u0026quot;微信支付\u0026quot;} 1 Payment.map.get(Payment.Payment_WX.getCode()); 7.2.2. 枚举治理工具类的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 public class VelocityEnumTools { public static final Logger logger = LoggerFactory.getLogger(VelocityEnumTools.class); // 通过枚举获取枚举code值，例如：{\u0026#34;Payment_WX\u0026#34;:\u0026#34;010000\u0026#34;,\u0026#34;Payment_YL\u0026#34;:\u0026#34;010002\u0026#34;,\u0026#34;Payment_ZFB\u0026#34;:\u0026#34;010001\u0026#34;} public static Map\u0026lt;String, Map\u0026lt;String, String\u0026gt;\u0026gt; mapKeyCode = new HashMap\u0026lt;\u0026gt;(); // 通过code值获取枚举name，例如：{\u0026#34;010002\u0026#34;:\u0026#34;银联支付\u0026#34;,\u0026#34;010001\u0026#34;:\u0026#34;支付宝支付\u0026#34;,\u0026#34;010000\u0026#34;:\u0026#34;微信支付\u0026#34;} public static Map\u0026lt;String, Map\u0026lt;String, String\u0026gt;\u0026gt; mapCodeName = new HashMap\u0026lt;\u0026gt;(); /** * 初始化项目中需要管理的枚举类，如Payment。其他枚举也类似添加即可 */ static { // 通过枚举获取code值 mapKeyCode.put(Payment.class.getSimpleName(), getEnumMap(Payment.class)); // 通过code值获取枚举name mapCodeName.put(Payment.class.getSimpleName(), getEnumCodeMap(Payment.class)); } /** * 通过枚举名称，取所有枚举实例名称(key) 与 code 属性值的映射集 * * @param enumKey * @return */ public static Map\u0026lt;String, String\u0026gt; getKeyCodeMapperInstance(String enumKey) { return mapKeyCode.get(enumKey); } /** * 通过枚举名称，获取所有枚举实例 code 与 name 属性值的映射集 * * @param enumKey * @return */ public static Map\u0026lt;String, String\u0026gt; getCodeNameMapperInstance(String enumKey) { return mapCodeName.get(enumKey); } /** * 根据枚举的类型，获取所有枚举实例名称(key) 与 code 属性值的映射集 * * @param clazz * @param \u0026lt;T\u0026gt; * @return */ public static \u0026lt;T\u0026gt; Map\u0026lt;String, String\u0026gt; getEnumMap(Class\u0026lt;T\u0026gt; clazz) { Map\u0026lt;String, String\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); try { if (clazz.isEnum()) { Object[] enumConstants = clazz.getEnumConstants(); // 获取所有枚举实例 for (int i = 0; i \u0026lt; enumConstants.length; i++) { T t = (T) enumConstants[i]; Field code = t.getClass().getDeclaredField(\u0026#34;code\u0026#34;); // 获取 code 属性对象 code.setAccessible(true); map.put(t.getClass().getDeclaredFields()[i].getName(), (String) code.get(t)); } } } catch (NoSuchFieldException e) { logger.error(\u0026#34;枚举工具启动报错：{}\u0026#34;, e); } catch (IllegalAccessException e) { logger.error(\u0026#34;枚举工具启动报错：{}\u0026#34;, e); } return map; } /** * 根据枚举的类型，获取所有枚举实例 code 与 name 属性值的映射集 * * @param clazz * @param \u0026lt;T\u0026gt; * @return */ private static \u0026lt;T\u0026gt; Map\u0026lt;String, String\u0026gt; getEnumCodeMap(Class\u0026lt;T\u0026gt; clazz) { Map\u0026lt;String, String\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); try { if (clazz.isEnum()) { Object[] enumConstants = clazz.getEnumConstants(); for (int i = 0; i \u0026lt; enumConstants.length; i++) { T t = (T) enumConstants[i]; Field code = t.getClass().getDeclaredField(\u0026#34;code\u0026#34;); // 获取 code 属性对象 Field name = t.getClass().getDeclaredField(\u0026#34;name\u0026#34;); // 获取 name 属性对象 code.setAccessible(true); name.setAccessible(true); map.put((String) code.get(t), (String) name.get(t)); } } } catch (NoSuchFieldException e) { logger.error(\u0026#34;枚举工具启动报错：{}\u0026#34;, e); } catch (IllegalAccessException e) { logger.error(\u0026#34;枚举工具启动报错：{}\u0026#34;, e); } return map; } } 测试：\n1 2 3 4 Map\u0026lt;String, String\u0026gt; enumMap = getEnumMap(Payment.class); System.out.println(enumMap); // {\u0026#34;Payment_WX\u0026#34;:\u0026#34;010000\u0026#34;,\u0026#34;Payment_YL\u0026#34;:\u0026#34;010002\u0026#34;,\u0026#34;Payment_ZFB\u0026#34;:\u0026#34;010001\u0026#34;} Map\u0026lt;String, String\u0026gt; enumCodeMap = getEnumCodeMap(Payment.class); System.out.println(enumCodeMap); // {\u0026#34;010002\u0026#34;:\u0026#34;银联支付\u0026#34;,\u0026#34;010001\u0026#34;:\u0026#34;支付宝支付\u0026#34;,\u0026#34;010000\u0026#34;:\u0026#34;微信支付\u0026#34;} 7.3. 枚举治理的扩展 - 在 velocity 中使用枚举（了解） Velocity 是一个基于 Java 的模板引擎，具有特定的语法，可以获取在 Java 语言中定义的对象，从而实现界面和 Java 代码的分离。本质就替代了以前老旧的 JSP 技术，是让后端人写前端页面逻辑的一种方式。以下示例使用这种技术，个人没有实际使用过，了解即可。\n7.3.1. 为什么会在velocity velocity 中使用枚举 当涉及与前端的交互时，可能需要从前端把三种支付方式对应的code值传到后台。此时，如果在页面上直接写010000这样的值，那么页面的逻辑就很不直观了，具体代码的意义除了加注释别无办法。\n因此为了解决后台可用，并且前端页面直观，可以尝试在页面上直接用枚举来解决问题。\n7.3.2. velocity 页面处理方式 1 2 3 4 #set($payment=$enumTool.getCodeNameMapperInstance(\u0026#34;Payment\u0026#34;)) // 直接写明要获取的枚举类型名称 #if($payment.get(\u0026#34;Payment_WX\u0026#34;) == $param.code) // 通过枚举值获取其code值 // 做微信支付页面逻辑 #end 7.3.3. velocity 中配置 velocity-tools 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;toolbox\u0026gt; \u0026lt;tool\u0026gt; \u0026lt;key\u0026gt;enumTool\u0026lt;/key\u0026gt; \u0026lt;class\u0026gt;com.moon.core.enumconstant.VelocityEnumTools\u0026lt;/class\u0026gt; \u0026lt;/tool\u0026gt; \u0026lt;tool\u0026gt; \u0026lt;key\u0026gt;stringTool\u0026lt;/key\u0026gt; \u0026lt;class\u0026gt;org.apache.commons.lang.StringUtils\u0026lt;/class\u0026gt; \u0026lt;/tool\u0026gt; \u0026lt;tool\u0026gt; \u0026lt;key\u0026gt;dateTool\u0026lt;/key\u0026gt; \u0026lt;class\u0026gt;org.apache.velocity.tools.generic.DateTool\u0026lt;/class\u0026gt; \u0026lt;/tool\u0026gt; \u0026lt;/toolbox\u0026gt; 经以上配置后，即可在页面中应用枚举治理工具类。例如：通过 code 值获取到相应描述\n1 $enumTool.getCodeNameMapperInstance(\u0026#34;Payment\u0026#34;).get($item.orderLoanStatus) // 显示“微信支付” 通过枚举获取到对应的 code 值\n1 2 #set($payment=$enumTool.getCodeNameMapperInstance(\u0026#34;Payment\u0026#34;)) // 拿到了Payment的map $payment.get(\u0026#34;Payment_WX\u0026#34;) 至此可以实现系统的中的枚举治理，并且可在前端页面灵活应用。\n","permalink":"https://ktzxy.top/posts/4s4cb7uckd/","summary":"Java基础 枚举","title":"Java基础 枚举"},{"content":"1. 全局命令 - 键(Key)的通用操作 Redis 对键(Key)的操作是通用的，不管 value 是五种类型中的哪一种类型，都可以用的操作\n1.1. KEYS 查询键 1.1.1. 基础使用 1 keys pattern 获取所有与pattern匹配的key，*表示任意0个或多个字符，?表示任意一个字符。\n比如：\nKEYS * 匹配数据库中所有 key KEYS h?llo 匹配 hello，hallo 和 hxllo 等 KEYS h*llo 匹配 hllo 和 heeeeello 等 KEYS h[ae]llo 匹配 hello 和 hallo ，但不匹配 hillo 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 redis\u0026gt; MSET one 1 two 2 three 3 four 4 # 一次设置 4 个 key OK redis\u0026gt; KEYS *o* 1) \u0026#34;four\u0026#34; 2) \u0026#34;two\u0026#34; 3) \u0026#34;one\u0026#34; redis\u0026gt; KEYS t?? 1) \u0026#34;two\u0026#34; redis\u0026gt; KEYS t[w]* 1) \u0026#34;two\u0026#34; redis\u0026gt; KEYS * # 匹配数据库内所有 key 1) \u0026#34;four\u0026#34; 2) \u0026#34;three\u0026#34; 3) \u0026#34;two\u0026#34; 4) \u0026#34;one\u0026#34; 1.1.2. keys 命令存在的问题 因为 Redis 是单线程的。keys 指令会导致线程阻塞一段时间，直到执行完毕，服务才能恢复。所以值得注意的是，如果存在大量键，线上禁止使用此指令\n1.2. SCAN 迭代集合元素 1.2.1. 基础使用 1 SCAN cursor [MATCH pattern] [COUNT count] SCAN 命令及其相关的命令(SSCAN、HSCAN、ZSCAN)都用于增量地迭代（incrementally iterate）一集元素（a collection of elements）：\nSCAN 命令用于迭代当前数据库中的数据库键。 SSCAN 命令用于迭代集合键中的元素。 HSCAN 命令用于迭代哈希键中的键值对。 ZSCAN 命令用于迭代有序集合中的元素（包括元素成员和元素分值）。 以上列出的四个命令都支持增量式迭代，它们每次执行都只会返回少量元素，所以这些命令可以用于生产环境，而不会出现像在大量键的情况下 KEYS 命令造成阻塞的问题。\n1.2.2. SCAN 命令优缺点 scan 的优点是：该命令采用渐进式遍历的方式来解决 keys 命令可能带来的阻塞问题，每次 scan 命令的时间复杂度是 O(1)，但是要真正实现 keys 的功能，需要执行多次 scan。\nscan 的缺点是：在执行命令的过程中，如果有键的变化（增加、删除、修改），遍历过程可能会出现，新增的键可能没有遍历到、遍历出了重复的键等情况。即 scan 命令并不能保证完整的遍历出来所有的键。\n1.3. DBSIZE 查询键总数 1 dbsize 查询目前存在的键的总数量。\n示例：\n1 2 redis\u0026gt; DBSIZE (integer) 5 1.4. EXISTS 检查键是否存在 1 exists key 判断该key是否存在，返回1表示存在，0表示不存在\n1 2 3 4 5 6 7 8 9 10 11 redis\u0026gt; SET db \u0026#34;redis\u0026#34; OK redis\u0026gt; EXISTS db (integer) 1 redis\u0026gt; DEL db (integer) 1 redis\u0026gt; EXISTS db (integer) 0 1.5. DEL 删除键 1 DEL key [key …] 删除给定的一个或多个 key。并返回被删除 key 的数量，如果删除不存在键返回0。无论值是什么数据结构类型，del命令都可以将其删除\n1 2 3 4 5 6 7 8 9 10 11 # 删除单个 key redis\u0026gt; DEL name (integer) 1 # 删除一个不存在的 key redis\u0026gt; DEL phone # 失败，没有 key 被删除 (integer) 0 # 同时删除多个 key redis\u0026gt; DEL name type website (integer) 3 1.6. EXPIRE 设置过期时间（秒级别） 1 expire key 为给定 key 设置生存时间(单位：秒)，当 key 过期时(生存时间为 0 )，它会被自动删除这个key。\n1 2 3 4 5 redis\u0026gt; EXPIRE cache_page 30 # 设置过期时间为 30 秒 (integer) 1 redis\u0026gt; EXPIRE cache_page 30000 # 如果在过期之前，再次使用EXPIRE命令，则更新过期时间 (integer) 1 1.7. TTL 查询剩余生存时间（秒级别） 1 ttl key 获取该key所剩余的超时时间（TTL, time to live），返回值类型如下：\n当 key 不存在时，返回-2 当 key 存在但没有设置剩余生存时间时，返回-1 当 key 存在且有设置剩余时间，则以秒为单位，返回大于等于0的整数（即 key 的剩余生存时间） 注：在 Redis 2.8 以前，当 key 不存在，或者 key 没有设置剩余生存时间时，命令都返回-1\n1 2 3 4 5 6 7 8 9 10 11 # 不存在的 key redis\u0026gt; TTL key (integer) -2 # key 存在，但没有设置剩余生存时间 redis\u0026gt; TTL key (integer) -1 # 有剩余生存时间的 key redis\u0026gt; TTL key (integer) 10084 1.8. EXPIREAT 设置生存时间（秒级别时间戳） 1 EXPIREAT key timestamp EXPIREAT 的作用和 EXPIRE 类似，都用于为 key 设置生存时间。不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。\n如果生存时间设置成功，返回1；当key不存在或没办法设置生存时间，返回0。\n1 2 redis\u0026gt; EXPIREAT cache 1355292000 # 这个 key 将在 2021.12.12 过期 (integer) 1 1.9. PEXPIRE 设置生存时间（毫秒级别） 1 PEXPIRE key milliseconds 这个命令和 EXPIRE 命令的作用类似，但是它以毫秒为单位设置 key 的生存时间，而EXPIRE命令以秒为单位。设置成功，返回1；key不存在或设置失败，返回0\n1 2 3 4 5 6 7 8 redis\u0026gt; PEXPIRE mykey 1500 (integer) 1 redis\u0026gt; TTL mykey # TTL 的返回值以秒为单位 (integer) 2 redis\u0026gt; PTTL mykey # PTTL 可以给出准确的毫秒数 (integer) 1499 1.10. PEXPIREAT 设置生存时间（毫秒级别时间戳） 1 PEXPIREAT key milliseconds-timestamp 这个命令和 expireat 命令类似，但它以毫秒为单位设置 key 的过期 unix 时间戳，而 expireat 命令则以秒为单位。如果生存时间设置成功，返回1。 当 key 不存在或没办法设置生存时间时，返回0\n1 2 3 4 5 6 7 8 redis\u0026gt; PEXPIREAT mykey 1555555555005 (integer) 1 redis\u0026gt; TTL mykey # TTL 返回秒 (integer) 223157079 redis\u0026gt; PTTL mykey # PTTL 返回毫秒 (integer) 223157079318 1.11. PTTL 查询剩余生存时间（毫秒级别） 1 PTTL key 这个命令类似于 TTL 命令，但它以毫秒为单位返回 key 的剩余生存时间，而 TTL 命令则以秒为单位。返回值类型如下：\n当 key 不存在时，返回-2 当 key 存在但没有设置剩余生存时间时，返回-1 当 key 存在且有设置剩余时间，则以毫秒为单位，返回大于等于0的整数（即 key 的剩余生存时间） 注：在 Redis 2.8 以前，当 key 不存在，或者 key 没有设置剩余生存时间时，命令都返回-1\n1.12. PERSIST 移除生存时间 1 PERSIST key 移除给定 key 的生存时间，相当于将这个key从“易失的”(带生存时间的key)转换成“持久的”(一个不带生存时间、永不过期的key)。当生存时间移除成功时，返回1； 如果 key 不存在或 key 没有设置生存时间，返回0。\n1 2 3 4 5 6 7 8 9 10 11 redis\u0026gt; EXPIRE mykey 10 # 为 key 设置生存时间 (integer) 1 redis\u0026gt; TTL mykey (integer) 10 redis\u0026gt; PERSIST mykey # 移除 key 的生存时间 (integer) 1 redis\u0026gt; TTL mykey (integer) -1 1.13. TYPE 键存储的数据结构类型 1 TYPE key 返回 key 所储存的值的数据结构类型(以字符串形式返回)。返回值：\nnone (key不存在) string (字符串) list (列表) set (集合) zset (有序集) hash (哈希表) stream (流) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 字符串 redis\u0026gt; SET weather \u0026#34;sunny\u0026#34; OK redis\u0026gt; TYPE weather string # 列表 redis\u0026gt; LPUSH book_list \u0026#34;programming in scala\u0026#34; (integer) 1 redis\u0026gt; TYPE book_list list # 集合 redis\u0026gt; SADD pat \u0026#34;dog\u0026#34; (integer) 1 redis\u0026gt; TYPE pat set 1.14. RANDOMKEY 随机获取一个key 1 RANDOMKEY 从当前数据库中随机返回(不删除)一个key。当数据库不为空时，返回一个key。当数据库为空时，返回nil\n1 2 3 4 redis\u0026gt; RANDOMKEY \u0026#34;food\u0026#34; redis\u0026gt; RANDOMKEY (nil) 1.15. RENAME 重命名 1 RENAME key newkey 将 key 重命名为 newkey。返回值如下：\n当key重命名成功时提示OK 当 key 和 newkey 相同，或者 key 不存在时，返回一个错误 特别注意：\n当newkey已经存在时，RENAME命令会覆盖旧值。为了防止被强行rename，Redis提供了renamenx命令，确保只有newKey不存在时候才被覆盖。 通过测试可知，由于重命名键期间会执行del命令删除旧的键，如果键对应的值比较大，会存在阻塞Redis的可能性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # key 存在且 newkey 不存在 redis\u0026gt; RENAME message greeting OK redis\u0026gt; EXISTS message # message 不复存在 (integer) 0 redis\u0026gt; EXISTS greeting # greeting 取而代之 (integer) 1 # 当 key 不存在时，返回错误 redis\u0026gt; RENAME fake_key never_exists (error) ERR no such key # newkey 已存在时， RENAME 会覆盖旧 newkey redis\u0026gt; SET name \u0026#34;moon\u0026#34; OK redis\u0026gt; SET name2 \u0026#34;kira\u0026#34; OK redis\u0026gt; RENAME name name2 OK redis\u0026gt; GET name (nil) redis\u0026gt; GET name2 # 原来的值 kira 被覆盖了 \u0026#34;moon\u0026#34; 1.16. SELECT 切换数据库 1 SELECT index 切换到指定的数据库，数据库索引号 index 用数字值指定。\n一个 Redis 服务器可以包括多个数据库，客户端可以指连接 Redis 中的的哪个数据库，就好比一个mysql服务器中创建多个数据库，客户端连接时指定连接到哪个数据库。Redis 实例最多可提供 16 个数据库，索引值从0到15，客户端默认连接索引值为0的数据库，也可以通过select命令选择哪个数据库。如果选择 16 时会报错，说明没有编号为 16 的数据库。\n1 2 redis\u0026gt; SELECT 1 # 使用 1 号数据库 OK 1.17. MOVE 迁移键 1 MOVE key db 将当前数据库的 key 移动到给定的数据库 db 当中。移动成功返回1，失败则返回0\n如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key ，或者 key 不存在于当前数据库，那么 MOVE 命令没有任何效果。因此，也可以利用这一特性，将 MOVE 命令当作锁(locking)原语(primitive)。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 # key 存在于当前数据库 redis\u0026gt; SELECT 0 # redis默认使用数据库 0，为了清晰起见，这里再显式指定一次。 OK redis\u0026gt; MOVE song 1 # 将 song 移动到数据库 1 (integer) 1 redis\u0026gt; EXISTS song # song 已经被移走 (integer) 0 redis\u0026gt; SELECT 1 # 使用数据库 1 OK redis:1\u0026gt; EXISTS song # 证实 song 被移到了数据库 1 (注意命令提示符变成了\u0026#34;redis:1\u0026#34;，表明正在使用数据库 1) (integer) 1 # 当 key 不存在的时候 redis:1\u0026gt; EXISTS fake_key (integer) 0 redis:1\u0026gt; MOVE fake_key 0 # 试图从数据库 1 移动一个不存在的 key 到数据库 0，失败 (integer) 0 redis:1\u0026gt; select 0 # 使用数据库0 OK redis\u0026gt; EXISTS fake_key # 证实 fake_key 不存在 (integer) 0 # 当源数据库和目标数据库有相同的 key 时 redis:1\u0026gt; SELECT 0 # 使用数据库0，并试图将 favorite_fruit 移动到数据库 1 OK redis\u0026gt; MOVE favorite_fruit 1 # 因为两个数据库有相同的 key，MOVE 失败 (integer) 0 redis\u0026gt; GET favorite_fruit # 数据库 0 的 favorite_fruit 没变 \u0026#34;banana\u0026#34; redis\u0026gt; SELECT 1 OK redis:1\u0026gt; GET favorite_fruit # 数据库 1 的 favorite_fruit 也是 \u0026#34;apple\u0026#34; 1.18. 其他小结 1.18.1. KEYS 与 DBSIZE 命令小结 dbsize 命令在计算键总数时不会遍历所有键，而是直接获取 Redis 内置的键总数变量，所以dbsize命令的时间复杂度是O(1)。 keys 命令会遍历所有键，所以它的时间复杂度是o(n)，当 Redis 保存了大量键时线上环境禁止使用keys命令。 1.18.2. 关于使用 Redis 相关过期命令时注意点 如果使用 expire key 命令时相应的键不存在，返回结果为 0 如果过期时间为负值，键会立即被删除，效果与使用 del 命令一样 persist 命令可以将键的过期时间清除 对于字符串类型的键，执行 set 命令后，会重置过期时间（如果没有设置则重置为不过期），这个问题很容易在开发中被忽视。 Redis 不支持二级数据结构（例如哈希、列表）内部元素的过期功能，例如不能对列表类型的一个元素做过期时间设置。 如果关了 Redis 服务器端，在默认情况下从控制台插入的 key=value 键值对数据，就算 key 时间未到，也会自动销毁。 2. String 类型命令（重点） 字符串类型是 Redis 中最为基础的数据存储类型，它在 Redis 中是二进制安全的，这便意味着该类型存入和获取的数据相同。字符串类型的值实际可以是简单的字符串、复杂的字符串(例如 JSON、XML)、数字(整数、浮点数)，甚至是二进制(图片、音频、视频)，在 Redis 中字符串类型的 Value 最多可以容纳的数据长度是 512M。\n注：Redis 所有类型的键都是字符串类型，而且其他几种数据结构都是在字符串类型基础上构建的。\n2.1. SET 赋值 1 SET key value [EX seconds] [PX milliseconds] [NX|XX] 设定key持有指定的字符串value，如果该key存在则进行覆盖操作，无视类型。返回结果为OK代表设置成功。\n当 SET 命令对一个带有生存时间（TTL）的键进行设置之后，该键原有的生存时间将被清除。\n可选参数：\n从 Redis 2.6.12 版本开始，SET命令的行为可以通过一系列参数来修改：\nEX seconds：将键的过期时间设置为seconds秒。执行SET key value EX seconds的效果等同于执行SETEX key seconds value PX milliseconds：将键的过期时间设置为milliseconds毫秒。执行SET key value PX milliseconds的效果等同于执行PSETEX key milliseconds value NX：只在键不存在时，才对键进行设置操作。执行SET key value NX的效果等同于执行SETNX key value XX：只在键已经存在时，才对键进行设置操作 note：因为SET命令可以通过参数来实现SETNX、SETEX以及PSETEX命令的效果，所以 Redis 将来的版本可能会移除并废弃SETNX、SETEX和PSETEX这三个命令。\n返回值：\n在 Redis 2.6.12 版本以前，SET 命令总是返回OK 从 Redis 2.6.12 版本开始，SET 命令只在设置操作成功完成时才返回OK；如果命令使用了NX或者XX选项，但是因为条件没达到而造成设置操作未执行，那么命令将返回空批量回复（NULL Bulk Reply） 代码示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 # 对不存在的键进行设置： redis\u0026gt; SET key \u0026#34;value\u0026#34; OK redis\u0026gt; GET key \u0026#34;value\u0026#34; # 对已存在的键进行设置： redis\u0026gt; SET key \u0026#34;new-value\u0026#34; OK redis\u0026gt; GET key \u0026#34;new-value\u0026#34; # 使用 EX 选项： redis\u0026gt; SET key-with-expire-time \u0026#34;hello\u0026#34; EX 10086 OK redis\u0026gt; GET key-with-expire-time \u0026#34;hello\u0026#34; redis\u0026gt; TTL key-with-expire-time (integer) 10069 # 使用 PX 选项： redis\u0026gt; SET key-with-pexpire-time \u0026#34;moto\u0026#34; PX 123321 OK redis\u0026gt; GET key-with-pexpire-time \u0026#34;moto\u0026#34; redis\u0026gt; PTTL key-with-pexpire-time (integer) 111939 # 使用 NX 选项： redis\u0026gt; SET not-exists-key \u0026#34;value\u0026#34; NX OK # 键不存在，设置成功 redis\u0026gt; GET not-exists-key \u0026#34;value\u0026#34; redis\u0026gt; SET not-exists-key \u0026#34;new-value\u0026#34; NX (nil) # 键已经存在，设置失败 redis\u0026gt; GEt not-exists-key \u0026#34;value\u0026#34; # 维持原值不变 # 使用 XX 选项： redis\u0026gt; EXISTS exists-key (integer) 0 redis\u0026gt; SET exists-key \u0026#34;value\u0026#34; XX (nil) # 因为键不存在，设置失败 redis\u0026gt; SET exists-key \u0026#34;value\u0026#34; OK # 先给键设置一个值 redis\u0026gt; SET exists-key \u0026#34;new-value\u0026#34; XX OK # 设置新值成功 redis\u0026gt; GET exists-key \u0026#34;new-value\u0026#34; 2.2. SETNX 不存在时赋值 1 SETNX key value 只在键key不存在的情况下，将键key的值设置为value。若键key已经存在，则SETNX命令不做任何动作。设置成功时返回1，设置失败时返回0\nSETNX是『SET if Not exists』(如果不存在，则SET)的简写。\n1 2 3 4 5 6 7 8 redis\u0026gt; EXISTS job # job 不存在 (integer) 0 redis\u0026gt; SETNX job \u0026#34;programmer\u0026#34; # job 设置成功 (integer) 1 redis\u0026gt; SETNX job \u0026#34;code-farmer\u0026#34; # 尝试覆盖 job ，失败 (integer) 0 redis\u0026gt; GET job # 没有被覆盖 \u0026#34;programmer\u0026#34; note: 由于Redis的单线程命令处理机制，如果有多个客户端同时执行setnx key value，根据setnx的特性只有一个客户端能设置成功，setnx可以作为分布式锁的一种实现方案。\n2.3. SETEX 赋值并设置生存时间(秒级别) 1 SETEX key seconds value 将键 key 的值设置为 value，并将键 key 的生存时间设置为 seconds 秒钟。如果键 key 已经存在， 那么 SETEX 命令将覆盖已有的值。设置成功时返回OK。 当 seconds 参数不合法时，命令将返回一个错误。\nSETEX 命令效果相当于以下命令：\n1 2 SET key value EXPIRE key seconds # 设置生存时间 SETEX 与以上两条命令组合的区别在于 SETEX 是一个原子（atomic）操作，它可以在同一时间内完成设置值和设置过期时间这两个操作，因此 SETEX 命令在储存缓存的时候非常实用。\n代码示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 在键 key 不存在的情况下执行 SETEX ： redis\u0026gt; SETEX cache_user_id 60 10086 OK redis\u0026gt; GET cache_user_id # 值 \u0026#34;10086\u0026#34; redis\u0026gt; TTL cache_user_id # 剩余生存时间 (integer) 49 # 键 key 已经存在， 使用 SETEX 覆盖旧值： redis\u0026gt; SET cd \u0026#34;timeless\u0026#34; OK redis\u0026gt; SETEX cd 3000 \u0026#34;goodbye my love\u0026#34; OK redis\u0026gt; GET cd \u0026#34;goodbye my love\u0026#34; redis\u0026gt; TTL cd (integer) 2997 2.4. PSETEX 赋值并设置生存时间(毫秒级别) 1 PSETEX key milliseconds value PSETEX 和 SETEX 命令相似，但它以毫秒为单位设置 key 的生存时间，而 SETEX 命令则以秒为单位进行设置。在设置成功时返回OK\n代码示例：\n1 2 3 4 5 6 redis\u0026gt; PSETEX mykey 1000 \u0026#34;Hello\u0026#34; OK redis\u0026gt; PTTL mykey (integer) 999 redis\u0026gt; GET mykey \u0026#34;Hello\u0026#34; 2.5. GET 取值 1 GET key 返回与键 key 相关联的字符串值。返回值情况如下：\n如果键 key 不存在， 那么返回特殊值 nil；否则，返回键 key 的值 如果键 key 的值并非字符串类型，那么返回一个错误，因为 GET 命令只能用于字符串值。 代码示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # 对不存在的键 key 或是字符串类型的键 key 执行 GET 命令： redis\u0026gt; GET db (nil) redis\u0026gt; SET db redis OK redis\u0026gt; GET db \u0026#34;redis\u0026#34; # 对不是字符串类型的键 key 执行 GET 命令： redis\u0026gt; LPUSH db redis mongodb mysql (integer) 3 redis\u0026gt; GET db (error) ERR Operation against a key holding the wrong kind of value 2.6. GETSET 先取值再赋值 1 GETSET key value 将键 key 的值设为 value，并返回键 key 在被设置之前的旧值。返回值情况如下：\n返回给定键 key 的旧值 如果键 key 没有旧值，也即是说，键 key 在被设置之前并不存在，那么命令返回 nil 如果键 key 存在但不是字符串类型时，命令返回一个错误 代码示例：\n1 2 3 4 5 6 7 8 redis\u0026gt; GETSET db mongodb # 没有旧值，返回 nil (nil) redis\u0026gt; GET db \u0026#34;mongodb\u0026#34; redis\u0026gt; GETSET db redis # 返回旧值 mongodb \u0026#34;mongodb\u0026#34; redis\u0026gt; GET db \u0026#34;redis\u0026#34; 2.7. MSET 批量赋值 1 MSET key value [key value …] 同时为多个键批量设置值。MSET 命令总是返回OK。如果某个给定键已经存在，那么 MSET 将使用新值去覆盖旧值。如果不想覆盖旧值，则使用MSETNX，此命令只会在所有给定键都不存在的情况下进行设置。\nMSET 是一个原子性(atomic)操作，所有给定键都会在同一时间内被设置，不会出现某些键被设置了但是另一些键没有被设置的情况。\n代码示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 同时对多个键进行设置： redis\u0026gt; MSET date \u0026#34;2021.3.30\u0026#34; time \u0026#34;11:00 a.m.\u0026#34; weather \u0026#34;sunny\u0026#34; OK redis\u0026gt; MGET date time weather 1) \u0026#34;2021.3.30\u0026#34; 2) \u0026#34;11:00 a.m.\u0026#34; 3) \u0026#34;sunny\u0026#34; # 覆盖已有的值： redis\u0026gt; MGET k1 k2 1) \u0026#34;hello\u0026#34; 2) \u0026#34;world\u0026#34; redis\u0026gt; MSET k1 \u0026#34;good\u0026#34; k2 \u0026#34;bye\u0026#34; OK redis\u0026gt; MGET k1 k2 1) \u0026#34;good\u0026#34; 2) \u0026#34;bye\u0026#34; 2.8. MGET 批量取值 1 MGET key [key …] 返回给定的一个或多个字符串键的值。MGET 命令结果是返回一个列表，列表中包含了所有给定键的值，是按照传入键的顺序返回。如果给定的字符串键里面，有某个键不存在，那么这个键的值将以特殊值 nil 表示。\n代码示例：\n1 2 3 4 redis\u0026gt; MGET redis mongodb mysql # 不存在的 mysql 返回 nil 1) \u0026#34;redis.com\u0026#34; 2) \u0026#34;mongodb.org\u0026#34; 3) (nil) note:\n批量操作命令可以有效提高效率，假如没有mget这样的命令，要执行n次get命令具体耗时是：n次get时间=n次网络时间+n次命令时间\n使用mget命令后，要执行n次get命令操作具体耗时是：n次get时间=1次网络时间+n次命令时间\nRedis可以支撑每秒数万的读写操作，但是这指的是Redis服务端的处理能力，对于客户端来说，一次命令除了命令时间还是有网络时间，假设网络时间为1毫秒，命令时间为0.1毫秒(按照每秒处理1万条命令算)，那么执行1000次get命令需要1.1秒(1000*1+1000*0.1=1100ms)，1次mget命令的需要0.101秒(1*1+1000*0.1=101ms)。\n2.9. INCR 数字递增 1 INCR key 为键 key 储存的数字值加1。INCR 命令会返回键 key 在执行加1操作之后的值。存在以下3种情况：\n如果键 key 不存在，那么它的值会先被初始化为0，然后再执行 INCR 命令。 如果键 key 存在并且储存的值为数字，则在原数值加1后替换原来的值，并返回加1后的结果 如果键 key 储存的值不能被解释为数字，那么 INCR 命令将返回一个错误。 note:\n本操作的值限制在 64 位(bit)有符号数字表示之内。 INCR 命令是一个针对字符串的操作。因为 Redis 并没有专用的整数类型，所以键 key 储存的值在执行 INCR 命令时会被解释为十进制64位有符号整数。 代码示例：\n1 2 3 4 5 6 redis\u0026gt; SET page_view 20 OK redis\u0026gt; INCR page_view (integer) 21 redis\u0026gt; GET page_view # 数字值在 Redis 中以字符串的形式保存 \u0026#34;21\u0026#34; 2.10. DECR 数字递减 1 DECR key 为键 key 储存的数字值减1。DECR 命令会返回键 key 在执行减1操作之后的值。存在以下3种情况：\n如果键 key 不存在，那么它的值会先被初始化为0，然后再执行 DECR 命令。 如果键 key 存在并且储存的值为数字，则在原数值减1后替换原来的值，并返回减1后的结果 如果键 key 储存的值不能被解释为数字，那么 DECR 命令将返回一个错误。 note: 本操作的值限制在 64 位(bit)有符号数字表示之内。\n1 2 3 4 5 6 7 8 9 10 11 # 对储存数字值的键 key 执行 DECR 命令： redis\u0026gt; SET failure_times 10 OK redis\u0026gt; DECR failure_times (integer) 9 # 对不存在的键执行 DECR 命令： redis\u0026gt; EXISTS count (integer) 0 redis\u0026gt; DECR count (integer) -1 2.11. INCRBY/DECRBY 数字递增/递减指定指定值 1 2 3 4 # 递增指定值 INCRBY key increment # 递减指定值 DECRBY key decrement 为键 key 储存的数字值加上增量 increment/减去减量decrement，并返回该值。存在以下3种情况：\n如果键 key 不存在，那么它的值会先被初始化为0，然后再执行 INCRBY/DECRBY 命令。 如果键 key 存在并且储存的值为数字，则在原数值加上increment值/减去decrement值后替换原来的值，并返回结果 如果键 key 储存的值不能被解释为数字，那么 INCRBY/DECRBY 命令将返回一个错误。 note: 本操作的值限制在 64 位(bit)有符号数字表示之内。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 # 键存在，并且值为数字： redis\u0026gt; SET rank 50 OK redis\u0026gt; INCRBY rank 20 (integer) 70 redis\u0026gt; GET rank \u0026#34;70\u0026#34; # 键不存在： redis\u0026gt; EXISTS counter (integer) 0 redis\u0026gt; INCRBY counter 30 (integer) 30 redis\u0026gt; GET counter \u0026#34;30\u0026#34; # 键存在，但值无法被解释为数字： redis\u0026gt; SET book \u0026#34;long long ago...\u0026#34; OK redis\u0026gt; INCRBY book 200 (error) ERR value is not an integer or out of range # 对已经存在的键执行 DECRBY 命令： redis\u0026gt; SET count 100 OK redis\u0026gt; DECRBY count 20 (integer) 80 # 对不存在的键执行 DECRBY 命令： redis\u0026gt; EXISTS pages (integer) 0 redis\u0026gt; DECRBY pages 10 (integer) -10 2.12. APPEND 字符追加 1 APPEND key value append 指令可以向字符串尾部追加值。返回值为追加 value 之后，键 key 的值的长度。\n如果键 key 已经存在并且它的值是一个字符串，APPEND 命令将把 value 追加到键 key 现有值的末尾。 如果键 key 不存在，APPEND 就简单地将键 key 的值设为 value，就像执行 SET key value 一样。 1 2 3 4 5 6 7 8 9 10 11 # 对不存在的 key 执行 APPEND ： redis\u0026gt; EXISTS myphone # 确保 myphone 不存在 (integer) 0 redis\u0026gt; APPEND myphone \u0026#34;nokia\u0026#34; # 对不存在的 key 进行 APPEND ，等同于 SET myphone \u0026#34;nokia\u0026#34; (integer) 5 # 字符长度 # 对已存在的字符串进行 APPEND ： redis\u0026gt; APPEND myphone \u0026#34; - 1110\u0026#34; # 长度从 5 个字符增加到 12 个字符 (integer) 12 redis\u0026gt; GET myphone \u0026#34;nokia - 1110\u0026#34; 2.13. STRLEN 字符串长度 1 STRLEN key 返回键 key 储存的字符串值的长度。当键 key 不存在时，命令返回0。当 key 储存的不是字符串值时，返回一个错误。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 获取字符串值的长度： redis\u0026gt; SET mykey \u0026#34;Hello world\u0026#34; OK redis\u0026gt; STRLEN mykey (integer) 11 # 不存在的键的长度为 0 ： redis\u0026gt; STRLEN nonexisting (integer) 0 # 中文字符串 redis\u0026gt; SET chinese \u0026#34;中文\u0026#34; OK redis\u0026gt; STRLEN chinese (integer) 6 注意：每个中文占 3 个字节\n2.14. GETSET 设置并返回原值 1 GETSET key value getset 和 set 一样会设置值，将键 key 的值设为 value，但 GETSET 命令会返回键 key 在被设置之前的旧值。存在以下特殊情况：\n当键 key 在被设置之前并不存在，返回 nil 当键 key 存在但不是字符串类型时，返回一个错误 1 2 3 4 5 6 7 8 redis\u0026gt; GETSET db mongodb # 没有旧值，返回 nil (nil) redis\u0026gt; GET db \u0026#34;mongodb\u0026#34; redis\u0026gt; GETSET db redis # 返回旧值 mongodb \u0026#34;mongodb\u0026#34; redis\u0026gt; GET db \u0026#34;redis\u0026#34; 2.15. SETRANGE 设置指定位置的字符 1 SETRANGE key offset value 从偏移量 offset 开始， 用 value 参数覆写(overwrite)键 key 储存的字符串值，并返回被修改之后，字符串值的长度。不存在的键 key 当作空白字符串处理。值的下标是从0开始计算。\nSETRANGE 命令会确保字符串足够长以便将 value 设置到指定的偏移量上， 如果键 key 原来储存的字符串长度比偏移量小(比如字符串只有5个字符长，但设置的offset是10)，那么原字符和偏移量之间的空白将用零字节(zerobytes, \u0026quot;\\x00\u0026quot;)进行填充。\nWarning: 当生成一个很长的字符串时， Redis 需要分配内存空间， 该操作有时候可能会造成服务器阻塞(block)。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 对非空字符串执行 SETRANGE 命令： redis\u0026gt; SET greeting \u0026#34;hello world\u0026#34; OK redis\u0026gt; SETRANGE greeting 6 \u0026#34;Redis\u0026#34; (integer) 11 redis\u0026gt; GET greeting \u0026#34;hello Redis\u0026#34; # 对空字符串/不存在的键执行 SETRANGE 命令： redis\u0026gt; EXISTS empty_string (integer) 0 redis\u0026gt; SETRANGE empty_string 5 \u0026#34;Redis!\u0026#34; # 对不存在的 key 使用 SETRANGE (integer) 11 redis\u0026gt; GET empty_string # 空白处被\u0026#34;\\x00\u0026#34;填充 \u0026#34;\\x00\\x00\\x00\\x00\\x00Redis!\u0026#34; 2.16. GETRANGE 截取字符串 1 GETRANGE key start end 返回键 key 储存的字符串值的指定部分，字符串的截取范围由 start 和 end 两个偏移量决定(包括 start 和 end 在内)。负数偏移量表示从字符串的末尾开始计数，-1 表示最后一个字符，-2 表示倒数第二个字符，以此类推。\nGETRANGE 通过保证子字符串的值域(range)不超过实际字符串的值域来处理超出范围的值域请求。\nnote: GETRANGE 命令在 Redis 2.0 之前的版本里面被称为 SUBSTR 命令\n1 2 3 4 5 6 7 8 9 10 11 12 redis\u0026gt; SET greeting \u0026#34;hello, my friend\u0026#34; OK redis\u0026gt; GETRANGE greeting 0 4 # 返回索引0-4的字符，包括4。 \u0026#34;hello\u0026#34; redis\u0026gt; GETRANGE greeting -1 -5 # 不支持回绕操作 \u0026#34;\u0026#34; redis\u0026gt; GETRANGE greeting -3 -1 # 负数索引 \u0026#34;end\u0026#34; redis\u0026gt; GETRANGE greeting 0 -1 # 从第一个到最后一个 \u0026#34;hello, my friend\u0026#34; redis\u0026gt; GETRANGE greeting 0 1008611 # 值域范围不超过实际字符串，超过部分自动被符略 \u0026#34;hello, my friend\u0026#34; 2.17. 命令的时间复杂度 字符串这些命令中，除了del、mset、mget支持多个键的批量操作，时间复杂度和键的个数相关，为O(n)，getrange和字符串长度相关，也是O(n)，其余的命令基本上都是O(1)的时间复杂度，所以操作速度非常快\n3. Hash 类型命令 Redis中的Hash类型可以看成具有String Key和String Value的map容器。所以该类型非常适合于存储值对象的信息。如Username、Password和Age等。如果Hash中包含很少的字段，那么该类型的数据也将仅占用很少的磁盘空间。每一个Hash可以存储4294967295个键值对。\n3.1. 赋值 hset key field value：为指定的key设定field/value对（键值对）。 hmset key field value [field2 value2 …]：设置key中的多个filed/value 3.2. 取值 hget key field：\t返回指定的key中的field的值 hmget key fileds：获取key中的多个filed的值 hgetall key：获取key中的所有filed-vaule 3.3. 删除 hdel key field [field … ]：可以删除一个或多个字段，返回值是被删除的字段个数，当value都删除后，key也会删除了 del key：删除对应的key的整个Hash 3.4. 增加数字 1 hincrby key field increment 设置 key 中 filed 的值增加 increment。如：hincrby age 20 increment，age 增加 20 3.5. 其他命令 hexists key field：判断指定的key中的filed是否存在 hlen key：获取key所包含的field的数量 hkeys key：获得所有的key hvals key：获得所有的value 4. List 类型命令 在Redis中，list类型是按照插入顺序排序的字符串链表。我们可以在其头部（left）和尾部（right）添加新的元素。在插入时，如果该key不存在，Redis将为该key创建一个新的链表。与此相反，如果链表中的所有元素都被删除了，那么该key也将会被从数据库中删除。list中可以包含的最大元素数据量4294967295（十亿以上）。\n在 java 中 List 接口有常用的有 ArrayList、LinkedList，而在 redis 中的 list 就类似于 java 中的 LinkedList。\n4.1. 两端添加 1 lpush key value1 value2 ... 在指定的 key 对应的 list 的头部插入所有的 value，如果该 key 不存在，该命令在插入之前创建一个与该 key 对应的空链表，再从头部插入数据。插入成功，返回元素的个数。 1 rpush key value1 value2 ... 在指定的 key 对应的 list 的尾部插入所有的 value，如果该 key 不存在，该命令在插入之前创建一个与该 key 对应的空链表，再从尾部插入数据。插入成功，返回元素的个数。 4.2. 查看列表 lrange key start end：获取链表中从start到end的元素的值，start和end从0开始计数，如果为负数，-1表示倒数第一个元素，-2表示倒数第二个元素，以此类推。 查看所有列表：(0~-1就可以查看所有值)。例：lrange key 0 -1 4.3. 两边弹出 1 lpop key 返回并弹出指定的key对应链表中头部（left）第一个元素，如果该key不存在，返回nil。 1 rpop key 返回并弹出指定的key对应链表中尾部（right）第一个元素，如果该key不存在，返回nil。 4.4. 获取列表中元素的个数 1 llen key 返回指定 key 对应链表中元素的个数。命令的 l 代表 list，len 代表 length 5. Set 类型命令 Redis 中，可以将set类型看作是没有排序的字符集合，set中可以包含的最大元素数据量4294967295（十亿以上）。\n和list不同，set集合不允许出现重复元素，如果多次添加相同元素，set中仅保留一份。\n5.1. 添加/删除元素 sadd key value1 value2……：向set中添加数据，如果该key的值已存在，则不会重复添加，返回添加成功个数 srem key value1 value2……：删除set中指定的成员，返回删除成功个数 5.2. 获得集体中的元素 smembers key：获取set集合中所有元素\n直接删除key，那么key对应的list-set-sortedset都会删除； 如果key对应的所有值删除了，那么key也会自动被删除 5.3. 判断元素是否在集合中存在 sismember key value：判断key中指定的元素是否在该set集合中存在。存在则返回1，不存在则返回0 6. SortedSet 类型命令 SortedSet 和 Set 类型极为类似，它们都是字符串的集合，都不允许重复的元素出现在一个 Set 中。它们之间的主要区别是SortedSet 中每一个元素都会有一个分数（score）与之关联，Redis 正是通过分数来为集合中的元素进行从小到大的排序（默认）。\nSortedSet 集合中的元素必须是唯一的，但分数（score）却是可以重复。\n6.1. 添加元素 1 zadd key score value score value score value 将所有元素以及对应的分数，存放到 sortedset 集合中，如果该元素已存在则会用新的分数替换原来的分数。返回值是新加入到集合中的元素个数，不包含之前已经存在的元素。 6.2. 查询元素（从小到大） 1 zrange key start end 获取集合中下标为 start 到 end 的元素，不带分数排序之后的 SortedSet 与 list 的位置是一样，位置从左到右是正数，从0开始，位置从右到左是负数，从-1开始，-1是倒数第一个元素，-2倒数第二个元素 1 zrange key start end withscores 获取集合中下标为 start 到 end 的元素，带分数按分数从小到大排序。如果相同用户的话，不会再将用户名插入集合中，但分数可以替换原来的分数 6.3. 查询元素（从大到小） 1 zrevrange key start end 按照元素分数从大到小，获取集合中下标为start到end的元素，不带分数 1 zrevrange key start end withscores 按照元素分数从大到小，获取集合中下标为start到end的元素，带分数 6.4. 获取元素分数 1 zscore key member 返回指定元素的分数 6.5. 获取元素数量 1 zcard key 获取集合中元素数量 6.6. 删除元素 1 zrem key member member member 从集合中删除指定的元素 6.7. 按照分数范围删除元素 1 zremrangebyscore key min max 按照分数范围删除元素 7. Redis 其他命令（了解） 7.1. pipeline 批命令 Redis 客户端执行一条命令分4个过程：发送命令、命令排队、命令执行、返回结果。使用 pipeline 可以批量请求，批量返回结果，将多次 IO 往返的时间缩减为一次，执行速度比逐条执行要快。使用时有以下注意事项：\n使用 pipeline 执行的指令之间没有因果相关性。因为 pipeline 命令是非原子性，即 pipeline 命令中途异常退出，之前执行成功的命令不会回滚。 使用 pipeline 组装的命令个数不能太多，不然数据量过大，增加客户端的等待时间，还可能造成网络阻塞，可以将大量命令的拆分多个小的 pipeline 命令完成。 原生批命令（mset和mget）与 pipeline 对比：\n原生批命令是原子性；pipeline 是非原子性。 原生批命令只有一个命令；但 pipeline 支持多命令。 7.2. 服务器命令 ping，测试连接是否存活。停止 redis 服务器再测试此命令： 1 2 redis 127.0.0.1:6379\u0026gt; ping Could not connect to Redis at 127.0.0.1:6379: Connection refused echo，在命令行打印一些内容。 quit，退出连接。 info，获取服务器的信息和统计。 flushdb，删除当前选择数据库中的所有key。 flushall，删除所有数据库中的所有key。 7.3. 消息订阅与发布 subscribe：订阅指定的一个频道的信息。例如，subscribe mychat，订阅“mychat”这个频道 psubscribe：订阅一个或多个符合指定模式的频道。例如，psubscribe s*：批量订阅以“s”开头的频道 unsubscribe：取消订阅指定的频道 pubsub：查看订阅与发布系统的状态 publish：在指定的频道中发布消息。例如，publish mychat 'today is a newday' 7.4. redis 事务 7.4.1. multi 开启事务 1 multi 开启事务，用于标记事务的开始，其后执行的命令都将被存入命令队列，直到执行 EXEC 命令时，这些命令才会被原子的执行，类似与关系型数据库中的：begin transaction 7.4.2. exec 提交事务 1 exec 提交事务，会执行所有事务块内的命令。类似与关系型数据库中的：commit 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 127.0.0.1:6379\u0026gt; multi OK 127.0.0.1:6379\u0026gt; set a 1 QUEUED 127.0.0.1:6379\u0026gt; set b 1 2 QUEUED 127.0.0.1:6379\u0026gt; set c 3 QUEUED 127.0.0.1:6379\u0026gt; exec 1) OK 2) (error) ERR syntax error 3) OK 7.4.3. discard 事务回滚 1 discard 事务回滚，清空事务队列，并放弃执行事务块内的所有命令，并且客户端会从事务状态中退出。类似与关系型数据库中的：rollback 7.4.4. watch 监听键 1 watch keyName 监视一个（或多个）key，如果在事务执行之前这个（或这些）key 被其他命令改动，那么事务将被打断（类似于乐观锁）。另外执行 EXEC 命令之后，也会自动取消监控。 使用示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 127.0.0.1:6379\u0026gt; watch name OK 127.0.0.1:6379\u0026gt; set name 1 OK 127.0.0.1:6379\u0026gt; multi OK 127.0.0.1:6379\u0026gt; set name 2 QUEUED 127.0.0.1:6379\u0026gt; set gender 1 QUEUED 127.0.0.1:6379\u0026gt; exec (nil) 127.0.0.1:6379\u0026gt; get gender (nil) 示例命令实现说明：\nwatch name 开启了对 name 这个 key 的监控 修改 name 的值 开启事务 a 在事务 a 中设置了 name 和 gender 的值 使用 EXEC 命令进提交事务 使用命令 get gender 发现不存在，即事务 a 没有执行 7.4.5. unwath 取消所有控制 1 unwath 取消 watch 命令对所有 key 的监控，所有监控锁将会被取消。 8. LUA 脚本 8.1. 概述 Redis 通过 LUA 脚本创建具有原子性的命令：当 lua 脚本命令正在运行的时候，不会有其他脚本或 Redis 命令被执行，实现组合命令的原子操作。\nlua 脚本作用：\nLua 脚本在 Redis 中是原子执行的，执行过程中间不会插入其他命令。 Lua 脚本可以将多条命令一次性打包，有效地减少网络开销。 8.2. Lua 脚本的使用 在 Redis 中执行 Lua 脚本有两种方法：eval 和 evalsha。eval 命令使用内置的 Lua 解释器，对 Lua 脚本进行求值。\n1 2 3 4 5 6 // 第一个参数是lua脚本，第二个参数是键名参数个数，剩下的是键名参数和附加参数 127.0.0.1:6379\u0026gt; eval \u0026#34;return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}\u0026#34; 2 key1 key2 first second 1) \u0026#34;key1\u0026#34; 2) \u0026#34;key2\u0026#34; 3) \u0026#34;first\u0026#34; 4) \u0026#34;second\u0026#34; 8.3. 应用场景 8.3.1. 限制接口访问频率(未验证正确性) 在 Redis 维护一个接口访问次数的键值对，key 是接口名称，value 是访问次数。每次访问接口时，会执行以下操作：\n通过 aop 拦截接口的请求，对接口请求进行计数，每次进来一个请求，相应的接口访问次数 count 加 1，存入 redis。 如果是第一次请求，则会设置 count=1，并设置过期时间。因为这里 set() 和 expire() 组合操作不是原子操作，所以需要引入 lua 脚本实现原子操作，避免并发访问问题。 如果给定时间范围内超过最大访问次数，则会抛出异常。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private String buildLuaScript() { return \u0026#34;local c\u0026#34; + \u0026#34;\\nc = redis.call(\u0026#39;get\u0026#39;,KEYS[1])\u0026#34; + \u0026#34;\\nif c and tonumber(c) \u0026gt; tonumber(ARGV[1]) then\u0026#34; + \u0026#34;\\nreturn c;\u0026#34; + \u0026#34;\\nend\u0026#34; + \u0026#34;\\nc = redis.call(\u0026#39;incr\u0026#39;,KEYS[1])\u0026#34; + \u0026#34;\\nif tonumber(c) == 1 then\u0026#34; + \u0026#34;\\nredis.call(\u0026#39;expire\u0026#39;,KEYS[1],ARGV[2])\u0026#34; + \u0026#34;\\nend\u0026#34; + \u0026#34;\\nreturn c;\u0026#34;; } @Test public void testLua() { String luaScript = buildLuaScript(); RedisScript\u0026lt;Number\u0026gt; redisScript = new DefaultRedisScript\u0026lt;\u0026gt;(luaScript, Number.class); Number count = redisTemplate.execute(redisScript, keys, limit.count(), limit.period()); } Notes: 这种接口限流的实现方式只是示例，实现比较简单，问题也比较多，一般不会使用，接口限流用的比较多的是令牌桶算法和漏桶算法。\n","permalink":"https://ktzxy.top/posts/k3tn18rw3a/","summary":"Redis 操作命令","title":"Redis 操作命令"},{"content":"1. 数据结构概述 数据结构指数据的存储、组织方式。因此良好的数据结构对于程序的运行至关重要，尤其是在复杂的系统中，设计优秀的数据结构能够提高系统的灵活性和性能。\n数据存储的常用结构有：数组、栈、队列、链表、二叉树、红黑树、散列表和位图。\n2. 数组结构 查询元素快：通过索引，可以快速访问指定位置的元素 增删元素慢： 指定索引位置增加元素：需要创建一个新数组，将指定新元素存储在指定索引位置，再把原数组元素根据索引，复制到新数组对应索引的位置。 指定索引位置删除元素：需要创建一个新数组，把原数组元素根据索引，复制到新数组对应索引的位置，原数组中指定索引位置元素不复制到新数组中。 3. 堆栈结构（stack） 3.1. 概述 栈（Stack）又名堆栈，是允许在同一端进行插入和删除操作的特殊线性表。存储的元素是先进后出（即，存进去的元素，要在后它后面的元素依次取出后，才能取出该元素）(First In Last Out/FILO)。\n栈的入口、出口的都是栈的顶端位置。允许进行插入和删除操作的一端叫作栈顶（Top）。另一端叫作栈底（Bottom），栈底固定，栈顶浮动。\n以下是栈结构一些专用名词：\n空栈：栈中的元素个数为零时。 进栈（压栈）：就是存元素。即，把元素存储到栈的顶端位置，栈中已有元素依次向栈底方向移动一个位置。 出栈（弹栈、退栈）：就是取元素。即，把栈的顶端位置元素取出，栈中已有元素依次向栈顶方向移动一个位置。 3.2. 栈结构的核心方法 要实现一个栈，需要先实现以下核心方法：\npush()：向栈中压入一个数据，先入栈的数据在最下边。 pop()：弹出栈顶数据，即移除栈顶数据。 peek()：返回当前的栈顶数据。 3.3. 栈的 Java 实现 TODO: 待整理\n4. 队列结构（queue） 4.1. 概述 队列，是一种只允许在表的前端进行删除操作且在表的后端进行插入操作的线性表。存储的元素是先进先出（即，存进去的元素，要在后它前面的元素依次取出后，才能取出该元素）（First In First Out/FIFO）。\n队列结构一些专用名词：\n队尾：执行插入操作的一端 队头：执行删除操作的一端 空队列：没有元素的队列 入队：在队列中插入一个队列元素 出队：从队列中删除一个队列元素 队列的入口、出口各占一侧，并且只允许在队头删除，在队尾插入，就可以实现先进先出的效果。\n4.2. 队列结构的核心方法 要实现一个队列，需要先实现以下核心方法：\nadd()：向队列的尾部加入一个元素（入队），先入队列的元素在最前边。 poll()：删除队列头部的元素（出队）。 peek()：取出队列头部的元素。 4.3. 队列的 Java 实现 TODO: 待整理\n5. 链表结构（link） 5.1. 概述 链表是由一系列节点，通过地址进行连接而组成的数据结构，节点可以在运行过程中动态生成。\nNotes: 链表中的每一个元素都叫作一个节点\n为了表示每个数据元素与其直接后继数据元素之间的逻辑关系，每个节点存储包括两部分内容：\n存储元素本身的数据域 存储直接后继数据元素的信息（即直接后继数据元素的存储地址的指针域） 5.1.1. 链表的特点 链表通过一组存储单元存储线性表中的数据元素，这组存储单元可以是连续的，也可以是不连续的。由于节点包含了元素数据与下一个元素的地址这两部分信息，因此有以下特点：\n查询元素慢：想查找某个元素，从链表头或链表尾开始查找，需要通过连接的节点，依次向后一个个遍历查询指定元素。 增删元素快：增删元素不需要移动元素的位置，只需要修改元素记录连接下个元素的地址值即可。 5.1.2. 链表的分类 链表有 3 种不同的类型：单向链表、双向链表及循环链表。\n5.2. 单向链表 5.2.1. 组成结构 单向链表（又称单链表）是链表的一种，其特点是链表的链接方向是单向的，访问链表时要从头部开始顺序读取。单向链表是链表中结构最简单的。一个单向链表的节点（Node）可分为两部分：\n第1部分为数据区（data），用于保存节点的数据信息 第2部分为指针区，用于存储下一个节点的地址，最后一个节点的指针指向null。 5.2.2. 单向链表的操作 查找：单向链表只可向一个方向遍历，一般在查找一个节点时需要从单向链表的第1个节点开始依次访问下一个节点，一直访问到需要的位置。 插入：对于单向链表的插入，只需将当前插入的节点设置为头节点，将 Next 指针指向原来的头节点即可。 删除：对于单向链表的删除，只需将该节点的上一个节点的 Next 指针指向该节点的下一个节点，然后删除该节点即可。 5.2.3. 单向链表的 Java 实现 TODO: 待整理\n5.3. 双向链表 5.3.1. 组成结构 双向链表的每个数据节点中都有 Prev 和 Next 两个指针，分别指向其上一个节点和下一个节点。因此双向链表中的任意一个节点都很方便访问其前后的节点，出方便从两个方向遍历并处理节点的数据。\n5.3.2. 双向链表的 Java 实现 TODO: 待整理\n5.4. 循环链表 循环链表的链式存储结构的特点是：链表中最后一个节点的指针域指向头节点，整个链表形成一个环。\n循环节点的实现和单向链表十分相似，只是在链表中，尾部元素的 Next 指针不再是 null，而是指向头部节点，其他实现和单向链表相同。\n6. 散列表（Hash Table） 6.1. 概述 散列表（Hash Table，也叫作哈希表）是根据数据的关键码值（Key-Value对）对数据进行存取的数据结构。\n散列表通过映射函数把关键码值（key）映射到表中的一个位置来加快查找。这个映射函数叫作散列函数（可用 h(key) 表示），存放记录的数组叫作散列表。\n散列表算法通过在数据元素的存储位置和它的关键字（可用 key 表示）之间建立一个确定的对应关系，使每个关键字和散列表中唯一的存储位置相对应。在查找时只需根据这个对应关系找到给定关键字在散列表中的位置即可，真正做到一次查找命中。\n6.2. 散列函数 6.2.1. 散列函数的基本要求 散列函数计算得到的散列值必须是大于等于 0 的正整数，因为 hashValue 需要作为数组的下标。 如果 key1 == key2，那么经过 hash 后得到的哈希值也必相同。即：hash(key1) == hash(key2) 如果 key1 != key2，那么经过 hash 后得到的哈希值大概率不相同。即：hash(key1) != hash(key2) 但实际的情况下，一个散列函数（如 MD5，SHA 等哈希算法）几乎是不可能实现对于不同的 key 计算得到的散列值都不同，这就是散列冲突(或者哈希冲突，哈希碰撞，就是指多个 key 映射到同一个数组下标位置)\n6.2.2. 散列常用的构造函数 常用的构造散列函数有如下几种：\n直接定址法：取关键字或关键字的某个线性函数值为散列地址，即 h(key) = key 或 h(key)=a×key+b，其中a和b为常数。 平方取值法：取关键字平方后的中间几位为散列地址。 折叠法：将关键字分割成位数相同的几部分，然后取这几部分的叠加和作为散列地址。 除留余数法：取关键字被某个不大于散列表长度 m 的数 p 除后所得的余数为散列地址，即 h(key)=key/p，其中 p \u0026lt;= m。 随机数法：选择一个随机函数，取关键字的随机函数值作为其散列地址，即 h(key)=random(key)。 数字分析法。 Java HashCode 实现：在 Java 中计算 HashCode 的公式为 f(key) = s[0] × 31n-1+s[1] × 31n-2 +\u0026hellip;+s[n-1]。具体实现如下： 6.3. Hash的应用(待整理) TODO: 待整理\n7. 二叉排序树 7.1. 概述 首先如果普通二叉树每个节点满足：左子树所有节点值小于它的根节点值，且右子树所有节点值大于它的根节点值，则这类型的二叉树就是二叉排序树。\n7.2. 插入操作 插入操作首先要从根节点开始往下找到自己要插入的位置（即新节点的父节点）；具体流程是：\n新节点与当前节点比较，如果相同则表示已经存在且不能再重复插入； 如果小于当前节点，则到左子树中 寻找，如果左子树为空则当前节点为要找的父节点，新节点插入到当前节点的左子树即可； 如果大于当前节点，则到右子树中寻找，如果右子树为空则当前节点为要找的父节点，新节点插入到当前节点的右子树即可。 7.3. 删除操作 删除操作主要分为三种情况， 即要删除的节点无子节点，要删除的节点只有一个子节点，要删除的节点有两个子节点。\n对于要删除的节点无子节点可以直接删除，即让其父节点将该子节点置空即可。 对于要删除的节点只有一个子节点，则替换要删除的节点为其子节点。 对于要删除的节点有两个子节点，则首先找该节点的替换节点（即右子树中最小的节点），接着替换要删除的节点为替换节点，然后删除替换节点。 7.4. 查询操作 查找操作的主要流程为：先和根节点比较，如果相同就返回， 如果小于根节点则到左子树中归查找，如果大于根节点则到右子树中递归查找。因此在排序二叉树中可以很容易获取最大（最右最深子节点）和最小（最左最深子节点）值。\n8. 前缀树 前缀树(Prefix Trees 或者 Trie)与树结构类似，用于处理字符串相关问题时非常高效。它可以实现快速检索，常用于字典中的单词查询，搜索引擎的自动补全甚至 IP 路由。\n下图展示了“top”, “thus”和“their”三个单词在前缀树中如何存储：\n9. 红黑树（待整理） TODO: 待整理\n10. B TREE（待整理） TODO: 待整理\n11. B+Tree（待整理） 11.1. b+tree 和 b tree 的区别 B 树和 B+树是常用的一种平衡搜索树，它们的主要区别在于内部节点和叶子节点的存储方式和指针结构不同，从而导致它们在不同场景下具有不同的性能特点。\nB 树是一种多路平衡查找树，每个节点可以存储多个 key-value 键值对，具有较好的磁盘 IO 性能。B 树通常被用于文件系统和数据库系统中，可以高效地支持范围查找和随机访问等操作。 B+树是在 B 树的基础上进一步优化的一种平衡树，其内部节点仅存储 key，而真正的 value 均存储在叶子节点中。这种设计使得 B+树具有更好的磁盘 IO 性能和更高的查询效率，适用于需要频繁范围查询和顺序遍历的场景，例如数据库索引。 总结：B+树与 B 树相比，具有更高的查询效率和更好的空间利用率。因为 B+树只需要访问叶子节点才能查找到 value，而 B 树需要遍历所有节点。此外，B+树的内部节点仅存储 key，可以存储更多的 key，从而提高了空间利用率。\n12. 位图 位图的原理就是用一个 bit 来标识一个数字是否存在，采用一个 bit 来存储一个数据，所以这样可以大大的节省空间。 bitmap 是很常用的数据结构，比如用于 Bloom Filter 中；用于无重复整数的排序等等。\nbitmap 通常基于数组来实现，数组中每个元素可以看成是一系列二进制数，所有元素组成更大的二进制集合。\n","permalink":"https://ktzxy.top/posts/gur8zxmrf5/","summary":"Java扩展 数据结构","title":"Java扩展 数据结构"},{"content":"Kafka消费示例 Kafka是一种高吞吐量的分布式发布订阅消息系统，它可以处理消费者规模的网站中的所有动作流数据，具有高性能、持久化、多副本备份、横向扩展等特点。本文介绍了如何使用Go语言发送和接收kafka消息。\n来源 https://www.liwenzhou.com/posts/Go/go_kafka/\n启动Kafka 上一篇博客中，讲了kafka的安装和启动\n1 2 3 4 5 # 启动Zookeeper .\\zookeeper-server-start.bat ..\\..\\config\\zookeeper.properties # 启动kafka .\\kafka-server-start.bat ..\\..\\config\\server.properties sarama Go语言中连接kafka使用第三方库:github.com/Shopify/sarama。\n下载及安装 1 go get github.com/Shopify/sarama 注意事项 sarama v1.20之后的版本加入了zstd压缩算法，需要用到cgo，在Windows平台编译时会提示类似如下错误：\n1 2 # github.com/DataDog/zstd exec: \u0026#34;gcc\u0026#34;:executable file not found in %PATH% 所以在Windows平台请使用v1.19版本的sarama。\n连接kafka发送消息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/Shopify/sarama\u0026#34; ) // 基于sarama第三方库开发的kafka client func main() { config := sarama.NewConfig() config.Producer.RequiredAcks = sarama.WaitForAll // 发送完数据需要leader和follow都确认 config.Producer.Partitioner = sarama.NewRandomPartitioner // 新选出一个partition config.Producer.Return.Successes = true // 成功交付的消息将在success channel返回 // 构造一个消息 msg := \u0026amp;sarama.ProducerMessage{} msg.Topic = \u0026#34;web_log\u0026#34; msg.Value = sarama.StringEncoder(\u0026#34;this is a test log\u0026#34;) // 连接kafka client, err := sarama.NewSyncProducer([]string{\u0026#34;192.168.1.7:9092\u0026#34;}, config) if err != nil { fmt.Println(\u0026#34;producer closed, err:\u0026#34;, err) return } defer client.Close() // 发送消息 pid, offset, err := client.SendMessage(msg) if err != nil { fmt.Println(\u0026#34;send msg failed, err:\u0026#34;, err) return } fmt.Printf(\u0026#34;pid:%v offset:%v\\n\u0026#34;, pid, offset) } 连接kafka消费消息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/Shopify/sarama\u0026#34; ) // kafka consumer func main() { consumer, err := sarama.NewConsumer([]string{\u0026#34;127.0.0.1:9092\u0026#34;}, nil) if err != nil { fmt.Printf(\u0026#34;fail to start consumer, err:%v\\n\u0026#34;, err) return } partitionList, err := consumer.Partitions(\u0026#34;web_log\u0026#34;) // 根据topic取到所有的分区 if err != nil { fmt.Printf(\u0026#34;fail to get list of partition:err%v\\n\u0026#34;, err) return } fmt.Println(partitionList) for partition := range partitionList { // 遍历所有的分区 // 针对每个分区创建一个对应的分区消费者 pc, err := consumer.ConsumePartition(\u0026#34;web_log\u0026#34;, int32(partition), sarama.OffsetNewest) if err != nil { fmt.Printf(\u0026#34;failed to start consumer for partition %d,err:%v\\n\u0026#34;, partition, err) return } defer pc.AsyncClose() // 异步从每个分区消费信息 go func(sarama.PartitionConsumer) { for msg := range pc.Messages() { fmt.Printf(\u0026#34;Partition:%d Offset:%d Key:%v Value:%v\u0026#34;, msg.Partition, msg.Offset, msg.Key, msg.Value) } }(pc) } } LogTransfer实现 参考源码 20_LogTransfer\nLogTransfer的主要功能，就是将kafka中的日志信息取出来，然后发送到ElasticSearch中，下面我们就需要编码实现以下过程\n文件结构 LogTransfer首先包含多个模块\nkafka：用于kafka操作相关 es：用于es操作相关 conf：配置相关 Conf模块 conf模块是配置模块，用于进行LogTransfer的配置\ncfg.ini 我们使用ini管理配置信息\n1 2 3 4 5 6 [kafka] address=127.0.0.1:9092 topic=web_log [es] address=127.0.0.1:9200 cfg.go 然后定义配置类的结构体\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package conf type LogTransferCfg struct { KafkaCfg `ini:\u0026#34;kafka\u0026#34;` // 这个对应ini文件中的 [kafka] EsCfg `ini:\u0026#34;es\u0026#34;` // 这个对应ini文件中的 [es] } // Kafka配置类 type KafkaCfg struct { Address string `ini:\u0026#34;address\u0026#34;` Topic string `ini:\u0026#34;topic\u0026#34;` } // Es配置类 type EsCfg struct { Address string `ini:\u0026#34;address\u0026#34;` } ","permalink":"https://ktzxy.top/posts/cs9itzyc9p/","summary":"Kafka消费示例","title":"Kafka消费示例"},{"content":"防火墙 1 2 3 4 5 6 7 8 9 10 11 # 清空以下规则 [root@localhost ~]#ip tables -F # 禁止防火墙服务开机自启 [root@localhost ~]#systemctl disable firewalld # 停止防火墙服务 [root@localhost ~]#systemctl stop firewalld # 获取selinux的状态 [root@localhost ~]#getenforce # 如果selinux没有关闭，手动关闭 [root@localhost ~]#vim /etc/selinux/config SELINUX=disabled 修改系统字符集 1 2 3 4 5 6 修改字符集，否则可能报 input/output error的问题，因为日志里打印了中文 [root@localhost ~]# localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 [root@localhost ~]# export LC_ALL=zh_CN.UTF-8 [root@localhost ~]# echo \u0026#39;LANG=\u0026#34;zh_CN.UTF-8\u0026#34;\u0026#39; \u0026gt; /etc/locale.conf # 检查系统编码 [root@localhost ~]# locale vi编辑器 查看 1 2 #把文件里面的以#和空白行信息都排除掉 grep -Ev \u0026#39;^#|^$\u0026#39; 文件名 生成密钥SECRET_KEY 1 if [ \u0026#34;$SECRET_KEY\u0026#34; = \u0026#34;\u0026#34; ]; then SECRET_KEY=`cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 50` ; echo \u0026#34;SECRET_KEY=$SECRET_KEY\u0026#34; \u0026gt;\u0026gt; ~/.bashrc; echo $SECRET_KEY; else echo $SECRET_KEY; fi 生成TOKEN密钥 1 if [ \u0026#34;$BOOTSTRAP_TOKEN\u0026#34; = \u0026#34;\u0026#34; ]; then BOOTSTRAP_TOKEN=`cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 16`; echo \u0026#34;BOOTSTRAP_TOKEN=$BOOTSTRAP_TOKEN\u0026#34; \u0026gt;\u0026gt; ~/.bashrc; echo $BOOTSTRAP_TOKEN; else echo $BOOTSTRAP_TOKEN; fi 查看密钥 1 2 echo $SECRET_KEY echo $BOOTSTRAP_TOKEN 查看端口 1 netstat -tunlp 查看进程 1 ps -ef | grep 进程号 更换yum源 CentOS 7更换阿里和清华大学yum源_liunx centos无法保存阿里云和清华源地址-CSDN博客\nvscode相关问题 解决 VScode (因为在此系统上禁止运行脚本)报错 在使用 VScode 自带程序终端的时候会报出\u0026quot;系统禁止脚本运行的错误\u0026quot;, 准备的原因，是因为 PowerShell 执行策略的问题。 解决方法： 管理员身份运行 window.powershell 执行：get-ExecutionPolicy，显示Restricted，表示状态是禁止的; 执行：set-ExecutionPolicy 会提示输入参数：RemoteSigned 会提示进行 选择： 2. 输入：Y 之后就不会有问题了。\n","permalink":"https://ktzxy.top/posts/79v0cgnmxw/","summary":"运维杂学","title":"运维杂学"},{"content":"Go中的接口 接口的介绍 现实生活中手机、相机、U盘都可以和电脑的USB接口建立连接。我们不需要关注usb卡槽大小是否一样，因为所有的USB接口都是按照统一的标准来设计的。\nGolang中的接口是一种抽象数据类型，Golang中接口定义了对象的行为规范，只定义规范不实现。接口中定义的规范由具体的对象来实现。\n通俗的讲接口就一个标准，它是对一个对象的行为和规范进行约定，约定实现接口的对象必须得按照接口的规范。\nGo接口的定义 在Golang中接口（interface）是一种类型，一种抽象的类型。接口（interface）是一组函数method的集合，Golang中的接口不能包含任何变量。\n在Golang中接口中的所有方法都没有方法体，接口定义了一个对象的行为规范，只定义规范不实现。接口体现了程序设计的多态和高内聚低耦合的思想N Golang中的接口也是一种数据类型，不需要显示实现。只需要一个变量含有接口类型中的所有方法，那么这个变量就实现了这个接口。\nGolang中每个接口由数个方法组成，接口的定义格式如下：\n1 2 3 4 type 接口名 interface { 方法名1 (参数列表1) 返回值列表1 方法名2 (参数列表2) 返回值列表2 } 其中\n接口名：使用type将接口定义为自定义的类型名。Go语言的接口在命名时，一般会在单词后面添加er，如有写操作的接口叫Writer，有字符串功能的接口叫Stringer等，接口名最好突出该接口的类型含义。 方法名：当方法名首字母是大写且这个接口类型名首字母也是大写时，这个方法可以被接口所在的包（package）之外的代码访问。 参数列表、返回值列表：参数列表和返回值列表中的参数变量名是可以省略 演示：定义一个Usber接口让Phone 和 Camera结构体实现这个接口\n首先我们定义一个Usber接口，接口里面就定义了两个方法\n1 2 3 4 5 // 定义一个Usber接口 type Usber interface { start() stop() } 然后我们在创建一个手机结构体\n1 2 3 4 5 6 7 8 9 10 11 12 13 // 如果接口里面有方法的话，必须要通过结构体或自定义类型实现这个接口 // 使用结构体来实现 接口 type Phone struct { Name string } // 手机要实现Usber接口的话，必须实现usb接口的所有方法 func (p Phone) Start() { fmt.Println(p.Name, \u0026#34;启动\u0026#34;) } func (p Phone) Stop() { fmt.Println(p.Name, \u0026#34;关闭\u0026#34;) } 然后我们在创建一个Phone的结构体，来实现这个接口\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 如果接口里面有方法的话，必须要通过结构体或自定义类型实现这个接口 // 使用结构体来实现 接口 type Phone struct { Name string } // 手机要实现Usber接口的话，必须实现usb接口的所有方法 func (p Phone) start() { fmt.Println(p.Name, \u0026#34;启动\u0026#34;) } func (p Phone) stop() { fmt.Println(p.Name, \u0026#34;关闭\u0026#34;) } func main() { var phone Usber = Phone{ \u0026#34;三星手机\u0026#34;, } phone.start() phone.stop() } 我们在创建一个Camera结构体\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 使用相机结构体来实现 接口 type Camera struct { Name string } // 相机要实现Usber接口的话，必须实现usb接口的所有方法 func (p Camera) start() { fmt.Println(p.Name, \u0026#34;启动\u0026#34;) } func (p Camera) stop() { fmt.Println(p.Name, \u0026#34;关闭\u0026#34;) } func main() { var camera Usber = Camera{ \u0026#34;佳能\u0026#34;, } camera.start() camera.stop() } 我们创建一个电脑的结构体，电脑的结构体就是用于接收两个实现了Usber的结构体，然后让其工作\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 电脑 type Computer struct { } // 接收一个实现了Usber接口的 结构体 func (computer Computer) Startup(usb Usber) { usb.start() } // 关闭 func (computer Computer) Shutdown (usb Usber) { usb.stop() } 最后我们在main中调用方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main() { var camera interfaceDemo.Camera = interfaceDemo.Camera{ \u0026#34;佳能\u0026#34;, } var phone interfaceDemo.Phone = interfaceDemo.Phone{ \u0026#34;苹果\u0026#34;, } var computer interfaceDemo.Computer = interfaceDemo.Computer{} computer.Startup(camera) computer.Startup(phone) computer.Shutdown(camera) computer.Shutdown(phone) } 运行结果如下所示：\n1 2 3 4 佳能 启动 苹果 启动 佳能 关闭 苹果 关闭 空接口（Object类型） Golang中的接口可以不定义任何方法，没有定义任何方法的接口就是空接口。空接口表示没有任何约束，因此任何类型变量都可以实现空接口。\n空接口在实际项目中用的是非常多的，用空接口可以表示任意数据类型。\n1 2 3 4 5 6 7 8 9 10 11 12 // 空接口表示没有任何约束，任意的类型都可以实现空接口 type EmptyA interface { } func main() { var a EmptyA var str = \u0026#34;你好golang\u0026#34; // 让字符串实现A接口 a = str fmt.Println(a) } 同时golang中空接口也可以直接当做类型来使用，可以表示任意类型。相当于Java中的Object类型\n1 2 3 4 var a interface{} a = 20 a = \u0026#34;hello\u0026#34; a = true 空接口可以作为函数的参数，使用空接口可以接收任意类型的函数参数\n1 2 3 4 // 空接口作为函数参数 func show(a interface{}) { fmt.println(a) } map的值实现空接口 使用空接口实现可以保存任意值的字典\n1 2 3 4 5 // 定义一个值为空接口类型 var studentInfo = make(map[string]interface{}) studentInfo[\u0026#34;userName\u0026#34;] = \u0026#34;张三\u0026#34; studentInfo[\u0026#34;age\u0026#34;] = 15 studentInfo[\u0026#34;isWork\u0026#34;] = true slice切片实现空接口 1 2 3 4 5 // 定义一个空接口类型的切片 var slice = make([]interface{}, 4, 4) slice[0] = \u0026#34;张三\u0026#34; slice[1] = 1 slice[2] = true 类型断言 一个接口的值（简称接口值）是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。\n如果我们想要判断空接口中值的类型，那么这个时候就可以使用类型断言，其语法格式：\n1 x.(T) 其中：\nX：表示类型为interface{}的变量 T：表示断言x可能是的类型 该语法返回两个参数，第一个参数是x转化为T类型后的变量，第二个值是一个布尔值，若为true则表示断言成功，为false则表示断言失败\n1 2 3 4 5 6 7 8 9 // 类型断言 var a interface{} a = \u0026#34;132\u0026#34; value, isString := a.(string) if isString { fmt.Println(\u0026#34;是String类型, 值为：\u0026#34;, value) } else { fmt.Println(\u0026#34;断言失败\u0026#34;) } 或者我们可以定义一个能传入任意类型的方法\n1 2 3 4 5 6 7 8 9 10 // 定义一个方法，可以传入任意数据类型，然后根据不同类型实现不同的功能 func Print(x interface{}) { if _,ok := x.(string); ok { fmt.Println(\u0026#34;传入参数是string类型\u0026#34;) } else if _, ok := x.(int); ok { fmt.Println(\u0026#34;传入参数是int类型\u0026#34;) } else { fmt.Println(\u0026#34;传入其它类型\u0026#34;) } } 上面的示例代码中，如果要断言多次，那么就需要写很多if，这个时候我们可以使用switch语句来实现：\n注意： 类型.(type) 只能结合switch语句使用\n1 2 3 4 5 6 7 8 9 10 11 12 func Print2(x interface{}) { switch x.(type) { case int: fmt.Println(\u0026#34;int类型\u0026#34;) case string: fmt.Println(\u0026#34;string类型\u0026#34;) case bool: fmt.Println(\u0026#34;bool类型\u0026#34;) default: fmt.Println(\u0026#34;其它类型\u0026#34;) } } 结构体接收者 值接收者 如果结构体中的方法是值接收者，那么实例化后的结构体值类型和结构体指针类型都可以赋值给接口变量\n结构体实现多个接口 实现多个接口的话，可能就同时用两个接口进行结构体的接受\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 // 定义一个Animal的接口，Animal中定义了两个方法，分别是setName 和 getName，分别让DOg结构体和Cat结构体实现 type Animal interface { SetName(string) } // 接口2 type Animal2 interface { GetName()string } type Dog struct { Name string } func (d *Dog) SetName(name string) { d.Name = name } func (d Dog)GetName()string { return d.Name } func main() { var dog = \u0026amp;Dog{ \u0026#34;小黑\u0026#34;, } // 同时实现两个接口 var d1 Animal = dog var d2 Animal2 = dog d1.SetName(\u0026#34;小鸡\u0026#34;) fmt.Println(d2.GetName()) } 接口嵌套 在golang中，允许接口嵌套接口，我们首先创建一个 Animal1 和 Animal2 接口，然后使用Animal接受刚刚的两个接口，实现接口的嵌套。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 // 定义一个Animal的接口，Animal中定义了两个方法，分别是setName 和 getName，分别让DOg结构体和Cat结构体实现 type Animal1 interface { SetName(string) } // 接口2 type Animal2 interface { GetName()string } type Animal interface { Animal1 Animal2 } type Dog struct { Name string } func (d *Dog) SetName(name string) { d.Name = name } func (d Dog)GetName()string { return d.Name } func main() { var dog = \u0026amp;Dog{ \u0026#34;小黑\u0026#34;, } // 同时实现两个接口 var d Animal = dog d.SetName(\u0026#34;小鸡\u0026#34;) fmt.Println(d.GetName()) } Golang中空接口和类型断言 1 2 3 4 5 6 7 8 9 10 // golang中空接口和类型断言 var userInfo = make(map[string]interface{}) userInfo[\u0026#34;userName\u0026#34;] = \u0026#34;zhangsan\u0026#34; userInfo[\u0026#34;age\u0026#34;] = 10 userInfo[\u0026#34;hobby\u0026#34;] = []string{\u0026#34;吃饭\u0026#34;, \u0026#34;睡觉\u0026#34;} fmt.Println(userInfo[\u0026#34;userName\u0026#34;]) fmt.Println(userInfo[\u0026#34;age\u0026#34;]) fmt.Println(userInfo[\u0026#34;hobby\u0026#34;]) // 但是我们空接口如何获取数组中的值？发现 userInfo[\u0026#34;hobby\u0026#34;][0] 这样做不行 // fmt.Println(userInfo[\u0026#34;hobby\u0026#34;][0]) 也就是我们的空接口，无法直接通过索引获取数组中的内容，因此这个时候就需要使用类型断言了\n1 2 3 4 5 // 这个时候我们就可以使用类型断言了 hobbyValue,ok := userInfo[\u0026#34;hobby\u0026#34;].([]string) if ok { fmt.Println(hobbyValue[0]) } 通过类型断言返回来的值，我们就能够直接通过角标获取了。\n","permalink":"https://ktzxy.top/posts/i2hcp3yuuk/","summary":"14 Go中的接口","title":"14 Go中的接口"},{"content":"1. JVM 调优参数配置的位置 按不同的部署方式，对应的参数配置位置也不同：\nwar 包部署，在 tomcat 中设置 jar 包部署，在启动参数设置 1.1. 以 tomcat 方式部署 war 包 war 包部署时，可修改 TOMCAT_HOME/bin/catalina.sh 文件(linux 系统，若是 windows 系统则是 *.bat) 来设置 vm 参数，如下图，配置 JAVA_OPTS=\u0026quot;-Xms512m -Xmx1024m\u0026quot;\n1.2. 以 jar 文件部署的 SpringBoot 项目 通常在 linux 系统下直接加参数启动 springboot 项目即可，如：\n1 nohup java -Xms512m -Xmx1024m -jar xxxx.jar --spring.profiles.active=prod \u0026amp; 命令参数解析：\nnohup：用于在系统后台不挂断地运行命令，退出终端不会影响程序的运行 \u0026amp;：让命令在后台执行，终端退出后命令仍旧执行。 2. 常用的 JVM 调优的参数 对于 JVM 调优，主要是调整年轻代、老年代、元空间的内存空间大小及使用的垃圾回收器类型。\nJava SE 8 版本 JVM 虚拟机参数设置参考文档地址：https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html vm 参数官方文档：https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html 2.1. 内存设置 2.1.1. 设置堆空间大小 为了防止垃圾收集器在初始大小、最大内存之间收缩堆而产生额外的时间，通常把最大、初始大小设置为相同的值。\n-Xms：设置堆的初始化大小 -Xmx：设置堆的最大内存 Tips: 配置时不指定单位默认为字节；指定单位，按照指定的单位设置。\n例如：\n1 2 3 4 5 6 7 8 9 10 # 初始化推大小为 2g -Xms2g # 堆最大内存为 2g -Xmx2g # 设置为 1024 字节 -Xms:1024 # 设置为 1024 kb -Xms:1024k # 设置为 1024 mb -Xms:1024m\u000b堆空间设置多少合适？\n堆的默认最大值是物理内存的 1/4，初始大小是物理内存的 1/64。堆太小，可能会频繁的导致年轻代和老年代的垃圾回收，会产生 STW，暂停用户线程。堆内存大肯定是好的，存在风险，假如发生了 full GC，它会扫描整个堆空间，暂停用户线程的时间长。\n设置参考推荐：尽量大，也要考察一下当前计算机其他程序的内存使用情况。\n2.1.2. 设置年轻代中 Eden 区和两个 Survivor 区的大小比例 -XXSurvivorRatio 参数用于设置年轻代中 Eden 区和两个 Survivor 区的大小比例。该值如果不设置，则默认比例为 8:1:1。Java 官方通过增大 Eden 区的大小，来减少 YGC 发生的次数，但有时虽然次数减少了，但 Eden 区满的时候，由于占用的空间较大，导致释放缓慢，此时 STW(Stop the world) 的时间较长，因此需要按照程序情况去调优。\n1 2 3 4 5 # 表示年轻代中的分配比率：survivor : eden = 2:3 -XXSurvivorRatio=3 # 设置新生代 Eden 和 Survivor 比例为 8:2，具体 Eden : from : to = 8 : 1 : 1 -XX:SurvivorRatio=8 2.1.3. 年轻代和老年代的比例 年轻代和老年代默认比例为 1:2。可以通过调整二者空间大小比率来设置两者的大小。\n-XX:newSize：设置年轻代的初始大小 -XX:MaxNewSize：设置年轻代的最大大小，初始大小和最大大小两个值通常相同 -XX:NewRatio：设置年轻代与老年代的比例值 例如：\n1 2 3 4 # 设置新生代大小 -XX:NewSize=1 # 设置年轻的和老年代的内存比例为 1:4 -XX:NewRatio=4 2.1.4. 虚拟机栈的设置 虚拟机栈的设置，每个线程默认会开启 1M 的堆栈，用于存放栈帧、调用参数、局部变量等，但一般 256K 就够用。通常减少每个线程的堆栈，可以产生更多的线程，但这实际上还受限于操作系统。-Xss 用于对每个线程 stack 大小的调整。例如：\n1 -Xss256k 2.1.5. 年轻代（新生代）的内存大小 一般来说，当 survivor 区不够大或者占用量达到 50%，就会把一些对象放到老年区。通过设置合理的 eden 区，survivor 区及使用率，可以将年轻对象保存在年轻代，从而避免 full GC，使用 -Xmn 设置年轻代的大小。\n1 -Xmn 2.1.6. 大对象分配内存配置 对于占用内存比较多的大对象，一般会选择在老年代分配内存。如果在年轻代给大对象分配内存，年轻代内存不够了，就要在 eden 区移动大量对象到老年代，然后这些移动的对象可能很快消亡，因此导致 full GC。通过设置参数：-XX:PetenureSizeThreshold=1000000，单位为 B，标明对象大小超过 1M 时，在老年代(tenured)分配内存空间。\n2.1.7. 年轻代晋升老年代阈值 一般情况下，年轻对象放在 eden 区，当第一次 GC 后，如果对象还存活，放到 survivor 区，此后每 GC 一次，年龄增加 1，当对象的年龄达到阈值，就被放到 tenured 老年区。这个阈值可以通过 -XX:MaxTenuringThreshold 设置：\n1 2 # threshold 默认为15，取值范围 0-15 -XX:MaxTenuringThreshold=threshold 如果想让对象留在年轻代，可以设置比较大的阈值。\n2.2. 垃圾回收器的配置 2.2.1. 选择垃圾回收器的类型 -XX:+UseParallelGC：年轻代使用并行垃圾回收收集器。这是一个关注吞吐量的收集器，可以尽可能的减少垃圾回收时间。 -XX:+UseParallelOldGC：设置老年代使用 ParNew + ParNew Old 组合并行垃圾回收收集器。 –XX:+UseParNewGC：指定使用 ParNew + Serial Old 垃圾回收器组合。 -XX:+UseConcMarkSweepGC：老年代使用 CMS + Serial Old 垃圾收集器组合，降低停顿。 -XX:ParallelGCThreads：设置 Parallel GC 的线程数。 -XX:+UseG1GC：使用 G1 垃圾收集器。 2.2.2. GC 打印信息配置 -XX:+PrintGC：开启打印 gc 信息 -XX:+PrintGCDetails：打印 gc 详细信息。 -XX:+PrintGCTimeStamps：打印 GC 的时间戳 -Xloggc:filename：设置 GC log 文件的位置 -XX:+PrintTenuringDistribution：查看熬过收集后剩余对象的年龄分布信息 2.2.3. CMS 垃圾回收器相关 -XX:+UseCMSInitiatingOccupancyOnly、-XX:CMSInitiatingOccupancyFraction：与前者配合使用，指定 MajorGC 的发生时机。 -XX:+ExplicitGCInvokesConcurrent：代码调用 System.gc() 开始并行 FullGC，建议加上这个参数。 -XX:+CMSScavengeBeforeRemark：表示开启或关闭在 CMS 重新标记阶段之前的清除（YGC）尝试，它可以降低 remark 时间，建议加上。 -XX:+ParallelRefProcEnabled：可以用来并行处理 Reference，以加快处理速度，缩短耗时。 2.2.4. G1 垃圾回收器相关 -XX:MaxGCPauseMillis：用于设置目标停顿时间，G1 会尽力达成 -XX:G1HeapRegionSize：用于设置小堆区大小，建议保持默认。 -XX:InitiatingHeapOccupancyPercent：表示当整个堆内存使用达到一定比例（默认是 45%），并发标记阶段就会被启动。 -XX:ConcGCThreads：表示并发垃圾收集器使用的线程数量，默认值随 JVM 运行的平台不同而变动，不建议修改 2.3. 内存分页 尝试使用大的内存分页，增加 CPU 的内存寻址能力，从而系统的性能。\n1 2 # 设置内存页的大小 -XX:+LargePageSizeInBytes 2.4. -XX:+UseCompressedOops 当应用从 32 位的 JVM 迁移到 64 位的 JVM 时，由于对象的指针从 32 位增加到了 64 位，因此堆内存会突然增加差不多翻倍，这也会对 CPU 缓存（容量比内存小很多）的数据产生不利的影响。\n迁移到 64 位的 JVM 主要动机在于可以指定最大堆大小，通过压缩 OOP 可以节省一定的内存。通过 -XX:+UseCompressedOops 选项，JVM 会使用 32 位的 OOP，而不是 64 位的 OOP。\n2.5. 示例：生产环境用的什么JDK？如何配置的垃圾收集器？ 例如生产环境使用了 Oracle JDK 1.8，G1 收集器相关配置如下：\n1 2 3 4 5 6 7 8 9 -Xmx12g -Xms12g -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=45 -XX:MaxGCPauseMillis=200 -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=512m -XX:G1HeapRegionSize 未指定 核心思路：\n每个内存区域设置上限，避免溢出。 堆设置为操作系统的 70% 左右，超过 8G，首选 G1。 根据老年代对象提升速度，调整新生代与老年代之间的内存比例。 等过 GC 信息，针对项目敏感指标优化，比如访问延迟、吞吐量等。 3. 常用的 JVM 调优命令 3.1. jps（虚拟机进程状况工具） jps：JVM Process Status Tool，列出本机所有正在运行的虚拟机（Java）进程。显示执行主类（Main Class, main() 函数所在的类）的名称，以及进程的本地虚拟机的唯一 ID。\n1 jps -lvm 常用参数如下：\n-m 输出虚拟机进程启动时传递给主类 main 方法的参数。 -l 输出完整的包名和应用主类名。如果进程执行是 jar 包，输出 jar 路径。 -v 输出虚拟机进程启动时 JVM 参数。 -q 只输出lvmid，省略主类的名称 3.2. jstack（堆栈跟踪工具） jstack：查看某个 Java 进程内当前时刻的线程堆栈信息。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合，生成线程快照的主要目的是定位线程出现长时间停顿的原因。语法如下：\n1 jstack [option] vmid option 选项取值说明：\n-F 当正常输出的请求不被响应时，强制输出线程堆栈 -l 除堆栈外，显示关于锁的额外附加信息 -m 如果调用本地方法的花，可以显示 C/C++ 的堆栈 发生死锁时可以使用 jstack -l pid 观察锁持有情况。\n1 jstack -l 4124 | more 3.3. jstat（虚拟机统计信息工具） jstat：JVM statistics Monitoring，用于监视虚拟机各种运行状态信息（类装载、内存、垃圾收集、JIT编译等运行数据）。命令语法如下：\n1 jstat [option vmid [interval[s|ms] [count]] ] 参数说明：\noption 代表用户希望查询的虚拟机信息，主要分3类： 类装载 垃圾收集 运行期编译状况 vmid 虚拟机id interval 查询间隔 count 查询次数 Tips: 如果不指定 interval 与 count 此两个参数，就默认查询一次。\n3.3.1. option 选项取值说明 -class 监视类装载、卸载数量、总空间及类装载锁消耗的时间 -gc 监视 Java 堆状况，包括 Eden 区，2 个 survivor 区、老年代 -gccapacity 监视内容与 -gc 基本相同，但输出主要关注Java堆各个区域使用的最大和最小空间 -gcutil 监视内容与 -gc 基本相同，主要关注已经使用空间站空间百分比 -gccause 与 -gcutil 功能一样，但是会额外输出导致上一次 GC 产生的原因 -gcnew 监视新生代的 GC 的状况 -gcnewcapacity 监视内容与 -gcnew 基本相同，输出主要关注使用到的最大最小空间 -gcold 监视老年代的 GC 情况 -gcoldcapacity 监控内容与 -gcold 基本相同，主要关注使用到的最大最小空间 -compiler 输出 JIT 编译器编译过的方法、耗时等信息 3.3.2. 示例及输出信息说明 使用参数 -gcuitl 可以查看垃圾回收的统计信息。 1 2 3 4 jstat -gcutil 4124 S0 S1 E O M CCS YGC YGCT FGC FGCT GCT 0.00 0.00 67.21 19.20 96.36 94.96 10 0.084 3 0.191 0.275 参数说明：\nS0： Survivor0 区当前使用比例 S1： Survivor1 区当前使用比例 E： Eden 区使用比例 O：老年代使用比例 M：元数据区使用比例 CCS：压缩使用比例 YGC：年轻代垃圾回收次数 FGC：老年代垃圾回收次数 FGCT：老年代垃圾回收消耗时间 GCT：垃圾回收消耗总时间 使用 -gc 参数，查看垃圾回收统计。 1 jstat -gc pid 3.4. jmap（内存映像工具） jmap：JVM Memory Map，查看运行中的堆内存的快照（heap dump 文件），从而可以对堆内存进行离线分析。该命令工具还可以查询 finalize 执行队列，Java 堆和永久代的详细信息，如果空间使用率、当前用的是哪种收集器等。命令语法：\n1 jmap [option] vmid option 选项说明：\n-dump 生成 Java 堆转储快照，其中 live 自参数说明是否只 dump 出存活对象 -finalizerinfo 显示在 F -Queue 中等待 Finalizer 线程执行 finalize 方法的对象。只在 Linux/Solaris 平台下有效 -heap 显示 Java 堆详细信息，如使用哪种回收器、参数配置、分代状况。 -histo 显示堆中对象统计信息、包括类、实例数量和合计容量。 -F 当虚拟机进程对 -dump 选项没有响应时，可使用这个选项强制生成 dump 快照。 1 jmap -dump:format=b,file=heap.hprof pid format=b 表示以 hprof 二进制格式转储 Java 堆的内存 file=\u0026lt;filename\u0026gt; 用于指定快照 dump 文件的文件名 例如：查询进程 53280\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 C:\\Users\\MooN\u0026gt;jmap -heap 53280 Attaching to process ID 53280, please wait... Debugger attached successfully. Server compiler detected. JVM version is 25.321-b07 using thread-local object allocation. Parallel GC with 8 thread(s) //并行的垃圾回收器 Heap Configuration: //堆配置 MinHeapFreeRatio = 0 //空闲堆空间的最小百分比 MaxHeapFreeRatio = 100 //空闲堆空间的最大百分比 MaxHeapSize = 8524922880 (8130.0MB) //堆空间允许的最大值 NewSize = 178257920 (170.0MB) //新生代堆空间的默认值 MaxNewSize = 2841640960 (2710.0MB) //新生代堆空间允许的最大值 OldSize = 356515840 (340.0MB) //老年代堆空间的默认值 NewRatio = 2 //新生代与老年代的堆空间比值，表示新生代：老年代=1：2 SurvivorRatio = 8 //两个Survivor区和Eden区的堆空间比值为8,表示S0:S1:Eden=1:1:8 MetaspaceSize = 21807104 (20.796875MB) //元空间的默认值 CompressedClassSpaceSize = 1073741824 (1024.0MB) //压缩类使用空间大小 MaxMetaspaceSize = 17592186044415 MB //元空间允许的最大值 G1HeapRegionSize = 0 (0.0MB)//在使用 G1 垃圾回收算法时，JVM 会将 Heap 空间分隔为若干个 Region，该参数用来指定每个 Region 空间的大小。 Heap Usage: PS Young Generation Eden Space: //Eden使用情况 capacity = 134217728 (128.0MB) used = 10737496 (10.240074157714844MB) free = 123480232 (117.75992584228516MB) 8.000057935714722% used From Space: //Survivor-From 使用情况 capacity = 22020096 (21.0MB) used = 0 (0.0MB) free = 22020096 (21.0MB) 0.0% used To Space: //Survivor-To 使用情况 capacity = 22020096 (21.0MB) used = 0 (0.0MB) free = 22020096 (21.0MB) 0.0% used PS Old Generation //老年代 使用情况 capacity = 356515840 (340.0MB) used = 0 (0.0MB) free = 356515840 (340.0MB) 0.0% used 3185 interned Strings occupying 261264 bytes. Tips: 它是一个进程或系统在某一给定的时间的快照。比如在进程崩溃时，甚至是任何时候，我们都可以通过工具将系统或某进程的内存备份出来供调试分析用。dump 文件中包含了程序运行的模块信息、线程信息、堆栈调用信息、异常信息等数据，方便系统技术人员进行错误排查。\n3.5. jhat jhat：JVM Heap Analysis Tool，此命令是与 jmap 搭配使用，用来分析 jmap 生成的 dump，jhat 内置了一个微型的 HTTP/HTML 服务器，生成 dump 的分析结果后，可以在浏览器中查看。\n3.6. jinfo（配置信息工具） jinfo：JVM Configuration info，实时查看当前的应用 JVM 运行参数配置。语法格式：\n1 jinfo [option] pid 例如：\n1 jinfo -flags 1 Tips: 与 jps -v 命令比较，该只是查看虚拟机启动时显式指定的参数列表。\n3.7. 查询 JVM 相关参数值 查看所有参数的最终值，初始值可能被修改掉。 1 java -XX:+PrintFlagsFinal -version 查看所有参数的初始值 1 java -XX:+PrintFlagsInitial 3.8. Arthas（Java 诊断工具） Arthas 是 Alibaba 开源的 Java 诊断工具。支持 JDK 6+，支持 Linux/Mac/Winodws，采用命令行交互模式，同时提供丰富的 Tab 自动补全功能，进一步方便进行问题的定位和诊断。\n详见《Arthas - Alibaba 开源的 Java 诊断工具》笔记\n4. JVM 性能调优可视化工具 JDK 自带了很多监控工具，都位于 JDK 的 bin 目录下，其中最常用的是 jconsole 和 jvisualvm 这两款视图监控工具。第三方有：MAT(Memory Analyzer Tool)、GChisto。\nMAT（Memory Analyzer Tool）：一个基于 Eclipse 的内存分析工具，是一个快速、功能丰富的 Javaheap 分析工具，可以帮助开发者查找内存泄漏和减少内存消耗。 GChisto：一款专业分析 gc 日志的工具 4.1. jconsole jconsole（Java Monitoring and Management Console）：从 Java5 开始，在 JDK 中自带的 java 监控和管理控制台。用于对 JVM 中的内存、线程和类等进行监控，是一个基于 JMX 的 GUI 性能监控工具。\n打开方式：java 安装目录 bin 目录下 直接启动 jconsole.exe 即可。 可以内存、线程、类等信息 4.2. VisualVM：故障处理工具 jvisualvm：JDK 自带的全能分析工具，可以分析内存快照、线程快照、程序死锁、监控内存的变化、gc 变化等。能够监控线程，内存情况，查看方法的 CPU 时间和内存中的对象，已被 GC 的对象，反向查看分配的堆栈。\n打开方式：java 安装目录 bin 目录下 直接启动 jvisualvm.exe 即可 监控程序运行情况 查看运行中的 dump 查看堆中的信息 5. JVM 常见问题排查 5.1. OutOfMemoryError 问题的排查 Java 内存泄露原因：\n如果线程请求分配的栈容量超过 java 虚拟机栈允许的最大容量的时候，java 虚拟机将抛出一个 StackOverFlowError 异常 如果 java 虚拟机栈可以动态拓展，并且扩展的动作已经尝试过，但是目前无法申请到足够的内存去完成拓展，或者在建立新线程的时候没有足够的内存去创建对应的虚拟机栈，那 java 虚拟机将会抛出一个 OutOfMemoryError 异常 如果一次加载的类太多，元空间内存不足，则会报 OutOfMemoryError: Metaspace 排查 OOM 的方案：\n查看服务器运行日志日志，捕捉到内存溢出异常 使用 jstat 命令工具查看监控 JVM 的内存和 GC 情况，评估问题大概出在什么区域 使用 MAT 工具载入 dump 文件，分析大对象的占用情况 其中一种排查 OOM 的思路如下：\n通过 jmap 指定打印他的内存快照 dump。（Dump 文件是进程的内存镜像。可以把程序的执行状态通过调试器保存到 dump 文件中） 1 jmap -dump:format=b,file=heap.hprof pid 建议使用另一方式，使用 vm 参数获取 dump 文件。因为有的情况是内存溢出之后程序则会直接中断，而 jmap 只能打印在运行中的程序，所以建议通过参数的方式的生成 dump 文件，配置如下：\n1 2 3 4 # 线上 JVM 必须配置 -XX:+HeapDumpOnOutOfMemoryError # 指定当 OOM 发生时自动 dump 堆内存信息到指定目录 -XX:HeapDumpPath=/home/app/dumps/heapdump.hprof 通过工具，VisualVM（Ecplise MAT）去分析 dump 文件。VisualVM 可以加载离线的 dump 文件，文件 -\u0026gt; 装入 -\u0026gt; 选择 dump 文件即可查看堆快照信息。如下图 Tips: 如果是 linux 系统中的程序，则需要把 dump 文件下载到本地（windows环境）下，打开 VisualVM 工具分析。VisualVM 目前只支持在 windows 环境下运行可视化\n通过查看堆信息的情况，可以大概定位内存溢出是哪行代码出了问题 找到对应的代码，通过阅读上下文的情况，进行修复即可 5.2. CPU 飙高排查方案与思路 使用 top 命令查看占用 cpu 的情况 通过 top 命令查看后，可以查看是哪一个进程占用 cpu 较高，上图所示的进程为：30978 查看当前线程中的进程信息 1 ps H -eo pid,tid,%cpu | grep 30978 命令参数说明：\npid：进程 id tid：进程中的线程 id %：cpu 使用率 通过上图分析，在进程 30978 中的线程 30979 占用 cpu 较高。注意：上述的线程 id 是一个十进制，需要把这个线程 id 转换为 16 进制，因为通常在日志中展示的都是 16 进制的线程 id 名称。在 linux 中执行命令进行转换： 1 printf \u0026#34;%x\\n\u0026#34; 30979 可以根据线程 id 找到有问题的线程，进一步定位到问题代码的源码行号。执行以下命令： 1 jstack 30978 ","permalink":"https://ktzxy.top/posts/25juvax5y3/","summary":"JVM 性能调优","title":"JVM 性能调优"},{"content":"1. Oracle 的基本概念 ORACLE 数据库系统是美国 ORACLE 公司（甲骨文）提供的以分布式数据库为核心的一组软件产品，是目前最流行的客户/服务器(CLIENT/SERVER)或 B/S 体系结构的数据库之一。\nOracle默认的端口号是：1521\n2. Oracle 数据库的体系结构 数据库：一个操作系统只有一个oracle数据库 实例：数据库的后台的一系列进程，一个计算机只有一个实例 数据文件（dbf）：数据库软件操作的 表空间：一个表空间可以有多个数据文件，但是一个数据文件只属于一个表空间 用户：用户操作表空间 2.1. 数据库：database Oracle 数据库是数据的物理存储。这就包括（数据文件 ORA 或者 DBF、控制文件、联机日志、参数文件）。其实 Oracle 数据库的概念和其它数据库不一样，一个操作系统只有一个oracle数据库。可以看作是 Oracle 就只有一个大数据库\n2.2. 实例：Oracle Instance 一个计算机只有一个Oracle实例\n一个 Oracle 实例（Oracle Instance）有一系列的后台进程（Backguound Processes)和内存结构（Memory Structures)组成。一个数据库可以有 n 个实例。\n2.3. 数据文件（dbf） 数据文件(.dbf)，由数据库软件操作的\n数据文件是数据库的物理存储单位。数据库的数据是存储在表空间中的，真正是在某一个或者多个数据文件中。而一个表空间可以由一个或多个数据文件组成，一个数据文件只能属于一个表空间。一旦数据文件被加入到某个表空间后，就不能删除这个文件，如果要删除某个数据文件，只能删除其所属于的表空间才行。\n2.4. 表空间 表空间是 Oracle 对物理数据库上相关数据文件（ORA 或者 DBF 文件）的逻辑映射。一个数据库在逻辑上被划分成一到若干个表空间，每个表空间包含了在逻辑上相关联的一组结构。每个数据库至少有一个表空间(称之为 system 表空间)。 每个表空间由同一磁盘上的一个或多个文件组成，这些文件叫数据文件(datafile)。一个数据文件只能属于一个表空间\n2.5. 用户 用户是在实例下建立的。不同实例中可以建相同名字的用户\n注：表的数据，是有用户放入某一个表空间的，而这个表空间会随机把这些表数据放到一个或者多个数据文件中\n由于 oracle 的数据库不是普通的概念，oracle 是有用户和表空间对数据进行管理和存放的。但是表不是有表空间去查询的，而是由用户去查的。因为不同用户可以在同一个表空间建立同一个名字的表！这里区分就是用户了！\n2.6. SCOTT 用户和 HR 用户 SCOTT 与 HR 就是初始的普通用户，这些用户下面都默认存在了表结构。scott用户与hr用户的表结构：\n3. Oracle 的 SQL 语法 3.1. SQL简介 结构化查询语言(Structured Query Language)简称 SQL。结构化查询语言是一种数据库查询和程序设计语言，用于存取数据以及查询、更新和管理关系数据库系统；同时也是数据库脚本文件的扩展名\nDML(数据库操作语言): 其语句包括动词 INSERT，UPDATE 和 DELETE。它们分别用于添加，修改和删除表中的行。也称为动作查询语言。 DDL(数据库定义语言): 其语句包括动词 CREATE 和 DROP。在数据库中创建新表或删除表（CREAT TABLE 或 DROP TABLE）；为表加入索引等。DDL 包括许多与人数据库目录中获得数据有关的保留字。它也是动作查询的一部分。 DCL(数据库控制语言):它的语句通过 GRANT 或 REVOKE 获得许可，确定单个用户和用户组对数据库对象的访问。某些 RDBMS 可用 GRANT 或 REVOKE 控制对表单个列的访问。 3.2. Oracle 语法注意事项 3.2.1. Oracle语句大小写问题 oracle中分为两种情况，单纯的sql语句不区分大小写，但是如果查询某个字符的话就需要区分大小写。\n如以下情况，是不区分大小写的，查询结果都是一致的： 1 2 select * from emp; SELECT * FROM EMP; 如在emp表中查询ename为\u0026quot;SMITH\u0026quot;（不含引号）的信息，就必须注意大小写： 1 select * from emp where ename=\u0026#39;SMITH\u0026#39;; 3.2.2. dual 伪表 dual：是oracle提供的一张伪表，用于补全sql语法\n1 2 select upper(\u0026#39;abc\u0026#39;); // 执行会报错 select upper(\u0026#39;abc\u0026#39;) from dual; // 这样才可以执行 4. Oracle 查询操作 4.1. Oracle 完整的查询 SQL 语法 1 2 3 4 5 6 7 8 9 10 11 12 select *|指定的列名 from 表名 where 条件 group by 分组 having 条件 order by 排序 4.2. Select 语句的语法格式 1 2 select *|{[distinct column|expression [aliad],……]} from table; 4.2.1. 查询语法 1 select *|列名 from 表名 示例\n1 2 3 4 -- 查询所有 select * from emp; -- 查询部分列 select ename,job from emp; 4.2.2. 别名用法 在查询的结果列中可以使用别名，后面的中文只能用双引号，一般的别名都不用中文\n1 select 列名 别名, 列名 别名, …… from 表名; 别名中，有没有双引号的区别就在于别名中有没有特殊的符号或者关键字。示例如下：\n1 2 -- 别名用法 select ename \u0026#34;员工名称\u0026#34;, job \u0026#34;工作内容\u0026#34; from emp; 4.2.3. 消除重复的数据 使用 distinct 可以消除重复的行，如果查询多列的必须保证多列都重复才能去掉重复\n1 select distinct *|列名, ... from 表名; 示例：\n1 2 -- 消除重复的数据 select distinct ename from emp; 4.2.4. 查询中四则运算 sql 中支持四则运算【+，-，*，/】\nCode Dome:\n1 2 3 4 5 6 --查询员工的年薪:空值是oracle里面最大的 select ename, sal*12 from emp; -- 这里有点问题：如果comm是空的，那么所有数据都是空的 select ename, sal*12+comm from emp; --nvl函数，判断空值用的 第一个参是值，第二参数：如果第一个参数是null，那么就用第二个参数来替换 select ename, sal*12+nvl(comm, 0) from emp; 4.3. 空值 空值是无效的，未指定的，未知的或不可预知的值。空值不是空格或者0。注意事项如下：\n包含 null 的表达式都为 null 空值永远不等于空值 空值是oracle里面最大的 4.4. 连接符 || 字符串的连接使用“||”。示例如下：\n1 2 -- 使用||拼接字符 select \u0026#39;abc\u0026#39; || \u0026#39;def\u0026#39; || \u0026#39;ghi\u0026#39; from dual; 4.5. 使用 where 语句对结果进行过滤语法 1 2 3 select *|{[distinct column|expression [aliad],……]} from table [where condition(s)] 4.6. 运算符 4.6.1. 比较运算符 操作符 含义 = 等于(不是==) \u0026gt; 大于 \u0026gt;= 大于等于 \u0026lt; 小于 \u0026lt;= 小于等于 \u0026lt;\u0026gt;或!= 不等于 注：赋值使用 := 符号\n1 2 3 4 5 6 7 8 -- 查询薪资大于1500 select * from emp where sal \u0026gt; 1500; -- 查询薪资大于600，并且有奖金的员工 select * from emp where sal \u0026gt; 600 and comm \u0026gt; 0; -- 查询薪资大于等于1500 并且小于等于3000 select * from emp where sal \u0026gt;= 1500 and sal \u0026lt;= 3000; 4.6.2. 其他比较运算符 操作符 含义 between … and … 在两个值之间(包含边界) in(set) 等于值列表中的一个 like 模糊查询 is null 空值 1 2 -- 范围查询： between xx and XXX; 包括边界值 select * from emp where sal between 1500 and 3000; 4.6.3. 逻辑运算符 操作符 含义 and 逻辑并（与） or 逻辑或 not 逻辑否 4.7. where 语句 4.7.1. 非空和空的限制 只要字段中存在内容表示不为空，如果不存在内容就是 null，语法：\n1 2 3 4 -- 列不为空 列名 IS NOT NULL -- 列为空 列名 IS NULL 示例：\n1 2 -- 查询薪资大于600，并且有奖金的员工 select * from emp where sal \u0026gt; 600 and comm is not null and comm \u0026gt; 0; 4.7.2. 范围限制 指定了查询范围，那么 sql 可以使用 IN 关键字。语法：\n1 2 3 4 -- 列的值在某个范围内 列名 IN (值1, 值2, ....) -- 列的值不在某个范围内 列名 NOT IN (值1, 值2,...) 其中的值不仅可以是数值类型也可以是字符串\n1 2 -- in操作 ：在某个数据内 select * from emp where deptno in(10, 20); 4.7.3. 模糊查询 在 LIKE 关键字中主要使用以下两种通配符\n%：可以匹配任意长度的内容 _：可以匹配一个长度的内容 在 LIKE 中如果没有关键字表示查询全部\n1 2 3 4 5 -- 模糊查询 like -- 查询名字中有个M 字符的 select * from emp where ename like \u0026#39;%M%\u0026#39;; -- 查询名字中第二个字母是M 字符的 select * from emp where ename like \u0026#39;_M%\u0026#39;; 4.8. 使用 order by 对结果排序 4.8.1. 排序语法 在 sql 中可以使用 ORDER BY 对查询结果进行排序。语法如下：\n1 2 3 4 SELECT * |列名 FROM 表名 {WEHRE 查询条件} ORDER BY 列名 1 ASC|DESC，列名 2...ASC|DESC ORDER BY 列名，默认的排序规则是升序排列，可以不指定 ASC，如果按着降序排列必须指定 DESC。如果存在多个排序字段可以用逗号分隔\n注意：ORDER BY 语句要放在 sql 的最后执行\n1 2 3 4 5 6 7 -- 工资从小到大排列 select * from emp order by sal asc; -- 如果存在多个排序字段可以用逗号分隔 select * from emp order by sal asc, hiredate desc; -- 如果出现Null就要注意 select * from emp where sal \u0026gt; 400 order by comm asc nulls first; select * from emp where sal \u0026gt; 400 order by comm desc nulls last; 4.8.2. 排序中的空值问题 当排序时有可能存在 null 时就会产生问题，可以用 nulls first , nulls last 来指定 null 值显示的位置\n1 2 select * from emp order by sal nulls first; select * from emp order by sal desc nulls last; 4.9. Oracle分页查询 分页操作需要伪列，rownum\n注意：不能使用大于号，如rownum\u0026gt;6，因为是每查询一行分配一个rownum，后面的还没有分配，所以得到的查询永远结果为空\nOracle 分页的语法：\n1 2 3 4 select * from (select rownum as xx,e.* from emp e) t where xx \u0026gt;startIndex and xx \u0026lt;= maxResult; 相关参数：\nstartIndex = (currentPage-1)*pageSize maxResult = (currentPage-1)*pageSize + pageSize startIndex: 开始页数 currentPage: 当前页 pageSize: 每页大小 maxResult: 最大页数 Code Dome:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- =======对员工表的工资排序【降序】之后，取出第二页每页3条记录 -- 先将表按工资进行排序 select * from emp order by sal desc,empno; -- 以上面查询结果做为新表，增加rownum列 select rownum rnum,t.* from (select * from emp order by sal desc,empno) t; -- 再以上面查询结果做为新表，以rownum做为查询索引 select * from ( select rownum rnum,t.* from ( select * from emp order by sal desc,empno ) t ) e where e.rnum\u0026gt;3 and e.rnum\u0026lt;=6; 4.10. 多表查询 4.10.1. Oracle 的连接条件的类型 等值连接 不等值连接 外连接 自连接 4.10.2. 多表连接基本查询 使用一张以上的表做查询就是多表查询。语法：\n1 2 SELECT {DISTINCT} *|列名.. FROM 表名 别名，表名 1 别名 {WHERE 限制条件ORDER BY 排序字段 ASC|DESC...} 这种多表查询也相当于内连接\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 -- 多表查询（笛卡尔积） select * from emp,dept; -- 多表关联查询 select * from emp e,dept d where e.deptno=d.deptno; select e.empno,e.ename,d.deptno,d.dname,d.loc from emp e,dept d where e.deptno=d.deptno; -- 自关联查询 -- 需求：查询出每个员工的上级领导 -- emp表中的mgr字段是当前雇员的上级领导的编号，所以该字段对emp表产生了自身关联，可以使用mgr字段和empno来关联 select e.empno,e.ename,e.mgr,e1.empno,e1.ename from emp e,emp e1 where e.mgr=e1.empno; -- 需求：在上一个例子的基础上查询该员工的部门名称 -- 只要在上一个例子基础上再加一张表dept的关联，使用deptno来做关联字段 select e.empno,e.ename,e.mgr,e1.empno,e1.ename,e.deptno,d.dname from emp e,emp e1,dept d where e.mgr=e1.empno and e.deptno=d.deptno; -- 需求：查询出每个员工编号，姓名，部门名称，工资等级和他的上级领导的姓名，工资等级 select e.empno,e.ename,e.mgr,e.deptno,decode(s.grade, \u0026#39;1\u0026#39;,\u0026#39;等级1\u0026#39;, \u0026#39;2\u0026#39;,\u0026#39;等级2\u0026#39;, \u0026#39;3\u0026#39;,\u0026#39;等级3\u0026#39;, \u0026#39;4\u0026#39;,\u0026#39;等级4\u0026#39;, \u0026#39;5\u0026#39;,\u0026#39;等级5\u0026#39;, \u0026#39;?\u0026#39;) as \u0026#34;员工薪级\u0026#34;, d.dname,e1.empno,e1.ename, case s1.grade when 1 then \u0026#39;等级1\u0026#39; when 2 then \u0026#39;等级2\u0026#39; when 3 then \u0026#39;等级3\u0026#39; when 4 then \u0026#39;等级4\u0026#39; else \u0026#39;等级5\u0026#39; end as \u0026#34;领导薪级\u0026#34; from emp e,emp e1,dept d,salgrade s,salgrade s1 where e.mgr=e1.empno and e.deptno=d.deptno and e.sal between s.losal and s.hisal and e1.sal between s1.losal and s1.hisal; 4.10.3. 外连接（左右连接） 右连接\n用右表的记录去匹配左表的记录，如果条件满足，则左边显示左表的记录；否则左边显示 null。（左表和右表取决于定义在实际语句的位置）\n右连接特点：如果右外连接，右边的表的记录一定会全部显示完整\n左连接\n用左表的记录去匹配右表的记录，如果条件满足，则右边显示右表的记录；否则右表显示 null。（左表和右表取决于定义在实际语句的位置）\n左连接特点：左边的表的记录一定会全部显示完整\n外连接查询：判断以哪个表为基准表，如果是基准表，那么它的数据全部显示。使用(+)表示左连接或者右连接，等价于 left join 或者 right join\n因为（+）这种形式是 oracle 数据库独有的，所以优先掌握 left join 或 right join 方式的写法。想让哪个表为基准表，那么将(+)号放到对面一方。示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 -- 右连接，特点：右表记录一定会全部显示完整 -- 需求：当我们在做基本连接查询的时候，查询出所有的部门下的员工 -- 编号为 40 的部门下没有员工，但是要求把该部门也展示出来 select * from emp e,dept d where e.deptno(+)=d.deptno; -- 等价于： select * from emp e right join dept d on e.deptno=d.deptno; -- 左连接，特点：左表记录一定会全部显示完整 -- 需求：查询出所有员工的上级领导 -- KING 的上级领导没有被展示，需要使用外连接把他查询出来 select e.empno,e.ename,e.mgr,e1.empno,e1.ename from emp e,emp e1 where e.mgr=e1.empno(+); -- 等价于： select e.empno,e.ename,e.mgr,e1.empno,e1.ename from emp e left join emp e1 on e.mgr=e1.empno; 4.11. 子查询 4.11.1. 子查询概述 一条 SQL 语句(子查询)的查询结果做为另一条查询语句(父查询)的条件或查询结果，这种操作则称为子查询。\n多条 SQL 语句嵌套使用，内部的 SQL 查询语句称为子查询。\n4.11.2. 子查询的语法 1 2 3 4 5 select select_list from table where expr operator (select select_list from table); 注意：\n子查询 (内查询) 在主查询之前一次执行完成。 子查询的结果被主查询使用 (外查询)。 4.11.3. 子查询的类型 4.11.4. 单行子查询 只返回一条记录。单行操作符\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 -- 单行子查询 select ename,job,sal from emp where job= (select job from emp where empno=7566) and sal\u0026gt; (select sal from emp where empno=7782); -- 使用聚合函数作为子查询条件 select deptno,min(sal) from emp group by deptno having min(sal)\u0026gt; (select min(sal) from emp where deptno=20); -- 需求：查询出比雇员7654的工资高，同时从事和7788的工作一样的员工 select e1.ename,e1.job,e1.sal from emp e1 where e1.sal\u0026gt; (select e2.sal from emp e2 where e2.empno=7654) and e1.job= (select e3.job from emp e3 where e3.empno=7788); -- 需求：查询每个部门的最低工资和最低工资的雇员和部门名称 select e.ename,s.minsal,d.dname from emp e, (select e1.deptno,min(e1.sal) minsal from emp e1 group by e1.deptno) s, dept d where e.deptno=d.deptno and e.sal=s.minsal; 4.11.5. 多行子查询 返回了多条记录，多行操作符\n4.11.6. 子查询中的 null 值问题 多行子查询中 null 值需要注意的问题：查询结果为空，不会报错。\n单行子查询中的 null 值问题\n多行子查询中的 null 值问题\n4.11.7. Exists 用法 语法：\n1 exists(sql 查询语句) 函数的效果是：sql 查询语句为空，则返回值是 false；sql 查询语句有值，则返回值就是 true\n示例：\n1 2 3 4 5 6 7 8 select * from emp where exists (select * from dept where deptno=1); -- 等同于：select * from emp where 1=2 select * from emp where exists (select * from dept where deptno=10); -- 等同于：select * from emp where 1=1 -- 范例：查询有员工的部门 select * from dept d where exists (select * from emp e where e.deptno = d.deptno); -- 使用in替换 select * from dept d where d.deptno in (select distinct deptno from emp); 如果是大数据量【百万级】的查询，建议使用 exists，使用 in 的话会全表查询\n4.11.8. Oracle 中的伪列 oracle提供的两个伪列\nROWNUM：表示行号，实际上只是一个列，但是这个列是一个伪列，此列可以在每张表中出现。【先查询数据，再分配序号。找出一条数据，分配一个rownum】 ROWID：表中每行数据指向磁盘上的物理地址 4.12. Oracle 查询综合示例 4.12.1. Oracle查询练习 找到员工表中工资最高的前三名 1 2 3 4 5 6 7 8 9 10 -- 先排序 select e.empno,e.ename,e.sal from emp e order by e.sal desc nulls last,e.empno asc; -- 使用伪列rownum select rownum,t.empno,t.ename,t.sal from (select e.empno,e.ename,e.sal from emp e order by e.sal desc nulls last,e.empno asc) t where rownum \u0026lt;= 3; 找到员工表中薪水大于本部门平均薪水的员工 1 2 3 4 5 6 7 8 -- 先查询每个部门的平均薪水 select e.empno,e.ename,e.sal,t.avgsal from emp e,(select deptno,avg(sal) avgsal from emp group by deptno) t where e.sal \u0026gt; t.avgsal and e.deptno=t.deptno order by e.empno asc; 统计每年入职的员工个数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -- TO_CHAR 函数对日期的转换成字符串 select e.hdate,count(*) from (select to_char(hiredate, \u0026#39;yyyy\u0026#39;) hdate from emp) e group by e.hdate order by e.hdate asc; -- 把上面的当成一张表，这里的聚合函数是为了补全语法 select sum(t.dcount) as \u0026#34;Total\u0026#34;, sum(decode(hdate,\u0026#39;1980\u0026#39;,dcount)) as \u0026#34;1980\u0026#34;, sum(decode(hdate,\u0026#39;1981\u0026#39;,dcount)) as \u0026#34;1981\u0026#34;, sum(decode(hdate,\u0026#39;1982\u0026#39;,dcount)) as \u0026#34;1982\u0026#34;, sum(decode(hdate,\u0026#39;1987\u0026#39;,dcount)) as \u0026#34;1987\u0026#34; from (select e.hdate,count(*) dcount from (select to_char(hiredate, \u0026#39;yyyy\u0026#39;) hdate from emp) e group by e.hdate) t; 4.12.2. Oracle 综合查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 -- 建表语句和插入数据 -- 案例【1.1】查询学员列表，显示学员姓名，电话，班级名称 select stu.name \u0026#34;学员姓名\u0026#34;,stu.tele \u0026#34;电话\u0026#34;,cla.name \u0026#34;班级名称\u0026#34; from T_STUDENT stu,T_CLASS cla where stu.classid=cla.id; -- 案例【1.2】查询学员成绩列表，显示班级类型名称，班级名称，学员姓名，课程名称，考试分数 select ct.name \u0026#34;班级类型名称\u0026#34;,cla.name \u0026#34;班级名称\u0026#34;,stu.name \u0026#34;学员姓名\u0026#34;,course.name \u0026#34;课程名称\u0026#34;,mark.score \u0026#34;考试分数\u0026#34; from T_STUDENT stu,T_CLASS cla,T_CLASS_TYPE ct,T_COURSE course,T_MARK mark where stu.classid=cla.id and cla.type=ct.id and stu.id=mark.studentid and course.id=mark.courseid and course.type=ct.id; -- 案例【1.3】查询学员成绩列表，显示学员姓名，考试分数，如果该学员没有考试记录也要列出姓名 -- 方式1 select s.name \u0026#34;学员姓名\u0026#34;,m.score \u0026#34;分数\u0026#34; from T_STUDENT s left join T_MARK m on s.id=m.studentid; -- 方式2 oracle特有方式 select s.name \u0026#34;学员姓名\u0026#34;,m.score \u0026#34;分数\u0026#34; from T_STUDENT s,T_MARK m where s.id=m.studentid(+); -- 案例【1.4】统计每门课程的平均分 select courseid,round(avg(score)) \u0026#34;AVGSCORE\u0026#34; from T_MARK group by courseid; -- 案例【1.5】上例的基础上显示课程名称 select m.courseid,c.name,round(avg(m.score)) \u0026#34;AVGSCORE\u0026#34; from T_MARK m,T_COURSE c where m.courseid=c.id group by m.courseid,c.name; -- 案例【1.6】继续在上例的基础上统计平均分大于60且小于80的课程，并按平均分由高到低排序 select * from (select m.courseid,c.name,round(avg(m.score)) \u0026#34;AVGSCORE\u0026#34; from T_MARK m,T_COURSE c where m.courseid=c.id group by m.courseid,c.name) t where t.AVGSCORE \u0026gt; 60 and t.AVGSCORE \u0026lt; 80 order by t.AVGSCORE desc; -- 案例【1.7】查询唐浩然的同班同学 -- 先查询所在班级 select classid from T_STUDENT where name=\u0026#39;唐浩然\u0026#39;; -- 以上表做为基础条件，再查询学生表， select * from T_STUDENT where classid=( select classid from T_STUDENT where name=\u0026#39;唐浩然\u0026#39; ) and name \u0026lt;\u0026gt; \u0026#39;唐浩然\u0026#39;; -- 案例【1.8】查询唐浩然的同班同学并且学历一样的同学 -- 查询学历 select edu from T_STUDENT where name=\u0026#39;唐浩然\u0026#39;; -- 以上面查询学历做为子查询条件 select * from T_STUDENT where classid=( select classid from T_STUDENT where name=\u0026#39;唐浩然\u0026#39;) and name \u0026lt;\u0026gt; \u0026#39;唐浩然\u0026#39; and edu=( select edu from T_STUDENT where name=\u0026#39;唐浩然\u0026#39;); -- 案例【1.9】查询每门科目最高分数 -- 方式1 select c.name,max(m.score) maxscore from T_COURSE c,T_MARK m where c.id=m.courseid group by m.courseid,c.name; -- 方式2,子查询方式 select courseid,max(score) maxscore from T_MARK group by courseid; -- 以上表做为子表 select c.name,m.maxscore from T_COURSE c,( select courseid,max(score) maxscore from T_MARK group by courseid) m where c.id=m.courseid; -- 案例【2.0】分数在90分以上的同学 -- 查询90分以上所有学生的ID，并去重 select distinct STUDENTID from T_MARK where SCORE \u0026gt; 90; -- 以上表做为子查询条件 select * from T_STUDENT where ID in ( select distinct STUDENTID from T_MARK where SCORE \u0026gt; 90); -- 案例【2.1】查询曾经缺考的同学 -- 查询有参数考试的学生 select distinct STUDENTID from T_MARK; -- 以上表子查询条件 select * from T_STUDENT where ID not in ( select distinct STUDENTID from T_MARK); -- 案例【2.2】查询成绩排名表的第二页（按每页5条数据） -- 先按成绩排名查询 select * from T_MARK order by score desc; -- 以上面查询的表为基础，添加rownum select rownum \u0026#34;R\u0026#34;,t.* from ( select * from T_MARK order by score desc) t; -- 以上表为基础，进行分页查询 select * from ( select rownum \u0026#34;R\u0026#34;,t.* from ( select * from T_MARK order by score desc) t) where r\u0026gt;5 and r\u0026lt;=10; 4.13. 集合运算 4.13.1. 什么是集合运算 union/union all 并集 union all 合并多个结果集 union 合并多个结果集，去除重复 intersect 交集 minus 差集 4.13.2. 并集，类似于or 范例：工资大于 1500，或者是 20 号部门下的员工（并集）\n1 2 3 4 5 6 7 8 9 10 -- 并集 select * from emp where sal\u0026gt;1500 or deptno=20; -- or 关键字效率会很低，全表查询，索引用不了 -- 方式2 select * from emp where sal \u0026gt; 1500 union -- 使用union all包括重复部分 select * from emp where deptno = 20; 4.13.3. 交集，类似于and 范例：工资大于 1500，并且是 20 号部门下的员工（交集）\n1 2 3 4 5 6 7 8 9 -- 交集 -- 方式1 select * from emp where sal\u0026gt;1500 and deptno=20; -- 方式2 select * from emp where sal \u0026gt; 1600 intersect select * from emp where deptno = 20; 4.13.4. 差集 范例：1981 年入职的普通员工（不包括总裁和经理）（差集）\n1 2 3 4 5 -- 差集 select * from emp where to_char(hiredate,\u0026#39;yyyy\u0026#39;)=\u0026#39;1981\u0026#39; -- a minus select * from emp where job=\u0026#39;MANAGER\u0026#39; or job = \u0026#39;PRESIDENT\u0026#39;; -- b -- 如果a minus b,a 中没有在b 中的数据，全部显示 4.13.5. 集合运算的特征 集合运算两边查询的字段数量、字段类型、顺序必须一致\n4.14. 递归查询【了解】 语法：\n1 2 3 select * from 表名 start with 条件一 --开始查询位置 connect by 条件二 --指定查询当前的关系 connect by 条件二中使用到关键字prior：从哪个方向进行检索。如以下示例：\n1 2 3 4 5 6 7 8 9 10 -- 查询员工的领导 select * from emp start with empno=7369 -- prior:从哪个方向进行检索 connect by empno=prior mgr; -- 查询领导的下属是那些 select * from emp start with empno=7566 connect by mgr=prior empno; 5. SQL 函数 5.1. 定义 注意：函数可以没有参数，但必须要有返回值\n5.2. 函数的类型 单行函数 多行函数 注意：无论是单行函数还是多行函数，返回的结果都是一个\n5.3. 单行函数 5.3.1. 单行函数分类 字符函数 数值函数 日期函数 转换函数 通用函数 5.3.2. 字符函数 字符控制函数\n关键字 作用 concat 字符串的连接可以使用 concat 函数也可以使用“` substr 字符串的截取，substr(s, p1, p2)第1个参数s是源字符串第2个参数p1是开始索引，开始的索引使用 1 和 0 效果相同，第二个字符的索引是2第3个参数p2是截取的长度 length/lengthb 获取字符串的长度 instr lpad/rpad trim replace 字符串替换，第一个参数是源字符串，第二个参数被替换的字符串，第三个是替换字符串 大小写控制函数\n关键字 作用 lower 将结果转成小写字母 upper 将结果转成大写字母 initcap 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 -- 字符串拼接(只能是两个拼接) select concat(\u0026#39;abc\u0026#39;,\u0026#39;zxy\u0026#39;) from dual; -- 字符串拼接（三个拼接）建议使用|| select concat(123,concat(\u0026#39;abc\u0026#39;,\u0026#39;zxy\u0026#39;)) from dual; -- 大小写 select lower(\u0026#39;MOON\u0026#39;) from dual; select upper(\u0026#39;moon\u0026#39;) from dual; -- 查询用户名称小写的 select lower(ename) from emp; -- 字符串长度 select length(\u0026#39;abc123\u0026#39;) as \u0026#34;字符长度\u0026#34; from dual; -- 截取字符串 /* substr(v1,p1,p2) v1:要截取的字符串 p1: 从哪里开始截取,0 和 1 都是一样的，都是开始下标 p2: 截取的长度 */ select substr(\u0026#39;moonzero\u0026#39;,0,3) from dual; select substr(\u0026#39;moonzero\u0026#39;,1,3) from dual; select substr(\u0026#39;moonzero\u0026#39;,2,3) from dual; -- 字符串替换，第一个参数是源字符串，第二个参数被替换的字符串，第三个是替换字符串 select replace (\u0026#39;moonzero\u0026#39;,\u0026#39;oo\u0026#39;,\u0026#39;00\u0026#39;) from dual; 5.3.3. 数值函数 用于操作数字\n5.3.3.1. round：四舍五入 1 round(数字, 保留的小数位) 如果不写参数，默认情况下，只保留整数位\n5.3.3.2. trunc：截断，直接截断字符，不会进行四舍五入 1 trunc(数字, 保留的小数位) 如果不写保留的小数位，默认是直接切断只保留整数位\n5.3.3.3. mod：求余 1 mod(被除数，除数) 5.3.3.4. 示例 1 2 3 4 5 6 7 8 9 10 11 --操作数字 -- 四舍五入 --只保留整数位(不写参数，默认只保留整数位) select round(58.8888) from dual; -- 第二个参数，保留两位小数 select round(58.8888, 2) from dual; select round(58.8838, 2) from dual; -- 直接截断 select trunc(58.8888, 2) from dual; -- 求余 select mod(1200, 500) from dual; 5.3.4. 日期函数 sysdate：当前数据库所在服务器的时间\n5.3.4.1. Oracle的日期 Oracle 中的日期型数据实际含有两个值:日期和时间\n默认的日期格式是 DD-MON-RR\n5.3.4.2. 日期的数学运算 在日期上加上或减去一个数字结果仍为日期。两个日期相减返回日期之间相差的天数，可以用数字除 24 获得两个时间段中的月数：MONTHS_BETWEEN(date, date) 获得几个月后的日期：ADD_MONTHS(sysdate, num) 5.3.4.3. 示例 1 2 3 4 5 6 7 8 9 10 11 12 -- sysdate : 当前数据库所在服务器的时间 select sysdate from dual; -- 查询员工入职的天数 select * from emp; select ename,round(sysdate - hiredate) \u0026#34;入职天数\u0026#34; from emp; -- 在上面的基础上，算一下入职的周数 select ename,round((sysdate - hiredate)/7) \u0026#34;入职周数\u0026#34; from emp; -- 计算入职的月数 select ename,round(months_between(sysdate,hiredate)) \u0026#34;入职月数\u0026#34; from emp; -- 获得几个月后的日期 select add_months(sysdate, 3) from dual; 5.3.5. 转换函数 5.3.5.1. TO_CHAR 函数对日期的转换成字符串 1 to_char(date, \u0026#39;format_model\u0026#39;) 日期的格式：\n注：在设置'yyyy-mm-dd'格式后，10 以下的月份前面被被补了前导零，可以使用 fm 去掉前导零\n5.3.5.2. TO_CHAR 函数对数字的转换成字符串 1 to_char(number, \u0026#39;format_model\u0026#39;) 数字转换的格式：\n5.3.5.3. TO_NUMBER 和 TO_DATE 函数 使用TO_NUMBER函数将字符转换成数字，转换后的数字可以进行运算\n1 to_number(char[, \u0026#39;format_model\u0026#39;]) 使用TO_DATE函数将字符转换成日期\n1 to_date(char[, \u0026#39;format_model\u0026#39;]) Code Dome:\n1 2 3 4 5 6 7 8 9 -- 把时间转成字符串,10以下的月前面被被补了前导零，可以使用 fm 去掉前导零 select to_char(sysdate, \u0026#39;yyyy/mm/dd\u0026#39;) from dual; select to_char(sysdate, \u0026#39;fmyyyy/mm/dd\u0026#39;) from dual; -- 把字符串转成时间 select to_date(\u0026#39;2018/9/10\u0026#39;, \u0026#39;yyyy/mm/dd\u0026#39;) from dual; -- 把数字转成字符 select to_char(123) from dual; -- 把字符串转成数字,转换后可以进行运算 select to_number(\u0026#39;123\u0026#39;)+to_number(\u0026#39;321\u0026#39;) from dual; 5.3.6. 通用函数 5.3.6.1. 通用函数的定义 适用于任何数据类型，同时也适用于空值的函数\n5.3.6.2. 常用的通用函数 1 2 3 4 5 -- 如果expr1为空，则输出expr2。前后的数据类型要一致。 nvl(expr1, expr2) nvl2(expr1, expr2, expr3) nullif(expr1, expr2) coalesce(expr1, expr2,……, exprn) Code Dome:\n1 2 -- nvl（p1,p2） ,如果p1为空，就输出p2,前后的数据类型要一直 select ename,nvl(comm, 0) from emp; 5.3.7. 条件表达式 5.3.7.1. 条件表达式定义 在 SQL 语句中使用 IF-THEN-ELSE\n5.3.7.2. 实现方式1：case 表达式 CASE 表达式：SQL99 的语法，类似 Basic，比较繁琐。语法如下：\n1 2 3 4 5 6 case expr when comparison_expr1 then return_expr1 [when comparison_expr2 then return_expr2 …… when comparison_exprn then return_exprn else else_expr] end 5.3.7.3. 实现方式2：decode 函数（推荐） DECODE 函数：Oracle 自己的语法，类似 Java，比较简洁。语法如下：\n1 decode(条件,值1,翻译值1,值2,翻译值2,...值n,翻译值n,缺省值) 作用：根据条件返回相应值 参数：c1, c2, \u0026hellip;,cn,字符型/数值型/日期型，必须类型相同或 null 注：值1……n 不能为条件表达式，这种情况只能用case when then end解决 5.3.7.4. 示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -- 条件表达式 select ename,deptno from emp; -- 使用DECODE 函数 select ename,deptno, decode(deptno,10,\u0026#39;开发部\u0026#39;,20,\u0026#39;销售部\u0026#39;,30,\u0026#39;运维部\u0026#39;,\u0026#39;大Boss\u0026#39;) from emp; -- 使用CASE 表达式 select ename,deptno, case deptno when 10 then \u0026#39;开发部\u0026#39; when 20 then \u0026#39;销售部\u0026#39; when 30 then \u0026#39;运维部\u0026#39; else \u0026#39;大Boss\u0026#39; end from emp; 5.4. 多行函数 5.4.1. 多行函数的定义 也叫组函数、分组函数，即聚合查询。分组函数作用于一组数据，并对一组数据返回一个值。\n在where条件中不能写聚合函数，只能写在having中\n组函数会忽略空值；NVL 函数使分组函数无法忽略空值\n5.4.2. 常用的多行函数 函数名称 作用 avg 查询平均值 count 统计记录数 max 最大值查询 min 最小值查询 sum 求和函数 注：所有的聚合函数都不统计null值\ncount(字段) 是根据字段统计所有行，如果字段为 null，不进行统计\ncount(1) 与 count(*) 效果完全一样，但 count(1) 效率更高。count(1) 就是将所有行都看成“1”，不行的数据，所以查询效率更高\n5.4.3. 分组数据 可以使用 group by 子句将表中的数据分成若干组。语法格式：\n1 2 3 4 5 select *|{[distinct column|expression [aliad],……]} from table [where condition(s)] [group by group_by_expression] [order by column] 注意：如果两个表关联使用分组查询，group by 后面的跟的条件是两个表的分组条件的列名。\n5.4.4. 过滤分组数据 报了一个 ORA-00937 的错误\n注意：\n如果使用分组函数，SQL 只可以把 GOURP BY 分组条件字段和分组函数查询出来，不能有其他字段。 如果使用分组函数，不使用 GROUP BY 只可以查询出来分组函数的值 5.4.5. WHERE 和 HAVING 的区别 最大区别在于：where 后面不能有聚合函数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 -- 计算所有员工数 select count(*) from emp; -- 把所有员工的工资加起来 select sum(sal) from emp; -- 计算部门的平均工资 select round(avg(sal),2),decode(deptno,10,\u0026#39;开发部\u0026#39;,20,\u0026#39;销售部\u0026#39;,30,\u0026#39;运维部\u0026#39;,\u0026#39;大Boss\u0026#39;) from emp group by deptno; -- 计算部门的平均工资大于2000 select round(avg(sal)),decode(deptno,10,\u0026#39;开发部\u0026#39;,20,\u0026#39;销售部\u0026#39;,30,\u0026#39;运维部\u0026#39;,\u0026#39;大Boss\u0026#39;) from emp group by deptno having avg(sal) \u0026gt; 2000; 6. 使用 DDL 语句管理表 6.1. 创建表空间 表空间：ORACLE 数据库的逻辑单元，oracle面向的是表空间。\n一个实例（数据库）对应多个表空间 一个表空间可以与多个数据文件（物理结构）关联一个数据库下可以建立多个表空间 一个表空间可以建立多个用户、一个用户下可以建立多个表。 我们(用户)操作表空间，真正存储数据，存到.dbf文件\n注：创建表空间需要dba权限，创建表空间的语法顺序不能调换\n创建表空间语法与相关参数：\n参数 说明 create tablespace 表空间名称 创建表空间 datafile 指定表空间对应的存储位置和数据文件 size 后定义的是表空间的初始大小 autoextend on 开启自动扩展，当表空间存储都占满时，自动扩展 next 指定的是下一次扩展的文件大小 创建表空间示例:\n1 2 3 4 5 6 -- 创建表空间 create tablespace moonzero datafile \u0026#39;c:\\test.dbf\u0026#39; size 100m autoextend on next 10m 6.2. 用户 6.2.1. 创建用户 1 2 3 4 -- 创建用户 create user moon identified by moon default tablespace moonzero 相关参数：\ncreate user：创建用户 identified by：设置用户的密码，不能是纯数字，经过测试，如果想使用纯数字，必须使用双引号，如：\u0026ldquo;123456\u0026rdquo; default tablespace：用户管理（使用）的表空间名称 oracle 数据库与其它数据库产品的区别在于，表和其它的数据库对象都是存储在用户下的\n6.2.2. 用户赋权限 新创建的用户没有任何权限，登陆后会提示没有权限，需要进行授权。\nOracle 中已存在三个重要的角色：connect 角色，resource 角色，dba 角色。\nCONNECT 角色： \u0026ndash;是授予最终用户的典型权利，最基本的 ALTER SESSION \u0026ndash;修改会话 CREATE CLUSTER \u0026ndash;建立聚簇 CREATE DATABASE LINK \u0026ndash;建立数据库链接 CREATE SEQUENCE \u0026ndash;建立序列 CREATE SESSION \u0026ndash;建立会话 CREATE SYNONYM \u0026ndash;建立同义词 CREATE VIEW \u0026ndash;建立视图 RESOURCE 角色： \u0026ndash;是授予开发人员的 CREATE CLUSTER \u0026ndash;建立聚簇 CREATE PROCEDURE \u0026ndash;建立过程 CREATE SEQUENCE \u0026ndash;建立序列 CREATE TABLE \u0026ndash;建表 CREATE TRIGGER \u0026ndash;建立触发器 CREATE TYPE \u0026ndash;建立类型 DBA 角色：拥有全部特权，是系统最高权限，只有 DBA 才可以创建数据库结构，并且系统权限也需要 DBA 授出，且 DBA 用户可以操作全体用户的任意基表，包括删除 进入 system 用户下给用户赋予权限（connect，resource，dba），否则无法正常登陆\n1 grant dba to 用户名 6.2.3. 回收权限 1 revoke connect,resource from 用户名; 多个权限，使用“,”分隔\n6.3. Oracle 数据类型 6.3.1. 字符串 数据类型 说明 char(size) 定长的字符串数据；例：name char(10): 存储\u0026rsquo;xxxx\u0026rsquo;,实际占用10个字符，不够10个使用空格补全 varchar2(size) 可变长的字符串数据，最大长度是4000字节；例：name varchar2(10): 存储\u0026rsquo;xxxx\u0026rsquo;，实际占用4个字符 long 存储字符串数据，最大为2G 6.3.2. 数字 数据类型 说明 number(p1,p2) 标识数字类型，可变长数值数据参数p1：指定数字的总长度（注：总长度包括小数位）参数p2：指定几位小数 6.3.3. 日期 数据类型 说明 date 日期类型数据，区分到时分秒，类似mysql的datatime timestamp 时间撮，区分到秒的后9位 6.3.4. 大数据类型 数据类型 说明 clob 存储字符数据，最大可达到4G blob 存储二进制数据，最大可达到4G 6.3.5. 其它类型 数据类型 说明 raw and long raw 原始的二进制数据 bfile 存储外部文件的二进制数据，最大可达到4G rowid 行地址 6.4. uuid 生成值 sys_guid() 函数，用于生成不同的uuid\n1 select sys_guid() from dual; 6.5. 创建表 创建表语法：\n1 2 create table [schema.]table (column datatype [default expr][,…]); 使用子查询创建表的语法：\n1 2 3 create TABLE table [(column, column,……)] as subquery; 示例:\n1 2 3 4 5 6 7 8 9 -- 创建表 create table person( pid number(10), name varchar2(10), gender number(1) default 1, birthday date ); -- 插入数据，插入数据后，记得要点击提交 insert into person values (1,\u0026#39;剑圣\u0026#39;,1,to_date(\u0026#39;2018-2-2\u0026#39;, \u0026#39;yyyy-mm-dd\u0026#39;)); 6.6. 修改表 在 sql 中使用 alter 可以修改表\n添加语法： 1 ALTER TABLE 表名称 ADD(列名 1 类型 [DEFAULT 默认值]，列名 1 类型 [DEFAULT 默认值]...) 修改语法： 1 ALTER TABLE 表名称 MODIFY(列名 1 类型 [DEFAULT 默认值]，列名 1 类型 [DEFAULT 默认值]...) 修改列名语法: 1 ALTER TABLE 表名称 RENAME COLUMN 列名 1 TO 列名 2 删除列语法： 1 alter table 表名称 drop column 列名 示例：\n1 2 3 4 5 6 -- 在 person 表中增加列 address alter table person add(address varchar2(10)); -- 把 person 表的 address 列的长度修改成 20 长度 alter table person modify(address varchar2(20)); -- 修改列名 alter table person rename column ddd to address; 6.7. 删除表 语法：\n1 DROP TABLE 表名; 6.8. 复制整个表 使用子查询可以复制整个表\n1 create table 新表名 as (select * from 要复制的表名); 6.9. 约束 在数据库开发中，约束是必不可少，使用约束可以更好的保证数据的完整性。在 Oracle 数据库中。约束的类型包括：\n主键约束（Primary Key） 非空约束（Not Null） 唯一约束（Unique） 外键约束（Foreign Key） 检查性约束（Check） 6.9.1. 主键约束 主键约束都是在 id 上使用，而且本身已经默认了内容不能为空，可以在建表的时候指定。关键字：primary key\n指定主键约束的名字的语法：\n1 constraint person_pk_pid primary key(pid) 示例：\n1 2 3 4 5 6 7 create table person( pid number(10), name varchar2(10), gender number(1) default 1, birthday date, constraint person_pk_pid primary key(pid) ); 6.9.2. 非空约束 使用非空约束，可以使指定的字段不可以为空。\n关键字：not null\n6.9.3. 唯一约束（unique） 表中的一个字段的内容是唯一的。关键字：unique\n唯一约束的名字也可以自定义\n1 constraint person_name_uk unique(name) 6.9.4. 检查约束 使用检查约束可以来约束字段值的合法范围。关键字：check(列名 in(值1,值2,...))\n检查约束也可以自定义\n1 constraint person_gender_ck check(gender in (1,2)) 6.9.5. 外键约束 外键是两张表的约束，可以保证关联数据的完整性\n外键关联注意：\n外键一定是主表的主键，外键的位置在从表中 删表时一定先删子表再删主表，如果直接删主表会出现由于约束存在无法删除的问题 可以强制删除主表 drop table 主表名 cascade constraint;【不建议使用】 删除主表的数据可以先删除子表的关联数据，再删主表，也可以使用级联删除 级联删除在外键约束上要加上 on delete cascade【不建议使用】 1 2 constraint order_detail_order_id_fk foreign key(order_id) references orders(order_id) on delete cascade 7. 使用 DML 语句处理数据 7.1. 插入数据 语法：\n1 insert into 表名[(列名 1，列名 2，...)] values (值 1，值 2，...) 简单写法（不建议）\n1 insert into 表名 values(值 1，值 2，...) 注意：使用简单的写法必须按照表中的字段的顺序来插入值，而且如果有为空的字段使用 null\n7.2. 更新数据 语法：\n1 2 3 4 -- 全部修改： UPDATE 表名 SET 列名 1=值 1，列名 2=值 2，.... -- 局部修改： UPDATE 表名 SET 列名 1=值 1，列名 2=值 2，....WHERE 修改条件； Code Dome:\n1 2 3 4 5 -- 在 update 中使用子查询： -- 例如：给 NEW YORK 地区的所有员工涨 100 员工资 update emp set sal=sal+100 where deptno in ( select deptno from dept where loc=\u0026#39;NEW YORK\u0026#39;); 7.3. ORACLE 如果数据存在则执行更新，不存在则执行插入的方法 MERGE语句是Oracle9i新增的语法，用来合并 UPDATE 和 INSERT 语句。通过MERGE语句，根据一张表或子查询的连接条件对另外一张表进行查询，连接条件匹配上的进行 UPDATE，无法匹配的执行INSERT。\n这个语法仅需要一次全表扫描就完成了全部工作，执行效率要高于 INSERT＋UPDATE。\n语法：\n1 2 3 4 5 6 7 MERGE \u0026lt;hint\u0026gt; INTO \u0026lt;table_name\u0026gt; -- 表名称 USING \u0026lt;table_view_or_query\u0026gt; -- 表查询信息 ON (\u0026lt;condition\u0026gt;) -- 条件 WHEN MATCHED THEN \u0026lt;update_clause\u0026gt; -- 更新操作 DELETE \u0026lt;where_clause\u0026gt; -- 删除操作 WHEN NOT MATCHED THEN \u0026lt;insert_clause\u0026gt; -- 插入操作 [LOG ERRORS \u0026lt;log_errors_clause\u0026gt; \u0026lt;reject limit \u0026lt;integer | unlimited\u0026gt;]; 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 -- 例子1： MERGE INTO USERINFO a USING ( SELECT \u0026#39;1\u0026#39; as UNAME FROM dual ) b ON (a.UNAME = b.UNAME) WHEN MATCHED THEN UPDATE SET a.ADDR = \u0026#39;ddd\u0026#39;, a.UTC = 1234567 WHEN NOT MATCHED THEN INSERT (a.ADDR, a.UTC, a.UNAME) VALUES (\u0026#39;aaa\u0026#39;, 7654321, \u0026#39;1\u0026#39;); -- 例子2： MERGE INTO T T1 USING (SELECT \u0026#39;1001\u0026#39; AS a,2 AS b FROM dual) T2 ON ( T1.a=T2.a) WHEN MATCHED THEN UPDATE SET T1.b = T2.b WHEN NOT MATCHED THEN INSERT (a,b) VALUES(T2.a,T2.b); 7.4. 删除数据 语法:\n1 delete from 表名 where 删除条件; 在删除语句中如果不指定删除条件的话就会删除所有的数据\n**truncate table 实现数据删除，摧毁表结构，再重建，效率最高。\n比较 truncat 与 delete 实现数据删除\ndelete 删除的数据可以 rollback，也可以闪回 delete 删除可能产生碎片，并且不释放空间 truncate 是先摧毁表结构，再重构表结构 注意：插入、更新和删除会引起数据的变化。我们就必须考虑数据的完整性。\n7.5. Oracle 中的事务 oracle 的事务对数据库的变更的处理，必须做提交事务才能让数据真正的插入到数据库中，在同样在执行完数据库变更的操作后还可以把事务进行回滚，这样就不会插入到数据库。如果事务提交后则不可以再回滚。\n提交：commit 回滚：rollback to 保存点 Oracle 中事务的保存点（savepoint）：回滚到某个保存点，这个保存点之前的数据插入，之后回滚。\n事务的隔离级别和隔离性：\nOracle 支持的 3 种事务隔离级别：READ COMMITED, SERIALIZABLE，READ ONLY。 Oracle 默认的事务隔离级别为: READ COMMITED。 Oracle事务示例:\n1 2 3 4 5 6 7 8 -- 事务保存点：savepoint -- savepoint sp1; insert into person values(3,\u0026#39;幽鬼\u0026#39;,2,null,\u0026#39;dota2\u0026#39;); -- 设置保存点,保存点之前提交，保存点之后的回滚 savepoint sp1; insert into person values(2,\u0026#39;插入不进去的\u0026#39;,3,null,\u0026#39;xxoo\u0026#39;); rollback to sp1; commit; 8. 管理其他数据库对象 8.1. 视图 视图，专门用于查询，里面不存储数据，数据都来源于真正的表。\n注：创建视图需要dba权限\n视图的好处：\n视图就是封装了一条复杂查询的语句。视图是一个虚表。最大的优点就是简化复杂的查询。 用视图可以屏蔽一些敏感数据。 8.1.1. 创建视图的语法1 1 2 3 4 5 create [or replace] [force|noforce] view视图名称 [(alias[, alias]...)] as 查询语句 [with check option [constraint constraint]] [with read only [constraint constraint]]; 创建视图示例:\n1 2 3 4 -- 创建视图（权限不够，可以使用system开通全部特权，是系统最高权限） create view empvd20 as select * from emp t where t.deptno=20; -- 视图创建完毕就可以使用视图来查询（这个名字的虚表），查询出来的都是 20 部门的员工 select * from empvd20; 8.1.2. 创建视图的语法2 1 create or replace view 视图名称 as 查询语句 使用语法2来创建视图，如果视图已经存在，这样已有的视图会被覆盖。\n不建议通过视图对表中的数据进行修改，因为会受到很多的限制。\n8.1.3. 创建视图的语法3 1 create or replace view 视图名称 as 查询语句 with read only 创建只读视图\n8.2. 序列【了解】 在很多数据库中都存在一个自动增长的列，如果现在要想在oracle中完成自动增长的功能，则只能依靠序列完成，所有的自动增长操作，需要用户手工完成处理。并且Oracle将序列值装入内存可以提高访问效率\n完整语法【了解】：\n1 2 3 4 5 6 7 create sequence sequence(序列名称) [increment by n]\t-- 步长值，每次增加的数值 [start with n]\t-- 初始值 [{maxvalue n | nomaxvalue}]\t-- 使用最大值，必须循环 [{minvalue n | nominvalue}] [{cache n | nocache}]\t-- 缓存n个值，默认值20 [{cycle | nocycle}];\t-- 循环，循环结束都从1开始 序列创建完成之后,所有的自动增长应该由用户自己处理,所以在序列中提供了以下的两种操作：\nnextval：取得序列的下一个内容 currval：取得序列的当前内容 创建序列: 在插入数据时需要自增的主键中可以这样使用\n序列可能产生裂缝的原因(中间跳过了数字)：\n回滚 系统异常 多个表共用一个序列 8.3. 索引 索引是用于加速数据存取的数据对象，提高检索数据效率。合理的使用索引可以大大降低 i/o 次数,从而提高数据访问性能。\n8.3.1. 单列索引 单列索引是基于单个列所建立的索引。语法：\n1 create index 索引名 on 表名(列名) 8.3.2. 复合索引 语法：\n1 create index 索引名 on 表名(列名1, 列名2) 复合索引是基于两个列或多个列的索引。在同一张表上可以有多个索引，但是要求列的组合必须不同，如：\n1 2 create index idx1 on 表名(a列,b列); create index idx1 on 表名(b列,a列); 示例：\n1 2 3 4 -- 范例：给 person 表的 name 建立索引 create index pname_index on person(name); -- 范例：给 person 表创建一个 name 和 gender 的索引 create index pname_gender_index on person(name,gender); 8.3.3. 索引使用原则 在数据量大表中使用索引 在经常使用字段上加索引 触发复合索引【联合】条件（name,address）\u0026ndash;必须有优先查询索引列 1 2 3 4 select * from person where name=\u0026#39;11\u0026#39;; 会触发索引 select * from person where name =\u0026#39;11\u0026#39; and address =\u0026#39;22\u0026#39;; 会触发 select * from person where name =\u0026#39;11\u0026#39; or address =\u0026#39;22\u0026#39;; 不会触发 select * from person where address =\u0026#39;22\u0026#39;; 不会触发 一般在不经常修改，添加，删除的表上添加索引。因为这些操作会引起索引重构 8.4. 同义词【了解】 同义词：给其他用户下的表起别名，在本用户下直接查询别名。属于跨库查询。\n定义同义词需要dba权限。语法：\n1 create [public] synonym 同义词名称 for 用户.表名; 示例：\n1 create public synonym emp for scott.emp; 同义词作用：\n可以很方便的访问其它用户的数据库对象 缩短了对象名字的长度 9. 数据的导入导出 9.1. 使用 cmd 命令整库导出与导入 在安装了 oracle 的电脑上执行。整库导出命令：\n1 exp system/密码 full=y 添加参数 full=y 表示整库导出。执行命令后会在当前目录下生成一个叫 EXPDAT.DMP，此文件为备份文件。\n如果想指定备份文件的名称，则添加 file 参数即可，命令如下\n1 exp system/密码 file=C:\\文件名.dmp full=y 整库导入命令：\n1 imp system/密码 full=y 此命令如果不指定 file 参数，则默认用备份文件 EXPDAT.DMP 进行导入\n如果指定 file 参数，则按照 file 指定的备份文件进行恢复\n1 imp system/密码 full=y file= C:\\文件名.dmp 执行导入命令前需确保 oracle 数据库中无即将导入的对象，否则将报以下提示：\n9.2. 使用 cmd 命令按用户导出与导入 按用户导出\n1 exp 用户名/密码 owner=用户名 file= c:\\文件名.dmp 按用户导入\n1 imp 用户名/密码 file= c:\\文件名.dmp fromuser=用户名1 touser=用户名2 9.3. 使用 cmd 命令按表导出与导入 按表导出\n1 exp 用户名/密码 file=文件名.dmp tables=导入的表名 用 tables 参数指定需要导出的表，如果有多个表用逗号分割即可\n按表导入\n1 imp 用户名/密码 file=文件名.dmp fromuser=用户名1 touser=用户名2 tables=导入的表名 9.4. 使用 PLSQL Developer 导出数据 Tools → Export User Objects\u0026hellip;选项，导出 .sql 文件。说明：此操作导出的是建表语句 Tools → Export Tables\u0026hellip;导出表结构及数据 PL/SQL 工具包含三种方式导出 Oracle 表结构及数据，三种方式分别为：Oracle Export 、SQL Inserts、PL/SQL Developer,下面分别简单介绍下区别：\n第一种方式导出.dmp 格式的文件，.dmp 是二进制文件，可跨平台，还能包含权限，效率不错，用的最为广泛。 第二种方式导出.sql 格式的文件，可用文本编辑器查看，通用性比较好，效率不如第一种，适合小数据量导入导出。尤其注意的是表中不能有大字段（blob,clob,long），如果有，会提示不能导出(提示如下： table contains one or more LONG columns cannot export in sql format,user Pl/sql developer format instead)。 第三种方式导出.pde 格式的文件，.pde 为 PL/SQL Developer 自有的文件格式，只能用 PL/SQL Developer 工具导入导出，不能用文本编辑器查看。 9.5. 使用 PLSQL Developer 导入数据 导入数据之前最好把以前的表删掉，当然导入另外的数据库数据除外\nTools → Import Tables\u0026hellip; 根据对应格式，在不同界面选择即将导入的文件。 10. PL/SQL 编程语言 10.1. PL/SQL 概述 PL/SQL（Procedure Language/SQL），PLSQL 是 Oracle 对 sql 语言的过程化扩展，指在 SQL 命令语言中增加了过程处理语句（如分支、循环等），使 SQL 语言具有过程处理能力。把 SQL 语言的数据操纵能力与过程语言的数据处理能力结合起来，使得 PLSQL 面向过程但比过程语言简单、高效、灵活和实用。\n10.2. PL/SQL 的语法 1 2 3 4 5 6 7 8 declare 说明部分 (变量说明，光标申明，异常（例外）说明) begin 语句序列 (DML语句)... pl/SQL程序体 exception 异常(例外)处理语句 end; 注：\n如果不需要声明变量，declare可以省略 变量不能在begin内定义，必须在declare里声明 10.3. 常量和变量的定义 变量的类型(char, varchar2, date, number, boolean, long)\n说明变量例子 说明 my_char char(15); 说明变量名，数据类型和长度后用分号结束说明语句 my_boolean boolean :=true; 定义boolean类型的变量，并赋值(:=) my_number number(7,2); 定义数字类型的变量 my_name emp.enmae%type; 引用型变量，即my_name的类型与emp表中ename列类型一样在sql中使用 into 来赋值 emp_rec emp%rowtype; 记录型变量，即为表格中的一条记录。类似一个对象在sql中使用 into 来赋值 注：在控制台输入的语法：在declare中定义变量时后写上【:=\u0026amp;xx;】，xx随便写，一般xx名字跟变量名一样\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 -- 引用变量 declare -- 声明引用变量，即emprec的类型与emp表中ename列类型一样 emprec emp.ename%type; begin -- 在sql中使用into赋值 select e.ename into emprec from emp e where e.empno=7369; -- 输出引用变量 dbms_output.put_line(emprec); end; -- 记录型变量 declare -- 声明记录型变量 p emp%rowtype; begin -- 对记录型变量赋值 select * into p from emp e where e.empno=7369; -- 输出记录型变量的属性值 dbms_output.put_line(p.ename || \u0026#39;===\u0026#39; || p.sal); end; 10.3.1. 定义RECORD类型 与%rowtype不一样，%rowtype与表的字段类型一致，record是直接定义一个类型。语法：\n1 2 3 4 5 6 7 type xxx is record( x1 in/out varchar, x2 in/out number, x3 表.字段名%type, x4 表.字段名%type, ... ); 10.4. if 分之语句 语法1：\n1 2 if 条件 then 语句1; end if; 语法2：\n1 2 3 if 条件 then 语句1; else 语句2; end if; 语法3：注意elsif中els没有e\n1 2 3 4 if 条件 then 语句1; elsif 条件 then 语句2; else 语句3; end if; 三种if语句示例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 -- 范例 1：如果从控制台输入 1 则输出我是 1 declare -- 声明数字类型变量，\u0026amp;num定义输入值，:=输入值后给pnum赋值 pnum number :=\u0026amp;num; begin if pnum = 1 then dbms_output.put_line(\u0026#39;我是1\u0026#39;); end if; end; -- 范例 2：如果从控制台输入 1 则输出我是 1 否则输出我不是 1 declare -- 声明数字类型变量 pnum number :=\u0026amp;num; begin if pnum = 1 then dbms_output.put_line(\u0026#39;我是1\u0026#39;); else dbms_output.put_line(\u0026#39;我不是1\u0026#39;); end if; end; -- 范例 3:判断人的不同年龄段 18 岁以下是未成年人，18 岁以上 40 以下是成年人，40 以上是老年人 declare -- 声明数字类型变量 page number :=\u0026amp;num; begin -- 使用多重if判断 if page\u0026lt;18 then dbms_output.put_line(\u0026#39;未成年人\u0026#39;); elsif page\u0026gt;=18 and page\u0026lt;40 then dbms_output.put_line(\u0026#39;成年人\u0026#39;); elsif page\u0026gt;=40 then dbms_output.put_line(\u0026#39;老年人\u0026#39;); end if; end; 10.5. loop 循环语句 while循环语法：类似java中的while循环\n1 2 3 4 while 循环条件 loop ...循环内容; end loop; loop循环语法：类似java中的do-while\n1 2 3 4 loop ...循环内容; exit [when 退出条件]; end loop; for循环语法：注意in后面是两个【.】。类似java中的for循环\n1 2 3 4 for 变量 in 1..n loop ...循环内容; end loop; 三种循环方式示例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 -- 范例:使用语法 1 输出 1 到 10 的数字 declare -- 声明数字类型变量，:=给变量赋值 i number :=1; begin while i\u0026lt;=10 loop dbms_output.put_line(i); i := i + 1; end loop; end; -- 范例:使用语法 2 输出 1 到 10 的数字 declare i number := 1; begin loop dbms_output.put_line(i); i := i + 1; exit when i \u0026gt; 10; end loop; end; -- 范例:使用语法 3 输出 1 到 10 的数字（declare可以省略） begin for i in 1..10 loop dbms_output.put_line(i); end loop; end; 10.6. 数组 明确一个概念：Oracle中本是没有数组的概念的，数组其实就是一张表(Table),每个数组元素就是表中的一个记录。\n使用数组时，用户可以使用 Oracle 已经定义好的数组类型，或可根据自己的需要定义数组类型。\n10.6.1. 使用 Oracle 自带的数组类型 使用时需要进行初始化，语法格式：变量名 array;\n1 2 3 4 5 6 create or replace procedure test(y out array) is x array; begin x := new array(); y := x; end; 10.6.2. 自定义的数组类型 自定义数据类型时，建议通过创建 Package 的方式实现，以便于管理\n10.7. 游标（光标 Cursor） 在pl/sql中会用到多条记录（类似java程序中的集合概念），使用游标可以存储查询返回的多条数据，结果集。\n游标的定义语法：\n1 cursor 游标名 [(参数名 数据类型,参数名 数据类型,...)] is select 语句; 示例：\n1 2 3 4 cursor c1 is select ename from emp; -- 带参数： cursor c(dno my_emp.deptno%type) is select e.empno from my_emp e where e.deptno = dno; 游标的使用语句：\n1 2 3 4 5 6 open 游标名; loop fetch 游标名 into 变量名; exit when 游标名%notfound; end loop; close 游标名; 游标的使用步骤（例子）:\n打开游标：open c1; (打开游标执行查询) 取一行游标的值：fetch c1 into 变量; (取一行到变量中，指针自动往下移，类型java程序中的迭代器iterator) 关闭游标：close c1;(关闭游标释放资源) 游标的结束方式：exit when c1%notfound 注意：\n游标结束的条件要放在取出游标值后面，否则最好一个值会输出两次 上面的 【变量】 必须与 emp 表中的指定的列类型一致： 定义：pjob emp.empjob%type; Code Dome:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 -- 范例 1：使用游标方式输出 emp 表中的员工编号和姓名 -- 方法1： declare -- 定义游标 cursor c is select empno,ename from emp; -- 声明引用变量，与emp表中的员工编号和姓名类型一致 pid emp.empno%type; pname emp.ename%type; begin -- 打开游标 open c; -- 使用循环将游标的值取出 loop -- 多个变量使用逗号分隔，顺序要与定义游标的语句一致 fetch c into pid,pname; -- 设置循环结束条件 exit when c%notfound; dbms_output.put_line(pid || \u0026#39; === \u0026#39; || pname); end loop; -- 关闭游标 close c; end; -- 教材方法2： declare -- 定义游标 cursor c is select * from emp; -- 声明记录型变量 pemp emp%rowtype; begin -- 打开游标 open c; -- 使用循环读取游标的记录 loop -- 从游标读取值 fetch c into pemp; -- 设置循环结束条件 exit when c%notfound; dbms_output.put_line(pemp.empno || \u0026#39; === \u0026#39; || pemp.ename); end loop; -- 关闭游标 close c; end; -- 范例 2：写一段 PL/SQL 程序，为部门号为 10 的员工涨工资。 -- 方式1：自己的做法 declare -- 定义游标，存储查询所有部门号为10的员工 cursor c is select e.empno from my_emp e where e.deptno=10; -- 声明引用型变量 pno my_emp.empno%type; begin -- 打开游标 open c; -- 使用循环读取游标中的值 loop -- 从游标读取值 fetch c into pno; -- 设置结束循环条件 exit when c%notfound; -- 执行sql语句 update my_emp e set e.sal=e.sal+100 where e.empno=pno; end loop; commit; -- 关闭游标 close c; end; -- 方式2：教材做法 declare -- 定义游标，存储查询所有部门号为10的员工 cursor c(dno my_emp.deptno%type) is select e.empno from my_emp e where e.deptno = dno; -- 声明引用型变量 pno my_emp.empno%type; begin -- 打开游标 open c(10); -- 使用循环读取游标中的值 loop -- 从游标读取值 fetch c into pno; -- 设置结束循环条件 exit when c%notfound; -- 执行sql语句 update my_emp e set e.sal=e.sal+2 where e.empno=pno; end loop; commit; -- 关闭游标 close c; end; 10.8. 异常【例外】 异常是程序设计语言提供的一种功能，用来增强程序的健壮性和容错性。异常语法：\n1 2 3 4 5 6 7 8 9 declare 声明语句; begin 执行语句（出现异常）; exception when 系统定义异常1 then 执行语句; when 系统定义异常2 then 执行语句; ... end; 系统定义异常\nno_data_found (没有找到数据) too_many_rows (select ...into 语句匹配多个行) zero_divide (被零除) value_error (算术或转换错误) timeout_on_resource (在等待资源时发生超时) 系统定义异常示例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -- 范例 1：写出被 0 除的异常的 plsql 程序 declare -- 声明变量 pnum number; begin -- 模拟异常 pnum := 1/0; exception when zero_divide then dbms_output.put_line(\u0026#39;被0除\u0026#39;); when value_error then dbms_output.put_line(\u0026#39;数值转换错误\u0026#39;); when others then dbms_output.put_line(\u0026#39;其他错误\u0026#39;); end; 用户也可以自定义异常，在声明中来定义异常：\n1 2 3 4 5 6 7 8 9 10 11 declare -- 定义异常 no_data_found exception; begin 执行语句; --如果遇到异常我们要抛出 if c%notfound then raise no_data_found; exception when no_data_found then 执行语句; when others then 执行语句; end; 自定义异常:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 -- 范例 2：查询部门编号是 50 的员工 declare -- 定义异常 no_data_found exception; cursor c is select e.ename from emp e where e.deptno=50; pename emp.ename%type; begin -- 开启游标 open c; -- 获取值 fetch c into pename; if c%notfound then raise no_data_found; end if; close c; exception when no_data_found then dbms_output.put_line(\u0026#39;没有找到该员工\u0026#39;); when others then dbms_output.put_line(\u0026#39;其他错误\u0026#39;); end; 11. 存储过程 11.1. 存储过程概述 存储过程（Stored Procedure）是在大型数据库系统中，一组为了完成特定功能的 SQL 语句集，经编译后存储在数据库中，用户通过指定存储过程的名字并给出参数（如果该存储过程带有参数）来执行它。存储过程是数据库中的一个重要对象，任何一个设计良好的数据库应用程序都应该用到存储过程\n存储过程：封装了一系列了sql语句，事先编译好，存储在数据库端，供其他程序员调用\n好处：效率高\n创建好的存储过程会存在当前用户的Procedures中 创建好的存储函数会存在当前用户的Functions中 11.2. 创建存储过程语法 1 2 3 4 5 6 create [or replace] procedure 过程名[(参数名1 in/out 数据类型1, 参数名2 in/out 数据类型2, ......)] as/is 在存储过程中定义的变量 begin PLSQL 子程序体，封装多条sql语句； end; 注：如果参数是输入类型in，可以省略不写\n11.3. 调用存储过程 方式1：不推荐，不能接收存储过程out的参数\n1 call 过程名(参数1, 参数2, ......); 方式2：可以接收存储过程out的参数，注意传递的参数顺序必须按定义存储过程参数的顺序【推荐】\n1 2 3 4 5 declare 声明变量，如果没有可以省略不写 begin 过程名(参数1, 参数2, ......); end; 方式3：调用存储过程时，传递参数的方式写法不一样，此方式的好处是指定值对应变量顺序可变【推荐】\n1 2 3 4 5 6 7 declare 声明变量，如果没有可以省略不写 begin 过程名(定义的输入/输出参数名1=\u0026gt;值1, 定义的输入/输出参数名2=\u0026gt;值2, 定义的输入/输出参数名3=\u0026gt;值3, ......); end; 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 -- 给某个员工涨工资，打印出涨前涨后的工资 create or replace procedure pro_emp_sal (eno number, money number) as -- 在存储过程定义变量 -- 声明数字类型变量，存储涨前和涨后的工资 psal number; begin -- 封装的sql语句 -- 查询涨前工资 select sal into psal from my_emp where empno=eno; dbms_output.put_line(\u0026#39;涨前工资\u0026#39;||psal); -- 修改工资 update my_emp set sal=sal+money where empno=eno; commit; -- 查询涨后工资 select sal into psal from my_emp where empno=eno; dbms_output.put_line(\u0026#39;涨后工资\u0026#39;||psal); end; -- 调用存储过程 -- 方式1：不推荐，不能接收存储过程out的参数 call pro_emp_sal(7788, 100); -- 方式2：可以接收存储过程out的参数【推荐】 declare -- 声明变量，如果没有可以省略不写 begin pro_emp_sal(7788, 2); end; 11.4. 创建包与体的存储过程 11.4.1. 定义 包用于组合逻辑相关的过程和函数，它由包规范和包体两个部分组成。包规范用于定义公用的常量、变量、过程和函数，创建包规范可以使用CREATE PACKAGE命令，创建包体可以使用CREATE PACKAGE BODY\n11.4.2. 语法 创建包规范\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 create or replace package PCKG_包名 is 定义一些全局的常量，或者是游标类型，如： xxx_xxx_xxx constant number := 100; xxx_xxx_xxx constant varchar2(30) := \u0026#39;\u0026#39; Type 游标名 is ref cursor; procedure PROC_存储过程名 (参数名1 in/out 数据类型1, 参数名2 in/out 数据类型2, ......); function FUNC_存储函数名 (参数1 in|out 数据类型1, 参数2 in|out 数据类型2, ...) return 数据类型; end PCKG_包名; / create or replace package body PCKG_包名 is procedure PROC_存储过程名 (参数名1 in/out 数据类型1, 参数名2 in/out 数据类型2, ......) is/as v_变量名 数据类型; begin 业务逻辑; end; function FUNC_存储函数名 (参数1 in|out 数据类型1, 参数2 in|out 数据类型2, ...) return 数据类型 is/as v_变量名 数据类型; begin 业务逻辑; end; end PCKG_包名; / 11.5. 关于oracle存储过程的若干问题备忘 在oracle的存储过程中，数据表别名不能加as。也许，是怕和oracle中的存储过程中的关键字as冲突的问题吧 1 2 selecta.appname from appinfo a; -- 正确 selecta.appname from appinfo as a; -- 错误 在存储过程中，select 某一字段时，后面必须紧跟 into，如果 select 整个记录，利用游标的话就另当别论了 在利用 select...into... 语法时，必须先确保数据库中有该条记录，否则会报出\u0026quot;no datafound\u0026quot;异常。 可以在该语法之前，先利用 select count(*) from 查看数据库中是否存在该记录，如果存在，再利用 select...into... 在存储过程中，别名不能和字段名称相同，否则虽然编译可以通过，但在运行阶段会报错 11.6. Oracle中的Packages与Packagebodies 11.6.1. package的作用 可以简化应用设计、提高应用性能、实现信息隐藏、子程序重载\n11.6.2. packages 与 packagebodies比较 定义:packae是一种将过程、函数和数据结构捆绑在一起的容器；\n由两个部分组成：外部可视包规范，包括函数头，过程头，和外部可视数据结构；\n另一部分是包主体(package body),包主体包含了所有被捆绑的过程和函数的声明、执行、异常处理部分。\n简单说就是packages 中只有各个方法的定义，bodies中涉及具体的实现.\n所以 packages 和 packagebodies 是一体的，必须同时存在。如果要外部调用的，就在package里声明一下，包内调用的，只要在body里写就行了。\npackage可包括function，procedure。\n需要先创建package(也就是包的定义)，再创建body。\n增加包中的过程或者修改包中过程的输入参数个数等也是要先改package再改body\n12. 存储函数 12.1. 存储函数概述 与存储过程类似，封装一些sql语句，事先编译好，存在数据库端，供其他程序员调用。\n12.2. 存储函数语法 1 2 3 4 5 6 7 8 create [or replace] function 函数名(参数1 in|out 数据类型1, 参数2 in|out 数据类型2, ...) return 数据类型 as|is -- 定义变量 结果变量 数据类型; begin -- 变量必须跟函数返回类型一致 return 结果变量; end[函数名]; 注：如果参数是输入类型in，可以省略不写。黄色部分需要注意，与存储过程不一样，函数必须有一个返回值\n过程：\n1 public void methodName(params....){} 函数：\n1 public String methodName(params....){return \u0026#34;\u0026#34;;} 12.3. 调用存储函数 1 2 3 4 5 6 declare -- 声明变量，如果没有可以省略不写 接收返回值变量 变量类型（与返回值类型一致）; begin 返回值变量 := 函数名(参数1, 参数2, ......); end; 存储函数与存储过程使用out参数返回值:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 -- 使用存储函数查询某个员工的年薪 create or replace function fun_total_sal(pno number) return number as -- 定义一个number类型变量接收查询结果 totalSal number; begin -- 使用判空函数判断是否有奖金 select sal*12+nvl(comm, 0) into totalSal from my_emp where empno=pno; -- 返回查询结果 return totalSal; end; -- 调用函数 declare -- 定义数字类型变量接收返回值 t number; begin -- 调用存储函数，获取函数中返回值 t := fun_total_sal(7788); dbms_output.put_line(t); end; -- 使用存储过程来完成：查询某个员工的年薪 -- 可以利用 out 参数，在过程和函数中实现返回多个值 create or replace procedure pro_total_sal(pno number, totalSal out number) as -- 无声明的变量 begin -- 将查询到结果赋值给out参数 select sal*12+nvl(comm, 0) into totalSal from my_emp where empno=pno; end; -- 调用存储过程 declare -- 定义一个变量接收年薪 t number; begin -- 调用过程，将定义的变量作为参数传递到out参数位置 pro_total_sal(7788,t); -- 打印输出out参数 dbms_output.put_line(\u0026#39;员工的年薪是：\u0026#39; || t); end; 12.4. 存储过程和存储函数的区别 过程和函数最大的区别是： 函数（function）总是向调用者返回数据，并且一般只返回一个值； 存储过程（procedure）不直接返回数据，但可以改变输出参数的值，这可以近似看作能返回值，且存储过程输出参数的值个数没有限制。 但过程和函数都可以通过 out 指定一个或多个输出参数。我们可以利用 out 参数，在过程和函数中实现返回多个值 存储过程一般是作为一个独立的部分来执行（CALL语句执行），而函数可以作为查询语句的一个部分来调用（SELECT调用）。SQL语句中不可用存储过程，而可以使用函数 以下需要注意的地方是：\n定义函数或者存储过程时，IN/OUT表示调用函数时，传进来或传出去的参数。如果没有说明in/out，则默认为in； 定义的函数必须要有return子句，其后紧跟着返回值得类型； 实际调用函数或存储过程时，在declare中声明的变量至少应该对应创建的函数或存储过程中的OUT参数和return参数合起来的个数； 可以建立不带参数（即没有返回的参数）、没有变量的存储过程。 执行方式略有不同，存储过程的执行方式有两种（1.使用call；2.使用begin和end），函数除了存储过程的两种方式外，还可以当做表达式使用，例如放在select中（select f1() form dual;）。 过程与函数区别：1、语法不同，函数必须有返回值；2、函数可以用在select部分:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 -- 编写函数：根据部门编号查询部门名称 create or replace function findDNameByDNo(dno number) return varchar2 is -- 定义返回值 dNameStr dept.dname%type; begin -- 接收查询返回值 select dname into dNameStr from dept where deptno=dno; -- 返回查询结果 return dNameStr; end; -- 调用存储函数 declare -- 声明返回值 pname dept.dname%type; begin -- 获取查询返回值，并输出结果 pname := findDNameByDNo(20); dbms_output.put_line(pname); end; -- 在select语句中使用函数 select ename,deptno,findDNameByDNo(deptno) from emp; 13. Java程序调用存储过程 13.1. java 连接 oracle 的 jar 包 在oracle的安装目录:\\oracle\\product\\10.2.0\\db_1\\jdbc\\lib\n找到 jar 包：ojdbc14.jar\n13.2. 数据库连接字符串 数据库的连接要素可以在 hibernate-release-5.0.12.Final\\project\\etc\\hibernate.propertie 找到\norcl是安装oracle时填写的全局数据库名！！！！！\n1 2 3 4 String driver=\u0026#34;oracle.jdbc.OracleDriver\u0026#34;; String url=\u0026#34;jdbc:oracle:thin:@192.168.17.10:1521:orcl\u0026#34;; String username=\u0026#34;scott\u0026#34;; String password=\u0026#34;123456\u0026#34;; 13.3. Connection 接口获取 CallableStatement 1 CallableStatement prepareCall(String sql) 创建一个 CallableStatement 对象来调用数据库存储过程。CallableStatement 对象提供了设置其 IN 和 OUT 参数的方法，以及用来执行调用存储过程的方法。\nprepareCall(String sql)方法中的sql参数：\n1 {?= call \u0026lt;procedure-name\u0026gt;[(\u0026lt;arg1\u0026gt;,\u0026lt;arg2\u0026gt;, ...)]} 用于调用存储函数，第1个“?”是存储函数的返回值 procedure-name：存储函数的名字 arg1,arg2...：函数的形式参数 1 {call \u0026lt;procedure-name\u0026gt;[(\u0026lt;arg1\u0026gt;,\u0026lt;arg2\u0026gt;, ...)]} 用于调用存储过程 procedure-name：存储过程的名字 arg1,arg2...：过程的形式参数 13.4. CallableStatement 接口 用于执行 SQL 存储过程的接口。JDBC API 提供了一个存储过程 SQL 转义语法，该语法允许对所有 RDBMS 使用标准方式调用存储过程。\n此转义语法有一个包含结果参数的形式和一个不包含结果参数的形式。如果使用结果参数，则必须将其注册为 OUT 参数。其他参数可用于输入、输出或同时用于二者。\n参数是根据编号按顺序引用的，第一个参数的编号是 1。\nCallableStatement 常用方法\n1 void setString(String parameterName, String x) 设置String类型的参数。\nparameterIndex：sql语句?参数的索引 1 void setInt(int parameterIndex, int x) 设置int类型的参数。\nparameterIndex：sql语句?参数的索引 1 boolean execute() 执行 SQL 语句，该语句可以是任何种类的 SQL 语句。\n1 void registerOutParameter(int parameterIndex, int sqlType) 按顺序位置 parameterIndex 将 OUT 参数注册为 JDBC 类型 sqlType。所有 OUT 参数都必须在执行存储过程前注册。\n参数sqlType：使用oracle的驱动中类OracleTypes的常量 1 Object getObject(int parameterIndex) 获取指定参数存储过程返回的值。如果值为 SQL NULL，则驱动程序返回一个 Java null。\n此方法返回一个 Java 对象，其类型对应于使用 registerOutParameter 方法为此参数注册的 JDBC 类型。\n13.5. JDBC调用Oracle对象-表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 /** * jdbc:调用oracle数据库，查询emp表的数据打印到控制台 */ public class JdbcOracleDemo01 { // 定义数据库连接4要素 private String driver = \u0026#34;oracle.jdbc.driver.OracleDriver\u0026#34;; private String url = \u0026#34;jdbc:oracle:thin:@192.168.187.10:1521:orcl\u0026#34;; private String username = \u0026#34;scott\u0026#34;; private String password = \u0026#34;123456\u0026#34;; @Test public void testOracle() throws Exception { // 加载oracle驱动 Class.forName(driver); // 设置数据库连接其他要素，获取连接对象 Connection conn = DriverManager.getConnection(url, username, password); // 获取预编译对象 PreparedStatement pstm = conn.prepareStatement(\u0026#34;select * from emp\u0026#34;); // 执行 ResultSet rs = pstm.executeQuery(); // 输出查询结果 while (rs.next()) { System.out.println(\u0026#34;员工姓名：\u0026#34; + rs.getString(\u0026#34;ename\u0026#34;) + \u0026#34; ,员工工作：\u0026#34; + rs.getString(\u0026#34;job\u0026#34;)); } // 关闭资源 if (pstm != null) { pstm.close(); } if (conn != null) { conn.close(); } } } 13.6. JDBC调用Oracle对象-过程：无返回 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 /** * jdbc:调用oracle数据库 的存储过程 ： 没有返回值 */ public class JdbcOracleDemo2 { // 定义数据库连接4要素 private String driver = \u0026#34;oracle.jdbc.driver.OracleDriver\u0026#34;; private String url = \u0026#34;jdbc:oracle:thin:@192.168.187.10:1521:orcl\u0026#34;; private String username = \u0026#34;scott\u0026#34;; private String password = \u0026#34;123456\u0026#34;; @Test public void testOracle() throws Exception { // 加载oracle驱动 Class.forName(driver); // 设置数据库连接其他要素，获取连接对象 Connection conn = DriverManager.getConnection(url, username, password); // 存储过程:create or replace procedure pro_emp_sal (eno number, money number) // 获取CallableStatement对象，用于调用执行存储过程(没有返回) CallableStatement callSt = conn.prepareCall(\u0026#34;{call pro_emp_sal(?,?)}\u0026#34;); // 设置参数，第1个参数是?的索引，第2个参数是设置的值 callSt.setInt(1, 7788); callSt.setInt(2, 100); // 执行 callSt.execute(); // 关闭资源 if (callSt != null) { callSt.close(); } if (conn != null) { conn.close(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ******对应的存储过程***** create or replace procedure pro_emp_sal (eno number, money number) as -- 在存储过程定义变量 -- 声明数字类型变量，存储涨前和涨后的工资 psal number; begin -- 封装的sql语句 -- 查询涨前工资 select sal into psal from my_emp where empno=eno; dbms_output.put_line(\u0026#39;涨前工资\u0026#39;||psal); -- 修改工资 update my_emp set sal=sal+money where empno=eno; commit; -- 查询涨后工资 select sal into psal from my_emp where empno=eno; dbms_output.put_line(\u0026#39;涨后工资\u0026#39;||psal); end; 13.7. JDBC调用Oracle对象-过程：有返回 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 /** * jdbc:调用oracle数据库 的存储过程 ： 有返回值 */ public class JdbcOracleDemo3 { // 定义数据库连接4要素 private String driver = \u0026#34;oracle.jdbc.driver.OracleDriver\u0026#34;; private String url = \u0026#34;jdbc:oracle:thin:@192.168.187.10:1521:orcl\u0026#34;; private String username = \u0026#34;scott\u0026#34;; private String password = \u0026#34;123456\u0026#34;; @Test public void testOracle() throws Exception { // 加载oracle驱动 Class.forName(driver); // 设置数据库连接其他要素，获取连接对象 Connection conn = DriverManager.getConnection(url, username, password); // 存储过程:create or replace procedure pro_total_sal(pno number, totalSal out number) // 获取CallableStatement对象，用于调用执行存储过程(有返回参数) CallableStatement callSt = conn.prepareCall(\u0026#34;{call pro_total_sal(?,?)}\u0026#34;); // 设置参数，第1个参数是?的索引，第2个参数是设置的值 callSt.setInt(1, 7788); // 第2个?是返回值，如果使用结果参数，则必须将其注册为OUT参数 callSt.registerOutParameter(2, OracleTypes.NUMBER); // 执行 callSt.execute(); // 获取返回值，方法参数为?符号的位置 Object obj = callSt.getObject(2); System.out.println(obj); // 关闭资源 if (callSt != null) { callSt.close(); } if (conn != null) { conn.close(); } } } 1 2 3 4 5 6 7 8 ******对应的存储过程***** create or replace procedure pro_total_sal(pno number, totalSal out number) as -- 无声明的变量 begin -- 将查询到结果赋值给out参数 select sal*12+nvl(comm, 0) into totalSal from my_emp where empno=pno; end; 13.8. JDBC调用Oracle对象-函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 /** * jdbc:调用oracle数据库 的存储函数 ：肯定有返回值 */ public class JdbcOracleDemo4 { // 定义数据库连接4要素 private String driver = \u0026#34;oracle.jdbc.driver.OracleDriver\u0026#34;; private String url = \u0026#34;jdbc:oracle:thin:@192.168.187.10:1521:orcl\u0026#34;; private String username = \u0026#34;scott\u0026#34;; private String password = \u0026#34;123456\u0026#34;; @Test public void testOracle() throws Exception { // 加载oracle驱动 Class.forName(driver); // 设置数据库连接其他要素，获取连接对象 Connection conn = DriverManager.getConnection(url, username, password); // 存储函数:create or replace function fun_total_sal(pno number) // 获取CallableStatement对象，用于调用执行存储函数(肯定有返回值) CallableStatement callSt = conn.prepareCall(\u0026#34;{?= call fun_total_sal(?)}\u0026#34;); // 设置参数，第1个参数是?的索引，第2个参数是设置的值 // 注意参数的位置是2 callSt.setInt(2, 7788); // 第1个?是返回值，如果使用结果参数，则必须将其注册为OUT参数 callSt.registerOutParameter(1, OracleTypes.NUMBER); // 执行 callSt.execute(); // 获取返回值，参数是返回值的位置索引 Object obj = callSt.getObject(1); System.out.println(obj); // 关闭资源 if (callSt != null) { callSt.close(); } if (conn != null) { conn.close(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 ******对应的存储函数***** -- 使用存储函数查询某个员工的年薪 create or replace function fun_total_sal(pno number) return number as -- 定义一个number类型变量接收查询结果 totalSal number; begin -- 使用判空函数判断是否有奖金 select sal*12+nvl(comm, 0) into totalSal from my_emp where empno=pno; -- 返回查询结果 return totalSal; end; 13.9. JDBC调用存储过程返回：多个结果 sys_refcursor\n优点一：sys_refcursor，可以在存储过程中作为参数返回一个table格式的结构集（把它认为是table类型，容易理解，其实是一个游标集）， cursor 只能用在存储过程，函数，包等的实现体中，不能做参数使用。 优点二：sys_refcursor可以使用在包中做参数，进行数据库面向对象开放。 获取sys_refcursor游标集的返回结果\n将CallableStatement强制成子类OracleCallableStatement 使用java.sql.ResultSet getCursor(int arg0) 方法获取游标集的结果 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 /** * jdbc:调用oracle数据库 的存储过程 ：有多个值的情况 */ public class JdbcOracleDemo5 { // 定义数据库连接4要素 private String driver = \u0026#34;oracle.jdbc.driver.OracleDriver\u0026#34;; private String url = \u0026#34;jdbc:oracle:thin:@192.168.187.10:1521:orcl\u0026#34;; private String username = \u0026#34;scott\u0026#34;; private String password = \u0026#34;123456\u0026#34;; @Test public void testOracle() throws Exception { // 加载oracle驱动 Class.forName(driver); // 设置数据库连接其他要素，获取连接对象 Connection conn = DriverManager.getConnection(url, username, password); // 存储过程:create or replace procedure get_dept_emp_list(dno in number, emplist out sys_refcursor) // 获取CallableStatement对象，用于调用执行存储过程(多个返回值) CallableStatement callSt = conn.prepareCall(\u0026#34;{call get_dept_emp_list(?,?)}\u0026#34;); // 设置参数，第1个参数是?的索引，第2个参数是设置的值 callSt.setInt(1, 20); // 第2个?是返回值，如果使用结果参数，则必须将其注册为OUT参数,类型是cursor callSt.registerOutParameter(2, OracleTypes.CURSOR); // 执行 callSt.execute(); // 获取返回值,强制成子类 OracleCallableStatement ocs = (OracleCallableStatement) callSt; // 使用子类的返回结果集的方法,getCursor的参数是?占位符的索引 ResultSet rs = ocs.getCursor(2); // 循环读取结果集 while (rs.next()) { System.out.println(\u0026#34;姓名：\u0026#34; + rs.getString(\u0026#34;ename\u0026#34;) + \u0026#34; == 工作：\u0026#34; + rs.getString(\u0026#34;job\u0026#34;)); } // 关闭资源 if (callSt != null) { callSt.close(); } if (conn != null) { conn.close(); } } } 1 2 3 4 5 6 7 8 9 ******对应的存储过程***** -- 查询某个部门的员工信息 create or replace procedure get_dept_emp_list(dno in number, emplist out sys_refcursor) as begin -- sys_refcursor相当于一个游标集 open emplist for select * from emp where deptno = dno; end; 14. 触发器【了解】 触发器，监视器，监视表中记录，当对表中记录进行操作(增删改)触发器工作\n数据库触发器是一个与表相关联的、存储的 PL/SQL 程序。每当一个特定的数据操作语句(Insert,update,delete)在指定的表上发出时，Oracle 自动地执行触发器中定义的语句序列\n14.1. 触发器作用 数据确认 示例：员工涨后的工资不能少于涨前的工资 实施复杂的安全性检查 示例：禁止在非工作时间插入新员工 做审计，跟踪表上所做的数据操作等 数据的备份和同步 14.2. 触发器的类型 语句级触发器：在指定的操作语句操作之前或之后执行一次，不管这条语句影响了多少行 行级触发器（FOR EACH ROW）：触发语句作用的每一条记录都被触发。在行级触发器中使用old和new伪记录变量,识别值的状态 14.3. 触发器的语法 1 2 3 4 5 6 7 8 9 10 CREATE [or REPLACE] trigger 触发器名 {BEFORE | AFTER} {DELETE | INSERT | UPDATE [OF 列名]} ON 表名 [FOR EACH ROW [WHEN(条件) ] ] declare ...... begin PLSQL 块 end [触发器名]; 在触发器中触发语句与伪记录变量的值\n触发语句 :old :new insert 所有字段都是空(null) 将要插入的数据 update 更新以前该行的值 更新后的值 delete 删除以前该行的值 所有字段都是空(null) 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 -- =============================== 触发器 =============================== /* 触发器：监视器，监视表中记录，当对表中记录进行操作（增删改）触发器工作 --行级触发器：for each row 使用：:old, :new 语法： create or replace trigger 触发器名称 after|before insert|update|delete on 表名 declare begin end; */ -- 当插入一条数据之后，输出一句“插入成功了....” create or replace trigger emp_insert_log after insert on my_emp declare begin dbms_output.put_line(\u0026#39;插入成功了....\u0026#39;); end; -- 测试 insert into my_emp (empno,ename,deptno) values (9999,\u0026#39;johnny\u0026#39;,\u0026#39;10\u0026#39;); -- =============================== 触发器应用 =============================== /* 实现跟MySQL数据中主键自增效果 insert into emp(ename) values(’zhangsna‘); oracle: insert into emp(empno,ename) values(sequence.nextval,’zhangsna‘); */ -- 配合序列使用 create sequence s_my_emp_id; -- 创建触发器 create or replace trigger emp_insertRow_before before insert on my_emp for each row declare begin -- 给将要添加记录，从某个序列中查询到一个值----设置主键值 select s_my_emp_id.nextval into :new.empno from dual; end; -- 测试 insert into my_emp (ename) values (\u0026#39;jack\u0026#39;); 15. Oracle 其他知识 15.1. 系统表user_tables/all_tables/dba_tables user_tables、all_tables、dba_tables关系： user_tables：可查询当前用户的表； all_tables：可查询所有的(当前用户有权限)表；（只要对某个表有任何权限，即可在此视图中看到表的相关信息） dba_tables：可查询包括系统表在内的所有表。需要DBA权限才能查询 以上3个视图中，user_tables的范围最小，all_tables看到的东西稍多一些，而dba_tables看到最多的信息 15.2. 数据库锁表 for update：会对所查询到得结果集进行加锁，不允许其他程序修改 for update nowait：也会对所查询到得结果集进行加锁，别的线程对结果集进行操作时会报错ORA-00054，提示：内容是资源正忙，但指定以 NOWAIT 方式获取资源。 select t.*,t.rowid from 表名：用ROWID来定位记录是最快的，比索引还快，不加锁查询。不会锁表\n","permalink":"https://ktzxy.top/posts/8slmdac1h8/","summary":"Oracle 基础","title":"Oracle 基础"},{"content":"Go中的包 Go中的包的介绍和定义 包（package）是多个Go源码的集合，是一种高级的代码复用方案，Go语言为我们提供了很多内置包，如fmt、strconv、strings、sort、errors、time、encoding/json、os、io等。\nGolang中的包可以分为三种：1、系统内置包 2、自定义包 3、第三方包\n系统内置包：Golang 语言给我们提供的内置包，引入后可以直接使用，如fmt、strconv、strings、sort、errors、time、encoding/json、os、io等。 自定义包：开发者自己写的包 第三方包：属于自定义包的一种，需要下载安装到本地后才可以使用，如前面给大家介绍的 \u0026ldquo;github.com/shopspring/decimal\u0026quot;包解决float精度丢失问题。 Go包管理工具 go mod 在Golang1.11版本之前如果我们要自定义包的话必须把项目放在GOPATH目录。Go1.11版本之后无需手动配置环境变量，使用go mod 管理项目，也不需要非得把项目放到GOPATH指定目录下，你可以在你磁盘的任何位置新建一个项目，Go1.13以后可以彻底不要GOPATH了。\ngo mod init初始化项目 实际项目开发中我们首先要在我们项目目录中用go mod命令生成一个go.mod文件管理我们项目的依赖。\n比如我们的golang项目文件要放在了itying这个文件夹，这个时候我们需要在itying文件夹里面使用go mod命令生成一个go.mod文件\n1 go mod init goProject 然后会生成一个 go.mod 的文件，里面的内容是go版本，以及以后添加的包\n1 2 3 module goProject go 1.14 引入其它项目的包 首先我们创建一个 calc，然后里面有一个calc的文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package calc // 自定义包，最好和文件夹统一起来 // 公有变量 var age = 10 // 私有变量 var Name = \u0026#34;张三\u0026#34; // 首字母大写，表示共有方法 func Add(x, y int)int { return x + y } func Sub(x, y int)int { return x - y } 在其它地方需要引用的话，就是这样\n1 2 3 4 5 6 7 8 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;goProject/calc\u0026#34; ) func main() { fmt.Printf(\u0026#34;%v\u0026#34;, calc.Add(2, 5)) } Golang中自定义包 包（package）是多个Go源码的集合，一个包可以简单理解为一个存放多个.go文件的文件夹。该文件夹下面的所有go文件都要在代码的第一行添加如下代码，声明该文件归属的包。\n1 package 包名 注意事项\n一个文件夹下面直接包含的文件只能归属一个package，同样一个package的文件不能在多个文件夹下。 包名可以不和文件夹的名字一样，包名不能包含-符号。 包名为main的包为应用程序的入口包，这种包编译后会得到一个可执行文件，而编译不包含main包的源代码则不会得到可执行文件。 Go中init()初始化函数 init函数介绍 在Go 语言程序执行时导入包语句会自动触发包内部init（）函数的调用。需要注意的是：init（） 函数没有参数也没有返回值。init（）函数在程序运行时自动被调用执行，不能在代码中主动调用它。 包初始化执行的顺序如下图所示：\n包初始化执行的顺序如下图所示：\ninit函数执行顺序 Go语言包会从main包开始检查其导入的所有包，每个包中又可能导入了其他的包。Go编译器由此构建出一个树状的包引用关系，再根据引用顺序决定编译顺序，依次编译这些包的代码。\n在运行时，被最后导入的包会最先初始化并调用其init（）函数，如下图示：\n也就是父类中的init先执行\nGo中的第三方包 我们可以在 https://pkg.go.dev/ 查找看常见的golang第三方包\n例如，前面找到前面我们需要下载的第三方包的地址\n1 https://github.com/shopspring/decimal 然后安装这个包\n方法1：go get 包全名 （全局） 1 go get github.com/shopspring/decimal 方法2：go mod download （全局） 1 go mod download 依赖包会自动下载到 $GOPATH/pkg/mod目录，并且多个项目可以共享缓存的mod，注意使用go mod download的时候，需要首先在你的项目中引入第三方包\n方法3：go mod vendor 将依赖复制到当前项目的vendor（本项目） 1 go mod vendor 将依赖复制到当前项目的vendor下\n注意：使用go mod vendor的时候，首先需要在你的项目里面引入第三方包\ngo mod常见命令 go download：下载依赖的module到本地cache go edit：编辑go.mod文件 go graph：打印模块依赖图 go init：在当前文件夹下初始化一个新的module，创建go.mod文件 tidy：增加丢失的module，去掉未使用的module vendor：将依赖复制到vendor下 verify：校验依赖，检查下载的第三方库有没有本地修改，如果有修改，则会返回非0，否则校验成功 安装依赖 首先我们先去官网找到这个包，https://github.com/shopspring/decimal\n然后在我们的项目中引入\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/shopspring/decimal\u0026#34; \u0026#34;goProject/calc\u0026#34; ) func main() { fmt.Printf(\u0026#34;%v \\n\u0026#34;, calc.Add(2, 5)) // 打印公有变量 fmt.Println(calc.Name) _, err := decimal.NewFromString(\u0026#34;136.02\u0026#34;) if err != nil { panic(err) } } 引入后，我们运行项目，就会去下载了，下载完成后，我们到 go.mod文件夹，能够看到依赖被引入了\n1 2 3 4 5 module goProject go 1.14 require github.com/shopspring/decimal v1.2.0 // indirect 同时还生成了一个 go.sum文件\n1 2 github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 这样我们就可以使用第三包开始具体的使用了~，我们实现一个Float类型的加法\n1 2 3 4 5 6 7 8 9 10 11 12 13 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;github.com/shopspring/decimal\u0026#34; ) func main() { var num1 float64 = 3.1 var num2 float64 = 4.2 d1 := decimal.NewFromFloat(num1).Add(decimal.NewFromFloat(num2)) fmt.Println(d1) } 完整案例 寻找依赖 首先我们需要去 依赖官网，类似于我们的 maven repository\n然后我们搜索gJson的包，这个包主要是用于json相关的操作\n我们进去后，找到它的https://github.com/tidwall/gjson，然后提供了完整的教程\n1 2 # 下载依赖 go get -u github.com/tidwall/gjson 使用\n1 2 3 4 5 6 7 8 9 10 package main import \u0026#34;github.com/tidwall/gjson\u0026#34; const json = `{\u0026#34;name\u0026#34;:{\u0026#34;first\u0026#34;:\u0026#34;Janet\u0026#34;,\u0026#34;last\u0026#34;:\u0026#34;Prichard\u0026#34;},\u0026#34;age\u0026#34;:47}` func main() { value := gjson.Get(json, \u0026#34;name.last\u0026#34;) println(value.String()) } ","permalink":"https://ktzxy.top/posts/urxke0jrkl/","summary":"13 Go中的包以及GoMod","title":"13 Go中的包以及GoMod"},{"content":" promethus监控Redis Prometheus 监控 Redis 集群的正确姿势 一、概述 Prometheus exporter for Redis metrics.\ngithub地址：\nhttps://github.com/oliver006/redis_exporter\n二、安装redis_exporter 下载最新版本：\nhttps://github.com/oliver006/redis_exporter/releases/download/v1.3.5/redis_exporter-v1.3.5.linux-amd64.tar.gz\n登录到redis服务器，解压安装\n1 2 tar zxvf redis_exporter-v1.3.5.linux-amd64.tar.gz -C /data mv /data/redis_exporter-v1.3.5.linux-amd64 /data/redis_exporter redis_exporter 用法\n解压后只有一个二进制程序就叫 redis_exporter 通过 -h 可以获取到帮助信息，下面列出一些常用的选项：\n1 2 3 4 -redis.addr：指明一个或多个 Redis 节点的地址，多个节点使用逗号分隔，默认为 redis://localhost:6379 -redis.password：验证 Redis 时使用的密码； -redis.file：包含一个或多个redis 节点的文件路径，每行一个节点，此选项与 -redis.addr 互斥。 -web.listen-address：监听的地址和端口，默认为 0.0.0.0:9121 运行 redis_exporter 服务\n1 2 3 4 ## 无密码 nohup ./redis_exporter redis//192.168.111.11:6379 \u0026amp; ## 有密码 nohup ./redis_exporter -redis.addr 192.168.111.11:6379 -redis.password 123456 \u0026amp; 三、配置 prometheus.yml 单机版 添加监控目标\n1 vim /data/prometheus/prometheus.yml 最后一行添加\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 - job_name: \u0026#39;redis_exporter\u0026#39; static_configs: - targets: [\u0026#39;192.168.10.147:9121\u0026#39;] labels: instance: 生产实例1 - targets: [\u0026#39;192.168.10.148:9121\u0026#39;] labels: instance: 生产实例2 - targets: [\u0026#39;192.168.10.149:9121\u0026#39;] labels: instance: 生产实例3 - targets: [\u0026#39;192.168.10.150:9121\u0026#39;] labels: instance: 生产实例4 - targets: [\u0026#39;192.168.10.151:9121\u0026#39;] labels: instance: 生产实例5 - targets: [\u0026#39;192.168.10.152:9121\u0026#39;] labels: instance: 生产实例6 集群版 运行 redis_exporter 服务，只需要连接其中一个节点即可。\n1 2 3 4 ## 无密码 nohup ./redis_exporter redis//192.168.111.11:7000 \u0026amp; ## 有密码 nohup ./redis_exporter -redis.addr 192.168.111.11:7000 -redis.password 123456 \u0026amp; 最后一行添加\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 - job_name: \u0026#39;redis_cluster\u0026#39; static_configs: - targets: - redis://192.168.111.11:7000 - redis://192.168.111.11:7001 - redis://192.168.111.11:7002 - redis://192.168.111.11:7003 - redis://192.168.111.11:7004 - redis://192.168.111.11:7005 metrics_path: /scrape relabel_configs: - source_labels: [__address__] target_label: __param_target - source_labels: [__param_target] target_label: instance - target_label: __address__ replacement: 192.168.111.11:9121 重启prometheus即可。\n四、配置 Grafana 的模板 redis_exporter 在 Grafana 上为我们提供好了 Dashboard 模板：https://grafana.com/dashboards/763\n下载后在 Grafana 中导入 json 模板就可以看到官方这样的示例截图啦：\n注意：Memory Usage这个图表，一直是N/A。是因为redis_memory_max_bytes 获取的值为0\n导致 redis_memory_used_bytes / redis_memory_max_bytes 结果不正常。\n解决办法：将redis_memory_max_bytes 改为服务器的真实内存大小。\n所以我更改计算公式\n1 redis_memory_used_bytes{instance=~\u0026#34;$instance\u0026#34;} / 8193428 本文参考链接：\nhttp://www.eryajf.net/2497.html\nhttps://www.cnblogs.com/fsckzy/p/12053604.html\n","permalink":"https://ktzxy.top/posts/zth6mq8heo/","summary":"Promethus监控Redis","title":"Promethus监控Redis"},{"content":"ps学习 1.认识主界面 选项栏是调节工具相关参数的面板，位置和显示都可以调节\n“Ctrl + 鼠标滚轮” “Shift+ 鼠标滚轮” 也可以实现画板移动\n“Alt+ 鼠标滚轮” 可以放大可缩小画面\n画面周围灰色的区域为文档的工作面\n选择油漆桶工具并设定前景色\n按住“Shift” 点击灰色工作面区域\n2.新建文档 点击菜单栏-文件-新建，可以打开新建选项（快捷键 Ctrl + N）\n剪贴板 未采集像素则不可用，选框为灰色，不能点用剪贴板截图，打开ps,系统会自动计算该截图尺寸，直接建立应该相同大小文档，或直接使用该剪贴图 或者直接去复制—张图片，也能激活剪贴板，建立相同大小文档，粘贴图片即可使用 如果是文件夹里复制—张图片来平时，是不能激活剪贴板功能的，这里的复制是文件整体的复制，剪贴板是复制图片的数据\nPhotoshop数值输入框前的文字都可以拖动，都可以通过鼠标拖拽来改变数值大小\n像素和矢量图 “像素” 是计算数码影像的一种单位，1个像素就是最小的图形的单位，在屏幕上显示的通常就是单个的染色点，放大会失真，边缘锯齿化\n矢量图没有像素的概念，它是面向对象的图像或绘图图像，在数学上定义为一系列由线连接的点\n网络图片，网页或者软件界面 视频图片等显示视频上使用的图片就要以像素为单位。\n分辨率 分辨率的单位简称Ppi，也就是Pixel Per Inch。72Ppi即表示1英寸由72个像素点组成\n分辨率越大，像素越多，图像的细节也就越多自然就越清楚\n颜色模式 新建文档中的【颜色模式】包括【位图】【灰度】【RGB颜色】【CMYK颜色】【Lab颜色】5种\nRGB:红绿蓝三种颜色叠合，按不同比例混合能呈现16777216种颜色（8位深度），是广泛用于显示屏的一种基本色彩模式，大多为网页，网图，视频，3D贴画等电子图像\nCMYK:由青色，品红，黄色，黑色油墨进行混合，来表现初各种印刷颜色，由于是印刷色不是发光色，在显示器上会看起来黯淡一些，多用于海报，宣传册，单页等印刷图像\n位数，常规选8位，16位/32位是专业研究用的\n背景内容\u0026ndash;背景色 点击左下角小屏幕标识可恢复为默认色，也可按D 点击右上角箭头小标识可切换前景背景色，也可按X\n按住Alt键不松，“取消”按钮变成了“复位”\n修改图像大小 快捷键：Ctrl + Alt + i\n像素的多少决定了文件的大小\n文档大小是修改文档的尺寸，文档尺寸改变需要结合分辨率\n勾选缩放样式修改文档大小时，它的图层样式也会跟着放大缩小，是成比例的。\n约束比例意思是限制长宽比，选择后高度和宽度后面会出现锁定框（修改宽度的数值高度也会随比例改变）\n取消重定图像像素图像大小会被锁定，只能修改文档大小\n为了文件用于印制所以调整文档尺寸也很重要，分辨率的单位是像素/英寸，在像素固定的情况下修改分辨率高度宽度也会变化\n画面总像素值=宽度像素值×高度像素值\n画面总像素值=高（英寸）×宽（英寸）×分辨率（像素/英寸）²\n对于网络图片/显示屏图片的分辨率设置为72或96\n挂网也有一个精度指标，叫做线分辨率，简称LPI。LPI是印刷图象所用的网屏的每英寸的网线数，也就是挂网网线数。\n3.文档的打开与保存 打开 1.文件\u0026gt;打开\n2.双击舞台\n3.快捷键Ctrl + o\n勾选图像序列，可以选择命名上有次序的多个图象\n保存 Ctrl + S 存储（原地存储）\nShift + Ctrl + S 存储为（可以选择存储位置、存储名称和存储格式）\nPSD是Photoshop的标准文件格式，包含颜色、图层、通道、路径、动画等信息，是创作图像作品的原始文件。可以说有Photoshop的地方，必有PSD文件\nJPG是最流行的图片文件格式，体积小巧、可变压缩比、支持交错，广泛用于互联网传输，是我们最常见的图片格式\nGIF图片支持透明色、支持动画，网页上看到的动态图、聊天表情大都是GIF图片\nShift + Ctrl +Alt + S 存储为Web所用格式\n关闭 Ctrl + w 关闭\nCtrl + Alt + w 关闭全部\n4.前期准备 暂存盘 找到首选项，选择性能，快捷键 Ctrl + K\nPhotoshop在工作的时候，会产生临时文件，因为软件在运算的过程中，会产生大量的数据，要把数据暂时存在硬盘空间上。一般情况下，【暂存盘】默认设置为第一个驱动器。对于Windows系统来说，也就是C盘，取消C盘，改为其它空闲盘\n为了方便操作失误后，找回更多的历史记录，我们可以将历史记录状态选项调大一些\n自动保存设置 首选项\u0026gt;文件处理\n自动存储恢复信息时间间隔 改为5分钟\nPS的自动保存可以在后台运行，并不拖慢前台的处理速度。\n快捷键设置 有的命令经常用，但却没有快捷键\n有的快捷键按键太多，一个手几乎按不过来\n可以自定义快捷键【编辑】\u0026gt;【键盘快捷键】\n注意：如果不是特殊工作需要，请勿修改快捷键\n5.图层基础知识 认识图层 1.菜单栏上的图层菜单\n2.面板栏的图层面板（窗口\u0026gt;图层 调出 快捷键F7）\n图层锁定 填充是对图像像素填充度的调节\n图层列表，图层是按上下叠加的次序来排列的\n底栏按钮 列表下方还有一栏按钮\n从左往右依次是链接图层按钮，图层样式按钮，添加蒙版按钮，添加调整图层按钮，创建新组按钮，创建新图层按钮，删除按钮。\n图层理解 图层简单的来说就是图像的层次\n一个空白图层就像是一个透明胶片，可以很方便的单独调整和修改图层。\n图层类型 普通图层 背景图层 智能对象图层 调整图层 填充图层 视频图层 矢量图层 3D图层 文字图层 创建图层 底栏有新建图层按钮，点击可创建新图层\n【图层】\u0026gt;【新建】\u0026gt;【图层】快捷键 Ctrl + Shift + N\n可以选择图层的识别颜色，并不是图层上面像素颜色\n双击图层名字能修改图层名称，只有双击文字才能修改名字，否则会弹出图层样式窗口\n图层操作 选择、隐藏、显示、移动、复制和删除\n可以结合Ctrl和Shift键，一次性选择多个图层\n点击小眼睛，来打开和关闭图层像素的显示\n存jpg或者其它最终格式的图像的时候，隐藏图层不显示\n可以通过快捷键隐藏目标图层以外的图层，快捷键Alt\n图层上下次序改变后，画面上的叠加次序也就发生变化了，快捷键Ct + [ 可以把选中的图层向下移动一层，快捷键Ct + ] 可以把选中的图层向上移动一层\nCtrl+Shift+左中括号，把选中的图层直接移动到最下方\nCtrl+Shift+右中括号，把选中的图层直接移动到最上方\n【图层】\u0026gt;【复制图层】进行复制\n按住alt键，来拖拽某个图层也可以复制图层，鼠标拖拽的位置就是复制的位置\n【图层】\u0026gt;【新建】\u0026gt;【通过拷贝的图层】意思是通过对下方图层的拷贝新建一个下方图层一样的图层\n快捷键Ctrl+j\n6.平移和缩放 在工作信息栏左下角可以改变画面大小\n放大、缩小、旋转、平移、只是对视图进行调整，并不是对图像的像素进行修改\n工具栏下面小手一样的图表是抓手工具，选择抓手工具，鼠标在画面就会变成一个小手，这个小手就是用于移动画面视图的，在画面上点击拖动，就可以自由的移动画面\n菜单栏的下方是选项栏，是对工具进行调节的\n按住空格键不松可以直接转换为抓手工具，松开空格会返回到当前工具\n在变成抓手工具的同时按住Alt就变成了缩小工具\n按抓手工具不松，会弹出折叠在里面的一个工具，旋转视图工具，快捷键是R，按住鼠标不松，进行拖动，旋转工具可以对视图进行旋转\n在选项栏可以自定旋转的角度，也可以拖拽属性名称来修改数值，点击复位视图，图像就回到了原始状态\n旋转视图工具主要用途是与数位板相结合\n缩放工具可以对画面进行放大或缩小，在选项栏有放大缩小的选择，也可以点击鼠标右键对画面进行放大或缩小\n细微缩放：点击图像左右移动鼠标对视图进行缩放\n放大快捷键：Ctrl + ‘’加号“ 缩小快捷键：Ctrl + ‘’减号“\nCtrl + 0 是显示图像全部\nCtrl + Alt + 0 是显示图像实际大小\n空格键+Ctrl键是放大工具，空格键+Alt键是缩小工具\n在窗口菜单里找到导航器，点击它，导航器下方的滑杆也可以控制画面的大小，也可以输入数值改变画面大小\n7.移动工具 移动工具 快捷键 V\n移动工具可以多文件间拖拽图层对象，拽至标签激活目标文档，并拽入，原文档并没有移走\n拖拽位置根据鼠标位置而定\n如何移动同时保持原位置不懂呢？\n文档大小相同，开始拖动后，按下Shift，可保持原位\n文档大小不同，开始拖动后，按下Shift，会自动居中，如果目标文档包含选区，会到选区中央\n选项栏上的工具和设置\n自动选择可以靠点击图层对象来选择图层\n自动选择状态下，在透明区域可框选\n移动工具状态下，可以按Ctrl快速切换自动选择模式\n其它工具状态下，按下Ctrl快速切换移动工具\n组合Alt键，可以复制图层\n组合Shift键，约束角度\n方向键可以微调，组合Shift，步长加大\n方向键结合Alt键是复制图层\n创建新组按钮可以建组，可以选择组进行检索\n图层菜单里图层编组可以快捷的建组，快捷键：Ctrl + G\n勾选自动选择，然后选择组，然后选择组里的任何一个图层，都会选择组，改为图层后，就这能选择图层，而不能选择组\n选中显示变换控件，就能对图层对象进行变换\n后面的工具是对齐与分布方式和自动对齐图层\n【图层】\u0026gt;【对齐】和此时的选项都是一样的，都是针对于图层来操作的。\n顶对齐是以画面最上方图层的顶线为基准的对齐\n垂直居中对齐是以图层垂直方向的中心点进行对齐\n底对齐是以画面最下方图层的底线为基准的对齐\n左对齐是以画面最左边图层的左边线为基准对齐\n水平对齐是以图层的水平中心点位基准对齐\n右对齐是以画面最右边图层的右边线为基准对齐\n默认情况下，目标侧的图层固定，其他图层移动找齐\n图层链接后就会以选中的图层为基准对齐\n按左分布是以图层左边线为基准分布\n第一点：2、3、4要在1和5的范围内\n第二点：可以把图层打上链接\n8.选区和选框工具 在进行图像的编辑时，要对图层中某部分的像素进行处理，要把这部分单独选择出来，这个部分叫做选区，在PS中，选区表现为一个封闭的游动虚线区域，因为看上去像是一圈爬动的蚂蚁，俗称蚂蚁线。\n选区内像素可被编辑，可被移动\n矩形选框工具，快捷键M，光标显示方式为十字\n点击选区，按住Shift键，可进行正方形选区\n按住Alt键，可以从中心点建立选区\n同时按住Alt和Shift键，从中心点建立正方形选区\n矩形选区包含了所有类型选区的属性，鼠标进入选区变成白箭头可以移动选区\nCtrl+D取消选区，Ctrl+Shift+D重新选择\nCtrl+H可以隐藏蚂蚁线显示，选区仍存在\n历史工具也可以找回选区，”重新选择“记录上一步选区\n选区是暂时性的，只有保存选区后才能被存储到PSD文件，在通道面板可以找到被保存的选区\n9.选框工具全解 选区有四种组合方式\n新选区 添加到选区（在新选区模式下按Shift键切换到添加到选区，先按住Shift键后再选区） 从选区减去（在新选区模式下按Alt键切换到从选区减去） 与选区交叉（在新选区模式下按Shift+Alt键切换到与选区交叉） 羽化就是选区边缘部分实现过渡式虚化，从而起到渐变的作用，从而达到选区内外自然衔接的效果\n羽化值的大小决定虚化程度\n也可以先做普通选区，然后再进行羽化【选择】【修改】【羽化】快捷键Shift+F6\n矩形选区不存在锯齿\n固定比例，锁定宽高比，固定大小，锁定大小\n调整边缘，必须要有选区\n选中椭圆选框工具，消除锯齿被激活，不选择消除锯齿图像边缘会有锯齿，选择则会变得柔和\n单行和竖行选区工具属于特殊的矩形选框\n单行和竖行选区是可以羽化的\n警告：任何像素都不大于50%选择。选区边将不可见。\n此情况下，选区不可见，仍然存在\n10.套索与魔法工具 套索工具也叫自由套索工具，快捷键是L\n用法：点击鼠标左键拖动鼠标建立选区\n套索工具灵活快速但不够准确\n套索工具的选项栏是选区类工具通用的参数\n按Shift+L键或者按着Alt键来切换\n多边形套索的用法，点击画面的关键的点，来直线连接（在选区未闭合情况下，鼠标不能点选其他命令）\n我们可以通过Backspace键或Delete，来删除刚才的点\nEsc取消选择\n首尾接近时光标出现小圆圈，可以点击闭合（交接快捷方式双击、按住Ctrl左击、或者回车）\n多边形套索工具可以画到文档的外面，而自由套索则不行\n套索工具和多边形套索工具可以按着Alt键相互切换\n多边形套索适合于选取直线边缘（可以结合放大、缩小、平移，来进行多边形选区制作）\n磁性套索的控点可以智能识别像素边缘，磁性套索可以通过Alt键，与多边形套索快速切换。宽度的大小控制识别的范围，高对比度适合清晰边缘，低对比度适合模糊边缘，频率来调节磁性控点的多少\n魔棒工具可以快速的将颜色相近的区域变成选区，容差越大识别范围就越大，反之就越小；勾选连续只能选取互为相邻的相似像素；勾选对所有图层取样，可以选择没有选中图层的颜色\n快速选择工具，三种模式是做选取类通用的模式，但是它没有交叉模式；画笔拾取器包含大小，硬度，间距，大小代表识别的范围（快捷键左、右中括号），硬度是边缘的识别能力（快捷键Shift+左、右中括号），间距是识别的连贯程度；勾选自动增强，识别边缘的能力就更强\n调整边缘命令可以优化选区\n11.选取的编辑调整 选取的自由变换 鼠标右击找到变换选区，或在选择菜单中找到变换选区，此时出现的控杆控点跟前面讲到的变换控件是一样的，拖动控点对选区进行变形，控杆也可以进行这样操作。鼠标右键，打开右键菜单可以对变形进行很多操作\n组合快捷键或调整上方选项栏参数对选区进行准确调节，变换完后，点击对号，确认变换结束，点击别的工具也可以完成选区变换的确认；如果选区变换的不是很满意，点击取消符号或者Esc键取消选区\n反向选择 选取背景后，选择反向，留住主体；通过右键菜单，右击，选择反向；选择菜单中也可以找到反向命令，快捷键Ctrl+Shift+I\n羽化 羽化可以制作出边缘比较柔和的选区\n羽化使选区内透明度值向外递减\n透明度 两个外形一样选区，透明度却可以不同\n通过填充颜色，来看出两个选区透明度的差别\n填充前景色，快捷键Alt+Delete\n半透明选区制作：\n打开通道面板，新建通道\n在通道里制作一个选区，填充灰色\nCtrl+D取消选区\n在通道面板点击按钮：将通道作为选区载入\n边界 边界命令可以让边界变成选区\n扩展与收缩 平滑命令可以平滑尖角\n扩展与收缩来对选区进行周边缩放\n选区转路径 选区可以转换为路径\n路径是重要的辅助工具，是矢量元素\n12.历史工具 当选区出现错误时，使用历史工具返回\n找到窗口下的历史记录点击后出现历史记录面板\n最上方为原始图片，也可以叫做快照栏，下面出现的操作步骤栏，为历史记录栏，面板下方还有三个按钮\n删除中间操作记录，下面的所有记录也就被删除了；也可以右击删除键、或清除历史记录\n【编辑】【清理】也可以找到清除历史记录\n【编辑】【还原移动】快捷键：Ctrl+Z，只能返回一步\n多次点击后退一步或Ctrl+Alt+Z命令可返回多步\n历史工具下的“从当前状态创建新文档”按钮，形成两个不同的文档\n下一个按钮：创建新快照\n利用快照，把状态保存下来，使之不被新的步骤顶替，也可以右击新建快照；或按住Alt键不松，点击创建快照按钮弹出对话窗口\n历史记录只是暂存的信息，是不会被存储的\n勾选 “允许非线性历史记录”后，会特定删除记录\n历史记录画笔工具，快捷键Y，历史画笔工具就起到了恢复原始图片区域的作用，它的设置符合画笔的通用设置\n通过不同历史源以及画笔效果产生特别的画面效果\n13.画笔工具 画笔工具 画笔工具的操作就是在画面上直接进行绘制，快捷键B\n点击画笔工具，光标变成圆圈，圆圈代表画笔大小\nShift键可画出直线，或连接两个点成一条直线\n改变前景色，可该表画笔画的颜色\n画笔状态下按住Alt键，拾取颜色\n按住Shift+Alt+鼠标右键可以使用HUD拾色器\n画笔预设 画笔预设选取器可以改变画笔大小、硬度\n改变画笔大小：“[” “]” 键，Alt+右键水平移动\n改变画笔硬度：Shift + “[” “]” 键，Alt+右键垂直移动\n可以在列表选择丰富的笔刷效果，快捷键“,” “.” 结合Shift，快速切换画笔预设\n原色通道用灰色图像表示，示意发光强弱\n通过载入外部画笔来丰富我们的画笔库\n选择基本画笔，调节大小硬度、来新建简单画笔预设\n画笔面板 选项栏中——切换画笔画板\n画笔面板用于更加全面的调节、编辑画笔\n【窗口】【画笔】快捷键F5\n存储笔刷文件：面板菜单、存储画笔\n工具预设面板里可直接使用保存好各项参数的工具，与存储画笔区别在：保存的参数设置范围是不一样的\n绘画模式 混合模式就是对我们绘画的模式进行更改的地方\n不透明度和流量 不透明度就是设置色彩的不透明度的\n在画笔模式下，快捷键：按下数字键调整画笔不透明度\n流量好比是设定画笔里颜料流出来多少\n不同的流量跟不透明度可使我们的绘制工作更加细腻\n使用喷枪样式，越按住鼠标左键不松，颜色会越多\n两个按钮分别代表：对不透明度、大小使用压力\n数位板通过笔尖的真实压力在PS里绘制\n压力大小来控制画笔透明度和大小\n铅笔工具 铅笔使用方法与画笔基本相同\n铅笔的笔触带有锯齿\n铅笔与画笔不同它有一个自动抹除选项\n橡皮擦工具 快捷键E\n跟画笔工具使用方法一样\n了解第一点：橡皮擦是擦除图层上的像素\n了解第二点：从橡皮擦选项栏的模式里切换擦除的模式\n了解第三点：抹到历史记录；跟历史记录工具效果是一样的\n了解第四点：学会了蒙版，橡皮擦工具几乎是不用的\n背景橡皮擦 可智能擦除背景，得到主题图像\n取样方式不同擦出效果不同\n擦除范围可选三种限制方式\n容差越高擦除范围越大，反之越小\n选择相应的前景色可以受到保护不被擦除\n魔术橡皮擦 魔术橡皮擦可以擦除相近颜色的区域\n14.修复工具 模糊工具 模糊工具的主要功能就是把图像变得模糊\n这样模糊处理后容易营造出画面的空间、层次感\n在对图像进行模糊的同时还可以通过模式改变对象颜色\n强度值越大，模糊效果越大，反之越小\n对所有图层取样指的是可对所有可见图层进行模糊处理，勾选“对所有图层取样”操作都会作用于目标图层上\n滤镜里面也可以制作模糊效果\n锐化工具 与模糊工具对立，它可使图像变得锐利清晰\n勾选“保护细节”可保护图像微小像素防止画面失真\n锐化工具效果无法与模糊工具效果相转换\n使用模糊工具后会改变、损失很多原有像素信息\n锐化工具是在图像现有像素基础上进行锐化的\n涂抹工具 涂抹强度值越高，涂抹效果越强\n勾选“手指绘画”可以绘制前景色，快捷键Alt\n减淡、加深工具 保护色调可以保护图像在使用减淡时色调方面失真\n海绵工具 作用：局部增加饱和度或降低饱和度\n降低饱和度最终可使图像接近黑白\n仿制图章工具 按住Alt键，光标变成靶心形状后代表可以寻找仿制源了，取样后就可以按照画笔用法来涂抹仿制对象了\n“对齐” 保证鼠标每次操作都与源点对齐而画出整片图像\n当前图层模式在当前图层定义仿制源\n当前和下方模式可以定义当前及下方图层图像为源\n“切换仿制源面板” 在窗口中也能找到；通过仿制源面板可以对仿制源进行更多深层次的编辑，还可以从这里定义多个仿制源；“位移” 参数记录仿制源位移的位置；设置宽、高参数可以仿制出放大缩小的仿制源图像；同理设置角度也可以仿制出旋转一定角度后的图像\n图案图章工具 用法就是直接在画面上画图案\n勾选印象派效果命令，就会绘画出印象派效果\n修复画笔工具 修复画笔工具快捷键“J”；Shift+J可以在工具组里进行切换\n它的使用方法跟仿制图章工具基本上是一样的，使用修复画笔工具后，边缘会有融合的效果\n仿制图章进行的仿制是很严格的，一板一眼；修复画笔有一定智能性，可以和周围环境进行融合；两者区别在于一个较智能，一个较准确\n污点修复画笔工具 在污点上点击、绘画就可以了\n在去污的过程中还可以选择相应的模式，内容识别是一种智能的识别方式，一般都会选择它；也可以选择：近似匹配、创建纹理；效果页略显不同；近似匹配计算较为简单，就近取样覆盖污点。创建纹理：在擦除污点同时创建出和周围相似的纹理效果，在某些有特殊纹理的图像上修复污点时就可以使用此模式\n修补工具 修补工具可以快速地对画面进行修复\n首先是修补工具选项栏的四种选区模式\n内容感知移动工具 比修补工具更加智能和简单\n“移动”代表的是移走 “扩展”代表的是复制\n在“适应”里控制边缘融合的效果\n红眼工具 眼睛在闪光灯作用下会扩张，而毛细血管就会呈现出红色，该工具就会快速的把红眼消掉，使用红眼工具在红眼上框一下就可以了\n15.颜色和图案的填充 填充工具和命令可以大概填充实色，渐变，图案等\n油漆桶工具\t快捷键G；可以填充前景色和图案\n油漆桶用来填充相近颜色的区域\n油漆桶工具还可以填充图案\n不透明度的数值决定填充的透明效果\n【编辑】【填充】命令\t快捷键 Shift + F5或Shift + 后退键\n填充前景色的快捷键Alt + Delete\n填充背景色的快捷键Ctrl+ Delete\n内容识别填充具有智能计算的功能\n填充-图案，可以填充整体区域或选区的图案\n编辑-定义图案，可以定制自己的图形作为图案元素\n填充-历史记录，可以填充相应的历史源\n渐变工具的用法是点线拖拽\n渐变拾色器可以选取预设的渐变类型\n渐变编辑器可以制作任何渐变的效果\n色标的位置，色彩，透明度来控制渐变的效果；色标可以自由的增加或删除；平滑度控制渐变过渡的平滑程度\n五种渐变样式，丰富渐变的过渡方式\n双击图层缩览图可以唤出图层样式面板\n图层样式是针对图层的一种效果控制，功能很强大\n特殊的新建图层：填充图层\n16.自由变换 变换可以针对整个图层（组或链接图层），或者选区内\n编辑-自由变换\tCtrl+T\n自由变换的控件框来控制变换的效果\n控点控制形变，参考点是变换轴心；参考点可以随意移动，或按Alt键定位\n参考点位置按钮可以快速精准定位参考点\n选项栏上可以调节X，Y值，来精确控制对象移动\n变换的结果要应用（Enter）或者取消（Esc）\n再次变换可以快捷的重复变换的操作，快捷键Ctrl+Shift+T\n修改选项栏的百分比实现精确缩放\nCtrl+Z可以还原一步变换的操作\n位图对象的缩放要谨慎\n斜切是一种倾斜效果的变换\n变形，变换控件框为九宫格式的贝塞尔网面\n变形模式的选项栏提供了许多预设的样式，变形不能再次变换\n扭曲和透视需要鼠标来操控，移动控点，定位控点，完成变换操作\n在自由变换模式下，按住Ctrl键快速进入扭曲模式\n透视也是直接拖拽控点定位，但其遵循透视法则\n17.通道和蒙版基础知识 可以把通道看作另一个图层，在通道里可以通过颜色信息或者透明度区域信息，把图像分成若干个层次，便于调整图像色彩或者制作各种选区，辅助图层进行图像处理\n打开一张色彩模式为RGB的图像，在通道面板可以发现，PS自动分许出了图像的红绿蓝颜色层次\n通道基于图像的色彩或透明度来分析、编辑\n通道也有不同的类型，图像本身带有原色通道，还可以添加专色通道以及Alpha通道\n通道面板 用于查看编辑通道的工具\n通道面板有通道列表，下方功能按钮，面板菜单组成【窗口】【通道】\n复合通道是原色通道的合成效果\n原色通道用灰度图像表示，示意发光强弱\nRGB色彩模式下，白色代表色光强度最高，最高值为：255；RGB色彩模式下，黑色代表色光强度最底，不发光，值为：0\n可以通过首选项来修改原色通道的预览效果\n色彩模式不同，通道构成也不同\n认知Alpha通道 PSD，TGA，TIFF等格式的图片可以支持Alpha通道写入\n通过存储选区，面板按钮，面板菜单，都可以创建Alpha通道\n除了三色通道，还可以添加53哥Alpha个通道\n通道和选区的关系 不同的透明度区域表示着选区的范围\nCtrl 快捷载入选区\nAlpha通道:黑色不包含像素信息,代表着透明。\nAlpha通道:白色乃100%的像素覆盖,代表着此处是不透明的实色区域。\n不同的灰度表示不同的透明度\n编辑Alpha通道 编辑通道和编辑图层一样，可以使用常用的工具和命令\n灰阶像素的调整和形状的编辑，就是为了得到相应的选区\n基于原色通道的灰度编辑，可以获得复杂精细的选区\nAlpha通道=选区加工厂\n认识图层蒙版 可以隐藏和显示图层上的部分区域\nAlt键可以得到一个隐藏选区的蒙版\n编辑图层蒙版 图层蒙版编辑状态下，对应的就是一个临时的Alpha通道\n添加黑色隐藏图像，添加白色显示图像\n蒙版可以暂时停用或启用，快捷键Shift+点击蒙版\n快速蒙版模式 快速蒙版就是一个临时的Alpha通道\n【选择】【在快速蒙版模式下编辑】快捷键Q\n快速蒙版模式的优势：1.快捷2.和图层叠加显示，方便查看\n设置快速蒙版 通过选项可以修改蒙版方式和显示区域颜色类型\n快速蒙版是一种模式，创立的选区是普通的临时选区\n18.图层进阶知识 图层过滤 可以根据图层不同的性质进行查看管理\n图层锁定 即是对图层或图层某部分进行操作保护\n锁定透明像素：禁止对透明区域进行操作\n锁定图像像素：禁止编辑图像，但可以移动变换\n锁定位置：禁止移动变换\n锁定全部：禁止一切操作\n隐藏图层也可以保护图层编辑\n图层链接 图层链接可以多图层统一移动变换\n按住Shift键点击链接图标可以临时停用链接\n取消链接，可以单层，也可以多层选中同时取消\n图层链接后统一移动变换，但图层仍属独立编辑\n图层整合以后，图层群们是否具有统一的操作属性，详见此表：\n统一移动变换 统一调整命令 统一混合模式 同级叠加次序 统一施加滤镜 统一施加蒙版 统一合并命令 图层链接 √ × × × × × × 图层编组 √ × √ √ × √ √ 智能对象 √ √ √ √ √ √ √ 图层编组 图层编组可以整合管理，并具有多种统一属性\n【图层】【新建】【组】\n组的操作类似于操作系统的文件与文件夹的操作\n常用的编组快捷键操作：Ctrl+G\n取消编组：Ctrl+Shift+G\n组具有多种整合的特性\n图层拼合 Ctrl+E，Ctrl+Alt+E，图层合并命令\n盖印图层 盖印图层：所有可见图层拼合效果的新图层\n快捷键：Ctrl+Shift+Alt+E\n图层复合 保存当前图层的位置，可见性，样式信息\n图层剪贴蒙版 剪贴蒙版：上方图层进入下方图层形状\n19.色彩基础 单色光：三基色光（红Red，绿Green，蓝Blue一切色彩的基础）\n复合光：色光混合（太阳光等其他可见光）\n在通道内发现色光 RGB基于色光的混合模式，是最常见的色彩模式\n在RGB原色通道里，填充白色，即为本色光最强的发光\n吸管工具与颜色面板 吸管工具 快捷键I，用于拾取色彩色值\nAlt键切换吸管工具，另有Shift+Alt+右键：HUD拾色器\n吸管直接生成前景色；按Alt键吸收，生成背景色\n颜色面板 快捷键F6\nRGB色值表达方法 红色的RGB色值255.0.0\n黑色的RGB色值0.0.0\n在RGB通道创造色彩 红+绿=黄\t红+蓝=品红\t绿+蓝=青\n色彩三要素：色相 颜色的品相\n色相/饱和度命令可以根据色彩三要素来直观的调色 快捷键Ctrl + U\n调整角度值用来改变色相\n色彩三要素：饱和度 饱和度是色彩的鲜艳程度，其实就是加入中性灰的程度\n调节饱和度命令，可以看作是增减中性灰的操作\n色彩三要素：明度 调节明度，就是调节发光量，加入额外的白光\nHSB拾色模式 取色模式可以选择HSB, RGB, LAB, CMYK, Web模式\nHSB并不是计算机色彩模式，而是颜色表现模式\nCMYK模式详解 印刷色彩模式\nCMYK模式是反光色，屏幕显示颜色没有那么亮\nCMYK通道里，黑色代表100%浓度的油墨\nCMYK靠百分比来表达当前通道的色值\nRGB是加色模式，CMYK是减色模式\n制作印刷类图像，不要使用RGB模式，否则成品会偏色\n拾色器会对超出印刷色域的颜色做警告，并可以自动修复\n20.亮度与色阶看懂直方图 控制明暗的视觉因素，三要素之一，明度\n明度较低时，RGB色值偏低，CMYK色值较高\n明暗对比：明靠暗衬托，暗配明更暗。\nCtrl+U 色相/饱和度也能调节明度，但效果太单一\n灰度模式的图像，只有亮度的对比，更能够直观观察明暗\n灰度模式，好比CMYK通道中的单个K通道\n在灰度模式下，使用亮度/对比度命令\n调节滑块改变数值，相应的发生明暗变化\n亮度/对比度是最简单的调色命令，功能有限\nCtrl+L 色阶\n色阶，合并颜色的发光级别\n直方图：用二维坐标表示画面像素发光强度的分布\n水平方向从左到右依次为：黑场，灰场，白场\n垂直方向上的高度，代表着像素的数量\n调整输入色阶 黑场数值增大，画面变暗\n亮度级别为70以下的像素，都合并为最低的0\n亮度级别70以上，至灰场以下的像素，都会阶梯式变暗\n70以下的像素亮度都归0，所以暗部图像的细节不再有\n调整黑场，就是合并暗部的颜色，对亮部暂无影响\n调节白场，和黑场的道理一样，反向去理解\n调节灰场，是调节的黑白场的比例\n调整输出色阶 输出色阶，对最高级别和最低级别，做一个输出限定\n预设里提供了一些设定好的参数，也可以保存预设。\n合并红通道的黑场，红色减少，颜色开始偏蓝绿\n合并红通道白场，红色增强，亮部发红\n色阶里合并单个通道，也是调色手段，按情况使用即可\n自动是PS根据图像，自己做出的调整，一般不会过度\n调整类的命令，会损失原图像素的色彩信息，不能灵活修改\n调整图层 在调整图层可以找到和对应的调整命令相同的参数\n调整图层，对下方所有的图层起着调整作用\n调整图层可以即调即得，随时修改参数，改变图像效果\n调整图层不对下方图层的像素做任何修改破坏\n调整图层自带蒙版，可以灵活控制调节或者不调节的区域\n调整图层对其下方所有的层起作用，对上方的没有影响\n调整图层结合剪贴蒙版，可以对单个图层起调整作用\n21.曲线和色彩平衡调色 ","permalink":"https://ktzxy.top/posts/ddwk3m7fso/","summary":"ps学习","title":"ps学习"},{"content":"在我们对数据库技术方案设计的时候，我们是否有自己的设计理念或者原则，还是更多的依据自己的直觉去设计，是否曾经懊悔线上发生过的一次低级故障，可能稍微注意点就可以避免，是否想过怎么才能很好的避免，下面规范的价值正是我们工作的检查清单，需要我们不断从错误中积累有效经验来指导未来的工作。以下规范在大型互联网公司经过了充分的验证，尤其适用于并发量大、数据量大的业务场景。先介绍的是安全规范，因为安全无小事，很多公司都曾经因为自己的数据泄露导致用户的惨痛损失，所以将安全规范放到了第一位。﻿\n一、安全规范 1.【强制】禁止在数据库中存储明文密码，需把密码加密后存储\n**说明：**对于加密操作建议由公司的中间件团队基于如mybatis的扩展，提供统一的加密算法及密钥管理，避免每个业务线单独开发一套，同时也与具体的业务进行了解耦\n2.【强制】禁止在数据库中明文存储用户敏感信息，如手机号等\n**说明：**对于手机号建议公司搭建统一的手机号查询服务，避免在每个业务线单独存储\n3.【强制】禁止开发直接给业务同学导出或者查询涉及到用户敏感信息的数据，如需要需上级领导审批\n4.【强制】涉及到导出数据功能的操作，如包含敏感字段都需加密或脱敏\n5.【强制】跟数据库交互涉及的敏感数据操作都需有审计日志，必要时要做告警\n6.【强制】对连接数据库的IP需设置白名单功能，杜绝非法IP接入\n7.【强制】对重要sql（如订单信息的查询）的访问频率或次数要做历史趋势监控，及时发现异常行为\n8.【推荐】线上连接数据库的用户名、密码建议定期进行更换\n二、基础规范 1.【推荐】尽量不在数据库做运算，复杂运算需移到业务应用里完成\n2.【推荐】拒绝大sql语句、拒绝大事务、拒绝大批量，可转化到业务端完成\n**说明：**大批量操作可能会造成严重的主从延迟，binlog日志为row格式会产生大量的日志\n3.【推荐】避免使用存储过程、触发器、函数等，容易造成业务逻辑与DB耦合\n**说明：**数据库擅长存储与索引、要解放数据库CPU，将计算转移到服务层、也具备更好的扩展性\n4.【强制】数据表、数据字段必须加入中文注释\n**说明：**后续维护的同学看到后才清楚表是干什么用的\n5.【强制】不在数据库中存储图片、文件等大数据\n**说明：**大文件和图片需要储在文件系统\n6.【推荐】对于程序连接数据库账号，遵循权限最小原则\n7.【推荐】数据库设计时，需要问下自己是否对以后的扩展性进行了考虑\n8.【推荐】利用 pt-query-digest 定期分析slow query log并进行优化\n9.【推荐】使用内网域名而不是ip连接数据库\n10.【推荐】如果数据量或数据增长在前期规划时就较大，那么在设计评审时就应加入分表策略\n11.【推荐】要求所有研发SQL关键字全部是小写，每个词只允许有一个空格﻿\n三、命名规范 1.【强制】库名、表名、字段名要小写，下划线风格，不超过32个字符，必须见名知意，建议使用名词而不是动词，词义与业务、产品线等相关联，禁止拼音英文混用\n2.【强制】普通索引命名格式：idx_表名_索引字段名（如果以首个字段名为索引有多个，可以加上第二个字段名，太长可以考虑缩写）；唯一索引命名格式：uk_表名_索引字段名（索引名必须全部小写，长度太长可以利用缩写）；主键索引命名：pk_字段名\n3.【强制】库名、表名、字段名禁止使用MySQL保留字\n4.【强制】临时库表名必须以tmp为前缀，并以日期为后缀\n5.【强制】备份库表必须以bak为前缀，并以日期为后缀\n6.【推荐】用HASH进行散表，表名后缀使用16进制数，下标从0开始\n7.【推荐】按日期时间分表需符合YYYY[MM][DD][HH]格式\n8.【推荐】散表如果使用md5（或者类似的hash算法）进行散表，表名后缀使用16进制，比如user_ff\n9.【推荐】使用CRC32求余（或者类似的算术算法）进行散表，表名后缀使用数字，数字必须从0开始并等宽，比如散100张表，后缀从00-99\n10.【推荐】使用时间散表，表名后缀必须使用特定格式，比如按日散表user_20110209、按月散表user_201102\n11.【强制】表达是与否概念的字段，使用 is _ xxx 的方式进行命名﻿\n四、库设计规范 1.【推荐】数据库使用InnoDB存储引擎\n**说明：**支持事务、行级锁、并发性能更好、CPU及内存缓存页优化使得资源利用率更高\n2.【推荐】数据库和表的字符集统一使用UTF8\n**说明：**utf8号称万国码，其无需转码、无乱码风险且节省空间。若是有字段需要存储emoji表情之类的，则将表或字段设置成utf8mb4，utf8mb4向下兼容utf8。\n3.【推荐】不同业务，使用不同的数据库，避免互相影响\n4.【强制】所有线上业务库均必须搭建MHA高可用架构，避免单点问题\n五、表设计规范 1.【推荐】建表规范示例\n1 2 3 4 5 6 7 8 9 10 11 12 CREATE TABLE `student_info` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT \u0026#39;主键\u0026#39;, `stu_name` varchar(10) NOT NULL DEFAULT \u0026#39;\u0026#39; COMMENT \u0026#39;姓名\u0026#39;, `stu_score` smallint(5) unsigned NOT NULL DEFAULT \u0026#39;0\u0026#39; COMMENT \u0026#39;总分\u0026#39;, `stu_num` int(11) NOT NULL COMMENT \u0026#39;学号\u0026#39;, `gmt_create` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT \u0026#39;创建时间\u0026#39;, `gmt_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT \u0026#39;更新时间\u0026#39;, `status` tinyint(4) DEFAULT \u0026#39;1\u0026#39; COMMENT \u0026#39;1代表记录有效，0代表记录无效\u0026#39;, PRIMARY KEY (`id`), UNIQUE KEY `uk_student_info_stu_num` (`stu_num`) USING BTREE, KEY `idx_student_info_stu_name` (`stu_name`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=\u0026#39;学生信息表\u0026#39;; 2.【强制】禁止使用外键，如果有外键完整性约束，需要应用程序控制\n3.【强制】每个Innodb 表必须有一个主键\n**说明：**Innodb 是一种索引组织表，其数据存储的逻辑顺序和索引的顺序是相同的。每张表可以有多个索引，但表的存储顺序只能有一种，Innodb 是按照主键索引的顺序来组织表的，因此不要使用更新频繁的列如UUID、MD5、HASH和字符串列作为主键，这些列无法保证数据的顺序增长，主键建议使用自增ID 值。\n4.【推荐】单表列数目最好小于50\n5.【强制】禁止使用分区表\n**说明：**分区表在物理上表现为多个文件，在逻辑上表现为一个表，谨慎选择分区键，跨分区查询效率可能更低，建议采用物理分表的方式管理大数据\n6.【推荐】拆分大字段和访问频率低的字段，分离冷热数据\n7.【推荐】采用合适的分库分表策略，例如千库十表、十库百表等（建议表大小控制在2G）\n8.【推荐】单表不超过50个int字段；不超过20个char字段，不超过2个text字段\n9.【推荐】表默认设置创建时间戳和更改时间戳字段\n10.【推荐】日志类型的表可以考虑按创建时间水平切割，定期归档历史数据\n11.【强制】禁止使用order by rand()\n**说明：**order by rand()会为表增加一个伪列，然后用rand()函数为每一行数据计算出rand()值，基于该行排序，这通常都会生成磁盘上的临时表，因此效率非常低。\n12.【参考】可以结合使用hash、range、lookup table进行散表\n13.【推荐】每张表数据量建议控制在500w以下，超过500w可以使用历史数据归档或分库分表来实现（500万行并不是MySQL数据库的限制。过大对于修改表结构，备份，恢复都会有很大问题。MySQL没有对存储有限制，取决于存储设置和文件系统）\n14.【强制】禁止在表中建立预留字段\n**说明：**预留字段的命名很难做到见名识义，预留字段无法确认存储的数据类型，所以无法选择合适的类型；对预留字段类型的修改，会对表进行锁定\n六、字段设计规范 1.【强制】必须把字段定义为NOT NULL并且提供默认值\n**说明：**NULL字段很难查询优化，NULL字段的索引需要额外空间，NULL字段的复合索引无效\n2.【强制】禁止使用ENUM，可使用TINYINT代替\n3.【强制】禁止使用TEXT、BLOB类型(如果表的记录数在万级以下可以考虑)\n4.【强制】必须使用varchar(20)存储手机号\n5.【强制】禁止使用小数存储国币、使用“分”作为单位，这样数据库里就是整数了\n6.【强制】用DECIMAL代替FLOAT和DOUBLE存储精确浮点数\n7.【推荐】使用UNSIGNED存储非负整数\n**说明：**同样的字节数，存储的数值范围更大\n8.【推荐】建议使用INT UNSIGNED存储IPV4\n**说明：**用UNSINGED INT存储IP地址占用4字节，CHAR(15)则占用15字节。另外，计算机处理整数类型比字符串类型快。使用INT UNSIGNED而不是CHAR(15)来存储IPV4地址，通过MySQL函数inet_ntoa和inet_aton来进行转化。IPv6地址目前没有转化函数，需要使用DECIMAL或两个BIGINT来存储。例如:\nSELECT INET_ATON(\u0026lsquo;192.168.172.3\u0026rsquo;); 3232279555 SELECT INET_NTOA(3232279555); 192.168.172.3\n9.【推荐】字段长度尽量按实际需要进行分配，不要随意分配一个很大的容量\n10.【推荐】核心表字段数量尽可能地少，有大字段要考虑拆分\n11.【推荐】适当考虑一些反范式的表设计，增加冗余字段，减少JOIN\n12.【推荐】资金字段考虑统一*100处理成整型，避免使用decimal浮点类型存储\n13.【推荐】使用VARBINARY存储大小写敏感的变长字符串或二进制内容\n**说明：**VARBINARY默认区分大小写，没有字符集概念，速度快\n14.【参考】INT类型固定占用4字节存储\n**说明：**INT(4)仅代表显示字符宽度为4位，不代表存储长度。数值类型括号后面的数字只是表示宽度而跟存储范围没有关系，比如INT(3)默认显示3位，空格补齐，超出时正常显示，Python、Java客户端等不具备这个功能\n15.【参考】区分使用DATETIME和TIMESTAMP\n**说明：**存储年使用YEAR类型、存储日期使用DATE类型、存储时间(精确到秒)建议使用TIMESTAMP类型。\nDATETIME和TIMESTAMP都是精确到秒，优先选择TIMESTAMP，因为TIMESTAMP只有4个字节，而DATETIME8个字节，同时TIMESTAMP具有自动赋值以及⾃自动更新的特性。\n补充：如何使用TIMESTAMP的自动赋值属性?\n自动初始化，而且自动更新：column1 TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATECURRENT_TIMESTAMP 只是自动初始化：column1 TIMESTAMP DEFAULT CURRENT_TIMESTAMP 自动更新，初始化的值为0：column1 TIMESTAMP DEFAULT 0 ON UPDATE CURRENT_TIMESTAMP 初始化的值为0：column1 TIMESTAMP DEFAULT 0\n16.【推荐】将大字段、访问频率低的字段拆分到单独的表中存储，分离冷热数据\n**说明：**有利于有效利用缓存，防⽌读入无用的冷数据，较少磁盘IO，同时保证热数据常驻内存提⾼高缓存命中率\n17.【参考】VARCHAR(N)，N表示的是字符数不是字节数，比如VARCHAR(255)，可以最大可存储255个汉字，需要根据实际的宽度来选择N\n18.【参考】VARCHAR(N)，N尽可能小，因为MySQL一个表中所有的VARCHAR字段最大长度是65535个字节，进行排序和创建临时表一类的内存操作时，会使用N的长度申请内存\n19.【推荐】VARCHAR(N)，N\u0026gt;5000时，使用BLOB类型\n20.【推荐】使用短数据类型，比如取值范围为0~80时，使用TINYINT UNSIGNED\n21.【强制】存储状态，性别等，用TINYINT\n22.【强制】所有存储相同数据的列名和列类型必须一致（在多个表中的字段如user_id，它们类型必须一致）\n23.【推荐】优先选择符合存储需要的最小数据类型\n24.【推荐】如果存储的字符串长度几乎相等，使用 char 定长字符串类型\n七、索引设计规范 1.【推荐】单表索引建议控制在5个以内\n**说明：**索引可以增加查询效率，但同样也会降低插入和更新的效率，甚至有些情况下会降低查询效率，所以不是越多越好\n2.【强制】禁止在更新十分频繁，区分度不高的属性上建立索引\n3.【强制】建立组合索引必须把区分度高的字段放在前面\n4.【推荐】对字符串使用索引，如果字符串定义长度超过128的，可以考虑前缀索引\n5.【强制】表必须有主键，并且是auto_increment及not null的，根据表的实际情况定义无符号的tinyint,smallint,int,bigint\n6.【强制】禁止更新频繁的列作为主键\n7.【强制】禁止字符串列作为主键\n8.【强制】禁止UUID MD5 HASH这些作为主键(数值太离散了)\n9.【推荐】默认使用非空的唯一键作为主键\n10.【推荐】主键建议选择自增或发号器\n11.【推荐】核心SQL优先考虑覆盖索引\n12.【参考】避免冗余和重复索引\n13.【参考】索引要综合评估数据密度和分布以及考虑查询和更新比例\n14.【强制】不在索引列进行数学运算和函数运算\n15.【推荐】研发要经常使用explain，如果发现索引选择性差，必须要学会使用hint\n16.【推荐】能使用唯一索引就要使用唯一索引，提高查询效率\n17.【推荐】多条字段重复的语句，要修改语句条件字段的顺序，为其建立一条联合索引，减少索引数量\n18.【强制】索引字段要保证不为NULL，考虑default value进去。NULL也是占空间，而且NULL非常影响索引的查询效率\n19.【强制】新建的唯一索引不能和主键重复\n20.【推荐】尽量不使用外键、外键用来保护参照完整性，可在业务端实现\n**说明：**避免对父表和子表的操作会相互影响，降低可用性\n21.【强制】字符串不应做主键\n22.【强制】表必须有无符号int型自增主键，对应表中id字段\n**说明：**必须得有主键的原因：采用RBR模式复制，无主键的表删除，会导致备库夯住 ；使用自增的原因：\n数据写入可以提高插入性能，避免page分裂，减少表碎片\n23.【推荐】对长度过长的VARCHAR字段建立索引时，添加crc32或者MD5 Hash字段，对Hash字段建立索引\n**说明：**下面的表增加一列url_crc32，然后对url_crc32建立索引，减少索引字段的长度，提高效率\nCREATE TABLE url( \u0026hellip; url VARCHAR(255) NOT NULL DEFAULT 0, url_crc32 INT UNSIGNED NOT NULL DEFAULT 0, \u0026hellip; index idx_url(url_crc32) ）\n24.【推荐】WHERE条件中的非等值条件（IN、BETWEEN、\u0026lt;、\u0026lt;=、\u0026gt;、\u0026gt;=）会导致后面的条件使用不了索引\n25.【推荐】索引字段的顺序需要考虑字段值去重之后的个数，个数多的放在前面\n26.【推荐】ORDER BY，GROUP BY，DISTINCT的字段需要添加在索引的后面\n27.【参考】合理创建联合索引（避免冗余），如(a,b,c) 相当于 (a) 、(a,b) 、(a,b,c)\n28.【推荐】复合索引中的字段数建议不超过5个\n29.【强制】不在选择性低的列上建立索引，例如\u0026quot;性别\u0026quot;, \u0026ldquo;状态\u0026rdquo;， \u0026ldquo;类型\u0026rdquo;\n30.【推荐】对于单独条件如果走不了索引，可以使用force –index强制指定索引\n31.【强制】禁止给表中的每一列都建立单独的索引\n32.【推荐】在varchar字段上建立索引时，必须指定索引长度，没必要对全字段建立索引，根据实际文本区分度决定索引长度即可﻿\n八、SQL使用规范 1.【强制】禁止使用SELECT *，只获取必要的字段，需要显示说明列属性\n**说明：**按需获取可以减少网络带宽消耗，能有效利用覆盖索引，表结构变更对程序基本无影响。\n2.【强制】禁止使用INSERT INTO t_xxx VALUES(xxx)，必须显示指定插入的列属性\n3.【强制】WHERE条件中必须使用合适的类型，避免MySQL进行隐式类型转化\n**说明：**因为MySQL进行隐式类型转化之后，可能会将索引字段类型转化成=号右边值的类型，导致使用不到索引，原因和避免在索引字段中使用函数是类似的，例子 select uid from t_user where phone=15855550101（phone为 varchat 类型，此时查询中使用数字查询，会导致索引失效）\n4.【强制】禁止在WHERE条件的属性上使用函数或者表达式\n5.【强制】禁止负向查询，以及%开头的模糊查询\n6.【强制】应用程序必须捕获SQL异常，并有相应处理\n7.【推荐】sql语句尽可能简单、大的sql想办法拆成小的sql语句\n**说明：**简单的SQL容易使用到MySQL的querycache、减少锁表时间特别是MyISAM、可以使用多核cpu\n8.【推荐】事务要简单，整个事务的时间长度不要太长\n9.【强制】避免在数据库中进行数学运算或者函数运算(MySQL不擅长数学运算和逻辑判断，也容易将业务逻辑和DB耦合在一起)\n10.【推荐】sql中使用到OR的改写为用IN() (or的效率没有in的效率高)\n11.【参考】SQL语句中IN包含的值不应过多，里面数字的个数建议控制在1000个以内\n12.【推荐】limit分页注意效率。Limit越大，效率越低。可以改写limit\n**说明：**改写例子：\n1）改写方法一\n延迟回表写法 select xx,xx from t t1, (select id from t where \u0026hellip;. limit 10000,10) t2 where t1.id = t2.id\n2）改写方法二\nselect id from t limit 10000, 10; 应该改为 =\u0026gt; select id from t where id \u0026gt; 10000 limit 10;\n13.【推荐】尽量使用union all替代union\n14.【参考】避免使用大表JOIN\n15.【推荐】对数据的更新要打散后批量更新，不要一次更新太多数据\n16.【推荐】使用合理的SQL语句减少与数据库的交互次数\n17.【参考】注意使用性能分析工具 Sql explain / showprofile / mysqlsla\n18.【推荐】能不用NOT IN就不用NOT IN，坑太多了，会把空和NULL给查出来\n19.【推荐】关于分页查询，程序里建议合理使用分页来提高效率，limit、offset较大要配合子查询使用\n20.【强制】禁止在数据库中跑大查询\n21.【强制】禁止单条SQL语句同时更新多个表\n22.【推荐】统计表中记录数时使用COUNT(*)，而不是COUNT(primary_key)和COUNT(1)\n说明：count( * ) 会统计值为 NULL 的行，而 count( 列名 ) 不会统计此列为 NULL 值的行\n23.【推荐】INSERT语句使用batch提交（INSERT INTO tableVALUES(),(),()……），values的个数不应过多\n24.【推荐】获取大量数据时，建议分批次获取数据，每次获取数据少于2000条，结果集应小于1M\n25.【推荐】在做开发时建议使用数据库框架(如 mybatis) 或 prepared statement，可以提升性能并避免 SQL 注入\n26.【强制】禁止跨库查询（为数据迁移和分库分表留出余地，降低耦合度，降低风险）\n27.【推荐】尽量避免使用子查询，可以把子查询优化为join操作（子查询的结果集无法使用索引，子查询会产生临时表操作，如果子查询数据量大会影响效率，消耗过多的CPU及IO资源）\n28.【强制】超过三个表禁止 join。（需要 join 的字段，数据类型必须绝对一致；多表关联查询时，保证被关联的字段需要有索引。即使双表 join 也要注意表索引、SQL 性能。）\n29.【推荐】SQL 性能优化的目标：至少要达到 range 级别，要求是 ref 级别，如果可以是 consts最好\n30.【推荐】尽量不要使用物理删除（即直接删除，如果要删除的话提前做好备份），而是使用逻辑删除，使用字段delete_flag做逻辑删除，类型为tinyint，0表示未删除，1表示已删除\n31.【强制】在代码中写分页查询逻辑时，若 count 为 0 应直接返回，避免执行后面的分页语句\n32.【强制】程序连接不同的数据库要使用不同的账号\n33.【推荐】使用 ISNULL()来判断是否为 NULL 值﻿\n九、行为规范 1.【强制】禁止使用应用程序配置文件内的帐号手工访问线上数据库\n2.【强制】禁止非DBA对线上数据库进行写操作，修改线上数据需要提交工单，由DBA执行，提交的SQL语句必须经过测试\n3.【强制】禁止在线上做数据库压力测试\n4.【强制】禁止从测试、开发环境直连线上数据库\n5.【强制】禁止在主库进行后台统计操作，避免影响业务，可以在离线从库上执行后台统计﻿\n十、流程规范 1.【强制】所有的建表操作需要提前告知该表涉及的查询sql\n2.【强制】所有的建表需要确定建立哪些索引后才可以建表上线\n3.【强制】所有的改表结构、加索引操作都需要将涉及到所改表的查询sql发出来告知DBA等相关人员\n4.【强制】在建新表加字段之前，要求至少要提前3天邮件出来，给dba们评估、优化和审核的时间\n5.【强制】批量导入、导出数据需要DBA进行审查，并在执行过程中观察服务\n6.【强制】禁止有super权限的应用程序账号存在\n7.【强制】推广活动或上线新功能必须提前通知DBA进行流量评估\n8.【强制】不在业务高峰期批量更新、查询数据库\n9.【强制】隔离线上线下环境（开发测试程序禁止访问线上数据库）\n10.【强制】在对大表做表结构变更时，如修改字段属性会造成锁表，并会造成从库延迟，从而影响线上业务，必须在凌晨后业务低峰期执行，另统一用工具pt-online-schema-change避免锁表且降低延迟执行时间\n11.【强制】核心业务数据库变更需在凌晨执行\n12.【推荐】汇总库开启Audit审计日志功能，出现问题时方可追溯\n13.【强制】给业务方开权限时，密码要用MD5加密，至少16位。权限如没有特殊要求，均为select查询权限，并做库表级限制\n14.【推荐】如果出现业务部门人为误操作导致数据丢失，需要恢复数据，请在第一时间通知DBA，并提供准确时间，误操作语句等重要线索。\n15.【强制】批量更新数据，如update,delete 操作，需要DBA进行审查，并在执行过程中观察服务\n16.【强制】业务部门程序出现bug等影响数据库服务的问题，请及时通知DBA便于维护服务稳定\n17.【强制】线上数据库的变更操作必须提供对应的回滚方案\n18.【强制】批量清洗数据，需要开发和DBA共同进行审查，应避开业务高峰期时段执行，并在执行过程中观察服务状态\n19.【强制】数据订正如删除和修改记录时，要先 select ，确认无误才能执行更新语句，避免出现误删除\n","permalink":"https://ktzxy.top/posts/jkk4t4s90r/","summary":"MySQL设计规约","title":"MySQL设计规约"},{"content":"Go语言中的变量和常量 Go语言中变量的声明 Go语言变量是由字母、数字、下划线组成，其中首个字符不能为数字。Go语言中关键字和保留字都不能用作变量名\nGo语言中变量需要声明后才能使用，同一作用域内不支持重复声明。并且Go语言的变量声明后必须使用。\n变量声明后，没有初始化，打印出来的是空\n如何定义变量 方式1： 1 var name = \u0026#34;zhangsan\u0026#34; 方式2：带类型 1 var name string = \u0026#34;zhangsan\u0026#34; 方式3：类型推导方式定义变量 a在函数内部，可以使用更简略的 := 方式声明并初始化变量\n注意：短变量只能用于声明局部变量，不能用于全局变量声明\n1 变量名 := 表达式 方式4：声明多个变量 类型都是一样的变量\n1 var 变量名称， 变量名称 类型 类型不一样的变量\n1 2 3 4 var ( 变量名称 类型 变量名称 类型 ) 案例\n1 2 3 4 5 var a1, a2 string a1 = \u0026#34;123\u0026#34; a2 = \u0026#34;123\u0026#34; fmt.Printf(a1) fmt.Printf(a2) 总结 全部的定义方式\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package main import \u0026#34;fmt\u0026#34; func main() { fmt.Println(\u0026#34;hello\u0026#34;) fmt.Print(\u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;) fmt.Println() var a = 10 fmt.Printf( \u0026#34;%d\u0026#34;, a ) var name = \u0026#34;zhangsan1\u0026#34; var name2 string = \u0026#34;zhangsan2\u0026#34; name3 := \u0026#34;zhangsan3\u0026#34; fmt.Println(name) fmt.Println(name2) fmt.Println(name3) fmt.Printf(\u0026#34;name1=%v name2=%v name3=%v \\n\u0026#34;, name, name2, name3) } 如何定义常量 相对于变量，常量是恒定不变的值，多用于定义程序运行期间不会改变的那些值。常量的声明和变量声明非常类似，只是把var换成了const，常量在定义的时候必须赋值。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 定义了常量，可以不用立即使用 const pi = 3.14 // 定义两个常量 const( A = \u0026#34;A\u0026#34; B = \u0026#34;B\u0026#34; ) // const同时声明多个常量时，如果省略了值表示和上面一行的值相同 const( A = \u0026#34;A\u0026#34; B C ) Const常量结合iota的使用 iota是golang 语言的常量计数器，只能在常量的表达式中使用\niota在const关键字出现时将被重置为0（const内部的第一行之前），const中每新增一行常量声明将使iota计数一次（iota可理解为const语句块中的行索引）。\n每次const出现，都会让iota初始化为0【自增长】\n1 2 3 4 5 6 const a = iota // a = 0 const ( b = iota // b=0 c // c = 1 d // d = 2 ) const iota使用_跳过某些值\n1 2 3 4 5 const ( b = iota // b=0 _ d // d = 2 ) ","permalink":"https://ktzxy.top/posts/32tsl342w1/","summary":"2 Go的变量","title":"2 Go的变量"},{"content":"http和https http http是一种无状态协议。无状态是指客户机和服务器之间不需要建立持久连接，这意味着当一个客户端向服务器发出请求，然后服务器返回响应（response），连接就被关闭了，在服务器端不保留连接的有关信息，HTTP遵循请求/应答模型。客户机向服务器发送请求，服务器处理请求并返回适当的应答。所有HTTP连接都构成一套请求和应答。\nhttps HTTPS是以安全为目标的HTTP通道，简单将就是HTTP的安全版。即HTTP下加入SSL层，HTTPS的安全基础是SSL。其所用的端口是443，过程大致如下：\n获取连接证书 SSL客户端通过TCP和服务器建立连接后（443端口），并且在一般的TCP连接协商过程中请求证书。即客户端发出一个消息给服务器，这个消息里面包含了自己可实现的算法列表和其它一些需要的消息，SSL的服务器端会回应一个数据包，这里面确定了这次通信所需要的算法，然后服务器向客户端返回证书。（证书里面包含了服务器信息：域名。申请证书的公司，公共密钥）\n证书验证 客户端在收到服务器返回的证书后，判断签发这个证书的公共签发机构，并使用这个机构的公共密钥确认签名是否有效，客户端还会确保证书中列出的域名就是它正在连接的域名\n数据加密和传输 如果确认证书有效，那么生成对称密钥并使用服务器的公共密钥进行加密。然后发送给服务器，服务器使用它的密钥进行解密，这样两台计算机可以开始进行对称加密进行通信。\n对称加密：是指加密和解密用的都是同一个密钥，目前微信小程序采用的就是这个加密方式\n对称加密存在的问题 首先我们知道对称加密是指：加密和解密都使用的同一个密钥，这种方式存在的最大的问题就是密钥发送问题，即如果安全的将密钥发送给对方。\n为什么叫对称加密？ 一方通过密钥将信息加密后，把密文传给另一个方，另一方通过这个相同的密钥将密文解密，转换成可以理解的明文。他们之间的关系如下\n明文 -\u0026gt; 密钥 -\u0026gt; 密文\n但是从上面的图我们可以看出，我们在进行加密后，首先需要将密钥发送给服务器，那么这个过程就可能存在危险的\n非对称加密 上面提到的是对称加密，其实还有一种是非对称加密，非对称加密是通过两个密钥（公钥 - 私钥）来实现对数据的加密和解密的，公钥用于加密，私钥用于解密。\n过程如下：\n首先服务器会颁发一个公钥放在网络中，同时它自己还有一份私钥，然后客户端可以直接获取到对应的公钥\n然后将客户端的数据进行公钥的加密，加密后传输的服务器中，服务器在进行私钥解密，得到最终的数据\n由于非对称加密的方式不需要发送用来解密的私钥，所以可以保证安全性，但是和对称加密比起来，它非常慢，所以我们还是要用对称加密来传送消息，但是对称加密使用的密钥我们通过非对称加密的方式发送出去。这个结果就变成了：\n但是我们需要注意的是，此时交换的两个公钥不一定正确，因为可能会被中间人截获，同时掉包\n例如：中间人虽然不知道小红的私钥是什么，但是在截获了小红的公钥Key1之后，却可以偷天换日，自己另外生成一对公钥私钥，把自己的公钥Key3发送给小灰。\n这一次通信再次被中间人截获，中间人先用自己的私钥解开了Key3的加密，获得Key2，然后再用当初小红发来的Key1重新加密，再发给小红\n证书机制 这个时候我们需要做的就是从指定的机构出获取公钥，而不是任由其在网络传输\n作为服务器端的小红，首先先把自己的公钥给证书颁发机构，向证书颁发机构申请证书 证书颁发机构自己也有一堆公钥和私钥。机构利用自己的私钥来解密Key1，通过服务端网址等信息生成一个证书签名，证书签名同样经过机构的私钥加密。证书制作完成后，机构把证书发送给服务端的小红。 当小灰向小红请求通信的时候，小红不再直接返回自己的公钥，而是把自己申请的证书返回给小灰。 小灰收到证书以后，要做的第一件事就是验证证书的真伪，需要说明的是，各大浏览器和操作系统已经维护了所有权威证书机构的名称和公钥，所以小灰只需要知道是哪个机构颁发的证书，就可以从本地找到对应的机构公钥，解密出证书签名。 参考 https://blog.csdn.net/jiangshangchunjiezi/article/details/88545263\n","permalink":"https://ktzxy.top/posts/ffm1molr8n/","summary":"https和http","title":"https和http"},{"content":"﻿### （1）创建一个有输入参数的存储过程，用于查询指定类别的所有商品信息。并执行该存储过程。\n1 2 3 4 5 6 7 8 9 create procedure proc_displaygoods @category varchar(100) as select G.GoodsNO 商品编号,G.GoodsName 商品名,G.SalePrice 售价,G.InPrice 进价,G.Number 数量,C.CategoryNO 类别编号,C.CategoryName 类别 from Goods G join Category C on G.CategoryNO = C.CategoryNO where CategoryName like @category execute dbo.proc_displaygoods \u0026#39;饼干\u0026#39; （2）创建一个有输入输出参数的存储过程，用于查询指定商品名的售价。并执行该存储过程。 1 2 3 4 5 6 7 8 9 10 11 create procedure proc_findsale @goodsname varchar(100),@price decimal(18,2) output as select @price=SalePrice from Goods where GoodsName = @goodsname go declare @goodsname varchar(100),@price decimal(18,2) set @goodsname = \u0026#39;好吃点\u0026#39; execute dbo.proc_findsale @goodsname,@price output select @goodsname 商品名,@price 售价 go （3）创建一个触发器。向销售表SaleBill中插入一条记录时，这个触发器将更新商品表Goods。Goods 表中数量为原有数量减去销售数量。如果库存数量小于10，则提示\u0026quot;该商品数量小于10，低于安全库存量，请及时进货”；如果原有数量不足，则提示“数量不足！”。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 create trigger tri_updateSaleBill on SaleBill after insert as begin declare @number int,@goodsno varchar(10) select @number = Number,@goodsno = GoodsNO from inserted if(select Number from Goods where GoodsNO = @goodsno) \u0026lt; @number begin print \u0026#39;库存不足\u0026#39; rollback end else begin update Goods set Number = Number - @number where GoodsNO = @goodsno if(select Number from Goods where GoodsNO = @goodsno) \u0026lt; 10 begin print \u0026#39;该商品数量小于10，低于安全库存量，请及时进货\u0026#39; end end end CREATE TRIGGER update_goods_number ON salebill AFTER INSERT AS BEGIN SET NOCOUNT ON; declare @salenumber int declare @storenumber int declare @goodsno varchar(30) select @salenumber=number, @goodsno=goodsno from inserted select @storenumber=number from goods where goodsno=@goodsno if @storenumber\u0026lt;@salenumber begin print \u0026#39;库存数量不足\u0026#39; rollback end else begin update goods set number=number-@salenumber where goodsno=@goodsno select @storenumber=number from goods where goodsno=@goodsno if @storenumber \u0026lt; 10 print \u0026#39;该商品数量小于10，低于安全库存量，请及时进货\u0026#39; end END --添加一条记录，用于验证 insert into supermarket.dbo.SaleBill values(\u0026#39;GN0020\u0026#39;,\u0026#39;S01\u0026#39;,\u0026#39;2018-06-09 00:00:00\u0026#39;,3); ","permalink":"https://ktzxy.top/posts/v3nfoa61qn/","summary":"实验8 存储过程和触发器的创建及应用","title":"实验8 存储过程和触发器的创建及应用"},{"content":"prometheus.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # 全局配置 global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: # 通过node_exporter将监控数据传给prometheus，如果要监控多台服务器，只要在每个服务器上安装node_exporter，指定不同多ip地址就好了 - job_name: \u0026#39;腾讯云服务器监控\u0026#39; static_configs: - targets: [\u0026#39;172.21.0.15:9100\u0026#39;] # 监控mysql - job_name: \u0026#39;MySql实例监控\u0026#39; static_configs: - targets: [\u0026#39;172.21.0.15:9104\u0026#39;] # 监控Docker # - job_name: \u0026#39;腾讯云服务器Docker监控\u0026#39; # file_sd_configs: # - refresh_interval: 1m # files: # - \u0026#34;/var/minio/prometheus/docker_exporter.yml\u0026#34; # - job_name: \u0026#39;腾讯云服务器SpringBoot监控\u0026#39; # metrics_path: \u0026#39;/actuator/prometheus\u0026#39; # static_configs: # - targets: [\u0026#39;172.21.0.15:7081\u0026#39;] #alerting: # alertmanagers: # - static_configs: # - targets: # - 172.21.0.15:9093 #rule_files: # - \u0026#34;/var/minio/prometheus/node_down.yml\u0026#34; # 实例存活报警规则文件 # - \u0026#34;/var/minio/prometheus/memory_over.yml\u0026#34; # 内存报警规则文件 # - \u0026#34;/var/minio/prometheus/cpu_over.yml\u0026#34; # cpu报警规则文件 alertmanager.yml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 global: resolve_timeout: 5m route: receiver: webhook group_wait: 30s group_interval: 5m repeat_interval: 5m group_by: [alertname] routes: - receiver: webhook group_wait: 10s receivers: - name: webhook webhook_configs: - url: http://172.21.0.15:8060/dingtalk/webhook1/send send_resolved: true ","permalink":"https://ktzxy.top/posts/iqp3ylxji6/","summary":"prometheus配置文件","title":"prometheus配置文件"},{"content":"﻿### 1、使用SQL Server Management Studio管理平台完成教材例3-7、例3-8、例3-9、例3-10和例3-11，分别为supermarket数据库创建学生表、商品表、商品种类表、供应商表和销售表。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 create database supermarket go create table Student( SNO varchar(20) primary key, SName varchar(20), BirthYear int, Ssex varchar(2), College varchar(100), Major varchar(100), WeiXin varchar(100) ) create table Category( CategoryNO varchar(20) primary key, CategoryName varchar(100), Description varchar(500) ) create table Supplier( SupplierNO varchar(20) primary key, SupplierName varchar(100), Address varchar(200), Telephone varchar(20) ) create table Goods( GoodsNO varchar(20) primary key, SupplierNO varchar(20), CategoryNO varchar(20), GoodsName varchar(100), InPrice decimal(18,2), SalePrice decimal(18,2), Number int, ProductTime smalldatetime, QGPeriod tinyint, foreign key(CategoryNO) references Category (CategoryNO), foreign key (SupplierNO) references Supplier (SupplierNO) ) create table SaleBill( GoodsNO varchar(20), SNO varchar(20), HappenTime datetime, Number int, primary key (GoodsNO,SNO), foreign key (GoodsNO) references Goods (GoodsNO), foreign key (SNO) references Student (SNO) ) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 use supermarket go insert into supermarket.dbo.Student values(\u0026#39;S01\u0026#39;,\u0026#39;李明\u0026#39;,2001,\u0026#39;男\u0026#39;,\u0026#39;CS\u0026#39;,\u0026#39;IT\u0026#39;,\u0026#39;wx001\u0026#39;); insert into supermarket.dbo.Student values(\u0026#39;S02\u0026#39;,\u0026#39;徐好\u0026#39;,2000,\u0026#39;女\u0026#39;,\u0026#39;CS\u0026#39;,\u0026#39;IT\u0026#39;,\u0026#39;wx002\u0026#39;); insert into supermarket.dbo.Student values(\u0026#39;S03\u0026#39;,\u0026#39;伍民\u0026#39;,1998,\u0026#39;男\u0026#39;,\u0026#39;CS\u0026#39;,\u0026#39;MIS\u0026#39;,\u0026#39;wx003\u0026#39;); insert into supermarket.dbo.Student values(\u0026#39;S04\u0026#39;,\u0026#39;闵红\u0026#39;,1999,\u0026#39;女\u0026#39;,\u0026#39;ACC\u0026#39;,\u0026#39;IT\u0026#39;,\u0026#39;wx004\u0026#39;); insert into supermarket.dbo.Student values(\u0026#39;S05\u0026#39;,\u0026#39;张小红\u0026#39;,1999,\u0026#39;女\u0026#39;,\u0026#39;ACC\u0026#39;,\u0026#39;IT\u0026#39;,\u0026#39;wx005\u0026#39;); insert into supermarket.dbo.Student values(\u0026#39;S06\u0026#39;,\u0026#39;张舒\u0026#39;,2001,\u0026#39;男\u0026#39;,\u0026#39;CS\u0026#39;,\u0026#39;MIS\u0026#39;,\u0026#39;wx006\u0026#39;); insert into supermarket.dbo.Student values(\u0026#39;S07\u0026#39;,\u0026#39;王民为\u0026#39;,1999,\u0026#39;男\u0026#39;,\u0026#39;CS\u0026#39;,\u0026#39;MIS\u0026#39;,\u0026#39;wx007\u0026#39;); insert into supermarket.dbo.Student values(\u0026#39;S08\u0026#39;,\u0026#39;李士任\u0026#39;,2001,\u0026#39;男\u0026#39;,\u0026#39;ACC\u0026#39;,\u0026#39;IT\u0026#39;,\u0026#39;wx008\u0026#39;); insert into supermarket.dbo.Category values(\u0026#39;CN001\u0026#39;,\u0026#39;咖啡\u0026#39;,\u0026#39;速溶咖啡、咖啡粉、罐装咖啡\u0026#39;); insert into supermarket.dbo.Category values(\u0026#39;CN002\u0026#39;,\u0026#39;洗发水\u0026#39;,\u0026#39;袋装、瓶装洗发水\u0026#39;); insert into supermarket.dbo.Category values(\u0026#39;CN003\u0026#39;,\u0026#39;方便面\u0026#39;,\u0026#39;袋装、碗装方便面\u0026#39;); insert into supermarket.dbo.Supplier values(\u0026#39;Sup001\u0026#39;,\u0026#39;卡夫食品（中国）有限公司广州分公司\u0026#39;,\u0026#39;广州佛山\u0026#39;,\u0026#39;12348768900\u0026#39;); insert into supermarket.dbo.Supplier values(\u0026#39;Sup002\u0026#39;,\u0026#39;东菀市南城久润食品贸易部\u0026#39;,\u0026#39;广州东菀\u0026#39;,\u0026#39;13248768901\u0026#39;); insert into supermarket.dbo.Supplier values(\u0026#39;Sup003\u0026#39;,\u0026#39;重庆飞鹤食品贸易公司\u0026#39;,\u0026#39;重庆解放碑\u0026#39;,\u0026#39;12648768901\u0026#39;); insert into supermarket.dbo.Supplier values(\u0026#39;Sup004\u0026#39;,\u0026#39;重庆南山日化品贸易公司\u0026#39;,\u0026#39;重庆南坪\u0026#39;,\u0026#39;11648768901\u0026#39;); insert into supermarket.dbo.Supplier values(\u0026#39;Sup005\u0026#39;,\u0026#39;重庆缙云日化品贸易公司\u0026#39;,\u0026#39;重庆北碚\u0026#39;,\u0026#39;19648768903\u0026#39;); insert into supermarket.dbo.Goods values(\u0026#39;GN0001\u0026#39;,\u0026#39;Sup001\u0026#39;,\u0026#39;CN001\u0026#39;,\u0026#39;麦氏威尔冰咖啡\u0026#39;,5.79,7.80,20,\u0026#39;2016-02-08 00:00:00\u0026#39;,18); insert into supermarket.dbo.Goods values(\u0026#39;GN0002\u0026#39;,\u0026#39;Sup002\u0026#39;,\u0026#39;CN001\u0026#39;,\u0026#39;捷荣三合一咖啡\u0026#39;,12.30,17.30,15,\u0026#39;2017-10-08 00:00:00\u0026#39;,18); insert into supermarket.dbo.Goods values(\u0026#39;GN0003\u0026#39;,\u0026#39;Sup002\u0026#39;,\u0026#39;CN001\u0026#39;,\u0026#39;力神咖啡\u0026#39;,1.81,2.70,30,\u0026#39;2018-05-06 00:00:00\u0026#39;,18); insert into supermarket.dbo.Goods values(\u0026#39;GN0004\u0026#39;,\u0026#39;Sup001\u0026#39;,\u0026#39;CN001\u0026#39;,\u0026#39;麦氏威尔小三合一咖啡\u0026#39;,8.12,10.80,20,\u0026#39;2017-05-06 00:00:00\u0026#39;,18); insert into supermarket.dbo.Goods values(\u0026#39;GN0005\u0026#39;,\u0026#39;Sup003\u0026#39;,\u0026#39;CN001\u0026#39;,\u0026#39;雀巢香滑咖啡饮料\u0026#39;,1.99,2.70,3,\u0026#39;2018-01-01 00:00:00\u0026#39;,18); insert into supermarket.dbo.Goods values(\u0026#39;GN0006\u0026#39;,\u0026#39;Sup003\u0026#39;,\u0026#39;CN001\u0026#39;,\u0026#39;雀巢听装咖啡\u0026#39;,84.21,113.70,6,\u0026#39;2018-05-06 00:00:00\u0026#39;,18); insert into supermarket.dbo.Goods values(\u0026#39;GN0007\u0026#39;,\u0026#39;Sup004\u0026#39;,\u0026#39;CN002\u0026#39;,\u0026#39;夏士莲丝质柔顺洗发水\u0026#39;,25.85,35.70,30,\u0026#39;2018-03-08 00:00:00\u0026#39;,36); insert into supermarket.dbo.Goods values(\u0026#39;GN0008\u0026#39;,\u0026#39;Sup005\u0026#39;,\u0026#39;CN002\u0026#39;,\u0026#39;飞逸清新爽节洗发水\u0026#39;,20.47,30.00,50,\u0026#39;2018-03-09 00:00:00\u0026#39;,36); insert into supermarket.dbo.Goods values(\u0026#39;GN0009\u0026#39;,\u0026#39;Sup005\u0026#39;,\u0026#39;CN002\u0026#39;,\u0026#39;力士柔亮洗发水（中/干）\u0026#39;,22.65,32.30,20,\u0026#39;2017-12-08 00:00:00\u0026#39;,36); insert into supermarket.dbo.Goods values(\u0026#39;GN0010\u0026#39;,\u0026#39;Sup005\u0026#39;,\u0026#39;CN002\u0026#39;,\u0026#39;风影去屑洗发水（清爽）\u0026#39;,22.98,34.20,6,\u0026#39;2017-10-07 00:00:00\u0026#39;,36); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0001\u0026#39;,\u0026#39;S01\u0026#39;,\u0026#39;2018-06-09 00:00:00\u0026#39;,3); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0001\u0026#39;,\u0026#39;S02\u0026#39;,\u0026#39;2018-05-03 00:00:00\u0026#39;,1); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0001\u0026#39;,\u0026#39;S03\u0026#39;,\u0026#39;2018-04-07 00:00:00\u0026#39;,1); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0001\u0026#39;,\u0026#39;S06\u0026#39;,\u0026#39;2018-06-12 00:00:00\u0026#39;,2); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0002\u0026#39;,\u0026#39;S02\u0026#39;,\u0026#39;2018-05-08 00:00:00\u0026#39;,2); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0002\u0026#39;,\u0026#39;S05\u0026#39;,\u0026#39;2018-06-26 00:00:00\u0026#39;,3); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0002\u0026#39;,\u0026#39;S06\u0026#39;,\u0026#39;2018-06-16 00:00:00\u0026#39;,2); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0003\u0026#39;,\u0026#39;S01\u0026#39;,\u0026#39;2018-07-10 00:00:00\u0026#39;,2); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0003\u0026#39;,\u0026#39;S02\u0026#39;,\u0026#39;2018-07-08 00:00:00\u0026#39;,2); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0003\u0026#39;,\u0026#39;S05\u0026#39;,\u0026#39;2018-06-01 00:00:00\u0026#39;,2); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0003\u0026#39;,\u0026#39;S06\u0026#39;,\u0026#39;2018-07-01 00:00:00\u0026#39;,2); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0005\u0026#39;,\u0026#39;S05\u0026#39;,\u0026#39;2018-06-11 00:00:00\u0026#39;,1); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0006\u0026#39;,\u0026#39;S03\u0026#39;,\u0026#39;2018-05-07 00:00:00\u0026#39;,1); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0007\u0026#39;,\u0026#39;S01\u0026#39;,\u0026#39;2018-06-09 00:00:00\u0026#39;,1); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0007\u0026#39;,\u0026#39;S04\u0026#39;,\u0026#39;2018-06-08 00:00:00\u0026#39;,1); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0007\u0026#39;,\u0026#39;S05\u0026#39;,\u0026#39;2018-06-09 00:00:00\u0026#39;,1); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0008\u0026#39;,\u0026#39;S02\u0026#39;,\u0026#39;2018-06-04 00:00:00\u0026#39;,1); insert into supermarket.dbo.SaleBill values(\u0026#39;GN0008\u0026#39;,\u0026#39;S06\u0026#39;,\u0026#39;2018-06-28 00:00:00\u0026#39;,1); 2、使用SQL Server Management Studio管理平台完成教材例3-12、例3-13、例3-14，完成对supermarket数据库中已有数据库表的修改与删除。 1 2 3 4 5 alter table Category add Cat_CategoryNO varchar(20) alter table Goods drop column Barcode alter table Supplier alter column SupplierName nvarchar(200) not null; 3、使用SQL Server Management Studio管理平台为sjkDB数据库创建数据库表sjktable（表结构自行设计），然后完成例3-15的要求。 1 drop table sjktable 4、使用SQL语句，为Students数据库创建数据库表Student、Course、Sc，表结构如教材表3-7~表3-9所示。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 use students go create table Student( Sno char(7) primary key, Sname nchar(5) not null, Sex nchar(1), Sage tinyint, Sdept nvarchar(20) ) create table Course( Cno char(6) primary key, Cname nvarchar(20) not null, Ccredit tinyint, Semester tinyint ) create table Sc( Sno char(7), Cno char(6), Grade tinyint, primary key(Sno,Cno) ) 5、使用SQL语句，完成对Students数据库中数据库表结构的修改： （1）为表Student添加地址列Address，数据类型为NVARCHAR(50)。\n1 alter table Student add Address nvarchar(50) （2）将地址列数据类型修改为NVARCHAR(30)。\n1 alter table Student alter column Address nvarchar(30) （3）删除地址列。\n1 alter table Student drop column Address 6、在数据库sjkDB上完成，使用教材P48页的SQL语句创建教材的表3-3和表3-4所示的员工表和薪资表，然后使用SQL Server Management Studio管理平台和SQL语句依次完成例3-25、例3-26和例3-27的操作。 1 2 3 4 5 create unique nonclustered index index_xinz on salary(Saname asc) create unique index index_yfsf on salary(Sapayabl asc,Psalary desc) drop index salary.index_xinz,salary.index_yfsf 7、在数据库supermarket上，使用SQL语句完成下列操作。 （1）为表supplier的字段SupplierName创建一个非聚集、唯一索引。\n1 2 3 use supermarket go create unique nonclustered index index_supplier on supplier(SupplierName) （2）使用系统存储过程sp_helpindex查看表Supplier的索引情况，如果已有主码，能否为其再建立一个聚集索引？为什么？\n1 2 聚集索引是通过设置主码来完成的，每个表的主码都是聚集索引，一个表只能有一个聚集索引， 非聚集索引可以有多个。因此无法重复创建聚焦索引 （3）删除第（1）题中所建立的索引。\n1 drop index supplier.index_supplier ","permalink":"https://ktzxy.top/posts/rk5mm0ex49/","summary":"实验2 SQL的数据定义功能","title":"实验2 SQL的数据定义功能"},{"content":"Golang map详解 map的介绍 map是一种无序的基于key-value的数据结构，Go语言中的map是引用类型，必须初始化才能使用。\nGo语言中map的定义语法如下：\n1 map[KeyType]ValueType 其中：\nKeyType：表示键的类型 ValueType：表示键对应的值的类型 map类型的变量默认初始值为nil，需要使用make()函数来分配内存。语法为：\nmake：用于slice、map和channel的初始化\n示例如下所示：\n1 2 3 4 5 6 7 // 方式1初始化 var userInfo = make(map[string]string) userInfo[\u0026#34;userName\u0026#34;] = \u0026#34;zhangsan\u0026#34; userInfo[\u0026#34;age\u0026#34;] = \u0026#34;20\u0026#34; userInfo[\u0026#34;sex\u0026#34;] = \u0026#34;男\u0026#34; fmt.Println(userInfo) fmt.Println(userInfo[\u0026#34;userName\u0026#34;]) 1 2 3 4 5 6 7 // 创建方式2，map也支持声明的时候填充元素 var userInfo2 = map[string]string { \u0026#34;username\u0026#34;:\u0026#34;张三\u0026#34;, \u0026#34;age\u0026#34;:\u0026#34;21\u0026#34;, \u0026#34;sex\u0026#34;:\u0026#34;女\u0026#34;, } fmt.Println(userInfo2) 遍历map 使用for range遍历\n1 2 3 4 // 遍历map for key, value := range userInfo2 { fmt.Println(\u0026#34;key:\u0026#34;, key, \u0026#34; value:\u0026#34;, value) } 判断map中某个键值是否存在 我们在获取map的时候，会返回两个值，也可以是返回的结果，一个是是否有该元素\n1 2 3 // 判断是否存在,如果存在 ok = true，否则 ok = false value, ok := userInfo2[\u0026#34;username2\u0026#34;] fmt.Println(value, ok) 使用delete()函数删除键值对 使用delete()内建函数从map中删除一组键值对，delete函数的格式如下所示\n1 delete(map 对象, key) 其中：\nmap对象：表示要删除键值对的map对象 key：表示要删除的键值对的键 示例代码如下\n1 2 3 // 删除map数据里面的key，以及对应的值 delete(userInfo2, \u0026#34;sex\u0026#34;) fmt.Println(userInfo2) 元素为map类型的切片 我们想要在切片里面存放一系列用户的信息，这时候我们就可以定义一个元素为map类型的切片\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 // 切片在中存放map var userInfoList = make([]map[string]string, 3, 3) var user = map[string]string{ \u0026#34;userName\u0026#34;: \u0026#34;张安\u0026#34;, \u0026#34;age\u0026#34;: \u0026#34;15\u0026#34;, } var user2 = map[string]string{ \u0026#34;userName\u0026#34;: \u0026#34;张2\u0026#34;, \u0026#34;age\u0026#34;: \u0026#34;15\u0026#34;, } var user3 = map[string]string{ \u0026#34;userName\u0026#34;: \u0026#34;张3\u0026#34;, \u0026#34;age\u0026#34;: \u0026#34;15\u0026#34;, } userInfoList[0] = user userInfoList[1] = user2 userInfoList[2] = user3 fmt.Println(userInfoList) for _, item := range userInfoList { fmt.Println(item) } 值为切片类型的map 我们可以在map中存储切片\n1 2 3 4 // 将map类型的值 var userinfo = make(map[string][]string) userinfo[\u0026#34;hobby\u0026#34;] = []string {\u0026#34;吃饭\u0026#34;, \u0026#34;睡觉\u0026#34;, \u0026#34;敲代码\u0026#34;} fmt.Println(userinfo) 示例 统计字符串中单词出现的次数\n1 2 3 4 5 6 7 8 9 // 写一个程序，统计一个字符串中每个单词出现的次数。比如 \u0026#34;how do you do\u0026#34; var str = \u0026#34;how do you do\u0026#34; array := strings.Split(str, \u0026#34; \u0026#34;) fmt.Println(array) countMap := make(map[string]int) for _, item := range array { countMap[item]++ } fmt.Println(countMap) ","permalink":"https://ktzxy.top/posts/q9wnhajy9l/","summary":"8 Go的map","title":"8 Go的map"},{"content":"[TOC]\n1、Too many connections（连接数过多，导致连接不上数据库，业务无法正常进行） 问题还原：\n1 2 3 4 5 6 7 8 mysql\u0026gt; show variables like \u0026#39;%max_connection%\u0026#39;; | Variable_name | Value | | max_connections | 151 | mysql\u0026gt; set global max_connections=1;Query OK, 0 rows affected (0.00 sec) [root@node4 ~]# mysql -uzs -p123456 -h 192.168.56.132 ERROR 1040 (00000): Too many connections 解决问题的思路：\n首先先要考虑在我们 MySQL 数据库参数文件里面，对应的max_connections 这个参数值是不是设置的太小了，导致客户端连接数超过了数据库所承受的最大值。 ● 该值默认大小是151，我们可以根据实际情况进行调整。 ● 对应解决办法：set global max_connections=500 但这样调整会有隐患，因为我们无法确认数据库是否可以承担这么大的连接压力，就好比原来一个人只能吃一个馒头，但现在却非要让他吃 10 个，他肯定接受不了。反应到服务器上面，就有可能会出现宕机的可能。 所以这又反应出了，我们在新上线一个业务系统的时候，要做好压力测试。保证后期对数据库进行优化调整。 其次可以限制Innodb 的并发处理数量，如果 innodb_thread_concurrency = 0（这种代表不受限制） 可以先改成 16或是64 看服务器压力。如果非常大，可以先改的小一点让服务器的压力下来之后,然后再慢慢增大,根据自己的业务而定。个人建议可以先调整为 16 即可。 MySQL 随着连接数的增加性能是会下降的，可以让开发配合设置 thread pool，连接复用。在MySQL商业版中加入了thread pool这项功能,另外对于有的监控程序会读取 information_schema 下面的表，可以考虑关闭下面的参数 1 innodb_stats_on_metadata=0set global innodb_stats_on_metadata=0 1.2 法二 查看从这次 mysql 服务启动到现在，同一时刻并行连接数的最大值： show status like 'Max_used_connections';\n更改最大连接数只能从表面上解决问题，随着我们开发人员的增多，Sleep 连接也会更多，到时候万一又达到了 1000 的上限，难道我们又得改成 10000 吗？这显然是非常不可取的。所以杀掉多余的 Sleep 连接。\n杀掉Sleep连接 我们可以通过 show processlist 命令来查看当前的所有连接状态。\nMysql 数据库有一个属性 wait_timeout 就是 sleep 连接最大存活时间，默认是 28800 s，换算成小时就是 8 小时。 执行命令：\n1 show global variables like \u0026#39;%wait_timeout\u0026#39;; 将他修改成一个合适的值，这里我改成了 250s。当然也可以在配置文件中修改，添加 wait_timeout = 250。这个值可以根据项目的需要进行修改，以 s 为单位。我在这里结合 navicat 的超时请求机制配置了 240s。 执行命令：\n1 set global wait_timeout=250; 这样，就能从根本上解决 Too Many Connections 的问题了\n2、数据库密码忘记的问题 1 2 3 4 5 6 [root@zs ~]# mysql -uroot -p Enter password: ERROR 1045 (28000): Access denied for user \u0026#39;root\u0026#39;@\u0026#39;localhost\u0026#39; (using password: YES) [root@zs ~]# mysql -uroot -p Enter password: ERROR 1045 (28000): Access denied for user \u0026#39;root\u0026#39;@\u0026#39;localhost\u0026#39; (using password: YES) 我们有可能刚刚接手别人的 MySQL 数据库，而且没有完善的交接文档。root 密码可以丢失或者忘记了。\n解决思路： 目前是进入不了数据库的情况，所以我们要考虑是不是可以跳过权限。因为在数据库中，mysql数据库中user表记录着我们用户的信息。\n解决方法： 启动 MySQL 数据库的过程中，可以这样执行：\n1 /usr/local/mysql/bin/mysqld_safe --defaults-file=/etc/my.cnf --skip-grant-tables \u0026amp; 这样启动，就可以不用输入密码，直接进入 mysql 数据库了。然后在修改你自己想要改的root密码即可。\n1 update mysql.user set password=password(\u0026#39;root123\u0026#39;) where user=\u0026#39;root\u0026#39;; 3、Mysql乱码问题 3.1 Mysql乱码问题 博客园：StrongerW：MySQL乱码问题(为什么？追根溯源)\nwin下my.ini、Linux下my.cnf：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # Win下 my.ini 有的默认被注释掉，只需要去掉注释就可以 #在[client]下追加： default-character-set=utf8 #在[mysqld]下追加： character-set-server=utf8 #在[mysql]下追加： default-character-set=utf8 # Linux下，这里就有所不同，每个人当初安装MySQL的方式,添加的my.cnf #是否是用的官网模板还是网上复制的内容填充的，但是方式要添加的内容和win #大同小异，如果当初指定了相应的默认字符集就无需指定字符集。 #【注】无论是my.ini还是my.cnf里面的mysql相关的配置项一定要在所属的组下面， 比如default-character-set就只能放在[mysql]/[client],不能放在[mysqld]下， 不然会导致mysql服务启动失败，可能如下： #[Error]start Starting MySQL .. The server quit without updating PID file # 所以说mysql服务起不来了，可能是配置文件出现了问题 最关键的一项是[mysqld] character-set-server=utf8，其它两项，对于my.cnf只需要追加[mysql] character-set-server=utf8就可以改变 character_set_client、character_set_connection、character_set_results这三项的值.\ncharacter_set_client/connection/results变量 3.2 数据库总会出现中文乱码的情况 解决思路： 对于中文乱码的情况，记住老师告诉你的三个统一就可以。还要知道在目前的mysql数据库中字符集编码都是默认的UTF8\n处理办法：\n数据终端，也就是我们连接数据库的工具设置为 utf8 操作系统层面；可以通过 cat /etc/sysconfig/i18n 查看；也要设置为 utf8 数据库层面；在参数文件中的 mysqld 下，加入 character-set-server=utf8。 Emoji 表情符号录入 mysql 数据库中报错。 1 2 3 4 5 6 7 8 9 Caused by: java.sql.SQLException: Incorrect string value: \u0026#39;\\xF0\\x9F\\x98\\x97\\xF0\\x9F...\u0026#39; for column \u0026#39;CONTENT\u0026#39; at row 1 at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:1074) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4096) at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4028) at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2490) at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2651) at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2734) at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:2155) at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1379) 解决思路：针对表情插入的问题，一定还是字符集的问题。 处理方法：我们可以直接在参数文件中，加入\n1 2 3 4 vim /etc/my.cnf [mysqld] init-connect=\u0026#39;SET NAMES utf8mb4\u0026#39; character-set-server=utf8mb4 注：utf8mb4 是 utf8 的超集。\n4、MySQL 数据库连接超时的报错 1 2 3 4 5 6 7 8 org.hibernate.util.JDBCExceptionReporter - SQL Error:0, SQLState: 08S01 org.hibernate.util.JDBCExceptionReporter - The last packet successfully received from the server was43200 milliseconds ago.The last packet sent successfully to the server was 43200 milliseconds ago, which is longer than the server configured value of \u0026#39;wait_timeout\u0026#39;. You should consider either expiring and/or testing connection validity before use in your application, increasing the server configured values for client timeouts, or using the Connector/J connection \u0026#39;autoReconnect=true\u0026#39; to avoid this problem. org.hibernate.event.def.AbstractFlushingEventListener - Could not synchronize database state with session org.hibernate.exception.JDBCConnectionException: Could not execute JDBC batch update com.mysql.jdbc.exceptions.jdbc4.MySQLNonTransientConnectionException: Connection.close() has already been called. Invalid operation in this state. org.hibernate.util.JDBCExceptionReporter - SQL Error:0, SQLState: 08003 org.hibernate.util.JDBCExceptionReporter - No operations allowed after connection closed. Connection was implicitly closed due to underlying exception/error: ** BEGIN NESTED EXCEPTION ** 大多数做 DBA 的同学，可能都会被开发人员告知，你们的数据库报了这个错误了。赶紧看看是哪里的问题。\n这个问题是由两个参数影响的，wait_timeout 和 interactive_timeout。数据默认的配置时间是28800（8小时）意味着，超过这个时间之后，MySQL 数据库为了节省资源，就会在数据库端断开这个连接，Mysql服务器端将其断开了，但是我们的程序再次使用这个连接时没有做任何判断，所以就挂了。\n解决思路： 先要了解这两个参数的特性；这两个参数必须同时设置，而且必须要保证值一致才可以。 我们可以适当加大这个值，8小时太长了，不适用于生产环境。因为一个连接长时间不工作，还占用我们的连接数，会消耗我们的系统资源。\n解决方法： 可以适当在程序中做判断；强烈建议在操作结束时更改应用程序逻辑以正确关闭连接；然后设置一个比较合理的timeout的值（根据业务情况来判断）\n5、MySql设置InnoDb级别和密码 1 2 3 4 5 [mysqld] # Mysql innodb级别 innodb_force_recovery = 6 # 设置免密登录 skip-grant-tables ","permalink":"https://ktzxy.top/posts/jjz0oze62m/","summary":"mysq错误l经典记录","title":"mysq错误l经典记录"},{"content":" MySQL性能优化的方法有很多，预编译是使用比较多，效果比较好的一种方法。本文将简单介绍什么是MySQL预编译语句，如何使用预编译语句，以及使用预编译语句在性能上能带来多少提高。\n什么是预编译语句？ MySQL在执行SQL语句时，分为几个阶段。\n词法解析、语义解析。 优化SQL语句，生成执行计划。 执行SQL并返回结果。 很多情况下，同一类SQL会反复执行，只是SQL中的某些具体的值不同，本质上属于同一类SQL。比如select中的where条件值不同，insert中的values值不同，update中的set值不同等等。这些SQL只要解析一次，以后再次执行时，就不再需要进行词法解析、语义解析、SQL语句优化、生成执行计划，这些过程都不再需要，只要告诉MySQL具体的参数值变了就行。通常我们称这类SQL为Prepared Statements。\n使用预编译语句，同一类SQL，只要一次编译，多次运行，省去了不必要的重复的解析优化过程，大大提高了SQL语句执行性能，另外预编译功能也能防止SQL注入。\n如何使用预编译语句？ 通过prepare关键字来编译一个SQL语句，通过execute关键字来执行一个具体的SQL，通过deallocate关键字来释放一个预编译SQL语句。示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 mysql\u0026gt; prepare sel from \u0026#39;select * from t where c1=? and c2=?\u0026#39;; mysql\u0026gt; set @a=\u0026#39;100\u0026#39;,@b=\u0026#39;200\u0026#39;; mysql\u0026gt; execute sel using @a,@b; +------+------+------+---------------------+ | id | c1 | c2 | ts | +------+------+------+---------------------+ | 3275 | 100 | 200 | 2020-01-18 11:53:39 | | 3277 | 100 | 200 | 2020-01-18 11:53:39 | +------+------+------+---------------------+ 2 rows in set (0.01 sec) mysql\u0026gt; set @a=\u0026#39;400\u0026#39;,@b=\u0026#39;400\u0026#39;; mysql\u0026gt; execute sel using @a,@b; +------+------+------+---------------------+ | id | c1 | c2 | ts | +------+------+------+---------------------+ | 3279 | 400 | 400 | 2020-01-19 15:10:09 | +------+------+------+---------------------+ 1 row in set (0.00 sec) mysql\u0026gt; deallocate prepare sel; Query OK, 0 rows affected (0.00 sec) 除了直接在mysql命令行中使用预编译语句，也可以在JDBC中使用预编译功能。代码如下：\n1 2 3 4 5 String sql = \u0026#34;insert into t select ?,?\u0026#34;; PreparedStatement statement = con.prepareStatement(sql); statement.setString(1, \u0026#34;abc\u0026#34;); statement.setString(2, \u0026#34;abc\u0026#34;); statement.executeUpdate(); 预编译语句性能有多少提高？ 使用预编译语句：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 -------------------QPS--TPS----------- time | ins upd del sel iud| 15:37:55| 1837 0 0 1 1837| 15:37:56| 1916 0 0 1 1916| 15:37:57| 1818 0 0 1 1818| 15:37:58| 1603 0 0 1 1603| 15:37:59| 1784 0 0 1 1784| 15:38:00| 1751 0 0 1 1751| 15:38:01| 1756 0 0 1 1756| 15:38:02| 1502 0 0 1 1502| 15:38:03| 2088 0 0 1 2088| 15:38:04| 1886 0 0 2 1886| 15:38:05| 1986 0 0 1 1986| 15:38:06| 1593 0 0 1 1593| 15:38:07| 1003 0 0 1 1003| 15:38:08| 1561 0 0 1 1561| 15:38:09| 2024 0 0 1 2024| 不使用预编译语句：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 -------------------QPS--TPS----------- time | ins upd del sel iud| 15:49:08| 1557 0 0 1 1557| 15:49:09| 1583 0 0 2 1583| 15:49:10| 1593 0 0 1 1593| 15:49:11| 1677 0 0 1 1677| 15:49:12| 1841 0 0 1 1841| 15:49:13| 1568 0 0 1 1568| 15:49:14| 1692 0 0 1 1692| 15:49:15| 1631 0 0 1 1631| 15:49:16| 1774 0 0 1 1774| 15:49:17| 1642 0 0 1 1642| 15:49:18| 1748 0 0 1 1748| 15:49:19| 1520 0 0 2 1520| 15:49:20| 1666 0 0 1 1666| 15:49:21| 1657 0 0 1 1657| 15:49:22| 1659 0 0 1 1659| 使用预编译平均TPS：1740 不使用预编译平均TPS：1653\n测试结果：使用预编译SQL，性能提高约5.2%\n注： 测试使用的MySQL版本为5.7.19，1核1G，并发10线程。\n","permalink":"https://ktzxy.top/posts/072436zoq6/","summary":"MySQL性能优化 预编译语句","title":"MySQL性能优化 预编译语句"},{"content":"﻿### 1、完成教材例3-70~例3-76的操作。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 insert into Student values(\u0026#39;S09\u0026#39;,\u0026#39;程浩\u0026#39;,1999,\u0026#39;男\u0026#39;,\u0026#39;CS\u0026#39;,\u0026#39;IT\u0026#39;,\u0026#39;wx009\u0026#39;); use supermarket; create table SubGoods( GoodName varchar(100), Number int ) insert into SubGoods select GoodsName,G.Number from Goods G left join SaleBill SA on G.GoodsNO = SA.GoodsNO where SA.SNO is null update Goods set Number = Number + 2 update Goods set Number = 0 where DATEDIFF(DAY,ProductTime,GETDATE()) - QGPeriod * 30 \u0026gt;0 update Goods set SalePrice = SalePrice * 1.1 from Supplier S join Goods G on S.SupplierNO = G.SupplierNO where SupplierName = \u0026#39;重庆缙云日化品贸易公司\u0026#39; delete SubGoods delete from Goods from Supplier S join Goods G on S.SupplierNO = G.SupplierNO where SupplierName = \u0026#39;重庆缙云日化品贸易公司\u0026#39; 2、在数据库supermarket上完成下列操作。 （1） 添加新品“GN0011 Sup002 CN001 乐至三合一咖啡 12. 30 17. 30 100 2018-11-12 18”。\n1 insert into Goods values(\u0026#39;GN0011\u0026#39;,\u0026#39;Sup002\u0026#39;,\u0026#39;CN001\u0026#39;,\u0026#39;乐至三合一咖啡\u0026#39;,12.30,17.30,100,\u0026#39;2018-11-12 00:00:00\u0026#39;,18); （2）先建立一张新表，使用子查询将各月的销售额插入该表，存储月份及销售额。\n1 2 3 4 5 6 7 8 9 10 11 12 if exists(select name from sysobjects where name=\u0026#39;sale_report\u0026#39;) drop table sale_report create table sale_report( 月份 char(7), 销售额 decimal(18,2) ) insert into sale_report select convert(char(7),HappenTime,120), sum(s.number*g.SalePrice) /*子查询,获取月份及销售额,结果集作为values被插入到目标表*/ from SaleBill s, Goods g where s.GoodsNO=g.GoodsNO group by convert(char(7),HappenTime,120) go （3） 使用子查询将各学生的购买额插入新表，由系统自建新表，存储学生学号、姓名、销售额。\n1 2 3 4 5 6 7 8 9 10 11 if exists(select * from sysobjects where name=\u0026#39;sale_report_stu\u0026#39;) drop table sale_report_stu select s.sno 学号, s.SName 姓名, sa.amount 销售额 into stu_sale_report from student s join (select student.sno sno, sum(salebill.number*goods.saleprice) amount /* 子查询,获取学号及其销售额,结果作为派生表*/ from SaleBill,Student,Goods where SaleBill.SNO=Student.SNO and SaleBill.GoodsNO=Goods.GoodsNO group by Student.SNO ) sa /*为了方便他处引用派生表的字段,为派生表指定别名*/ on s.sno=sa.sno go （4）将所有商品存量增加2。\n1 update supermarket.dbo.Goods set Number = Number + 2 （5）将保质期还有30天的商品价格打8折。\n1 2 update goods set saleprice=saleprice*0.8 where QGPeriod*30-datediff(day,producttime,getdate())\u0026lt;=30 （6）分别使用子查询方式与连接方式将广州地区供货商的商品加价10%。\n1 2 3 4 5 6 update Goods set SalePrice = SalePrice * 1.1 from Supplier S join Goods G on S.SupplierNO = G.SupplierNO where Address like \u0026#39;广州%\u0026#39; update Goods set SalePrice = SalePrice * 1.1 where SupplierNO in(select SupplierNO from Supplier where Address like \u0026#39;广州%\u0026#39;) （7）将销售额后两位的商品下架。\n1 2 3 4 5 6 7 8 9 alter table SaleBill nocheck constraint all; delete from Goods where GoodsNO in( select GoodsNO from ( select top 2 G.GoodsNO,SUM(SA.Number * G.SalePrice) GOODSUM from Goods G join SaleBill SA on G.GoodsNO = SA.GoodsNO group by G.GoodsNO order by GOODSUM) as s) alter table SaleBill check constraint all; （8）删除销售额最小的供应商信息。\n1 2 3 4 5 6 7 8 9 10 11 alter table Goods nocheck constraint all; delete from Supplier where SupplierNO in( select * from ( select top 1 S.SupplierNO from Supplier S join Goods G on S.SupplierNO = G.SupplierNO join SaleBill SA on G.GoodsNO = SA.GoodsNO group by S.SupplierNO order by SUM(SalePrice * SA.Number) ) as a) alter table Goods check constraint all; ","permalink":"https://ktzxy.top/posts/v6ygzp6ydl/","summary":"实验5 SQL的数据操作功能","title":"实验5 SQL的数据操作功能"},{"content":"1. Java 概述 一个 Java 程序可以认为是一系列对象的集合，而这些对象通过调用彼此的方法来协同工作。一个基础的程序涉及如下的概念：\n对象：对象是类的一个实例，有状态和行为。例如，一条狗是一个对象，它的状态有：颜色、名字、品种；行为有：摇尾巴、叫、吃等。 类：类是一个模板，它描述一类对象的行为和状态。 方法：方法就是行为，一个类可以有很多方法。逻辑运算、数据修改以及所有动作都是在方法中完成的。 实例变量：每个对象都有独特的实例变量，对象的状态由这些实例变量的值决定。 1.1. Java 程序 1 2 3 4 5 6 7 8 public class HelloWorld { /* 第一个Java程序 * 它将输出字符串 Hello World */ public static void main(String[] args) { System.out.println(\u0026#34;Hello World\u0026#34;); // 输出 Hello World } } Notes: main 是特殊的方法名，但不是关键字\n1.2. Java 程序基本语法 大小写敏感：Java 是大小写敏感的，这就意味着标识符 Hello 与 hello 是不同的。 类名：对于所有的类来说，类名的首字母应该大写。如果类名由若干单词组成，那么每个单词的首字母应该大写，例如 MyFirstJavaClass 方法名：所有的方法名都应该以小写字母开头。如果方法名含有若干单词，则后面的每个单词首字母大写。 源文件名：源文件名必须和类名相同。当保存文件的时候，你应该使用类名作为文件名保存（切记 Java 是大小写敏感的），文件名的后缀为 .java。（如果文件名和类名不相同则会导致编译错误）。 主方法入口：所有的 Java 程序由 public static void main(String[] args) 方法开始执行。 1.3. Java 标识符 1.3.1. 概念 Java 程序中所有定义的内容都需要名字。类名、变量名以及方法名都被称为标识符。\n1.3.2. 标识符的命名规则 命名规则（硬性要求）：\n所有的标识符只能以字母（A-Z 或者 a-z）、美元符（$）、或者下划线（_）开始，但不能以数字开头 首字符之后可以是字母（A-Z 或者 a-z）、美元符（$）、下划线（_）或数字的任何字符组合 标识符不能是关键字 命名规范（非硬性要求）：\n类名规范：首字符大写，后面每个单词首字母大写（大驼峰式）。 变量名规范：首字母小写，后面每个单词首字母大写（小驼峰式）。 方法名规范：同变量名。 示例：\n1 2 3 4 5 // 合法标识符举例 age、$salary、_value、__1_value // 非法标识符举例 123abc、-salary 1.4. 代码块 代码块就是用于包裹多行（一行）代码的符号，符号是：{ }\n1 2 3 { N行代码; } Tips: 如果代码块 {} 内只有一行代码(就是只有一个;)，可以省略\n1.5. Java 源程序与编译型运行区别 2. Java 关键字 以下是 Java 关键字汇总表。其中保留字不能用于常量、变量、和任何标识符的名称。\n后续在各个知识点中再详细说明\n2.1. 访问控制 关键字 说明 private 私有的 protected 受保护的 public 公共的 default 默认 2.2. 类、方法和变量修饰符 关键字 说明 abstract 声明抽象 class 类 extends 扩充，继承 final 最终值，不可改变的 implements 实现（接口） interface 接口 native 本地，原生方法（非 Java 实现） new 新，创建 static 静态 strictfp 严格，精准 synchronized 线程，同步 transient 短暂 volatile 易失 实现一些其他的功能，Java 也提供了许多非访问修饰符。\nstatic 修饰符，用来修饰类方法和类变量。 final 修饰符，用来修饰类、方法和变量，final 修饰的类不能够被继承，修饰的方法不能被继承类重新定义，修饰的变量为常量，是不可修改的。 abstract 修饰符，用来创建抽象类和抽象方法。 synchronized 和 volatile 修饰符，主要用于线程的编程。 2.3. 程序控制语句 关键字 说明 break 跳出循环 continue 继续 default 默认 do 运行 else 否则 for 循环 if 如果 instanceof 实例 return 返回；终止当前的方法 switch 根据值选择执行 case 定义一个值以供 switch 选择 while 循环 2.4. 错误处理 关键字 说明 assert 断言表达式是否为真 catch 捕捉异常 finally 有没有异常都执行 throw 抛出一个异常对象 throws 声明一个异常可能被抛出 try 捕获异常 2.5. 包相关 关键字 说明 import 引入 package 包 2.6. 基本类型 关键字 说明 boolean 布尔型 byte 字节型 char 字符型 double 双精度浮点 float 单精度浮点 int 整型 long 长整型 short 短整型 2.7. 变量引用 关键字 说明 super 父类，超类 this 本类 void 无返回值 2.8. 定义数据类型值 关键字 说明 true 真 false 非 null 空值 2.9. 保留关键字 关键字 说明 goto 是关键字，但不能使用 const 是关键字，但不能使用 2.10. JAVA 转义字符 作用 转义字符 作用 转义字符 退格键 \\b Tab键 \\t 换行符号 \\n 进纸 \\f 回车键 \\r 反斜杠 \\\\ 单引号 \\' 双引号 \\\u0026quot; 2.10.1. 换行符 通常换行符 \\n 可以实现换行，但是 windows 系统自带的记事本打开并没有换行。这是因为 windows 识别的换行符不是 \\n，而是 \\r\\n。以下各类系统相应的换行符：\nwindows: \\r\\n linux: \\n mac: \\r 3. Java 基本（内置）数据类型（整理中） 3.1. 变量与数据类型 变量就是申请内存来存储值。也就是说，当创建变量的时候，需要在内存中申请空间。\n内存管理系统根据变量的类型为变量分配存储空间，分配的空间只能用来储存该类型数据。\n因此，通过定义不同类型的变量，可以在内存中储存整数、小数或者字符。Java 的两大数据类型:\n内置数据类型 引用数据类型（对象） 3.1.1. Java 基本数据类型汇总表 基本类型 大小（字节） 默认值 包装类 byte 1 (byte)0 Byte short 2 (short)0 Short int 4 0 Integer long 8 0L Long float 4 0.0f Float double 8 0.0d Double boolean - false Boolean char 2 \\u0000(null) Character Tips: boolean 类型单独使用是 4 个字节，在数组中又是 1 个字节。\n3.1.2. 各种数据类型的默认值 byte, short, int, long 默认值均是 0 boolean 默认值是 false char 类型的默认值是 '' float、double 类型的默认值是 0.0 对象类型的默认值是 null 3.2. char char 类型是一个单一的 16 位 Unicode 字符。最小值是 \\u0000（十进制等效值为 0）；最大值是 \\uffff（即为 65535）。字符是使用''单引号包裹。\n1 char letter = \u0026#39;A\u0026#39;; 补充说明：unicode 编码占用两个字节，所以 char 类型的变量也是占用两个字节。\n3.2.1. char 类型存储中文 char 数据类型可以储存任何字符，Java 默认是 Unicode 编码，而 unicode 编码字符集中也包含了汉字。因此 char 类型变量可以存储汉字。但如果某个特殊的汉字没有被包含在 unicode 编码字符集中，则不能存储这个特殊汉字。\n3.2.2. 番外：字符型常量和字符串常量的区别 形式上：字符常量是单引号引起的一个字符；字符串常量是双引号引起的若干个字符 含义上：字符常量相当于一个整形值(ASCII值)，可以参加表达式运算；字符串常量代表一个地址值(该字符串在内存中存放位置) 占内存大小：字符常量只占一个字节；字符串常量占若干个字节(至少一个字符结束标志) 3.2.3. 字符存储数值 大写字母与小写字母的 ASCII 码相差 32\n1 2 3 \u0026#39;A\u0026#39;=65; \u0026#39;a\u0026#39;=97; \u0026#39;0\u0026#39;=48; 3.3. 数值类型之间的转换 上图是数值类型之间的合法转换，其中6个实心箭头表示无信息丢失的转换；3个虚箭头，表示可能有精度损失的转换。\n3.3.1. 隐式转换 隐式转换，又称『自动类型转换』，就是使用大范围的数据类型变量来接收小范围的数据类型。\n3.3.2. 强制类型转换 强制类型转换，又称『显示转换』，就是将一个大范围的数据类型强制赋值给小类型的数据类型，可能会出现精度的损失。语法格式如下：\n1 2 类型1 x = xxx; 类型2 y = (类型2) x; Notes: 如果试图将一个数值从一种类型强制转换为另一种类型，而又超出了目标类型的表示范围，结果就会截断成一个完全不同的值。如：(byte) 300 强制类型转换后的实际值为 44。\n如果想对浮点数进行舍入运算，为了得到最接近的整数，很多情况下会使用 Math.round 静态方法\n1 2 double x = 9.997; int y = (int) Math.round(x); 扩展的赋值运算符，隐含了强制类型转换。\n1 2 3 a += 20; // 相当于 a = (a的数据类型)(a + 20); 为了保留两位小数，利用数据类型强制转换可以实现\n1 2 3 4 d = 11.853333; d * 100 = 1185.3333; (int)(d * 100) = 1185; (int)(d * 100)/100.0 = 11.85; 注意：最后一步除以 100 的时候，必须写成 100.0，这个才会根据数据隐性转换原理，最后得出来的结果是浮点型。\n3.3.3. 数值类型转换的精度损失 如果对数值类型进行向下转型（down-casting，也称为窄化），会造成精度损失。例如将双精度型（double）赋值给浮点型（float）：\n1 2 // 小数默认是双精度型（double） float f = 3.4; 因此需要强制类型转换正确写法如下：\n1 2 3 float f = (float) 3.4; // 或者 float f = 3.4F;s 3.4. 类型转换相关问题 3.4.1. char 类型能否转成 int、String、double 类型 char 是 Java 中比较特殊的类型，它的 int 值从 1 开始，一共有 216 个数据。取值范围如下：\n1 char \u0026lt; int \u0026lt; long \u0026lt; float \u0026lt; double 因此，char 类型可以隐式转成 int、double 类型，但是不能隐式转换成 String；如果 char 类型转成 byte、short 类型的时候，需要强转。\n3.4.2. 计算机大小单位转换 byte 字节，bit 是位（二进制的位数）。如：\n1Mb = 1024kb 1 k = 1024 byte 1 byte = 8 bit 3.4.3. int 类型转换为 byte 类型的问题 int 类型可以强制转换为 byte 类型，但是 Java 中 int 是 32 位的，而 byte 是 8 位的，所以如果强制转化，int 类型的高 24 位将会被丢弃，因为 byte 类型的范围是从 -128 到 127。\n3.4.4. 数值类型转换经典面试题 以下哪种写法正常编译？\n1 2 3 4 5 6 // 写法1： short s1 = 1; s1 = s1 + 1; // 写法2： short s2 = 1; s2 += 1; 解析：\n对于写法1，由于 1 默认是 int 类型，因此 s1+1 运算结果也是 int 类型，需要强制转换类型才能赋值给 short 型。因此不能正确编译。 对于写法2，+= 操作符会进行隐式自动类型转换，是 Java 语言规定的运算符。Java 编译器会对它进行特殊处理，s1 += 1 相当于 s1 = (short)(s1 + 1)，因此可以正确编译。 3.4.5. char 与 byte 的区别 byte 是字节数据类型，是有符号型的，占 1 个字节。大小范围为-128 ~ 127。 char 是字符数据类型，是无符号型的，占 2 个字节(Unicode码)。大小范围 是0 ~ 65535。 用实例来比较一下二者的区别：\nchar 是无符号型的，可以表示一个整数，不能表示负数；而 byte 是有符号型的，可以表示 -128 ~ 127 的数。如： 1 2 3 4 5 6 7 8 9 10 char c = (char) -3; // char不能识别负数，必须强制转换否则报错，即使强制转换之后，也无法识别 System.out.println(c); // 结果是:? byte d1 = 1; byte d2 = -1; byte d3 = 127; // 如果是 byte d3 = 128; 会报错 byte d4 = -128; // 如果是 byte d4 = -129; 会报错 System.out.println(d1); // 结果是:1 System.out.println(d2); // 结果是:-1 System.out.println(d3); // 结果是:127 System.out.println(d4); // 结果是:-128 char 可以是中文字符；byte 不可以。如： 1 2 3 4 5 char e1 = \u0026#39;中\u0026#39;, e2 = \u0026#39;国\u0026#39;; byte f = (byte) \u0026#39;中\u0026#39;; // 必须强制转换否则报错 System.out.println(e1); // 结果是:中 System.out.println(e2); // 结果是:国 System.out.println(f); // 结果是:45 char、byte、int 对于英文字符，可以相互转化。如： 1 2 3 4 5 6 7 8 byte g = \u0026#39;b\u0026#39;; // b 对应 ASCII 是 98 char h = (char) g; char i = 85; // U 对应 ASCII 是 85 int j = \u0026#39;h\u0026#39;; // h 对应 ASCII 是 104 System.out.println(g); // 结果是:97 System.out.println(h); // 结果是:b System.out.println(i); // 结果是:u System.out.println(j); // 结果是:14 3.5. Java 包装类 3.5.1. 概述 一般地，当需要使用数字的时候，通常使用内置数据类型，如：byte、int、long、double 等。所有数值类型的包装类（Integer、Long、Byte、Double、Float、Short）都是抽象类 Number 的子类，其实就是基本类型对应的引用类型(包装类)。\nTips: 除了 char 与 int 的包装类之外，其他包装类类名称均为基本类型名称的首字母大写\n3.5.1.1. 包装类的产生原因 基本类型包装类的产生原因：因为泛型类包括预定义的集合，使用的参数都是对象类型，无法直接使用基本数据类型。在实际开发中，用户输入的内容都是以字符串形式存在，需要参与数学运算时需要将字符串转换成对应的基本数据类型。\n3.5.1.2. 基本类型与包装类的区别 基本类型和对应的封装类由于本质的不同。具有一些区别：\n基本类型只能按值传递，而封装类按引用传递。 基本类型会在栈中创建，而对于对象类型，对象在堆中创建，对象的引用在栈中创建，基本类型由于在栈中，效率会比较高，但是可能存在内存泄漏的问题。 3.5.2. Integer Tips: 以 Integer 包装类为例，其他的包装类相关方法与使用基本一致\n3.5.2.1. 概述 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public final class Integer extends Number implements Comparable\u0026lt;Integer\u0026gt; { /** * A constant holding the minimum value an {@code int} can * have, -2\u0026lt;sup\u0026gt;31\u0026lt;/sup\u0026gt;. */ // 静态成员变量，直接用类名调用，返回整形的最小取值数 @Native public static final int MIN_VALUE = 0x80000000; /** * A constant holding the maximum value an {@code int} can * have, 2\u0026lt;sup\u0026gt;31\u0026lt;/sup\u0026gt;-1. */ // 静态成员变量，直接用类名调用，返回整形的最大取值数 @Native public static final int MAX_VALUE = 0x7fffffff; // 省略... } 3.5.2.2. 核心方法 1 public Integer(String s) throws NumberFormatException 构造一个新分配的 Integer 对象，它表示 String 参数所指示的 int 值。 1 public Integer(int value) 构造一个新分配的 Integer 对象，它表示指定的 int 值。 1 public int intValue() 将构造方法中指定的数字字符串转换基本数据类型 1 public static int parseInt(String s) throws NumberFormatException 将字符串数字转换整数(传入s必须是数字字符串，不能有字母和空格) 1 public String toString() 重写 Object 类的方法，将整数转换成字符串 1 public static String toBinaryString(int i) 将指定的整数转成二进制字符串 1 public static String toOctalString(int i) 将指定的整数转成八进制字符串 1 public static String toHexString(int i) 将指定的整数转成十六进制字符串 3.5.3. 自动装箱和自动拆箱 3.5.3.1. 概念 JDK 1.5 之前，如果要生成一个数值为 10 的 Integer 对象，需要以下操作：\n1 Integer i = new Integer(10); 在 JDK 1.5 后的增加新特性自动拆装箱：\n自动装箱：Java 自动将基本数据类型转换成其对应的包装类的过程就是自动装箱。 1 Integer i = 10; // 自动装箱，本质是调用 Integer.valueOf(10) 自动拆箱：Java自动将包装类转换为其对应的基本数据类型的过程就是自动拆箱。 1 int a = i; // 自动拆箱，本质是调用 a.intValue() 自动装拆箱的好处：基本数据类型的变量可以直接和对应的包装类引用变量进行数学运算。\n3.5.3.2. 自动装拆箱的情况 当基础类型与它们的包装类有如下几种情况时，编译器会自动帮我们进行装箱或拆箱：\n赋值操作（装箱或拆箱） 进行加减乘除混合运算（拆箱） 进行\u0026gt;、\u0026lt;、==比较运算（拆箱） 调用equals进行比较（装箱） ArrayList、HashMap 等集合类添加基础类型数据时（装箱） 3.5.3.3. IntegerCache 自动拆箱和自动装箱是由编译器自动完成，根据语法来决定是否需要装箱和拆箱。 如果整型字面量的值在 -128 到 127 之间，那么自动装箱时不会创建新的 Integer 对象，而是直接引用常量池中的 Integer 对象，若超过范围才会创建新的对象（经典面试题） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Integer a = new Integer(3); Integer b = 3; // 将3自动装箱成Integer类型 int c = 3; System.out.println(a == b); // false 两个引用没有引用同一对象 System.out.println(a == c); // true a自动拆箱成int类型再和c比较 System.out.println(b == c); // true Integer a1 = 128; Integer b1 = 128; System.out.println(a1 == b1); // false Integer a2 = 127; Integer b2 = 127; System.out.println(a2 == b2); // true 示例中的 a1 自动拆箱，实质是调用 Integer.valueOf(128)。\n1 2 3 4 5 public static Integer valueOf(int i) { if (i \u0026gt;= IntegerCache.low \u0026amp;\u0026amp; i \u0026lt;= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); } 通过查看 Integer 类的源码可知，该方法并不是直接进行 new Integer 操作，而是用内部类 IntegerCache 的 cache[] 数组中获取数据（即直接引用常量池中的 Integer 对象）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty(\u0026#34;java.lang.Integer.IntegerCache.high\u0026#34;); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k \u0026lt; cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high \u0026gt;= 127; } private IntegerCache() {} } 默认 Integer cache 的下限是 -128，上限默认 127。当赋值 127 给 Integer 时，刚好在这个范围内，所以从 cache 中取对应的 Integer 并返回，所以 a2 和 b2 返回的是同一个对象，所以使用==比较是相等的；当赋值 128 给 Integer 时，不在 cache 的范围内，所以会 new Integer 创建新的对象并返回，比较的结果必然不相等的。最大边界可以通过 -XX:AutoBoxCacheMax 进行配置。\n4. 权限修饰符 权限大小顺序：private \u0026lt; 默认 \u0026lt; protected \u0026lt; public\npublic: 任意包下任意类都可以访问； protected: 任意包下任意子类都可以访问或同包下的任意类 默认(包权限): 同包下的任意类都可以访问 private: 只能在本类中使用 修饰符权限列表图：\npublic protected 空的（default） private 同一类中 ✔ ✔ ✔ ✔ 同一包中（子类与无关类） ✔ ✔ ✔ 不同包的子类 ✔ ✔ 不同包中的无关类 ✔ 类的成员不写访问修饰时默认为default。默认对于同一个包中的其他类相当于公开（public），对于不是同一个包中的其他类相当于私有（private）。受保护（protected）对子类相当于公开，对不是同一包中的没有父子关系的类相当于私有。\n总结：在日常开发过程中，编写的类、方法、成员变量的访问\n要想仅能在本类中访问使用 private 修饰; 要想本包中的类都可以访问不加修饰符即可; 要想本类与子类可以访问使用 protected 修饰; 要想任意包中的任意类都可以访问使用 public 修饰; 注意项总结：\n如果类用 public 修饰，则类名必须与文件名相同。一个文件中只能有一个 public 修饰的类。 Java 中，外部类的修饰符只能是 public 或默认 ，类的成员（包括内部类）的修饰符可以是以上四种。 5. Java 运算符 计算机的最基本用途之一就是执行数学运算，作为一门计算机语言，Java 也提供了一套丰富的运算符来操纵变量。运算符主分成以下几类：\n算术运算符 关系运算符 位运算符 逻辑运算符 赋值运算符 其他运算符 5.1. 算术运算符 算术运算符用在数学表达式中，它们的作用和在数学中的作用一样。下表列出了所有的算术运算符。\n表格中的实例假设整数变量A的值为10，变量B的值为20：\n操作符 描述 例子 + 加法 - 相加运算符两侧的值 A + B 等于 30 - 减法 - 左操作数减去右操作数 A – B 等于 -10 * 乘法 - 相乘操作符两侧的值 A * B 等于 200 / 除法 - 左操作数除以右操作数 B / A 等于 2 % 取余 - 左操作数除以右操作数的余数 B%A 等于 0 ++ 自增: 操作数的值增加1 B++ 或 ++B 等于 21 -- 自减: 操作数的值减少1 B-- 或 --B 等于 19 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Test { public static void main(String args[]) { int a = 10; int b = 20; int c = 25; int d = 25; System.out.println(\u0026#34;a + b = \u0026#34; + (a + b)); // a + b = 30 System.out.println(\u0026#34;a - b = \u0026#34; + (a - b)); // a - b = -10 System.out.println(\u0026#34;a * b = \u0026#34; + (a * b)); // a * b = 200 System.out.println(\u0026#34;b / a = \u0026#34; + (b / a)); // b / a = 2 System.out.println(\u0026#34;b % a = \u0026#34; + (b % a)); // b % a = 0 System.out.println(\u0026#34;c % a = \u0026#34; + (c % a)); // c % a = 5 System.out.println(\u0026#34;a++ = \u0026#34; + (a++)); // a++ = 10 System.out.println(\u0026#34;a-- = \u0026#34; + (a--)); // a-- = 11 // 观察 d++ 与 ++d 的不同 System.out.println(\u0026#34;d++ = \u0026#34; + (d++)); // d++ = 25 System.out.println(\u0026#34;++d = \u0026#34; + (++d)); // ++d = 27 } } 5.1.1. 自增自减运算符 自增（++）自减（--）运算符是一种特殊的算术运算符，在算术运算符中需要两个操作数来进行运算，而自增自减运算符是一个操作数。又分以下两种：\n前缀自增自减法(++a,--a)：先进行自增或者自减运算，再进行表达式运算。 后缀自增自减法(a++,a--)：先进行表达式运算，再进行自增或者自减运算 1 2 3 4 5 6 7 8 public static void main(String args[]) { int a = 5; // 定义一个变量； int b = 5; int x = 2 * ++a; int y = 2 * b++; System.out.println(\u0026#34;自增运算符前缀运算后a=\u0026#34; + a + \u0026#34;,x=\u0026#34; + x); // a=6,x=12 System.out.println(\u0026#34;自增运算符后缀运算后b=\u0026#34; + b + \u0026#34;,y=\u0026#34; + y); // b=6,y=10 } 还一种情况 a += 值，相当于自增任意值。\n5.1.2. 取余运算符 % 用于取得余数，左操作数除以右操作数的余数。有以下几个特点：\n左边如果大于右边，结果是余数。 左边如果小于右边，结果是左边。 左边如果等于右边，结果是0。 正负号跟左边一致。即负数取余也是负数。 5.2. 关系运算符 算术运算符用在数学表达式中，它们的作用和在数学中的作用一样。下表列出了所有的算术运算符。\n表格中的实例假设整数变量A的值为10，变量B的值为20：\n运算符 描述 例子 == 检查如果两个操作数的值是否相等，如果相等则条件为真 A == B 为 false != 检查如果两个操作数的值是否相等，如果值不相等则条件为真 A != B 为 true \u0026gt; 检查左操作数的值是否大于右操作数的值，如果是那么条件为真 A \u0026gt; B 为 false \u0026lt; 检查左操作数的值是否小于右操作数的值，如果是那么条件为真 A \u0026lt; B 为 true \u0026gt;= 检查左操作数的值是否大于或等于右操作数的值，如果是那么条件为真 A \u0026gt;= B 为 false \u0026lt;= 检查左操作数的值是否小于或等于右操作数的值，如果是那么条件为真 A \u0026lt;= B 为 true 示例：\n1 2 3 4 5 6 7 8 9 10 public static void main(String args[]) { int a = 10; int b = 20; System.out.println(\u0026#34;a == b = \u0026#34; + (a == b)); // a == b = false System.out.println(\u0026#34;a != b = \u0026#34; + (a != b)); // a != b = true System.out.println(\u0026#34;a \u0026gt; b = \u0026#34; + (a \u0026gt; b)); // a \u0026gt; b = false System.out.println(\u0026#34;a \u0026lt; b = \u0026#34; + (a \u0026lt; b)); // a \u0026lt; b = true System.out.println(\u0026#34;b \u0026gt;= a = \u0026#34; + (b \u0026gt;= a)); // b \u0026gt;= a = true System.out.println(\u0026#34;b \u0026lt;= a = \u0026#34; + (b \u0026lt;= a)); // b \u0026lt;= a = false } 5.3. 位运算符 Java 定义了位运算符，应用于整数类型(int)，长整型(long)，短整型(short)，字符型(char)，和字节型(byte)等类型。\n下表列出了位运算符的基本运算，假设整数变量 A 的值为 60 和变量 B 的值为 13：\n操作符 描述 例子 \u0026amp; 如果相对应位都是1，则结果为1，否则为0 A \u0026amp; B 得到12，即0000 1100 ` ` 如果相对应位都是0，则结果为0，否则为1 ^ 如果相对应位值相同，则结果为0，否则为1 A ^ B 得到49，即 0011 0001 〜 按位取反运算符翻转操作数的每一位，即0变成1，1变成0 〜A 得到-61，即1100 0011 \u0026lt;\u0026lt; 按位左移运算符。左操作数按位左移右操作数指定的位数 A \u0026lt;\u0026lt; 2 得到240，即 1111 0000 \u0026gt;\u0026gt; 按位右移运算符。左操作数按位右移右操作数指定的位数 A \u0026gt;\u0026gt; 2 得到15，即 1111 \u0026gt;\u0026gt;\u0026gt; 按位右移补零操作符。左操作数的值按右操作数指定的位数右移，移动得到的空位以零填充 A\u0026gt;\u0026gt;\u0026gt;2 得到15，即0000 1111 位运算符作用在所有的位上，并且按位运算。它们的二进制格式表示将如下：\n1 2 3 4 5 6 7 A = 0011 1100 B = 0000 1101 ----------------- A \u0026amp; B = 0000 1100 A | B = 0011 1101 A ^ B = 0011 0001 ~A = 1100 0011 示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static void main(String args[]) { int a = 60; /* 60 = 0011 1100 */ int b = 13; /* 13 = 0000 1101 */ int c = 0; c = a \u0026amp; b; /* 12 = 0000 1100 */ System.out.println(\u0026#34;a \u0026amp; b = \u0026#34; + c); // a \u0026amp; b = 12 c = a | b; /* 61 = 0011 1101 */ System.out.println(\u0026#34;a | b = \u0026#34; + c); // a | b = 61 c = a ^ b; /* 49 = 0011 0001 */ System.out.println(\u0026#34;a ^ b = \u0026#34; + c); // a ^ b = 49 c = ~a; /*-61 = 1100 0011 */ System.out.println(\u0026#34;~a = \u0026#34; + c); // ~a = -61 c = a \u0026lt;\u0026lt; 2; /* 240 = 1111 0000 */ System.out.println(\u0026#34;a \u0026lt;\u0026lt; 2 = \u0026#34; + c); // a \u0026lt;\u0026lt; 2 = 240 c = a \u0026gt;\u0026gt; 2; /* 15 = 1111 */ System.out.println(\u0026#34;a \u0026gt;\u0026gt; 2 = \u0026#34; + c); // a \u0026gt;\u0026gt; 2 = 15 c = a \u0026gt;\u0026gt;\u0026gt; 2; /* 15 = 0000 1111 */ System.out.println(\u0026#34;a \u0026gt;\u0026gt;\u0026gt; 2 = \u0026#34; + c); // a \u0026gt;\u0026gt;\u0026gt; 2 = 15 } 5.4. 逻辑运算符 下表列出了逻辑运算符的基本运算，假设布尔变量A为真，变量B为假\n操作符 描述 例子 \u0026amp;\u0026amp; 称为逻辑与运算符。当且仅当两个操作数都为真，条件才为真 A \u0026amp;\u0026amp; B 为 false ` ` ! 称为逻辑非运算符。用来反转操作数的逻辑状态。如果条件为true，则逻辑非运算符将得到false !(A \u0026amp;\u0026amp; B) 为 true 示例：\n1 2 3 4 5 6 7 public static void main(String args[]) { boolean a = true; boolean b = false; System.out.println(\u0026#34;a \u0026amp;\u0026amp; b = \u0026#34; + (a \u0026amp;\u0026amp; b)); // a \u0026amp;\u0026amp; b = false System.out.println(\u0026#34;a || b = \u0026#34; + (a || b)); // a || b = true System.out.println(\u0026#34;!(a \u0026amp;\u0026amp; b) = \u0026#34; + !(a \u0026amp;\u0026amp; b)); // !(a \u0026amp;\u0026amp; b) = true } 5.4.1. 短路逻辑运算符 当使用与逻辑运算符时，在两个操作数都为 true 时，结果才为 true，但是当得到第一个操作为 false 时，其结果就必定是 false，这时候就不会再判断第二个操作了。\n示例\n1 2 3 4 5 6 public static void main(String args[]) { int a = 5; // 定义一个变量； boolean b = (a \u0026lt; 4) \u0026amp;\u0026amp; (a++ \u0026lt; 10); System.out.println(\u0026#34;使用短路逻辑运算符的结果为\u0026#34; + b); // false System.out.println(\u0026#34;a的结果为\u0026#34; + a); // 5 } 解析：该程序使用到了短路逻辑运算符(\u0026amp;\u0026amp;)，首先判断 a\u0026lt;4 的结果为 false，则 b 的结果必定是 false，所以不再执行第二个操作 a++ \u0026lt; 10 的判断，所以 a 的值为 5\n5.4.2. 注意问题 在逻辑判断中，\u0026amp;\u0026amp; 和 \u0026amp; 的结果一样；|| 和 | 的结果一样。两者的区别如下：\n\u0026amp;\u0026amp; 如果左边是 false，则右边不执行；而 \u0026amp; 无论左边是 true 还是 false，右边都会执行。 | 和 || 的区别同理，双或时，左边为真，右边不参与运算。 5.5. 赋值运算符 下面是Java语言支持的赋值运算符：\n操作符 描述 例子 = 简单的赋值运算符，将右操作数的值赋给左侧操作数 C = A + B将把A + B得到的值赋给C += 加和赋值操作符，它把左操作数和右操作数相加赋值给左操作数 C + = A等价于C = C + A -= 减和赋值操作符，它把左操作数和右操作数相减赋值给左操作数 C -= A等价于C = C - A *= 乘和赋值操作符，它把左操作数和右操作数相乘赋值给左操作数 C *= A等价于C = C * A /= 除和赋值操作符，它把左操作数和右操作数相除赋值给左操作数 C /= A，C 与 A 同类型时等价于C = C / A %= 取模和赋值操作符，它把左操作数和右操作数取模后赋值给左操作数 C %= A等价于C = C%A \u0026lt;\u0026lt;= 左移位赋值运算符 C \u0026lt;\u0026lt; = 2等价于C = C \u0026lt;\u0026lt; 2 \u0026gt;\u0026gt;= 右移位赋值运算符 C \u0026gt;\u0026gt; = 2等价于C = C \u0026gt;\u0026gt; 2 \u0026amp;= 按位与赋值运算符 C \u0026amp;= 2等价于C = C\u0026amp;2 ^= 按位异或赋值操作符 C ^= 2等价于C = C ^ 2 ` =` 按位或赋值操作符 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public static void main(String args[]) { int a = 10; int b = 20; int c = 0; c = a + b; System.out.println(\u0026#34;c = a + b, c = \u0026#34; + c); // 30 c += a; System.out.println(\u0026#34;c += a, c = \u0026#34; + c); // 40 c -= a; System.out.println(\u0026#34;c -= a, c = \u0026#34; + c); // 30 c *= a; System.out.println(\u0026#34;c *= a, c = \u0026#34; + c); // 300 a = 10; c = 15; c /= a; System.out.println(\u0026#34;c /= a, c = \u0026#34; + c); // 1 a = 10; c = 15; c %= a; System.out.println(\u0026#34;c %= a , c = \u0026#34; + c); // 5 c \u0026lt;\u0026lt;= 2; System.out.println(\u0026#34;c \u0026lt;\u0026lt;= 2, c = \u0026#34; + c); // 20 c \u0026gt;\u0026gt;= 2; System.out.println(\u0026#34;c \u0026gt;\u0026gt;= 2, c = \u0026#34; + c); // 5 c \u0026gt;\u0026gt;= 2; System.out.println(\u0026#34;c \u0026gt;\u0026gt;= 2, c = \u0026#34; + c); // 1 c \u0026amp;= a; System.out.println(\u0026#34;c \u0026amp;= a, c = \u0026#34; + c); // 0 c ^= a; System.out.println(\u0026#34;c ^= a, c = \u0026#34; + c); // 10 c |= a; System.out.println(\u0026#34;c |= a, c = \u0026#34; + c); // 10 } 5.6. 条件运算符（三元运算符） 条件运算符也被称为三元运算符。该运算符有3个操作数，并且需要判断布尔表达式的值。该运算符的主要是决定哪个值应该赋值给变量。\n1 variable x = (expression) ? value if true : value if false 示例：\n1 2 3 4 5 6 7 8 9 10 11 public static void main(String args[]) { int a, b; a = 10; // 如果 a 等于 1 成立，则设置 b 为 20，否则为 30 b = (a == 1) ? 20 : 30; System.out.println(\u0026#34;Value of b is : \u0026#34; + b); // 如果 a 等于 10 成立，则设置 b 为 20，否则为 30 b = (a == 10) ? 20 : 30; System.out.println(\u0026#34;Value of b is : \u0026#34; + b); } 5.6.1. 三元表达式的类型转化规则 引用阿里巴巴Java开发手册的规则：\n【强制】三目运算符 condition ? 表达式 1：表达式 2 中，高度注意表达式 1 和 2 在类型对齐时，可能抛出因自动拆箱导致的 NPE 异常。说明：以下两种场景会触发类型对齐的拆箱操作：\n表达式 1 或 表达式 2 的值只要有一个是原始类型。 表达式 1 或 表达式 2 的值的类型不一致，会强制拆箱升级成表示范围更大的那个类型。 由于三元表达式拆包，有可能引发空指针异常，而三元表达式的类型转化规则如下：\n若两个表达式类型相同，返回值类型为该类型 若两个表达式类型不同，但类型不可转换，返回值类型为 Object 类型 若两个表达式类型不同，但类型可以转化，先把包装数据类型转化为基本数据类型，然后按照基本数据类型的转换规则（byte \u0026lt; short(char) \u0026lt; int \u0026lt; long \u0026lt; float \u0026lt; double）来转化，返回值类型为优先级最高的基本数据类型。 5.6.2. 三元表达式使用建议 如果三元表达式中有包装数据类型的算术计算，尽量避免使用三元表达式，可以考虑利用 if-else 语句代替。 如果在三元表达式中有算术计算，尽量使用基本数据类型，避免包装数据类型的拆装包。 5.7. instanceof 运算符 5.7.1. 概述 instanceof 运算符（双目运算符）用于操作对象实例，检查该对象是否是一个特定类型（类类型或接口类型）。即判断父类引用指定的到底是哪一个子类类型的对象。语法格式如下：\n1 2 // 父类引用对象 instanceof 子类类名或接口名; ( Object reference variable ) instanceof (class/interface type) 如果运算符左侧变量所指的对象，是操作符右侧类或接口(class/interface)的一个对象，那么结果为真。示例如下：\n1 2 3 4 5 6 String name = \u0026#34;James\u0026#34;; boolean result = name instanceof String; // 由于 name 是 String 类型，所以返回真 // public class Car extends Vehicle Vehicle a = new Car(); boolean result = a instanceof Car; // true 5.7.2. 使用注意事项 instanceof 关键字前面的对象和后面的类型必须是子父类关系或类实现接口关系，毫无关系的两个对象不能进行判断。\n编译器会检查左侧变量的对象是否能转换成右边的类型，如果不能转换则直接报错；如果不能确定类型，则通过编译，具体看运行时定。\n5.8. Java 运算符优先级 当多个运算符出现在一个表达式中，就涉及到运算符的优先级别的问题。在一个多运算符的表达式中，运算符优先级不同会导致最后得出的结果差别甚大。\n下表中具有最高优先级的运算符在的表的最上面，最低优先级的在表的底部。\n类别 操作符 关联性 后缀 ()、[]、.(点操作符) 左到右 一元 expr++、expr-- 从左到右 一元 ++expr、--expr、+、-、～、! 从右到左 乘性 *、/、% 左到右 加性 +、- 左到右 移位 \u0026gt;\u0026gt;、\u0026gt;\u0026gt;\u0026gt;、\u0026lt;\u0026lt; 左到右 关系 \u0026gt;、\u0026gt;=、\u0026lt;、\u0026lt;= 左到右 相等 ==、!= 左到右 按位与 \u0026amp; 左到右 按位异或 ^ 左到右 按位或 ` ` 逻辑与 \u0026amp;\u0026amp; 左到右 逻辑或 ` 条件 ? : 从右到左 赋值 =、+=、-=、*=、/=、%=、\u0026gt;\u0026gt;=、\u0026lt;\u0026lt;=、\u0026amp;=、^=、` =` 逗号 , 左到右 6. 流程控制 6.1. 控制流程的概念 与任何程序设计语言一样，Java 使用条件语句和循环结构来控制程序流程。\n6.2. 条件语句 Java 中的条件语句允许程序根据条件的不同执行不同的代码块。一个 if 语句包含一个布尔表达式和一条或多条语句。\n6.2.1. if 语句 if 语句的语法如下：\n1 2 3 if (布尔表达式) { // 如果布尔表达式为true将执行的语句 } 6.2.2. if\u0026hellip;else 语句 if 语句后面可以跟 else 语句，当 if 语句的布尔表达式值为 false 时，else 语句块会被执行。语法如下：\n1 2 3 4 5 if (布尔表达式) { // 如果布尔表达式的值为true } else { // 如果布尔表达式的值为false } 6.2.3. if\u0026hellip;else if\u0026hellip;else 语句 if 语句后面可以跟 else if…else 语句，这种语句可以检测到多种可能的情况。使用时需要注意下面几点：\nif 语句至多有 1 个 else 语句，else 语句在所有的 else if 语句之后。 if 语句可以有若干个 else if 语句，它们必须在 else 语句之前。 一旦其中一个 else if 语句检测为 true，其他的 else if 以及 else 语句都将跳过执行。 语法格式如下:\n1 2 3 4 5 6 7 8 9 if (布尔表达式 1) { // 如果布尔表达式 1的值为true执行代码 } else if (布尔表达式 2) { // 如果布尔表达式 2的值为true执行代码 } else if (布尔表达式 3) { // 如果布尔表达式 3的值为true执行代码 } else { // 如果以上布尔表达式都不为true执行代码 } 6.2.4. 嵌套的 if\u0026hellip;else 语句 使用嵌套的 if…else 语句是合法的。即可以在另一个 if 或者 else if 语句的代码块内使用 if 或者 else if 语句。语法格式如下：\n1 2 3 4 5 6 if (布尔表达式 1) { // 如果布尔表达式 1的值为true执行代码 if (布尔表达式 2) { // 如果布尔表达式 2的值为true执行代码 } } 6.3. switch 多重选择语句 6.3.1. 语法格式 switch case 语句判断一个变量与一系列值中某个值是否相等，每个值称为一个分支。语法格式如下：\n1 2 3 4 5 6 7 8 9 10 11 switch (expression) { case value: // 语句 break; case value: // 语句 break; // ...可以有任意数量的case语句 default: // 可选 // 语句 } 如果输出语句都一样的话，case 可以合并。如：\n1 2 3 4 5 6 7 8 9 switch (expression) { case value1: case value2: case value3: // 语句体 break; default: // 可选 // 语句 } 6.3.2. switch case 语句规则说明 switch 语句中的 expression（表达式）可以是： byte、short、int 或者 char 等基本类型。 JDK1.5 以后支持枚举类型 JDK1.7 以后支持字符串(String)类型，同时 case 标签必须为字符串常量或字面量 switch 语句可以拥有多个 case 语句。每个 case 后面跟一个要比较的值和冒号。 case 语句中的值的数据类型必须与变量的数据类型相同，而且只能是常量或者字面常量。 当变量的值与 case 语句的值相等时，那么 case 语句之后的语句开始执行，直到 break 语句出现才会跳出 switch 语句。 当遇到 break 语句时，switch 语句终止。程序跳转到 switch 语句后面的语句执行。case 语句不必须要包含 break 语句。如果没有 break 语句出现，程序会继续执行下一条 case 语句，直到出现 break 语句。 switch 语句可以包含一个 default 分支（非必需），该分支一般是 switch 语句的最后一个分支（可以在任何位置，但建议在最后一个）。default 在所有 case 语句的值和变量值都不匹配的时候执行。default 分支不需要 break 语句。 6.3.3. 执行流程 switch case 执行时，一定会先进行匹配，匹配成功返回当前 case 的值，再根据是否有 break，判断是否继续输出，或是跳出判断。\n计算出表达式的值 拿计算出来的值和 case 后面的值依次比较，一旦有对应的值，就执行该处的语句，在执行过程中，遇到 break，就结束。 如果所有的 case 都不匹配，就会执行 default 控制的语句，然后结束。 6.4. 循环结构 顺序结构的程序语句只能被执行一次。如果想要同样的操作执行多次，则需要使用循环结构。Java 中有三种主要的循环结构：\nwhile 循环 do\u0026hellip;while 循环 for 循环 在 Java 5 中引入了一种主要用于数组的增强型 for 循环。\n6.4.1. for 循环 for 循环执行的次数是在执行前就确定的。语法格式如下：\n1 2 3 for (初始化语句; 判断条件语句; 控制条件语句) { 循环体语句; } 执行流程：\n执行初始化语句，可以声明一种类型，但可初始化一个或多个循环控制变量，也可以是空语句。 执行判断条件语句，看结果是 true 还是 false 如果是 true，就继续执行 如果是 false，就结束循环 执行循环体语句 执行控制条件语句 回到第2步继续 6.4.2. while 循环 语法格式如下：\n1 2 3 while (判断条件语句) { 循环体语句; } 扩展格式：\n1 2 3 4 5 初始化语句; while (判断条件语句) { 循环体语句; 控制条件语句; } 如果布尔表达式为 true，循环就会一直执行下去。\n6.4.3. do\u0026hellip;while 循环 do\u0026hellip;while 循环和 while 循环相似，但对于 while 语句，如果不满足条件，则不能进入循环。而 do\u0026hellip;while 循环即使不满足条件，也至少执行一次。基础语法格式如下：\n1 2 3 do { 循环体语句; } while (判断条件语句); 扩展格式：\n1 2 3 4 5 初始化语句; do { 循环体语句; 控制条件语句; } while (判断条件语句); Notes: 判断条件语句在循环体的后面，所以语句块在检测布尔表达式之前已经执行了。如果判断条件语句的值为 true，则语句块一直执行，直到判断条件语句的值为 false 为止。\n执行流程：\n执行初始化语句; 执行循环体语句; 执行控制条件语句; 执行判断条件语句，判断是 true 还是 false 如果是 true，回到第2步继续 如果是 false，则结束循环 6.4.4. 三种循环的区别 do\u0026hellip;while 至少执行一次循环体 for、while 循环先判断条件是否成立，然后决定是否执行循环体 for 和 while 的小区别：\nfor 循环的初始化变量，在循环结束后，不可以被访问。而 while 循环的初始化变量，是可以被继续使用的(因为变量是定义在循环体外)。 如果初始化变量，后面还要继续访问，就使用 while，否则，推荐使用 for。 Tips: 循环里的定义的变量，循环一次，重新创建一次。\n6.4.5. 增强 for 循环 Java5 引入了一种主要用于数组的增强型 for 循环。语法格式如下:\n1 2 3 for (声明语句 : 表达式) { // 代码句子 } 语法说明：\n声明语句：声明新的局部变量，该变量的类型必须和数组元素的类型匹配。其作用域限定在循环语句块，其值与此时数组元素的值相等。 表达式：表达式是要访问的数组名，或者是返回值为数组的方法。 6.4.6. break 与 continue 关键字 break 主要用在循环语句或者 switch 语句中，用来跳出整个语句块。\n1 2 3 4 5 6 7 8 9 10 int[] numbers = {10, 20, 30, 40, 50}; for (int x : numbers) { // x 等于 30 时跳出循环 if (x == 30) { break; } System.out.print(x); System.out.print(\u0026#34;\\n\u0026#34;); } continue 适用于任何循环控制结构中。作用是让程序立刻跳转到下一次循环的迭代。\n在 for 循环中，continue 语句使程序立即跳转到更新语句。 在 while 或者 do\u0026hellip;while 循环中，程序立即跳转到布尔表达式的判断语句。 1 2 3 4 5 6 7 8 9 10 int[] numbers = {10, 20, 30, 40, 50}; for (int x : numbers) { // x 等于 30 时立刻跳转下一次循环，后面的语句不会执行 if (x == 30) { continue; } System.out.print(x); System.out.print(\u0026#34;\\n\u0026#34;); } 6.4.7. 死循环的两种写法 while 循环：\n1 2 3 while (true) { // 执行内容 } for 循环：\n1 2 3 for (; ; ) { // 执行内容 } 6.4.8. 跳出当前的多重嵌套循环 Java 支持带标签的 break 和 continue 语句，作用有点类似于 C 和 C++ 中的 goto 语句。可用于跳出多重循环，可以在外面的循环语句前定义一个标号，然后在里层循环体的代码中使用带有标号的 break 语句，即可跳出外层循环。例如：\n1 2 3 4 5 6 7 8 9 10 11 public static void main(String[] args) { ok: for (int i = 0; i \u0026lt; 10; i++) { for (int j = 0; j \u0026lt; 10; j++) { System.out.println(\u0026#34;i=\u0026#34; + i + \u0026#34;,j=\u0026#34; + j); if (j == 5) { break ok; } } } } 但是就像要避免使用 goto 一样，应该避免使用带标签的 break 和 continue，因为它不会让你的程序变得更优雅，很多时候甚至有相反的作用。建议使用自定义中断标识变量，通过逻辑来跳出多重循环。\n7. 数组 7.1. 概述 数组对于每一门编程语言来说都是重要的数据结构之一，Java 语言中提供的数组是用来存储固定大小的同类型元素。\n7.1.1. 数组声明（定义）语法 声明数组变量的语法，有如下两种方式：\n方式一(推荐)： 1 数据类型[] 数组变量名; 方式二： 1 数据类型 数组变量名[]; Notes: 建议使用 dataType[] arrayRefVar 的声明风格声明数组变量。dataType arrayRefVar[] 风格是来自 C/C++ 语言，在 Java 中采用是为了让 C/C++ 程序员能够快速理解 Java 语言。\n示例：\n1 2 3 4 5 6 // 定义了一个int类型的数组，数组名是arr int[] arr; double[] myList; // 定义了一个int类型的变量，变量名是arr数组 int arr[]; double myList[]; 7.1.2. 数组初始化(创建数组) 数组初始化，就是为数组开辟内存空间，并为数组中的每个元素赋予初始值。Java 语言使用 new 关键字来创建数组，有以下两种方式可以实现数组的初始化：\n动态初始化：只给数组规定长度，由系统给出初始化值。例如： 1 int[] arr = new int[3]; 静态初始化：给数组初始化值，由系统决定长度。例如： 1 2 3 int[] arr = new int[]{1,2,3}; // 简化版 int[] arr = {1,2,3}; 7.1.3. 数组的分类 数组可以分为：基本类型的数组与对象数组。\n基本类型的数组：存储的元素为基本类型 1 int[] arr = {1,2,3,4}; 对象数组：存储的元素为引用类型 1 2 3 // Student 是一个自定义类 Student[] stus = new Student[3]; // Stus 数组中 stus[0],stus[1],stus[2] 的元素数据类型为 Student 的对象，初始化后默认保存是 null 7.2. 数组的使用 7.2.1. 数组的长度属性 数组提供了一个属性：length，用于获取数组中数元素的个数。\n1 int arrLength = 数组变量名.length 7.2.2. 数组的索引 数组的元素是通过索引访问的。数组索引从 0 开始，因此索引值范围从 0 到 数组.length-1。\n1 2 3 int[] arr = {1,2,3}; // 根据索引获取数组第二个元素 int a = arr[1]; 7.2.3. 循环数组 数组的元素类型和数组的大小都是确定的，所以当处理数组元素时候，通常使用基本循环或者 For-Each 循环。\n基础 for 循环 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 double[] myList = {1.9, 2.9, 3.4, 3.5}; // 打印所有数组元素 for (int i = 0; i \u0026lt; myList.length; i++) { System.out.println(myList[i] + \u0026#34; \u0026#34;); } // 计算所有元素的总和 double total = 0; for (int i = 0; i \u0026lt; myList.length; i++) { total += myList[i]; } System.out.println(\u0026#34;Total is \u0026#34; + total); // 查找最大元素 double max = myList[0]; for (int i = 1; i \u0026lt; myList.length; i++) { if (myList[i] \u0026gt; max) max = myList[i]; } System.out.println(\u0026#34;Max is \u0026#34; + max); JDK 1.5 引进了一种新的循环类型，被称为 For-Each 循环或者加强型循环，它能在不使用下标的情况下遍历数组。语法格式如下： 1 2 3 for(数据类型 数组中元素变量: 数组变量名) { // do something... } 示例\n1 2 3 4 5 6 double[] myList = {1.9, 2.9, 3.4, 3.5}; // 打印所有数组元素 for (double element: myList) { System.out.println(element); } 7.2.4. 数组常见小问题 ArrayIndexOutOfBoundsException（数组索引越界异常），产生的原因是访问了不存在的索引 NullPointerException（空指针异常），产生的原因是数组已经不在指向堆内存的数据了，还使用数组变量去访问元素 7.3. 二维数组【了解】 二维数组，就是一个特殊的一维数组，该数组的元素是一维数组。\n7.3.1. 定义语法 有以下三种声明二维数组的方式：\n数据类型[][] 数组名; (推荐的方式) 数据类型 数组名[][]; 数据类型[] 数组名[]; 7.3.2. 初始化语法 二维数组的初始化语法与普通数组一样，有以下两种方式：\n动态初始化 1 数据类型[][] 数组名 = new 数据类型[m][n]; 参数 m 表示的是二维数组中一维数组的个数 参数 n 表示的是一维数组中的元素个数 静态初始化 1 2 3 数据类型[][] 数组名 = new 数据类型[][]{{元素...},{元素...},{元素...},...}; // 简化格式： 数据类型[][] 数组名 = {{元素...},{元素...},{元素...},...}; 7.3.3. 数据操作 二维数组名配合索引可以获取到每一个元素（一维数组）。每一个一维数组配合索引名可以获取到数组中的元素。\n假如有一个二维数组：arr\n要从中获取某个元素（一维数组）：arr[索引] 要从中获取二维数组的元素：arr[索引][索引] 8. static 关键字 8.1. 概述 static 是一个修饰符。可以用于修饰内部类、成员变量、成员方法以及代码块。\nstatic 关键字的主要意义是，在于创建独立于具体对象的域变量或者方法。以致于即使没有创建对象，也能使用属性和调用方法！还有一个比较关键的作用就是，用来形成静态代码块以优化程序性能。\nTips: 为什么说 static 块可以用来优化程序性能，是因为它的特性：只会在类加载的时候执行一次。\n8.2. static 修饰变量 有 static 修饰的变量，称为静态变量（类变量）。没有 static 修饰的变量，称为成员变量（实例变量）。\n1 private static 类型 变量名称 = 值; 8.2.1. 静态变量的特点 static 关键字用来声明的静态变量是独立于对象的，静态成员变量是属于类，不再属于某个对象，会被该类的所有对象共享。无论一个类实例化多少对象，它的静态变量只有一份拷贝，若有一个对象修改了静态变量的值，其他对象会受影响。 在类被第一次加载的时候，就会去加载被 static 修饰的变量，而且只在类第一次使用时加载并进行初始化。注：后面可以根据需要再次赋值。 static 修饰的成员变量，在类的加载过程中，JVM 只为静态变量分配一次内存空间，以后创建该类对象的时候不会重新分配。而多个静态变量的初始化顺序是按照定义顺序来进行。 static 修饰的变量（或者方法）是优先于对象存在的，即当一个类加载完毕之后，即便没有创建对象，也可以通过 类名.静态变量名 或者 类名.静态方法名 进行访问。 8.2.2. 静态变量使用方式 建议使用类名访问(类名.xxx)静态属性，不推荐使用对象访问(对象名.xxx)。\n8.3. static 修饰方法 使用 static 修饰的方法，称为静态方法（类方法）。没有 static 修饰的方法，称为成员方法（实例对象方法）。\nstatic 关键字声明的静态方法是属于当前类，而不属于某个对象的，静态方法不能被重写。静态方法中不能使用类的非静态变量，也不能使用this或者super关键字。\n1 2 3 public static void foo(参数列表) { // ... } 可以通过类名直接调用静态方法（推荐），即 类名.静态方法；也可以使用实例调用（不推荐），即 对象.静态方法\n8.4. static 修饰代码块 被 static 修饰的代码块称为“静态代码块”，可以定义在类中的任意位置，并且可以定义多个。在类初次被加载的时候，会按照定义的顺序来执行每个 static 代码块，并且只会执行一次。静态代码块的执行优先级高于非静态的代码块。\n1 2 3 4 5 6 7 8 9 // 静态代码块 static { // ... } // 非静态代码块 { // ... } Tips: 根据静态代码只会在类加载的时候执行一次的特性。因此最常见的应用场景就是，将一些只需要进行一次的初始化操作都放在 static 代码块中进行。\n8.5. static 修饰类【只能修饰内部类也就是静态内部类】（待整理） static 修饰类，只能用于修饰内部类也就是静态内部类。\n8.6. 静态导包（待整理） 在 JDK 1.5 之后引入的新特性，import static 静态导包。可以用来指定导入某个类中的静态资源，并且不需要使用类名，可以直接使用类的静态变量与静态方法。比如：\n1 2 3 4 5 6 7 8 import static java.lang.Math.*; public class Test{ public static void main(String[] args){ // System.out.println(Math.sin(20)); // 传统写法 System.out.println(sin(20)); // 静态导包后的写法 } } 8.7. 总结 8.7.1. 静态成员变量和成员变量的区别 语法区别 静态成员变量：有 static 修饰的; 成员变量：没有 static 修饰的; 数量区别 静态成员变量：在内存中只存在一份，在类的加载过程中，JVM只为静态变量分配一次内存空间并初始化一次，会受每一个对象的影响 成员变量：每创建一个对象，都会为成员变量分配内存。每一个对象都有一份自己的成员变量。互不干扰的。 生命周期区别 静态成员变量：在类加载的时候完成内存分配并初始化。(类只会加载一次)。跟随类的卸载面销毁。 成员变量：在创建对象的时候完成内存分配和初始化。跟随对象的销毁而销毁。 访问方式区别 静态成员变量：可以通过类名访问(类.xxx)，也可以通过对象名访问(不推荐); 成员变量：只能通过对象名访问(对象名.xxx); 8.7.2. 静态方法和成员方法的区别 调用方式 静态方法可以通过类名调用(类.xxx)，也可以通过对象名调用(不推荐); 成员方法只能通过对象名调用(对象名.xxx); 成员的访问限制 静态方法中不能访问非静态成员(成员变量和成员方法)，只能访问带有 static 修饰的静态变量 成员方法则无成员的访问限制。如果在本类中，直接通过成员变量名或方法名来使用，在其他类中，需要(类名.成员变量名)或(类.方法名)才能使用; 8.7.3. static 的注意事项 静态方法中不能使用 this 和 super 关键字。(因为 this 是代表当前对象的引用，如果没有创建对象， this 没有任何意义。)\n8.7.4. static 应用场景 静态变量 当某个成员变量在值需要在该类的所有对象共享时就可以将该变量定义为静态成员变量。 静态方法 如果方法中没有使用任何非静态成员，就可以将该方法定义为静态方法（因为静态方法可以直接用类名调用『类.xxx』，比较方便）。 定义工具类时，如果一个类中的所有方法都是静态方法，则该类可以认为是一个工具类。 静态代码块 修饰类【只能修饰内部类也就是静态内部类】 静态导包 9. final 关键字 9.1. final 概述 final 也是一个修饰符。可以修饰变量、方法、类\n9.2. final 修饰变量 修饰基本数据类型的变量：被它修饰的变量其实是一个常量(常量命名是全部字母大写，多个单词用下划线 _ 分隔)，只能赋值一次，不能再修改。常用格式： 1 public static final int NUM = 10; 修饰引用数据类型变量：此时该引用变量就不能再指向其他对象，但可以修改已经指向对象的成员变量的值(只要该成员变量不是使用 final 修饰)。相当于此引用变量的地址值固定，不能改变。 9.3. final 修饰方法 使用 final 修饰的方法不能被子类重写。其目的有以下两个：\n第一个原因是把方法锁定，以防任何继承类修改它的含义； 第二个原因是效率。在早期的 Java 实现版本中，会将 final 方法转为内嵌调用。内联对于提升 Java 运行效率作用重大，能够使性能平均提高 50%。但是如果方法过于庞大，可能看不到内嵌调用带来的任何性能提升（现在的 Java 版本已经不需要使用 final 方法进行这些优化了）。 Notes: 类中所有的 private 方法都隐式地指定为 final。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // 原代码 public static void test(){ String s1 = \u0026#34;包夹方法a\u0026#34;; a(); String s2 = \u0026#34;包夹方法a\u0026#34;; } public static final void a(){ System.out.println(\u0026#34;我是方法a中的代码\u0026#34;); System.out.println(\u0026#34;我是方法a中的代码\u0026#34;); } // 经过编译后 public static void test(){ String s1 = \u0026#34;包夹方法a\u0026#34;; System.out.println(\u0026#34;我是方法a中的代码\u0026#34;); System.out.println(\u0026#34;我是方法a中的代码\u0026#34;); String s2 = \u0026#34;包夹方法a\u0026#34;; } 9.4. final 修饰类 使用 final 修饰类的目的简单明确：表明该类不能再被其他类继承。 被 final 修饰的类中，所有成员方法都会被隐式地指定为 final 方法。(就不存在方法重写的情况，只能创建对象和调用。) 1 2 public final class Xxx{ } 9.5. final 注意事项 当引用变量使用 final 修饰时，表示其指向的地址值不能发生改变，但指向对象的成员变量值可以改变。\n9.6. 常量治理 虽然推荐在 java 中使用枚举来对数据字典及常量进行控制，但是有些时候，还是使用常量控制更为便捷。比如，对于数据字典，可以使用枚举值来处理；对于一些其他的信息，还是会使用常量保存和使用。\n9.6.1. 代码的坏味道 很多程序中的常量类，随着业务与功能的迭代，类中定义的常量越来越多，导致类中行数过多，造成查找内容十分不方便。例如：\n1 2 3 4 5 6 7 8 9 10 11 12 public class Constants { public static final String REAL_NAME1 = \u0026#34;v1\u0026#34;; public static final String REAL_NAME2 = \u0026#34;v2\u0026#34;; public static final String REAL_NAME3 = \u0026#34;v3\u0026#34;; public static final String REAL_NAME4 = \u0026#34;v4\u0026#34;; public static final String REAL_NAME5 = \u0026#34;v5\u0026#34;; public static final String REAL_NAME5 = \u0026#34;v6\u0026#34;; public static final String REAL_NAME7 = \u0026#34;v7\u0026#34;; public static final String REAL_NAME8 = \u0026#34;v8\u0026#34;; public static final String REAL_NAME9 = \u0026#34;v9\u0026#34;; ...... } 一个无穷尽的常量类，设想这个类篇幅巨长，这样的常量管理会带来的问题如下：\n不好维护，相关的代码写到一起，此时，常量的篇幅较长导致找不到对应的常量块进行维护； 虽然是在不同的业务场景下，但是有些常量的名称还是有可能重复； 有时为了减少常量的定义，就得共用一些常量，而这样的共用会导致某种业务场景下需要对该常量进行修改，而导致另外一些业务场景下的常量使用产生歧义； 因此常能看到一些常量类的“代码里的坏味道”，假如常量名称如上所示，名称类似的很多；名称不明确的也很多，还没有注释，这样的歧义也是因为代码不好管理造成的。想对这种常量类进行新增或修改内容，是十分困难。\n9.6.2. 初级治理 - 使用内部类 使用 java 的内部类进行常量的初步治理，对常量根据不同的业务模块进行管理，代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 /** * Created by SZBright on 2017/3/1. * * @author : */ public class Constants { public static final class TOKEN_FLAG_ONE { public static final String REAL_NAME = \u0026#34;v1\u0026#34;; public static final String CRET = \u0026#34;v2\u0026#34;; public static final String GUR = \u0026#34;v5\u0026#34;; } } 这样的好处是，通过常量的内部类的名称，可以直接获取对应模块的常量的引用信息。使用代码如下：\n1 2 3 4 @Test public void test(){ System.out.println(Constants.TOKEN_FLAG_ONE.REAL_NAME); } 9.6.3. 中级治理 - 集中管理 在初级治理中，是无法实现通过 value 来获取到常量 key 。进而出现中级治理，主要是通过 map，每个内部类都会存为 map 中的一个 entry，每个 entry 又都是 map 类型的集合，集合中包含该内部类的所有常量。\n代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Constants { public static final Map\u0026lt;String, Map\u0026lt;String, String\u0026gt;\u0026gt; keyValueMapCons = new LinkedHashMap\u0026lt;\u0026gt;(); public static final Map\u0026lt;String, Map\u0026lt;String, String\u0026gt;\u0026gt; valueKeyMapCons = new LinkedHashMap\u0026lt;\u0026gt;(); /* 初始化所有常量 */ static { try { // 获取所有内部类 for (Class cls : Constants.class.getClasses()) { Map\u0026lt;String, String\u0026gt; keyValueMap = new LinkedHashMap\u0026lt;\u0026gt;(); // 存放 key 和 value 的 map Map\u0026lt;String, String\u0026gt; valueKeyMap = new LinkedHashMap\u0026lt;\u0026gt;(); // 存放 value 和 key 的 map，每个内部类-获取所有属性（不包括父类的） for (Field fd : cls.getDeclaredFields()) { keyValueMap.put(fd.getName(), fd.get(cls).toString()); // 注解对象空，其值为该 field 的值 valueKeyMap.put(fd.get(cls).toString(), fd.getName()); } // 以内部类的名称作为 key 保存 keyValueMapCons.put(cls.getSimpleName(), keyValueMap); valueKeyMapCons.put(cls.getSimpleName(), valueKeyMap); } } catch (Exception e) { e.printStackTrace(); } } public static final class TOKEN_FLAG_ONE { public static final String REAL_NAME = \u0026#34;v1\u0026#34;; public static final String CRET = \u0026#34;v2\u0026#34;; public static final String GUR = \u0026#34;v5\u0026#34;; } } 示例中在 Constants 类中维护了两个 Map 常量集合：\n一个名为 keyValueMapCons 的 Map 集合，按照内部类的名称作为 key 存储，其 value 是一个常量 Map 集合，该结构是将常量的名字作为 map 的 key，值作为 map 的 value； 一个名为 valueKeyMapCons 的 Map 集合，按照内部类的名称作为 key 存储，其 value 是一个常量 Map 集合，该结构是将常量的值作为 map 的 key，常量的名字作为 map 的 key。 这样就可以通过这两个 map 集合，根据不同的的使用需求来获取相应的常量。使用示例代码如下：\n1 2 3 4 5 @Test public void test1(){ System.out.println(Constants.keyValueMapCons.get(\u0026#34;TOKEN_FLAG_ONE\u0026#34;).get(\u0026#34;REAL_NAME\u0026#34;)); // v1 System.out.println(Constants.valueKeyMapCons.get(\u0026#34;TOKEN_FLAG_ONE\u0026#34;).get(\u0026#34;v5\u0026#34;)); // GUR } 9.6.4. 中高级治理 - 使用注解 目前实现了通过 key 获取到 value，也可以通过 value 获取到 key 了。但在使用常量时，不光要有常量的定义、常量的值，还应该有对常量的描述，而传统的对于常量的定义，往往只在通过 ide 的文档功能来查看常量的描述。\n可以通过注解的方式来实现存储常量描述的功能，例如自定义注解类型如下：\n1 2 3 4 5 @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.TYPE}) public @interface ConstantAnnotation { String value(); // 用于记录常量的描述 } 通过自定义的注解，将常量的中文描述都记录到注解的 value 属性中。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Constants { private static final String CONSTANT_STRING = \u0026#34;这是一条短信消息\u0026#34;; public static final class TOKEN_FLAG_ONE { @ConstantAnnotation(\u0026#34;实名\u0026#34;) public static final String REAL_NAME = \u0026#34;v1\u0026#34;; @ConstantAnnotation(\u0026#34;证书\u0026#34;) public static final String CRET = \u0026#34;v2\u0026#34;; @ConstantAnnotation(CONSTANT_STRING) // 把描述与注解分离 public static final String GUR = \u0026#34;v5\u0026#34;; } } 比如，我们希望在某个业务场景下，符合gur常量的业务，发送一条短信消息，而这个消息我们就可以定义在我们的自定义注解中。例如GUR这个常量，我们把它的描述声明成一个常量，这个常量可用来存放对应的短信消息。我们的常量类中如果再有一个通过常量获取到描述的map，这是不是就完美了？\n于是，我们有了下面的代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class Constants { // 存储常量描述的 Map 集合，根据常量名称来获取常量描述的值 public static final Map\u0026lt;String, Map\u0026lt;String, String\u0026gt;\u0026gt; keyDescMapCons = new LinkedHashMap\u0026lt;\u0026gt;(); /* 初始化所有常量 */ static { try { // 获取所有内部类 for (Class cls : Constants.class.getClasses()) { Map\u0026lt;String, String\u0026gt; keyDescMap = new LinkedHashMap\u0026lt;\u0026gt;(); // 存放常量 key 和 desc 的 map // 每个内部类-获取所有属性（不包括父类的） for (Field fd : cls.getDeclaredFields()) { // 每个属性获取指定的 annotation 的注解对象 ConstantAnnotation ca = fd.getAnnotation(ConstantAnnotation.class); if (ca != null) { keyDescMap.put(fd.getName(), ca.value()); // 注解对象不空，常量的描述即为注解对象中的值 } } keyDescMapCons.put(cls.getSimpleName(), keyDescMap); } } catch (Exception e) { e.printStackTrace(); } } private static final String CONSTANT_STRING = \u0026#34;这是一条短信消息\u0026#34;; public static final class TOKEN_FLAG_ONE { @ConstantAnnotation(\u0026#34;实名\u0026#34;) public static final String REAL_NAME = \u0026#34;v1\u0026#34;; @ConstantAnnotation(\u0026#34;证书\u0026#34;) public static final String CRET = \u0026#34;v2\u0026#34;; @ConstantAnnotation(CONSTANT_STRING) // 把描述与注解分离 public static final String GUR = \u0026#34;v5\u0026#34;; } } 经过改造后，可以通过 keyDescMap 集合，根据常量的名称获取到该常量对应的描述了。使用示例代码如下：\n1 2 3 4 @Test public void test1(){ System.out.println(Constants.keyDescMapCons.get(\u0026#34;TOKEN_FLAG_ONE\u0026#34;).get(\u0026#34;GUR\u0026#34;)); // 打印输出“这是一条短消息” } 9.6.5. 综合治理（终极治理） 目前有了常量的治理，有了注解的描述，有时需要通过key获取到value，有时需要通过value获取描述，有时需要通过key获取到描述等等。排列组合共6种形式，以下是综合治理的实现（虽然实际项目中不一定有用得上）。代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 public class Constants { public static final Map\u0026lt;String, Map\u0026lt;String, String\u0026gt;\u0026gt; keyValueMapCons = new LinkedHashMap\u0026lt;\u0026gt;(); public static final Map\u0026lt;String, Map\u0026lt;String, String\u0026gt;\u0026gt; keyDescMapCons = new LinkedHashMap\u0026lt;\u0026gt;(); public static final Map\u0026lt;String, Map\u0026lt;String, String\u0026gt;\u0026gt; descValueMapCons = new LinkedHashMap\u0026lt;\u0026gt;(); public static final Map\u0026lt;String, Map\u0026lt;String, String\u0026gt;\u0026gt; descKeyMapCons = new LinkedHashMap\u0026lt;\u0026gt;(); public static final Map\u0026lt;String, Map\u0026lt;String, String\u0026gt;\u0026gt; valueDescMapCons = new LinkedHashMap\u0026lt;\u0026gt;(); public static final Map\u0026lt;String, Map\u0026lt;String, String\u0026gt;\u0026gt; valueKeyMapCons = new LinkedHashMap\u0026lt;\u0026gt;(); /* 初始化所有常量 */ static { try { // 获取所有内部类 for (Class cls : Constants.class.getClasses()) { Map\u0026lt;String, String\u0026gt; keyDescMap = new LinkedHashMap\u0026lt;\u0026gt;(); // 存放key和desc的map Map\u0026lt;String, String\u0026gt; keyValueMap = new LinkedHashMap\u0026lt;\u0026gt;(); // 存放key和value的map Map\u0026lt;String, String\u0026gt; valueKeyMap = new LinkedHashMap\u0026lt;\u0026gt;(); // 存放value和key的map Map\u0026lt;String, String\u0026gt; valueDescMap = new LinkedHashMap\u0026lt;\u0026gt;(); // 存放value和desc的map Map\u0026lt;String, String\u0026gt; descValueMap = new LinkedHashMap\u0026lt;\u0026gt;(); // 存放desc和value的map Map\u0026lt;String, String\u0026gt; descKeyMap = new LinkedHashMap\u0026lt;\u0026gt;(); // 存放desc和key的map // 每个内部类-获取所有属性（不包括父类的） for (Field fd : cls.getDeclaredFields()) { // 每个属性获取指定的 annotation的 注解对象 ConstantAnnotation ca = fd.getAnnotation(ConstantAnnotation.class); keyValueMap.put(fd.getName(), fd.get(cls).toString()); // 获取该 field 的名称 valueKeyMap.put(fd.get(cls).toString(), fd.getName()); if (ca != null) { keyDescMap.put(fd.getName(), ca.value()); // 注解对象不空，常量的描述即为注解对象中的值 valueDescMap.put(fd.get(cls).toString(), ca.value()); descValueMap.put(ca.value(), fd.get(cls).toString()); descKeyMap.put(ca.value(), fd.getName()); } } keyValueMapCons.put(cls.getSimpleName(), keyValueMap); keyDescMapCons.put(cls.getSimpleName(), keyDescMap); descValueMapCons.put(cls.getSimpleName(), descValueMap); descKeyMapCons.put(cls.getSimpleName(), descKeyMap); valueDescMapCons.put(cls.getSimpleName(), valueDescMap); valueKeyMapCons.put(cls.getSimpleName(), valueKeyMap); } } catch (Exception e) { e.printStackTrace(); } } private static final String CONSTANT_STRING = \u0026#34;这是一条短信消息\u0026#34;; public static final class TOKEN_FLAG_ONE { @ConstantAnnotation(\u0026#34;实名\u0026#34;) public static final String REAL_NAME = \u0026#34;v1\u0026#34;; @ConstantAnnotation(\u0026#34;证书\u0026#34;) public static final String CRET = \u0026#34;v2\u0026#34;; @ConstantAnnotation(CONSTANT_STRING) // 把描述与注解分离 public static final String GUR = \u0026#34;v5\u0026#34;; } } 以上就是综合治理后的常量类，使用先拿常量的声明，再用对应的map，然后指定内部类名称，根据具体需要获取相应的内容。\nTips: 枚举治理和常量治理结合使用，可能才是系统开发时的最佳实践\n10. 包 package 10.1. 包的概述 包就是文件夹。分包管理是组织软件项目结构的基本方式。将同类功能放到一个包中，方便管理。并且日常项目的分工也是以包作为边界。\n包在文件系统中是以文件夹的形式存在的。类中定义的包必须与实际class件所在的文件夹情况相统一，即定义包时类在a包下，则生成的.class 文件必须在a文件夹下，否则找不到类。\n10.2. 包的作用 避免类命名冲突。 将功能相似或相关的类和接口组织在同一个文件夹，方便类的查找和使用。 10.3. 包的定义格式 定义格式：package com.qq.包名1.xxx...; 规范：一般以公司域名倒着写，最后是功能内容的分类。即 www.qq.com ===\u0026gt; com.qq.login 多级包使用“.”分割，包名全部小写英文字母，一般不用数字。 10.4. 包的注意事项 定义包的语句必须是类中第一行语句。 如果定义多个类，只能有一个是 public 修饰。且文件名一定要与 public 修饰的类名一致。 类和所生成的 class 必须在相同的目录结构下。 10.5. 导包格式 导包位置：package 的下面，class 的上面\n1 2 import 包名.包名.xxx...类名; import java.util.*;\t// 将指定包下的所有类导入 10.6. 类的访问方式 带包类全名访问：\n当在一个类中需要使用两个不同包下同名的类时，只能有一个类被导包，另一个类只能使用类全名方式访问。\n不带包名访问：\n被使用的类在java.lang包下。 被使用的类和当前类是在同一个包下。 使用导包方式访问。 跨包导包访问注意事项：如果在一个类中需要使用不同包下相同类名的类，只能有一个被导包，其他只能通过类全名访问。\ncode demo\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.moon.packaeg; import com.moon.statci.Student; public class Test01 { public static void main(String[] args) { // 创建Person对象 // 带包类全名访问:不需要导入 com.moon.packaeg.Person p = new com.moon.packaeg.Person(); // 创建学生对象 Student s = new Student(); // 在java.lang包下的类：不需要导包 String str = \u0026#34;nihao\u0026#34;; com.moon.finla.Student s1 = new com.moon.finla.Student(); s1.sleep(); Person p1 = new Person(); } } 10.7. JDK 中常用的包 java.lang：系统的基础类 java.io：所有输入输出有关的类，比如文件操作等 java.nio：为了完善 io 包中的功能，提高 io 包中性能而写的一个新包 java.net：与网络有关的类 java.util：系统辅助类，特别是集合类 java.sql：数据库操作的类 Tips: java 开头的包是较原始的 JavaAPI，javax 开头的包是扩展的 API\n11. 可变参数 11.1. 可变参数概述 JDK1.5的新特性，方法的参数类型相同，但是个数变化。 可变参数可以多个，也可以不传值。 11.2. 可变参数格式 使用前提：数据类型明确，参数个数任意。 语法定义：数据类型… 【注意是3个点(.)】 1 2 修饰符 返回值类型 方法名(数据类型… 变量名) { } 与普通方法相比在参数类型后面添加…\n11.3. 可变参数的本质 可变参数方法本质是数组。所以不可以与数组类型参数重载。\n11.4. 可变参数注意事项 参数列表中只能有一个可变参数 如果出现不同类型的参数，可变参数必须放在参数列表的最后 12. GUI 图形用户界面 (了解) Graphical User Interface，简称 GUI\n1 JOptionPane.showMessageDialog(null, x); x就是要显示的文本字符串，如：\u0026ldquo;Welcome to Java.\u0026rdquo;\t用于弹出消息对话框，需要导包\n1 JOptionPane.showMessageDialog(null, x, y, JOptionPane.INFORMATION_MESSAGE); X是显示的文本字符串，y是表示消息对话框标题的字符串。第四个参数可以是 JOptionPane.INFORMATION_MESSAGE (它是为了让消息框能够显示图标(!))\n如果是 JOptionPane.QUESTION_MESSAGE 让消息框能够显示图标(?)\n从输入对话框获取输入\n1 JOptionPane.showInputDialog(\u0026#34;xxx\u0026#34;) 13. Java 综合扩展知识 13.1. null == object 与 object == null 在 java 里面，null == object 与 object == null 两者效果是一样的。但是为什么要将 null 写到前面？具体点来说，是在 C 语言里面引申出来的。\n在 C 语言里面，为了防止少敲一个等号，编程人员仍然能在编译的时候找到错误。因为if(obj=null)是在编译的时候，不会出现错误，而if(null=obj)是会编译出错。\n当然了，在 java 里面，if(name=null)是会出现编译错误的，不用担心这个错误了。但是，良好的编程习惯，还是建议写成 null==objcet。\n因为，object!=null，这种形式，会导致判断之前先去读取 object 的信息，然后再判断是否 null。而 null!=object 这种形式，不会导致读取 object 信息，从而提高判断速度。一般来说不用考虑非写成后者，但对于性能要求高的系统来说就要考虑用后者了。\n14. 参考资料 developer.microsoft.com/java，微软推出了一个专门针对 Java 所有相关内容的新网站，该网站提供了微软 Java 云开发团队的最新内容以及技术文档、工具、资源、教程、视频和代码示例。 ","permalink":"https://ktzxy.top/posts/i1ukzwmw0x/","summary":"Java基础 语法","title":"Java基础 语法"},{"content":"﻿### 实现从表中查询所有学生基本信息的存储过程（存储过程名称一定要为 proc_student_info\n1 2 3 4 5 create procedure proc_student_info as Begin select * from student End 1 exec proc_student_info 创建一个带参数的存储过程，输出指定学号的学生信息； 1 2 3 4 5 6 create procedure proc_sno @sno_input varchar(30) as Begin select * from student where sno = @sno_input End 1 exec proc_sno \u0026#39;1001\u0026#39; 创建一个带参数的存储过程，根据指定参数增加学生信息，如果学生编号已经存在则不能增加(调用此存储过程时，会依次填充各个字段值，请注意 insert 时，参数顺序与表字段的顺序一致)； 1 2 3 4 5 6 7 8 9 10 11 12 13 14 create procedure proc_add @sno varchar(100), @sname varchar(100), @sex varchar(100), @birthday date, @discipline varchar(100), @school varchar(100) as Begin if exists(select * from student where sno = @sno) print \u0026#39;Already have a primary key \u0026#39;+@sno else insert into student values(@sno,@sname,@sex,@birthday,@discipline,@school) End 1 2 3 4 exec proc_add \u0026#39;1004\u0026#39;,\u0026#39;HMM\u0026#39;,\u0026#39;female\u0026#39;,\u0026#39;2019-6-2\u0026#39;,\u0026#39;English\u0026#39;,\u0026#39;national school\u0026#39; go exec proc_student_info go 创建一个带参数的存储过程，删除指定学号的学生信息。若成功，则输出 successfully deleted ；若没有该学号，则输出 No such student 1 2 3 4 5 6 7 8 9 10 11 12 create procedure student_del @sno varchar(100) as Begin if exists (select * from student where sno = @sno) Begin delete from student where sno = @sno print \u0026#39;successfully deleted\u0026#39; End else print \u0026#39;No such student\u0026#39; End 1 2 3 4 go exec student_del \u0026#39;1001\u0026#39; go exec proc_student_info ","permalink":"https://ktzxy.top/posts/ywrw27f2s9/","summary":"笔记二：存储过程部分","title":"笔记二：存储过程部分"},{"content":"1. 基础语法 1.1. 标题（Headings） 常规写法 要创建标题，请在单词或短语前面添加井号 (#) 。井号的数量代表了标题的级别。例如，添加三个井号即创建一个三级标题 (\u0026lt;h3\u0026gt;) (例如：### My Header)。\nMarkdown HTML 渲染效果 # Heading level 1 \u0026lt;h1\u0026gt;Heading level 1\u0026lt;/h1\u0026gt; Heading level 1 ## Heading level 2 \u0026lt;h2\u0026gt;Heading level 2\u0026lt;/h2\u0026gt; Heading level 2 ### Heading level 3 \u0026lt;h3\u0026gt;Heading level 3\u0026lt;/h3\u0026gt; Heading level 3 #### Heading level 4 \u0026lt;h4\u0026gt;Heading level 4\u0026lt;/h4\u0026gt; Heading level 4 ##### Heading level 5 \u0026lt;h5\u0026gt;Heading level 5\u0026lt;/h5\u0026gt; Heading level 5 ###### Heading level 6 \u0026lt;h6\u0026gt;Heading level 6\u0026lt;/h6\u0026gt; Heading level 6 可选语法 还可以在文本下方添加任意数量的 == 号来标识一级标题，或者 -- 号来标识二级标题。\nMarkdown HTML 渲染效果 Heading level 2============== \u0026lt;h2\u0026gt;Heading level 2\u0026lt;/h2\u0026gt; Heading level 2 Heading level 3--------------- \u0026lt;h3\u0026gt;Heading level 3\u0026lt;/h3\u0026gt; Heading level 3 1.2. 段落（Paragraphs） 要创建段落，使用空白行将一行或多行文本进行分隔。\n1.3. 换行（Line Breaks） 在一行的末尾添加两个或多个空格，然后按回车键（return），即可创建一个换行（line break）或新行 (\u0026lt;br/\u0026gt;)。\nMarkdown HTML 渲染效果 first line. second line. \u0026lt;p\u0026gt;first line.\u0026lt;br\u0026gt;second line.\u0026lt;/p\u0026gt; first line.second line. first line.\u0026lt;br\u0026gt;second line. \u0026lt;p\u0026gt;first line.\u0026lt;br\u0026gt;second line.\u0026lt;/p\u0026gt; first line.second line. 换行（Line Break）用法的最佳实践\n几乎每个 Markdown 应用程序都支持两个或多个空格进行换行 (称为 “结尾空格（trailing whitespace）”) 的方式，但这是有争议的，因为很难在编辑器中直接看到空格，并且很多人在每个句子后面都会有意或无意地添加两个空格。由于这个原因，可能需要使用除结尾空格以外的其它方式来进行换行。如果所使用的 Markdown 应用程序支持 HTML 的话，推荐使用 HTML 的 \u0026lt;br\u0026gt; 标签来实现换行。\n1.4. 强调（Emphasis） 通过将文本设置为粗体或斜体来强调其重要性。\n1.4.1. 粗体（Bold） 要加粗文本，请在单词或短语的前后各添加两个星号（asterisks）**或下划线（underscores）__。如需加粗一个单词或短语的中间部分用以表示强调的话，请在要加粗部分的两侧各添加两个星号（asterisks）。\nMarkdown HTML 渲染效果 I just love **bold text** I just love \u0026lt;strong\u0026gt;bold text\u0026lt;/strong\u0026gt; I just love bold text I just love __bold text__ I just love \u0026lt;strong\u0026gt;bold text\u0026lt;/strong\u0026gt; I just love bold text Love**is**bold Love\u0026lt;strong\u0026gt;is\u0026lt;/strong\u0026gt;bold Loveisbold 粗体（Bold）用法最佳实践：Markdown 应用程序在如何处理单词或短语中间的下划线上并不一致。为兼容考虑，在单词或短语中间部分加粗的话，请使用星号（asterisks）。\n1.4.2. 斜体（Italic） 要用斜体显示文本，请在单词或短语前后添加一个星号（asterisk）*或下划线（underscore）_。要斜体突出单词的中间部分，请在字母前后各添加一个星号，中间不要带空格。\nMarkdown HTML 渲染效果 Italicized text is the *cat's meow* Italicized text is the \u0026lt;em\u0026gt;cat's meow\u0026lt;/em\u0026gt; Italicized text is the cat’s meow Italicized text is the _cat's meow_ Italicized text is the \u0026lt;em\u0026gt;cat's meow\u0026lt;/em\u0026gt; Italicized text is the cat’s meow A*cat*meow A\u0026lt;em\u0026gt;cat\u0026lt;/em\u0026gt;meow Acatmeow 斜体（Italic）用法的最佳实践：Markdown 的众多应用程序在如何处理单词中间的下划线上意见不一致。为了兼容起见，请用星号标注文本斜体\n1.4.3. 粗斜体 Markdown HTML 渲染效果 This text is ***really important*** This text is \u0026lt;strong\u0026gt;\u0026lt;em\u0026gt;really important\u0026lt;/em\u0026gt;\u0026lt;/strong\u0026gt; This text is really important This text is ___really important___ This text is \u0026lt;strong\u0026gt;\u0026lt;em\u0026gt;really important\u0026lt;/em\u0026gt;\u0026lt;/strong\u0026gt; This text is really important This text is __*really important*__ This text is \u0026lt;strong\u0026gt;\u0026lt;em\u0026gt;really important\u0026lt;/em\u0026gt;\u0026lt;/strong\u0026gt; This text is really important This text is **_really important_** This text is \u0026lt;strong\u0026gt;\u0026lt;em\u0026gt;really important\u0026lt;/em\u0026gt;\u0026lt;/strong\u0026gt; This text is really important Markdown 应用程序在处理单词或短语中间添加的下划线上并不一致。为了实现兼容性，请使用星号加粗并以斜体显示\n1.5. 删除线（Strikethrough） 贯穿单词的中心放一条横线从而删除这些单词。其效果看起来是这样的：like this。此功能允许标记某些单词是错误的，不应该出现在文档中。在单词前面和后面分别放置两个波浪号（~~） 来表示删除这些单词。\n1 ~~The world is flat.~~ We now know that the world is round. 渲染效果如下：\nThe world is flat. We now know that the world is round.\n1.6. 块引用（Blockquotes） 1.6.1. 基础引用语法 要创建块引用，请在段落前添加一个 \u0026gt; 符号。 Notes: 为兼容起见，在分块引号前后放上空行。\n1 \u0026gt; Dorothy followed her through many of the beautiful rooms in her castle. 渲染效果如下：\nDorothy followed her through many of the beautiful rooms in her castle.\n1.6.2. 多个段落的块引用（Blockquotes） 块引用可以包含多个段落。为段落之间的空白行各添加一个 \u0026gt; 符号。\n1 2 3 \u0026gt; Dorothy followed her through many of the beautiful rooms in her castle. \u0026gt; \u0026gt; The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood. 渲染效果如下：\nDorothy followed her through many of the beautiful rooms in her castle.\nThe Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.\n1.6.3. 嵌套块引用（Nested Blockquotes） 块引用可以嵌套。在要嵌套的段落前添加一个 \u0026gt;\u0026gt; 符号。\n1 2 3 \u0026gt; Dorothy followed her through many of the beautiful rooms in her castle. \u0026gt; \u0026gt;\u0026gt; The Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood. 渲染效果如下：\nDorothy followed her through many of the beautiful rooms in her castle.\nThe Witch bade her clean the pots and kettles and sweep the floor and keep the fire fed with wood.\n1.6.4. 带有其它元素的块引用（Blockquotes with Other Elements） 块引用可以包含其他 Markdown 格式的元素。注意：并非所有元素都可以使用\n1 2 3 4 5 6 \u0026gt; #### The quarterly results look great! \u0026gt; \u0026gt; - Revenue was off the chart. \u0026gt; - Profits were higher than ever. \u0026gt; \u0026gt; *Everything* is going according to **plan**. 渲染效果如下：\nThe quarterly results look great! Revenue was off the chart. Profits were higher than ever. Everything is going according to plan.\n1.7. 列表（Lists） 可以将多个条目与内容组织成有序或无序列表\n1.7.1. 有序列表（Ordered Lists） 要创建有序列表，请在每个列表项前添加数字并紧跟一个英文句点。数字不必按数学顺序排列，但是列表应当以数字 1 起始。\n有序列表（Ordered List）用法的最佳实践：CommonMark 和其它几种轻量级标记语言可以使用括号（)）作为分隔符（例如 1) First item），但并非所有的 Markdown 应用程序都支持此种用法，因此，从兼容的角度来看，此用法不推荐。为了兼容起见，请只使用英文句点作为分隔符。\n1.7.2. 无序列表（Unordered Lists） 要创建无序列表，请在每个列表项前面添加破折号 (-)、星号 (*) 或加号 (+) 。缩进一个或多个列表项可创建嵌套列表。\n****无序列表（Unordered List）用法的最佳实践：Markdown 应用程序在如何处理同一列表中混用不同分隔符上并不一致。为了兼容起见，请不要在同一个列表中混用不同的分隔符，最好选定一种分隔符并一直用下去。\n1.7.3. 以数字开头的无序列表项 如果需要以数字开头并且紧跟一个英文句号（也就是 .）的无序列表项，则可以使使用反斜线（\\）来转义这个英文句号。\n1.7.4. 在列表中添加列表项 要在保留列表连续性的同时在列表中添加另一种元素，请将该元素缩进四个空格或一个制表符。\n段落（Paragraphs）\n1 2 3 4 5 6 * This is the first list item. * Here\u0026#39;s the second list item. I need to add another paragraph below the second list item. * And here\u0026#39;s the third list item. 渲染效果如下：\nThis is the first list item.\nHere\u0026rsquo;s the second list item.\nI need to add another paragraph below the second list item.\nAnd here\u0026rsquo;s the third list item.\n引用块（Blockquotes）\n1 2 3 4 5 6 * This is the first list item. * Here\u0026#39;s the second list item. \u0026gt; A blockquote would look great below the second list item. * And here\u0026#39;s the third list item. 渲染效果如下：\nThis is the first list item.\nHere\u0026rsquo;s the second list item.\nA blockquote would look great below the second list item.\nAnd here\u0026rsquo;s the third list item.\n代码块（Code Blocks）\n代码块（Code blocks）通常采用四个空格或一个制表符缩进。当它们被放在列表中时，请将它们缩进八个空格或两个制表符。\n1 2 3 4 5 6 7 8 1. Open the file. 2. Find the following code block on line 21: \u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Test\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; 3. Update the title to match the name of your website. 渲染效果如下：\nOpen the file.\nFind the following code block on line 21:\n\u0026lt;head\u0026gt; \u0026lt;title\u0026gt;Test\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; Update the title to match the name of your website.\n图片（Images）\n1 2 3 4 5 6 1. Open the file containing the Linux mascot. 2. Marvel at its beauty. ![Tux, the Linux mascot](/assets/images/tux.png) 3. Close the file. 渲染效果如下：\nOpen the file containing the Linux mascot.\nMarvel at its beauty.\nClose the file.\n列表（Lists），可以将无序列表嵌套在有序列表中，反之亦然。\n1 2 3 4 5 6 1. First item 2. Second item 3. Third item - Indented item - Indented item 4. Fourth item 渲染效果如下：\nFirst item Second item Third item Indented item Indented item Fourth item 1.8. 行内代码 1.8.1. 行内代码基础语法 要将单词或短语表示为代码，请将其包裹在反引号 (`) 中。\nMarkdown HTML 渲染效果 At the command prompt, type `nano` At the command prompt, type \u0026lt;code\u0026gt;nano\u0026lt;/code\u0026gt;. At the command prompt, type nano 1.8.2. 转义反引号 如果要表示为代码的单词或短语中包含一个或多个反引号，则可以通过将单词或短语包裹在双反引号(``)中。\nMarkdown HTML 渲染效果 ``Use `code` in your Markdown file.`` \u0026lt;code\u0026gt;Use `code` in your Markdown file.\u0026lt;/code\u0026gt; Use `code` in your Markdown file. 1.9. 代码块（Code Blocks） 1.9.1. 缩进式代码块 要创建代码块，可以将代码块的每一行缩进至少四个空格或一个制表符。\n1 2 3 4 \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;/html\u0026gt; 渲染效果如下：\n\u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;/html\u0026gt; Notes: 个人不推荐使用缩进的代码块，如果是html的代码，容易出现渲染问题，推荐使用下面围栏式代码块（fenced code blocks）.\n1.9.2. 围栏式代码块（fenced code blocks） Markdown 的基本语法允许你通过缩进四个空格或一个制表符来创建 代码块 。如果觉得不方便，可以试试围栏代码块（fenced code blocks）。根据 Markdown 解析器或编辑器的不同，代码块的前后可以使用三个反引号（```）或三个波浪号（~~~）来标记围栏代码块。不必费力缩进任何行了！许多 Markdown 解析器都支持围栏代码块的语法高亮功能。此功能允许编写代码所用的编程语言添加带颜色的语法高亮显示。如需添加语法高亮，请在围栏代码块前的反引号旁指定所用的编程语言。\n渲染效果如下：\n1 2 3 4 5 { \u0026#34;firstName\u0026#34;: \u0026#34;John\u0026#34;, \u0026#34;lastName\u0026#34;: \u0026#34;Smith\u0026#34;, \u0026#34;age\u0026#34;: 25 } 1.10. 分隔线（Horizontal Rules） 要创建分隔线，请在单独一行上使用三个或多个星号 (***)、破折号 (---) 或下划线 (___) ，并且不能包含其他内容。为了兼容性，请在分隔线的前后均添加空白行。\n1 2 3 4 5 *** --- _________________ 以上三个分隔线的渲染效果看起来都一样：\n1.11. 链接（Links） 要创建链接，请将链接文本括在方括号（例如 [moon]）中，后面紧跟着括在圆括号中的 URL（例如 (https://moon.com) ）。\n1 My favorite search engine is [Duck Duck Go](https://duckduckgo.com). 渲染效果如下：\nMy favorite search engine is Duck Duck Go.\n1.11.1. 添加标题 可以选择为链接添加标题（即 title 属性）。当用户将鼠标悬停在链接上时，将显示一个提示。将要添加标题放在 URL 后面即可\n1 My favorite search engine is [Duck Duck Go](https://duckduckgo.com \u0026#34;The best search engine for privacy\u0026#34;). 渲染效果如下：\nMy favorite search engine is Duck Duck Go.\n1.11.2. 网址和电子邮件地址 要将 URL 或电子邮件地址快速转换为链接，请将其括在尖括号中。\n1 2 \u0026lt;https://www.markdownguide.org\u0026gt; \u0026lt;fake@example.com\u0026gt; 渲染效果如下：\nhttps://www.markdownguide.org fake@example.com\n1.11.3. 格式化链接 如需强调（emphasize）某个链接，请在方括号前及圆括号后添加星号。要将链接表示为代码（code），请在方括号内添加反引号。\n1 2 3 I love supporting the **[EFF](https://eff.org)**. This is the *[Markdown Guide](https://www.markdownguide.org)*. See the section on [`code`](#code). 渲染效果如下：\nI love supporting the EFF.\nThis is the Markdown Guide.\nSee the section on code.\n1.11.4. 引用式链接 引用式（Reference-style）链接是一种特殊类型的链接，它使得 URL 在 Markdown 中更易于显示和阅读。引用式链接由两部分组成：一部分被放置在正文文本中；另一部分被放置在文档中的其它地方，以便于阅读。\n1.12. 图片（Images） 1.12.1. 图片基础语法 要添加图片，首先请添加感叹号（!），然后紧跟着是方括号，方括号中可添加替代文本（alt text，即图片显示失败后显示此文本），最后跟着圆括号，圆括号中添加图片资源的路径或 URL。你可以选择在圆括号中的 URL 之后添加标题（即 title 属性）。\n1 ![The San Juan Mountains are beautiful!](/assets/images/san-juan-mountains.jpg \u0026#34;San Juan Mountains\u0026#34;) 1.12.2. 带链接的图片 要为图片添加链接，请先为图片的 Markdown 标记添加一个方括号，然后紧跟着一个圆括号，并在圆括号中添加链接地址。\n1 [![An old rock in the desert](/assets/images/shiprock.jpg \u0026#34;Shiprock, New Mexico by Beau Rogers\u0026#34;)](https://xxx) 1.13. 表格（Tables） 1.13.1. 基础表格语法 如需添加表格，请使用三个或更多个连字符（---）来为每个列创建表头，并使用管道符（|）来分隔每个列。为兼容考虑，你还应该在行的两侧添加管道符。单元格（cell）宽度是可变的不会影响渲染效果。\n1 2 3 4 | Syntax | Description | | ----------- | ----------- | | Header | Title | | Paragraph | Text | 渲染效果如下：\nSyntax Description Header Title Paragraph Text Tips: 使用连字符（hyphens）和管道符（pipes）创建表格会很乏味。若要加快进度，可使用 Markdown 表格生成器。使用图形界面生成表格，然后将生成的 Markdown 格式的文本复制粘贴到文件中即可。\n1.13.2. 单元格内容对齐 通过在标题行中的连字符（hyphens）的左侧或右侧或两侧添加冒号（:），可以将对应列中的文本向左或向右或居中对齐。\n1 2 3 4 | Syntax | Description | Test Text | | :--- | :----: | ---: | | Header | Title | Here\u0026#39;s this | | Paragraph | Text | And more | 渲染效果如下：\nSyntax Description Test Text Header Title Here\u0026rsquo;s this Paragraph Text And more 1.13.3. 格式化表格中的文本 可以为表格中的文本设置格式。\n支持的格式包括：链接（links）、行内代码（code）（注意，只能为单词或短语添加反引号 (`) ，不能添加 代码块（code blocks））以及强调（emphasis）粗斜体。 不支持的格式包括：标题（headings）、块引用（blockquotes）、列表（lists）、水平分割线（horizontal rules）、图片（images）或 HTML 标记。 1.13.4. 转义表格中出现的管道符（Pipe Characters） 如需在表格中显示管道符 (|)，可以使用管道符的 HTML 字符编码（\u0026amp;#124; 或者 \u0026amp;#x7C;）来实现。\n1.14. 任务列表（Task Lists） 任务列表（task lists 或者 checklists）允许创建带有复选框的项目列表。在支持任务列表的 Markdown 应用程序中，复选框将显示在内容旁边。要创建任务列表，请在任务列表项前面添加破折号（-）和中间带空格的方括号（[ ]）。要选中复选框，请在方括号中间添加一个x，即（[x]）。\n1 2 3 - [x] Write the press release - [ ] Update the website - [ ] Contact the media 渲染效果如下：\nWrite the press release Update the website Contact the media 2. 扩展语法 2.1. 转义字符（Escaping Characters） 2.1.1. 基础语法 要显示原本用于格式化 Markdown 文档的字符，请在字符前面添加反斜杠字符 (\\) 。\n1 \\* 如果没有开头的反斜杠字符的话，这一行将显示为无序列表。 渲染效果如下：\n* 如果没有开头的反斜杠字符的话，这一行将显示为无序列表。\n2.1.2. 可做转义的（英文）字符 字符 名称 \\ 反斜杠（backslash） ` backtick * 星号（asterisk） _ 下划线（underscore） { } 花括号（curly braces） [ ] 方括号（brackets） \u0026lt; \u0026gt; angle brackets ( ) 圆括号或括号（parentheses） # 井号（pound sign） + 加号（plus sign） - 减号（minus sign） (也叫连字符 hyphen) . 句点（dot） ! 感叹号（exclamation mark） | 管道符（pipe） 2.2. 脚注（Footnotes） 脚注（Footnotes）允许添加注释（notes）和引用（references），而不会使文档正文混乱。当你创建脚注时，带有链接的上标数字会出现在你引用脚注的位置。读者可以单击链接以跳转至页面底部的脚注内容处。\n要创建一个脚注的引用，请在方括号中添加一个插入符（caret）以及一个标识符，标识符可以是数字或单词，但不能包含空格或制表符。标识符的作用仅是将脚注的引用和脚注本身进行关联，在输出中，脚注按顺序编号。\n另一种创建脚注的方式是在方括号内添加一个插入符（caret）以及一个数字，后面跟着冒号和文本，即（[^1]: My footnote.）。这种方式让不必在文档末尾添加脚注。可以将脚注放到除列表（lists）、块引用（block quotes）和表格（tables）之外的任何位置上。\n1 2 3 4 5 Here\u0026#39;s a simple footnote,[^1] and here\u0026#39;s a longer one.[^bignote] [^1]: This is the first footnote. [^bignote]: Here\u0026#39;s one with multiple paragraphs and code. 渲染效果如下：\nHere\u0026rsquo;s a simple footnote,1 and here\u0026rsquo;s a longer one.2\n2.3. 表情符号（Emoji） 有两种方式可以将表情符号添加到 Markdown 文档中：将表情符号复制并粘贴到 Markdown 格式的文本中，或者键入表情符号的简码（emoji shortcodes）。\n2.3.1. 复制并粘贴表情符号 在大多数情况下，可以简单地从 Emojipedia 等来源复制表情符号，然后将其粘贴到文档中。许多 Markdown 应用程序就会自动以 Markdown 格式的文本来显示表情符号。从 Markdown 应用程序导出的 HTML 和 PDF 文件也是可以显示表情符号的。\nTips: 如果使用的是静态站点生成器，请确保 HTML 页面的字符编码为 UTF-8。\n2.3.2. 使用表情符号的简码（Shortcodes） 某些 Markdown 应用程序允许你通过键入表情符号的简码（shortcodes）来插入表情符号。简码以冒号开头和结尾，两个冒号中间是表情符号的名称。\n1 2 3 Gone camping! :tent: Be back soon. That is so funny! :joy: 渲染效果如下：\nGone camping! :tent: Be back soon.\nThat is so funny! :joy:\nTips: 可以使用这个表情符号简码列表，但请记住，表情符号的简码随着 Markdown 应用程序的不同而不同。具体详细参阅使用的 Markdown 应用程序的文档。\n3. Markdown 中的 html 语法 大多 Markdown 应用程序允许你在 Markdown 格式文本中添加 HTML 标签。如通过 HTML 标签添加图像更加容易。当需要更改元素的属性时（例如为文本指定颜色或更改图像的宽度），使用 HTML 标签更方便些。\n3.1. 上下标 1 H\u0026lt;sup\u0026gt;上标\u0026lt;/sup\u0026gt; 上标效果如下：\nH上标\n1 H\u0026lt;sub\u0026gt;下标\u0026lt;/sub\u0026gt; 下标效果如下：\nH下标\n3.2. 下划线 Markdown可以和HTML的语法兼容，可以通过HTML的标签来实现下划线效果\n1 \u0026lt;u\u0026gt;下划线\u0026lt;/u\u0026gt; 渲染效果如下：\n下划线\n3.3. 实现锚点跳转的语法 1 2 3 4 5 6 7 8 9 // 定义锚点方式一： [锚点1](#锚点) // 定义锚点方式二： \u0026lt;a href=\u0026#34;#锚点\u0026#34;\u0026gt;锚点2\u0026lt;/a\u0026gt; // 定义锚点的位置，需要跳转的地方 \u0026lt;a id=\u0026#34;锚点\u0026#34;\u0026gt;我是一个锚点位置\u0026lt;/a\u0026gt; // 或者 \u0026lt;a name=\u0026#34;锚点\u0026#34;\u0026gt;我是目标位置\u0026lt;/a\u0026gt; 跳转到当前文档的指定位置： 1 [点击跳转的文字](#相应的章节或者位置id) 跳转到其他的文档 1 [点击跳转的文字](/文档的url) 3.4. 文字居中 使用 \u0026lt;center\u0026gt; 标签来文字居中，原理就是此标签自带了 text-align: center 的样式\n1 \u0026lt;center\u0026gt;我居中了？？\u0026lt;/center\u0026gt; 渲染效果如下：\n3.5. 文字悬浮注释 \u0026lt;abbr\u0026gt; 标签全称是 abbreviations，意思是缩写。应用场景是，为文字增加鼠标悬浮时显示一些注释\n1 \u0026lt;abbr title=\u0026#34;全称是 abbreviations\u0026#34;\u0026gt;abbr\u0026lt;/abbr\u0026gt;，这个标签就可以把全称隐藏掉，弱化信息量，让真正不知道该缩写的用户主动去获取缩写的具体意思 渲染效果如下：\nabbr，这个标签就可以把全称隐藏掉，弱化信息量，让真正不知道该缩写的用户主动去获取缩写的具体意思\n3.6. 文本高亮 \u0026lt;mark\u0026gt; 标签用于将包裹的文本高亮展示\n1 \u0026lt;mark\u0026gt;高亮文本\u0026lt;/mark\u0026gt; 渲染效果如下：\n高亮文本\n3.7. 展开详情 \u0026lt;details\u0026gt; 标签包裹了的内容默认会被隐藏，只留下一个简述的文字，点击文字后会展示详细的内容。默认情况下，简要文字为\u0026quot;详情\u0026quot;，想要修改这个文字，要搭配 summary 标签来使用\n1 2 3 4 \u0026lt;details\u0026gt; \u0026lt;summary\u0026gt;点击查看更多\u0026lt;/summary\u0026gt; \u0026lt;p\u0026gt;我是一段被隐藏的内容\u0026lt;/p\u0026gt; \u0026lt;/details\u0026gt; 渲染效果如下：\n3.8. 进度条 1 2 \u0026lt;!-- 进度条最大值为100，当前进度为60，即60% --\u0026gt; \u0026lt;progress max=\u0026#34;100\u0026#34; value=\u0026#34;60\u0026#34;/\u0026gt; 渲染效果如下：\n4. 字体、字号与颜色、背景颜色 4.1. 常用颜色表 颜色名 十六进制颜色值 RGB值 颜色 red #ff0000 rgb(255, 0, 0) purple #800080 rgb(128, 0, 128) violet #ee82ee rgb(238, 130, 238) lightgray #c7edcc rgb(199,237,204) yellowgreen #8BCC29 rgb(139,204,41) whitesmoke #EAEFEF rgb(234, 239, 239) 颜色相关在线工具\n颜色空间转换工具 颜色格式转换工具与颜色参考表 代码示例：\n1 2 3 \u0026lt;font color=red\u0026gt;****\u0026lt;/font\u0026gt; \u0026lt;font color=purple\u0026gt;****\u0026lt;/font\u0026gt; \u0026lt;font color=violet\u0026gt;****\u0026lt;/font\u0026gt; 4.2. 字体、字号配置 一些示例\n1 2 3 4 5 6 \u0026lt;font face=\u0026#34;黑体\u0026#34;\u0026gt;我是黑体字\u0026lt;/font\u0026gt; \u0026lt;font face=\u0026#34;微软雅黑\u0026#34;\u0026gt;我是微软雅黑\u0026lt;/font\u0026gt; \u0026lt;font face=\u0026#34;STCAIYUN\u0026#34;\u0026gt;我是华文彩云\u0026lt;/font\u0026gt; \u0026lt;font color=#0099ff size=7 face=\u0026#34;黑体\u0026#34;\u0026gt;color=#0099ff size=72 face=\u0026#34;黑体\u0026#34;\u0026lt;/font\u0026gt; \u0026lt;font color=#00ffff size=72\u0026gt;color=#00ffff\u0026lt;/font\u0026gt; \u0026lt;font color=gray size=72\u0026gt;color=gray\u0026lt;/font\u0026gt; Size：规定文本的尺寸大小。可能的值：从 1 到 7 的数字。浏览器默认值是 3。\n1 \u0026lt;font color=red face=\u0026#34;微软雅黑\u0026#34;\u0026gt;**XXX**\u0026lt;/font\u0026gt; 4.3. 某些支持markdown的云笔记设置背景颜色 有道云笔记，在md类型的文件头部增加\n1 \u0026lt;div style=\u0026#34;background-color: #c7e6c8\u0026#34;\u0026gt; 5. markdown 中实现首行缩进的两种方法 由于 markdown 语法主要考虑的是英文，所以对于中文的首行缩进并不太友好，两种方法都可以完美解决这个问题。\n5.1. 方式一 把输入法由半角改为全角。两次空格之后就能够有两个汉字的缩进。\n半方大的空白\u0026amp;ensp;或\u0026amp;#8194; 全方大的空白\u0026amp;emsp;或\u0026amp;#8195; 不断行的空白格\u0026amp;nbsp;或\u0026amp;#160; 5.2. 方式二 在段落首输入如下代码，实现首行缩进\n1 \u0026amp;ensp;\u0026amp;ensp;\u0026amp;ensp;\u0026amp;ensp; 这里是实现首先缩进的效果！\n6. 一些 markdown 软件设置 简单又好看，你的 Markdown 文稿也能加上个性化主题\n背景颜色 1 2 3 4 5 6 7 // 设置背景为保护颜色 body { font-family: \u0026#34;Open Sans\u0026#34;,\u0026#34;Clear Sans\u0026#34;,\u0026#34;Helvetica Neue\u0026#34;,Helvetica,Arial,sans-serif; color: rgb(51, 51, 51); background-color:rgb(199, 237, 204); //修改 line-height: 1.6; } 7. 替换 md 文档中的图片 src 属性的脚本 1 2 3 4 5 6 7 8 9 10 \u0026lt;script\u0026gt; window.onload = function () { var imgs = document.getElementsByTagName(\u0026#34;img\u0026#34;); for (let index = 0; index \u0026lt; imgs.length; index++) { const element = imgs[index]; const originSrc = element.src; element.src = originSrc.replace(/\\S+\\/images\\//, \u0026#34;https://moon.com/images/\u0026#34;); } } \u0026lt;/script\u0026gt; 8. Markdown 相关软件推荐 8.1. Markdown Monster 官网：https://markdownmonster.west-wind.com/\n8.2. Typora 一款 Markdown 编辑器和阅读器。官网：https://typoraio.cn/\nTypora 的 Markdown 语法参考手册 8.2.1. 主题 官网主题地址：https://theme.typoraio.cn/\n个人推荐主题：\nTypora Docsify See Yue 主题 Drake Rainbow typora-vue-theme typora-theme-next 8.2.2. 使用参考资料（待整理） typora-生产力工具\n9. 参考资源 Markdown 官方教程：Markdown 是一种轻量级标记语言，它允许人们使用易读易写的纯文本格式编写文档，Markdown文件的后缀名便是“.md”。 Markdown 指南中文版：是一份免费且开源的 Markdown 参考手册，详细讲解了 Markdown 这一简单、易用的文档格式化标记语言的用法。 This is the first footnote.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nHere\u0026rsquo;s one with multiple paragraphs and code.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://ktzxy.top/posts/98xrscljza/","summary":"Markdown学习笔记","title":"Markdown"},{"content":"﻿### 在超市管理数据库SuperMarket的基础上进行实验。\n1、自定义数据类型GoodID_Type，用于描述商品的编号。 1 2 3 4 5 6 7 8 --查看该表的数据类型 sp_columns Goods --创建（方法一） create type GoodID_Type from varchar(20) not null --创建（方法二） exec sp_addtype GoodID_Type,\u0026#39;varchar(20)\u0026#39;,\u0026#39;not null\u0026#39; --查看 exec sp_help GoodID_Type 2、在SuperMarket数据库中创建表good，表结构与Goods类似，而GoodsNO的数据类型为自定义数据类型GoodID_type。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 drop table if exists good; create table good( GoodsNO GoodID_type primary key, SupplierNO varchar(20), CategoryNO varchar(20), GoodsName varchar(100), Barcode varchar(100), InPrice decimal(18,2), SalePrice decimal(18,2), Number int, ProductTime smalldatetime, QGPeriod tinyint, foreign key(CategoryNO) references Category (CategoryNO), foreign key (SupplierNO) references Supplier (SupplierNO) ) go 3、创建一个局部变量goods_type，并在SELECT语句中使用该变量查找商品表中所有毛巾类商品的名称和售价。 1 2 3 4 5 6 7 8 declare @goods_type varchar(20) set @goods_type = \u0026#39;毛巾\u0026#39; select GoodsName,SalePrice from Goods where CategoryNO in ( select CategoryNO from Category where CategoryName like @goods_type) go 4、判断商品表Goods是否存在商品类型为“白酒”的商品，如果存在则显示该类别的所有商品信息，否则显示无此类商品。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 declare @gname varchar(20),@num int set @gname=\u0026#39;白酒\u0026#39; select @num=COUNT(*) from Goods where CategoryNO in ( select CategoryNO from Category where CategoryName like @gname) if @num \u0026gt; 0 select * from Goods where CategoryNO in (select CategoryNO from Category where CategoryName like @gname) else print \u0026#39;无此类商品\u0026#39; go declare @s int,@goods_type varchar(100) set @goods_type=\u0026#39;白酒\u0026#39; select @s=(select count(*) from goods, category where goods.CategoryNO=Category.CategoryNO and CategoryName=@goods_type) if @s\u0026gt;0 select * from goods, category where goods.CategoryNO=Category.CategoryNO and CategoryName=@goods_type else print \u0026#39;无此类商品\u0026#39; go 5、如果商品表Goods中存在商品数量小于10的情况，则将所有商品数量增加10，反复执行直到所有商品的数量都不小于10为止。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 while (select MAX(Number) from Goods) \u0026gt;=10 begin if(select MIN(Number) from Goods) \u0026lt;10 begin update Goods set Number = Number + 10 break end end go while exists(select * from goods where number\u0026lt;10) begin update goods set number=number+10 end go 6、声明一个游标，用于对“饼干”类商品的售价降价5%。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 declare @gname varchar(20) declare @now_goods_id varchar(20) declare @total int set @gname = \u0026#39;饼干\u0026#39; --声明游标 declare sale_cur cursor for select GoodsNO from Goods where CategoryNO in ( select CategoryNO from Category where CategoryName like @gname) --给局部变量赋值 select @total=COUNT(*) from Goods where CategoryNO in ( select CategoryNO from Category where CategoryName like @gname) --打开游标 open sale_cur --使用游标 while @total \u0026gt; 0 begin --游标下移 fetch sale_cur into @now_goods_id --更新数据 begin update Goods set SalePrice = SalePrice - SalePrice * 0.05 where GoodsNO like @now_goods_id --break 不可以设置break,否则只生效一条数据 end set @total = @total -1 end --关闭游标 close sale_cur --释放游标 deallocate sale_cur go declare @goods_no varchar(20) declare cur_pie cursor for select GoodsNO from goods, category where goods.CategoryNO=Category.CategoryNO and CategoryName=\u0026#39;咖啡\u0026#39; for update of SalePrice open cur_pie fetch next from cur_pie into @goods_no while @@FETCH_STATUS=0 begin update goods set SalePrice=SalePrice*0.95 where GoodsNO=@goods_no fetch next from cur_pie into @goods_no end close cur_pie deallocate cur_pie go 7、创建自定义函数，用于统计销售表SaleBill 中某段时间内的销售情况。并调用该函数输出执行结果。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 create function fun_saleinfo(@pre smalldatetime,@last smalldatetime) returns int begin return(select SUM(Number) from SaleBill where HappenTime \u0026gt;= @pre and HappenTime\u0026lt;=@last) end go declare @formerly smalldatetime,@latter smalldatetime set @formerly = \u0026#39;2018-04-01\u0026#39; set @latter = \u0026#39;2018-05-01\u0026#39; select dbo.fun_saleinfo(@formerly,@latter) go create function salereport(@begindate smalldatetime, @enddate smalldatetime) returns decimal(18,2) as begin declare @sum_amount decimal(18,2) select @sum_amount=(select sum(saleprice*S.number) from salebill S,goods G where S.GoodsNO=G.GoodsNO) return @sum_amount end go declare @begindate smalldatetime, @enddate smalldatetime set @begindate=\u0026#39;2018-7-1\u0026#39; set @enddate=\u0026#39;2018-7-31\u0026#39; select dbo.salereport(@begindate,@enddate) as \u0026#39;2018年7月份销售金额\u0026#39; go 8、创建自定义函数，用于显示商品表Goods中售价大于指定价格的商品信息。并调用该函数输出执行结果。 1 2 3 4 5 6 7 8 create function showGoods(@saleprice decimal(18,2)) returns table as return select * from goods where saleprice\u0026gt;@saleprice go select * from dbo.showGoods(50) go ","permalink":"https://ktzxy.top/posts/d1lx0hx0un/","summary":"实验7 T SQL编程","title":"实验7 T SQL编程"},{"content":"Nginx服务端404以及502等页面配置 进入nginx.conf配置文件: 1 vi /usr/local/nginx/conf/nginx.conf 新手请记得备份一下再操作。\n添加页面重定向 http内添加一行 fastcgi_intercept_errors on; 开启页面重定向功能。 1 2 3 4 5 6 7 8 9 10 11 12 13 http { include mime.types; default_type application/octet-stream; #log_format main \u0026#39;$remote_addr - $remote_user [$time_local] \u0026#34;$request\u0026#34; \u0026#39; # \u0026#39;$status $body_bytes_sent \u0026#34;$http_referer\u0026#34; \u0026#39; # \u0026#39;\u0026#34;$http_user_agent\u0026#34; \u0026#34;$http_x_forwarded_for\u0026#34;\u0026#39;; #access_log logs/access.log main; # 404 500 fastcgi_intercept_errors on; ... server 内添加页面指向 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; # 假如服务器ip是192.168.10.125，则访问 192.168.10.125 实际访问的目录是：/usr/local/nginx/html/index.html location / { root html; index index.html index.htm; proxy_pass http://127.0.0.1:3000; } # 配置 404 状态页面指向，指向的是：root 中的 html 的绝对路径是 /usr/local/nginx/html/404.html 文件 error_page 404 /404.html; location = /404.html { root html; } ... error_page 500 /500.html; location = /500.html { root html; } error_page 502 /502.html; location = /502.html { root html; } ... 检查nginx配置文件是否准确 1 /usr/local/nginx/sbin/nginx -t 重启nginx 1 /usr/local/nginx/sbin/nginx -s reload 访问服务失败 访问服务器ip时，失败，通过tail -n 5 /usr/local/nginx/logs/error.log查看nginx错误日志发现：\n1 2 3 2019/01/29 16:01:10 [error] 4351#0: *21 connect() failed (111: Connection refused) while connecting to ups tream, client: xxx.xxx.xxx.xxx, server: localhost, request: \u0026#34;GET / HTTP/1.1\u0026#34;, upstream: \u0026#34;http://127.0.0.1:3 000/\u0026#34;, host: \u0026#34;xxx.xxx.xxx.xxx:80\u0026#34; 原因可能是：\nphp-fpm没有安装 新买的阿里云服务器 就属于这种情况,有nginx，但是没安装php-fpm 这种情况下可参考 centos安装php php-fpm 以及 配置nginx\nNginx的页面乱码解决方法 在server段里加字符集配置：\n1 2 default_type \u0026#39;text/html\u0026#39;; charset utf-8; 1 2 3 4 5 检查: /usr/local/nginx/sbin/nginx -t 重启: /usr/local/nginx/sbin/nginx -s reload 高可用 如果nginx服务存在异常，上面的负载均衡架构就面临问题，服务不可用。\n这个时候就会采用高可用方案了，简单的高可用是一般主从备份机制。\n我们可以使用2台机器，作为主、备服务器，各自运行nginx服务。\n需要一个监控程序，来控制传输主、备之间的心跳信息（我是否还活着的信息），如果备份机在一段时间内没有收到主的发送信息，则认为主已经挂了，自己上去挑大梁，接管主服务继续提供负载均衡服务。\n当备再次可以从主那里获得信息时，释放主服务，这样原来的主又可以再次提供负载均衡服务了。\n这里有3个角色： 一主、一备、监控程序。其中的监控程序或者软件我们可以采用Keepalived。\nKeepalived Keepalived工作原理 Keepalived类似一个工作在layer3,4\u0026amp;7的交换机。\nLayer3,4\u0026amp;7工作在IP/TCP协议栈的IP层，TCP层，及应用层,原理分别如下：\nLayer3：Keepalived使用Layer3的方式工作式时，Keepalived会定期向服务器群中的服务器发送一个ICMP的数据包（即我们平时用的Ping程序）,如果发现某台服务的IP地址没有激活，Keepalived便报告这台服务器失效，并将它从服务器群中剔除，这种情况的典型例子是某台服务器被 非法关机。Layer3的方式是以服务器的IP地址是否有效作为服务器工作正常与否的标准。\nLayer4:如果您理解了Layer3的方式，Layer4就容易了。Layer4主要以TCP端口的状态来决定服务器工作正常与否。如web server的服务端口一般是80，如果Keepalived检测到80端口没有启动，则Keepalived将把这台服务器从服务器群中剔除。\nLayer7：Layer7就是工作在具体的应用层了，比Layer3,Layer4要复杂一点，在网络上占用的带宽也要大一些。Keepalived将根据用户的设定检查服务器程序的运行是否正常，如果与用户的设定不相符，则Keepalived将把服务器从服务器群中剔除。\n\u0026mdash;- 来自搜狗百科\nKeepalived作用 主要用作真实服务器的健康状态检查以及负载均衡的主机和备机之间故障切换实现。\n一般的高可用web架构组件：LVS+keepalived+nginx\nKeepalived高可用架构 Keepalived高可用简单架构概念视图：\n当主down机后，会提升备为主，保证服务可用。\n当主再次上线时，备会退位，主继续服务。\nKeepalived组成 Keepalived 模块化设计，主要有三个模块，分别是core、check和 vrrp。\ncore keepalived的核心，负责主进程的启动和维护、全局配置文件的加载解析 check 负责healthchecker(健康检查)，包括各种健康检查方式，以及对应的配置的解析包括LVS的配置解析；可基于脚本检查对IPVS后端服务器健康状况进行检查。 vrrp VRRPD子进程就是来实现VRRP（虚拟路由冗余协议）协议的 安装Keepalived 方式一：可以直接使用yum安装。 1 yum install -y keepalived 方拾二： 使用源码安装方式。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #下载 wget http://www.keepalived.org/software/keepalived-1.2.23.tar.gz #解压 tar -zxvf keepalived-1.2.23.tar.gz cd keepalived-1.2.23 #安装 ./configure --prefix=/usr/local/keepalived #prefix指定安装目录 make make install vi /usr/local/keepalived/etc/keepalived/keepalived.conf 替换为你自身逻辑的配置文件的内容。 vi /usr/local/keepalived/etc/sysconfig/keepalived 添加以下内容： KEEPALIVED_OPTIONS=\u0026#34;-D -f /usr/local/keepalived/etc/keepalived/keepalived.conf\u0026#34; #不使用默认的，自己显示指定keepalived配置文件路径 # 因为我们使用非默认路径（/usr/local）安装keepalived，需要设置一些软链接以保证keepalived能正常启动： ln -s /usr/local/keepalived/sbin/keepalived /usr/bin #将keepalived主程序加入到环境变量 ln -s /usr/local/keepalived/etc/rc.d/init.d/keepalived /etc/init.d/ #keepalived启动脚本，放到/etc/init.d/目录下就可以使用service命令便捷调用 ln -s /usr/local/keepalived/etc/sysconfig/keepalived /etc/sysconfig/ #keepalived启动脚本变量引用文件，默认文件路径是/etc/sysconfig/，也可以不做软链接，直接修改启动脚本中文件路径即可 启动服务 service keepalived start # 可以检查下服务是否正常（没有消息就是最好的消息） chkconfig keepalived on # 查看绑定好的虚拟ip（vip） ip addr # 测试 使用配置好的虚拟ip，在浏览器访问测试一下即可。 也可以这样设置，配置文件：安装完成之后，可以把配置文件放到目录： /etc/keepalived\n后面则统一修改 /etc/keepalived/keepalived.conf 配置文件即可。\n/usr/local/keepalived/etc/keepalived/keepalived.conf\n/etc/keepalived/keepalived.conf\n启动|停止|重启服务\n1 service keepalived start|stop|restart 检查服务\n1 chkconfig keepalived on 查看keepalived版本\n1 2 keepalived -v Keepalived v1.2.23 (12/20,2019) 需求 使用keepalived结合nginx，搭建一套简单的高可用集群方案。\n并且测试主宕机的情况；\n演示主上线的情况。\n分析 选择4台虚拟机：2台做nginx的主、备；2台做tomcat集群。 为了使nginx命令方便的话，可以将其所在目录添加到：/etc/profile 文件中。 选择192.168.75.132、192.168.75.135安装keepalived和nginx并分别做主、备。 选择192.168.75.130、192.168.75.134安装tomcat做应用集群。 实战 在192.168.75.132、192.168.75.135分别安装keepalived并配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 #下载 mkdir /usr/local/keepalived cd /usr/local/keepalived wget http://www.keepalived.org/software/keepalived-1.2.23.tar.gz # 或者 wget http://www.keepalived.org/software/keepalived-1.2.23.tar.gz --no-check-certificate 跳过证书验证 #解压 tar -zxvf keepalived-1.2.23.tar.gz # cd 到解压后的目录 cd keepalived-1.2.23 # 源码安装三板斧搞起来 ./configure --prefix=/usr/local/keepalived #prefix指定安装目录 make make install ​\t在192.168.75.132(作为主)修改配置文件 keepalived.conf：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 ! Configuration File for keepalived # 全局配置 global_defs { # 发生切换时，需要通知的邮件； 一行一个用户的email地址；这里假定了几个人.. notification_email { zs@163.com ls@163.com ww@163.com } notification_email_from admin@163.com # 发件人，谁发的；这里假定了一个管理员邮箱admin@163.com smtp_server smtp.163.com # smtp服务器地址，这里示例使用163的公开地址 smtp_connect_timeout 30\t# smtp连接超时时间设置 router_id LVS_DEVEL\t# 运行keepalived的一个路由标志id } # VRRP 配置 vrrp_instance VI_1 { state MASTER\t# 配置标志，MASTER 为主；BACKUP 为备 interface eth0\t# 该keepalived实例绑定的网卡; RHEL7以前可以设置为eth0,7以及之后可以设置为ens33 virtual_router_id 51\t#VRRP组名，两个节点的设置必须一样，以指明各个节点属于同一VRRP组 priority 100\t# 主节点的优先级（1-254之间），备用节点必须比主节点优先级低 advert_int 1\t# 主、备之间检查是否一致的时间间隔：单位秒 # 认证配置 设置验证信息，主、备节点必须一致 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress {\t# 指定虚拟IP, 主、备节点设置必须一样 # 可以设置多个虚拟ip，换行即可；随便写；这个地址是虚拟的，并不需要实体机器 # 会将该vip绑定到当前机器的网卡eth0上（因为vrrp_instance的interface指定了eth0） 192.168.75.88\t} } 在192.168.75.135（作为备）修改配置文件 keepalived.conf：\nvrrp_instance 下面的state改为了 BACKUP。\nvrrp_instance 下面的额priority改为了90 ，比主的100低即可。\n其他的配置项一样。\n启动keepalived服务 1 service keepalived start 可以使用 ip addr 命令查看vip是否已经绑定 测试正常情况\n在Master和Backup上分别启动keepalived。此时VIP(192.168.75.88)是可访问的，也可以ping通，通过虚拟ip（192.168.75.88）访问，可以看到一直访问的是主（192.168.75.132）：\n测试Master宕掉的情况\n现在把主（192.168.75.132）的keepalived关掉，在浏览器上再次访问192.168.75.88，会发现此时访问的是备（192.168.75.135）了：\n测试Master宕掉又恢复的情况\n重新把192.168.75.132的keepalived启动后，会看到浏览器输入192.168.75.88访问的一直是192.168.75.132（主）。\n综上所述，当前的架构方案，在keepalived出现问题时，是可以及时更换主备的。\n原始高可用方案存在的问题 我们把主的nginx服务关闭，再访问192.168.75.88，发现服务访问失败，因为此时一直访问的是主192.168.75.132的nginx。但是http://192.168.75.132:8080/服务其实是可用的。\n这不是我们想要的结果，因为此时备（192.168.75.135）的nginx服务是OK的。能不能在主的nginx挂掉后，自动地切换到备的nginx服务呢？\n毋庸置疑，肯定是可以的，我们还需要更多的配置。\n截至目前，该方案不能保证主nginx挂掉后服务仍可用。所以，还需要继续提高高可用性。\n我们需要在keepalived.conf中增加对nginx服务的检测，根据nginx服务的状态做出一些处理。\nvrrp_script节点 ​\t此模块专门用于对集群中服务资源进行监控 。与此模块同时使用的还有 track_script 模块，在此模块中可以引入监控脚本、命令组合、shell 语句等 ，以实现对服务、端口等多方面的监控。\n​\ttrack_script 模块主要用来调用 vrrp_script 模块使 keepalived执行对集群服务资源的检测。\n​\tvrrp_script 模块中还可以定义对服务资源检测的时间间隔、权重等参数，通过 vrrp_script 和 track_script 组合，可以实现对集群资源的监控并改变优先级，进而实现 keepalived 主备节点切换。\n我们可以在keepalived.conf文件中新增一个vrrp_script节点，来指定监控一个shell脚本文件: /etc/keepalived/check_and_start_nginx.sh。\n1 2 3 4 5 6 7 8 # 新增一个vrrp_script节点，用来监控nginx vrrp_script chk_nginx { script \u0026#34;/etc/keepalived/check_and_start_nginx.sh\u0026#34; # 检测nginx服务并尝试重启 interval 2 # 每2s检查一次 weight -5 # 检测失败（脚本返回非0）则优先级减少5个值 fall 3 # 如果连续失败次数达到此值，则认为服务器已down rise 2 # 如果连续成功次数达到此值，则认为服务器已up，但不修改优先级 } 还需要在虚拟节点中增加track_script子节点引用vrrp_script节点：\n1 2 3 4 5 6 7 8 9 10 11 # VRRP 配置 vrrp_instance VI_1 { state MASTER\t# 配置标志，MASTER 为主；BACKUP # ...... 省略 # 引用VRRP脚本，即在 vrrp_script 中定义的 track_script { # 引用VRRP脚本 chk_nginx } } shell脚本编写 vi /etc/keepalived/check_and_start_nginx.sh 编写以下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/bin/bash # 查看nginx进程是否正在运行，为0则表示已经down掉 nginx_result=$(ps -C nginx --no-heading|wc -l) if [ \u0026#34;${nginx_result}\u0026#34; = \u0026#34;0\u0026#34; ]; then # 使用service的前提需要将nginx设置为servic； 或者直接使用nginx所在绝对路径比如： /usr/local/nginx/sbin/nginx service nginx start # 或者使用 /usr/local/nginx/sbin/nginx sleep 2 nginx_result=$(ps -C nginx --no-heading|wc -l) if [ \u0026#34;${nginx_result}\u0026#34; = \u0026#34;0\u0026#34; ]; then # 如果重启nginx服务还是不行的话，就把keepalived也停止； # 这样会访问备keepalived，从而保证主的nginx挂掉备也能使用 /etc/rc.d/init.d/keepalived stop fi fi 注意！！！！： 需要加执行权限：\n1 chmod a+x /etc/keepalived/check_and_start_nginx.sh 主的keepalived.conf 以下是完整的主的keepalived.conf（/etc/keepalived/keepalived.conf）文件配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 ! Configuration File for keepalived # 全局配置 global_defs { # 发生切换时，需要通知的邮件； 一行一个用户的email地址；这里假定了几个人.. notification_email { zs@163.com ls@163.com ww@163.com } notification_email_from admin@163.com # 发件人，谁发的；这里假定了一个管理员邮箱admin@163.com smtp_server smtp.163.com # smtp服务器地址，这里示例使用163的公开地址 smtp_connect_timeout 30\t# smtp连接超时时间设置 router_id LVS_DEVEL\t# 运行keepalived的一个路由标志id } # 新增一个vrrp_script节点，用来监控nginx vrrp_script chk_nginx { script \u0026#34;/etc/keepalived/check_and_start_nginx.sh\u0026#34; # 检测nginx服务并尝试重启 interval 2 # 每2s检查一次 weight -5 # 检测失败（脚本返回非0）则优先级减少5个值 fall 3 # 如果连续失败次数达到此值，则认为服务器已down rise 2 # 如果连续成功次数达到此值，则认为服务器已up，但不修改优先级 } # VRRP 配置 vrrp_instance VI_1 { state MASTER\t# 配置标志，MASTER 为主；BACKUP 为备 interface eth0\t# 该keepalived实例绑定的网卡; RHEL7以前可以设置为eth0,7以及之后可以设置为ens33 virtual_router_id 51\t#VRRP组名，两个节点的设置必须一样，以指明各个节点属于同一VRRP组 priority 100\t# 主节点的优先级（1-254之间），备用节点必须比主节点优先级低 advert_int 1\t# 主、备之间检查是否一致的时间间隔：单位秒 # 认证配置 设置验证信息，主、备节点必须一致 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress {\t# 指定虚拟IP, 主、备节点设置必须一样 # 可以设置多个虚拟ip，换行即可；随便写；这个地址是虚拟的，并不需要实体机器 # 会将该vip绑定到当前机器的网卡eth0上 192.168.75.88\t} # 引用VRRP脚本，即在 vrrp_script 中定义的 track_script { # 引用VRRP脚本 chk_nginx } } 启动keepalived： service keepalived start 现在主（192.168.75.132）和备（192.168.75.135）的keepalived、nginx都已经启动；\n此时 浏览器访问vip ： http://192.168.75.88/， 回请求到主192.168.75.132：\n现在将主的keepalived关闭，再访问http://192.168.75.88/，会请求到备192.168.75.135。\n再将主的keepalived启动，又会访问到主。\n注意： 原来我们将主的nginx关闭，访问 http://192.168.75.88/ 时会一直调用主，导致服务不可用。现在我们在主的keepalived中配置了监控nginx服务，看他是否会尝试重启主的nginx服务并且保证给外界提供访问。\n我们在主上关闭nginx服务。稍等一会，访问 http://192.168.75.88/ ，发现可以访问到主的页面，这样keepalived监控了nginx的状态并将nginx服务启动了，保证了高可用，现在不需要人工干预去启动服务了。\n多学一招 将nginx设置为系统service【掌握】 源码编译的一个缺陷是没法将安装好的应用设置为系统的service， 即无法使用 service 服务名 start | stop | restart 等命令统一操作。\n以nginx为例，需要做一些配置，该配置文件的样本示例： https://www.nginx.com/resources/wiki/start/topics/examples/redhatnginxinit/\nvi /etc/init.d/nginx 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 #!/bin/sh # # nginx - this script starts and stops the nginx daemon # # chkconfig: - 85 15 # description: NGINX is an HTTP(S) server, HTTP(S) reverse \\ # proxy and IMAP/POP3 proxy server # processname: nginx # config: /etc/nginx/nginx.conf # config: /etc/sysconfig/nginx # pidfile: /var/run/nginx.pid # Source function library. . /etc/rc.d/init.d/functions # Source networking configuration. . /etc/sysconfig/network # Check that networking is up. [ \u0026#34;$NETWORKING\u0026#34; = \u0026#34;no\u0026#34; ] \u0026amp;\u0026amp; exit 0 # 配置nginx命令的位置 # 修改为你的nginx可执行命令的路径如： /usr/local/nginx/sbin/nginx nginx=\u0026#34;/usr/local/nginx/sbin/nginx\u0026#34; prog=$(basename $nginx) # 指向你的配置文件路径，如：/usr/local/nginx/conf/nginx.conf NGINX_CONF_FILE=\u0026#34;/usr/local/nginx/conf/nginx.conf\u0026#34; [ -f /etc/sysconfig/nginx ] \u0026amp;\u0026amp; . /etc/sysconfig/nginx lockfile=/var/lock/subsys/nginx make_dirs() { # make required directories user=`$nginx -V 2\u0026gt;\u0026amp;1 | grep \u0026#34;configure arguments:.*--user=\u0026#34; | sed \u0026#39;s/[^*]*--user=\\([^ ]*\\).*/\\1/g\u0026#39; -` if [ -n \u0026#34;$user\u0026#34; ]; then if [ -z \u0026#34;`grep $user /etc/passwd`\u0026#34; ]; then useradd -M -s /bin/nologin $user fi options=`$nginx -V 2\u0026gt;\u0026amp;1 | grep \u0026#39;configure arguments:\u0026#39;` for opt in $options; do if [ `echo $opt | grep \u0026#39;.*-temp-path\u0026#39;` ]; then value=`echo $opt | cut -d \u0026#34;=\u0026#34; -f 2` if [ ! -d \u0026#34;$value\u0026#34; ]; then # echo \u0026#34;creating\u0026#34; $value mkdir -p $value \u0026amp;\u0026amp; chown -R $user $value fi fi done fi } start() { [ -x $nginx ] || exit 5 [ -f $NGINX_CONF_FILE ] || exit 6 make_dirs echo -n $\u0026#34;Starting $prog: \u0026#34; daemon $nginx -c $NGINX_CONF_FILE retval=$? echo [ $retval -eq 0 ] \u0026amp;\u0026amp; touch $lockfile return $retval } stop() { echo -n $\u0026#34;Stopping $prog: \u0026#34; killproc $prog -QUIT retval=$? echo [ $retval -eq 0 ] \u0026amp;\u0026amp; rm -f $lockfile return $retval } restart() { configtest || return $? stop sleep 1 start } reload() { configtest || return $? echo -n $\u0026#34;Reloading $prog: \u0026#34; killproc $prog -HUP retval=$? echo } force_reload() { restart } configtest() { $nginx -t -c $NGINX_CONF_FILE } rh_status() { status $prog } rh_status_q() { rh_status \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 } case \u0026#34;$1\u0026#34; in start) rh_status_q \u0026amp;\u0026amp; exit 0 $1 ;; stop) rh_status_q || exit 0 $1 ;; restart|configtest) $1 ;; reload) rh_status_q || exit 7 $1 ;; force-reload) force_reload ;; status) rh_status ;; condrestart|try-restart) rh_status_q || exit 0 ;; *) echo $\u0026#34;Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload|configtest}\u0026#34; exit 2 esac 添加可执行权限：\n1 chmod a+x /etc/init.d/nginx 将一个新服务添加到启动列表中(关于chkconfig 命令更多详情，可以参考这里 )\n1 chkconfig --add /etc/init.d/nginx 配置启动\n1 chkconfig nginx on OK。 就可以使用： service nginx start 之类的命令了。\n负载均衡实现实践 负载均衡技术 负载均衡实现多种，硬件到软件，商业的、开源的。常见的负载均衡方式：\nHTTP重定向负载均衡（较差） 需要HTTP重定向服务器。\n流程 浏览器先去HTTP重定向服务器请求，获取到实际被定向到的服务器；浏览器再请求被实际定向到的服务器。 特点 简单易实现。\n缺点 浏览器需要两次请求才能完成一次完整的访问，性能以及体验太差。 依赖于重定向服务器自身处理能力，集群伸缩性规模有限。 使用HTTP 302进行重定向可能使搜索引擎判断为SEO作弊，降低搜索排名。 DNS域名解析负载均衡（一般） 需要DNS服务器。\n流程 浏览器请求某个域名，DNS服务器返回web服务器集群中的某个ip地址。 浏览器再去请求DNS返回的服务器地址。 特点 该方案要要求在DNS服务器中配置多个A记录，例如：\nwww.example.com IN A 10.192.168.66 www.example.com IN A 10.192.168.77 www.example.com IN A 10.192.168.88 该方案将负载均衡的工作交给了DNS，节省了网站管理维护负载均衡服务器的麻烦。\n缺点 目前的DNS是多级解析的，每一级别都可能缓存了A记录，当某台业务服务器下线后，即使修改了DNS的A记录，要使其生效还需要一定时间。这期间，可能导致用户会访问已下线的服务器造成访问失败。 DNS负载均衡的控制权在域名服务商那里，网站无法对其做出更多的改善和管理。 温馨提示\n事实上，可能是部分使用DNS域名解析，利用域名解析作为第一级的负载均衡手段，域名解析得到的是同样提供负载均衡的内部服务器，这组服务器再进行负载均衡，请求分发到真是的web应用服务器。\n反向代理负载均衡（良） 反向代理服务器需要配置双网卡和内外部两套IP地址。即反向代理服务器需要有外部IP、内部IP。\n特点 部署简单，负载均衡功能和反向代理服务器功能集成在一块。\n缺点 反向代理服务器是所有请求和相应的中转站，性能可能成为瓶颈。\nIP负载均衡（良） 特点 优点是在内核进程中完成了数据分发，而反向代理负载均衡是在应用程序中分发数据，IP负载均衡处理性能更优。\n缺点 所有请求、响应都经由负载均衡服务器，导致集群最大响应数据吞吐量不得不受制于负载均衡服务器网卡带宽。\n数据链路层负载均衡（优） 特点 数据链路层负载均衡也叫三角传输模式。 负载均衡数据分发过程中不修改IP地址，而是修改MAC地址。 由于实际处理请求的真实物理IP地址和数据请求目的IP地址一致，所以不需要通过负载均衡服务器进行地址转换，可以将响应数据包直接返回给用户浏览器，避免负载均衡服务器网卡宽带成为瓶颈。 这种负载均衡方式也叫作直接路由方式（DR）。\n使用三角传输模式的链路层负载均衡是目前大型网站使用最广泛的一种负载均衡手段。\n在Linux平台上最好的链路层负载均衡开源产品是LVS（Linux Virutal Server）。\n参考干货资料 SegmentFault负载均衡算法实现详细介绍\n云栖社区-负载均衡调度算法\n淘宝大牛分享经验\n菜鸟教程-负载均衡算法大全\nLVS实战\nNginx配置文件nginx.conf详细介绍 Nginx配置文件nginx.conf全解 nginx配置文件nginx.conf的配置http、upstream、server、location等；\nnginx负载均衡算法：轮询、加权轮询、ip_hash、url_hash等策略配置；\nnginx日志文件access_log配置；\n代理服务缓存proxy_buffer设置。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 #user nobody; # 用户 用户组 ；一般只有类unix系统有，windows不需要 worker_processes 1; # 工作进程，一般配置为cpu核心数的2倍 # 错误日志的配置路径 #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; # 进程id存放路径 #pid logs/nginx.pid; # 指定进程可以打开的最大描述符的数量 # 整个系统的最大文件描述符打开数目可以通过命令 ulimit -n 查看 # 所以一般而言，这个值可以是 ulimit -n 除以nginx进程数 # worker_rlimit_nofile 65535; events { # 使用epoll的I/O 模型；epoll 使用于Linux内核2.6版本及以后的系统 use epoll; # 每一个工作进程的最大连接数量； 理论上而言每一台nginx服务器的最大连接数为： worker_processes*worker_connections worker_connections 1024; # 超时时间 #keepalive_timeout 60 # 客户端请求头部的缓冲区大小，客户端请求一般会小于一页； 可以根据你的系统的分页大小来设定， 命令 getconf PAGESIZE 可以获得当前系统的分页大小（一般4K） #client_header_buffer_size 4k; # 为打开的文件指定缓存，默认是不启用； max指定缓存数量，建议和打开文件数一致；inactive是指经过这个时间后还没有被请求过则清除该文件的缓存。 #open_file_cache max=65535 inactive=60s; # 多久会检查一次缓存的有效信息 #open_file_cache_valid 80s; # 如果在指定的参数open_file_cache的属性inactive设置的值之内，没有被访问这么多次（open_file_cache_min_uses），则清除缓存 # 则这里指的是 60s内都没有被访问过一次则清除 的意思 # open_file_cache_min_uses 1; } # 设定http服务；可以利用它的反向代理功能提供负载均衡支持 http { #设定mime类型,类型由mime.types文件定义； 可以 cat nginx/conf/mime.types 查看下支持哪些类型 include mime.types; # 默认mime类型； application/octet-stream 指的是原始二进制流 default_type application/octet-stream; # -------------------------------------------------------------------------- # # 日志格式设置： # $remote_addr、$http_x_forwarded_for 可以获得客户端ip地址 # $remote_user 可以获得客户端用户名 # $time_local 记录访问的时区以及时间 # $request 请求的url与http协议 # $status 响应状态成功为200 # $body_bytes_sent 发送给客户端主体内容大小 # $http_referer 记录从哪个页面过来的请求 # $http_user_agent 客户端浏览器信息 # # 注意事项： # 通常web服务器(我们的tomcat)放在反向代理(nginx)的后面，这样就不能获取到客户的IP地址了，通过$remote_add拿到的IP地址是反向代理服务器的iP地址。 # 反向代理服务器(nginx)在转发请求的http头信息中，可以增加$http_x_forwarded_for信息，记录原有客户端的IP地址和原来客户端的请求的服务器地址。 # -------------------------------------------------------------------------- # #log_format main \u0026#39;$remote_addr - $remote_user [$time_local] \u0026#34;$request\u0026#34; \u0026#39; # \u0026#39;$status $body_bytes_sent \u0026#34;$http_referer\u0026#34; \u0026#39; # \u0026#39;\u0026#34;$http_user_agent\u0026#34; \u0026#34;$http_x_forwarded_for\u0026#34;\u0026#39;; #log_format log404 \u0026#39;$status [$time_local] $remote_addr $host$request_uri $sent_http_location\u0026#39;; # 日志文件； 【注意：】用了log_format则必须指定access_log来指定日志文件 #access_log logs/access.log main; #access_log logs/access.404.log log404; # 保存服务器名字的hash表由 server_names_hash_bucket_size、server_names_hash_max_size 控制 # server_names_hash_bucket_size 128; # 限制通过nginx上传文件的大小 #client_max_body_size 300m; # sendfile 指定 nginx 是否调用sendfile 函数（零拷贝 方式）来输出文件； # 对于一般常见应用，必须设为on。 # 如果用来进行下载等应用磁盘IO重负载应用，可设置为off，以平衡磁盘与网络IO处理速度，降低系统uptime。 sendfile on; # 此选项允许或禁止使用socke的TCP_CORK的选项，此选项仅在使用sendfile的时候使用；TCP_CORK TCP_NODELAY 选项可以是否打开TCP的内格尔算法 #tcp_nopush on; #tcp_nodelay on; # 后端服务器连接的超时时间_发起握手等候响应超时时间 #proxy_connect_timeout 90; # 连接成功后_等候后端服务器响应时间_其实已经进入后端的排队之中等候处理（也可以说是后端服务器处理请求的时间） #proxy_read_timeout 180; # 后端服务器数据回传时间_就是在规定时间之内后端服务器必须传完所有的数据 #proxy_send_timeout 180; # proxy_buffering 这个参数用来控制是否打开后端响应内容的缓冲区，如果这个设置为on，以下的proxy_buffers才生效 #proxy_buffering on # 设置从被代理服务器读取的第一部分应答的缓冲区大小，通常情况下这部分应答中包含一个小的应答头， # 默认情况下这个值的大小为指令proxy_buffers中指定的一个缓冲区的大小，不过可以将其设置为更小 #proxy_buffer_size 256k; # 设置用于读取应答（来自被代理服务器--如tomcat）的缓冲区数目和大小，默认情况也为分页大小，根据操作系统的不同可能是4k或者8k #proxy_buffers 4 256k; # 同一时间处理的请求buffer大小；也可以说是一个最大的限制值--控制同时传输到客户端的buffer大小的。 #proxy_busy_buffers_size 256k; # 设置在写入proxy_temp_path时数据的大小，预防一个工作进程在传递文件时阻塞太长 #proxy_temp_file_write_size 256k; # proxy_temp_path和proxy_cache_path指定的路径必须在同一分区 #proxy_temp_path /app/tmp/proxy_temp_dir; # 设置内存缓存空间大小为200MB，1天没有被访问的内容自动清除，硬盘缓存空间大小为10GB。 #proxy_cache_path /app/tmp/proxy_cache_dir levels=1:2 keys_zone=cache_one:200m inactive=1d max_size=30g; # 如果把它设置为比较大的数值，例如256k，那么，无论使用firefox还是IE浏览器，来提交任意小于256k的图片，都很正常。 # 如果注释该指令，使用默认的client_body_buffer_size设置，也就是操作系统页面大小的两倍，8k或者16k，问题就出现了。 # 无论使用firefox4.0还是IE8.0，提交一个比较大，200k左右的图片，都返回500 Internal Server Error错误 #client_body_buffer_size 512k; # 默认是页大小的两倍 # 表示使nginx阻止HTTP应答代码为400或者更高的应答。可以结合error_page指向特定的错误页面展示错误信息 #proxy_intercept_errors on; #keepalive_timeout 0; keepalive_timeout 65; #gzip on; # 负载均衡 START\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; # upstream 指令定义的节点可以被proxy_pass指令引用；二者结合用来反向代理+负载均衡配置 # 【内置策略】：轮询、加权轮询、ip_hash、最少连接 默认编译进了nginx # 【扩展策略】：fair、通用hash、一致性hash 默认没有编译进nginx #-----------------------------------------------------------------------------------------------# # 【1】默认是轮询；如果后端服务器down掉，能自动剔除。 # upstream bakend { # server 192.168.75.130:8080; # server 192.168.75.132:8080; # server 192.168.75.134:8080; # } # #【2】权重轮询(加权轮询)：这样配置后，如果总共请求了3次，则前面两次请求到130，后面一次请求到132 # upstream bakend { # server 192.168.75.130:8080 weight=2; # server 192.168.75.132:8080 weight=1; # } # #【3】ip_hash：这种配置会使得每个请求按访问者的ip的hash结果分配，这样每个访客固定访问一个后端服务器，这样也可以解决session的问题。 # upstream bakend { # ip_hash; # server 192.168.75.130:8080; # server 192.168.75.132:8080; # } # #【4】最少连接：将请求分配给连接数最少的服务器。Nginx会统计哪些服务器的连接数最少。 # upstream bakend { # least_conn; # server 192.168.75.130:8080; # server 192.168.75.132:8080; # } # # #【5】fair策略(需要安装nginx的第三方模块fair)：按后端服务器的响应时间来分配请求，响应时间短的优先分配。 # upstream bakend { # fair; # server 192.168.75.130:8080; # server 192.168.75.132:8080; # } # #【6】url_hash策略（也是第三方策略）：按访问url的hash结果来分配请求，使每个url定向到同一个后端服务器，后端服务器为缓存时比较有效。 # 在upstream中加入hash语句，server语句中不能写入weight等其他的参数，hash_method指定hash算法 # upstream bakend { # server 192.168.75.130:8080; # server 192.168.75.132:8080; # hash $request_uri; # hash_method crc32; # } # #【7】其他设置，主要是设备的状态设置 # upstream bakend{ # ip_hash; # server 127.0.0.1:9090 down; # down 表示该机器处于下线状态不可用 # server 127.0.0.1:8080 weight=2; # server 127.0.0.1:6060; # # # max_fails 默认为1； 最大请求失败的次数，结合fail_timeout使用； # # 以下配置表示 192.168.0.100:8080在处理请求失败3次后，将在15s内不会受到任何请求了 # # fail_timeout 默认为10秒。某台Server达到max_fails次失败请求后，在fail_timeout期间内，nginx会认为这台Server暂时不可用，不会将请求分配给它。 # server 192.168.0.100:8080 weight=2 max_fails=3 fail_timeout=15; # server 192.168.0.101:8080 weight=3; # server 192.168.0.102:8080 weight=1; # # 限制分配给某台Server处理的最大连接数量，超过这个数量，将不会分配新的连接给它。默认为0，表示不限制。注意：1.5.9之后的版本才有这个配置 # server 192.168.0.103:8080 max_conns=1000; # server 127.0.0.1:7070 backup; # 备份机；其他机器都不可用时，这台机器就上场了 # server example.com my_dns_resolve; # 指定域名解析器；my_dns_resolve需要在http节点配置resolver节点如：resolver 10.0.0.1; # } # # # #负载均衡 END\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; #-----------------------------------------------------------------------------------------------# ###配置虚拟机START\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; # #server{ # listen 80; # 配置监听端口 # server_name image.***.com; # 配置访问域名 # location ~* \\.(mp3|exe)$ { # 对以“mp3或exe”结尾的地址进行负载均衡 # proxy_pass http://img_relay$request_uri; # 设置被代理服务器的端口或套接字，以及URL # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 以上三行，目的是将代理服务器收到的用户的信息传到真实服务器上 # } # # # # location /face { # if ($http_user_agent ~* \u0026#34;xnp\u0026#34;) { # rewrite ^(.*)$ http://211.151.188.190:8080/face.jpg redirect; # } # proxy_pass http://img_relay$request_uri; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # error_page 404 502 = @fetch; # } # # location @fetch { # access_log /data/logs/face.log log404; # rewrite ^(.*)$ http://211.151.188.190:8080/face.jpg redirect; # } # # location /image { # if ($http_user_agent ~* \u0026#34;xnp\u0026#34;) { # rewrite ^(.*)$ http://211.151.188.190:8080/face.jpg redirect; # # } # proxy_pass http://img_relay$request_uri; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # error_page 404 502 = @fetch; # } # # location @fetch { # access_log /data/logs/image.log log404; # rewrite ^(.*)$ http://211.151.188.190:8080/face.jpg redirect; # } #} # # # ###其他举例 # #server{ # # listen 80; # # server_name *.***.com *.***.cn; # # location ~* \\.(mp3|exe)$ { # proxy_pass http://img_relay$request_uri; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # } # # location / { # if ($http_user_agent ~* \u0026#34;xnp\u0026#34;) { # rewrite ^(.*)$ http://i1.***img.com/help/noimg.gif redirect; # } # # proxy_pass http://img_relay$request_uri; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # #error_page 404 http://i1.***img.com/help/noimg.gif; # error_page 404 502 = @fetch; # } # # location @fetch { # access_log /data/logs/baijiaqi.log log404; # rewrite ^(.*)$ http://i1.***img.com/help/noimg.gif redirect; # } #} # #server{ # listen 80; # server_name *.***img.com; # # location ~* \\.(mp3|exe)$ { # proxy_pass http://img_relay$request_uri; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # } # # location / { # if ($http_user_agent ~* \u0026#34;xnp\u0026#34;) { # rewrite ^(.*)$ http://i1.***img.com/help/noimg.gif; # } # # proxy_pass http://img_relay$request_uri; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # #error_page 404 http://i1.***img.com/help/noimg.gif; # error_page 404 = @fetch; # } # ##access_log off; # # location @fetch { # access_log /data/logs/baijiaqi.log log404; # rewrite ^(.*)$ http://i1.***img.com/help/noimg.gif redirect; # } #} # #server{ # listen 8080; # server_name ngx-ha.***img.com; # # location / { # stub_status on; # access_log off; # } #} # #server { # listen 80; # server_name imgsrc1.***.net; # root html; #} # # # #server { # listen 80; # server_name ***.com w.***.com; # # access_log /usr/local/nginx/logs/access_log main; # location / { # rewrite ^(.*)$ http://www.***.com/ ; # } #} # #server { # listen 80; # server_name *******.com w.*******.com; # # access_log /usr/local/nginx/logs/access_log main; # location / { # rewrite ^(.*)$ http://www.*******.com/; # } #} # #server { # listen 80; # server_name ******.com; # # # access_log /usr/local/nginx/logs/access_log main; # # location / { # rewrite ^(.*)$ http://www.******.com/; # } # # location /NginxStatus { # stub_status on; # access_log on; # auth_basic \u0026#34;NginxStatus\u0026#34;; # auth_basic_user_file conf/htpasswd; # } # # #设定查看Nginx状态的地址 # location ~ /\\.ht { # deny all; # }#禁止访问.htxxx文件 # #} #配置虚拟机END\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { #root html; root /app; index zyt505050.html; #index index.html index.htm; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 # #location ~ \\.php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \\.php$ { # root html; # fastcgi_pass 127.0.0.1:9000; # fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; # include fastcgi_params; #} # deny access to .htaccess files, if Apache\u0026#39;s document root # concurs with nginx\u0026#39;s one # #location ~ /\\.ht { # deny all; #} } # another virtual host using mix of IP-, name-, and port-based configuration # #server { # listen 8000; # listen somename:8080; # server_name somename alias another.alias; # location / { # root html; # index index.html index.htm; # } #} # HTTPS server # #server { # listen 443 ssl; # server_name localhost; # ssl_certificate cert.pem; # ssl_certificate_key cert.key; # ssl_session_cache shared:SSL:1m; # ssl_session_timeout 5m; # ssl_ciphers HIGH:!aNULL:!MD5; # ssl_prefer_server_ciphers on; # location / { # root html; # index index.html index.htm; # } #} } 出现大量TIME_WAIT的情况 1）导致 nginx端出现大量TIME_WAIT的情况有两种：\nkeepalive_requests设置比较小，高并发下超过此值后nginx会强制关闭和客户端保持的keepalive长连接；（主动关闭连接后导致nginx出现TIME_WAIT） keepalive设置的比较小（空闲数太小），导致高并发下nginx会频繁出现连接数震荡（超过该值会关闭连接），不停的关闭、开启和后端server保持的keepalive长连接； 2）导致后端server端出现大量TIME_WAIT的情况： nginx没有打开和后端的长连接，即：没有设置proxy_http_version 1.1;和proxy_set_header Connection “”;从而导致后端server每次关闭连接，高并发下就会出现server端出现大量TIME_WAIT\n1 2 3 4 5 6 7 8 http { server { location / { proxy_http_version 1.1; // 这两个最好也设置 proxy_set_header Connection \u0026#34;\u0026#34;; } } } 资料链接 大型网站技术架构淘宝大牛\n淘宝大牛的博客干货资料\nnginx实现请求的负载均衡 + keepalived实现nginx的高可用\nnginx优化之keepalive\nNginx+keepalived搭建\u0026mdash;nginx反向代理为什么这么快\nKeepalived介绍以及-安装与配置\nLinux高可用之Keepalived-简书\n","permalink":"https://ktzxy.top/posts/5q22x33vzy/","summary":"nginx的相关配置解析","title":"nginx的相关配置解析"},{"content":" 日常Shell脚本 1、显示系统使用的以下信息：主机名、IP地址、子网掩码、网关、DNS服务器IP地址信息 1 2 3 4 5 6 7 8 9 10 11 #!/bin/bash IP=`ifconfig eth0 | head -2 | tail -1 | awk \u0026#39;{print $2}\u0026#39; | awk -F\u0026#34;:\u0026#34; \u0026#39;{print $2}\u0026#39;` ZW=` ifconfig eth0 | head -2 | tail -1 | awk \u0026#39;{print $3}\u0026#39; | awk -F\u0026#34;:\u0026#34; \u0026#39;{print $2}\u0026#39;` GW=`route -n | tail -1 | awk \u0026#39;{print $2}\u0026#39;` HN=`hostname` DNS=`head -1 /etc/resolv.conf | awk \u0026#39;{print $2}\u0026#39;` echo \u0026#39;此机IP地址是\u0026#39; $IP echo \u0026#39;此机子网掩码是\u0026#39; $ZW echo \u0026#39;此机网关是\u0026#39; $GW echo \u0026#39;此机主机名是\u0026#39; $HN echo \u0026#39;此机DNS是\u0026#39; $DNS 2、mysqlbak.sh备份数据库目录脚本 1 2 3 4 5 6 7 8 9 10 11 #!/bin/bash DAY=`date +%Y%m%d` SIZE=`du -sh /var/lib/mysql` echo \u0026#34;Date: $DAY\u0026#34; \u0026gt;\u0026gt; /tmp/dbinfo.txt echo \u0026#34;Data Size: $SIZE\u0026#34; \u0026gt;\u0026gt; /tmp/dbinfo.txt cd /opt/dbbak \u0026amp;\u0026gt; /dev/null || mkdir /opt/dbbak tar zcf /opt/dbbak/mysqlbak-${DAY}.tar.gz /var/lib/mysql /tmp/dbinfo.txt \u0026amp;\u0026gt; /dev/null rm -f /tmp/dbinfo.txt crontab-e 55 23 */3 * * /opt/dbbak/dbbak.sh 3、每周日半夜23点半，对数据库服务器上的webdb库做完整备份 每备份文件保存到系统的/mysqlbak目录里\n用系统日期做备份文件名 webdb-YYYY-mm-dd.sql\n每次完整备份后都生成新的binlog日志\n把当前所有的binlog日志备份到/mysqlbinlog目录下\n1 2 3 4 5 6 7 8 9 10 11 12 13 #mkdir /mysqlbak #mkdir /mysqlbinlog #service mysqld start cd /shell #vi webdb.sh #!/bin/bash day=`date +%F` mysqldump -hlocalhost -uroot -p123 webdb \u0026gt; /mysqlbak/webdb-${day}.sql mysql -hlocalhost -uroot -p -e \u0026#34;flush logs\u0026#34; tar zcf /mysqlbinlog.tar.gz /var/lib/mysql/mysqld-bin.0* #chmod +x webdb.sh #crontab -e 30 23 * * 7 /shell/webdb.sh 4、检查任意一个服务的运行状态 只检查服务vsftpd httpd sshd crond、mysql中任意一个服务的状态 如果不是这5个中的服务，就提示用户能够检查的服务名并退出脚本 如果服务是运行着的就输出 \u0026ldquo;服务名 is running\u0026rdquo; 如果服务没有运行就启动服务\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 方法1：使用read写脚本 #!/bin/bash read -p \u0026#34;请输入你的服务名:\u0026#34; service if [ $service != \u0026#39;crond\u0026#39; -a $service != \u0026#39;httpd\u0026#39; -a $service != \u0026#39;sshd\u0026#39; -a $service != \u0026#39;mysqld\u0026#39; -a $service != \u0026#39;vsftpd\u0026#39; ];then echo \u0026#34;只能够检查\u0026#39;vsftpd,httpd,crond,mysqld,sshd\u0026#34; exit 5 fi service $service status \u0026amp;\u0026gt; /dev/null if [ $? -eq 0 ];thhen echo \u0026#34;服务在线\u0026#34; else service $service start fi 或 方法2：使用位置变量来写脚本 if [ -z $1 ];then echo \u0026#34;You mast specify a servername!\u0026#34; echo \u0026#34;Usage: `basename$0` servername\u0026#34; exit 2 fi if [ $1 == \u0026#34;crond\u0026#34; ] || [ $1 == \u0026#34;mysql\u0026#34; ] || [ $1 == \u0026#34;sshd\u0026#34; ] || [ $1 == \u0026#34;httpd\u0026#34; ] || [ $1 == \u0026#34;vsftpd\u0026#34; ];then service $1 status \u0026amp;\u0026gt; /dev/null if [ $? -eq 0 ];then echo \u0026#34;$1 is running\u0026#34; else service $1 start fi else echo \u0026#34;Usage:`basename $0` server name\u0026#34; echo \u0026#34;But only check for vsftpd httpd sshd crond mysqld\u0026#34; \u0026amp;\u0026amp; exit2 fi 5、输出192.168.1.0/24网段内在线主机的ip地址 统计不在线主机的台数， 并把不在线主机的ip地址和不在线时的时间保存到/tmp/ip.txt文件里\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/bin/bash ip=192.168.1. j=0 for i in `seq 10 12` do ping -c 3 $ip$i \u0026amp;\u0026gt; /dev/null if [ $? -eq 0 ];then echo 在线的主机有：$ip$i else let j++ echo $ip$i \u0026gt;\u0026gt; /tmp/ip.txt date \u0026gt;\u0026gt; /tmp/ip.txt fi done echo 不在线的主机台数有 $j 6、一个简单的网站论坛测试脚本 用交互式的输入方法实现自动登录论坛数据库，修改用户密码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 [root@test1 scripts]# vim input.sh #!/bin/bash End=ucenter_members MYsql=/home/lnmp/mysql/bin/mysql read -p \u0026#34;Enter a website directory : \u0026#34; webdir WebPath=/home/WebSer/$webdir/config echo $WebPath read -p \u0026#34;Enter dbuser name : \u0026#34; dbuser echo $dbuser read -sp \u0026#34;Enter dbuser password : \u0026#34; dbpass read -p \u0026#34;Enter db name : \u0026#34; dbname echo $dbname read -p \u0026#34;Enter db tablepre : \u0026#34; dbtablepre echo $dbtablepre Globalphp=`grep \u0026#34;tablepre*\u0026#34; $WebPath/config_global.php |cut -d \u0026#34;\u0026#39;\u0026#34; -f8` Ucenterphp=`grep \u0026#34;UC_DBTABLEPRE*\u0026#34; $WebPath/config_ucenter.php |cut -d \u0026#39;.\u0026#39; -f2 | awk -F \u0026#34;\u0026#39;\u0026#34; \u0026#39;{print $1}\u0026#39;` if [ $dbtablepre == $Globalphp ] \u0026amp;\u0026amp; [ $dbtablepre == $Ucenterphp ];then Start=$dbtablepre Pre=`echo $Start$End` read -p \u0026#34;Enter you name : \u0026#34; userset echo $userset Result=`$MYsql -u$dbuser -p$dbpass $dbname -e \u0026#34;select username from $Pre where username=\u0026#39;$userset\u0026#39;\\G\u0026#34;|cut -d \u0026#39; \u0026#39; -f2|tail -1` echo $Result if [ $userset == $Result ];then read -p \u0026#34;Enter your password : \u0026#34; userpass passnew=`echo -n $userpass|openssl md5|cut -d \u0026#39; \u0026#39; -f2` $MYsql -u$dbuser -p$dbpass $dbname -e \u0026#34;update $Pre set password=\u0026#39;$passnew\u0026#39; where username=\u0026#39;$userset\u0026#39;;\u0026#34; $MYsql -u$dbuser -p$dbpass $dbname -e \u0026#34;flush privileges;\u0026#34; else echo \u0026#34;$userset is not right user!\u0026#34; exit 1 fi else exit 2 fi 7、检查mysql主从从结构中从数据库服务器的状态 1）本机的数据库服务是否正在运行 2）能否与主数据库服务器正常通信 3）能否使用授权用户连接数据库服务器 4）本机的slave_IO进程是否处于YES状态 本机的slave_SQL进程是否处于YES状态\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 [root@test1 scripts]# vim test.sh #!/bin/bash netstat -tulnp | grep :3306 \u0026gt; /dev/null if [ $? -eq 0 ];then echo \u0026#34;服务正在运行\u0026#34; else service mysqld start fi ping -c 3 192.168.1.100 \u0026amp;\u0026gt; /dev/null if [ $? -eq 0 ];then echo \u0026#34;网络连接正常\u0026#34; else echo \u0026#34;网络连接失败\u0026#34; fi mysql -h192.168.1.100 -uroot -p123456 \u0026amp;\u0026gt; /dev/null if [ $? -eq 0 ];then echo \u0026#34;数据库连接成功\u0026#34; else echo \u0026#34;数据库连接失败\u0026#34; fi IO= mysql -uroot -p123 -e \u0026#34;show slave status\\G\u0026#34; | grep Slave_IO_Running | awk \u0026#39;{print $2}\u0026#39; \u0026gt; /dev/null SQL= mysql -uroot -p123 -e \u0026#34;show slave status\\G\u0026#34; | grep Slave_SQL_Running | awk \u0026#39;{print $2}\u0026#39; /dev/null if [ IO==Yes ] \u0026amp;\u0026amp; [ SQL==Yes ];then echo “IO and SQL 连接成功” else echo \u0026#34;IO线程和SQL线程连接失败\u0026#34; fi 8、rpm 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 rpm{ rpm -ivh lynx # rpm安装 rpm -e lynx # 卸载包 rpm -e lynx --nodeps # 强制卸载 rpm -qa # 查看所有安装的rpm包 rpm -qa | grep lynx # 查找包是否安装 rpm -ql # 软件包路径 rpm -Uvh # 升级包 rpm --test lynx # 测试 rpm -qc # 软件包配置文档 rpm --initdb # 初始化rpm 数据库 rpm --rebuilddb # 重建rpm数据库 在rpm和yum无响应的情况使用 先 rm -f /var/lib/rpm/__db.00* 在重建 } 9、yum 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 yum{ yum list # 所有软件列表 yum install 包名 # 安装包和依赖包 yum -y update # 升级所有包版本,依赖关系，系统版本内核都升级 yum -y update 软件包名 # 升级指定的软件包 yum -y upgrade # 不改变软件设置更新软件，系统版本升级，内核不改变 yum search mail # yum搜索相关包 yum grouplist # 软件包组 yum -y groupinstall \u0026#34;Virtualization\u0026#34; # 安装软件包组 repoquery -ql gstreamer # 不安装软件查看包含文件 yum clean all # 清除var下缓存 } yum使用epel源{ # 包下载地址: http://download.fedoraproject.org/pub/epel # 选择版本5\\6\\7 rpm -Uvh http://mirrors.hustunique.com/epel//6/x86_64/epel-release-6-8.noarch.rpm # 自适配版本 yum install epel-release } 自定义yum源{ find /etc/yum.repos.d -name \u0026#34;*.repo\u0026#34; -exec mv {} {}.bak \\; vim /etc/yum.repos.d/yum.repo [yum] #http baseurl=http://10.0.0.1/centos5.5 #挂载iso #mount -o loop CentOS-5.8-x86_64-bin-DVD-1of2.iso /data/iso/ #本地 #baseurl=file:///data/iso/ enable=1 #导入key rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-5 } 10、编译安装 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 编译{ 源码安装{ ./configure --help # 查看所有编译参数 ./configure --prefix=/usr/local/ # 配置参数 make # 编译 # make -j 8 # 多线程编译,速度较快,但有些软件不支持 make install # 安装包 make clean # 清除编译结果 } perl程序编译{ perl Makefile.PL make make test make install } python程序编译{ python file.py # 源码包编译安装 python setup.py build python setup.py install } 编译c程序{ gcc -g hello.c -o hello } } 11、系统 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 系统 wall # 给其它用户发消息 whereis ls # 搜索程序名，而且只搜索二进制文件 which # 查找命令是否存在,及存放位置 locate # 不是实时查找，查找的结果不精确，但查找速度很快 每天更新 /var/lib/locatedb clear # 清空整个屏幕 reset # 重新初始化屏幕 cal # 显示月历 echo -n 123456 | md5sum # md5加密 mkpasswd # 随机生成密码 -l位数 -C大小 -c小写 -d数字 -s特殊字符 netstat -ntupl | grep port # 是否打开了某个端口 ntpdate cn.pool.ntp.org # 同步时间, pool.ntp.org: public ntp time server for everyone(http://www.pool.ntp.org/zh/) tzselect # 选择时区 #+8=(5 9 1 1) # (TZ=\u0026#39;Asia/Shanghai\u0026#39;; export TZ)括号内写入 /etc/profile /sbin/hwclock -w # 时间保存到硬件 /etc/shadow # 账户影子文件 LANG=en # 修改语言 vim /etc/sysconfig/i18n # 修改编码 LANG=\u0026#34;en_US.UTF-8\u0026#34; export LC_ALL=C # 强制字符集 vi /etc/hosts # 查询静态主机名 alias # 别名 watch uptime # 监测命令动态刷新 监视 ipcs -a # 查看Linux系统当前单个共享内存段的最大值 ldconfig # 动态链接库管理命令 ldd `which cmd` # 查看命令的依赖库 dist-upgrade # 会改变配置文件,改变旧的依赖关系，改变系统版本 /boot/grub/grub.conf # grub启动项配置 ps -mfL \u0026lt;PID\u0026gt; # 查看指定进程启动的线程 线程数受 max user processes 限制 ps uxm |wc -l # 查看当前用户占用的进程数 [包括线程] max user processes top -p PID -H # 查看指定PID进程及线程 lsof |wc -l # 查看当前文件句柄数使用数量 open files lsof |grep /lib # 查看加载库文件 sysctl -a # 查看当前所有系统内核参数 sysctl -p # 修改内核参数/etc/sysctl.conf，让/etc/rc.d/rc.sysinit读取生效 strace -p pid # 跟踪系统调用 ps -eo \u0026#34;%p %C %z %a\u0026#34;|sort -k3 -n # 把进程按内存使用大小排序 strace uptime 2\u0026gt;\u0026amp;1|grep open # 查看命令打开的相关文件 grep Hugepagesize /proc/meminfo # 内存分页大小 mkpasswd -l 8 -C 2 -c 2 -d 4 -s 0 # 随机生成指定类型密码 echo 1 \u0026gt; /proc/sys/net/ipv4/tcp_syncookies # 使TCP SYN Cookie 保护生效 # \u0026#34;SYN Attack\u0026#34;是一种拒绝服务的攻击方式 grep Swap /proc/25151/smaps |awk \u0026#39;{a+=$2}END{print a}\u0026#39; # 查询某pid使用的swap大小 redir --lport=33060 --caddr=10.10.10.78 --cport=3306 # 端口映射 yum安装 用supervisor守护 12、开机启动脚本顺序 1 2 3 4 5 6 7 8 9 开机启动脚本顺序{ /etc/profile /etc/profile.d/*.sh ~/bash_profile ~/.bashrc /etc/bashrc } 13、进程管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 进程管理{ ps -eaf # 查看所有进程 kill -9 PID # 强制终止某个PID进程 kill -15 PID # 安全退出 需程序内部处理信号 cmd \u0026amp; # 命令后台运行 nohup cmd \u0026amp; # 后台运行不受shell退出影响 ctrl+z # 将前台放入后台(暂停) jobs # 查看后台运行程序 bg 2 # 启动后台暂停进程 fg 2 # 调回后台进程 pstree # 进程树 vmstat 1 9 # 每隔一秒报告系统性能信息9次 sar # 查看cpu等状态 lsof file # 显示打开指定文件的所有进程 lsof -i:32768 # 查看端口的进程 renice +1 180 # 把180号进程的优先级加1 exec sh a.sh # 子进程替换原来程序的pid， 避免supervisor无法强制杀死进程 14、ps 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 ps{ ps aux |grep -v USER | sort -nk +4 | tail # 显示消耗内存最多的10个运行中的进程，以内存使用量排序.cpu +3 # USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND %CPU # 进程的cpu占用率 %MEM # 进程的内存占用率 VSZ # 进程虚拟大小,单位K(即总占用内存大小,包括真实内存和虚拟内存) RSS # 进程使用的驻留集大小即实际物理内存大小 START # 进程启动时间和日期 占用的虚拟内存大小 = VSZ - RSS ps -eo pid,lstart,etime,args # 查看进程启动时间 } top{ 前五行是系统整体的统计信息。 第一行: 任务队列信息，同 uptime 命令的执行结果。内容如下： 01:06:48 当前时间 up 1:22 系统运行时间，格式为时:分 1 user 当前登录用户数 load average: 0.06, 0.60, 0.48 系统负载，即任务队列的平均长度。 三个数值分别为 1分钟、5分钟、15分钟前到现在的平均值。 第二、三行:为进程和CPU的信息。当有多个CPU时，这些内容可能会超过两行。内容如下： Tasks: 29 total 进程总数 1 running 正在运行的进程数 28 sleeping 睡眠的进程数 0 stopped 停止的进程数 0 zombie 僵尸进程数 Cpu(s): 0.3% us 用户空间占用CPU百分比 1.0% sy 内核空间占用CPU百分比 0.0% ni 用户进程空间内改变过优先级的进程占用CPU百分比 98.7% id 空闲CPU百分比 0.0% wa 等待输入输出的CPU时间百分比 0.0% hi 0.0% si 第四、五行:为内存信息。内容如下： Mem: 191272k total 物理内存总量 173656k used 使用的物理内存总量 17616k free 空闲内存总量 22052k buffers 用作内核缓存的内存量 Swap: 192772k total 交换区总量 0k used 使用的交换区总量 192772k free 空闲交换区总量 123988k cached 缓冲的交换区总量。 内存中的内容被换出到交换区，而后又被换入到内存，但使用过的交换区尚未被覆盖， 该数值即为这些内容已存在于内存中的交换区的大小。 相应的内存再次被换出时可不必再对交换区写入。 进程信息区,各列的含义如下: # 显示各个进程的详细信息 序号 列名 含义 a PID 进程id b PPID 父进程id c RUSER Real user name d UID 进程所有者的用户id e USER 进程所有者的用户名 f GROUP 进程所有者的组名 g TTY 启动进程的终端名。不是从终端启动的进程则显示为 ? h PR 优先级 i NI nice值。负值表示高优先级，正值表示低优先级 j P 最后使用的CPU，仅在多CPU环境下有意义 k %CPU 上次更新到现在的CPU时间占用百分比 l TIME 进程使用的CPU时间总计，单位秒 m TIME+ 进程使用的CPU时间总计，单位1/100秒 n %MEM 进程使用的物理内存百分比 o VIRT 进程使用的虚拟内存总量，单位kb。VIRT=SWAP+RES p SWAP 进程使用的虚拟内存中，被换出的大小，单位kb。 q RES 进程使用的、未被换出的物理内存大小，单位kb。RES=CODE+DATA r CODE 可执行代码占用的物理内存大小，单位kb s DATA 可执行代码以外的部分(数据段+栈)占用的物理内存大小，单位kb t SHR 共享内存大小，单位kb u nFLT 页面错误次数 v nDRT 最后一次写入到现在，被修改过的页面数。 w S 进程状态。 D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程 父进程在但并不等待子进程 x COMMAND 命令名/命令行 y WCHAN 若该进程在睡眠，则显示睡眠中的系统函数名 z Flags 任务标志，参考 sched.h } 15、列出正在占用swap的进程 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 列出正在占用swap的进程{ #!/bin/bash echo -e \u0026#34;PID\\t\\tSwap\\t\\tProc_Name\u0026#34; # 拿出/proc目录下所有以数字为名的目录（进程名是数字才是进程，其他如sys,net等存放的是其他信息） for pid in `ls -l /proc | grep ^d | awk \u0026#39;{ print $9 }\u0026#39;| grep -v [^0-9]` do # 让进程释放swap的方法只有一个：就是重启该进程。或者等其自动释放。放 # 如果进程会自动释放，那么我们就不会写脚本来找他了，找他都是因为他没有自动释放。 # 所以我们要列出占用swap并需要重启的进程，但是init这个进程是系统里所有进程的祖先进程 # 重启init进程意味着重启系统，这是万万不可以的，所以就不必检测他了，以免对系统造成影响。 if [ $pid -eq 1 ];then continue;fi grep -q \u0026#34;Swap\u0026#34; /proc/$pid/smaps 2\u0026gt;/dev/null if [ $? -eq 0 ];then swap=$(grep Swap /proc/$pid/smaps \\ | gawk \u0026#39;{ sum+=$2;} END{ print sum }\u0026#39;) proc_name=$(ps aux | grep -w \u0026#34;$pid\u0026#34; | grep -v grep \\ | awk \u0026#39;{ for(i=11;i\u0026lt;=NF;i++){ printf(\u0026#34;%s \u0026#34;,$i); }}\u0026#39;) if [ $swap -gt 0 ];then echo -e \u0026#34;${pid}\\t${swap}\\t${proc_name}\u0026#34; fi fi done | sort -k2 -n | awk -F\u0026#39;\\t\u0026#39; \u0026#39;{ pid[NR]=$1; size[NR]=$2; name[NR]=$3; } END{ for(id=1;id\u0026lt;=length(pid);id++) { if(size[id]\u0026lt;1024) printf(\u0026#34;%-10s\\t%15sKB\\t%s\\n\u0026#34;,pid[id],size[id],name[id]); else if(size[id]\u0026lt;1048576) printf(\u0026#34;%-10s\\t%15.2fMB\\t%s\\n\u0026#34;,pid[id],size[id]/1024,name[id]); else printf(\u0026#34;%-10s\\t%15.2fGB\\t%s\\n\u0026#34;,pid[id],size[id]/1048576,name[id]); } }\u0026#39; } 16、linux操作系统提供的信号 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 linux操作系统提供的信号{ kill -l # 查看linux提供的信号 trap \u0026#34;echo aaa\u0026#34; 2 3 15 # shell使用 trap 捕捉退出信号 # 发送信号一般有两种原因: # 1(被动式) 内核检测到一个系统事件.例如子进程退出会像父进程发送SIGCHLD信号.键盘按下control+c会发送SIGINT信号 # 2(主动式) 通过系统调用kill来向指定进程发送信号 # 进程结束信号 SIGTERM 和 SIGKILL 的区别: SIGTERM 比较友好，进程能捕捉这个信号，根据您的需要来关闭程序。在关闭程序之前，您可以结束打开的记录文件和完成正在做的任务。在某些情况下，假如进程正在进行作业而且不能中断，那么进程可以忽略这个SIGTERM信号。 # 如果一个进程收到一个SIGUSR1信号，然后执行信号绑定函数，第二个SIGUSR2信号又来了，第一个信号没有被处理完毕的话，第二个信号就会丢弃。 SIGHUP 1 A # 终端挂起或者控制进程终止 SIGINT 2 A # 键盘终端进程(如control+c) SIGQUIT 3 C # 键盘的退出键被按下 SIGILL 4 C # 非法指令 SIGABRT 6 C # 由abort(3)发出的退出指令 SIGFPE 8 C # 浮点异常 SIGKILL 9 AEF # Kill信号 立刻停止 SIGSEGV 11 C # 无效的内存引用 SIGPIPE 13 A # 管道破裂: 写一个没有读端口的管道 SIGALRM 14 A # 闹钟信号 由alarm(2)发出的信号 SIGTERM 15 A # 终止信号,可让程序安全退出 kill -15 SIGUSR1 30,10,16 A # 用户自定义信号1 SIGUSR2 31,12,17 A # 用户自定义信号2 SIGCHLD 20,17,18 B # 子进程结束自动向父进程发送SIGCHLD信号 SIGCONT 19,18,25 # 进程继续（曾被停止的进程） SIGSTOP 17,19,23 DEF # 终止进程 SIGTSTP 18,20,24 D # 控制终端（tty）上按下停止键 SIGTTIN 21,21,26 D # 后台进程企图从控制终端读 SIGTTOU 22,22,27 D # 后台进程企图从控制终端写 缺省处理动作一项中的字母含义如下: A 缺省的动作是终止进程 B 缺省的动作是忽略此信号，将该信号丢弃，不做处理 C 缺省的动作是终止进程并进行内核映像转储(dump core),内核映像转储是指将进程数据在内存的映像和进程在内核结构中的部分内容以一定格式转储到文件系统，并且进程退出执行，这样做的好处是为程序员提供了方便，使得他们可以得到进程当时执行时的数据值，允许他们确定转储的原因，并且可以调试他们的程序。 D 缺省的动作是停止进程，进入停止状况以后还能重新进行下去，一般是在调试的过程中（例如ptrace系统调用） E 信号不能被捕获 F 信号不能被忽略 } 17、系统性能状态 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 系统性能状态{ vmstat 1 9 r # 等待执行的任务数。当这个值超过了cpu线程数，就会出现cpu瓶颈。 b # 等待IO的进程数量,表示阻塞的进程。 swpd # 虚拟内存已使用的大小，如大于0，表示机器物理内存不足，如不是程序内存泄露，那么该升级内存。 free # 空闲的物理内存的大小 buff # 已用的buff大小，对块设备的读写进行缓冲 cache # cache直接用来记忆我们打开的文件,给文件做缓冲，(把空闲的物理内存的一部分拿来做文件和目录的缓存，是为了提高 程序执行的性能，当程序使用内存时，buffer/cached会很快地被使用。) inact # 非活跃内存大小，即被标明可回收的内存，区别于free和active -a选项时显示 active # 活跃的内存大小 -a选项时显示 si # 每秒从磁盘读入虚拟内存的大小，如果这个值大于0，表示物理内存不够用或者内存泄露，要查找耗内存进程解决掉。 so # 每秒虚拟内存写入磁盘的大小，如果这个值大于0，同上。 bi # 块设备每秒接收的块数量，这里的块设备是指系统上所有的磁盘和其他块设备，默认块大小是1024byte bo # 块设备每秒发送的块数量，例如读取文件，bo就要大于0。bi和bo一般都要接近0，不然就是IO过于频繁，需要调整。 in # 每秒CPU的中断次数，包括时间中断。in和cs这两个值越大，会看到由内核消耗的cpu时间会越多 cs # 每秒上下文切换次数，例如我们调用系统函数，就要进行上下文切换，线程的切换，也要进程上下文切换，这个值要越小越好，太大了，要考虑调低线程或者进程的数目,例如在apache和nginx这种web服务器中，我们一般做性能测试时会进行几千并发甚至几万并发的测试，选择web服务器的进程可以由进程或者线程的峰值一直下调，压测，直到cs到一个比较小的值，这个进程和线程数就是比较合适的值了。系统调用也是，每次调用系统函数，我们的代码就会进入内核空间，导致上下文切换，这个是很耗资源，也要尽量避免频繁调用系统函数。上下文切换次数过多表示你的CPU大部分浪费在上下文切换，导致CPU干正经事的时间少了，CPU没有充分利用。 us # 用户进程执行消耗cpu时间(user time) us的值比较高时，说明用户进程消耗的cpu时间多，但是如果长期超过50%的使用，那么我们就该考虑优化程序算法或其他措施 sy # 系统CPU时间，如果太高，表示系统调用时间长，例如是IO操作频繁。 id # 空闲 CPU时间，一般来说，id + us + sy = 100,一般认为id是空闲CPU使用率，us是用户CPU使用率，sy是系统CPU使用率。 wt # 等待IOCPU时间。Wa过高时，说明io等待比较严重，这可能是由于磁盘大量随机访问造成的，也有可能是磁盘的带宽出现瓶颈。 如果 r 经常大于4，且id经常少于40，表示cpu的负荷很重。 如果 pi po 长期不等于0，表示内存不足。 如果 b 队列经常大于3，表示io性能不好。 } } 18、日志管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 日志管理{ history # 历时命令默认1000条 HISTTIMEFORMAT=\u0026#34;%Y-%m-%d %H:%M:%S \u0026#34; # 让history命令显示具体时间 history -c # 清除记录命令 cat $HOME/.bash_history # 历史命令记录文件 lastb -a # 列出登录系统失败的用户相关信息 清空二进制日志记录文件 echo \u0026gt; /var/log/btmp last # 查看登陆过的用户信息 清空二进制日志记录文件 echo \u0026gt; /var/log/wtmp 默认打开乱码 who /var/log/wtmp # 查看登陆过的用户信息 lastlog # 用户最后登录的时间 tail -f /var/log/messages # 系统日志 tail -f /var/log/secure # ssh日志 } man{ man 2 read # 查看read函数的文档 1 使用者在shell中可以操作的指令或可执行档 2 系统核心可呼叫的函数与工具等 3 一些常用的函数(function)与函数库(library),大部分是C的函数库(libc) 4 装置档案的说明，通常在/dev下的档案 5 设定档或者是某些档案的格式 6 游戏games 7 惯例与协定等，例如linux档案系统、网络协定、ascll code等说明 8 系统管理员可用的管理指令 9 跟kernel有关的文件 } selinux{ sestatus -v # 查看selinux状态 getenforce # 查看selinux模式 setenforce 0 # 设置selinux为宽容模式(可避免阻止一些操作) semanage port -l # 查看selinux端口限制规则 semanage port -a -t http_port_t -p tcp 8000 # 在selinux中注册端口类型 vi /etc/selinux/config # selinux配置文件 SELINUX=enfoceing # 关闭selinux 把其修改为 SELINUX=disabled } 19、查看剩余内存 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 查看剩余内存{ free -m #-/+ buffers/cache: 6458 1649 #6458M为真实使用内存 1649M为真实剩余内存(剩余内存+缓存+缓冲器) #linux会利用所有的剩余内存作为缓存，所以要保证linux运行速度，就需要保证内存的缓存大小 } 系统信息{ uname -a # 查看Linux内核版本信息 cat /proc/version # 查看内核版本 cat /etc/issue # 查看系统版本 lsb_release -a # 查看系统版本 需安装 centos-release locale -a # 列出所有语系 locale # 当前环境变量中所有编码 hwclock # 查看时间 who # 当前在线用户 w # 当前在线用户 whoami # 查看当前用户名 logname # 查看初始登陆用户名 uptime # 查看服务器启动时间 sar -n DEV 1 10 # 查看网卡网速流量 dmesg # 显示开机信息 lsmod # 查看内核模块 } 硬件信息{ more /proc/cpuinfo # 查看cpu信息 lscpu # 查看cpu信息 cat /proc/cpuinfo | grep name | cut -f2 -d: | uniq -c # 查看cpu型号和逻辑核心数 getconf LONG_BIT # cpu运行的位数 cat /proc/cpuinfo | grep \u0026#39;physical id\u0026#39; |sort| uniq -c # 物理cpu个数 cat /proc/cpuinfo | grep flags | grep \u0026#39; lm \u0026#39; | wc -l # 结果大于0支持64位 cat /proc/cpuinfo|grep flags # 查看cpu是否支持虚拟化 pae支持半虚拟化 IntelVT 支持全虚拟化 more /proc/meminfo # 查看内存信息 dmidecode # 查看全面硬件信息 dmidecode | grep \u0026#34;Product Name\u0026#34; # 查看服务器型号 dmidecode | grep -P -A5 \u0026#34;Memory\\s+Device\u0026#34; | grep Size | grep -v Range # 查看内存插槽 cat /proc/mdstat # 查看软raid信息 cat /proc/scsi/scsi # 查看Dell硬raid信息(IBM、HP需要官方检测工具) lspci # 查看硬件信息 lspci|grep RAID # 查看是否支持raid lspci -vvv |grep Ethernet # 查看网卡型号 lspci -vvv |grep Kernel|grep driver # 查看驱动模块 modinfo tg2 # 查看驱动版本(驱动模块) ethtool -i em1 # 查看网卡驱动版本 ethtool em1 # 查看网卡带宽 } 终端快捷键{ Ctrl+A # 行前 Ctrl+E # 行尾 Ctrl+S # 终端锁屏 Ctrl+Q # 解锁屏 Ctrl+D # 退出 } 开机启动模式{ vi /etc/inittab id:3:initdefault: # 3为多用户命令 #ca::ctrlaltdel:/sbin/shutdown -t3 -r now # 注释此行 禁止 ctrl+alt+del 关闭计算机 } 终端提示显示{ echo $PS1 # 环境变量控制提示显示 PS1=\u0026#39;[\\u@ \\H \\w \\A \\@#]\\$\u0026#39; PS1=\u0026#39;[\\u@\\h \\W]\\$\u0026#39; export PS1=\u0026#39;[\\[\\e[32m\\]\\[\\e[31m\\]\\u@\\[\\e[36m\\]\\h \\w\\[\\e[m\\]]\\$ \u0026#39; # 高亮显示终端 } 20、定时任务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 定时任务{ at 5pm + 3 days /bin/ls # 单次定时任务 指定三天后下午5:00执行/bin/ls crontab -e # 编辑周期任务 #分钟 小时 天 月 星期 命令或脚本 1,30 1-3/2 * * * 命令或脚本 \u0026gt;\u0026gt; file.log 2\u0026gt;\u0026amp;1 echo \u0026#34;40 7 * * 2 /root/sh\u0026#34;\u0026gt;\u0026gt;/var/spool/cron/work # 普通用户可直接写入定时任务 crontab -l # 查看自动周期性任务 crontab -r # 删除自动周期性任务 cron.deny和cron.allow # 禁止或允许用户使用周期任务 service crond start|stop|restart # 启动自动周期性服务 * * * * * echo \u0026#34;d\u0026#34; \u0026gt;\u0026gt;d$(date +\\%Y\\%m\\%d).log # 让定时任务直接生成带日期的log 需要转义% } date{ 星期日[SUN] 星期一[MON] 星期二[TUE] 星期三[WED] 星期四[THU] 星期五[FRI] 星期六[SAT] 一月[JAN] 二月[FEB] 三月[MAR] 四月[APR] 五月[MAY] 六月[JUN] 七月[JUL] 八月[AUG] 九月[SEP] 十月[OCT] 十一月[NOV] 十二月[DEC] date -s 20091112 # 设日期 date -s 18:30:50 # 设时间 date -d \u0026#34;7 days ago\u0026#34; +%Y%m%d # 7天前日期 date -d \u0026#34;5 minute ago\u0026#34; +%H:%M # 5分钟前时间 date -d \u0026#34;1 month ago\u0026#34; +%Y%m%d # 一个月前 date -d \u0026#39;1 days\u0026#39; +%Y-%m-%d # 一天后 date -d \u0026#39;1 hours\u0026#39; +%H:%M:%S # 一小时后 date +%Y-%m-%d -d \u0026#39;20110902\u0026#39; # 日期格式转换 date +%Y-%m-%d_%X # 日期和时间 date +%N # 纳秒 date -d \u0026#34;2012-08-13 14:00:23\u0026#34; +%s # 换算成秒计算(1970年至今的秒数) date -d \u0026#34;@1363867952\u0026#34; +%Y-%m-%d-%T # 将时间戳换算成日期 date -d \u0026#34;1970-01-01 UTC 1363867952 seconds\u0026#34; +%Y-%m-%d-%T # 将时间戳换算成日期 date -d \u0026#34;`awk -F. \u0026#39;{print $1}\u0026#39; /proc/uptime` second ago\u0026#34; +\u0026#34;%Y-%m-%d %H:%M:%S\u0026#34; # 格式化系统启动时间(多少秒前) } limits.conf{ ulimit -SHn 65535 # 临时设置文件描述符大小 进程最大打开文件柄数 还有socket最大连接数, 等同配置 nofile ulimit -SHu 65535 # 临时设置用户最大进程数 ulimit -a # 查看 /etc/security/limits.conf # 文件描述符大小 open files # lsof |wc -l 查看当前文件句柄数使用数量 * soft nofile 16384 # 设置太大，进程使用过多会把机器拖死 * hard nofile 32768 # 用户最大进程数 max user processes # echo $((`ps uxm |wc -l`-`ps ux |wc -l`)) 查看当前用户占用的进程数 [包括线程] user soft nproc 16384 user hard nproc 32768 # 如果/etc/security/limits.d/有配置文件，将会覆盖/etc/security/limits.conf里的配置 # 即/etc/security/limits.d/的配置文件里就不要有同样的参量设置 /etc/security/limits.d/90-nproc.conf # centos6.3的默认这个文件会覆盖 limits.conf user soft nproc 16384 user hard nproc 32768 sysctl -p # 修改配置文件后让系统生效 } ","permalink":"https://ktzxy.top/posts/afd13o8vgj/","summary":"日常Shell脚本","title":"日常Shell脚本"},{"content":"搭建K8S集群 搭建k8s环境平台规划 单master集群 单个master节点，然后管理多个node节点\n多master集群 多个master节点，管理多个node节点，同时中间多了一个负载均衡的过程\n服务器硬件配置要求 测试环境 master：2核 4G 20G\nnode： 4核 8G 40G\n生产环境 master：8核 16G 100G\nnode： 16核 64G 200G\n目前生产部署Kubernetes集群主要有两种方式\nkubeadm kubeadm是一个K8S部署工具，提供kubeadm init 和 kubeadm join，用于快速部署Kubernetes集群\n官网地址：点我传送\n二进制包 从github下载发行版的二进制包，手动部署每个组件，组成Kubernetes集群。\nKubeadm降低部署门槛，但屏蔽了很多细节，遇到问题很难排查。如果想更容易可控，推荐使用二进制包部署Kubernetes集群，虽然手动部署麻烦点，期间可以学习很多工作原理，也利于后期维护。\nKubeadm部署集群 kubeadm 是官方社区推出的一个用于快速部署kubernetes 集群的工具，这个工具能通过两条指令完成一个kubernetes 集群的部署：\n创建一个Master 节点kubeadm init 将Node 节点加入到当前集群中$ kubeadm join \u0026lt;Master 节点的IP 和端口\u0026gt; 安装要求 在开始之前，部署Kubernetes集群机器需要满足以下几个条件\n一台或多台机器，操作系统为Centos7.X 硬件配置：2GB或更多GAM，2个CPU或更多CPU，硬盘30G 集群中所有机器之间网络互通 可以访问外网，需要拉取镜像 禁止swap分区 ","permalink":"https://ktzxy.top/posts/rsm58vhr4g/","summary":"2 搭建K8S集群前置知识","title":"2 搭建K8S集群前置知识"},{"content":"设计模式 1. 简介 设计模式（Design Pattern），是经过高度抽象化的在编程中可以被反复使用的代码设计经验的总结，是解决特定问题的一系列套路，是一种编程思想。它不是语法规定，而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。\n正确使用设计模式能有效提高代码的可读性、可重用性和可靠性，编写符合设计模式规范的代码不但有利于自身系统的稳定、可靠，还有利于外部系统的对接。在使用了良好的设计模式的系统工程中，无论是对满足当前的需求，还是对适应未来的需求，无论是对自身系统间模块的对接，还是对外部系统的对接，都有很大的帮助。\n随着软件工程的不断演进，针对不同的需求，新的设计模式不断被提出（比如大数据领域中这些年不断被大家认可的数据分片思想），但设计模式的原则不会变。基于设计模式的原则，可以使用已有的设计模式，也可以根据产品或项目的开发需求在现有的设计模式基础上组合、改造或重新设计自身的设计模式。\n2. 设计模式的原则 设计模式有 7 个原则：单一职责原则、开闭原则、里氏代换原则、依赖倒转原则、接口隔离原则、合成／聚合复用原则、迪米特法则。具体内容如下：\n单一职责原则，又称单一功能原则，它规定一个类只有一个职责。如果有多个职责（功能）被设计在一个类中，这个类就违反了单一职责原则。 开闭原则，规定软件中的对象（类、模块、函数等）对扩展开放，对修改封闭，这意味着一个实体允许在不改变其源代码的前提下改变其行为，该特性在产品化的环境下是特别有价值的，在这种环境下，改变惊代码需要经过代码审查、单元测试等过程，以确保产品的使用质量。遵循这个原则的代码在扩展时并不发生改变，因此不需要经历上述过程。 里氏代换原则，是对开闭原则的补充，规定了在任意父类可以出现的地方，子类都一定可以出现。实现开闭原则的关键就是抽象化，父类与子类的继承关系就是抽象化的具体表现，所以里氏代换原则是对实现抽象化的具体步骤的规范。 依赖倒转原则，指程序要依赖于抽象（Java 中的抽象类和接口），而不依赖于具体的实现（Java 中的实现类）。简单地说，就是要求对抽象进行编程，不要求对实现进行编程，这就降低了用户与实现模块之间的耦合度。 接口隔离原则，指通过将不同的功能定义在不同的接口中来实现接口的隔离，这样就避免了其他类在依赖该接口（接口上定义的功能）时依赖其不需要的接口，可减少接口之间依赖的冗余性和复杂性。 合成／聚合复用原则，指通过在一个新的对象中引人（注入）已有的对象以达到类的功能复用和扩展的目的。它的设计原则是要尽量使用合成或聚合而不要使用继承来扩展类的功能。 迪米特法则，指一个对象尽可能少地与其他对象发生相互了解或依赖。其核心思想在于降低模块之间的耦合度，提高模块的内聚性。迪米特法则规定每个模块对其他模块都要有尽可能少的了解和依赖，因此很容易使系统模块之间功能独立，这使得各个模块的独立运行变得更简单，同时使得各个模块之间的组合变得更容易。 3. 设计模式的分类 设计模式按照其功能和使用场景可以分为三大类：创建型模式（Creational Pattern）、结构型模式（Structural Pattern）和行为型模式（Behavioral Pattern）\n创建型模式：提供了多种优雅创建对象的方法 工厂模式（Factory Pattern） 抽象工厂模式（Abstract Factory Pattern） 单例模式（Singleton Pattern） 建造者模式（Builder Pattern） 原型模式（Prototype Pattern） 结构型模式：通过类和接口之间的继承和引用实现创建复杂结构对象的功能 适配器模式（Adapter Pattern ） 桥接模式（Bridge Pattern） 过滤器模式（Filter 、Criteria Pattern） 组合模式（Composite Pattern） 装饰器模式（Decorator Pattern） 外观模式（Facade Pattern） 享元模式（Flyweight Pattern） 代理模式（Proxy Pattern） 行为型模式：通过类之间不同的通信方式实现不同的行为方式 责任链模式（Chain of Responsibility Pattern） 命令模式（Command Pattern） 解释器模式（Interpreter Pattern） 迭代器模式（Iterator Pattern） 中介者模式（Mediator Pattern） 备忘录模式（Memento Pattern） 观察者模式（Observer Pattern） 状态模式（State Pattern） 策略模式（Strategy Pattern） 模板模式（Template Pattern） 访问者模式（Visitor Pattern） 单例设计模式（Singleten Pattern） 框架中会经常出现单例类\n1. 单例的特点 类在程序的运行过程中只有一个对象(实例)。 私有构造方法，不能让外界创建该类的对象。 类必须提供一个静态公共方法返回该类的对象。 2. 单例实现方式（饿汉式） 当类加载时，就立即创建该单例对象。饿汉式是线程安全的。实现步骤如下：\n定义一个静态的对象成员变量 要私有构造方法，如果不处理，系统会自动提供一个无参的构造方法。外界就可以直接new对象 定义一个静态的成员方法，用来获取创建好的对象成员，一般命名用 getInstance() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /** * 单例类，饿汉式：非懒加载。 * 当类加载时，就立即创建该单例对象。以空间换时间 */ public class EagerSingleton { // 定义一个静态的成员变量，在类加载完成之后都已经完成了初始化赋值的操作。 private static EagerSingleton instance = new EagerSingleton(); // 注意：要私有构造方法，如果不处理，系统会自动提供一个无参的构造方法。 // 保证其他类对象使用时不能直接new一个新的实例 private EagerSingleton() { } // 定义一个静态的成员方法，用来获取创建好的成员对象 public static EagerSingleton getInstance() { return instance; } } 涉及知识点提示：所有的静态成员在类加载完成之后都已经完成了初始化赋值的操作。也就是EagerSingleton类的私有构造函数会被调用，实例会被创建。还有就是构造函数要使用private修饰，为了防止使用new关键字来创建一个新的实例，就会变成“多例”并存的情况。\n3. 单例实现方式（懒汉式） 3.1. 实现前置分析 当外界第一次要使用该类的对象时，如果还没有创建出来，则创建该单例对象。懒汉式是线程不安全的，要处理线程安全问题，实际上创建对象要进过如下几个步骤：\n分配内存空间 调用构造器，初始化实例 返回地址给引用 因为创建对象是一个非原子操作，编译器可能会重排序⌈构造函数可能在整个对象初始化完成前执行完毕，即赋值操作（只是在内存中开辟一片存储区域后直接返回内存的引用）在初始化对象前完成⌋。而线程C在线程A赋值完时判断 instance 就不为 null 了，此时线程C拿到的将是一个没有初始化完成的半成品。这样是很危险的。因为极有可能线程C会继续拿着个没有初始化的对象中的数据进行操作，此时容易触发 NPE。\n另外还由于可见性问题，线程A在自己的工作线程内创建了实例，但此时还未同步到主存中；此时线程C在主存中判断 instance 还是 null，那么线程C又将在自己的工作线程中创建一个实例，这样就创建了多个实例。\n3.2. 具体实现 综合以上分析后，推荐使用volatile 双重检查模式来创建单例对象。从性能上考虑，一般选择同步代码块去处理线程安全问题。具体实现步骤如下：\n定义一个静态的对象成员变量 要私有构造方法，如果不处理，系统会自动提供一个无参的构造方法。外界就可以直接 new 对象 定义一个静态的成员方法，用来获取对象成员，一般命名用 getInstance 需要考虑线程安全的问题。 第一种是使用同步方法，增加 synchronized，在方法内再进行判断对象是否为空，如果为空，就直接创建。 第二种是使用同步代码块，一开始先判断对象是否为空，为了性能的问题，为了后面的线程不再需要加锁；同步对象写，LazySingleton.class 保证锁对象被所有对象共享； 完整的示例实现代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.moon.design.singleten; /** * 单例类，懒加载 * 懒汉式：当外界第一次要使用该类的对象时，如果还没有创建出来，则创建该单例对象。 */ public class LazySingleton { // 定义一个静态的对象成员变量。注意：采用 volatile 关键字修饰也是很有必要 private static volatile LazySingleton instance; // 要私有构造方法，如果不处理，系统会自动提供一个无参的构造方法。 private LazySingleton() { } // 定义一个静态的成员方法，用来获取创建好的成员对象(同步代码块) public static LazySingleton getInstance1() { // 判断如果之前对象不存在就进行创建，再加个判断，为了性能的问题，为了后面的线程不再需要加锁 if (instance == null) { // 为了解决线程安全问题，需要同步代码块，LazySingleton.class保证锁对象被所有对象共享 // 如果在 instance==null 前已经有多个线程进来，所以同步方法块中的if判断不能省略 // 在synchronized锁定代码中，需要再次进行是否为null检查。这种方法叫做双重检查锁定（Double-Check Locking）。 synchronized (LazySingleton.class) { if (instance == null) { instance = new LazySingleton(); } } } return instance; } // 定义一个静态的成员方法，用来获取创建好的成员对象(同步方法) public static synchronized LazySingleton getInstance2() { // 判断如果之前对象不存在就进行创建 if (instance == null) { instance = new LazySingleton(); } return instance; } } 3.3. 小结 虽然懒汉模式在资源利用率上有一定优势，但是懒汉模式在高并发场景，也就是多线程场景下，遇到的问题也比较多。为了防止多个线程同时调用getInstance方法，需要在该方法前面增加关键字synchronized进行线程访问锁定。但是这样就引出了新的问题，在高并发场景下，每次调用都进行线程访问锁定判断，会对系统性能产生较大的负面影响。\n上面示例中，为了解决高并发中的线程安全问题，在synchronized锁定代码中，需要再次进行是否为null检查，因为可能在instance == null前已经多个线程进来，如果不做判断，当前一个线程创建对象后，其他线程也可以抢到锁再进入创建对象。这种方法叫做双重检查锁定（Double-Check Locking）。另外，静态的对象成员变量 instance 采用 volatile 关键字修饰也是很有必要，使用 volatile 可以禁止 JVM 的指令重排，并且在第一个获取锁的线程创建实例后，保证其他线程的可见性，从而让程序在多线程环境下正常运行。\n4. 单例实现方式（静态内部类实现式） mybatis框架，在创建VFS类单例实例使用了此种方式。具体参考框架的org.apache.ibatis.io.VFS类\n静态内部类的实例方式，是利用 JVM 加载静态类的特性来实现。JVM 在类初始化阶段（即在Class被加载后，且线程使用之前），会执行类的初始化。在执行类的初始化期间，JVM 会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。因此静态内部类实现式是线程安全的。具体的实现步骤如下：\n定义一个静态的内部类 要私有构造方法，如果不处理，系统会自动提供一个无参的构造方法。外界就可以直接 new 对象 定义一个静态的成员方法，用来获取对象成员，一般命名用 getInstance 在内部类中定义静态的成员变量，类型是外部类类型，初始化时调用外部类的构造函数，创建外部类的实例，是真正的实例变量。 在getInstance的静态方法中，返回内部类的静态成员变量（外部类的实例） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.moon.design.singleten; /** * 单例类实现，静态内部类实现式 */ public class HolderSingleton { private HolderSingleton() { } /* * 当getInstance方法第一次被调用的时候，它第一次读取InstanceHolder.INSTANCE时，会触发InstanceHolder类的初始化。 * 而InstanceHolder类在装载并被初始化的时候，会初始化它的静态成员变量、静态域，从而创建HolderSingleton的实例。 * 由于是静态的域，因此只会在虚拟机装载类的时候初始化一次，并由虚拟机来保证它的线程安全性。 * 这个模式的优势在于，getInstance方法并没有做线程同步控制，并且只是执行一个域的访问，因此延迟初始化并没有增加任何访问成本。 */ public static HolderSingleton getInstance() { return InstanceHolder.INSTANCE; } /* 内部类前面加static关键字，表示的是类级内部类，类级内部类只有在使用时才会被加载 */ private static class InstanceHolder { // 静态变量的初始化是由JVM保证线程安全的，在类的加载时就完成了静态变量的赋值 static final HolderSingleton INSTANCE = new HolderSingleton(); } } 小结：\n上述代码中：虽然内部类中的成员变量INSTANCE是被static修改，但这个是懒加载的，原因是内部类前面加static关键字，表示的是类级内部类，类级内部类只有在使用时才会被加载。\n具体的执行流程是：当getInstance方法第一次被调用的时候，它第一次读取InstanceHolder.INSTANCE时，会触发InstanceHolder类的初始化。而InstanceHolder类在装载并被初始化的时候，会初始化它的静态成员变量、静态域，从而创建HolderSingleton的实例。由于是静态的域，因此只会在虚拟机装载类的时候初始化一次，并静态变量的初始化是由虚拟机（JVM）来保证它的线程安全性，在内存只会存在一份，jvm的初始化时是线程互斥的（待日后理解）。这个模式的优势在于，getInstance方法并没有做线程同步控制，并且只是执行一个域的访问，因此延迟初始化并没有增加任何访问成本。\n通过对比基于 volatile 的双重检查锁定方案和基于类初始化方案的对比，会发现基于类初始化的方案的实现代码更简洁。但是基于 volatile 的双重检查锁定方案有一个额外的优势：除了可以对静态字段实现延迟加载初始化外，还可以对实例字段实现延迟初始化。\n5. 单例实例方式（枚举式） 使用枚举来实现单例模式\n1 2 3 public enum EnumSingleton { instance; } 首先创建 Enum 时，编译器会自动生成一个继承自java.lang.Enum的类，枚举成员声明中被static和final所修饰，虚拟机会保证这些静态成员在多线程环境中被正确的加锁和同步，所以是线程安全的。编译后生成的类代码如下：\n1 2 3 4 class EnumSingleton extends Enum { public static final EnumSingleton instance; ....省略 } Enum的构造方法本身就是private修饰的，所以也防止了使用new关键字创建新实例。从Enum类的声明中也可以看出，Enum是提供了序列化的支持的，在某些需要序列化的场景下，提供了非常大的便利。另一个重要功能就是反序列化仍然可以保证对象在虚拟机范围内是单例的。\n6. 单例实例方式（单例注册表式） 单例注册表来实现单例模式，Spring框架就是最有代表性的使用者。\n其实实现原理很容易理解，以一个HashMap（Spring 是使用了线程安全的ConcurrentHashMap）来存储目前已生成的类的实例，如果可以根据类名找到对象，就返回这个对象，不再创建新对象。如果找不到，就利用反射机制创建一个，并加入到 Map 中。以上只是一个示意代码，作为 Spring 核心理念IoC的重要部分，单例注册表在 Spring 中的源码要复杂的多，也做了很多性能上的优化，具体可以参考 Spring 中AbstractBeanFactory类的源码。\n7. 涉及使用单例类的场景 一个类只要一个对象的时候 视频播放器，音频播放器 数据库的连接 设置信息 8. 其他小问题 开发中一般使用饿汉式，面试的时候使用懒汉式\n享元模式（Flyweight Pattern）（!整理中） 1. 定义与特点 享元模式（Flyweight Pattern）：主要通过对象的复用来减少对象创建的次数和数量，以减少系统内存的使用和降低系统的负载。享元模式属于结构型模式，在系统需要一个对象时，享元模式首先在系统中查找并尝试重用现有的对象，如果未找到匹配的对象，则创建新对象并将其缓存在系统中以便下次使用。\nwikipedia: A flyweight is an object that minimizes memory usage by sharing as much data as possible with other similar objects\n享元模式会把其中共同的部分抽象出来，如果有相同的业务请求，则直接返回内存中已有的对象，避免频繁重复创建和销毁大量的对象，造成系统资源的浪费。\n2. 模式的结构 3. 基础实现 TODO: 待整理！\n模板方法设计模式（Template Method） 1. 定义与特点 模板方法（Template Method）模式的定义：定义一个操作中的算法骨架，而将算法的一些步骤延迟到子类中，使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。\n模式的主要优点如下：\n它封装了不变部分，扩展可变部分。它把认为是不变部分的算法封装到父类中实现，而把可变部分算法由子类继承实现，便于子类继续扩展。 它在父类中提取了公共的部分代码，便于代码复用。 部分方法是由子类实现的，因此子类可以通过扩展方式增加相应的功能，符合开闭原则。 该模式的主要缺点如下：\n对每个不同的实现都需要定义一个子类，这会导致类的个数增加，系统更加庞大，设计也更加抽象，间接地增加了系统实现的复杂度。 父类中的抽象方法由子类实现，子类执行的结果会影响父类的结果，这导致一种反向的控制结构，它提高了代码阅读的难度。 由于继承关系自身的缺点，如果父类添加新的抽象方法，则所有子类都要改一遍。 2. 模式的结构 模板方法模式，需要抽象类与具体子类之间的协作，利用多态来实现。模板方法模式包含以下主要角色：\n2.1. 抽象类/抽象模板（Abstract Class） 抽象模板类，负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。这些方法的定义如下：\n模板方法：定义了算法的骨架，按某种顺序调用其包含的基本方法。 基本方法：是整个算法中的一个步骤，包含以下几种类型。 抽象方法：在抽象类中声明，由具体子类实现。 具体方法：在抽象类中已经实现，在具体子类中可以继承或重写它。 钩子方法：在抽象类中已经实现，包括用于判断的逻辑方法和需要子类重写的空方法两种。 2.2. 具体子类/具体实现（Concrete Class） 具体实现类，实现抽象类中所定义的抽象方法和钩子方法，它们是一个抽象类中模板方法逻辑的其中一个组成步骤。\n2.3. 模板方法模式的结构图 3. 基础实现 定义抽象类，类中定义主要的模板方法与抽象方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public abstract class AbstractClass { // 模板方法。定义了方法逻辑的骨架，按某种顺序调用其包含的基本方法。 public void TemplateMethod() { abstractMethod1(); // 抽象方法 hookMethod1(); if (hookMethod2()) { // 抽象类已实现，但子类可以进行修改 SpecificMethod(); // 具体方法 } abstractMethod2(); // 抽象方法 } // 具体方法 public void SpecificMethod() { System.out.println(\u0026#34;抽象类中的具体方法被调用...\u0026#34;); } // 钩子方法1，方法无处理逻辑，由子类来重写 public void hookMethod1() { } // 钩子方法2 public boolean hookMethod2() { return true; } // 抽象方法1，由实现类来处理 public abstract void abstractMethod1(); // 抽象方法2，由实现类来处理 public abstract void abstractMethod2(); } 创建实现类， 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class ConcreteClass extends AbstractClass { @Override public void abstractMethod1() { System.out.println(\u0026#34;抽象方法1的实现被调用...\u0026#34;); } @Override public void abstractMethod2() { System.out.println(\u0026#34;抽象方法2的实现被调用...\u0026#34;); } @Override public void hookMethod1() { System.out.println(\u0026#34;钩子方法1被重写...\u0026#34;); } @Override public boolean hookMethod2() { // 改变抽象类中原有方法逻辑 return false; } } 测试调用模板方法 1 2 3 4 5 6 7 public class TemplateMethodPattern { public static void main(String[] args) { AbstractClass tm = new ConcreteClass(); tm.TemplateMethod(); } } 输出结果\n1 2 3 抽象方法1的实现被调用... 钩子方法1被重写... 抽象方法2的实现被调用... 适配器模式（Adapter Pattern） 1. 定义与特点 适配器模式（Adapter）的定义：将一个类的接口转换成另外一个接口，使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种，前者类之间的耦合度比后者高，且要求程序员了解现有组件库中的相关组件的内部结构，所以应用相对较少些。\n该模式的主要优点如下： 客户端通过适配器可以透明地调用目标接口。 复用了现存的类，程序员不需要修改原有代码而重用现有的适配者类。 将目标类和适配者类解耦，解决了目标类和适配者类接口不一致的问题。 在很多业务场景中符合开闭原则。 其缺点是： 适配器编写过程需要结合业务场景全面考虑，可能会增加系统的复杂性。 增加代码阅读难度，降低代码可读性，过多使用适配器会使系统代码变得凌乱。 2. 模式结构 类适配器模式可采用多重继承方式实现。Java 不支持多继承，但可以定义一个适配器类来实现当前系统的业务接口，同时又继承现有组件库中已经存在的组件。\n对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中，该类同时实现当前系统的业务接口。\n2.1. 主要角色 适配器模式（Adapter）包含以下主要角色\n目标（Target）接口：当前系统业务所期待的接口，它可以是抽象类或接口。 适配者（Adaptee）类：它是被访问和适配的现存组件库中的组件接口。 适配器（Adapter）类：它是一个转换器，通过继承或引用适配者的对象，把适配者接口转换成目标接口，让客户端按目标接口的格式访问适配者。 2.2. 结构类图 类适配器模式的结构图\n对象适配器模式的结构图\n3. 基础实现 3.1. 准备公共接口与适配器 创建目标接口，最终客户端调用\n1 2 3 4 5 public interface Target { void request(); } 创建需要待适配者（类）\n1 2 3 4 5 6 public class Adaptee { public void specificRequest() { System.out.println(\u0026#34;适配者中的业务代码被调用！\u0026#34;); } } 3.2. 类适配器模式 创建类适配器。继承适配者（类），并实现目标接口。在目标接口中，调用适配者中相应的逻辑。\n1 2 3 4 5 6 7 public class ClassAdapter extends Adaptee implements Target { public void request() { // 调用适配者的方法 super.specificRequest(); } } 测试程序\n1 2 3 4 5 6 7 public static void main(String[] args) { System.out.println(\u0026#34;类适配器模式测试：\u0026#34;); // 创建适配器 Target target = new ClassAdapter(); // 调用目标接口方法，实际是调用了被适配类中相应的方法 target.request(); } 测试结果\n1 2 类适配器模式测试： 适配者中的业务代码被调用！ 3.3. 对象适配器模式 创建对象适配器，只需要实现目标接口。在类中定义适配者类型的属性，通过构造函数或者 setter 方法获取到适配者对象，在调用目标接口方法时，通过适配者对象调用相应的方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 public class ObjectAdapter implements Target { private final Adaptee adaptee; public ObjectAdapter(Adaptee adaptee) { this.adaptee = adaptee; } public void request() { // 通过适配者对象调用 adaptee.specificRequest(); } } 测试程序\n1 2 3 4 5 6 7 public static void main(String[] args) { System.out.println(\u0026#34;对象适配器模式测试：\u0026#34;); Adaptee adaptee = new Adaptee(); Target target = new ObjectAdapter(adaptee); // 调用目标接口方法，实际是调用了被适配者对象的方法 target.request(); } 测试结果\n1 2 对象适配器模式测试： 适配者中的业务代码被调用！ 建造者模式（Builder Pattern）（整理中！） 1. 定义与特点 建造者（Builder）模式的定义：指将一个复杂对象的构造与它的表示分离，使同样的构建过程可以创建不同的表示，这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象，然后一步一步构建而成。它将变与不变相分离，即产品的组成部分是不变的，但每一部分是可以灵活选择的。\n该模式的主要优点如下： 封装性好，构建和表示分离。 扩展性好，各个具体的建造者相互独立，有利于系统的解耦。 客户端不必知道产品内部组成的细节，建造者可以对创建过程逐步细化，而不对其它模块产生任何影响，便于控制细节风险。 其缺点如下： 产品的组成部分必须相同，这限制了其使用范围。 如果产品的内部变化复杂，如果产品内部发生变化，则建造者也要同步修改，后期维护成本较大。 建造者（Builder）模式和工厂模式的关注点不同：建造者模式注重零部件的组装过程，而工厂方法模式更注重零部件的创建过程，但两者可以结合使用。\n2. 结构与实现 建造者（Builder）模式由产品、抽象建造者、具体建造者、指挥者等 4 个要素构成\n工厂模式（Factory Pattern） 1. 定义 在 Java 中，万物皆对象，这些对象都需要创建，如果创建的时候直接 new 该对象，就会对该对象耦合严重，假如要更换对象，所有 new 对象的地方都需要修改一遍，这显然违背了软件设计的开闭原则。如果使用工厂来生产对象，只需要和工厂打交道即可，彻底和对象解耦，如果要更换对象，直接在工厂里更换该对象即可，达到了与对象解耦的目的；所以工厂模式最大的优点就是：解耦。\n开闭原则：对扩展开放，对修改关闭。在程序需要进行拓展的时候，不能去修改原有的代码，实现一个热插拔的效果。简言之，是为了使程序的扩展性好，易于维护和升级。\n三种工厂模式：\n简单工厂模式 工厂方法模式 抽象工厂模式 1.1. 案例需求概述 需求：设计一个咖啡店点餐系统。设计一个咖啡类（Coffee），并定义其两个子类（美式咖啡【AmericanCoffee】和拿铁咖啡【LatteCoffee】）；再设计一个咖啡店类（CoffeeStore），咖啡店具有点咖啡的功能。\n具体类的设计如下：\n类图的元素说明：\n类图中的符号： +：表示 public 权限方法 -：表示 private 权限方法 #：表示 protected 权限方法 泛化关系(继承)用带空心三角箭头的实线来表示 依赖关系使用带箭头的虚线来表示 1.2. 无设计模式实现 定义接口： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public interface Coffee { /** * 获取名字 */ String getName(); /** * 加牛奶 */ void addMilk(); /** * 加糖 */ void addSuqar(); } public class LatteCoffee implements Coffee { /** * 获取名字 */ @Override public String getName() { return \u0026#34;latteCoffee\u0026#34;; } /** * 加牛奶 */ @Override public void addMilk() { System.out.println(\u0026#34;LatteCoffee...addMilk...\u0026#34;); } /** * 加糖 */ @Override public void addSuqar() { System.out.println(\u0026#34;LatteCoffee...addSuqar...\u0026#34;); } } 定义实现类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class AmericanCoffee implements Coffee { /** * 获取名字 */ @Override public String getName() { return \u0026#34;americanCoffee\u0026#34;; } /** * 加牛奶 */ @Override public void addMilk() { System.out.println(\u0026#34;AmericanCoffee...addMilk...\u0026#34;); } /** * 加糖 */ @Override public void addSuqar() { System.out.println(\u0026#34;AmericanCoffee...addSuqar...\u0026#34;); } } 无使用设计模式实现获取不同实现类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class CoffeeStore { public static void main(String[] args) { Coffee coffee = orderCoffee(\u0026#34;latte\u0026#34;); System.out.println(coffee.getName()); } public static Coffee orderCoffee(String type) { Coffee coffee = null; if (\u0026#34;american\u0026#34;.equals(type)) { coffee = new AmericanCoffee(); } else if (\u0026#34;latte\u0026#34;.equals(type)) { coffee = new LatteCoffee(); } // 对生产的对象做其他处理 coffee.addMilk(); coffee.addSuqar(); return coffee; } } 2. 简单工厂模式 简单工厂不是一种设计模式，反而比较像是一种编程习惯。\n2.1. 模式结构 简单工厂包含如下角色：\n抽象产品：定义了产品的规范，描述了产品的主要特性和功能。 具体产品：实现或者继承抽象产品的子类 具体工厂：提供了创建产品的方法，调用者通过该方法来获取产品。 2.2. 具体实现 使用简单工厂模式对上面案例进行改进，类图如下：\n创建静态工厂类 1 2 3 4 5 6 7 8 9 10 11 12 public class SimpleCoffeeFactory { public static Coffee createCoffee(String type) { Coffee coffee = null; if (\u0026#34;american\u0026#34;.equals(type)) { coffee = new AmericanCoffee(); } else if (\u0026#34;latte\u0026#34;.equals(type)) { coffee = new LatteCoffee(); } return coffee; } } Tips: 在开发中，经常会将工厂类中的创建对象的功能定义为静态的，这就是静态工厂模式，它也不属于 23 种设计模式。\n改进使用简单静态工厂模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class CoffeeStore { public static void main(String[] args) { // 使用简单静态工厂模式 Coffee coffee1 = simpleOrderCoffee(\u0026#34;american\u0026#34;); System.out.println(coffee1.getName()); } public static Coffee simpleOrderCoffee(String type) { // 通过工厂获得对象，不需要知道对象实现的细节 Coffee coffee = SimpleCoffeeFactory.createCoffee(type); if (Objects.nonNull(coffee)) { coffee.addMilk(); coffee.addSuqar(); } return coffee; } } 总结：工厂（factory）处理创建对象的细节，一旦有了 SimpleCoffeeFactory，CoffeeStore 类中的 simpleOrderCoffee() 就变成此对象的客户，后期如果需要 Coffee 对象直接从工厂中获取即可。这样也就解除了和 Coffee 实现类的耦合，同时又产生了新的耦合，CoffeeStore 对象和 SimpleCoffeeFactory 工厂对象的耦合，工厂对象和商品对象的耦合。\n后期如果再加新品种的咖啡，势必要需求修改 SimpleCoffeeFactory 的代码，违反了开闭原则。工厂类的客户端可能有很多，比如创建美团外卖等，这样只需要修改工厂类的代码，省去其他的修改操作。\n2.3. 优缺点 优点：封装了创建对象的过程，可以通过参数直接获取对象。把对象的创建和业务逻辑层分开，这样以后就避免了修改客户代码，如果要实现新产品直接修改工厂类，而不需要在原代码中修改，这样就降低了客户代码修改的可能性，更加容易扩展。 缺点：增加新产品时还是需要修改工厂类的代码，违背了“开闭原则”。 3. 工厂方法模式 针对上例中的缺点，使用工厂方法模式就可以完美的解决，完全遵循开闭原则。\n3.1. 概述 工厂方法模式，需要定义一个用于创建对象的接口，让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。\n3.2. 模式结构 工厂方法模式的主要角色：\n抽象工厂（Abstract Factory）：提供了创建产品的接口，调用者通过它访问具体工厂的工厂方法来创建产品。 具体工厂（ConcreteFactory）：主要是实现抽象工厂中的抽象方法，完成具体产品的创建。 抽象产品（Product）：定义了产品的规范，描述了产品的主要特性和功能。 具体产品（ConcreteProduct）：实现了抽象产品角色所定义的接口，由具体工厂来创建，它同具体工厂之间一一对应。 3.3. 具体实现 使用工厂方法模式对上例进行改进，类图如下：\n抽象工厂 1 2 3 4 5 6 public interface CoffeeFactory { /** * 创建咖啡 */ Coffee createCoffee(); } 定义具体的工厂实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class AmericanCoffeeFactory implements CoffeeFactory { /** * 创建美式咖啡 */ @Override public Coffee createCoffee() { return new AmericanCoffee(); } } public class LatteCoffeeFactory implements CoffeeFactory { /** * 创建拿铁咖啡 */ @Override public Coffee createCoffee() { return new LatteCoffee(); } } 改造通过不同的工厂生成相应的对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class CoffeeStore { public static void main(String[] args) { // 使用工厂方法模式 CoffeeStore coffeeStore = new CoffeeStore(new LatteCoffeeFactory()); Coffee coffee2 = coffeeStore.methodOrderCoffee(); System.out.println(coffee2.getName()); } private CoffeeFactory coffeeFactory; public CoffeeStore(CoffeeFactory coffeeFactory){ this.coffeeFactory = coffeeFactory; } public Coffee methodOrderCoffee() { // 可以根据不同的工厂，创建不同的产品 Coffee coffee = coffeeFactory.createCoffee(); if (Objects.nonNull(coffee)) { coffee.addMilk(); coffee.addSuqar(); } return coffee; } } 总结：从以上编写的代码可以看到，工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性，要增加产品类时也要相应地增加工厂类，不需要修改工厂类的代码。工厂方法模式保持了简单工厂模式的优点，而且解决了简单工厂模式的缺点。\n3.4. 优缺点 优点：\n用户只需要知道具体工厂的名称就可得到所要的产品，无须知道产品的具体创建过程； 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类，无须对原工厂进行任何修改，满足开闭原则； 缺点：\n每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类，这增加了系统的复杂度。 抽象工厂模式（Abstract Factory Pattern） 这些工厂只生产同种类产品，同种类产品称为同等级产品，也就是说：工厂方法模式只考虑生产同等级的产品，但是在现实生活中许多工厂是综合型的工厂，能生产多等级（种类） 的产品，如电器厂既生产电视机又生产洗衣机或空调，大学既有软件专业又有生物专业等。\n抽象工厂模式将考虑多等级产品的生产，将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族，下图所示\n产品族：一个品牌下面的所有产品；例如华为下面的电脑、手机称为华为的产品族。 产品等级：多个品牌下面的同种产品；例如华为和小米都有手机电脑为一个产品等级。 1. 概念 抽象工厂模式是工厂方法模式的升级版本，工厂方法模式只生产一个等级的产品，而抽象工厂模式可生产多个等级的产品。该模式是一种为访问类提供一个创建一组相关或相互依赖对象的接口，且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。\n一个超级工厂创建其他工厂，该超级工厂又称为其他工厂的工厂。\n2. 模式结构 抽象工厂模式的主要角色如下：\n抽象工厂（Abstract Factory）：提供了创建产品的接口，它包含多个创建产品的方法，可以创建多个不同等级的产品。 具体工厂（Concrete Factory）：主要是实现抽象工厂中的多个抽象方法，完成具体产品的创建。 抽象产品（Product）：定义了产品的规范，描述了产品的主要特性和功能，抽象工厂模式有多个抽象产品。 具体产品（ConcreteProduct）：实现了抽象产品角色所定义的接口，由具体工厂来创建，它 同具体工厂之间是多对一的关系。 3. 具体实现（暂未整理代码） 现咖啡店业务发生改变，不仅要生产咖啡还要生产甜点\n同一个产品等级（产品分类） 咖啡：拿铁咖啡、美式咖啡 甜点：提拉米苏、抹茶慕斯 同一个风味，就是同一个产品族（相当于同一个品牌） 美式风味：美式咖啡、抹茶慕斯 意大利风味：拿铁咖啡、提拉米苏 如果按照工厂方法模式，需要定义提拉米苏类、抹茶慕斯类、提拉米苏工厂、抹茶慕斯工厂、甜点工厂类，很容易发生类爆炸情况。所以这个案例可以使用抽象工厂模式实现。类图如下：\nTips: 实现关系使用带空心三角箭头的虚线来表示\n整体调用思路：\n4. 优缺点 优点：当一个产品族中的多个对象被设计成一起工作时，它能保证客户端始终只使用同一个产品族中的对象。 缺点：当产品族中需要增加一个新的产品时，所有的工厂类都需要进行修改。 5. 使用场景 当需要创建的对象是一系列相互关联或相互依赖的产品族时，如电器工厂中的电视机、洗衣机、空调等。 系统中有多个产品族，但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。 系统中提供了产品的类库，且所有产品的接口相同，客户端不依赖产品实例的创建细节和内部结构。 如：输入法换皮肤，一整套一起换。生成不同操作系统的程序。\n策略模式（Strategy Pattern） 1. 定义 策略模式定义了一系列算法，并将每个算法封装起来，使它们可以相互替换，且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式，它通过对算法进行封装，把使用算法的责任和算法的实现分割开来，并委派给不同的对象对这些算法进行管理。\n2. 模式结构 策略模式的主要角色如下：\n抽象策略类（Strategy）：这是一个抽象角色，通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。 具体策略类（Concrete Strategy）：实现了抽象策略定义的接口，提供具体的算法实现或行为。 环境类（Context）：持有一个策略类的引用，最终给客户端调用。 3. 基础实现 3.1. 需求说明 案例（促销活动）需求：一家百货公司在定年度的促销活动。针对不同的节日（春节、中秋节、圣诞节）推出不同的促销活动，由促销员将促销活动展示给客户。类图如下：\nTips: 聚合关系可以用带空心菱形的实线来表示\n3.2. 代码实现 定义百货公司所有促销活动的共同接口 1 2 3 public interface Strategy { void show(); } 定义具体策略角色（Concrete Strategy）：每个节日具体的促销活动 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class StrategyA implements Strategy { // 为春节准备的促销活动A @Override public void show() { System.out.println(\u0026#34;买一送一\u0026#34;); } } public class StrategyB implements Strategy { // 为中秋准备的促销活动B @Override public void show() { System.out.println(\u0026#34;满200元减50元\u0026#34;); } } public class StrategyC implements Strategy { // 为圣诞准备的促销活动C @Override public void show() { System.out.println(\u0026#34;满1000元加一元换购任意200元以下商品\u0026#34;); } } 定义环境角色（Context）：用于连接上下文。这里可以理解为销售员，即把促销活动推销给客户。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class SalesMan { // 持有抽象策略角色的引用 private Strategy strategy; public SalesMan(Strategy strategy) { this.strategy = strategy; } // 向客户展示促销活动 public void salesManShow() { strategy.show(); } // 测试 public static void main(String[] args) { SalesMan salesMan = new SalesMan(new StrategyB()); salesMan.salesManShow(); } } 4. 进阶：工厂方法设计模式+策略模式 4.1. 概述 一般网站都会提供有多种方式可以进行登录。如果使用传统方式实现该功能，一般会存在以下问题：\n业务层代码大量使用到了 if\u0026hellip;else，在后期阅读代码的时候会非常不友好，大量使用 if\u0026hellip;else 性能也不高 如果业务发生变更，比如现在新增了QQ登录方式，这个时候需要修改业务层代码，违反了开闭原则 解决方案：使用工厂方法设计模式+策略模式。\n一句话总结：只要代码中有冗长的 if-else 或 switch 分支判断都可以采用策略模式优化。\n4.2. 工厂+策略模式实现 4.2.1. 整体思路 不在 service 中写业务分支逻辑，而去调用工厂，然后通过 service 传递不同的参数来获取不同的登录策略（登录方式）。流程图如下：\n4.2.2. 具体实现 基础请求/响应 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 // 请求参数：LoginReq @Data public class LoginReq { private String name; private String password; private String phone; private String validateCode;//手机验证码 private String wxCode;//用于微信登录 /** * account : 用户名密码登录 * sms : 手机验证码登录 * we_chat : 微信登录 */ private String type; } // 响应参数：LoginResp @Data public class LoginResp{ private Integer userId; private String userName; private String roleCode; private String token; //jwt令牌 private boolean success; } 抽象策略类：UserGranter 1 2 3 4 5 6 7 8 9 10 11 12 public class UserGranter { /** * 获取数据 * * @param loginReq 传入的参数 * 0:账号密码 * 1:短信验证 * 2:微信授权 * @return map值 */ LoginResp login(LoginReq loginReq); } 提供具体的策略：AccountGranter、SmsGranter、WeChatGranter 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Component public class AccountGranter implements UserGranter { @Override public LoginResp login(LoginReq loginReq) { System.out.println(\u0026#34;策略:登录方式为账号登录\u0026#34;); // 执行业务操作 return new LoginResp(); } } @Component public class SmsGranter implements UserGranter { @Override public LoginResp login(LoginReq loginReq) { System.out.println(\u0026#34;策略:登录方式为短信登录\u0026#34;); // 执行业务操作 return new LoginResp(); } } @Component public class WeChatGranter implements UserGranter{ @Override public LoginResp login(LoginReq loginReq) { System.out.println(\u0026#34;策略:登录方式为微信登录\u0026#34;); // 执行业务操作 return new LoginResp(); } } 在 application.yml 文件中新增自定义配置，不同类型 1 2 3 4 5 login: types: account: accountGranter sms: smsGranter we_chat: weChatGranter 新增读取数据配置类 1 2 3 4 5 6 7 @Getter @Setter @Configuration @ConfigurationProperties(prefix = \u0026#34;login\u0026#34;) public class LoginTypeConfig { private Map\u0026lt;String, String\u0026gt; types; } 工厂类 UserLoginFactory，用于操作策略的上下文的环境类，主要初始化时将策略汇总管理，并对外提供获取具体策略的方法。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Component public class UserLoginFactory implements ApplicationContextAware { private static final Map\u0026lt;String, UserGranter\u0026gt; granterPool = new ConcurrentHashMap\u0026lt;\u0026gt;(); @Autowired private LoginTypeConfig loginTypeConfig; /** * 从配置文件中读取策略信息存储到map中 * { account:accountGranter, sms:smsGranter, we_chat:weChatGranter } * * @param applicationContext * @throws BeansException */ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { loginTypeConfig.getTypes().forEach((k, v) -\u0026gt; { granterPool.put(k, (UserGranter) applicationContext.getBean(v)); }); } /** * 对外提供获取具体策略 * * @param grantType 用户的登录方式，需要跟配置文件中匹配 * @return 具体策略 */ public UserGranter getGranter(String grantType) { return granterPool.get(grantType); } } 编写业务类，通过工厂获取不同的策略 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Service public class UserService { @Autowired private UserLoginFactory factory; public LoginResp login(LoginReq loginReq) { UserGranter granter = factory.getGranter(loginReq.getType()); if (granter == null) { LoginResp loginResp = new LoginResp(); loginResp.setSuccess(false); return loginResp; } LoginResp resp = granter.login(loginReq); resp.setSuccess(true); return resp; } } 登陆控制层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RestController @RequestMapping(\u0026#34;/api/user\u0026#34;) @Slf4j public class LoginController { @Autowired private UserService userService; @PostMapping(\u0026#34;/login\u0026#34;) public LoginResp login(@RequestBody LoginReq loginReq) throws InterruptedException { if (loginReq.getType().equals(\u0026#34;abc\u0026#34;)) { log.error(\u0026#34;没有这种登录方式:{}\u0026#34;, loginReq.getType()); } if (loginReq.getType().equals(\u0026#34;123\u0026#34;)) { throw new RuntimeException(\u0026#34;错误的登录方式\u0026#34;); } return userService.login(loginReq); } } 4.2.3. 测试 使用 postman 请求测试\n使用了工厂+策略设计模式之后，业务层的代码不需要使用大量的 if\u0026hellip;else，如果后期有新的需求改动，比如加入了QQ登录，只需要添加对应的策略就可以，无需再改动业务层代码。\n装饰器模式(整理中！) 1. 定义与特点 装饰器（Decorator）模式的定义：指在不改变现有对象结构的情况下，动态地给该对象增加一些职责（即增加其额外功能）的模式，它属于对象结构型模式。\n装饰器模式的主要优点有：\n装饰器是继承的有力补充，比继承灵活，在不改变原有对象的情况下，动态的给一个对象扩展功能，即插即用 通过使用不用装饰类及这些装饰类的排列组合，可以实现不同效果 装饰器模式完全遵守开闭原则 其主要缺点是：装饰器模式会增加许多子类，过度使用会增加程序得复杂性。\n责任链模式（Chain of Responsibility） 1. 定义 责任链（Chain of Responsibility）模式（也叫职责链模式）的定义：为了避免请求发送者与多个请求处理者耦合在一起，于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链；当有请求发生时，可将请求沿着这条链传递，直到有对象处理它为止。\n责任链模式的本质是解耦请求与处理，让请求在处理链中能进行传递与被处理。责任链模式的独到之处是将其节点处理者组合成了链式结构，并允许节点自身决定是否进行请求处理或转发，相当于让请求流动起来。\n责任链模式的应用比较常见的是，springmvc 中的拦截器，web 开发中的 filter 过滤器等。\n2. 优缺点 责任链模式是一种对象行为型模式，其主要优点如下：\n降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构，发送者和接收者也无须拥有对方的明确信息。 增强了系统的可扩展性。可以根据需要增加新的请求处理类，满足开闭原则。 增强了给对象指派职责的灵活性。当工作流程发生变化，可以动态地改变链内的成员或者调动它们的次序，也可动态地新增或者删除责任。 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用，不需保持其他所有处理者的引用，这避免了使用众多的 if 或者 if···else 语句。 责任分担。每个类只需要处理自己该处理的工作，不该处理的传递给下一个对象完成，明确各类的责任范围，符合类的单一职责原则。 其主要缺点如下：\n不能保证每个请求一定被处理。由于一个请求没有明确的接收者，所以不能保证它一定会被处理，该请求可能一直传到链的末端都得不到处理。 对比较长的职责链，请求的处理可能涉及多个处理对象，系统性能将受到一定影响。 职责链建立的合理性要靠客户端来保证，增加了客户端的复杂性，可能会由于职责链的错误设置而导致系统出错，如可能会造成循环调用（死循环）。 3. 模式结构 3.1. 主要角色 职责链模式主要包含以下角色：\n抽象处理者（Handler）角色：定义一个处理请求的接口，包含抽象处理方法和一个后继连接。 具体处理者（Concrete Handler）角色：实现抽象处理者的处理方法，判断能否处理本次请求，如果可以处理请求则处理，否则将该请求转给它的后继者。 客户类（Client）角色：创建处理链，并向链头的具体处理者对象提交请求，它不关心处理细节和请求的传递过程。 3.2. 结构图 4. 基础实现 抽象处理者角色\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public abstract class Handler { private Handler next; public void setNext(Handler next) { this.next = next; } public Handler getNext() { return next; } // 使用模板模式，定义具体处理请求的流程的方法 public final void handleRequest(String request) { // 调用子类实现的处理流程 if (!doHandle(request)) { if (getNext() != null) { getNext().handleRequest(request); } else { System.out.println(\u0026#34;没有人处理该请求！\u0026#34;); } } } // 定义抽象的模板方法，由每个子类具体实现 public abstract boolean doHandle(String request); } 创建具体处理者角色x（其他都可以复制里面的代码用于测试）\n1 2 3 4 5 6 7 8 9 10 11 public class ConcreteHandler1 extends Handler { @Override public boolean doHandle(String request) { System.out.println(\u0026#34;流转至 ConcreteHandler1\u0026#34;); boolean b = request.equals(\u0026#34;one\u0026#34;); if (b) { System.out.println(\u0026#34;具体处理者1负责处理该请求！\u0026#34;); } return b; } } 职责链模式测试\n1 2 3 4 5 6 7 8 9 10 11 12 @Test public void chainOfResponsibilityPatternTest() { // 初始化责任链 Handler handler1 = new ConcreteHandler1(); Handler handler2 = new ConcreteHandler2(); Handler handler3 = new ConcreteHandler3(); handler1.setNext(handler2); handler2.setNext(handler3); // 调用键进行处理 handler1.handleRequest(\u0026#34;three\u0026#34;); } 测试结果：\n1 2 3 4 流转至 ConcreteHandler1 流转至 ConcreteHandler2 流转至 ConcreteHandler3 具体处理者3负责处理该请求！ 5. 进阶案例实战 5.1. 案例需求分析 案例需求：以创建商品为例，假设商品创建逻辑分为以下三步完成：①创建商品、②校验商品参数、③保存商品。\n第②步校验商品又分为多种情况的校验，必填字段校验、规格校验、价格校验、库存校验等等。这些检验逻辑像一个流水线，要想创建出一个商品，必须通过这些校验。\n综上分析，可以使用责任链模式优化：创建商品的每个校验步骤都可以作为一个单独的处理器，抽离为一个单独的类，便于复用。这些处理器形成一条链式调用，请求在处理器链上传递，如果校验条件不通过，则处理器不再向下传递请求，直接返回错误信息；若所有的处理器都通过检验，则执行保存商品步骤。\n5.2. 接口设计分析 UML 图：\nAbstractCheckHandler 为处理器抽象类，负责抽象处理器行为。定义了处理器的抽象方法 handle()，其子类需要重写 handle() 方法以实现特殊的处理器校验逻辑。其有 3 个子类，分别是：\nNullValueCheckHandler：空值校验处理器 PriceCheckHandler：价格校验处理 StockCheckHandler：库存校验处理器 AbstractCheckHandler 抽象类其他属性和方法说明：\nprotected ProductCheckHandlerConfig config 是处理器的动态配置类，使用 protected 声明，每个子类处理器都持有该对象。该对象用于声明当前处理器、以及当前处理器的下一个处理器 nextHandler，另外也可以配置一些特殊属性，比如说接口降级配置、超时时间配置等。 AbstractCheckHandler 类中的 nextHandler 属性是当前处理器持有的下一个处理器的引用，当前处理器执行完毕时，便调用 nextHandler 执行下一处理器的 handle() 校验方法； protected Result next() 是抽象类中定义执行下一个处理器的方法，使用 protected 声明，每个子类处理器都持有该对象。当子类处理器执行完毕(通过)时，调用父类的方法执行下一个处理器 nextHandler。 另外，HandlerClient 类是执行处理器链路的客户端，HandlerClient.executeChain() 方法负责发起整个链路调用，并接收处理器链路的返回值。\n5.3. 代码实现 5.3.1. 产品实体类 定义 ProductVO ，保存创建商品的参数对象，包含商品的基础信息。并且其作为责任链模式中多个处理器的入参，多个处理器都以 roductVO 为入参进行特定的逻辑处理。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 /** * 商品对象 */ @Data @Builder public class ProductVO { /** 商品SKU，唯一 */ private Long skuId; /** 商品名称 */ private String skuName; /** 商品图片路径 */ private String Path; /** 价格 */ private BigDecimal price; /** 库存 */ private Integer stock; } 5.3.2. 抽象类处理器：抽象行为，子类共有属性、方法 创建抽象类处理器 AbstractCheckHandler，并使用 @Component 注解注册为由 Spring 管理的 Bean 对象，方便使用 Spring 来管理各个处理器实现 Bean。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 /** * 抽象类处理器 */ @Component public abstract class AbstractCheckHandler { /** * 当前处理器持有下一个处理器的引用 */ @Getter @Setter protected AbstractCheckHandler nextHandler; /** * 处理器配置 */ @Setter @Getter protected ProductCheckHandlerConfig config; /** * 处理器执行方法 * @param param * @return */ public abstract Result handle(ProductVO param); /** * 链路传递 * @param param * @return */ protected Result next(ProductVO param) { // 下一个链路没有处理器了，直接返回 if (Objects.isNull(nextHandler)) { return Result.success(); } // 执行下一个处理器 return nextHandler.handle(param); } } 抽象类的属性和方法说明如下：\npublic abstract Result handle(ProductVO param)：表示抽象的校验方法，每个处理器都应该继承 AbstractCheckHandler 抽象类处理器，并重写其 handle 方法，各个处理器从而实现特殊的校验逻辑，实际上就是多态的思想。 protected ProductCheckHandlerConfig config：表示每个处理器的动态配置类，可以通过“配置中心”动态修改该配置，实现处理器的“动态编排”和“顺序控制”。配置类中可以配置处理器的名称、下一个处理器、以及处理器是否降级等属性。 protected AbstractCheckHandler nextHandler：表示当前处理器持有下一个处理器的引用，如果当前处理器 handle() 校验方法执行完毕，则执行下一个处理器 nextHandler 的 handle() 校验方法执行校验逻辑。 protected Result next(ProductVO param)：此方法用于处理器链路传递，子类处理器执行完毕后，调用父类的 next() 方法执行在 config 配置的链路上的下一个处理器，如果所有处理器都执行完毕了，就返回结果了。 创建 ProductCheckHandlerConfig 配置类\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /** * 处理器配置类 */ @AllArgsConstructor @Data public class ProductCheckHandlerConfig { /** * 处理器Bean名称 */ private String handler; /** * 下一个处理器 */ private ProductCheckHandlerConfig next; /** * 是否降级 */ private Boolean down = Boolean.FALSE; } 5.3.3. 子类处理器：处理特有的校验逻辑 创建3个子类处理器，各个处理器继承 AbstractCheckHandler 抽象类处理器，并重写其 handle() 处理方法以实现特有的校验逻辑。\nNullValueCheckHandler：空值校验处理器。针对性校验创建商品中必填的参数。如果校验未通过，则返回错误码 ErrorCode，责任链在此截断(停止)，创建商品返回被校验住的错误信息。*注意代码中的降级配置！*使用 @Component 注册到 Spring 容器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 /** * 空值校验处理器 */ @Component public class NullValueCheckHandler extends AbstractCheckHandler { @Override public Result handle(ProductVO param) { System.out.println(\u0026#34;空值校验 Handler 开始...\u0026#34;); // 降级：如果配置了降级，则跳过此处理器，执行下一个处理器 if (super.getConfig().getDown()) { System.out.println(\u0026#34;空值校验 Handler 已降级，跳过空值校验 Handler...\u0026#34;); return super.next(param); } // 参数必填校验 if (Objects.isNull(param)) { return Result.failure(ErrorCode.PARAM_NULL_ERROR); } // SkuId 商品主键参数必填校验 if (Objects.isNull(param.getSkuId())) { return Result.failure(ErrorCode.PARAM_SKU_NULL_ERROR); } // Price 价格参数必填校验 if (Objects.isNull(param.getPrice())) { return Result.failure(ErrorCode.PARAM_PRICE_NULL_ERROR); } // Stock 库存参数必填校验 if (Objects.isNull(param.getStock())) { return Result.failure(ErrorCode.PARAM_STOCK_NULL_ERROR); } System.out.println(\u0026#34;空值校验 Handler 通过...\u0026#34;); // 执行下一个处理器 return super.next(param); } } Notes: super.getConfig().getDown() 是获取 AbstractCheckHandler 处理器对象中保存的配置信息，如果处理器配置了降级，则跳过该处理器，直接调用 super.next() 执行下一个处理器逻辑。\nPriceCheckHandler：价格校验处理。针对创建商品的价格参数进行校验。这里只是做了简单的判断价格大于0的校验，实际业务中比较复杂，比如“价格门”这些防范措施等。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /** * 价格校验处理器 */ @Component public class PriceCheckHandler extends AbstractCheckHandler { @Override public Result handle(ProductVO param) { System.out.println(\u0026#34;价格校验 Handler 开始...\u0026#34;); // 非法价格校验 boolean illegalPrice = param.getPrice().compareTo(BigDecimal.ZERO) \u0026lt;= 0; if (illegalPrice) { return Result.failure(ErrorCode.PARAM_PRICE_ILLEGAL_ERROR); } // 其他校验逻辑... System.out.println(\u0026#34;价格校验 Handler 通过...\u0026#34;); // 执行下一个处理器 return super.next(param); } } StockCheckHandler：库存校验处理器。针对创建商品的库存参数进行校验。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Component public class StockCheckHandler extends AbstractCheckHandler { @Override public Result handle(ProductVO param) { System.out.println(\u0026#34;库存校验 Handler 开始...\u0026#34;); // 非法库存校验 boolean illegalStock = param.getStock() \u0026lt; 0; if (illegalStock) { return Result.failure(ErrorCode.PARAM_STOCK_ILLEGAL_ERROR); } // 其他校验逻辑.. System.out.println(\u0026#34;库存校验 Handler 通过...\u0026#34;); // 执行下一个处理器 return super.next(param); } } 5.3.4. 客户端：执行处理器链路 HandlerClient 客户端类负责发起整个处理器链路的执行，通过 executeChain() 方法。如果处理器链路返回错误信息，即校验未通过，则整个链路截断（停止），返回相应的错误信息。\n1 2 3 4 5 6 7 8 9 10 11 12 public class HandlerClient { public static Result executeChain(AbstractCheckHandler handler, ProductVO param) { //执行处理器 Result handlerResult = handler.handle(param); if (!handlerResult.isSuccess()) { System.out.println(\u0026#34;HandlerClient 责任链执行失败返回：\u0026#34; + handlerResult.toString()); return handlerResult; } return Result.success(); } } 5.3.5. 责任链模式参数校验 paramCheck 方法步骤说明 创建参数校验 paramCheck() 方法使用责任链模式进行参数校验，方法内没有声明具体都有哪些校验，具体有哪些参数校验逻辑是通过多个处理器链传递的。如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private Result paramCheck(ProductVO param) { // 获取处理器配置：通常配置使用统一配置中心存储，支持动态变更 ProductCheckHandlerConfig handlerConfig = this.getHandlerConfigFile(); // 获取处理器 AbstractCheckHandler handler = this.getHandler(handlerConfig); // 通过客户端，执行处理器链路 Result executeChainResult = HandlerClient.executeChain(handler, param); if (!executeChainResult.isSuccess()) { System.out.println(\u0026#34;创建商品 失败...\u0026#34;); return executeChainResult; } // 处理器链路全部成功 return Result.success(); } 5.3.5.1. 步骤1：获取处理器配置 通过 getHandlerConfigFile() 方法获取处理器配置类对象，配置类保存了链上各个处理器的上下级节点配置，支持流程编排、动态扩展。通常配置是通过Ducc(京东自研的配置中心)、Nacos(阿里开源的配置中心)等配置中心存储的，支持动态变更、实时生效。\n1 2 3 4 5 6 7 8 9 10 /** * 获取处理器配置：通常配置使用统一配置中心存储，支持动态变更 */ private ProductCheckHandlerConfig getHandlerConfigFile() { // 模拟配置中心存储的配置 String configJson = \u0026#34;{\\\u0026#34;handler\\\u0026#34;:\\\u0026#34;nullValueCheckHandler\\\u0026#34;,\\\u0026#34;down\\\u0026#34;:true,\\\u0026#34;next\\\u0026#34;:{\\\u0026#34;handler\\\u0026#34;:\\\u0026#34;priceCheckHandler\\\u0026#34;,\\\u0026#34;next\\\u0026#34;:{\\\u0026#34;handler\\\u0026#34;:\\\u0026#34;stockCheckHandler\\\u0026#34;,\\\u0026#34;next\\\u0026#34;:null}}}\u0026#34;; // 转成Config对象 ProductCheckHandlerConfig handlerConfig = JSON.parseObject(configJson, ProductCheckHandlerConfig.class); return handlerConfig; } 示例没有使用配置中心存储处理器链路的配置，而是使用 JSON 串的形式去模拟配置。\nConfigJson 存储的处理器链路配置 JSON 串，使用 json.cn 等格式化如下，配置的整个调用链路规则特别清晰\ngetHandlerConfigFile() 获到配置类方法，只是把在配置中心储存的配置规则，转换成配置类 ProductCheckHandlerConfig 对象，用于程序处理。结构如下：\nNotes: 此时配置类中存储的仅仅是处理器 Spring Bean 的 name 而已，并非实际处理器对象。\n5.3.5.1.1. 步骤2-1：配置检查 首先会进行了配置的一些检查操作。如果配置错误，则获取不到对应的处理器。handlerMap.get(config.getHandler()) 是从所有处理器映射 Map 中获取到对应的处理器 Spring Bean。\nTips: handlerMap 存储了所有的处理器映射，是通过 Spring 的 @Resource 注解注入。注入的规则是：所有继承了 AbstractCheckHandler 抽象类（它是 Spring 管理的 Bean）的子类（子类也是 Spring 管理的 Bean）都会注入进来。\n注入进来的 handlerMap 中 Key 对应 Bean 的 name，Value 是对应的 Bean 实例，也就是实际的处理器，这里指空值校验处理器、价格校验处理器、库存校验处理器。\n5.3.5.1.2. 步骤2-2：保存处理器规则 将配置规则保存到对应的处理器中 abstractCheckHandler.setConfig(config)，子类处理器就持有了配置的规则。\n5.3.5.1.3. 步骤2-3：递归设置处理器链路 1 abstractCheckHandler.setNextHandler(this.getHandler(config.getNext())); 以上方法是递归设置链路上的处理器，结合 ConfigJson 配置的规则来看：\n由上而下，NullValueCheckHandler 空值校验处理器通过 setNextHandler() 方法法设置自己持有的下一节点的处理器，也就是价格处理器 PriceCheckHandler。 接着，PriceCheckHandler 价格处理器，同样需要经过步骤2-1配置检查、步骤2-2保存配置规则，并且最重要的是，它也需要设置下一节点的处理器 StockCheckHandler 库存校验处理器。 最后 StockCheckHandler 库存校验处理器也一样，同样需要经过步骤2-1配置检查、步骤2-2保存配置规则。值得注意的是，StockCheckHandler 的 next 规则配置了 null，这表示它下面没有任何处理器要执行了，它就是整个链路上的最后一个处理节点。 通过递归调用 getHandler() 获取处理器方法，就将整个处理器链路对象串联起来了。如下：\n实际上，getHandler() 获取处理器对象的代码就是把在配置中心配置的规则 ConfigJson，转换成配置类 ProductCheckHandlerConfig 对象，再根据配置类对象，转换成实际的处理器对象，这个处理器对象持有整个链路的调用顺序。\nTips: 使用递归一定要注意截断递归的条件处理，否则可能造成死循环！\n5.3.5.2. 步骤2：根据配置获取处理器 上面步骤1通过 getHandlerConfigFile() 方法获取到处理器链路配置规则后，再调用 getHandler() 获取处理器。\ngetHandler() 方法参数是如上 ConfigJson 配置的规则，即步骤1转换成的 ProductCheckHandlerConfig 对象；根据 ProductCheckHandlerConfig 配置规则转换成处理器链路对象。代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 /** * 使用 Spring 注入:所有继承了AbstractCheckHandler抽象类的Spring Bean都会注入进来。Map的Key对应Bean的name,Value是name对应相应的Bean */ @Resource private Map\u0026lt;String, AbstractCheckHandler\u0026gt; handlerMap; /** * 获取处理器 * * @param config * @return */ private AbstractCheckHandler getHandler(ProductCheckHandlerConfig config) { // 配置检查：没有配置处理器链路，则不执行校验逻辑 if (Objects.isNull(config)) { return null; } // 配置错误 String handler = config.getHandler(); if (StringUtils.isBlank(handler)) { return null; } // 配置了不存在的处理器 AbstractCheckHandler abstractCheckHandler = handlerMap.get(config.getHandler()); if (Objects.isNull(abstractCheckHandler)) { return null; } // 处理器设置配置 Config abstractCheckHandler.setConfig(config); // 递归设置链路处理器 abstractCheckHandler.setNextHandler(this.getHandler(config.getNext())); return abstractCheckHandler; } 5.3.5.3. 步骤3：客户端执行调用链路 getHandler() 获取完处理器后，整个调用链路的执行顺序也就确定了。最后通过 HandlerClient 客户端类的 executeChain(handler, param) 方法执行处理器整个调用链路，并接收处理器链路的返回值。\nexecuteChain() 通过 AbstractCheckHandler.handle() 触发整个链路处理器顺序执行，如果某个处理器校验没有通过 !handlerResult.isSuccess()，则返回错误信息；所有处理器都校验通过，则返回正确信息 Result.success()。\n5.4. 测试 最后创建 createProduct() 测试用例方法，创建商品方法抽象为2个步骤：①参数校验、②创建商品。参数校验使用责任链模式进行校验，包含：空值校验、价格校验、库存校验等等，只有链上的所有处理器均校验通过，才调用 saveProduct() 创建商品方法；否则返回校验错误信息。\n在 createProduct() 创建商品方法中，通过责任链模式，将校验逻辑进行解耦。createProduct() 创建商品方法中不需要关注都要经过哪些校验处理器，以及校验处理器的细节。\n1 2 3 4 5 6 7 8 9 10 11 @Test public Result createProduct(ProductVO param) { // 参数校验，使用责任链模式 Result paramCheckResult = this.paramCheck(param); if (!paramCheckResult.isSuccess()) { return paramCheckResult; } // 创建商品 return this.saveProduct(param); } 场景1：创建商品参数中有空值（如下skuId参数为null），链路被空值处理器截断，返回错误信息\n1 2 3 4 5 ProductVO param = ProductVO.builder() .skuId(null).skuName(\u0026#34;华为手机\u0026#34;).Path(\u0026#34;http://...\u0026#34;) .price(new BigDecimal(1)) .stock(1) .build(); 测试结果\n场景2：创建商品价格参数异常（如下price参数），被价格处理器截断，返回错误信息\n1 2 3 4 5 ProductVO param = ProductVO.builder() .skuId(1L).skuName(\u0026#34;华为手机\u0026#34;).Path(\u0026#34;http://...\u0026#34;) .price(new BigDecimal(-999)) .stock(1) .build(); 测试结果\n场景 3：创建商品库存参数异常（如下stock参数），被库存处理器截断，返回错误信息。\n1 2 3 4 5 ProductVO param = ProductVO.builder() .skuId(1L).skuName(\u0026#34;华为手机\u0026#34;).Path(\u0026#34;http://...\u0026#34;) .price(new BigDecimal(1)) .stock(-999) .build(); 测试结果\n场景4：创建商品所有处理器校验通过，保存商品。\n1 2 3 4 ProductVO param = ProductVO.builder() .skuId(1L).skuName(\u0026#34;华为手机\u0026#34;).Path(\u0026#34;http://...\u0026#34;) .price(new BigDecimal(999)) .stock(1).build(); 测试结果\n门面模式（整理中！） 在软件开发领域有这样一句话：计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决。而门面模式就是对于这句话的典型实践。\n门面模式（Facade Pattern），也称之为外观模式，其核心为：外部与一个子系统的通信必须通过一个统一的外观对象进行，使得子系统更易于使用。\n其他 1. 网络参考资料 设计模式内容聚合 Java设计模式：23种设计模式全面解析（超级详细），内容被清空了？ Java Design Patterns (中文)，软件设计模式，编程原则还有代码片段 ","permalink":"https://ktzxy.top/posts/2bwk68tkfk/","summary":"Java扩展 设计模式","title":"Java扩展 设计模式"},{"content":"webpack 笔记 webpack 官网文档：https://www.pngackjs.com/concepts/\n1. 前端工程化 模块化（js 的模块化、css 的模块化、资源的模块化） 组件化（复用现有的 UI 结构、样式、行为） 规范化（目录结构的划分、编码规范化、接口规范化、文档规范化、 Git 分支管理） 自动化（自动化构建、自动部署、自动化测试） 1.1. 什么是前端工程化 前端工程化指的是：在企业级的前端项目开发中，把前端开发所需的工具、技术、流程、经验等进行规范化、标准化。 工程化的好处：前端开发自成体系，有一套标准的开发方案和流程。 企业中的 Vue 项目和 React 项目，都是基于工程化的方式进行开发的。\n1.2. 前端工程化的解决方案 早期的前端工程化解决方案：\ngrunt，官网：https://www.gruntjs.net/ gulp，官网：https://www.gulpjs.com.cn/ 目前主流的前端工程化解决方案：\nwebpack，官网：https://www.pngackjs.com/ parcel，官网：https://zh.parceljs.org/ 2. webpack介绍 2.1. 什么是 webpack 概念：Webpack 是一个前端资源的打包工具，它可以将js、image、css等资源当成一个模块进行打包。是前端项目工程化的具体解决方案。\n主要功能：它提供了友好的前端模块化开发支持，以及代码压缩混淆、处理浏览器端 JavaScript 的兼容性、性能优化等强大的功能。\n从图中可以看出，Webpack 可以将js、css、png等多种静态资源 进行打包，\n2.2. webpack 优缺点 webpack 的好处\n模块化开发 程序员在开发时可以分模块创建不同的js、 css等小文件方便开发，最后使用webpack将这些小文件打包成一个文件，减少了http的请求次数。\nwebpack可以实现按需打包，为了避免出现打包文件过大可以打包成多个文件。\n编译typescript、ES6等高级js语法 随着前端技术的强大，开发中可以使用javascript的很多高级版本，比如：typescript、ES6等，方便开发，webpack可以将打包文件转换成浏览器可识别的js语法。\nCSS预编译 webpack允许在开发中使用Sass 和 Less等原生CSS的扩展技术，通过sass-loader、less-loader将Sass 和 Less的语法编译成浏览器可识别的css语法。\nwebpack 的缺点： 配置有些繁琐 文档不丰富 注意：目前 Vue，React 等前端项目，基本上都是基于 webpack 进行工程化开发的。\n3. webpack 基础使用 3.1. 前提条件 在开始之前，请确保安装了 Node.js 的最新版本。使用 Node.js 最新的长期支持版本(LTS - Long Term Support)，是理想的起步。使用旧版本，你可能遇到各种问题，因为它们可能缺少 webpack 功能以及/或者缺少相关 package 包。\n3.2. 安装 Node.js webpack基于node.js运行，首先需要安装node.js\n简单介绍node.js？\n传统意义上的 JavaScript 运行在浏览器上，Chrome 使用的 JavaScript 引擎是 V8，Node.js 是一个运行在服务端的框架，它的底层就使用了 V8 引擎，这样就可以使用javascript去编写一些服务端的程序，这样也就实现了用javaScript去开发 Apache + PHP 以及 Java Servlet所开发的服务端功能，这样做的好处就是前端和后端都采用javascript，即开发一份js程序即可以运行在前端也可以运行的服务端，这样比一个应用使用多种语言在开发效率上要高，不过node.js属于新兴产品，一些公司也在尝试使用node.js完成一些业务领域，node.js基于V8引擎，基于事件驱动机制，在特定领域性能出色，比如用node.js实现消息推送、状态监控等的业务功能非常合适。\n下载对应系统的Node.js版本 下载网址：https://nodejs.org/en/download/。一般下载LTS版本，就是最稳定版本\n选安装目录进行安装 选择安装目录，安装完成检查PATH环境变量是否设置了node.js的路径。\n在命令提示符下输入命令测试是否安装成功 1 node -v 会显示当前node的版本\n3.3. 安装 NPM npm全称Node Package Manager，是node包管理和分发的工具，使用NPM可以对应用的依赖进行管理，NPM 的功能和服务端项目构建工具maven差不多，通过 npm 可以很方便地下载js库，打包js文件。\nnode.js已经集成了npm工具，在命令提示符输入 npm -v 可查看当前npm版本\n3.3.1. 删除模块包 删除与安装相对应，也分为全局删除和本地删除\n3.3.1.1. 全局删除 1 npm uninsatll \u0026lt;package-name\u0026gt; -g 3.3.1.2. 本地删除 对应的，本地删除也需要考虑是否再删除模块包的同时删除项目package.json中对应的信息，因此，利用npm本地删除模块包的命令也是三种，分别为：\n1 2 3 4 5 6 7 8 npm uninstall \u0026lt;package-name\u0026gt; # 删除模块包，对应模块包的信息不会从项目package.json文件中删除； npm uninstall \u0026lt;package-name\u0026gt; --save # 删除模块包，并且将对应的模块包信息从项目package.json的dependencies对象中删除； npm uninstall \u0026lt;package-name\u0026gt; --save-dev # 删除模块包，并且将对应的模块包信息从项目package.json的devDependencies对象中删除； 3.3.2. 安装 cnpm（可选） 3.3.2.1. 连网环境安装 cnpm npm默认会去国外的镜像去下载js包，在开发中通常使用国内镜像，这里使用淘宝镜像。\n使用npm下载资源会很慢，所以可以安装一个cnmp(淘宝镜像)来加快下载速度。\n输入命令，进行全局安装淘宝镜像。 1 npm install -g cnpm --registry=https://registry.npm.taobao.org 安装后，可以使用以下命令来查看cnpm的版本 1 cnpm -v 输入nrm ls命令，查看镜像是否已经指向taobao。注：带“*”号的为目前使用的镜像 注：如果输入nrm ls提示无此命令，就是是因为nrm没有设置为全局变量，所以需要进入到nrm下载的路径下执行\n使 nrm use XXX 切换镜像。如果nrm没有安装则需要进行全局安装 输入命令 npm install -g nrm 如果已经安装了cnpm，可以使用 cnpm install -g nrm 3.3.2.2. 非连网环境安装 cnpm 从本小节第3步开始就需要连网下载npm包，如果环境不能连网在课程的资料文件下有已经下载好的webpack相关包，下边是安装方法。\n配置环境变量 1 2 NODE_HOME = D:\\Program Files\\nodejs (node.js安装目录) 在PATH变量中添加：%NODE_HOME%;%NODE_HOME%\\npm_modules; 找到npm包路径 根据上边的安装说明npm包路径被设置到了node.js安装目录下的npm_modules目录。\n可以使用npm config ls查看。\n拷贝课程资料中的 npm_modules.zip到node.js安装目录，并解压npm_modules.zip覆盖本目录下的npm_modules文件夹。\n完成上边步骤测试 1 cnpm -v 3.4. 安装 webpack 3.4.1. 本地安装 ，安装 webpack 最新版本或特定版本。如果使用 webpack 4+ 版本，还需要安装 CLI。\n1 2 3 4 5 6 # 安装最新版本 npm install --save-dev webpack # 安装特定版本 npm install --save-dev webpack@\u0026lt;version\u0026gt; # webpack 4+ 版本，还需要安装 CLI npm install --save-dev webpack-cli 示例：进行项目目录位置，在终端运行如下的命令，安装 webpack 相关的两个包\n1 npm install webpack@5.42.1 webpack-cli@4.7.2 -D 注：上例命令中的 -D 相当于 --save-dev\n对于大多数项目，建议本地安装。这可以使在引入破坏式变更(breaking change)的依赖时，更容易分别升级项目。通常，webpack 通过运行一个或多个 npm scripts，会在本地 node_modules 目录中查找安装的 webpack，如下例：\n1 2 3 \u0026#34;scripts\u0026#34;: { \u0026#34;start\u0026#34;: \u0026#34;webpack --config webpack.config.js\u0026#34; } 当在本地安装 webpack 后，能够从 node_modules/.bin/webpack 访问它的 bin 版本。\n3.4.2. 全局安装(不推荐) 以下的 NPM 安装方式，将使 webpack 在全局环境下可用：\n1 npm install --global webpack 不推荐全局安装 webpack。这会将项目中的 webpack 锁定到指定版本，并且在使用不同的 webpack 版本的项目中，可能会导致构建失败。\n3.5. 配置与启动 webpack 3.5.1. webpack.config.js 文件的作用 webpack.config.js 是 webpack 的配置文件。webpack 在真正开始打包构建之前，会先读取这个配置文件，从而基于给定的配置，对项目进行打包。\n注意：由于 webpack 是基于 node.js 开发出来的打包工具，因此在它的配置文件中，支持使用 node.js 相关的语法和模块进行 webpack 的个性化配置。\n在项目根目录中，创建名为 webpack.config.js 的 webpack 配置文件，并初始化如下的基本配置 1 2 3 4 5 6 7 // 使用 Node.js 中的导出语法，向外导出一个 webpack 的配置对象 module.exports = { // mode 代表 webpack 运行的模式，可选值有两个 development 和 production // 结论：开发时候一定要用 development，因为追求的是打包的速度，而不是体积； // 反过来，发布上线的时候一定能要用 production，因为上线追求的是体积小，而不是打包速度快！ mode: \u0026#39;development\u0026#39;, } 注意：凡是修改了 webpack.config.js 配置文件，或修改了 package.json 配置文件，必须重启实时打包的服务器，否则最新的配置文件无法生效！\n3.5.2. 启动 在 package.json 的 scripts 节点下，新增 dev 脚本如下： 1 2 3 \u0026#34;scripts\u0026#34;: { \u0026#34;dev\u0026#34;: \u0026#34;webpack\u0026#34;, // script 节点下的脚本，可以通过 npm run 执行，例如：npm run dev }, 在终端中运行 npm run dev 命令，启动 webpack 进行项目的打包构建 4. webpack 的基本概念 从 webpack v4.0.0 开始，可以不用引入一个配置文件。然而，webpack 仍然还是高度可配置的。相应的配置包含四个核心概念：\n入口(entry) 输出(output) loader 插件(plugins) 4.1. 入口(entry) 入口起点(entry point)指示 webpack 应该使用哪个模块，来作为构建其内部依赖图的开始。进入入口起点后，webpack 会找出有哪些模块和库是入口起点（直接和间接）依赖的。\n可以通过在 webpack 配置中配置 entry 属性，来指定一个入口起点（或多个入口起点）。默认值为 ./src。\n1 2 3 module.exports = { entry: \u0026#39;./path/to/my/entry/file.js\u0026#39; }; 4.2. 出口(output) output 属性告诉 webpack 在哪里输出它所创建的 bundles，以及如何命名这些文件，默认值为 ./dist。基本上，整个应用程序结构，都会被编译到你指定的输出路径的文件夹中。你可以通过在配置中指定一个 output 字段，来配置这些处理过程：\n1 2 3 4 5 6 7 8 9 const path = require(\u0026#39;path\u0026#39;); module.exports = { entry: \u0026#39;./path/to/my/entry/file.js\u0026#39;, output: { path: path.resolve(__dirname, \u0026#39;dist\u0026#39;), filename: \u0026#39;my-first-webpack.bundle.js\u0026#39; } }; 上例中，通过 output.filename 和 output.path 属性，来告诉 webpack bundle 的名称，以及想要 bundle 生成(emit)到哪里。\n4.3. loader loader 让 webpack 能够去处理那些非 JavaScript 文件（webpack 自身只理解 JavaScript）。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块，然后就可以利用 webpack 的打包能力，对它们进行处理。\n注意，loader 能够 import 导入任何类型的模块（例如 .css 文件），这是 webpack 特有的功能，其他打包程序或任务执行器的可能并不支持。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 const path = require(\u0026#39;path\u0026#39;); const config = { output: { filename: \u0026#39;my-first-webpack.bundle.js\u0026#39; }, module: { rules: [ { test: /\\.txt$/, use: \u0026#39;raw-loader\u0026#39; } ] } }; module.exports = config; 在 webpack 的配置中 loader 有两个目标：\ntest 属性，用于标识出应该被对应的 loader 进行转换的某个或某些文件。 use 属性，表示进行转换时，应该使用哪个 loader。 4.4. 插件(plugins) loader 被用于转换某些类型的模块，而插件则可以用于执行范围更广的任务。插件的范围包括，从打包优化和压缩，一直到重新定义环境中的变量。插件接口功能极其强大，可以用来处理各种各样的任务。\n想要使用一个插件，只需要 require() 它，然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。也可以在一个配置文件中因为不同目的而多次使用同一个插件，这时需要通过使用 new 操作符来创建它的一个实例。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const HtmlWebpackPlugin = require(\u0026#39;html-webpack-plugin\u0026#39;); // 通过 npm 安装 const webpack = require(\u0026#39;webpack\u0026#39;); // 用于访问内置插件 const config = { module: { rules: [ { test: /\\.txt$/, use: \u0026#39;raw-loader\u0026#39; } ] }, plugins: [ new HtmlWebpackPlugin({template: \u0026#39;./src/index.html\u0026#39;}) ] }; module.exports = config; 4.5. 模式(mode) 通过选择 development 或 production 之中的一个，来设置 mode 参数，可以启用相应模式下的 webpack 内置的优化\n1 2 3 module.exports = { mode: \u0026#39;production\u0026#39; }; 4.6. webpack 中的默认约定 在 webpack 4.x 和 5.x 的版本中，有如下的默认约定：\n默认的打包入口文件为 src -\u0026gt; index.js 默认的输出文件路径为 dist -\u0026gt; main.js 注意：可以在 webpack.config.js 中修改打包的默认约定\n5. 入口起点(entry points) 在 webpack 配置中有多种方式定义 entry 属性\n5.1. 配置语法 5.1.1. 单个入口（简写）语法 用法：entry: string|Array\u0026lt;string\u0026gt;\n1 2 3 4 5 6 // webpack.config.js const config = { entry: \u0026#39;./path/to/my/entry/file.js\u0026#39; }; module.exports = config; entry 属性的单个入口语法，是下面的简写：\n1 2 3 4 5 const config = { entry: { main: \u0026#39;./path/to/my/entry/file.js\u0026#39; } }; 当向 entry 属性传入「文件路径(file path)数组」将创建“多个主入口(multi-main entry)”。在想要多个依赖文件一起注入，并且将它们的依赖导向(graph)到一个“chunk”时，传入数组的方式就很有用。\n5.1.2. 对象语法 用法：entry: {[entryChunkName: string]: string|Array\u0026lt;string\u0026gt;}\n1 2 3 4 5 6 7 // webpack.config.js const config = { entry: { app: \u0026#39;./src/app.js\u0026#39;, vendors: \u0026#39;./src/vendors.js\u0026#39; } }; 此配置方式应用程序中定义入口，是最可扩展的方式\n“可扩展的 webpack 配置”是指，可重用并且可以与其他配置组合使用。这是一种流行的技术，用于将关注点(concern)从环境(environment)、构建目标(build target)、运行时(runtime)中分离。然后使用专门的工具（如 webpack-merge）将它们合并。\n5.2. 常见场景 5.2.1. 分离 应用程序(app) 和 第三方库(vendor) 入口 1 2 3 4 5 6 7 // webpack.config.js const config = { entry: { app: \u0026#39;./src/app.js\u0026#39;, vendors: \u0026#39;./src/vendors.js\u0026#39; } }; 以上配置表示 webpack 从 app.js 和 vendors.js 开始创建依赖图(dependency graph)。这些依赖图是彼此完全分离、互相独立的（每个 bundle 中都有一个 webpack 引导(bootstrap)）。这种方式比较常见于，只有一个入口起点（不包括 vendor）的单页应用程序(single page application)中。\n5.2.2. 多页面应用程序 1 2 3 4 5 6 7 8 // webpack.config.js const config = { entry: { pageOne: \u0026#39;./src/pageOne/index.js\u0026#39;, pageTwo: \u0026#39;./src/pageTwo/index.js\u0026#39;, pageThree: \u0026#39;./src/pageThree/index.js\u0026#39; } }; 以上示例表示 webpack 需要 3 个独立分离的依赖图。在多页应用中，（译注：每当页面跳转时）服务器将为你获取一个新的 HTML 文档。页面重新加载新文档，并且资源被重新下载。\n5.2.3. 自定义打包的入口示例 在 webpack.config.js 配置文件中，通过 entry 节点指定打包的入口。\n1 2 3 4 5 6 7 const path = require(\u0026#39;path\u0026#39;) // 导入 node.js 中专门操作路径的模块 // 使用 Node.js 中的导出语法，向外导出一个 webpack 的配置对象 module.exports = { // entry: \u0026#39;指定要处理哪个文件\u0026#39;。打包入口文件的路径 entry: path.join(__dirname, \u0026#39;./src/index1.js\u0026#39;), } 6. 输出(output) 配置 output 选项可以控制 webpack 如何向硬盘写入编译文件。注意，即使可以存在多个入口起点，但只指定一个输出配置。\n6.1. 配置用法 在 webpack 中配置 output 属性的最低要求是，将它的值设置为一个对象，包括以下两点：\nfilename：用于输出文件的文件名。 path：目标输出目录的绝对路径。 1 2 3 4 5 6 7 8 9 // webpack.config.js const config = { output: { filename: \u0026#39;bundle.js\u0026#39;, path: \u0026#39;/home/proj/public/assets\u0026#39; } }; module.exports = config; 此配置将一个单独的 bundle.js 文件输出到 /home/proj/public/assets 目录中。\n6.2. 多个入口起点 如果配置创建了多个单独的 \u0026ldquo;chunk\u0026rdquo;（例如，使用多个入口起点或使用像 CommonsChunkPlugin 这样的插件），则应该使用占位符(substitutions)来确保每个文件具有唯一的名称。\n1 2 3 4 5 6 7 8 9 10 11 // webpack.config.js { entry: { app: \u0026#39;./src/app.js\u0026#39;, search: \u0026#39;./src/search.js\u0026#39; }, output: { filename: \u0026#39;[name].js\u0026#39;, path: __dirname + \u0026#39;/dist\u0026#39; } } 以上示例代表写入到硬盘：./dist/app.js, ./dist/search.js\n6.3. 自定义的打包的出口示例 在 webpack.config.js 配置文件中，通过 output 节点指定打包的出口\n1 2 3 4 5 6 7 8 9 10 11 12 13 // webpack.config.js const path = require(\u0026#39;path\u0026#39;); // 使用 Node.js 中的导出语法，向外导出一个 webpack 的配置对象 module.exports = { // 指定生成的文件要存放到哪里 output: { // 输入文件的存放目录路径 path: path.join(__dirname, \u0026#39;dist\u0026#39;), // 输出生成的文件名 filename: \u0026#39;js/bundle.js\u0026#39; } } 7. 模式(mode) webpack 提供 mode 配置选项，告知 webpack 使用相应模式的内置优化。\n7.1. 配置用法 配置文件中定义：\n1 2 3 module.exports = { mode: \u0026#39;production\u0026#39; }; 通过命令 CLI 参数中传递\n1 webpack --mode=production 7.2. mode 的可选值 选项 描述 development 会将 process.env.NODE_ENV 的值设为 development。启用 NamedChunksPlugin 和 NamedModulesPlugin production 会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin 两个选值作用总结：\ndevelopment 开发环境 不会对打包生成的文件进行代码压缩和性能优化 打包速度快，适合在开发阶段使用 production 生产环境 会对打包生成的文件进行代码压缩和性能优化 打包速度很慢，仅适合在项目发布阶段使用 7.3. 示例 mode: development\n1 2 3 4 5 6 7 8 // webpack.development.config.js module.exports = { + mode: \u0026#39;development\u0026#39; - plugins: [ - new webpack.NamedModulesPlugin(), - new webpack.DefinePlugin({ \u0026#34;process.env.NODE_ENV\u0026#34;: JSON.stringify(\u0026#34;development\u0026#34;) }), - ] } mode: production\n1 2 3 4 5 6 7 8 9 10 // webpack.production.config.js module.exports = { + mode: \u0026#39;production\u0026#39;, - plugins: [ - new UglifyJsPlugin(/* ... */), - new webpack.DefinePlugin({ \u0026#34;process.env.NODE_ENV\u0026#34;: JSON.stringify(\u0026#34;production\u0026#34;) }), - new webpack.optimize.ModuleConcatenationPlugin(), - new webpack.NoEmitOnErrorsPlugin() - ] } 8. 加载器(loader) loader 用于对模块的源代码进行转换。loader 可以使你在 import 或\u0026quot;加载\u0026quot;模块时预处理文件。因此，loader 类似于其他构建工具中“任务(task)”，并提供了处理前端构建步骤的强大方法。loader 可以将文件从不同的语言（如 TypeScript）转换为 JavaScript，或将内联图像转换为 data URL。loader 甚至允许你直接在 JavaScript 模块中 import CSS文件！\n8.1. loader 概述 在实际开发过程中，webpack 默认只能打包处理以 .js 后缀名结尾的模块。其他非 .js 后缀名结尾的模块，webpack 默认处理不了，需要调用 loader 加载器才可以正常打包，否则会报错！\nloader 加载器的作用：协助 webpack 打包处理特定的文件模块。常见的loader如：\ncss-loader 可以打包处理 .css 相关的文件 less-loader 可以打包处理 .less 相关的文件 babel-loader 可以打包处理 webpack 无法处理的高级 JS 语法 8.2. loader 的调用过程 8.3. loader 特性 loader 支持链式传递。能够对资源使用流水线(pipeline)。一组链式的 loader 将按照相反的顺序执行。loader 链中的第一个 loader 返回值给下一个 loader。在最后一个 loader，返回 webpack 所预期的 JavaScript。 loader 可以是同步的，也可以是异步的。 loader 运行在 Node.js 中，并且能够执行任何可能的操作。 loader 接收查询参数。用于对 loader 传递配置。 loader 也能够使用 options 对象进行配置。 除了使用 package.json 常见的 main 属性，还可以将普通的 npm 模块导出为 loader，做法是在 package.json 里定义一个 loader 字段。 插件(plugin)可以为 loader 带来更多特性。 loader 能够产生额外的任意文件。 loader 通过（loader）预处理函数，为 JavaScript 生态系统提供了更多能力。 用户现在可以更加灵活地引入细粒度逻辑，例如压缩、打包、语言翻译和其他更多。\n8.4. loader 的使用 有三种使用 loader 的方式：\n配置（推荐）：在 webpack.config.js 文件中指定 loader。 内联：在每个 import 语句中显式指定 loader。 CLI：在 shell 命令中指定它们。 8.4.1. 配置方式（推荐） module.rules 允许在 webpack 配置中指定多个 loader，同时可以对各个 loader 有个全局概览。示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 module: { rules: [ { test: /\\.css$/, use: [ { loader: \u0026#39;style-loader\u0026#39; }, { loader: \u0026#39;css-loader\u0026#39;, options: { modules: true } } ] } ] } 8.4.2. 内联方式 可以在 import 语句或任何等效于 \u0026ldquo;import\u0026rdquo; 的方式中指定 loader。使用 ! 将资源中的 loader 分开。分开的每个部分都相对于当前目录解析。\n1 import Styles from \u0026#39;style-loader!css-loader?modules!./styles.css\u0026#39;; 通过前置所有规则及使用 !，可以对应覆盖到配置中的任意 loader。选项可以传递查询参数，例如 ?key=value\u0026amp;foo=bar，或者一个 JSON 对象，例如 ?{\u0026quot;key\u0026quot;:\u0026quot;value\u0026quot;,\u0026quot;foo\u0026quot;:\u0026quot;bar\u0026quot;}\n8.4.3. CLI方式 通过 CLI 使用 loader：\n1 webpack --module-bind jade-loader --module-bind \u0026#39;css=style-loader!css-loader\u0026#39; 以上示例是对 .jade 文件使用 jade-loader，对 .css 文件使用 style-loader 和 css-loader。\n8.5. 常用的loader 8.5.1. 打包处理 css 文件 运行以下命令，安装处理 css 文件的 loader 1 npm i style-loader@3.0.0 css-loader@5.2.6 -D 在 webpack.config.js 的 module -\u0026gt; rules 数组中，添加 loader 规则如下 1 2 3 4 5 6 7 8 9 10 11 module.exports = { ... module: { // 所有第三方文件模块的匹配规则 // 定义了不同模块对应的 loader。文件后缀名的匹配规则 rules: [ // 定义处理 .css 文件的 loader { test: /\\.css$/, use: [\u0026#39;style-loader\u0026#39;, \u0026#39;css-loader\u0026#39;] } ] }, ... } 其中，test 表示匹配的文件类型， use 表示对应要调用的 loader\n注意：\nuse 数组中指定的 loader 顺序是固定的 多个 loader 的调用顺序是：从后往前调用 8.5.2. 打包处理 less 文件 运行以下命令，安装处理 less 文件的 loader 1 npm i less-loader@10.0.1 less@4.1.1 -D 在 webpack.config.js 的 module -\u0026gt; rules 数组中，添加 loader 规则如下 1 2 3 4 5 6 7 8 9 10 11 12 13 module.exports = { ... module: { // 所有第三方文件模块的匹配规则 // 定义了不同模块对应的 loader。文件后缀名的匹配规则 rules: [ // 定义处理 .css 文件的 loader { test: /\\.css$/, use: [\u0026#39;style-loader\u0026#39;, \u0026#39;css-loader\u0026#39;] }, // 处理 .less 文件的 loader { test: /\\.less$/, use: [\u0026#39;style-loader\u0026#39;, \u0026#39;css-loader\u0026#39;, \u0026#39;less-loader\u0026#39;] } ] }, ... } 注意：上面示例的 less 是包含在 less-loader 中，添加规则只需要 less-loader 即可\n8.5.3. 打包处理样式表中与 url 路径相关的文件 运行以下命令，安装处理 url 路径相关的文件的 loader 1 npm i url-loader@4.1.1 file-loader@6.2.0 -D 在 webpack.config.js 的 module -\u0026gt; rules 数组中，添加 loader 规则如下 1 2 3 4 5 6 7 8 9 10 11 12 13 module.exports = { ... module: { // 所有第三方文件模块的匹配规则 // 定义了不同模块对应的 loader。文件后缀名的匹配规则 rules: [ // 处理图片文件的 loader // 如果需要调用的 loader 只有一个，则只传递一个字符串也行，如果有多个loader，则必须指定数组 // 在配置 url-loader 时，可以在 ? 之后的增加 loader 的参数项。多个参数之间，使用 \u0026amp; 符号进行分隔 { test: /\\.jpg|png|gif$/, use: \u0026#39;url-loader?limit=470\u0026amp;outputPath=images\u0026#39; } ] }, ... } 注：其中 ? 之后的是 loader 的参数项，如多个参数之间，使用 \u0026amp; 符号进行分隔：\nlimit 用来指定图片的大小，单位是字节（byte） 只有 ≤ limit 大小的图片，才会被转为 base64 格式的图片 8.5.4. 打包处理 js 文件中的高级语法 8.5.4.1. 问题分析 webpack 只能打包处理一部分高级的 JavaScript 语法。对于那些 webpack 无法处理的高级 js 语法，需要借助于 babel-loader 进行打包处理。例如 webpack 无法处理下面的 JavaScript 代码：\n8.5.4.2. 安装与配置 运行以下命令，安装处理高级 js 语法的 babel-loader 及对应的依赖包 1 npm i babel-loader@8.2.2 @babel/core@7.14.6 @babel/plugin-proposal-decorators@7.14.5 -D 在 webpack.config.js 的 module -\u0026gt; rules 数组中，添加 loader 规则如下 1 2 3 4 5 6 7 8 9 10 11 12 13 module.exports = { ... module: { // 所有第三方文件模块的匹配规则 // 定义了不同模块对应的 loader。文件后缀名的匹配规则 rules: [ // 使用 babel-loader 处理高级的 JS 语法 // 在配置 babel-loader 的时候，程序员只需要把自己的代码进行转换即可；一定要排除 node_modules 目录中的 JS 文件 // 因为第三方包中的 JS 兼容性，不需要程序员关心 { test: /\\.js$/, use: \u0026#39;babel-loader\u0026#39;, exclude: /node_modules/ } ] }, ... } 注：在配置 babel-loader 时，只需要转换自身项目代码，要排除依赖第三方包，即（node_modules）\n在项目根目录下，创建名为 babel.config.js 的配置文件，定义 Babel 的配置项如下： 1 2 3 4 5 module.exports = { // 声明 babel 可用的插件 // 将来，webpack 在调用 babel-loader 的时候，会先加载 plugins 插件来使用 plugins: [[\u0026#39;@babel/plugin-proposal-decorators\u0026#39;, { legacy: true }]] } 详情请参考 Babel 的官网 https://babeljs.io/docs/en/babel-plugin-proposal-decorators\n9. 插件(plugins) 插件是 webpack 的支柱功能。通过安装和配置第三方的插件，可以拓展 webpack 的能力。插件目的在于解决 loader 无法实现的其他事。\n9.1. 配置语法 插件可以携带参数/选项，必须在 webpack 配置中，向 plugins 属性传入 new 实例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // webpack.config.js const HtmlWebpackPlugin = require(\u0026#39;html-webpack-plugin\u0026#39;); // 通过 npm 安装 const webpack = require(\u0026#39;webpack\u0026#39;); // 访问内置的插件 const path = require(\u0026#39;path\u0026#39;); const config = { entry: \u0026#39;./path/to/my/entry/file.js\u0026#39;, output: { filename: \u0026#39;my-first-webpack.bundle.js\u0026#39;, path: path.resolve(__dirname, \u0026#39;dist\u0026#39;) }, module: { rules: [ { test: /\\.(js|jsx)$/, use: \u0026#39;babel-loader\u0026#39; } ] }, plugins: [ new webpack.optimize.UglifyJsPlugin(), new HtmlWebpackPlugin({template: \u0026#39;./src/index.html\u0026#39;}) ] }; module.exports = config; 9.2. 常用插件 webpack-dev-server 热部署 webpack-dev-server 插件类似于 node.js 阶段用到的 nodemon 工具。每当修改了源代码，webpack 会自动进行项目的打包和构建\nwebpack-dev-server 可以让 webpack 监听项目源代码的变化，从而进行自动打包构建。\n9.2.1. 安装 运行如下的命令，即可在项目中安装 webpack-dev-server 插件：\n1 npm install webpack-dev-server@3.11.2 -D 9.2.2. 配置 devServer 节点 在 webpack.config.js 配置文件中，可以通过 devServer 节点对 webpack-dev-server 插件进行更多的配置\n1 2 3 4 5 6 7 8 devServer: { // 首次打包成功后，自动打开浏览器 open: true, // 配置实时打包所使用的端口号。在 http 协议中，如果访问的服务端口号是 80，则可以被省略 port: 80, // 指定实时打包运行所使用的的主机地址 host: \u0026#39;127.0.0.1\u0026#39; }, 注意：修改了 webpack.config.js 配置文件，必须重启实时打包的服务器，否则最新的配置文件无法生效！\n9.2.3. 启动插件 修改 package.json -\u0026gt; scripts 中的 dev 命令如下： 1 2 3 \u0026#34;scripts\u0026#34;: { \u0026#34;dev\u0026#34;: \u0026#34;webpack serve\u0026#34; // script 节点下的脚本，可以通过 npm run 执行 }, 再次运行 npm run dev 命令，重新进行项目的打包 在浏览器中访问项目地址，查看自动打包效果 注意：webpack-dev-server 会启动一个实时打包的 http 服务器\n9.2.4. 打包生成的文件的存放位置 不配置 webpack-dev-server 的情况下，webpack 打包生成的文件，会存放到实际的物理磁盘上 严格遵守开发者在 webpack.config.js 中指定配置 根据 output 节点指定路径进行存放 配置了 webpack-dev-server 之后，打包生成的文件存放到了内存中 不再根据 output 节点指定的路径，存放到实际的物理磁盘上 提高了实时打包输出的性能，因为内存比物理磁盘速度快很多 9.2.5. 生成到内存中的文件该如何访问 webpack-dev-server 生成到内存中的文件，默认放到了项目的根目录中，而且是虚拟的、不可见的。\n可以直接用 / 表示项目根目录，后面跟上要访问的文件名称，即可访问内存中的文件 例如 /bundle.js 就表示要访问 webpack-dev-server 生成到内存中的 bundle.js 文件 9.3. 常用插件 html-webpack-plugin html-webpack-plugin 是 webpack 中的 HTML 插件，可以通过此插件自定制 index.html 页面的内容。即会将 src 目录下的 index.html 首页，复制到项目根目录中一份！\n9.3.1. 安装 运行如下的命令，即可在项目中安装 html-webpack-plugin 插件：\n1 npm install html-webpack-plugin@5.3.2 -D 9.3.2. 配置 修改 webpack.config.js 配置文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 1. 导入 html-webpack-plugin 这个插件，得到插件的构造函数 const HtmlPlugin = require(\u0026#39;html-webpack-plugin\u0026#39;) // 2. new 构造函数，创建插件的实例对象 const htmlPlugin = new HtmlPlugin({ // 指定要复制哪个页面 template: \u0026#39;./src/index.html\u0026#39;, // 指定复制出来的文件名和存放路径 filename: \u0026#39;./index.html\u0026#39; }) // 使用 Node.js 中的导出语法，向外导出一个 webpack 的配置对象 module.exports = { ... // 3. 插件的数组，将来 webpack 在运行时，会加载并调用这些插件 plugins: [htmlPlugin], // 通过 plugins 节点，使 htmlPlugin 插件生效 ... } 9.3.3. 插件的实现原理 通过 html-webpack-plugin 插件复制到项目根目录中的 index.html 页面，也被放到了内存中 html-webpack-plugin 插件在生成的 index.html 页面，自动注入了打包的 bundle.js 文件 10. 打包发布 项目开发完成之后，需要使用 webpack 对项目进行打包发布，主要原因有以下两点：\n开发环境下，打包生成的文件存放于内存中，无法获取到最终打包生成的文件 开发环境下，打包生成的文件不会进行代码压缩和性能优化 为了让项目能够在生产环境中高性能的运行，因此需要对项目进行打包发布。\n10.1. 配置 webpack 的打包发布 在 package.json 文件的 scripts 节点下，新增 build 命令如下：\n1 2 3 4 \u0026#34;scripts\u0026#34;: { \u0026#34;dev\u0026#34;: \u0026#34;webpack serve\u0026#34;, \u0026#34;build\u0026#34;: \u0026#34;webpack --mode production\u0026#34; }, 说明：--mode 是一个参数项，用来指定 webpack 的运行模式。production 代表生产环境，会对打包生成的文件进行代码压缩和性能优化。通过 --mode 指定的参数项，会覆盖 webpack.config.js 中的 mode 选项。\n打包的命令可随意命名，以上示例是使用build\n10.2. 指定 JavaScript 文件统一生成到 js 目录中 在 webpack.config.js 配置文件的 output 节点中，进行如下的配置：\n1 2 3 4 5 6 7 // 指定生成的文件要存放到哪里 output: { // 存放的目录 path: path.join(__dirname, \u0026#39;dist\u0026#39;), // 指定生成的文件名，明确让 webpack 把生成的 bundle.js 文件存放到 dist 目录下的 js 子目录中 filename: \u0026#39;js/bundle.js\u0026#39; } 10.3. 把图片文件统一生成到 image 目录中 修改 webpack.config.js 中的 url-loader 配置项，新增 outputPath 选项即可指定图片文件的输出路径：\n10.4. 自动清理 dist 目录下的旧文件 为了在每次打包发布时自动清理掉 dist 目录中的旧文件，可以安装并配置 clean-webpack-plugin 插件：\n10.5. Source Map 10.5.1. 生产环境遇到的问题 前端项目在投入生产环境之前，都需要对 JavaScript 源代码进行压缩，从而减小文件的体积，提高文件的加载效率。此时就不可避免的产生了另一个问题：对压缩之后的代码除错（debug）是一件极其困难的事情。因为压缩后的代码：\n变量被替换成没有任何语义的名称 空行和注释被剔除 10.5.2. Source Map 是什么 Source Map 就是一个信息文件，里面储存着位置信息。也就是说，Source Map 文件中存储着压缩后的代码，所对应的转换前的位置。\n有了它，出错的时候，debug工具将直接显示原始代码，而不是转换后的代码，能够极大的方便后期的调试。\n10.5.3. webpack 开发环境下的 Source Map 在开发环境下，webpack 默认启用了 Source Map 功能。当程序运行出错时，可以直接在控制台提示错误行的位置，并定位到具体的源代码：\n开发环境下默认生成的 Source Map，记录的是生成后的代码的位置。会导致运行时报错的行数与源代码的行数不一致的问题。示意图如下：\n配置开发环境下的 Source Map\n开发环境下，推荐在 webpack.config.js 中添加如下的配置，即可保证运行时报错的行数与源代码的行数保持一致：\n1 2 3 4 5 module.exports = { // 在开发调试阶段，建议都把 devtool 的值设置为 eval-source-map // 此选项生成的 Source Map 能保证运行时报错的行数与源代码的行数保持一致 devtool: \u0026#39;eval-source-map\u0026#39; } 10.5.4. webpack 生产环境下的 Source Map 在生产环境下，如果省略了 devtool 选项，则最终生成的文件中不包含 Source Map。这能够防止原始代码通过 Source Map 的形式暴露给别有所图之人。\n10.5.4.1. 只定位行数不暴露源码 在生产环境下，如果只想定位报错的具体行数，且不想暴露源码。此时可以将 devtool 的值设置为 nosources-source-map。实际效果如图所示：\n10.5.4.2. 定位行数且暴露源码 在生产环境下，如果想在定位报错行数的同时，展示具体报错的源码。此时可以将 devtool 的值设置为 source-map。实际效果如图所示：\n使用此选项后：应该将服务器配置为，不允许普通用户访问 source map 文件！\n10.5.5. Source Map 的最佳实践 开发环境下：\n建议把 devtool 的值设置为 eval-source-map 好处：可以精准定位到具体的错误行 生产环境下：\n建议关闭 Source Map 或将 devtool 的值设置为 nosources-source-map 好处：防止源码泄露，提高网站的安全性 ","permalink":"https://ktzxy.top/posts/o5fs3ppjqp/","summary":"webpack","title":"webpack"},{"content":"Docker操作系统之Alpine 前言 这阵子我发现只要带着alpine前缀的镜像，相比于其它的镜像，体积都相对较小，例如下面这些\n1 2 java:alpine nginx:alpine 后面通过了解，发现了其实这些java镜像，或者nginx镜像都依赖于某个linux操作系统，我们常见的操作系统有 ubuntu、centos、debian。而这个alpine其实也是一个新的操作系统。但是它比其它的操作系统而言，体积更小，所以在他们的基础之上做的镜像，体积也会更小，常见的linux操作系统体积大小，如下所示\n1 2 3 4 5 REPOSITORY TAG IMAGE ID VIRTUAL SIZE alpine latest 4e38e38c8ce0 4.799 MB debian latest 4d6ce913b130 84.98 MB ubuntu latest b39b81afc8ca 188.3 MB centos latest 8efe422e6104 210 MB 我们也就发现了 alpine的大小远远小于 其它的操作系统，因此制作出来的镜像大小也远远小于其它的\nAlpine操作系统 Alpine操作系统主要是面向安全的轻量级Linux发行版，它和其它的发行版不同之处在于，Alpine采用了musllibc 和 busybox以减少系统体积和运行时资源消耗，但功能上比busybox又完善的多 ，因此越来越得到开源社区的青睐。在保持瘦身的同时，Alpine还提供了自己的包管理工具【CentOS是yum，ubuntu是 apt-get】，可以通过 Alpine包查询网站 ，来进行查看，例如下图所示，搜索自己需要安装的包进行查看\n然后通过 apk add vim 来进行安装即可。\nAlpine Docker镜像也继承了Alpine Linux发行版的优势，相比于其它的Docker镜像，它的容量体积非常小，仅仅只有5MB，我们通过打开 DockerHub中 Alpine的官网\n可以发现，它提供了只有5MB的系统镜像可供我们进行下载使用\n1 2 # 下载alpine镜像 docker pull alpine 同时，它还列举了一个例子 【通过制作一个mysql镜像】\n使用Alpine 和 Ubuntu 制作出来的镜像一个是 36.8MB 一个是 145MB，相差4倍多\n目前Docker官方已经开始推荐Alpine替代之前的Ubuntu作为基础镜像环境，这样所带来的好处包括：镜像下载速度更快、镜像安全性提高、主机之间的切换更方便、占用内存更少等特点。\n使用Alpine镜像 我们通过下面命令，能够非常快的运行一个Alpine容器【本地不存在会去官方下载】，并输出 hello alpine\n1 docker run alpine echo \u0026#34;hello alpine\u0026#34; 迁移至Alpine 目前，大部分Docker官方镜像，都已经提供了Alpine版本镜像的支持，我们非常容易镜像迁移\n例如，通过Nginx的 官方DockerHub地址，我们可以看到，也专门有 alpine稳定版本\n还有其它一些官方镜像也都提供了alpine版本，我们可以在\n1 2 3 ubuntu/debian -\u0026gt; alpine python:2.7 -\u0026gt; python:2.7-alpine ruby:2.3 -\u0026gt; ruby:2.3-alpine 另外，如果我们想要在alpine的基础上进行一些软件的安装，可以使用下面的命令\n1 apk add --no-cache \u0026lt;package\u0026gt; 思考 本段来源于V2EX上的 提问\n上面其实我们已经提到了很多关于alpine的优势，比如体积小，并且很多官方的Docker镜像都提供了基于alpine的版本。那如果alpine版本没有任何坑的话，从体积小，并且能满足使用正常使用的话，这相比于CentOS、Ubuntu和Debian的镜像，就拥有非常大的优势了，那么以后这些发行版的进行也就没有存在的必要，真实是这样的么？\n下面是针对上述问题，最后的总结\n首先在基于alpine的操作系统上编写Dockerfile制作新镜像，并不会像其他操作系统一样方便，甚至会出现alpine中不存在的情况。 虽然每个单个的基于alpine的软件镜像是明显少于其他操作系统，但是如果多个镜像【包括每个镜像运行的多个容器】，使用 了同一个基础镜像，是不会花费额外的空间【归结于docker的Overlay文件系统】 有些软件没办法在Alpine中运行，因为alpine不像其它发行版那样使用CGLIBC【MySQL没有Alpine镜像】 参考 Alpine 官网：http://alpinelinux.org/ Alpine 官方仓库：https://github.com/alpinelinux Alpine 官方镜像：https://hub.docker.com/_/alpine/ Alpine 官方镜像仓库：https://github.com/gliderlabs/docker-alpine ","permalink":"https://ktzxy.top/posts/n89ccfrysq/","summary":"Docker操作系统之Alpine","title":"Docker操作系统之Alpine"},{"content":"Nginx Nginx 简介 背景介绍 Nginx（“engine x”）一个具有高性能的【HTTP】和【反向代理】的【WEB服务器】，同时也是一个【POP3/SMTP/IMAP代理服务器】，是由伊戈尔·赛索耶夫(俄罗斯人)使用C语言编写的，Nginx的第一个版本是2004年10月4号发布的0.1.0版本。另外值得一提的是伊戈尔·赛索耶夫将Nginx的源码进行了开源，这也为Nginx的发展提供了良好的保障。\n名词解释 WEB服务器： WEB服务器也叫网页服务器，英文名叫Web Server，主要功能是为用户提供网上信息浏览服务。\nHTTP: HTTP是超文本传输协议的缩写，是用于从WEB服务器传输超文本到本地浏览器的传输协议，也是互联网上应用最为广泛的一种网络协议。HTTP是一个客户端和服务器端请求和应答的标准，客户端是终端用户，服务端是网站，通过使用Web浏览器、网络爬虫或者其他工具，客户端发起一个到服务器上指定端口的HTTP请求。\nPOP3/SMTP/IMAP： POP3(Post Offic Protocol 3)邮局协议的第三个版本，\nSMTP(Simple Mail Transfer Protocol)简单邮件传输协议，\nIMAP(Internet Mail Access Protocol)交互式邮件存取协议，\n通过上述名词的解释，我们可以了解到Nginx也可以作为电子邮件代理服务器。\n反向代理 正向代理\n反向代理\n常见服务器对比 1 Netcraft公司于1994年底在英国成立，多年来一直致力于互联网市场以及在线安全方面的咨询服务，其中在国际上最具影响力的当属其针对网站服务器、SSL市场所做的客观严谨的分析研究，公司官网每月公布的调研数据（Web Server Survey）已成为当今人们了解全球网站数量以及服务器市场分额情况的主要参考依据，时常被诸如华尔街杂志，英国BBC，Slashdot等媒体报道或引用。 我们先来看一组数据，我们先打开Nginx的官方网站 http://nginx.org/,找到Netcraft公司公布的数据，对当前主流服务器产品进行介绍。\n上面这张图展示了2019年全球主流Web服务器的市场情况，其中有Apache、Microsoft-IIS、google Servers、Nginx、Tomcat等，而我们在了解新事物的时候，往往习惯通过类比来帮助自己理解事物的概貌。所以下面我们把几种常见的服务器来给大家简单介绍下：\nIIS ​\t全称(Internet Information Services)即互联网信息服务，是由微软公司提供的基于windows系统的互联网基本服务。windows作为服务器在稳定性与其他一些性能上都不如类UNIX操作系统，因此在需要高性能Web服务器的场合下，IIS可能就会被\u0026quot;冷落\u0026quot;.\nTomcat ​\tTomcat是一个运行Servlet和JSP的Web应用软件，Tomcat技术先进、性能稳定而且开放源代码，因此深受Java爱好者的喜爱并得到了部分软件开发商的认可，成为目前比较流行的Web应用服务器。但是Tomcat天生是一个重量级的Web服务器，对静态文件和高并发的处理比较弱。\nApache ​\tApache的发展时期很长，同时也有过一段辉煌的业绩。从上图可以看出大概在2014年以前都是市场份额第一的服务器。Apache有很多优点，如稳定、开源、跨平台等。但是它出现的时间太久了，在它兴起的年代，互联网的产业规模远远不如今天，所以它被设计成一个重量级的、不支持高并发的Web服务器。在Apache服务器上，如果有数以万计的并发HTTP请求同时访问，就会导致服务器上消耗大量内存，操作系统内核对成百上千的Apache进程做进程间切换也会消耗大量的CUP资源，并导致HTTP请求的平均响应速度降低，这些都决定了Apache不可能成为高性能的Web服务器。这也促使了Lighttpd和Nginx的出现。\nLighttpd ​\tLighttpd是德国的一个开源的Web服务器软件，它和Nginx一样，都是轻量级、高性能的Web服务器，欧美的业界开发者比较钟爱Lighttpd,而国内的公司更多的青睐Nginx，同时网上Nginx的资源要更丰富些。\n其他的服务器 Google Servers，Weblogic, Webshpere(IBM)\u0026hellip;\n经过各个服务器的对比，种种迹象都表明，Nginx将以性能为王。这也是我们为什么选择Nginx的理由。\nNginx的优点 (1)速度更快、并发更高 单次请求或者高并发请求的环境下，Nginx都会比其他Web服务器响应的速度更快。一方面在正常情况下，单次请求会得到更快的响应，另一方面，在高峰期(如有数以万计的并发请求)，Nginx比其他Web服务器更快的响应请求。Nginx之所以有这么高的并发处理能力和这么好的性能原因在于Nginx采用了多进程和I/O多路复用(epoll)的底层实现。\n(2)配置简单，扩展性强 Nginx的设计极具扩展性，它本身就是由很多模块组成，这些模块的使用可以通过配置文件的配置来添加。这些模块有官方提供的也有第三方提供的模块，如果需要完全可以开发服务自己业务特性的定制模块。\n(3)高可靠性 Nginx采用的是多进程模式运行，其中有一个master主进程和N多个worker进程，worker进程的数量我们可以手动设置，每个worker进程之间都是相互独立提供服务，并且master主进程可以在某一个worker进程出错时，快速去\u0026quot;拉起\u0026quot;新的worker进程提供服务。\n(4)热部署 现在互联网项目都要求以7*24小时进行服务的提供，针对于这一要求，Nginx也提供了热部署功能，即可以在Nginx不停止的情况下，对Nginx进行文件升级、更新配置和更换日志文件等功能。\n(5)成本低、BSD许可证 BSD是一个开源的许可证，世界上的开源许可证有很多，现在比较流行的有六种分别是GPL、BSD、MIT、Mozilla、Apache、LGPL。这六种的区别是什么，我们可以通过下面一张图来解释下：\nNginx本身是开源的，我们不仅可以免费的将Nginx应用在商业领域，而且还可以在项目中直接修改Nginx的源码来定制自己的特殊要求。这些点也都是Nginx为什么能吸引无数开发者继续为Nginx来贡献自己的智慧和青春。OpenRestry [Nginx+Lua] Tengine[淘宝]\nNginx的功能特性及常用功能 Nginx提供的基本功能服务从大体上归纳为\u0026quot;基本HTTP服务\u0026quot;、“高级HTTP服务”和\u0026quot;邮件服务\u0026quot;等三大类。\n基本HTTP服务 Nginx可以提供基本HTTP服务，可以作为HTTP代理服务器和反向代理服务器，支持通过缓存加速访问，可以完成简单的负载均衡和容错，支持包过滤功能，支持SSL等。\n处理静态文件、处理索引文件以及支持自动索引； 提供反向代理服务器，并可以使用缓存加上反向代理，同时完成负载均衡和容错； 提供对FastCGI、memcached等服务的缓存机制，，同时完成负载均衡和容错； 使用Nginx的模块化特性提供过滤器功能。Nginx基本过滤器包括gzip压缩、ranges支持、chunked响应、XSLT、SSI以及图像缩放等。其中针对包含多个SSI的页面，经由FastCGI或反向代理，SSI过滤器可以并行处理。 支持HTTP下的安全套接层安全协议SSL. 支持基于加权和依赖的优先权的HTTP/2 高级HTTP服务 支持基于名字和IP的虚拟主机设置 支持HTTP/1.0中的KEEP-Alive模式和管线(PipeLined)模型连接 自定义访问日志格式、带缓存的日志写操作以及快速日志轮转。 提供3xx~5xx错误代码重定向功能 支持重写（Rewrite)模块扩展 支持重新加载配置以及在线升级时无需中断正在处理的请求 支持网络监控 支持FLV和MP4流媒体传输 邮件服务 Nginx提供邮件代理服务也是其基本开发需求之一，主要包含以下特性：\n支持IMPA/POP3代理服务功能 支持内部SMTP代理服务功能 Nginx常用的功能模块 1 2 3 4 5 6 7 8 9 10 静态资源部署 Rewrite地址重写 正则表达式 反向代理 负载均衡 轮询、加权轮询、ip_hash、url_hash、fair Web缓存 环境部署 高可用的环境 用户认证模块... Nginx的核心组成\n1 2 3 4 nginx二进制可执行文件 nginx.conf配置文件 error.log错误的日志记录 access.log访问日志记录 Nginx 环境准备 Nginx 版本介绍 Nginx的官方网站为: http://nginx.org\n打开源码可以看到如下的页面内容\nNginx的官方下载网站为http://nginx.org/en/download.html，当然你也可以之间在首页选中右边的download进入版本下载网页。在下载页面我们会看到如下内容：\nNginx安装方式介绍 Nginx的安装方式有两种分别是:\n1 2 3 4 通过Nginx源码 通过Nginx源码简单安装 (1) 通过Nginx源码复杂安装 (3) 通过yum安装 (2) 如果通过Nginx源码安装需要提前准备的内容：\nGCC编译器 Nginx是使用C语言编写的程序，因此想要运行Nginx就需要安装一个编译工具。GCC就是一个开源的编译器集合，用于处理各种各样的语言，其中就包含了C语言。\n使用命令yum install -y gcc来安装\n安装成功后，可以通过gcc --version来查看gcc是否安装成功\nPCRE Nginx在编译过程中需要使用到PCRE库（perl Compatible Regular Expressoin 兼容正则表达式库)，因为在Nginx的Rewrite模块和http核心模块都会使用到PCRE正则表达式语法。\n可以使用命令yum install -y pcre pcre-devel来进行安装\n安装成功后，可以通过rpm -qa pcre pcre-devel来查看是否安装成功\nzlib zlib库提供了开发人员的压缩算法，在Nginx的各个模块中需要使用gzip压缩，所以我们也需要提前安装其库及源代码zlib和zlib-devel\n可以使用命令yum install -y zlib zlib-devel来进行安装\n安装成功后，可以通过rpm -qa zlib zlib-devel来查看是否安装成功\nOpenSSL OpenSSL是一个开放源代码的软件库包，应用程序可以使用这个包进行安全通信，并且避免被窃听。\nSSL:Secure Sockets Layer安全套接协议的缩写，可以在Internet上提供秘密性传输，其目标是保证两个应用间通信的保密性和可靠性。在Nginx中，如果服务器需要提供安全网页时就需要用到OpenSSL库，所以我们需要对OpenSSL的库文件及它的开发安装包进行一个安装。\n可以使用命令yum install -y openssl openssl-devel来进行安装\n安装成功后，可以通过rpm -qa openssl openssl-devel来查看是否安装成功\n上述命令，一个个来的话比较麻烦，我们也可以通过一条命令来进行安装\nyum install -y gcc pcre pcre-devel zlib zlib-devel openssl openssl-devel进行全部安装。\n方案一：Nginx的源码简单安装 我使用的是这个方案\n(1)进入官网查找需要下载版本的链接地址，然后使用wget命令进行下载\n1 wget http://nginx.org/download/nginx-1.28.2.tar.gz (2)建议大家将下载的资源进行包管理\n1 2 mkdir -p /opt/nginx # 创建目录 mv nginx-1.28.2.tar.gz /opt/nginx # 移动nginx压缩包 (3)解压缩\n1 tar -xzf nginx-1.28.2.tar.gz #解压缩 (4)进入资源文件中，发现configure\n1 2 cd /opt/nginx/nginx-1.28.2 # 进入到目录 ./configure # 执行configure文件 (5)编译\n1 make (6)安装\n1 make install 默认安装地址：/usr/local/nginx/\n(7)启动\n1 2 cd /usr/local/nginx/sbin #进入目录 ./nginx #执行文件 (8)打开80端口\n1 2 firewall-cmd --permanent --add-port=80/tcp #打开端口 firewall-cmd --reload #刷新 (9)访问Nginx\n方案二：yum安装 使用源码进行简单安装，我们会发现安装的过程比较繁琐，需要提前准备GCC编译器、PCRE兼容正则表达式库、zlib压缩库、OpenSSL安全通信的软件库包，然后才能进行Nginx的安装。\n（1）安装yum-utils\n1 sudo yum install -y yum-utils （2）添加yum源文件\n1 vim /etc/yum.repos.d/nginx.repo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [nginx-stable] name=nginx stable repo baseurl=http://nginx.org/packages/centos/$releasever/$basearch/ gpgcheck=1 enabled=1 gpgkey=https://nginx.org/keys/nginx_signing.key module_hotfixes=true [nginx-mainline] name=nginx mainline repo baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/ gpgcheck=1 enabled=0 gpgkey=https://nginx.org/keys/nginx_signing.key module_hotfixes=true （3）查看是否安装成功\n1 yum list | grep nginx （4）使用yum进行安装\n1 yun install -y nginx （5）查看nginx的安装位置\n1 whereis nginx （6）启动测试\n源码简单安装和yum安装的差异： 这里先介绍一个命令: ./nginx -V,通过该命令可以查看到所安装Nginx的版本及相关配置信息。\n简单安装\nyum安装\n解压Nginx目录 执行tar -zxvf nginx-1.16.1.tar.gz对下载的资源进行解压缩，进入压缩后的目录，可以看到如下结构\n内容解释：\nauto:存放的是编译相关的脚本\nCHANGES:版本变更记录\nCHANGES.ru:俄罗斯文的版本变更记录\nconf:nginx默认的配置文件\nconfigure:nginx软件的自动脚本程序,是一个比较重要的文件，作用如下：\n​\t（1）检测环境及根据环境检测结果生成C代码\n​\t（2）生成编译代码需要的Makefile文件\ncontrib:存放的是几个特殊的脚本文件，其中README中对脚本有着详细的说明\nhtml:存放的是Nginx自带的两个html页面，访问Nginx的首页和错误页面\nLICENSE:许可证的相关描述文件\nman:nginx的man手册\nREADME:Nginx的阅读指南\nsrc:Nginx的源代码\n方案三:Nginx的源码复杂安装 这种方式和简单的安装配置不同的地方在第一步，通过./configure来对编译参数进行设置，需要我们手动来指定。那么都有哪些参数可以进行设置，接下来我们进行一个详细的说明。\nPATH:是和路径相关的配置信息\nwith:是启动模块，默认是关闭的\nwithout:是关闭模块，默认是开启的\n我们先来认识一些简单的路径配置已经通过这些配置来完成一个简单的编译：\n\u0026ndash;prefix=PATH\n1 指向Nginx的安装目录，默认值为/usr/local/nginx \u0026ndash;sbin-path=PATH\n1 指向(执行)程序文件(nginx)的路径,默认值为\u0026lt;prefix\u0026gt;/sbin/nginx \u0026ndash;modules-path=PATH\n1 指向Nginx动态模块安装目录，默认值为\u0026lt;prefix\u0026gt;/modules \u0026ndash;conf-path=PATH\n1 指向配置文件(nginx.conf)的路径,默认值为\u0026lt;prefix\u0026gt;/conf/nginx.conf \u0026ndash;error-log-path=PATH\n1 指向错误日志文件的路径,默认值为\u0026lt;prefix\u0026gt;/logs/error.log \u0026ndash;http-log-path=PATH\n1 指向访问日志文件的路径,默认值为\u0026lt;prefix\u0026gt;/logs/access.log \u0026ndash;pid-path=PATH\n1 指向Nginx启动后进行ID的文件路径，默认值为\u0026lt;prefix\u0026gt;/logs/nginx.pid \u0026ndash;lock-path=PATH\n1 指向Nginx锁文件的存放路径,默认值为\u0026lt;prefix\u0026gt;/logs/nginx.lock 要想使用可以通过如下命令\n1 2 3 4 5 6 7 8 ./configure --prefix=/usr/local/nginx \\ --sbin-path=/usr/local/nginx/sbin/nginx \\ --modules-path=/usr/local/nginx/modules \\ --conf-path=/usr/local/nginx/conf/nginx.conf \\ --error-log-path=/usr/local/nginx/logs/error.log \\ --http-log-path=/usr/local/nginx/logs/access.log \\ --pid-path=/usr/local/nginx/logs/nginx.pid \\ --lock-path=/usr/local/nginx/logs/nginx.lock 在使用上述命令之前，需要将之前服务器已经安装的nginx进行卸载，卸载的步骤分为三步骤：\n步骤一：需要将nginx的进程关闭\n1 ./nginx -s stop 步骤二:将安装的nginx进行删除\n1 rm -rf /usr/local/nginx 步骤三:将安装包之前编译的环境清除掉\n1 make clean Nginx目录结构分析 在使用Nginx之前，我们先对安装好的Nginx目录文件进行一个分析，在这块给大家介绍一个工具tree，通过tree我们可以很方面的去查看centos系统上的文件目录结构，当然，如果想使用tree工具，就得先通过yum install -y tree来进行安装，安装成功后，可以通过执行tree /usr/local/nginx(tree后面跟的是Nginx的安装目录)，获取的结果如下：\nconf:nginx所有配置文件目录\n​ CGI(Common Gateway Interface)通用网关【接口】，主要解决的问题是从客户端发送一个请求和数据，服务端获取到请求和数据后可以调用调用CGI【程序】处理及相应结果给客户端的一种标准规范。\n​\tfastcgi.conf:fastcgi相关配置文件\n​\tfastcgi.conf.default:fastcgi.conf的备份文件\n​\tfastcgi_params:fastcgi的参数文件\n​\tfastcgi_params.default:fastcgi的参数备份文件\n​\tscgi_params:scgi的参数文件\n​\tscgi_params.default：scgi的参数备份文件\n​ uwsgi_params:uwsgi的参数文件\n​\tuwsgi_params.default:uwsgi的参数备份文件\n​\tmime.types:记录的是HTTP协议中的Content-Type的值和文件后缀名的对应关系\n​\tmime.types.default:mime.types的备份文件\n​\tnginx.conf:这个是Nginx的核心配置文件，这个文件非常重要，也是我们即将要学习的重点\n​\tnginx.conf.default:nginx.conf的备份文件\n​\tkoi-utf、koi-win、win-utf这三个文件都是与编码转换映射相关的配置文件，用来将一种编码转换成另一种编码\nhtml:存放nginx自带的两个静态的html页面\n​\t50x.html:访问失败后的失败页面\n​\tindex.html:成功访问的默认首页\nlogs:记录入门的文件，当nginx服务器启动后，这里面会有 access.log error.log 和nginx.pid三个文件出现。\nsbin:是存放执行程序文件nginx\n​\tnginx是用来控制Nginx的启动和停止等相关的命令。\nNginx 常用命令 使用 Nginx 操作前必须进入 Nginx 的目录 usr/local/nginx/sbin\n查看 Nginx 版本号\n./nginx -v\n启动 Nginx\n./nginx\n关闭 Nginx\n./nginx -s stop\n重新加载 Nginx\n./nginx -s reload\n作用：刷新 nginx.conf 文件，不会关闭 Nginx\nNginx 配置文件 默认 Nginx 配置文件的位置：/usr/local/nginx/conf/nginx.conf\n可以使用 whereis nginx 查看 Nginx 配置文件的位置\nNginx 配置文件组成部分 全局块 从配置文件开始到 events 块之间的内容，主要会设置一些影响 nginx 服务器整体运行的配置指令，主要包括配置运行 Nginx 服务器的用户（组）、允许生成的 worker process 数，进程 PID 存放路径、日志存放路径和类型以及配置文件的引入等。\nworker_processes 1：==进程数设置==，这是 Nginx 服务器并发处理服务的关键配置，worker_processes 值越大，可以支持的并发处理量也越多，但是会受到硬件、软件等设备的制约。一般来说，拥有几个逻辑CPU，就设置为几个worker_processes 为宜，但是 worker_processes 超过8个就没有多大意义了。 events 块 events 块涉及的指令主要影响 Nginx 服务器与用户的网络连接，常用的设置包括是否开启对多 work process 下的网络连接进行序列化，是否允许同时接收多个网络连接，选取哪种事件驱动模型来处理连接请求，每个 word process 可以同时支持的最大连接数等\nworker_connections 1024：表示每个 work process 支持的最大连接数为 1024.这部分的配置对 Nginx 的性能影响较大，在实际中应该灵活配置。 http 块 这算是 Nginx 服务器配置中最频繁的部分，代理、缓存和日志定义等绝大多数功能和第三方模块的配置都在这里。需要注意的是：http 块也可以包括 http 全局块、erver 块。\nhttp 全局块 http 全局块配置的指令包括文件引入、MIME-TYPE 定义、日志自定义、连接超时时间、单链接请求数上限等。\nserver 块 这块和虚拟主机有密切关系，虚拟主机从用户角度看，和一台独立的硬件主机是完全一样的，该技术的产生是为了节省互联网服务器硬件成本。 每个 http 块可以包括多个 server 块，而每个 server 块就相当于一个虚拟主机。而每个 server 块也分为全局 server 块，以及可以同时包含多个 locaton 块。\n1、全局 server 块\n最常见的配置是本虚拟机主机的监听配置和本虚拟主机的名称或 IP 配置。\n2、location 块\n==一个 server 块可以配置多个 location 块。==这块的主要作用是基于 Nginx 服务器接收到的请求字符串（例如 server_name/uri-string），对虚拟主机名称（实现效果：使用 nginx 反向代理，访问 www.123.com 直接跳转到 127.0.0.1:8080也可以是 IP 别名）之外的字符串（例如 前面的 /uri-string）进行匹配，对特定的请求进行处理。地址定向、数据缓存和应答控制等功能，还有许多第三方模块的配置也在这里进行。\nNginx 反向代理 Nginx 反向代理 ( 1 ) 实现效果：使用 nginx 反向代理，访问 www.123.com 直接跳转到 127.0.0.1:8080\n修改 Windows 系统下的 hosts 文件 1 2 # 文件末尾添加 192.168.200.130 www.123.com ipconfig /flushdns 清除DNS缓存内容\n修改 nginx.conf 文件，增加反向代理配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 server { listen 80; server_name www.123.com; location / { proxy_pass http://127.0.0.1:8080; # 【必配】传递真实 IP 和主机头 proxy_set_header Host $host; # 让 Tomcat 知道用户访问的是 www.123.com proxy_set_header X-Real-IP $remote_addr; # 传递真实 IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 传递代理链 # 【可选但推荐】如果是 HTTPS 终结，需告诉后端原始协议 # proxy_set_header X-Forwarded-Proto $scheme; } } 然后重启 Nginx\n如上配置，我们监听 80 端口，访问域名为 www.123.com，不加端口号时默认为 80 端口，故访问该域名时会跳转到 127.0.0.1:8080 路径上。在浏览器端输入 www.123.com 结果如下：\nNginx 反向代理 ( 2 ) 环境搭建 cp -r tomcat tomcat2 复制一份 Tomcat，并修改 conf/server.xml，防止两个 Tomcat 端口冲突\n修改 Tomcat 关闭时的端口，默认8005\n修改 Tomcat 启动后的占用端口，默认8080\n在8080端口的 Tomcat 的 webapps 文件夹中创建文件夹 mkdir test，再创建文件 a.html\n1 \u0026lt;h1\u0026gt;8080\u0026lt;/h1\u0026gt; 在8081端口的 Tomcat 的 webapps 文件夹中创建文件夹 mkdir dev，再创建文件 a.html\n1 \u0026lt;h1\u0026gt;8081\u0026lt;/h1\u0026gt; 最后启动 Tomcat\n修改 Nginx.con 文件 在 http 块中添加如下配置\n1 2 3 4 5 6 7 8 9 10 11 12 server { listen 9001; server_name 192.168.130.200; location /dev/ { proxy_pass http://127.0.0.1:8081; } location /test/ { proxy_pass http://127.0.0.1:8080; } } proxy_pass 末尾的斜杠 /,不带/，则会在webapps/ROOT/dev/ 下找文件，反之，会在 webapps/ROOT/a.html 找文件\n以下是完善后的配置，包含了真实 IP 传递、超时控制、缓冲优化和错误处理\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 server { listen 9001; server_name 192.168.130.200; # 或者写 localhost # --- 优化点 1: 使用前缀匹配而非正则，性能更好 --- # 访问 /dev/xxx -\u0026gt; 转发给 8081 location /dev/ { # 注意：这里没有末尾斜杠，保留 /dev/ 传递给后端 proxy_pass http://127.0.0.1:8081; # --- 优化点 2: 传递真实用户信息 (至关重要) --- proxy_set_header Host $host; # 告诉后端原始域名是哪个 proxy_set_header X-Real-IP $remote_addr; # 传递真实用户 IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 传递代理链 # --- 优化点 3: 超时控制 (防止后端卡死拖垮 Nginx) --- proxy_connect_timeout 5s; # 连接后端超时 proxy_send_timeout 10s; # 发送数据给后端超时 proxy_read_timeout 30s; # 等待后端响应超时 (根据业务调整) # --- 优化点 4: 缓冲设置 (提升用户体验，保护后端) --- proxy_buffering on; # 开启缓冲 proxy_buffer_size 4k; # 缓冲区大小 proxy_buffers 8 4k; # 缓冲区数量和大小 # --- 优化点 5: 错误重试 (高可用) --- # 如果后端返回 502/503/504 或超时，自动尝试下一次（如果有 upstream 组） proxy_next_upstream error timeout http_502 http_503 http_504; } # 访问 /test/xxx -\u0026gt; 转发给 8080 location /test/ { proxy_pass http://127.0.0.1:8080; # 同样需要传递 Header 和设置超时 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_connect_timeout 5s; proxy_read_timeout 30s; } # 可选：全局错误页面定制 error_page 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } } localhost 指令说明 1 2 3 location [=|~|~*|^~] uri { } = ：用于不含正则表达式的 uri 前，要求请求字符串与 uri 严格匹配，如果匹配成功，就停止继续向下搜索并立即处理该请求。\n~：用于表示 uri 包含正则表达式，并且==区分大小写==。\n~*：用于表示 uri 包含正则表达式，并且==不区分大小写==。\n^~：用于不含正则表达式的 uri 前，要求 Nginx 服务器找到标识 uri 和请求字符串匹配度最高的 location 后，立即使用此 location 处理请求，而不再使用 location 块中的正则 uri 和请求字符串做匹配。\n打开9001端口 firewall-cmd --permanent --add-port=9001/tcp\nfirewall-cmd --reload\n测试 重新启动 Nginx\n访问 http://192.168.200.130:9001/test/a.html 和 http://192.168.200.130:9001/dev/a.html\nNginx 负载均衡 实现效果 浏览器地址栏输入地址 http://192.168.200.130/dev/a.html，平均转发到8080和8081端口中，实现负载均衡\n环境搭建 准备两台 Tomcat 服务器，一台8080，一台8081\n在两台 Tomcat 中的 webapps 目录中，创建 dev 文件夹，在 dev 文件夹中创建页面 a.html，用于测试\n修改 nginx.conf 文件 在 http 块中添加如下配置\n1 2 3 4 upstream myserver{ server 192.168.200.130:8080; server 192.168.200.130:8081; } 默认算法：Weighted Round Robin (加权轮询)。 行为：Nginx 按时间顺序逐一分配请求。第1个给8080，第2个给8081，第3个给8080\u0026hellip; 优点：简单，负载绝对平均。 缺点/局限： 无视服务器性能差异：如果8080是高性能服务器（32核），8081是低配服务器（2核），轮询会导致低配机累死，高配机闲死。 无视连接长短：如果某个请求需要处理10秒，另一个只需0.1秒，轮询可能导致长连接堆积在某台机器上。 Session 丢失问题：用户第一次请求落到8080（登录了），第二次请求落到8081（没登录状态），导致用户被迫重新登录。 补充：四大负载均衡算法 针对上述局限，Nginx upstream 模块提供了多种策略。根据具体的业务场景选择：\n1. 加权轮询 (weight) —— 最常用 场景：服务器硬件配置不一致。 配置：\n1 2 3 4 5 upstream myserver { # 权重越高，分到的请求越多。默认 weight=1 server 192.168.200.130:8080 weight=3; # 性能强，承担3份流量 server 192.168.200.130:8081 weight=1; # 性能弱，承担1份流量 } 理解：每4个请求中，3个去8080，1个去8081。\n2. IP 哈希 (ip_hash) —— 解决 Session 共享问题 场景：后端服务器没有做 Session 共享（如 Redis 集中存储），需要确保同一用户始终访问同一台服务器。 配置：\n1 2 3 4 5 upstream myserver { ip_hash; # 开启 IP 哈希算法 server 192.168.200.130:8080; server 192.168.200.130:8081; } 原理：根据客户端 IP 地址的 hash 值取模，将请求固定分配到某台后端。 注意：\n此时 weight 参数失效。 如果局域网出口 IP 相同（如公司所有人通过同一个公网 IP 上网），所有人都可能被分到同一台服务器，导致负载不均。 3. 最少连接 (least_conn) —— 长连接/耗时任务首选 场景：请求处理时间长短不一，或维持长连接（如 WebSocket, 数据库代理）。 配置：\n1 2 3 4 5 upstream myserver { least_conn; server 192.168.200.130:8080; server 192.168.200.130:8081; } 原理：Nginx 会实时统计每台后端的活跃连接数，将新请求发给当前连接数最少的那台。这能避免某台机器因为处理慢请求而“堵车”。\n4. URL 哈希 (hash $request_uri) —— 缓存命中优化 场景：后端有本地缓存（如 Squid, Varnish 或应用层缓存），希望同一 URL 的请求总是落到同一台机器，提高缓存命中率。 配置：\n1 2 3 4 5 upstream myserver { hash $request_uri consistent; # consistent 表示一致性哈希，节点变动时影响最小 server 192.168.200.130:8080; server 192.168.200.130:8081; } 作用：将多个服务设置到一个组中，Nginx 可以通过组名访问到组中对应的服务。\n修改 server 块 仅仅配置 upstream 是不够的，在 location 中转发时，必须传递正确的信息给后端 Tomcat，否则 Tomcat 获取不到真实用户 IP，日志也是 Nginx 的 IP。\n完整优化后的 server 块配置：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 server { listen 80; server_name 192.168.200.130; location /dev/ { # 1. 转发到 upstream 组 proxy_pass http://myserver; # 2. 传递真实用户 IP (Tomcat 需配置 RemoteIpValve 才能识别) proxy_set_header Host $host; # 保留原始 Host proxy_set_header X-Real-IP $remote_addr; # 传递真实 IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 传递代理链 IP # 3. 超时设置 (防止后端卡死拖垮 Nginx) proxy_connect_timeout 5s; # 连接后端超时 proxy_send_timeout 10s; # 向后端发送数据超时 proxy_read_timeout 30s; # 等待后端响应超时 (根据业务调整，长轮询需加大) # 4. 缓冲设置 (保护后端，快速响应用户) proxy_buffering on; proxy_buffer_size 4k; proxy_buffers 8 4k; # 5. 错误页面定制 (可选) proxy_next_upstream error timeout http_502 http_503 http_504; # 遇到这些错误自动切换下一台 } } 监听80端口，当访问 192.168.130.200 时 Nginx 会转发到 myserver 组中，并进行负载均衡。\n测试 重启 Nginx，浏览器访问 http://192.168.200.130/dev/a.html，8080和8081会交替出现\n故障转移与健康检查 如果关掉 8080 的 Tomcat，Nginx 默认会怎么做？\n默认行为：Nginx 会把请求发给 8080 -\u0026gt; 连接失败 -\u0026gt; 自动尝试发给 8081（故障转移）。 问题：默认情况下，Nginx 认为失败一次就标记为不可用，但过一段时间（默认 fail_timeout=10s）又会把它拉回来重试。如果 8080 一直没起，每次重试都会导致用户请求延迟（因为要等超时）。 优化配置：max_fails 和 fail_timeout 1 2 3 4 5 6 7 8 9 10 upstream myserver { # max_fails: 允许失败的最大次数（默认1次）。超过这个次数，认为服务器挂了。 # fail_timeout: 在多少秒内统计失败次数；以及服务器挂掉后，暂停多久不再分发请求。 server 192.168.200.130:8080 max_fails=3 fail_timeout=30s; server 192.168.200.130:8081 max_fails=3 fail_timeout=30s; # backup: 备用服务器。只有当所有非 backup 服务器都挂了，才启用它。 # server 192.168.200.130:8082 backup; } 效果：如果 8080 连续失败 3 次，Nginx 会在接下来的 30 秒内彻底不理它，直接全部分发给 8081，避免用户感知到延迟。30 秒后，Nginx 会再试探性地发一个请求给 8080，如果成功了，就恢复负载均衡。\n💡 进阶提示：开源版 Nginx 不支持主动式健康检查（即 Nginx 定期主动发心跳包探测）。它只能被动检测（用户请求失败了才知道）。如果需要主动探测（如每秒 ping 一次），需要购买 Nginx Plus 商业版，或者使用第三方模块（如 nginx_upstream_check_module），但在 Kubernetes 环境中通常由 K8s 负责健康检查。\nNginx 动静分离 概述 Nginx 动静分离简单来说就是把==动态跟静态请求分开==，不能理解成只是单纯的把动态页面和静态页面物理分离。严格意义上说应该是动态请求跟静态请求分开，可以理解成使用 Nginx 处理静态页面，Tomcat 处理动态页面。动静分离从目前实现角度来讲大致分为两种，\n一种是纯粹把静态文件独立成单独的域名，放在独立的服务器上，也是目前主流推崇的方案； 另外一种方法就是动态跟静态文件混合在一起发布，通过 nginx 来分开。 通过 location 指定不同的后缀名实现不同的请求转发。通过 expires 参数设置，可以设置浏览器缓存过期时间，减少与服务器之前的请求和流量。具体 Expires 定义：是给一个资源设定一个过期时间，也就是说无需去服务端验证，直接通过浏览器自身确认是否过期即可，所以不会产生额外的流量。此种方法非常适合不经常变动的资源。（如果经常更新的文件，不建议使用 Expires 来缓存），我这里设置 3d，表示在这 3 天之内访问这个 URL，发送一个请求，比对服务器该文件最后更新时间没有变化，则不会从服务器抓取，返回状态码304，如果有修改，则直接从服务器重新下载，返回状态码 200。\n准备工作 创建文件夹，结构如下\nhtml\n创建 /data/html/a.html 1 \u0026lt;h1\u0026gt;TEST\u0026lt;/h1\u0026gt; images\n随便复制一张图片过来放在 /data/images/ 目录下\n修改 nginx.conf 文件 核心配置指令的深度辨析：root vs alias root，这是最常用的，但在动静分离中，alias 往往更灵活。\n1. root (拼接模式) 逻辑：完整路径 = root 指定路径 + location 匹配到的 URI\n你的例子：\n1 2 3 location /html/ { root /data; } 访问：http://ip/html/a.html 映射：/data + /html/a.html = /data/html/a.html 特点：URL 中的路径必须存在于文件系统中。 2. alias (替换模式) —— 动静分离推荐 逻辑：完整路径 = alias 指定路径 (location 匹配到的部分被替换掉)\n场景：假设你的静态资源散落在服务器各处，或者你想让 URL 看起来更简洁。\n1 2 3 location /static/ { alias /opt/resources/files/; # 注意：alias 结尾建议加斜杠 } 访问：http://ip/static/logo.png 映射：/opt/resources/files/logo.png (/static/ 被丢弃了) 优势：文件系统结构可以和 URL 结构完全解耦。 💡 最佳实践建议： 如果目录结构一致（如 /data/images 对应 /images），用 root 效率高一点； 如果目录结构不一致（如 /var/www/assets 对应 /img），必须用 alias。\n缓存策略进阶：expires vs Cache-Control 1. 不常变动的资源（版本化文件名） 对于 jquery-3.6.0.js, logo.v1.png 这种带版本号的文件，一旦发布几乎不改。\n策略：永久缓存。\n配置：\n1 2 3 4 location ~* \\.(jpg|jpeg|png|gif|ico|css|js)$ { expires max; # 设置为最大时间（通常是 2037 年） add_header Cache-Control \u0026#34;public, immutable\u0026#34;; # 告诉浏览器：别问了，这文件永远不变，直接用本地的！ } 效果：用户第一次访问后，后续几天甚至几个月都不会再向服务器发送任何请求（连 304 验证都没有），极大节省带宽。\n2. 经常变动的资源（HTML 入口文件） 对于 index.html，它可能经常引用新的 JS/CSS 版本。\n策略：不缓存或短时间缓存。\n配置：\n1 2 3 4 location / { expires -1; # 禁止缓存 add_header Cache-Control \u0026#34;no-cache, no-store, must-revalidate\u0026#34;; } 效果：每次访问都重新下载 HTML，确保用户能获取到最新的资源链接（从而加载新的带版本的 JS/CSS）。\n3. 关于 304 状态码的补充 你提到的“比对最后更新时间”是 Last-Modified 机制。\n流程：浏览器发送 If-Modified-Since -\u0026gt; 服务器比对时间 -\u0026gt; 没变返回 304 Not Modified (无 body) -\u0026gt; 变了返回 200 + 新内容。 更强机制 ETag：Nginx 默认开启 etag on。它通过计算文件内容的哈希值来比对，比时间戳更精准（防止文件内容没变但时间戳变了导致的无效下载）。 性能优化：为什么 Nginx 处理静态这么快？ 除了“不用转发给 Tomcat”，Nginx 内部还有两个黑科技加速静态文件传输，建议在 http 块中开启：\n1. sendfile on (零拷贝技术) 传统方式：磁盘 -\u0026gt; 内核缓冲区 -\u0026gt; 用户缓冲区 (Nginx) -\u0026gt; 内核缓冲区 (Socket) -\u0026gt; 网卡。涉及多次 CPU 拷贝和上下文切换。\nNginx 方式：磁盘 -\u0026gt; 内核缓冲区 -\u0026gt; 网卡。\n配置：\n1 2 sendfile on; tcp_nopush on; # 配合 sendfile，积攒足够数据包再发送，减少网络包数量 2. open_file_cache (文件描述符缓存) 原理：Nginx 会缓存已打开文件的描述符、文件大小、修改时间等信息。\n作用：当高并发访问同一批静态文件时，不需要每次都去磁盘 stat 文件属性，直接从内存读取，极大降低磁盘 I/O。\n配置示例：\n1 2 3 4 5 http { open_file_cache max=10000 inactive=20s; # 最多缓存 1 万个文件，20 秒没访问则清除 open_file_cache_valid 30s; # 每隔 30 秒检查一次文件是否被修改 open_file_cache_min_uses 2; # 最少被访问 2 次才放入缓存 } 动静分离的完整实战配置模板 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 server { listen 80; server_name 192.168.200.130; # --- 1. 动态请求转发给 Tomcat (或其他后端) --- location / { # 如果没有匹配到下面的静态规则，就走到这里 proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # --- 2. 静态资源：图片/视频 (大文件，长期缓存) --- # 使用正则匹配后缀名 location ~* \\.(jpg|jpeg|png|gif|mp4|swf|flv|wmv|avi)$ { root /data; # 开启 autoindex 方便调试，生产环境建议关闭 (off) 以防泄露目录结构 autoindex off; expires 30d; # 缓存 30 天 add_header Cache-Control \u0026#34;public\u0026#34;; # 防盗链 (可选)：只允许自己的域名访问图片 valid_referers none blocked server_names *.example.com example.com; if ($invalid_referer) { return 403; } } # --- 3. 静态资源：CSS/JS (小文件，版本控制，永久缓存) --- location ~* \\.(css|js)$ { root /data; expires max; # 永久缓存 add_header Cache-Control \u0026#34;public, immutable\u0026#34;; # 关键：强制浏览器不使用验证 } # --- 4. 特殊目录：使用 alias 映射 --- # 比如上传的文件在 /opt/uploads，但希望 URL 是 /files/... location /files/ { alias /opt/uploads/; expires 1d; } } 测试 访问 http://hong/html/a.html\n访问 http://192.168.200.130/images/\n==记得后面有一个 /，一定不能漏了==\nNginx 高可用 需要两台 Nginx 服务器 需要 keepalived 需要虚拟 ip 安装 keepalived yum install keepalived -y 进行安装\n可能出现已下错误\n1 2 3 4 5 6 7 8 9 10 11 12 13 错误：软件包：1:net-snmp-agent-libs-5.7.2-49.el7_9.1.x86_64 (updates) 需要：libmysqlclient.so.18()(64bit) 错误：软件包：2:postfix-2.10.1-9.el7.x86_64 (@anaconda) 需要：libmysqlclient.so.18(libmysqlclient_18)(64bit) 错误：软件包：2:postfix-2.10.1-9.el7.x86_64 (@anaconda) 需要：libmysqlclient.so.18()(64bit) 错误：软件包：1:net-snmp-agent-libs-5.7.2-49.el7_9.1.x86_64 (updates) 需要：libmysqlclient.so.18(libmysqlclient_18)(64bit) 您可以尝试添加 --skip-broken 选项来解决该问题 ** 发现 3 个已存在的 RPM 数据库问题， \u0026#39;yum check\u0026#39; 输出如下： libkkc-0.3.1-9.el7.x86_64 有缺少的需求 libmarisa.so.0()(64bit) 2:postfix-2.10.1-9.el7.x86_64 有缺少的需求 libmysqlclient.so.18()(64bit) 2:postfix-2.10.1-9.el7.x86_64 有缺少的需求 libmysqlclient.so.18(libmysqlclient_18)(64bit) 解决方案\nwget https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-community-libs-compat-5.7.25-1.el7.x86_64.rpm\nrpm -ivh mysql-community-libs-compat-5.7.25-1.el7.x86_64.rpm\n在执行 yum install keepalived -y\n准备两台 Nginx 克隆一份 Linux 系统即可。记得修改 IP 地址。\n主节点（192.168.17.129）配置： /etc/keepalived/keepalived.conf\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 global_defs { # 故障通知邮箱列表（多个邮箱用空格或换行分隔） notification_email { acassen@firewall.loc failover@firewall.loc sysadmin@firewall.loc } # 发件人邮箱地址 notification_email_from Alexandre.Cassen@firewall.loc # SMTP 服务器地址（用于发送邮件告警） smtp_server 192.168.200.130 # SMTP 连接超时时间（秒） smtp_connect_timeout 30 # 路由器 ID，用于标识本台机器（主备建议不同，方便日志排查） router_id nginx_master } # 定义健康检查脚本 vrrp_script chk_http_port { # 执行脚本的路径（必须存在且可执行） script \u0026#34;/usr/local/src/nginx_check.sh\u0026#34; # 每隔 2 秒执行一次检查 interval 2 # 如果检查失败，优先级降低 2（原优先级 - weight） weight 2 # 可选：连续失败多少次才判定为失败（默认 3） fall 3 # 可选：连续成功多少次才恢复（默认 3） rise 2 } # VRRP 实例配置 vrrp_instance VI_1 { # 主节点状态设为 MASTER state MASTER # 绑定网卡名称（请用 \u0026#39;ip a\u0026#39; 命令确认实际网卡名，如 ens33, eth0 等） interface ens33 # 虚拟路由器 ID，主备必须一致（范围 1-255） virtual_router_id 51 # 优先级：主节点设高值（如 100），备节点设低值（如 90） priority 100 # VRRP 通告间隔（秒） advert_int 1 # 认证配置（主备必须一致） authentication { auth_type PASS # 简单密码认证 auth_pass 1111 # 密码（最多8字符） } # 关联健康检查脚本 track_script { chk_http_port } # 虚拟 IP 地址（即对外服务的 VIP） virtual_ipaddress { 192.168.17.50/24 dev ens33 label ens33:0 # 格式：VIP/子网掩码 dev 网卡名 label 别名 # /24 表示子网掩码 255.255.255.0，根据你的网络调整 } } 备节点（192.168.17.131）配置 /etc/keepalived/keepalived.conf 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 global_defs { notification_email { acassen@firewall.loc failover@firewall.loc sysadmin@firewall.loc } notification_email_from Alexandre.Cassen@firewall.loc smtp_server 192.168.200.130 smtp_connect_timeout 30 # 备节点 router_id 不同，便于区分 router_id nginx_backup } vrrp_script chk_http_port { script \u0026#34;/usr/local/src/nginx_check.sh\u0026#34; interval 2 weight 2 fall 3 rise 2 } vrrp_instance VI_1 { # 备节点状态设为 BACKUP state BACKUP interface ens33 virtual_router_id 51 # 必须与主节点一致 priority 90 # 优先级低于主节点 advert_int 1 authentication { auth_type PASS auth_pass 1111 # 必须与主节点一致 } track_script { chk_http_port } virtual_ipaddress { 192.168.17.50/24 dev ens33 label ens33:0 } } 在 /usr/local/src 添加检测脚本 脚本名称 nginx_check.sh\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #!/bin/bash # Nginx 健康检查脚本 # 功能：检测 nginx 进程是否存在，若不存在则尝试启动；若启动失败则停止 keepalived，触发 VIP 漂移 # 统计 nginx 进程数量（注意使用英文短横线 - ） A=$(ps -C nginx --no-header | wc -l) # 如果没有 nginx 进程 if [ $A -eq 0 ]; then # 尝试启动 nginx（根据你的实际安装路径调整） /usr/local/nginx/sbin/nginx # 等待 2 秒让进程启动 sleep 2 # 再次检查是否启动成功 if [ $(ps -C nginx --no-header | wc -l) -eq 0 ]; then # 如果仍失败，停止 keepalived，释放 VIP killall keepalived exit 1 fi fi # 检查成功，退出码 0 exit 0 设置脚本权限： 1 2 chmod +x /usr/local/src/nginx_check.sh chown root:root /usr/local/src/nginx_check.sh 测试与验证步骤 启动 1 2 3 # 主备节点分别执行 systemctl start keepalived systemctl enable keepalived 查看 VIP 是否绑定 1 2 3 ip addr show ens33 | grep 192.168.17.50 # 或在主节点执行 ip a 模拟故障测试 方法一：停止主节点 nginx\n1 systemctl stop nginx 观察备节点是否在 6 秒内（2s*3fall）接管 VIP。\n方法二：停止主节点 keepalived\n1 systemctl stop keepalived VIP 应立即漂移到备节点。\n方法三：关闭主节点防火墙或断网\n1 iptables -I INPUT -p vrrp -j DROP 查看日志 1 2 3 tail -f /var/log/messages | grep Keepalived # 或 journalctl -u keepalived -f 测试 删除动静分离的配置，不然会有问题 cd /usr/local/nginx/sbin/ ./nginx 启动 Nginx systemctl start keepalived.service 启动 keepalived 两台 Nginx 都要启动 访问 http://192.168.200.50/ 虚拟 IP\n关闭主机的 Nginx 再次访问\nNginx 原理 master 和 worker worker 如何进行工作的 一个 master 和多个 woker 有好处 （1）可以使用 nginx –s reload 热部署，利用 nginx 进行热部署操作\n（2）每个 woker 是独立的进程，如果有其中的一个 woker 出现问题，其他 woker 独立的，继续进行争抢，实现请求过程，不会造成服务中断\n底层机制：\n零宕机升级（Zero Downtime Upgrade） 当执行 nginx -s reload 时，Master 进程会检查新配置语法。如果正确，它会启动新的 Worker 进程，并发送信号给旧的 Worker 进程，让它们在处理完当前正在处理的请求后优雅退出。 关键点：在这个过程中，监听端口（Socket）始终由 Master 持有或平滑移交，因此没有任何一个连接会被强制断开。 惊群效应（Thundering Herd）的解决 在旧版本或某些配置下，新连接到来时，所有休眠的 Worker 都会被唤醒争抢，只有一个能抢到，其他白忙活。 优化：现代 Nginx 默认开启了 accept_mutex on;（或在较新版本中自动优化），确保同一时刻只有一个 Worker 去接受新连接，极大降低了上下文切换开销。 设置多少个 woker 合适 worker 数和服务器的 cpu 数相等是最为适宜的\n原因\nNginx 是多进程单线程\n模型（每个 Worker 内部是单线程事件驱动）。\n如果设置过多：会导致频繁的 CPU 上下文切换（Context Switch），反而降低性能。 如果设置过少：无法充分利用多核 CPU 的并行处理能力。 特殊情况\n如果服务器还运行了其他重负载应用（如 Java/Python），可以适当减少 Nginx 的 worker 数（例如 auto 或 核数 - 1）。 配置写法推荐：worker_processes auto;（Nginx 会自动检测并设置为 CPU 核数，最省心）。 连接数 worker_connection 发送请求，占用了 woker 的几个连接数？\n答案：2 或者 4 个\nnginx 有一个 master，有四个 woker，每个 woker 支持最大的连接数 1024，支持的最大并发数是多少？\n普通的静态访问最大并发数是： worker_connections * worker_processes / 2，\n静态资源场景（分母为 2）： 客户端发起请求 -\u0026gt; 建立连接（占用 1 个连接）。 Nginx 读取文件 -\u0026gt; 发送给客户端 -\u0026gt; 关闭连接（占用 1 个连接，或者是保持长连接但逻辑上算一次交互）。 实际上，对于短连接，通常理解为：1 个连接处理 1 个请求。但在高并发估算中，考虑到握手和挥手过程，以及部分长连接复用，经验值常除以 2 作为保守估计（或者理解为：每个请求需要“接收”和“发送”两个阶段的操作资源）。 更精准的理解：如果是 keepalive 开启，一个连接可以处理多个请求。公式 worker_connections * worker_processes / 2 是一个保守的安全阈值，防止内存耗尽。 而如果是 HTTP 作 为反向代理来说，最大并发数量应该是 worker_connections * worker_processes / 4。\n客户端 \u0026lt;-\u0026gt; Nginx（占用 1 个连接）。 Nginx \u0026lt;-\u0026gt; 后端服务器（占用 1 个连接）。 双向占用：处理一个用户请求，Nginx 需要维持至少 2 个网络连接（上游 + 下游）。 再加上握手、超时等待、后端响应慢导致的连接堆积，资源消耗翻倍。 所以：总连接数 / 2 (双向) / 2 (安全冗余) = / 4。 Nginx HTTPS / SSL 终结 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 server { listen 443 ssl http2; server_name www.example.com; ssl_certificate /etc/nginx/ssl/example.com.crt; ssl_certificate_key /etc/nginx/ssl/example.com.key; # 推荐的安全协议和加密套件 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; location / { proxy_pass http://backend_servers; proxy_set_header X-Forwarded-Proto $scheme; # 告诉后端原始协议是 HTTPS proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # HTTP 自动跳转 HTTPS listen 80; return 301 https://$server_name$request_uri; } Nginx 访问控制与安全加固 常见用途： 限制特定 IP 或网段访问（如后台管理只允许内网访问）。 防止恶意爬虫、DDoS 攻击。 隐藏敏感路径或文件。 核心配置示例： 1. IP 白名单/黑名单 1 2 3 4 5 6 location /admin/ { allow 192.168.1.0/24; # 允许内网 allow 10.0.0.5; # 允许特定 IP deny all; # 其他全部拒绝 proxy_pass http://admin_backend; } 2. 限制请求频率（防刷） 1 2 3 4 5 6 7 8 9 10 http { limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s; server { location /login/ { limit_req zone=one burst=5 nodelay; # 每秒1个请求，突发允许5个 proxy_pass http://auth_service; } } } 3. 隐藏版本号 \u0026amp; 禁用危险方法 1 2 3 4 5 6 7 http { server_tokens off; # 隐藏 Nginx 版本号 if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE)$ ) { return 405; } } 4. 禁止访问敏感文件 1 2 3 4 5 6 7 location ~ /\\.ht { deny all; } location ~* \\.(git|svn|env|bak|sql)$ { deny all; } Nginx 缓存优化 开启代理缓存 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 http { proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off; server { location /api/ { proxy_cache my_cache; proxy_cache_valid 200 302 10m; proxy_cache_valid 404 1m; proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; proxy_cache_lock on; add_header X-Cache-Status $upstream_cache_status; # 调试用 proxy_pass http://backend; } } } 静态资源强缓存（结合 expires） 1 2 3 4 5 location ~* \\.(jpg|jpeg|png|gif|ico|css|js)$ { expires 30d; add_header Cache-Control \u0026#34;public, immutable\u0026#34;; access_log off; # 关闭日志减少 IO } nginx 日志增强与监控 自定义日志格式 1 2 3 4 5 6 7 8 9 10 http { log_format main \u0026#39;$remote_addr - $remote_user [$time_local] \u0026#34;$request\u0026#34; \u0026#39; \u0026#39;$status $body_bytes_sent \u0026#34;$http_referer\u0026#34; \u0026#39; \u0026#39;\u0026#34;$http_user_agent\u0026#34; \u0026#34;$http_x_forwarded_for\u0026#34; \u0026#39; \u0026#39;rt=$request_time uct=\u0026#34;$upstream_connect_time\u0026#34; \u0026#39; \u0026#39;uht=\u0026#34;$upstream_header_time\u0026#34; urt=\u0026#34;$upstream_response_time\u0026#34; \u0026#39; \u0026#39;cs=$upstream_cache_status\u0026#39;; access_log /var/log/nginx/access.log main; } 关键指标说明： rt: 总请求耗时 uct: 连接上游耗时 uht: 等待上游响应头耗时 urt: 上游总响应耗时 cs: 缓存命中状态（HIT/MISS/BYPASS） Nginx URL 重写与重定向 301 永久重定向 1 2 3 4 5 server { listen 80; server_name example.com; return 301 https://www.example.com$request_uri; } URL 重写（伪静态） 1 2 3 location /article/ { rewrite ^/article/([0-9]+)\\.html$ /article.php?id=$1 last; } 去除尾部斜杠或添加斜杠 1 2 3 4 5 6 7 # 去除尾部斜杠 rewrite ^(.*)/$ $1 permanent; # 添加尾部斜杠（目录） if (-d $request_filename) { rewrite ^(.*)$ $1/ permanent; } Nginx 限流与熔断 按 IP 限流 1 2 3 4 5 6 limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; location /api/ { limit_req zone=api_limit burst=20 nodelay; proxy_pass http://api_backend; } 按区域限流（地理围栏） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 geo $country_code { default US; 192.168.1.0/24 CN; 10.0.0.0/8 RU; } map $country_code $deny_country { default 0; RU 1; KP 1; } server { if ($deny_country) { return 403; } } ","permalink":"https://ktzxy.top/posts/mutl9oasar/","summary":"Nginx学习笔记","title":"Nginx"},{"content":"Python 教程 (2) 这个教程根据廖雪峰的Python3教程所写. 从模块开始的高级部分.\n[TOC]\n模块 在计算机程序的开发过程中，随着程序代码越写越多，在一个文件里代码就会越来越长，越来越不容易维护。\n为了编写可维护的代码，我们把很多函数分组，分别放到不同的文件里，这样，每个文件包含的代码就相对较少，很多编程语言都采用这种组织代码的方式。在Python中，一个.py文件就称之为一个模块（Module）。\n使用模块还可以避免函数名和变量名冲突。相同名字的函数和变量完全可以分别存在不同的模块中，因此，我们自己在编写模块时，不必考虑名字会与其他模块冲突。但是也要注意，尽量不要与内置函数名字冲突。点这里查看Python的所有内置函数。\n为了避免模块名冲突，Python又引入了按目录来组织模块的方法，称为包（Package）。引入了包以后，只要顶层的包名不与别人冲突，那所有模块都不会与别人冲突。\n请注意，每一个包目录下面都会有一个__init__.py的文件，这个文件是必须存在的，否则，Python就把这个目录当成普通目录，而不是一个包。init.py可以是空文件，也可以有Python代码，因为__init__.py本身就是一个模块，而它的模块名就是顶层包名。\n常用内建模块 常用第三方模块 使用模块 Python本身就内置了很多非常有用的模块，只要安装完毕，这些模块就可以立刻使用。\n我们以内建的sys模块为例，编写一个hello的模块：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #!/usr/bin/env python3 # -*- coding: utf-8 -*- \u0026#39; a test module \u0026#39; __author__ = \u0026#39;whzecomjm\u0026#39; import sys def test(): args = sys.argv if len(args)==1: print(\u0026#39;Hello, world!\u0026#39;) elif len(args)==2: print(\u0026#39;Hello, %s!\u0026#39; % args[1]) else: print(\u0026#39;Too many arguments!\u0026#39;) if __name__==\u0026#39;__main__\u0026#39;: test() Too many arguments! 第一行注释为了兼容 Unix 上运行, 第二行是编码. 第四行是模块文档注释, 任何模块代码的第一个字符串都被视为模块的文档注释； 第6行使用__author__变量把作者写进去. 当我们在命令行运行hello模块文件时，Python解释器把一个特殊变量__name__置为__main__，而如果在其他地方导入该hello模块时，if判断将失败，因此，这种if测试可以让一个模块通过命令行运行时执行一些额外的代码，最常见的就是运行测试。\n在一个模块中，我们可能会定义很多函数和变量，但有的函数和变量我们希望给别人使用，有的函数和变量我们希望仅仅在模块内部使用。在Python中，是通过_前缀来实现的.\n类似_xxx和__xxx这样的函数或变量就是非公开的（private），不应该被直接引用；之所以我们说，private函数和变量“不应该”被直接引用，而不是“不能”被直接引用，是因为Python并没有一种方法可以完全限制访问private函数或变量，但是，从编程习惯上不应该引用private函数或变量。\n安装第三方模块 在Python中，安装第三方模块，是通过包管理工具pip完成的。注意：Mac或Linux上有可能并存Python 3.x和Python 2.x，因此对应的pip命令是pip3。(但是windows如果只安装py3只需要输入命令 pip)\n在使用Python时，我们经常需要用到很多第三方库，例如，上面提到的Pillow，以及MySQL驱动程序，Web框架Flask，科学计算Numpy等。用pip一个一个安装费时费力，还需要考虑兼容性。我们推荐直接使用 Anaconda，这是一个基于Python的数据处理和科学计算平台，它已经内置了许多非常有用的第三方库，我们装上Anaconda，就相当于把数十个第三方模块自动安装好了，非常简单易用。\n下载后直接安装，Anaconda会把系统Path中的python指向自己自带的Python，并且，Anaconda安装的第三方模块会安装在Anaconda自己的路径下，不影响系统已安装的Python目录。\n面向对象编程 面向对象编程——Object Oriented Programming，简称OOP，是一种程序设计思想。OOP把对象作为程序的基本单元，一个对象包含了数据和操作数据的函数。\n面向过程的程序设计把计算机程序视为一系列的命令集合，即一组函数的顺序执行。为了简化程序设计，面向过程把函数继续切分为子函数，即把大块函数通过切割成小块函数来降低系统的复杂度。\n在Python中，所有数据类型都可以视为对象，当然也可以自定义对象。自定义的对象数据类型就是面向对象中的类（Class）的概念。\n如果采用面向对象的程序设计思想，我们首选思考的不是程序的执行流程，而是Student这种数据类型应该被视为一个对象，这个对象拥有name和score这两个属性（Property）。如果要打印一个学生的成绩，首先必须创建出这个学生对应的对象，然后，给对象发一个print_score消息，让对象自己把自己的数据打印出来。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Student(object): # 创建类对象 def __init__(self, name, score): # 初始化对象, 注意每次构造器都得写个self self.name = name self.score = score def print_score(self): # 功能 print(\u0026#39;%s: %s\u0026#39; % (self.name, self.score)) bart = Student(\u0026#39;Bart Simpson\u0026#39;, 59) # 实例化 lisa = Student(\u0026#39;Lisa Simpson\u0026#39;, 87) bart.print_score() lisa.print_score() Bart Simpson: 59 Lisa Simpson: 87 总结：面向过程以步骤分类，面向对象以功能(属性)分类。面向对象功能上的统一保证了其可扩展性。\n类和实例 面向对象最重要的概念就是类（Class）和实例（Instance），必须牢记类是抽象的模板，比如Student类，而实例是根据类创建出来的一个个具体的“对象”，每个对象都拥有相同的方法，但各自的数据可能不同。\n由于类可以起到模板的作用，因此，可以在创建实例的时候，把一些我们认为必须绑定的属性强制填写进去。通过定义一个特殊的__init__方法，在创建实例的时候，就把name，score等属性绑上去.\n注意到__init__方法的第一个参数永远是self，表示创建的实例本身，因此，在__init__方法内部，就可以把各种属性绑定到self，因为self就指向创建的实例本身。\n和普通的函数相比，在类中定义的函数只有一点不同，就是第一个参数永远是实例变量self，并且，调用时，不用传递该参数。\n数据封装 但是，既然Student实例本身就拥有这些数据，要访问这些数据，就没有必要从外面的函数去访问，可以直接在Student类的内部定义访问数据的函数，这样，就把“数据”给封装起来了。\n1 2 3 4 5 6 7 8 class Student(object): def __init__(self, name, score): self.name = name self.score = score def print_score(self): print(\u0026#39;%s: %s\u0026#39; % (self.name, self.score)) 要定义一个方法，除了第一个参数是self外，其他和普通函数一样。要调用一个方法，只需要在实例变量上直接调用，除了self不用传递，其他参数正常传入.\n访问限制 在Class内部，可以有属性和方法，而外部代码可以通过直接调用实例变量的方法来操作数据，这样，就隐藏了内部的复杂逻辑。\n如果要让内部属性不被外部访问，可以把属性的名称前加上两个下划线__，在Python中，实例的变量名如果以__开头，就变成了一个私有变量（private），只有内部可以访问，外部不能访问.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Student(object): def __init__(self, name, gender): self.name = name self.__gender = gender def get_gender(self): return self.__gender def set_gender(self, gender): if gender == \u0026#39;male\u0026#39; or \u0026#39;female\u0026#39;: self.__gender = gender else: raise ValueError(\u0026#39;bad gender!\u0026#39;) bart = Student(\u0026#39;Bart\u0026#39;, \u0026#39;male\u0026#39;) tom = Student(\u0026#39;Tom\u0026#39;,\u0026#39;\u0026#39;) bart.get_gender() tom.set_gender(\u0026#39;female\u0026#39;) tom.get_gender() 'female' 继承和多态 在OOP程序设计中，当我们定义一个class的时候，可以从某个现有的class继承，新的class称为子类（Subclass），而被继承的class称为基类、父类或超类（Base class、Super class）。\n1 2 3 4 5 6 7 8 9 10 class Animal(object): def run(self): print(\u0026#39;Animal is running...\u0026#39;) class Dog(Animal): def run(self): print(\u0026#39;Dog is running...\u0026#39;) class Cat(Animal): def run(self): print(\u0026#39;Cat is running...\u0026#39;) 当子类和父类都存在相同的run()方法时，我们说，子类的run()覆盖了父类的run()，在代码运行的时候，总是会调用子类的run()。这样，我们就获得了继承的另一个好处：多态。\n所以，在继承关系中，如果一个实例的数据类型是某个子类，那它的数据类型也可以被看做是父类。但是，反过来就不行.\n获取对象信息 我们来判断对象类型，使用type()函数： 基本类型都可以用type()判断.\n使用isinstance() 对于class的继承关系来说，使用type()就很不方便。我们要判断class的类型，可以使用isinstance()函数。\n如果要获得一个对象的所有属性和方法，可以使用dir()函数，它返回一个包含字符串的list.\n面向对象高级编程 更多面向对象高级编程参见这里.\n文件读写 读写文件是最常见的IO操作。Python内置了读写文件的函数，用法和C是兼容的。\n读文件 要以读文件的模式打开一个文件对象，使用Python内置的open()函数，传入文件名和标示符：\n1 \u0026gt;\u0026gt;\u0026gt; f= open(\u0026#39;/Users/michael/test.txt\u0026#39;, \u0026#39;r\u0026#39;) 标示符\u0026rsquo;r\u0026rsquo;表示读，这样，我们就成功地打开了一个文件。\n如果文件打开成功，接下来，调用read()方法可以一次读取文件的全部内容，Python把内容读到内存，用一个str对象表示：\n1 2 \u0026gt;\u0026gt;\u0026gt; f.read() \u0026#39;Hello, world!\u0026#39; 最后一步是调用close()方法关闭文件。文件使用完毕后必须关闭，因为文件对象会占用操作系统的资源，并且操作系统同一时间能打开的文件数量也是有限的：\n1 \u0026gt;\u0026gt;\u0026gt; f.close() 如果文件很小，read()一次性读取最方便；如果不能确定文件大小，反复调用read(size)比较保险；如果是配置文件，调用readlines()最方便：\n1 2 for line in f.readlines(): print(line.strip()) # 把末尾的\u0026#39;\\n\u0026#39;删掉 二进制文件 前面讲的默认都是读取文本文件，并且是UTF-8编码的文本文件。要读取二进制文件，比如图片、视频等等，用\u0026rsquo;rb\u0026rsquo;模式打开文件即可.\n要读取非UTF-8编码的文本文件，需要给open()函数传入encoding参数，例如，读取GBK编码的文件：\n1 2 3 \u0026gt;\u0026gt;\u0026gt; f = open(\u0026#39;/Users/michael/gbk.txt\u0026#39;, \u0026#39;r\u0026#39;, encoding=\u0026#39;gbk\u0026#39;) \u0026gt;\u0026gt;\u0026gt; f.read() \u0026#39;测试\u0026#39; 写文件 写文件和读文件是一样的，唯一区别是调用open()函数时，传入标识符\u0026rsquo;w\u0026rsquo;或者\u0026rsquo;wb\u0026rsquo;表示写文本文件或写二进制文件：\n1 2 3 \u0026gt;\u0026gt;\u0026gt; f = open(\u0026#39;/Users/michael/test.txt\u0026#39;, \u0026#39;w\u0026#39;) \u0026gt;\u0026gt;\u0026gt; f.write(\u0026#39;Hello, world!\u0026#39;) \u0026gt;\u0026gt;\u0026gt; f.close() 用with语句来得保险：\n1 2 with open(\u0026#39;/Users/michael/test.txt\u0026#39;, \u0026#39;w\u0026#39;) as f: f.write(\u0026#39;Hello, world!\u0026#39;) 细心的童鞋会发现，以\u0026rsquo;w\u0026rsquo;模式写入文件时，如果文件已存在，会直接覆盖（相当于删掉后新写入一个文件）。如果我们希望追加到文件末尾怎么办？可以传入\u0026rsquo;a\u0026rsquo;以追加（append）模式写入。\n1 2 3 4 5 todo = r\u0026#39;C:\\Users\\whzec\\Desktop\\to-do.md\u0026#39; # \u0026#39;r\u0026#39;是防止字符转义的,因为有\\t. with open (todo, \u0026#39;r\u0026#39;) as f: s = f.read() print(s) # To-D0 List - [v] 犹太课程 2门可用其他系的课代替 - 给下学期课老师写邮件 - Javascripts python - 焦点效应与透明度错觉 习得性无助 - gsm73 p9. - 整理所有 quantization 资料 - 整理代数讨论班SUSTC - PI for ring theory - 邓肯图 - 基本的代数 各个定义，我们需要重新整理一下wiki的书签 - 写group algebra ### 课程表 - (v)88891 Colloquium Algebra #### 第一学期 - 888100 Symbolic dynamics 周日 没有考试 - 888580 Topics in Combinatorics 10--1300 周日(v) #### 第二学期 - 88778 Networks Science (周三 11-14) - (v)887810-01,02 Introduction to Artificial Intelligence (周四 14-16 16-17) - 88826\tDifferential geometry 2 (要考试 周三晚上5-8) - (v)88802 Walks on ordinals 周日10-13 - 88854 lie group and algebra 周二11-14 ​ ​\n操作文件和目录 Python内置的os模块也可以直接调用操作系统提供的接口函数。\n打开Python交互式命令行，我们来看看如何使用os模块的基本功能：\n1 2 3 \u0026gt;\u0026gt;\u0026gt; import os \u0026gt;\u0026gt;\u0026gt; os.name # 操作系统类型 \u0026#39;posix\u0026#39; 如果是posix，说明系统是Linux、Unix或Mac OS X，如果是nt，就是Windows系统。\n1 2 import os os.name 'nt' 序列化 我们把变量从内存中变成可存储或传输的过程称之为序列化，在Python中叫pickling，在其他语言中也被称之为serialization，marshalling，flattening等等，都是一个意思。\n序列化之后，就可以把序列化后的内容写入磁盘，或者通过网络传输到别的机器上。\n反过来，把变量内容从序列化的对象重新读到内存里称之为反序列化，即unpickling。\nJSON 如果我们要在不同的编程语言之间传递对象，就必须把对象序列化为标准格式，比如XML，但更好的方法是序列化为JSON，因为JSON表示出来就是一个字符串，可以被所有语言读取，也可以方便地存储到磁盘或者通过网络传输。JSON不仅是标准格式，并且比XML更快，而且可以直接在Web页面中读取，非常方便。\nPython内置的json模块提供了非常完善的Python对象到JSON格式的转换。我们先看看如何把Python对象变成一个JSON:\n1 2 3 4 \u0026gt;\u0026gt;\u0026gt; import json \u0026gt;\u0026gt;\u0026gt; d = dict(name=\u0026#39;Bob\u0026#39;, age=20, score=88) \u0026gt;\u0026gt;\u0026gt; json.dumps(d) \u0026#39;{\u0026#34;age\u0026#34;: 20, \u0026#34;score\u0026#34;: 88, \u0026#34;name\u0026#34;: \u0026#34;Bob\u0026#34;}\u0026#39; 要把JSON反序列化为Python对象，用loads()或者对应的load()方法，前者把JSON的字符串反序列化，后者从file-like Object中读取字符串并反序列化：\n1 2 3 \u0026gt;\u0026gt;\u0026gt; json_str = \u0026#39;{\u0026#34;age\u0026#34;: 20, \u0026#34;score\u0026#34;: 88, \u0026#34;name\u0026#34;: \u0026#34;Bob\u0026#34;}\u0026#39; \u0026gt;\u0026gt;\u0026gt; json.loads(json_str) {\u0026#39;age\u0026#39;: 20, \u0026#39;score\u0026#39;: 88, \u0026#39;name\u0026#39;: \u0026#39;Bob\u0026#39;} 多进程和多线程 python 在 MacOS 和 linux 支持 fork(), 所以可以更好的学习多进程和多线程. 具体内容参见这里.\n正则表达式 在正则表达式中，如果直接给出字符，就是精确匹配。用\\d可以匹配一个数字(digits)，\\w可以匹配一个字母或数字(words), .匹配任意一个字符, *匹配任意个字符(包括0个), + 表示至少一个, ?表示0或者1个, {n} 表示n 个字符, {n,m} 表示 n到 m 个字符, 来看一个例子:\n1 \\d{3}\\s+\\d{3,8} 我们来从左到右解读一下：\n\\d{3}表示匹配3个数字，例如'010\u0026rsquo;；\n\\s可以匹配一个空格（也包括Tab等空白符），所以\\s+表示至少有一个空格，例如匹配\u0026rsquo; \u0026lsquo;，\u0026rsquo; \u0026lsquo;等；\n\\d{3,8}表示3-8个数字，例如'1234567\u0026rsquo;。\n综合起来，上面的正则表达式可以匹配以任意个空格隔开的带区号的电话号码。\n进阶 要做更精确地匹配，可以用[]表示范围，比如：\n[0-9a-zA-Z\\_]可以匹配一个数字、字母或者下划线；\n[0-9a-zA-Z\\_]+可以匹配至少由一个数字、字母或者下划线组成的字符串，比如\u0026rsquo;a100\u0026rsquo;，\u0026lsquo;0_Z\u0026rsquo;，\u0026lsquo;Py3000\u0026rsquo;等等；\n[a-zA-Z\\_][0-9a-zA-Z\\_]*可以匹配由字母或下划线开头，后接任意个由一个数字、字母或者下划线组成的字符串，也就是Python合法的变量；\n[a-zA-Z\\_][0-9a-zA-Z\\_]{0, 19}更精确地限制了变量的长度是1-20个字符（前面1个字符+后面最多19个字符）。\nA|B可以匹配A或B，所以(P|p)ython可以匹配\u0026rsquo;Python\u0026rsquo;或者\u0026rsquo;python\u0026rsquo;。\n^表示行的开头，^\\d表示必须以数字开头。\n$表示行的结束，\\d$表示必须以数字结束。\n你可能注意到了，py也可以匹配\u0026rsquo;python\u0026rsquo;，但是加上^py$就变成了整行匹配，就只能匹配\u0026rsquo;py\u0026rsquo;了。\nre模块 Python提供re模块，包含所有正则表达式的功能。由于Python的字符串本身也用\\转义，所以要特别注意. 因此我们强烈建议使用Python的r前缀，就不用考虑转义的问题了：\n1 2 3 s = r\u0026#39;ABC\\-001\u0026#39; # Python的字符串 # 对应的正则表达式字符串不变： # \u0026#39;ABC\\-001\u0026#39; 先看看如何判断正则表达式是否匹配：\n1 2 3 4 5 \u0026gt;\u0026gt;\u0026gt; import re \u0026gt;\u0026gt;\u0026gt; re.match(r\u0026#39;^\\d{3}\\-\\d{3,8}$\u0026#39;, \u0026#39;010-12345\u0026#39;) \u0026lt;_sre.SRE_Match object; span=(0, 9), match=\u0026#39;010-12345\u0026#39;\u0026gt; \u0026gt;\u0026gt;\u0026gt; re.match(r\u0026#39;^\\d{3}\\-\\d{3,8}$\u0026#39;, \u0026#39;010 12345\u0026#39;) \u0026gt;\u0026gt;\u0026gt; match()方法判断是否匹配，如果匹配成功，返回一个Match对象，否则返回None。常见的判断方法就是：\n1 2 3 4 5 test = \u0026#39;用户输入的字符串\u0026#39; if re.match(r\u0026#39;正则表达式\u0026#39;, test): print(\u0026#39;ok\u0026#39;) else: print(\u0026#39;failed\u0026#39;) 切分字符串 用正则表达式切分字符串比用固定的字符更灵活，请看正常的切分代码：\n1 2 \u0026gt;\u0026gt;\u0026gt; \u0026#39;a b c\u0026#39;.split(\u0026#39; \u0026#39;) [\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;\u0026#39;, \u0026#39;\u0026#39;, \u0026#39;c\u0026#39;] 嗯，无法识别连续的空格，用正则表达式试试：\n1 2 \u0026gt;\u0026gt;\u0026gt; re.split(r\u0026#39;\\s+\u0026#39;, \u0026#39;a b c\u0026#39;) [\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;] 无论多少个空格都可以正常分割。加入,试试：\n1 2 \u0026gt;\u0026gt;\u0026gt; re.split(r\u0026#39;[\\s\\,]+\u0026#39;, \u0026#39;a,b, c d\u0026#39;) [\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;d\u0026#39;] 再加入;试试：\n1 2 \u0026gt;\u0026gt;\u0026gt; re.split(r\u0026#39;[\\s\\,\\;]+\u0026#39;, \u0026#39;a,b;; c d\u0026#39;) [\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;d\u0026#39;] 如果用户输入了一组标签，下次记得用正则表达式来把不规范的输入转化成正确的数组。\n分组 除了简单地判断是否匹配之外，正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组（Group）。比如：\n^(\\d{3})-(\\d{3,8})$分别定义了两个组，可以直接从匹配的字符串中提取出区号和本地号码：\n1 2 3 4 5 6 7 8 9 \u0026gt;\u0026gt;\u0026gt; m = re.match(r\u0026#39;^(\\d{3})-(\\d{3,8})$\u0026#39;, \u0026#39;010-12345\u0026#39;) \u0026gt;\u0026gt;\u0026gt; m \u0026lt;_sre.SRE_Match object; span=(0, 9), match=\u0026#39;010-12345\u0026#39;\u0026gt; \u0026gt;\u0026gt;\u0026gt; m.group(0) \u0026#39;010-12345\u0026#39; \u0026gt;\u0026gt;\u0026gt; m.group(1) \u0026#39;010\u0026#39; \u0026gt;\u0026gt;\u0026gt; m.group(2) \u0026#39;12345\u0026#39; 如果正则表达式中定义了组，就可以在Match对象上用group()方法提取出子串来。\n注意到group(0)永远是原始字符串，group(1)、group(2)……表示第1、2、……个子串。\n贪婪匹配 最后需要特别指出的是，正则匹配默认是贪婪匹配，也就是匹配尽可能多的字符。举例如下，匹配出数字后面的0：\n1 2 \u0026gt;\u0026gt;\u0026gt; re.match(r\u0026#39;^(\\d+)(0*)$\u0026#39;, \u0026#39;102300\u0026#39;).groups() (\u0026#39;102300\u0026#39;, \u0026#39;\u0026#39;) 由于\\d+采用贪婪匹配，直接把后面的0全部匹配了，结果0*只能匹配空字符串了。\n必须让\\d+采用非贪婪匹配（也就是尽可能少匹配），才能把后面的0匹配出来，加个?就可以让\\d+采用非贪婪匹配：\n1 2 \u0026gt;\u0026gt;\u0026gt; re.match(r\u0026#39;^(\\d+?)(0*)$\u0026#39;, \u0026#39;102300\u0026#39;).groups() (\u0026#39;1023\u0026#39;, \u0026#39;00\u0026#39;) 编译 如果一个正则表达式要重复使用几千次，出于效率的考虑，我们可以预编译该正则表达式，接下来重复使用时就不需要编译这个步骤了，直接匹配：\n1 2 3 4 import re re_telephone = re.compile(r\u0026#39;^(\\d{3})-(\\d{3,8})$\u0026#39;) # 使用： re_telephone.match(\u0026#39;010-12345\u0026#39;).groups() ('010', '12345') 网络编程 TCP/IP简介 IP协议负责把数据从一台计算机通过网络发送到另一台计算机。数据被分割成一小块一小块，然后通过IP包发送出去。由于互联网链路复杂，两台计算机之间经常有多条线路，因此，路由器就负责决定如何把一个IP包转发出去。IP包的特点是按块发送，途径多个路由，但不保证能到达，也不保证顺序到达。\nIP地址实际上是一个32位整数（称为IPv4），以字符串表示的IP地址如192.168.0.1实际上是把32位整数按8位分组后的数字表示，目的是便于阅读。\nIPv6地址实际上是一个128位整数，它是目前使用的IPv4的升级版，以字符串表示类似于2001:0db8:85a3:0042:1000:8a2e:0370:7334。\nTCP协议则是建立在IP协议之上的。TCP协议负责在两台计算机之间建立可靠连接，保证数据包按顺序到达。TCP协议会通过握手建立连接，然后，对每个IP包编号，确保对方按顺序收到，如果包丢掉了，就自动重发。\n一个TCP报文除了包含要传输的数据外，还包含源IP地址和目标IP地址，源端口和目标端口。许多常用的更高级的协议都是建立在TCP协议基础上的，比如用于浏览器的HTTP协议、发送邮件的SMTP协议等。TCP是建立可靠连接，并且通信双方都可以以流的形式发送数据。相对TCP，UDP则是面向无连接的协议。\n使用UDP协议时，不需要建立连接，只需要知道对方的IP地址和端口号，就可以直接发数据包。但是，能不能到达就不知道了。\n虽然用UDP传输数据不可靠，但它的优点是和TCP比，速度快，对于不要求可靠到达的数据，就可以使用UDP协议。\nSocket是网络编程的一个抽象概念。通常我们用一个Socket表示“打开了一个网络链接”，而打开一个Socket需要知道目标计算机的IP地址和端口号，再指定协议类型即可。\nUDP 协议 TCP是建立可靠连接，并且通信双方都可以以流的形式发送数据。相对TCP，UDP则是面向无连接的协议。\n使用UDP协议时，不需要建立连接，只需要知道对方的IP地址和端口号，就可以直接发数据包。但是，能不能到达就不知道了。\n虽然用UDP传输数据不可靠，但它的优点是和TCP比，速度快，对于不要求可靠到达的数据，就可以使用UDP协议。\n电子邮件 Email的历史比Web还要久远，直到现在，Email也是互联网上应用非常广泛的服务。\n一封电子邮件的旅程就是：\n1 发件人 -\u0026gt; MUA -\u0026gt; MTA -\u0026gt; MTA -\u0026gt; 若干个MTA -\u0026gt; MDA \u0026lt;- MUA \u0026lt;- 收件人 发邮件时，MUA和MTA使用的协议就是SMTP：Simple Mail Transfer Protocol，后面的MTA到另一个MTA也是用SMTP协议。\n收邮件时，MUA和MDA使用的协议有两种：POP：Post Office Protocol，目前版本是3，俗称POP3；IMAP：Internet Message Access Protocol，目前版本是4，优点是不但能取邮件，还可以直接操作MDA上存储的邮件，比如从收件箱移到垃圾箱，等等。\nSMTP发送邮件 SMTP是发送邮件的协议，Python内置对SMTP的支持，可以发送纯文本邮件、HTML邮件以及带附件的邮件。\nPython对SMTP支持有smtplib和email两个模块，email负责构造邮件，smtplib负责发送邮件。\n首先，我们来构造一个最简单的纯文本邮件, 然后，通过SMTP发出去：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from email.mime.text import MIMEText msg = MIMEText(\u0026#39;hello, send by Python...\u0026#39;, \u0026#39;plain\u0026#39;, \u0026#39;utf-8\u0026#39;) # 输入Email地址和口令: from_addr = input(\u0026#39;From: \u0026#39;) password = input(\u0026#39;Password: \u0026#39;) # 输入收件人地址: to_addr = input(\u0026#39;To: \u0026#39;) # 输入SMTP服务器地址: smtp_server = input(\u0026#39;SMTP server: \u0026#39;) import smtplib server = smtplib.SMTP(smtp_server, 25) # SMTP协议默认端口是25 server.set_debuglevel(1) server.login(from_addr, password) server.sendmail(from_addr, [to_addr], msg.as_string()) server.quit() 使用Python的smtplib发送邮件十分简单，只要掌握了各种邮件类型的构造方法，正确设置好邮件头，就可以顺利发出。\n构造一个邮件对象就是一个Messag对象，如果构造一个MIMEText对象，就表示一个文本邮件对象，如果构造一个MIMEImage对象，就表示一个作为附件的图片，要把多个对象组合起来，就用MIMEMultipart对象，而MIMEBase可以表示任何对象。\n访问数据库 不能做快速查询，只有把数据全部读到内存中才能自己遍历，但有时候数据的大小远远超过了内存（比如蓝光电影，40GB的数据），根本无法全部读入内存。\n为了便于程序保存和读取数据，而且，能直接通过条件快速查询到指定的数据，就出现了数据库（Database）这种专门用于集中存储和查询的软件。\n数据库软件诞生的历史非常久远，早在1950年数据库就诞生了。经历了网状数据库，层次数据库，我们现在广泛使用的关系数据库是20世纪70年代基于关系模型的基础上诞生的。\nSQLite SQLite是一种嵌入式数据库，它的数据库就是一个文件。由于SQLite本身是C写的，而且体积很小，所以，经常被集成到各种应用程序中，甚至在iOS和Android的App中都可以集成。\nPython就内置了SQLite3，所以，在Python中使用SQLite，不需要安装任何东西，直接使用。\nMySQL MySQL是Web世界中使用最广泛的数据库服务器。SQLite的特点是轻量级、可嵌入，但不能承受高并发访问，适合桌面和移动应用。而MySQL是为服务器端设计的数据库，能承受高并发访问，同时占用的内存也远远大于SQLite。\n此外，MySQL内部有多种数据库引擎，最常用的引擎是支持数据库事务的InnoDB。\nWeb开发 最早的软件都是运行在大型机上的，软件使用者通过“哑终端”登陆到大型机上去运行软件。后来随着PC机的兴起，软件开始主要运行在桌面上，而数据库这样的软件运行在服务器端，这种Client/Server模式简称CS架构。\n随着互联网的兴起，人们发现，CS架构不适合Web，最大的原因是Web应用程序的修改和升级非常迅速，而CS架构需要每个客户端逐个升级桌面App，因此，Browser/Server模式开始流行，简称BS架构。\n在BS架构下，客户端只需要浏览器，应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器，获取Web页面，并把Web页面展示给用户即可。\n当然，Web页面也具有极强的交互性。由于Web页面是用HTML编写的，而HTML具备超强的表现力，并且，服务器端升级后，客户端无需任何部署就可以使用到新的版本，因此，BS架构迅速流行起来。\n今天，除了重量级的软件如Office，Photoshop等，大部分软件都以Web形式提供。比如，新浪提供的新闻、博客、微博等服务，均是Web应用。\nWeb应用开发可以说是目前软件开发中最重要的部分。Web开发也经历了好几个阶段：\n静态Web页面：由文本编辑器直接编辑并生成静态的HTML页面，如果要修改Web页面的内容，就需要再次编辑HTML源文件，早期的互联网Web页面就是静态的；\nCGI：由于静态Web页面无法与用户交互，比如用户填写了一个注册表单，静态Web页面就无法处理。要处理用户发送的动态数据，出现了Common Gateway Interface，简称CGI，用C/C++编写。\nASP/JSP/PHP：由于Web应用特点是修改频繁，用C/C++这样的低级语言非常不适合Web开发，而脚本语言由于开发效率高，与HTML结合紧密，因此，迅速取代了CGI模式。ASP是微软推出的用VBScript脚本编程的Web开发技术，而JSP用Java来编写脚本，PHP本身则是开源的脚本语言。\nMVC：为了解决直接用脚本语言嵌入HTML导致的可维护性差的问题，Web应用也引入了Model-View-Controller的模式，来简化Web开发。ASP发展为ASP.Net，JSP和PHP也有一大堆MVC框架。\n目前，Web开发技术仍在快速发展中，异步开发、新的MVVM前端技术层出不穷。\nPython的诞生历史比Web还要早，由于Python是一种解释型的脚本语言，开发效率高，所以非常适合用来做Web开发。Python有上百种Web开发框架，有很多成熟的模板技术，选择Python开发Web应用，不但开发效率高，而且运行速度快。\n异步IO 由于我们要解决的问题是CPU高速执行能力和IO设备的龟速严重不匹配，多线程和多进程只是解决这一问题的一种方法。\n另一种解决IO问题的方法是异步IO。当代码需要执行一个耗时的IO操作时，它只发出IO指令，并不等待IO结果，然后就去执行其他代码了。一段时间后，当IO返回结果时，再通知CPU进行处理。\n具体python的操作参见这里.\n","permalink":"https://ktzxy.top/posts/7qrkoa8los/","summary":"Python3Notes2","title":"Python3Notes2"},{"content":"﻿# 1.网络传输三大基石\n三大基石：URL，HTTP协议，HTML\nURL：在WWW上，每一信息资源都有统一的且在网上唯一的地址，该地址就叫URL（Uniform Resource Locator,统一资源定位符），它是WWW的统一资源定位标志，就是指网络地址。\nHTTP协议：http是一个简单的请求-响应协议，它通常运行在TCP之上。它指定了客户端可能发送给服务器什么样的消息以及得到什么样的响应。请求和响应消息的头以ASCII码形式给出；而消息内容则具有一个类似MIME的格式。这个简单模型是早期Web成功的有功之臣，因为它使得开发和部署是那么的直截了当。\nHTML：HTML称为超文本标记语言。\n2.初识HTML 什么是HTML\nHTML Hyper Text Markup Language（超文本标记语言） W3C标准\nW3C World Wide Web Consortium 成立于1994年，Web技术领域最权威和具影响力的国际中立性技术标准机构 http://www.w3.org/ http://www.chinaw3c.org/ W3C标准包括 结构化标准语言（HTML、XML） 表现标准语言（CSS） 行为标准（DOM、ECMAScript） 3.网页基本信息 网页基本信息\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 \u0026lt;!-- DOCTYPE：告诉浏览器，我们要使用什么规范 --\u0026gt; \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;!-- 这是一个注释，注释的快捷键是ctrl+shift+/--\u0026gt; \u0026lt;head\u0026gt; \u0026lt;!--页面标题--\u0026gt; \u0026lt;title\u0026gt;网页基本信息学习\u0026lt;/title\u0026gt; \u0026lt;!--设置页面的编码，防止乱码现象 利用meta标签， charset=\u0026#34;utf-8\u0026#34; 这是属性，以键值对的形式给出 k=v a=b 告诉浏览器用utf-8来解析这个html文档 --\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34; /\u0026gt;\u0026lt;!--简写--\u0026gt; \u0026lt;!--繁写形式：（了解）--\u0026gt; \u0026lt;!--\u0026lt;meta http-equiv=\u0026#34;content-type\u0026#34; content=\u0026#34;text/html;charset=utf-8\u0026#34; /\u0026gt;--\u0026gt; \u0026lt;!--页面刷新效果--\u0026gt; \u0026lt;!--\u0026lt;meta http-equiv=\u0026#34;refresh\u0026#34; content=\u0026#34;3;https://www.baidu.com\u0026#34; /\u0026gt;--\u0026gt; \u0026lt;!--页面作者--\u0026gt; \u0026lt;meta name=\u0026#34;author\u0026#34; content=\u0026#34;zy;22515@qq.com\u0026#34; /\u0026gt; \u0026lt;!--设置页面搜索的关键字--\u0026gt; \u0026lt;meta name=\u0026#34;keywords\u0026#34; content=\u0026#34;赵羽;自学;\u0026#34; /\u0026gt; \u0026lt;!--页面描述--\u0026gt; \u0026lt;meta name=\u0026#34;description\u0026#34; content=\u0026#34;赵羽学习页面\u0026#34; /\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;!-- body标签中：放入：页面展示的内容 --\u0026gt; \u0026lt;body\u0026gt; Hello，World！ \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 【1】html标签 定义 HTML 文档，这个元素我们浏览器看到后就明白这是个HTML文档了，所以你的其它元素要包裹在它里面，标签限定了文档的开始点和结束点，在它们之间是文档的头部和主体。\n【2】head标签\u0026mdash;》里面放的是页面的配置信息 head标签用于定义文档的头部，它是所有头部元素的容器。 中的元素可以引用脚本、指示浏览器在哪里找到样式表。文档的头部描述了文档的各种属性和信息，包括文档的标题、在 Web 中的位置以及和其他文档的关系等。绝大多数文档头部包含的数据都不会真正作为内容显示给读者。 下面这些标签可用在 head 部分：\n1 2 3 \u0026lt;title\u0026gt;、\u0026lt;meta\u0026gt;、\u0026lt;link\u0026gt;、\u0026lt;style\u0026gt;、 \u0026lt;script\u0026gt;、 \u0026lt;base\u0026gt;。 应该把 \u0026lt;head\u0026gt; 标签放在文档的开始处，紧跟在 \u0026lt;html\u0026gt; 后面，并处于 \u0026lt;body\u0026gt; 标签之前。 文档的头部经常会包含一些 \u0026lt;meta\u0026gt; 标签，用来告诉浏览器关于文档的附加信息。 【3】body标签\u0026mdash;》里面放的就是页面上展示出来的内容 body 元素是定义文档的主体。body 元素包含文档的所有内容（比如文本、超链接、图像、表格和列表等等。）body是用在网页中的一种HTML标签，标签是用在网页中的一种HTML标签，表示网页的主体部分，也就是用户可以看到的内容，可以包含文本、图片、音频、视频等各种内容！\n4.网页基本标签 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 \u0026lt;!-- DOCTYPE：告诉浏览器，我们要使用什么规范 --\u0026gt; \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;基本标签学习\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!--标题标签--\u0026gt; \u0026lt;h1\u0026gt;一级标签\u0026lt;/h1\u0026gt; \u0026lt;h2\u0026gt;二级标签\u0026lt;/h2\u0026gt; \u0026lt;!--段落标签--\u0026gt; \u0026lt;p\u0026gt;你好！\u0026lt;/p\u0026gt; \u0026lt;!--水平标签--\u0026gt; \u0026lt;hr/\u0026gt; \u0026lt;!--换行标签--\u0026gt; 我喜欢学习\u0026lt;br /\u0026gt;HTML \u0026lt;hr/\u0026gt; \u0026lt;!--加粗倾斜下划线--\u0026gt; \u0026lt;b\u0026gt;加粗\u0026lt;/b\u0026gt;\u0026lt;br /\u0026gt; \u0026lt;i\u0026gt;倾斜\u0026lt;/i\u0026gt;\u0026lt;br /\u0026gt; \u0026lt;u\u0026gt;下划线\u0026lt;/u\u0026gt;\u0026lt;br /\u0026gt; \u0026lt;!--一箭穿心--\u0026gt; \u0026lt;del\u0026gt;你好 你不好\u0026lt;/del\u0026gt;\u0026lt;br /\u0026gt; \u0026lt;hr/\u0026gt; \u0026lt;!--特殊符号--\u0026gt; 空\u0026amp;nbsp;\u0026amp;nbsp;\u0026amp;nbsp;\u0026amp;nbsp;\u0026amp;nbsp;\u0026amp;nbsp;\u0026amp;nbsp;\u0026amp;nbsp;格 \u0026lt;hr/\u0026gt; \u0026lt;!--大于号--\u0026gt; \u0026amp;gt; \u0026lt;hr/\u0026gt; \u0026lt;!--小于号--\u0026gt; \u0026amp;lt; \u0026lt;hr/\u0026gt; \u0026lt;!--@版权所有--\u0026gt; \u0026amp;copy;版权所有 \u0026lt;!--字体标签--\u0026gt; \u0026lt;hr/\u0026gt; \u0026lt;font color=\u0026#34;#397655\u0026#34; size=\u0026#34;7\u0026#34; face=楷体\u0026gt;床前明月光\u0026lt;/font\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 5.图像标签 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!--图片 src:引入图片的位置 引入本地资源 引入网络资源 src：图像地址 alt：图像的替代文字 title：鼠标悬停提示文字 width：图像宽度 height：图像高度 注意:一般高度和宽度只设置一个即可，另一个会按照比例自动适应 title:鼠标悬浮在图片上的时候的提示语，默认情况下（没有设置alt属性） 图片如果加载失败那么提示语也是title的内容 alt:图片加载失败的提示语 --\u0026gt; ![图片加载失败](img/phone_1.jpg) ![](https://fastly.jsdelivr.net/gh/ktzxy/blog-img@main/2026/apic30792_7af9d6.webp) \u0026lt;!--音频和视频 src：资源路径 controls：控制条 autoplay ：自动播放 --\u0026gt; \u0026lt;video src=\u0026#34;audio/video.mp4\u0026#34; controls autoplay\u0026gt;\u0026lt;/video\u0026gt; \u0026lt;audio src=\u0026#34;audio/music.mp3\u0026#34; controls autoplay\u0026gt;\u0026lt;/audio\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 6.超链接标签应用 链接标签\n文本超链接 图像超链接 1 \u0026lt;a href=\u0026#34;链接路径\u0026#34; target=\u0026#34;目标窗口位置\u0026#34;\u0026gt;链接文本或图像\u0026lt;/a\u0026gt; 超链接\n页面间链接：从一个页面跳转到另一个页面 锚链接 功能性链接 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;链接标签学习\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!--使用name作为标记--\u0026gt; \u0026lt;a name=\u0026#34;top\u0026#34;\u0026gt;顶部\u0026lt;/a\u0026gt; \u0026lt;!--锚标签 1.需要一个锚标记 2.跳转到标记 --\u0026gt; \u0026lt;a href=\u0026#34;#down\u0026#34;\u0026gt;回到底部\u0026lt;/a\u0026gt;\u0026lt;br /\u0026gt; \u0026lt;!--a标签 href:必填，表示要跳转到那个页面 target:表示窗口在哪里打开 _blank:在新标签中打开 _self:在自己的网页中打开 --\u0026gt; \u0026lt;a href=\u0026#34;Hello.html\u0026#34; target=\u0026#34;_blank\u0026#34;\u0026gt;点击我跳转到页面\u0026lt;/a\u0026gt;\u0026lt;br /\u0026gt; \u0026lt;a href=\u0026#34;https://baidu.com\u0026#34; target=\u0026#34;_self\u0026#34;\u0026gt;点击我跳转到百度\u0026lt;/a\u0026gt;\u0026lt;br /\u0026gt; \u0026lt;!--锚标签 1.需要一个锚标记 2.跳转到标记 --\u0026gt; \u0026lt;a href=\u0026#34;#top\u0026#34;\u0026gt;回到顶部\u0026lt;/a\u0026gt; \u0026lt;a name=\u0026#34;down\u0026#34;\u0026gt;底部\u0026lt;/a\u0026gt; \u0026lt;br /\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 7.列表标签 列表\n什么是列表 列表就是信息资源的一种展示形式。它可以使信息结构化和条理化，并以列表的样式显示出来，以便浏览者能更快捷的获得相应信息 列表的分类 无序列表 有序列表 自定义列表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;列表学习\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!--有序列表: type:可以设置列表的标号：1,a,A,i,I start:设置起始标号 --\u0026gt; \u0026lt;ol type=\u0026#34;I\u0026#34;\u0026gt; \u0026lt;li\u0026gt;窗前明月光\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;疑是地上霜\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;举头望明月\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;低头思故乡\u0026lt;/li\u0026gt; \u0026lt;/ol\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;!--无序列表: type:可以设置列表前图标的样式 type=\u0026#34;square\u0026#34; 如果想要更换图标样式，需要借助css技术： style=\u0026#34;list-style:url(img/act.jpg) ;\u0026#34; --\u0026gt; \u0026lt;ul\u0026gt; \u0026lt;li\u0026gt;窗前明月光\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;疑是地上霜\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;举头望明月\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;低头思故乡\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;hr\u0026gt; \u0026lt;!--自定义列表 dl：标签 dt：列表名称 dd：列表内容 --\u0026gt; \u0026lt;dl\u0026gt; \u0026lt;dt\u0026gt;静夜思\u0026lt;/dt\u0026gt; \u0026lt;dt\u0026gt;李白\u0026lt;/dt\u0026gt; \u0026lt;dd\u0026gt;窗前明月光\u0026lt;/dd\u0026gt; \u0026lt;dd\u0026gt;疑是地上霜\u0026lt;/dd\u0026gt; \u0026lt;dd\u0026gt;举头望明月\u0026lt;/dd\u0026gt; \u0026lt;dd\u0026gt;低头思故乡\u0026lt;/dd\u0026gt; \u0026lt;/dl\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 8.表格标签 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!--表格：4行4列 table:表格 tr:行 td:单元格 th:特殊单元格：表头效果：加粗，居中 默认情况下表格是没有边框的，通过属性来增加表框： border:设置边框大小 cellspacing：设置单元格和边框之间的空隙 align=\u0026#34;center\u0026#34; 设置居中 background 设置背景图片 background=\u0026#34;img/xx.jpg\u0026#34; bgcolor :设置背景颜色 rowspan:行合并 colspan：列合并 --\u0026gt; \u0026lt;table border=\u0026#34;1px\u0026#34; cellspacing=\u0026#34;2px\u0026#34; width=\u0026#34;400px\u0026#34; height=\u0026#34;300px\u0026#34; bgcolor=cadetblue \u0026gt; \u0026lt;tr bgcolor=\u0026#34;bisque\u0026#34;\u0026gt; \u0026lt;th\u0026gt;学号\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;姓名\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;年纪\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;成绩\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td align=\u0026#34;center\u0026#34;\u0026gt;192001212\u0026lt;/td\u0026gt; \u0026lt;td align=\u0026#34;center\u0026#34;\u0026gt;张三\u0026lt;/td\u0026gt; \u0026lt;td align=\u0026#34;center\u0026#34;\u0026gt;19\u0026lt;/td\u0026gt; \u0026lt;td rowspan=\u0026#34;2\u0026#34; align=\u0026#34;center\u0026#34;\u0026gt;90.5\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td align=\u0026#34;center\u0026#34;\u0026gt;192001213\u0026lt;/td\u0026gt; \u0026lt;td align=\u0026#34;center\u0026#34;\u0026gt;李四\u0026lt;/td\u0026gt; \u0026lt;td align=\u0026#34;center\u0026#34;\u0026gt;18\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td colspan=\u0026#34;4\u0026#34;\u0026gt;备注：\u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 9.iframe内联框架 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;内联框架\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!--iframe内联框架 src：地址 w-h：宽度高度 --\u0026gt; \u0026lt;iframe src=\u0026#34;http://baidu.com\u0026#34; frameborder=\u0026#34;0\u0026#34; width=\u0026#34;800px\u0026#34; height=\u0026#34;300px\u0026#34;\u0026gt; \u0026lt;/iframe\u0026gt; \u0026lt;iframe src=\u0026#34;\u0026#34; name=\u0026#34;hello\u0026#34; frameborder=\u0026#34;0\u0026#34;\u0026gt;\u0026lt;/iframe\u0026gt; \u0026lt;a href=\u0026#34;Hello.html\u0026#34; target=\u0026#34;hello\u0026#34;\u0026gt;点击跳转\u0026lt;/a\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; frameset 元素可定义一个框架集。它被用来组织多个窗口（框架）。每个框架存有独立的文档。在其最简单的应用中，frameset 元素仅仅会规定在框架集中存在多少列或多少行。您必须使用 cols 或 rows 属性。\n里面如果只有一个框架用frame标签 如果多个框架用frameset标签 用cols 或 rows进行行，列的切割\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;!--框架集合：和body是并列的概念，不要将框架集合放入body中--\u0026gt; \u0026lt;frameset rows=\u0026#34;20%,*,30%\u0026#34;\u0026gt; \u0026lt;frame /\u0026gt; \u0026lt;frameset cols=\u0026#34;30%,40%,*\u0026#34;\u0026gt; \u0026lt;frame /\u0026gt; \u0026lt;frame src=\u0026#34;index.html\u0026#34;/\u0026gt; \u0026lt;frame /\u0026gt; \u0026lt;/frameset\u0026gt; \u0026lt;frameset cols=\u0026#34;50%,*\u0026#34;\u0026gt; \u0026lt;frame /\u0026gt; \u0026lt;frame /\u0026gt; \u0026lt;/frameset\u0026gt; \u0026lt;/frameset\u0026gt; \u0026lt;/html\u0026gt; 10.表单 表单在 Web 网页中用来给访问者填写信息，从而能采集客户端信息，使网页具有交互的功能。一般是将表单设计在一个Html 文档中，当用户填写完信息后做提交(submit)操作，于是表单的内容就从客户端的浏览器传送到服务器上，经过服务器上程序处理后，再将用户所需信息传送回客户端的浏览器上，这样网页就具有了交互性。这里我们只讲怎样使用Html 标志来设计表单。 所有的用户输入内容的地方都用表单来写，如登录注册、搜索框。 一个表单一般应该包含用户填写信息的输入框,提交按钮等，这些输入框,按钮叫做控件,表单很像容器,它能够容纳各种各样的控件。\n1 \u0026lt;form action＝\u0026#34;url\u0026#34; method=get|post name=\u0026#34;myform\u0026#34; \u0026gt;\u0026lt;/form\u0026gt; -name：表单提交时的名称 -action：提交到的地址 -method：提交方式，有get和post两种，默认为get\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;表单元素学习\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;!--表单form action：表单提交的位置，可以是网站，也可以是一个请求处理地址 method：post，get 提交方式 get方式提交：外面可以在url中看到外面提交的信息，不安全，高效 post方式提交，比较安全，可以传输大文件 --\u0026gt; \u0026lt;form action=\u0026#34;\u0026#34; method=\u0026#34;get\u0026#34;\u0026gt; \u0026lt;p\u0026gt; \u0026lt;!--文本框: input标签使用很广泛，通过type属性的不同值，来表现不同的形态。 type=\u0026#34;text\u0026#34; 文本框，里面文字可见 表单元素必须有一个属性：name 有了name才可以提交数据,才可以采集数据 然后提交的时候会以键值对的形式拼到一起。 value:就是文本框中的具体内容 键值对：name=value的形式 如果value提前写好，那么默认效果就是value中内容。 一般默认提示语：用placeholder属性，不会用value--》value只是文本框中的值。 size：指定表单元素的初始宽度。 但type为text或password时，表单元素的大小以字符为单位。 对于其他类型，宽度以像素为单位。 maxlength：type为text或password时，输入的最大字符数。 checked：type为radio或checkbox时，指定按钮是否被选中。 readonly只读：只是不能修改，但是其他操作都可以，可以正常提交 disabled禁用：完全不用，不能正常提交 写法： readonly=\u0026#34;readonly\u0026#34; readonly readonly = \u0026#34;true\u0026#34; --\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;uname\u0026#34; placeholder=\u0026#34;请录入身份证信息\u0026#34;/\u0026gt; \u0026lt;hr/\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;uname2\u0026#34; value=\u0026#34;123123\u0026#34; readonly=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;hr/\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;uname3\u0026#34; value=\u0026#34;456456\u0026#34; disabled=\u0026#34;disabled\u0026#34;/\u0026gt; \u0026lt;hr/\u0026gt; \u0026lt;!--密码框:效果录入信息不可见--\u0026gt; \u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;pwd\u0026#34; /\u0026gt; \u0026lt;hr/\u0026gt; \u0026lt;/p\u0026gt; \u0026lt;p\u0026gt; \u0026lt;!--单选按钮： 注意：一组单选按钮，必须通过name属性来控制，让它们在一个分组中，然后在一个分组里只能选择一个 正常状态下，提交数据为：gender=on ，后台不能区分你提交的数据 不同的选项的value值要控制为不同，这样后台接收就可以区分了 默认选中：checked=\u0026#34;checked\u0026#34; --\u0026gt; \u0026lt;a\u0026gt;性别：\u0026lt;/a\u0026gt; \u0026lt;input type=\u0026#34;radio\u0026#34; name=\u0026#34;gender\u0026#34; value=\u0026#34;1\u0026#34; checked=\u0026#34;checked\u0026#34;/\u0026gt;男 \u0026lt;input type=\u0026#34;radio\u0026#34; name=\u0026#34;gender\u0026#34; value=\u0026#34;0\u0026#34;/\u0026gt;女 \u0026lt;hr /\u0026gt; \u0026lt;/p\u0026gt;\t\u0026lt;p\u0026gt; \u0026lt;!--多选按钮: 必须通过name属性来控制，让它们在一个分组中，然后在一个分组里可以选择多个 不同的选项的value值要控制为不同，这样后台接收就可以区分了 多个选项提交的时候，键值对用\u0026amp;符号进行拼接：例如下： favlan=1\u0026amp;favlan=3 --\u0026gt; \u0026lt;a\u0026gt;你喜欢的语言：\u0026lt;/a\u0026gt; \u0026lt;input type=\u0026#34;checkbox\u0026#34; name=\u0026#34;favlan\u0026#34; value=\u0026#34;1\u0026#34; checked=\u0026#34;checked\u0026#34;/\u0026gt;汉语 \u0026lt;input type=\u0026#34;checkbox\u0026#34; name=\u0026#34;favlan\u0026#34; value=\u0026#34;2\u0026#34; checked=\u0026#34;checked\u0026#34;/\u0026gt;英语 \u0026lt;input type=\u0026#34;checkbox\u0026#34; name=\u0026#34;favlan\u0026#34; value=\u0026#34;3\u0026#34;/\u0026gt;法语 \u0026lt;input type=\u0026#34;checkbox\u0026#34; name=\u0026#34;favlan\u0026#34; value=\u0026#34;4\u0026#34;/\u0026gt;韩语 \u0026lt;hr /\u0026gt; \u0026lt;!--文件--\u0026gt; \u0026lt;input type=\u0026#34;file\u0026#34; /\u0026gt; \u0026lt;hr /\u0026gt; \u0026lt;!--普通按钮：普通按钮没有什么效果，就是可以点击，以后学了js，可以加入事件--\u0026gt; \u0026lt;input type=\u0026#34;button\u0026#34; value=\u0026#34;普通按钮\u0026#34; /\u0026gt; \u0026lt;hr /\u0026gt; \u0026lt;!--特殊按钮：重置按钮将页面恢复到初始状态--\u0026gt; \u0026lt;input type=\u0026#34;reset\u0026#34; /\u0026gt; \u0026lt;hr /\u0026gt; \u0026lt;!--特殊按钮：图片按钮--\u0026gt; ![](img/phone_1.jpg) \u0026lt;input type=\u0026#34;image\u0026#34; src=\u0026#34;img/phone_1.jpg\u0026#34; /\u0026gt; \u0026lt;hr /\u0026gt; \u0026lt;!--下拉列表 默认选中：selected=\u0026#34;selected\u0026#34; 多选：multiple=\u0026#34;multiple\u0026#34;--\u0026gt; \u0026lt;a\u0026gt;请选择你喜欢的城市：\u0026lt;/a\u0026gt; \u0026lt;select name=\u0026#34;city\u0026#34; multiple=\u0026#34;multiple\u0026#34;\u0026gt; \u0026lt;option value=\u0026#34;0\u0026#34;\u0026gt;---请选择---\u0026lt;/option\u0026gt; \u0026lt;option value=\u0026#34;1\u0026#34;\u0026gt;哈尔滨市\u0026lt;/option\u0026gt; \u0026lt;option value=\u0026#34;2\u0026#34; selected=\u0026#34;selected\u0026#34;\u0026gt;青岛市\u0026lt;/option\u0026gt; \u0026lt;option value=\u0026#34;3\u0026#34;\u0026gt;郑州市\u0026lt;/option\u0026gt; \u0026lt;option value=\u0026#34;4\u0026#34;\u0026gt;西安市\u0026lt;/option\u0026gt; \u0026lt;option value=\u0026#34;5\u0026#34;\u0026gt;天津市\u0026lt;/option\u0026gt; \u0026lt;/select\u0026gt; \u0026lt;hr /\u0026gt; \u0026lt;/p\u0026gt; \u0026lt;p\u0026gt; \u0026lt;!--多行文本框 利用css样式来控制大小不可变：style=\u0026#34;resize: none;\u0026#34;--\u0026gt; \u0026lt;a\u0026gt;自我介绍：\u0026lt;/a\u0026gt; \u0026lt;textarea style=\u0026#34;resize: none;\u0026#34; rows=\u0026#34;10\u0026#34; cols=\u0026#34;30\u0026#34;\u0026gt;请在这里填写信息。。\u0026lt;/textarea\u0026gt; \u0026lt;br /\u0026gt; \u0026lt;hr /\u0026gt; \u0026lt;/p\u0026gt; \u0026lt;p\u0026gt; \u0026lt;!--文本域--\u0026gt; \u0026lt;a\u0026gt;反馈：\u0026lt;/a\u0026gt; \u0026lt;textarea name=\u0026#34;textarea\u0026#34; id=\u0026#34;\u0026#34; cols=\u0026#34;30\u0026#34; rows=\u0026#34;10\u0026#34;\u0026gt;\u0026lt;/textarea\u0026gt; \u0026lt;hr /\u0026gt; \u0026lt;!--文件域--\u0026gt; \u0026lt;input type=\u0026#34;file\u0026#34; name=\u0026#34;files\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;button\u0026#34; value=\u0026#34;上传\u0026#34; name=\u0026#34;upload\u0026#34;\u0026gt; \u0026lt;hr /\u0026gt; \u0026lt;/p\u0026gt; \u0026lt;p\u0026gt; \u0026lt;!--label标签 一般会在想要获得焦点的标签上加入一个id属性，然后label中的for属性跟id配合使用。 --\u0026gt; \u0026lt;label for=\u0026#34;uname\u0026#34;\u0026gt;用户名：\u0026lt;/label\u0026gt;\u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;uername\u0026#34; id=\u0026#34;uname\u0026#34;/\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; /\u0026gt; \u0026lt;hr /\u0026gt; \u0026lt;/p\u0026gt; \u0026lt;p\u0026gt; \u0026lt;!--邮件验证--\u0026gt; \u0026lt;a\u0026gt;邮件：\u0026lt;/a\u0026gt; \u0026lt;input type=\u0026#34;email\u0026#34; name=\u0026#34;email\u0026#34;\u0026gt;\u0026lt;br /\u0026gt; \u0026lt;!--URL--\u0026gt; \u0026lt;a\u0026gt;URL：\u0026lt;/a\u0026gt; \u0026lt;input type=\u0026#34;url\u0026#34; name=\u0026#34;url\u0026#34;\u0026gt;\u0026lt;br /\u0026gt; \u0026lt;!--数字--\u0026gt; \u0026lt;a\u0026gt;数字：\u0026lt;/a\u0026gt; \u0026lt;input type=\u0026#34;number\u0026#34; name=\u0026#34;num\u0026#34; max=\u0026#34;100\u0026#34; min=\u0026#34;0\u0026#34; size=\u0026#34;10\u0026#34;\u0026gt;\u0026lt;br /\u0026gt; \u0026lt;!--滑块--\u0026gt; \u0026lt;a\u0026gt;音量：\u0026lt;/a\u0026gt; \u0026lt;input type=\u0026#34;range\u0026#34; name=\u0026#34;voice\u0026#34; min=\u0026#34;0\u0026#34; max=\u0026#34;100\u0026#34; step=\u0026#34;2\u0026#34;\u0026gt;\u0026lt;br /\u0026gt; \u0026lt;!--搜索框--\u0026gt; \u0026lt;a\u0026gt;搜索：\u0026lt;/a\u0026gt; \u0026lt;input type=\u0026#34;search\u0026#34; name=\u0026#34;search\u0026#34;\u0026gt;\u0026lt;br /\u0026gt; \u0026lt;/p\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 效果图：\n","permalink":"https://ktzxy.top/posts/p4nw47z9x3/","summary":"HTML学习","title":"HTML学习"},{"content":"Golang goroutine channel 实现并发和并行 为什么要使用goroutine呢 需求：要统计1-10000000的数字中那些是素数，并打印这些素数？\n素数：就是除了1和它本身不能被其他数整除的数\n实现方法：\n传统方法，通过一个for循环判断各个数是不是素数 使用并发或者并行的方式，将统计素数的任务分配给多个goroutine去完成，这个时候就用到了goroutine goroutine 结合 channel 进程、线程以及并行、并发 进程 进程（Process）就是程序在操作系统中的一次执行过程，是系统进行资源分配和调度的基本单位，进程是一个动态概念，是程序在执行过程中分配和管理资源的基本单位，每一个进程都有一个自己的地址空间。一个进程至少有5种基本状态，它们是：初始态，执行态，等待状态，就绪状态，终止状态。\n通俗的讲进程就是一个正在执行的程序。\n线程 线程是进程的一个执行实例，是程序执行的最小单元，它是比进程更小的能独立运行的基本单位\n一个进程可以创建多个线程，同一个进程中多个线程可以并发执行 ，一个线程要运行的话，至少有一个进程\n并发和并行 并发：多个线程同时竞争一个位置，竞争到的才可以执行，每一个时间段只有一个线程在执行。\n并行：多个线程可以同时执行，每一个时间段，可以有多个线程同时执行。\n通俗的讲多线程程序在单核CPU上面运行就是并发，多线程程序在多核CUP上运行就是并行，如果线程数大于CPU核数，则多线程程序在多个CPU上面运行既有并行又有并发\nGolang中协程（goroutine）以及主线程 golang中的主线程：（可以理解为线程/也可以理解为进程），在一个Golang程序的主线程上可以起多个协程。Golang中多协程可以实现并行或者并发。\n协程：可以理解为用户级线程，这是对内核透明的，也就是系统并不知道有协程的存在，是完全由用户自己的程序进行调度的。Golang的一大特色就是从语言层面原生持协程，在函数或者方法前面加go关键字就可创建一个协程。可以说Golang中的协程就是goroutine。\nGolang中的多协程有点类似于Java中的多线程\n多协程和多线程 多协程和多线程：Golang中每个goroutine（协程）默认占用内存远比Java、C的线程少。\nOS线程（操作系统线程）一般都有固定的栈内存（通常为2MB左右），一个goroutine（协程）占用内存非常小，只有2KB左右，多协程goroutine切换调度开销方面远比线程要少。\n这也是为什么越来越多的大公司使用Golang的原因之一。\ngoroutine的使用以及sync.WaitGroup 并行执行需求 在主线程（可以理解成进程）中，开启一个goroutine，该协程每隔50毫秒秒输出“你好golang\u0026quot;\n在主线程中也每隔50毫秒输出“你好golang\u0026quot;，输出10次后，退出程序，要求主线程和goroutine同时执行。\n这是时候，我们就可以开启协程来了，通过 go关键字开启\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 协程需要运行的方法 func test() { for i := 0; i \u0026lt; 5; i++ { fmt.Println(\u0026#34;test 你好golang\u0026#34;) time.Sleep(time.Millisecond * 100) } } func main() { // 通过go关键字，就可以直接开启一个协程 go test() // 这是主进程执行的 for i := 0; i \u0026lt; 5; i++ { fmt.Println(\u0026#34;main 你好golang\u0026#34;) time.Sleep(time.Millisecond * 100) } } 运行结果如下，我们能够看到他们之间不存在所谓的顺序关系了\n1 2 3 4 5 6 7 8 9 10 main 你好golang test 你好golang main 你好golang test 你好golang test 你好golang main 你好golang main 你好golang test 你好golang test 你好golang main 你好golang 但是上述的代码其实还有问题的，也就是说当主进程执行完毕后，不管协程有没有执行完成，都会退出\n这是使用我们就需要用到 sync.WaitGroup等待协程\n首先我们需要创建一个协程计数器\n1 2 // 定义一个协程计数器 var wg sync.WaitGroup 然后当我们开启协程的时候，我们要让计数器加1\n1 2 3 // 开启协程，协程计数器加1 wg.Add(1) go test2() 当我们协程结束前，我们需要让计数器减1\n1 2 // 协程计数器减1 wg.Done() 完整代码如下\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 // 定义一个协程计数器 var wg sync.WaitGroup func test() { // 这是主进程执行的 for i := 0; i \u0026lt; 1000; i++ { fmt.Println(\u0026#34;test1 你好golang\u0026#34;, i) //time.Sleep(time.Millisecond * 100) } // 协程计数器减1 wg.Done() } func test2() { // 这是主进程执行的 for i := 0; i \u0026lt; 1000; i++ { fmt.Println(\u0026#34;test2 你好golang\u0026#34;, i) //time.Sleep(time.Millisecond * 100) } // 协程计数器减1 wg.Done() } func main() { // 通过go关键字，就可以直接开启一个协程 wg.Add(1) go test() // 协程计数器加1 wg.Add(1) go test2() // 这是主进程执行的 for i := 0; i \u0026lt; 1000; i++ { fmt.Println(\u0026#34;main 你好golang\u0026#34;, i) //time.Sleep(time.Millisecond * 100) } // 等待所有的协程执行完毕 wg.Wait() fmt.Println(\u0026#34;主线程退出\u0026#34;) } 设置Go并行运行的时候占用的cpu数量 Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上，调度器会把Go代码同时调度到8个oS线程上。\nGo 语言中可以通过runtime.GOMAXPROCS（）函数设置当前程序并发时占用的CPU逻辑核心数。\nGo1.5版本之前，默认使用的是单核心执行。Go1.5版本之后，默认使用全部的CPU逻辑核心数。\n1 2 3 4 5 6 7 func main() { // 获取cpu个数 npmCpu := runtime.NumCPU() fmt.Println(\u0026#34;cup的个数:\u0026#34;, npmCpu) // 设置允许使用的CPU数量 runtime.GOMAXPROCS(runtime.NumCPU() - 1) } for循环开启多个协程 类似于Java里面开启多个线程，同时执行\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func test(num int) { for i := 0; i \u0026lt; 10; i++ { fmt.Printf(\u0026#34;协程（%v）打印的第%v条数据 \\n\u0026#34;, num, i) } // 协程计数器减1 vg.Done() } var vg sync.WaitGroup func main() { for i := 0; i \u0026lt; 10; i++ { go test(i) vg.Add(1) } vg.Wait() fmt.Println(\u0026#34;主线程退出\u0026#34;) } 因为我们协程会在主线程退出后就终止，所以我们还需要使用到 sync.WaitGroup来控制主线程的终止。\nChannel管道 管道是Golang在语言级别上提供的goroutine间的通讯方式，我们可以使用channel在多个goroutine之间传递消息。如果说goroutine是Go程序并发的执行体，channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。\nGolang的并发模型是CSP（Communicating Sequential Processes），提倡通过通信共享内存而不是通过共享内存而实现通信。\nGo语言中的管道（channel）是一种特殊的类型。管道像一个传送带或者队列，总是遵循先入先出（First In First Out）的规则，保证收发数据的顺序。每一个管道都是一个具体类型的导管，也就是声明channel的时候需要为其指定元素类型。\nchannel类型 channel是一种类型，一种引用类型。声明管道类型的格式如下：\n1 2 3 4 5 6 // 声明一个传递整型的管道 var ch1 chan int // 声明一个传递布尔类型的管道 var ch2 chan bool // 声明一个传递int切片的管道 var ch3 chan []int 创建channel 声明管道后，需要使用make函数初始化之后才能使用\n1 make(chan 元素类型, 容量) 举例如下：\n1 2 3 4 5 6 // 创建一个能存储10个int类型的数据管道 ch1 = make(chan int, 10) // 创建一个能存储4个bool类型的数据管道 ch2 = make(chan bool, 4) // 创建一个能存储3个[]int切片类型的管道 ch3 = make(chan []int, 3) channel操作 管道有发送，接收和关闭的三个功能\n发送和接收 都使用 \u0026lt;- 符号\n现在我们先使用以下语句定义一个管道：\n1 ch := make(chan int, 3) 发送 将数据放到管道内，将一个值发送到管道内\n1 2 // 把10发送到ch中 ch \u0026lt;- 10 取操作 1 x := \u0026lt;- ch 关闭管道. 通过调用内置的close函数来关闭管道\n1 close(ch) 完整示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // 创建管道 ch := make(chan int, 3) // 给管道里面存储数据 ch \u0026lt;- 10 ch \u0026lt;- 21 ch \u0026lt;- 32 // 获取管道里面的内容 a := \u0026lt;- ch fmt.Println(\u0026#34;打印出管道的值：\u0026#34;, a) fmt.Println(\u0026#34;打印出管道的值：\u0026#34;, \u0026lt;- ch) fmt.Println(\u0026#34;打印出管道的值：\u0026#34;, \u0026lt;- ch) // 管道的值、容量、长度 fmt.Printf(\u0026#34;地址：%v 容量：%v 长度：%v \\n\u0026#34;, ch, cap(ch), len(ch)) // 管道的类型 fmt.Printf(\u0026#34;%T \\n\u0026#34;, ch) // 管道阻塞（当没有数据的时候取，会出现阻塞，同时当管道满了，继续存也会） \u0026lt;- ch // 没有数据取，出现阻塞 ch \u0026lt;- 10 ch \u0026lt;- 10 ch \u0026lt;- 10 ch \u0026lt;- 10 // 管道满了，继续存，也出现阻塞 for range从管道循环取值 当向管道中发送完数据时，我们可以通过close函数来关闭管道，当管道被关闭时，再往该管道发送值会引发panic，从该管道取值的操作会去完管道中的值，再然后取到的值一直都是对应类型的零值。那如何判断一个管道是否被关闭的呢？\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 创建管道 ch := make(chan int, 10) // 循环写入值 for i := 0; i \u0026lt; 10; i++ { ch \u0026lt;- i } // 关闭管道 close(ch) // for range循环遍历管道的值(管道没有key) for value := range ch { fmt.Println(value) } // 通过上述的操作，能够打印值，但是出出现一个deadlock的死锁错误，也就说我们需要关闭管道 注意：使用for range遍历的时候，一定在之前需要先关闭管道\n思考：通过for循环来遍历管道，需要关闭么？\n1 2 3 4 5 6 7 8 9 10 // 创建管道 ch := make(chan int, 10) // 循环写入值 for i := 0; i \u0026lt; 10; i++ { ch \u0026lt;- i } for i := 0; i \u0026lt; 10; i++ { fmt.Println(\u0026lt;- ch) } 上述代码没有报错，说明通过for i的循环方式，可以不关闭管道\nGoroutine 结合 channel 管道 需求1：定义两个方法，一个方法给管道里面写数据，一个给管道里面读取数据。要求同步进行。\n开启一个fn1的的协程给向管道inChan中写入00条数据 开启一个fn2的协程读取inChan中写入的数据 注意：fn1和fn2同时操作一个管道 主线程必须等待操作完成后才可以退出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func write(ch chan int) { for i := 0; i \u0026lt; 10; i++ { fmt.Println(\u0026#34;写入:\u0026#34;, i) ch \u0026lt;- i time.Sleep(time.Microsecond * 10) } wg.Done() } func read(ch chan int) { for i := 0; i \u0026lt; 10; i++ { fmt.Println(\u0026#34;读取:\u0026#34;, \u0026lt;- ch) time.Sleep(time.Microsecond * 10) } wg.Done() } var wg sync.WaitGroup func main() { ch := make(chan int, 10) wg.Add(1) go write(ch) wg.Add(1) go read(ch) // 等待 wg.Wait() fmt.Println(\u0026#34;主线程执行完毕\u0026#34;) } 管道是安全的，是一边写入，一边读取，当读取比较快的时候，会等待写入\ngoroutine 结合 channel打印素数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 // 想intChan中放入 1~ 120000个数 func putNum(intChan chan int) { for i := 2; i \u0026lt; 120000; i++ { intChan \u0026lt;- i } wg.Done() close(intChan) } // cong intChan取出数据，并判断是否为素数，如果是的话，就把得到的素数放到primeChan中 func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) { for value := range intChan { var flag = true for i := 2; i \u0026lt;= int(math.Sqrt(float64(value))); i++ { if i % i == 0 { flag = false break } } if flag { // 是素数 primeChan \u0026lt;- value break } } // 这里需要关闭 primeChan，因为后面需要遍历输出 primeChan exitChan \u0026lt;- true wg.Done() } // 打印素数 func printPrime(primeChan chan int) { for value := range primeChan { fmt.Println(value) } wg.Done() } var wg sync.WaitGroup func main() { // 写入数字 intChan := make(chan int, 1000) // 存放素数 primeChan := make(chan int, 1000) // 存放 primeChan退出状态 exitChan := make(chan bool, 16) // 开启写值的协程 go putNum(intChan) // 开启计算素数的协程 for i := 0; i \u0026lt; 10; i++ { wg.Add(1) go primeNum(intChan, primeChan, exitChan) } // 开启打印的协程 wg.Add(1) go printPrime(primeChan) // 匿名自运行函数 wg.Add(1) go func() { for i := 0; i \u0026lt; 16; i++ { // 如果exitChan 没有完成16次遍历，将会等待 \u0026lt;- exitChan } // 关闭primeChan close(primeChan) wg.Done() }() wg.Wait() fmt.Println(\u0026#34;主线程执行完毕\u0026#34;) } 单向管道 有时候我们会将管道作为参数在多个任务函数间传递，很多时候我们在不同的任务函数中，使用管道都会对其进行限制，比如限制管道在函数中只能发送或者只能接受\n默认的管道是 可读可写\n1 2 3 4 5 6 7 8 9 10 11 12 // 定义一种可读可写的管道 var ch = make(chan int, 2) ch \u0026lt;- 10 \u0026lt;- ch // 管道声明为只写管道，只能够写入，不能读 var ch2 = make(chan\u0026lt;- int, 2) ch2 \u0026lt;- 10 // 声明一个只读管道 var ch3 = make(\u0026lt;-chan int, 2) \u0026lt;- ch3 Select多路复用 在某些场景下我们需要同时从多个通道接收数据。这个时候就可以用到golang中给我们提供的select多路复用。 通常情况通道在接收数据时，如果没有数据可以接收将会发生阻塞。\n比如说下面代码来实现从多个通道接受数据的时候就会发生阻塞\n这种方式虽然可以实现从多个管道接收值的需求，但是运行性能会差很多。为了应对这种场景，Go内置了select关键字，可以同时响应多个管道的操作。\nselect的使用类似于switch 语句，它有一系列case分支和一个默认的分支。每个case会对应一个管道的通信（接收或发送）过程。select会一直等待，直到某个case的通信操作完成时，就会执行case分支对应的语句。具体格式如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 intChan := make(chan int, 10) intChan \u0026lt;- 10 intChan \u0026lt;- 12 intChan \u0026lt;- 13 stringChan := make(chan int, 10) stringChan \u0026lt;- 20 stringChan \u0026lt;- 23 stringChan \u0026lt;- 24 // 每次循环的时候，会随机中一个chan中读取，其中for是死循环 for { select { case v:= \u0026lt;- intChan: fmt.Println(\u0026#34;从initChan中读取数据：\u0026#34;, v) case v:= \u0026lt;- stringChan: fmt.Println(\u0026#34;从stringChan中读取数据：\u0026#34;, v) default: fmt.Println(\u0026#34;所有的数据获取完毕\u0026#34;) return } } tip：使用select来获取数据的时候，不需要关闭chan，不然会出现问题\nGoroutine Recover解决协程中出现的Panic 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func sayHello() { for i := 0; i \u0026lt; 10; i++ { fmt.Println(\u0026#34;hello\u0026#34;) } } func errTest() { // 捕获异常 defer func() { if err := recover(); err != nil { fmt.Println(\u0026#34;errTest发生错误\u0026#34;) } }() var myMap map[int]string myMap[0] = \u0026#34;10\u0026#34; } func main { go sayHello() go errTest() } 当我们出现问题的时候，我们还是按照原来的方法，通过defer func创建匿名自启动\n1 2 3 4 5 6 // 捕获异常 defer func() { if err := recover(); err != nil { fmt.Println(\u0026#34;errTest发生错误\u0026#34;) } }() Go中的并发安全和锁 如下面一段代码，我们在并发环境下进行操作，就会出现并发访问的问题\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 var count = 0 var wg sync.WaitGroup func test() { count++ fmt.Println(\u0026#34;the count is : \u0026#34;, count) time.Sleep(time.Millisecond) wg.Done() } func main() { for i := 0; i \u0026lt; 20; i++ { wg.Add(1) go test() } time.Sleep(time.Second * 10) } 互斥锁 互斥锁是传统并发编程中对共享资源进行访问控制的主要手段，它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法，Lock和Unlock。Lock锁定当前的共享资源，Unlock 进行解锁\n1 2 3 4 5 6 // 定义一个锁 var mutex sync.Mutex // 加锁 mutex.Lock() // 解锁 mutex.Unlock() 完整代码\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 var count = 0 var wg sync.WaitGroup var mutex sync.Mutex func test() { // 加锁 mutex.Lock() count++ fmt.Println(\u0026#34;the count is : \u0026#34;, count) time.Sleep(time.Millisecond) wg.Done() // 解锁 mutex.Unlock() } func main() { for i := 0; i \u0026lt; 20; i++ { wg.Add(1) go test() } time.Sleep(time.Second * 10) } 通过下面命令，build的时候，可以查看是否具有竞争关系\n1 2 3 4 // 通过 -race 参数进行构建 go build -race main.go // 运行插件 main.ext 读写互斥锁 互斥锁的本质是当一个goroutine访问的时候，其他goroutine都不能访问。这样在资源同步，避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。\n其实，当我们对一个不会变化的数据只做“读”操作的话，是不存在资源竞争的问题的。因为数据是不变的，不管怎么读取，多少goroutine同时读取，都是可以的。\n所以问题不是出在“读”上，主要是修改，也就是“写”。修改的数据要同步，这样其他goroutine才可以感知到。所以真正的互斥应该是读取和修改、修改和修改之间，读和读是没有互斥操作的必要的。\n因此，衍生出另外一种锁，叫做读写锁。\n读写锁可以让多个读操作并发，同时读取，但是对于写操作是完全互斥的。也就是说，当一个goroutine进行写操作的时候，其他goroutine既不能进行读操作，也不能进行写操作。\nGO中的读写锁由结构体类型sync.RWMutex表示。此类型的方法集合中包含两对方法：\n","permalink":"https://ktzxy.top/posts/r0p30aqppx/","summary":"15 goroutine实现并行和并发","title":"15 goroutine实现并行和并发"},{"content":"互联网协议 来源 https://www.liwenzhou.com/posts/Go/15_socket/\n前言 现在我们几乎每天都在使用互联网，我们前面已经学习了如何编写Go语言程序，但是如何才能让我们的程序通过网络互相通信呢？本章我们就一起来学习下Go语言中的网络编程。 关于网络编程其实是一个很庞大的领域，本文只是简单的演示了如何使用net包进行TCP和UDP通信。如需了解更详细的网络编程请自行检索和阅读专业资料。\n互联网协议介绍 互联网的核心是一系列协议，总称为”互联网协议”（Internet Protocol Suite），正是这一些协议规定了电脑如何连接和组网。我们理解了这些协议，就理解了互联网的原理。由于这些协议太过庞大和复杂，没有办法在这里一概而全，只能介绍一下我们日常开发中接触较多的几个协议。\n互联网分层模型 互联网的逻辑实现被分为好几层。每一层都有自己的功能，就像建筑物一样，每一层都靠下一层支持。用户接触到的只是最上面的那一层，根本不会感觉到下面的几层。要理解互联网就需要自下而上理解每一层的实现的功能。\n如上图所示，互联网按照不同的模型划分会有不用的分层，但是不论按照什么模型去划分，越往上的层越靠近用户，越往下的层越靠近硬件。在软件开发中我们使用最多的是上图中将互联网划分为五个分层的模型。\n接下来我们一层一层的自底向上介绍一下每一层。\n物理层 我们的电脑要与外界互联网通信，需要先把电脑连接网络，我们可以用双绞线、光纤、无线电波等方式。这就叫做”实物理层”，它就是把电脑连接起来的物理手段。它主要规定了网络的一些电气特性，作用是负责传送0和1的电信号。\n数据链路层 单纯的0和1没有任何意义，所以我们使用者会为其赋予一些特定的含义，规定解读电信号的方式：例如：多少个电信号算一组？每个信号位有何意义？这就是”数据链接层”的功能，它在”物理层”的上方，确定了物理层传输的0和1的分组方式及代表的意义。早期的时候，每家公司都有自己的电信号分组方式。逐渐地，一种叫做”以太网”（Ethernet）的协议，占据了主导地位。\n以太网规定，一组电信号构成一个数据包，叫做”帧”（Frame）。每一帧分成两个部分：标头（Head）和数据（Data）。其中”标头”包含数据包的一些说明项，比如发送者、接受者、数据类型等等；”数据”则是数据包的具体内容。”标头”的长度，固定为18字节。”数据”的长度，最短为46字节，最长为1500字节。因此，整个”帧”最短为64字节，最长为1518字节。如果数据很长，就必须分割成多个帧进行发送。\n那么，发送者和接受者是如何标识呢？以太网规定，连入网络的所有设备都必须具有”网卡”接口。数据包必须是从一块网卡，传送到另一块网卡。网卡的地址，就是数据包的发送地址和接收地址，这叫做MAC地址。每块网卡出厂的时候，都有一个全世界独一无二的MAC地址，长度是48个二进制位，通常用12个十六进制数表示。前6个十六进制数是厂商编号，后6个是该厂商的网卡流水号。有了MAC地址，就可以定位网卡和数据包的路径了。\n我们会通过ARP协议来获取接受方的MAC地址，有了MAC地址之后，如何把数据准确的发送给接收方呢？其实这里以太网采用了一种很”原始”的方式，它不是把数据包准确送到接收方，而是向本网络内所有计算机都发送，让每台计算机读取这个包的”标头”，找到接收方的MAC地址，然后与自身的MAC地址相比较，如果两者相同，就接受这个包，做进一步处理，否则就丢弃这个包。这种发送方式就叫做”广播”（broadcasting）。\n网络层 按照以太网协议的规则我们可以依靠MAC地址来向外发送数据。理论上依靠MAC地址，你电脑的网卡就可以找到身在世界另一个角落的某台电脑的网卡了，但是这种做法有一个重大缺陷就是以太网采用广播方式发送数据包，所有成员人手一”包”，不仅效率低，而且发送的数据只能局限在发送者所在的子网络。也就是说如果两台计算机不在同一个子网络，广播是传不过去的。这种设计是合理且必要的，因为如果互联网上每一台计算机都会收到互联网上收发的所有数据包，那是不现实的。\n因此，必须找到一种方法区分哪些MAC地址属于同一个子网络，哪些不是。如果是同一个子网络，就采用广播方式发送，否则就采用”路由”方式发送。这就导致了”网络层”的诞生。它的作用是引进一套新的地址，使得我们能够区分不同的计算机是否属于同一个子网络。这套地址就叫做”网络地址”，简称”网址”。\n“网络层”出现以后，每台计算机有了两种地址，一种是MAC地址，另一种是网络地址。两种地址之间没有任何联系，MAC地址是绑定在网卡上的，网络地址则是网络管理员分配的。网络地址帮助我们确定计算机所在的子网络，MAC地址则将数据包送到该子网络中的目标网卡。因此，从逻辑上可以推断，必定是先处理网络地址，然后再处理MAC地址。\n规定网络地址的协议，叫做IP协议。它所定义的地址，就被称为IP地址。目前，广泛采用的是IP协议第四版，简称IPv4。IPv4这个版本规定，网络地址由32个二进制位组成，我们通常习惯用分成四段的十进制数表示IP地址，从0.0.0.0一直到255.255.255.255。\n根据IP协议发送的数据，就叫做IP数据包。IP数据包也分为”标头”和”数据”两个部分：”标头”部分主要包括版本、长度、IP地址等信息，”数据”部分则是IP数据包的具体内容。IP数据包的”标头”部分的长度为20到60字节，整个数据包的总长度最大为65535字节。\n传输层 有了MAC地址和IP地址，我们已经可以在互联网上任意两台主机上建立通信。但问题是同一台主机上会有许多程序都需要用网络收发数据，比如QQ和浏览器这两个程序都需要连接互联网并收发数据，我们如何区分某个数据包到底是归哪个程序的呢？也就是说，我们还需要一个参数，表示这个数据包到底供哪个程序（进程）使用。这个参数就叫做”端口”（port），它其实是每一个使用网卡的程序的编号。每个数据包都发到主机的特定端口，所以不同的程序就能取到自己所需要的数据。\n“端口”是0到65535之间的一个整数，正好16个二进制位。0到1023的端口被系统占用，用户只能选用大于1023的端口。有了IP和端口我们就能实现唯一确定互联网上一个程序，进而实现网络间的程序通信。\n我们必须在数据包中加入端口信息，这就需要新的协议。最简单的实现叫做UDP协议，它的格式几乎就是在数据前面，加上端口号。UDP数据包，也是由”标头”和”数据”两部分组成：”标头”部分主要定义了发出端口和接收端口，”数据”部分就是具体的内容。UDP数据包非常简单，”标头”部分一共只有8个字节，总长度不超过65,535字节，正好放进一个IP数据包。\nUDP协议的优点是比较简单，容易实现，但是缺点是可靠性较差，一旦数据包发出，无法知道对方是否收到。为了解决这个问题，提高网络可靠性，TCP协议就诞生了。TCP协议能够确保数据不会遗失。它的缺点是过程复杂、实现困难、消耗较多的资源。TCP数据包没有长度限制，理论上可以无限长，但是为了保证网络的效率，通常TCP数据包的长度不会超过IP数据包的长度，以确保单个TCP数据包不必再分割。\n应用层 应用程序收到”传输层”的数据，接下来就要对数据进行解包。由于互联网是开放架构，数据来源五花八门，必须事先规定好通信的数据格式，否则接收方根本无法获得真正发送的数据内容。”应用层”的作用就是规定应用程序使用的数据格式，例如我们TCP协议之上常见的Email、HTTP、FTP等协议，这些协议就组成了互联网协议的应用层。\n如下图所示，发送方的HTTP数据经过互联网的传输过程中会依次添加各层协议的标头信息，接收方收到数据包之后再依次根据协议解包得到数据。\nsocket编程 Socket是BSD UNIX的进程通信机制，通常也称作”套接字”，用于描述IP地址和端口，是一个通信链的句柄。Socket可以理解为TCP/IP网络的API，它定义了许多函数或例程，程序员可以用它们来开发TCP/IP网络上的应用程序。电脑上运行的应用程序通常通过”套接字”向网络发出请求或者应答网络请求。\nSocket把底层的内容给屏蔽掉了，我们只需要关注于Socket的API层面，即可完成网络通信\nsocket图解 Socket是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中，Socket其实就是一个门面模式，它把复杂的TCP/IP协议族隐藏在Socket后面，对用户来说只需要调用Socket规定的相关函数，让Socket去组织符合指定的协议数据然后进行通信。\nGo语言实现TCP通信 TCP协议 TCP/IP(Transmission Control Protocol/Internet Protocol) 即传输控制协议/网间协议，是一种面向连接（连接导向）的、可靠的、基于字节流的传输层（Transport layer）通信协议，因为是面向连接的协议，数据像水流一样传输，会存在黏包问题。\nTCP服务端 一个TCP服务端可以同时连接很多个客户端，例如世界各地的用户使用自己电脑上的浏览器访问淘宝网。因为Go语言中创建多个goroutine实现并发非常方便和高效，所以我们可以每建立一次链接就创建一个goroutine去处理。\nTCP服务端程序的处理流程：\n监听端口 接收客户端请求建立链接 创建goroutine处理链接。 我们使用Go语言的net包实现的TCP服务端代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 // 用于接收请求的方法 func processConn(conn net.Conn) { // 与客户端通信 var tmp [128]byte // 使用for循环监听消息 for { n, err := conn.Read(tmp[:]) if err != nil { fmt.Println(\u0026#34;read from conn failed, err:\u0026#34;, err) return } fmt.Println(conn, string(tmp[:n])) } } func main() { // 本地端口启动服务 listen, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;127.0.0.1:20000\u0026#34;) if err!= nil { fmt.Println(\u0026#34;start server on failed \u0026#34;, err) } // for循环监听 for { // 等待别人来建立连接 conn, err := listen.Accept() if err != nil { fmt.Println(\u0026#34;accept failed, err: \u0026#34;, err) return } go processConn(conn) } } TCP客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func getInput() string { //使用os.Stdin开启输入流 //函数原型 func NewReader(rd io.Reader) *Reader //NewReader创建一个具有默认大小缓冲、从r读取的*Reader 结构见官方文档 in := bufio.NewReader(os.Stdin) //in.ReadLine函数具有三个返回值 []byte bool error //分别为读取到的信息 是否数据太长导致缓冲区溢出 是否读取失败 str, _, err := in.ReadLine() if err != nil { return err.Error() } return string(str) } // tcp client func main() { // 与server端建立连接 conn, err := net.Dial(\u0026#34;tcp\u0026#34;, \u0026#34;127.0.0.1:20000\u0026#34;) if err != nil { fmt.Println(\u0026#34;dial 127.0.0.1:20000 failed, err：\u0026#34;, err) return } // 发送数据 conn.Write([]byte(getInput())) // 关闭流 defer conn.Close() } TCP黏包 黏包实例 服务端代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 // socket_stick/server/main.go func process(conn net.Conn) { defer conn.Close() reader := bufio.NewReader(conn) var buf [1024]byte for { n, err := reader.Read(buf[:]) if err == io.EOF { break } if err != nil { fmt.Println(\u0026#34;read from client failed, err:\u0026#34;, err) break } recvStr := string(buf[:n]) fmt.Println(\u0026#34;收到client发来的数据：\u0026#34;, recvStr) } } func main() { listen, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;127.0.0.1:30000\u0026#34;) if err != nil { fmt.Println(\u0026#34;listen failed, err:\u0026#34;, err) return } defer listen.Close() for { conn, err := listen.Accept() if err != nil { fmt.Println(\u0026#34;accept failed, err:\u0026#34;, err) continue } go process(conn) } } 客户端代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // socket_stick/client/main.go func main() { conn, err := net.Dial(\u0026#34;tcp\u0026#34;, \u0026#34;127.0.0.1:30000\u0026#34;) if err != nil { fmt.Println(\u0026#34;dial failed, err\u0026#34;, err) return } defer conn.Close() // 连续发送20次的hello到服务器 for i := 0; i \u0026lt; 20; i++ { msg := `Hello, Hello. How are you?` conn.Write([]byte(msg)) } } 将上面的代码保存后，分别编译。先启动服务端再启动客户端，可以看到服务端输出结果如下：\n1 2 3 4 5 收到client发来的数据： Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you? 收到client发来的数据： Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you? 收到client发来的数据： Hello, Hello. How are you?Hello, Hello. How are you? 收到client发来的数据： Hello, Hello. How are you?Hello, Hello. How are you?Hello, Hello. How are you? 收到client发来的数据： Hello, Hello. How are you?Hello, Hello. How are you? 客户端分10次发送的数据，在服务端并没有成功的输出10次，而是多条数据 粘 到了一起。\n为什么会出现粘包 主要原因就是tcp数据传递模式是流模式，在保持长连接的时候可以进行多次的收和发。\n“粘包”可发生在发送端也可发生在接收端：\n由Nagle算法造成的发送端的粘包：Nagle算法是一种改善网络传输效率的算法。简单来说就是当我们提交一段数据给TCP发送时，TCP并不立刻发送此段数据，而是等待一小段时间看看在等待期间是否还有要发送的数据，若有则会一次把这两段数据发送出去。 接收端接收不及时造成的接收端粘包：TCP会把接收到的数据存在自己的缓冲区中，然后通知应用层取数据。当应用层由于某些原因不能及时的把TCP的数据取出来，就会造成TCP缓冲区中存放了几段数据。 解决办法 出现”粘包”的关键在于接收方不确定将要传输的数据包的大小，因此我们可以对数据包进行封包和拆包的操作。\n封包：封包就是给一段数据加上包头，这样一来数据包就分为包头和包体两部分内容了(过滤非法包时封包会加入”包尾”内容)。包头部分的长度是固定的，并且它存储了包体的长度，根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。\n我们可以自己定义一个协议，比如数据包的前4个字节为包头，里面存储的是发送的数据的长度。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 // socket_stick/proto/proto.go package proto import ( \u0026#34;bufio\u0026#34; \u0026#34;bytes\u0026#34; \u0026#34;encoding/binary\u0026#34; ) // Encode 将消息编码 func Encode(message string) ([]byte, error) { // 读取消息的长度，转换成int32类型（占4个字节） var length = int32(len(message)) var pkg = new(bytes.Buffer) // 写入消息头 err := binary.Write(pkg, binary.LittleEndian, length) if err != nil { return nil, err } // 写入消息实体 err = binary.Write(pkg, binary.LittleEndian, []byte(message)) if err != nil { return nil, err } return pkg.Bytes(), nil } // Decode 解码消息 func Decode(reader *bufio.Reader) (string, error) { // 读取消息的长度 lengthByte, _ := reader.Peek(4) // 读取前4个字节的数据 lengthBuff := bytes.NewBuffer(lengthByte) var length int32 err := binary.Read(lengthBuff, binary.LittleEndian, \u0026amp;length) if err != nil { return \u0026#34;\u0026#34;, err } // Buffered返回缓冲中现有的可读取的字节数。 if int32(reader.Buffered()) \u0026lt; length+4 { return \u0026#34;\u0026#34;, err } // 读取真正的消息数据 pack := make([]byte, int(4+length)) _, err = reader.Read(pack) if err != nil { return \u0026#34;\u0026#34;, err } return string(pack[4:]), nil } 引申知识点：大端和小端\n接下来在服务端和客户端分别使用上面定义的proto包的Decode和Encode函数处理数据。\n服务端代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 // socket_stick/server2/main.go func process(conn net.Conn) { defer conn.Close() reader := bufio.NewReader(conn) for { msg, err := proto.Decode(reader) if err == io.EOF { return } if err != nil { fmt.Println(\u0026#34;decode msg failed, err:\u0026#34;, err) return } fmt.Println(\u0026#34;收到client发来的数据：\u0026#34;, msg) } } func main() { listen, err := net.Listen(\u0026#34;tcp\u0026#34;, \u0026#34;127.0.0.1:30000\u0026#34;) if err != nil { fmt.Println(\u0026#34;listen failed, err:\u0026#34;, err) return } defer listen.Close() for { conn, err := listen.Accept() if err != nil { fmt.Println(\u0026#34;accept failed, err:\u0026#34;, err) continue } go process(conn) } } 客户端代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 // socket_stick/client2/main.go func main() { conn, err := net.Dial(\u0026#34;tcp\u0026#34;, \u0026#34;127.0.0.1:30000\u0026#34;) if err != nil { fmt.Println(\u0026#34;dial failed, err\u0026#34;, err) return } defer conn.Close() for i := 0; i \u0026lt; 20; i++ { msg := `Hello, Hello. How are you?` data, err := proto.Encode(msg) if err != nil { fmt.Println(\u0026#34;encode msg failed, err:\u0026#34;, err) return } conn.Write(data) } } Go语言实现UDP通信 UDP协议 UDP协议（User Datagram Protocol）中文名称是用户数据报协议，是OSI（Open System Interconnection，开放式系统互联）参考模型中一种无连接的传输层协议，不需要建立连接就能直接进行数据发送和接收，属于不可靠的、没有时序的通信，但是UDP协议的实时性比较好，通常用于视频直播相关领域。\nUDP服务端 使用Go语言的net包实现的UDP服务端代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 // UDP/server/main.go // UDP server端 func main() { listen, err := net.ListenUDP(\u0026#34;udp\u0026#34;, \u0026amp;net.UDPAddr{ IP: net.IPv4(0, 0, 0, 0), Port: 30000, }) if err != nil { fmt.Println(\u0026#34;listen failed, err:\u0026#34;, err) return } defer listen.Close() for { var data [1024]byte n, addr, err := listen.ReadFromUDP(data[:]) // 接收数据 if err != nil { fmt.Println(\u0026#34;read udp failed, err:\u0026#34;, err) continue } fmt.Printf(\u0026#34;data:%v addr:%v count:%v\\n\u0026#34;, string(data[:n]), addr, n) _, err = listen.WriteToUDP(data[:n], addr) // 发送数据 if err != nil { fmt.Println(\u0026#34;write to udp failed, err:\u0026#34;, err) continue } } } UDP客户端 使用Go语言的net包实现的UDP客户端代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 // UDP 客户端 func main() { // 拨号连接 socket, err := net.DialUDP(\u0026#34;udp\u0026#34;, nil, \u0026amp;net.UDPAddr{ IP: net.IPv4(0, 0, 0, 0), Port: 30000, }) if err != nil { fmt.Println(\u0026#34;连接服务端失败，err:\u0026#34;, err) return } defer socket.Close() sendData := []byte(\u0026#34;Hello server\u0026#34;) _, err = socket.Write(sendData) // 发送数据 if err != nil { fmt.Println(\u0026#34;发送数据失败，err:\u0026#34;, err) return } data := make([]byte, 4096) n, remoteAddr, err := socket.ReadFromUDP(data) // 接收数据 if err != nil { fmt.Println(\u0026#34;接收数据失败，err:\u0026#34;, err) return } fmt.Printf(\u0026#34;recv:%v addr:%v count:%v\\n\u0026#34;, string(data[:n]), remoteAddr, n) } ","permalink":"https://ktzxy.top/posts/rqef8kumyy/","summary":"互联网协议介绍","title":"互联网协议介绍"},{"content":" count函数是用来获取表中满足一定条件的记录数，常见用法有三种，count(*),count(1),count(field)，这三种有什么区别？在性能上有何差异？本文将通过测试案例详细介绍和分析。\n原文地址：\nmytecdb.com/blogDetail.php?id=81\n三者有何区别：\ncount(field)不包含字段值为NULL的记录。 count(*)包含NULL记录。 select()与select(1) 在InnoDB中性能没有任何区别，处理方式相同。官方文档描述如下： InnoDB handles SELECT COUNT() and SELECT COUNT(1) operations in the same way. There is no performance difference. 1. 性能对比 通过案例来测试一下count(*)，count(1)，count(field)的性能差异，MySQL版本为5.7.19，测试表是一张sysbench生成的表，表名sbtest1，总记录数2411645，如下：\n1 2 3 4 5 6 7 8 CREATE TABLE sbtest1 ( id int(11) NOT NULL AUTO_INCREMENT, k int(11) DEFAULT NULL, c char(120) NOT NULL DEFAULT \u0026#39;\u0026#39;, pad char(60) NOT NULL DEFAULT \u0026#39;\u0026#39;, PRIMARY KEY (id), KEY k_1 (k) ) ENGINE=InnoDB; 测试SQL语句：\nselect count(*) from sbtest1; select count(1) from sbtest1; select count(id) from sbtest1; select count(k) from sbtest1; select count(c) from sbtest1; select count(pad) from sbtest1;\n针对count()、count(1)和count(id)，加了强制走主键的测试，如下： select count() from sbtest1 force index(primary); select count(1) from sbtest1 force index(primary); select count(id) from sbtest1 force index(primary);\n另外对不同的测试SQL，收集了profile，发现主要耗时都在Sending data这个阶段，记录Sending data值。\n汇总测试结果：\n类型 耗时(s) 索引 Sending data耗时(s) count(*) 0.47 k_1 0.463624 count(1) 0.46 k_1 0.463242 count(id) 0.52 k_1 0.521618 count(*)强制走主键 0.54 primay key 0.538737 count(1)强制走主键 0.55 primary key 0.545007 count(id)强制走主键 0.60 primary key 0.598975 count(k) 0.53 k_1 0.529366 count(c) 0.81 NULL 0.813918 count(pad) 0.76 NULL 0.762040 结果分析：\n从以上测试结果来看，count()和count(1)性能基本一样，默认走二级索引(k_1)，性能最好，这也验证了count()和count(1)在InnoDB内部处理方式一样。 count(id) 虽然也走二级索引(k_1)，但是性能明显低于count()和count(1)，可能MySQL内部在处理count()和count(1)时做了额外的优化。 强制走主键索引时，性能反而没有走更小的二级索引好，InnoDB存储引擎是索引组织表，行数据在主键索引的叶子节点上，走主键索引扫描时，处理的数据量比二级索引更多，所以性能不及二级索引。 count(c)和count(pad)没有走索引，性能最差，但是明显count(pad)比count(c)好，因为pad字段类型为char(60)，小于字段c的char(120)，尽管两者性能垫底，但是字段小的性能相对更好些。 2. count(*)延伸 在5.7.18版本之前，InnoDB处理select count(*) 是通过扫描聚簇索引，来获取总记录数。 从5.7.18版本开始，InnoDB扫描一个最小的可用的二级索引来获取总记录数，或者由SQL hint来告诉优化器使用哪个索引。如果二级索引不存在，InnoDB将会扫描聚簇索引。 执行select count(*)在大部分场景下性能都不会太好，尤其是表记录数特别大的情况下，索引数据不在buffer pool里面，需要频繁的读磁盘，性能将更差。\n3. count(*)优化思路 一种优化方法，是使用一个统计表来存储表的记录总数，在执行DML操作时，同时更新该统计表。这种方法适用于更新较少，读较多的场景，而对于高并发写操作，性能有很大影响，因为需要并发更新热点记录。 如果业务对count数量的精度没有太大要求，可使用show table status中的行数作为近似值。 ","permalink":"https://ktzxy.top/posts/vq96chnpg4/","summary":"MySQL count(),count(1),count(field)区别、性能差异及优化建议","title":"MySQL count(),count(1),count(field)区别、性能差异及优化建议"},{"content":"发展史 C 语言的诞生小故事 1、为什么发明 C 语言：C 语言的诞生是和 UNIX 操作系统的开发密不可分的，原先的 UNIX 操作系统都是用汇编 语言写的，1973 年 UNIX 操作系统的核心用 C 语言改写，从此以后，C 语言成为编写操作系统的主要语言\n、C 语言对其它语言的影响：很多编程语言都深受 C 语言的影响，比如 C++（原先是 C 语言的一个扩展）、C#、 Java、PHP、Javascript、Perl、LPC 和 UNIX 的 C Shell 等。 3、掌握 C 语言的人，再学其它编程语言，大多能很快上手，触类旁通，很多大学将 C 语言作为计算机教学的入门 语言\n4、发明人\n发展历程 ==说明：需要知道 C 语言的两个重要的版本 1. ANSI C (标准 C), C89 2. C99==\nC 语言的特点 1、代码级别的跨平台：由于标准的存在，使得几乎同样的 C 代码可用于多种操作系统，如 Windows、DOS、UNIX\n等等；也适用于多种机型。\n2、使允许直接访问物理地址，对硬件进行操作: 由于 C 语言允许直接访问物理地址，可以直接对硬件进行操作，\n因此它既具有高级语言的功能，又具有低级语言的许多功能，C 语言可用来写系统软件(比如操作系统, 数据库,\n杀毒软件，防火墙, 驱动， 服务器程序)\n3、 C 语言是一个有结构化程序设计、具有变量作用域（variable scope）以及递归功能的过程式语言\n4、C 语言传递参数可以是值传递（pass by value，值），也可以传递指针（a pointer passed by value， 地址）\n5、C 语言中，没有对象，不同的变量类型可以用结构体（struct）组合在一起\n6、预编译处理（preprocessor）, 生成目标代码质量高，程序执行效率高\n快速入门HelloWorld 1、VC++配置启动按钮，步骤如下\n1、点击按钮\n2、点击自定义\n3、添加命令\n4、点击如下图\n5、点击确认按钮，效果如下\n创建一个项目，让其输出 hello world！\n1、创建项目如图\n2、在test01.c中编写如下代码\n1 2 3 4 5 6 7 8 9 10 // 这是一个main函数，是程序执行的入口\t#include \u0026lt;stdio.h\u0026gt; //引入头文件 void main(){ // printf 是一个函数，需要引入头文件才能使用 // printf 是在\u0026lt;stdio.h\u0026gt; 下的一个文件，需要引入它才行 printf(\u0026#34;hello world!\u0026#34;); getchar();// 让窗口停留 } 3、点击运行\n程序运行机制 文件描述\n1、编辑：就是我们编写的 xx.c文件，就是源代码\n2、编译：将这个xx.c文件翻译成 目标文件（xx.obj）\n3、链接：将 目标文件（.obj）+库文件 生成 可执行文件（xx.exe）\n4、执行可执行文件（xx.exe）\n==编译和链接都是在计算机底层实现的==\nC程序运行机制 -图解\n==编译cl.exe 和连接link.exe 是在vs2010软件文件目录的bin目录下==\n目标文件（xx.obj） 、可执行文件（xx.exe） 在计算机中可以找到\n基础知识 转义字符 6个最常用的转义字符\n转义字符 效果 \\n 换行(LF) ，将当前位置移到下一行开头 010 \\r 回车(CR) ，将当前位置移到本行开头 013 \\t 水平制表(HT) （跳到下一个TAB位置） 009 \\ \\ 代表一个反斜线字符’\\’ 092 \\’ 代表一个单引号（撇号）字符 039 \\” 代表一个双引号字符 034 注释 1、 单行注释\n1 // 被注释的内容 2、多行注释\n1 2 3 /* 被注释的内容 */ 标识符 **C 标识符：**用来标识变量、函数，或任何其他用户自定义项目的名称。\n什么是标识符？\n==一个标识符以字母 A-Z 或 a-z 或下划线 _ 开始，后跟零个或多个字母、下划线和数字（0-9）。==\nC 标识符内不允许出现标点字符，比如 @、$ 和 %。C 是区分大小写的编程语言。因此，在 C 中，Manpower 和 manpower 是两个不同的标识符。下面列出几个有效的标识符：\n1 2 mohd zara abc move_name a_123 myname50 _temp j a23b9 retVal 标识符的命名规则和规范\n1、程序中不得出现仅靠大小写区分的相似的标识符，int x, X; 变量x 与X 容易混淆\n2、所有宏定义、枚举常数、常量(只读变量)全用大写字母命名，用下划线分隔单词，\n比如：\n1 const double TAX_RATE = 0.08;//TAX_RATE 只读变量#define FILE_PATH \u0026#34;/usr/tmp\u0026#34; 3、定义变量别忘了初始化。定义变量时编译器并不一定清空了这块内存，它的值可能是无效的数据, 运行程序，会异常退出.\n4、变量名、函数名：多单词组成时，第一个单词首字母小写，第二个单词开始每个单词首字母大写: xxxYyyZzz [驼峰法，小驼峰， 比如 short stuAge = 20;]\n关键字 下表列出了 C 中的保留字。这些保留字不能作为常量名、变量名或其他标识符名称。\n关键字 说明 auto 声明自动变量 break 跳出当前循环 case 开关语句分支 char 声明字符型变量或函数返回值类型 const 定义常量，如果一个变量被 const 修饰，那么它的值就不能再被改变 continue 结束当前循环，开始下一轮循环 default 开关语句中的\u0026quot;其它\u0026quot;分支 do 循环语句的循环体 double 声明双精度浮点型变量或函数返回值类型 else 条件语句否定分支（与 if 连用） enum 声明枚举类型 extern 声明变量或函数是在其它文件或本文件的其他位置定义 float 声明浮点型变量或函数返回值类型 for 一种循环语句 goto 无条件跳转语句 if 条件语句 int 声明整型变量或函数 long 声明长整型变量或函数返回值类型 register 声明寄存器变量 return 子程序返回语句（可以带参数，也可不带参数） short 声明短整型变量或函数 signed 声明有符号类型变量或函数 sizeof 计算数据类型或变量长度（即所占字节数） static 声明静态变量 struct 声明结构体类型 switch 用于开关语句 typedef 用以给数据类型取别名 unsigned 声明无符号类型变量或函数 union 声明共用体类型 void 声明函数无返回值或无参数，声明无类型指针 volatile 说明变量在程序执行中可被隐含地改变 while 循环语句的循环条件 变量 变量相当于内存中一个数据存储空间的表示，你可以把变量看做是一个房间的门牌号，通过门牌号我们可以找 到房间，而通过变量名可以访问到变量(值)。\n变量的三要素： (变量名+值+数据类型)\n变量使用案例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # include \u0026lt;stdio.h\u0026gt; void main(){ int num = 1 ; //整型 double score = 2.3; //小数 char gender = \u0026#39;A\u0026#39;;//字符 char name[] = \u0026#34;尚硅谷\u0026#34;;//字符串 /** 如果输出的是整数 --\u0026gt; %d 如果输出的是小数 --\u0026gt; %f ，如果希望保留到小数点2位 就 --\u0026gt; %.2f 如果输出的是一个字符char --\u0026gt; %c 如果输出的是一个字符串 ---\u0026gt; %s */ printf(\u0026#34;num=%d score=%.2f gender=%c name=%s\u0026#34;,num,score,gender,name); getchar();// 让窗口停留 } ==变量使用注意事项==\n变量表示内存中的一个存储区域（不同的数据类型，占用的空间大小不一样）\n该区域有自己的名称 和类型\n变量必须先声明，后使用\n该区域的数据可以在同一类型范围内不断变化\n变量在同一个作用域内不能重名\n代码演示\n数据类型 在 C 语言中，数据类型指的是用于声明不同类型的变量或函数的一个广泛的系统。变量的类型决定了变量存储占用的空间，以及如何解释存储的位模式。\nC 中的类型可分为以下几种：\n==注意：==\n1、在c中，没有字符串类型，是使用字符串数组来表示字符串\n2、在不同系统上，部分数据类型的字节长度不一样：例如int ，字节数可能为2，也可能为4\n整数类型 下表列出了关于标准整数类型的存储大小和值范围的细节：\n类型 存储大小 值范围 char 1 字节 -128 到 127 或 0 到 255 unsigned char 1 字节 0 到 255 signed char 1 字节 -128 到 127 int / signed int 2 或 4 字节 -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647 unsigned int 2 或 4 字节 0 到 65,535 或 0 到 4,294,967,295 short 2 字节 -32,768 到 32,767 unsigned short 2 字节 0 到 65,535 long 4 字节 -2,147,483,648 到 2,147,483,647 unsigned long 4 字节 0 到 4,294,967,295 ==解释：signed int 就是不区分正负的类型 精度\u0026gt; int的精度==\n==整型的使用细节==\n在不同操作系统和不同位数系统下，整型字节的差异\n1、各种类型的存储大小与操作系统、系统位数和编译器有关*，目前通用的以 64 位系统为主在实际工作中，c 程序通常运行在 linux/unix 操作系统下\n2、 C 语言的整型类型，分为有符号 signed 和无符号 unsigned 两种，默认是 signed\n3、C 程序中整型常声明为 int 型，除非不足以表示大数，才使用 long long\n4、 bit(位): 计算机中的最小存储单位。byte(字节):计算机中基本存储单元。\n1byte = 8bit [二进制再详细说，简单举例一个 short 3 和 int 3 ]\n示意图\nshort 3 在内存中占有 2 字节\nint 3 在内存中占有 4 个字节\n浮点型 下表列出了关于标准浮点类型的存储大小、值范围和精度的细节：\n类型 存储大小 值范围 精度 float 单精度 4 字节 1.2E-38 到 3.4E+38 6 位小数 double 双精度 8 字节 2.3E-308 到 1.7E+308 15 位小数 浮点数都是近似值\n==细节案例：==\n1 2 3 4 5 6 7 8 9 10 11 # include \u0026lt;stdio.h\u0026gt; void main(){ float num1 = 1.1; //“初始化”: 从“double”到“float”截断 //float num1 = 1.1f;// 1.1f就是float //double num3 = 1.3; // ok //double d4 = 5.12 double num5 = .512;// 等价于0.512 double num6 = 5.12e2;// 等价于5.12*(10的二次方 double num6 = 5.12e-2;// 等价于5.12*(10的负二次方) getchar(); } 字符类型char 字符类型可以表示单个字符,字符类型是 char，char 是 1 个字节(可以存字母或者数字)，多个字符称为字符串，在\nC 语言中 使用 char 数组 表示，数组不是基本数据类型，而是构造类型[关于数组我们后面详细讲解.]\n简单案例：\n1 2 3 4 5 6 7 8 # include \u0026lt;stdio.h\u0026gt; void main(){ char c1 = \u0026#39;A\u0026#39;; char c2 = \u0026#39;0\u0026#39;; char c3 = \u0026#39;\\t\u0026#39;; printf(\u0026#34;c1=%c c3=%c c2=%c\u0026#34;, c1, c3, c2); //%c 表示以字符的形式输出 getchar(); } 结果：\n(1)、字符常量是用单引号(\u0026rsquo;\u0026rsquo;)括起来的单个字符。例如：char c1 = \u0026lsquo;a\u0026rsquo;; char c3 = \u0026lsquo;9\u0026rsquo;;\n(2)、 C 中还允许使用转义字符‘\\’来将其后的字符转变为特殊字符型常量。例如：char c3 = ‘\\n’; // \u0026lsquo;\\n\u0026rsquo;表示换 行符\n(3)、 在 C 中，char 的本质是一个整数，在输出时，是 ASCII 码对应的字符。\n(4)、 可以直接给 char 赋一个整数，然后输出时，会按照对应的 ASCII 字符输出 [97]\n(5)、 char 类型是可以进行运算的，相当于一个整数，因为它都对应有 Unicode 码.\n(6)、 案例演示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # include \u0026lt;stdio.h\u0026gt; void main(){ char c1 = \u0026#39;a\u0026#39;; char c2 = \u0026#39;b\u0026#39;; char c3 = 97;//这时当我们以%c 输出时，就会安装 ASCII 编码表(理解 字符 \u0026lt;==\u0026gt; 数字 对应关系 ) 对应的 97 对应字 符输出 int num = c1+10; // =97+10 = 107 printf(\u0026#34;c1=%c c2=%c c3=%c\u0026#34;, c1, c2, c3); //%c 表示以字符的形式输出 // 注意： // 1、vs2010 编辑器是c89 // 2、要求变量的定义在语句之前 printf(\u0026#34;num=%d\u0026#34;,num); //“num”: 未声明的标识符 getchar(); } 字符类型本质探讨:\n字符型 存储到 计算机中，需要将字符对应的码值（整数）找出来\n存储：字符\u0026rsquo;a\u0026rsquo;——\u0026gt;码值 (97)——\u0026gt;二进制 (1100001)——\u0026gt;存储()\n读取：二进制(1100001)——\u0026gt;码值(97)——\u0026gt; 字符\u0026rsquo;a\u0026rsquo;——\u0026gt;读取(显示)\n布尔类型(boolean) 1、 C 语言标准(C89)没有定义布尔类型，所以 C 语言判断真假时以 0 为假，非 0 为真 [案例]\n2、但这种做法不直观，所以我们可以借助 C 语言的宏定义 。\n3、C 语言标准(C99)提供了_Bool 型，_Bool 仍是整数类型，但与一般整型不同的是，Bool 变量只能赋值为 0 或 1，\n_非 0 的值都会被存储为 1，C99 还提供了一个头文件 \u0026lt;stdbool.h\u0026gt; 定义了 bool 代表_Bool，true 代表 1，false 代\n表 0。只要导入 stdbool.h ，就能方便的操作布尔类型了 , 比如 bool flag = false\n条件控制语句； if\n循环控制语句； while \u0026hellip;\n1 2 3 4 5 6 7 # include \u0026lt;stdio.h\u0026gt; void main(){ int isPass = -1; if(isPass) { // 0表示假，非0表示真 printf(\u0026#34;通过考试\u0026#34;); } } 宏定义案例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # include \u0026lt;stdio.h\u0026gt; // 宏定义 #define BOOL int #define TURE 1 #define FALSE 0 void main(){ BOOL isOK = TURE; // 等价于int isOK = 1 if(isOK){ printf(\u0026#34;ok\u0026#34;); } getchar();\t} 基本数据类型转换 自动类型转换\n当 C 程序在进行赋值或者运算时，精度小的类型自动转换为精度大的数据类型\n数据类型自动转换表规则\n演示1：\nchar-\u0026gt;int-\u0026gt;double（精度低的转化成精度高的）\n1 2 3 4 5 6 7 8 # include \u0026lt;stdio.h\u0026gt; void main(){ char c1 = \u0026#39;a\u0026#39;; //ok int num1 = c1; // ok double d1 = num1; //ok printf(\u0026#34;d1= %.2f\u0026#34;,d1); getchar(); } 演示2:\n不同类型相加：精度低的相转化为精度高的再进行相加（short转成int，再和int相加）\n1 2 3 4 5 6 7 8 # include \u0026lt;stdio.h\u0026gt; void main(){ short s1 = 10; int num2 = 20; int num3 = s1 + num2;\t//ok printf(\u0026#34;num3 = %d\u0026#34;,num3); getchar(); } 演示3：\n精度高的转化为精度低的类型（double赋值给float），出现精度损失现象\n1 2 3 4 5 6 7 8 # include \u0026lt;stdio.h\u0026gt; void main(){ float f1 = 1.1f; //ok double d2 = 4.58667435; f1 = d2; // 出现精度损失 (double -\u0026gt; float ) printf(\u0026#34;f1=%.8f\u0026#34;, f1); // 期望： 4.58667435 getchar(); } 运行结果：\n强制类型转换 将精度高的数据类型转换为精度小的数据类型。使用时要加上强制转换符 ( )，但可能造成精度降低或溢出,格外要注意。\n1 2 3 4 5 6 7 8 9 10 11 12 13 # include \u0026lt;stdio.h\u0026gt; void main(){ double d1 = 1.934; int num = (int)d1; //这里注意，不是进行四舍五入，而是直接截断小数后的部分 //printf(\u0026#34;num = %d\u0026#34;,num); //强制转换只对最近的数有效, 如果希望针对更多的表达式转换，使用（） int num2 = (int)3.5 * 10 + 6 * 1.5; // 3 * 10 + 6 * 1.5 = 30 + 9.0 = 39.0 int num3 = (int)(3.5 * 10 + 6 * 1.5); // 35.0 + 9.0 = 44.0 -\u0026gt; int = 44 getchar(); }\t练习案例：\n1 2 3 4 5 6 7 8 9 10 11 # include \u0026lt;stdio.h\u0026gt; void main(){ char c = \u0026#39;a\u0026#39;; int i = 5; float d = .314F; double d2 = 1.0; //double result = c+i+d; // float -\u0026gt; double char result = c+i+d+d2; // 提示? // 警告 double -\u0026gt; char getchar(); } 总结：\n当进行数据的从 精度高——\u0026gt;精度低，就需要使用到强制转换 强转符号只针对于最近的操作数有效，往往会使用小括号提升优先级 键盘输入语句 在编程中，需要接收用户输入的数据，就可以使用键盘输入语句来获取。\n演示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # include \u0026lt;stdio.h\u0026gt; void main() { char name[10] = \u0026#34;\u0026#34;; int age = 0; double sal = 0.0; char gender = \u0026#39; \u0026#39;; printf(\u0026#34;请输入用户名：\u0026#34;); scanf(\u0026#34;%s\u0026#34;,name); printf(\u0026#34;请输入年龄：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;age); // 因为我们将得到输入存放到 age 变量指向地址,因此需要加 \u0026amp; printf(\u0026#34;请输入薪水：\u0026#34;); scanf(\u0026#34;%lf\u0026#34;,\u0026amp;sal); // lf%:接受double类型 printf(\u0026#34;请输入性别（m/f）：\u0026#34;); scanf(\u0026#34;%c\u0026#34;,\u0026amp;gender); //回车被gender接受了，所以为空 scanf(\u0026#34;%c\u0026#34;,\u0026amp;gender);// 这个地方才是等待用户输入 printf(\u0026#34;\\n name %s age %d sal %.2f gender %c\u0026#34;,name,age,sal,gender); getchar();//接受到一个回车 getchar();//这个getchar()才会让控制台暂停 } ASCII 码介绍(了解) 1、在计算机内部，所有数据都使用==二进制==表示。每一个二进制位（bit）有 0 和 1 两种状态，因此 8 个二进制\n位就可以组合出 256 种状态，这被称为一个字节（byte）。一个字节一共可以用来表示 ==256 （2的8次方）==种不同的状态，\n每一个状态对应一个符号，就是 256 个符号，从 0000000 到 11111111。\n2、 ASCII 码：上个世纪 60 年代，美国制定了一套字符编码，对英语字符与二进制位之间的关系，做了统一规定。\n这被称为 ASCII 码。ASCII 码一共规定了 127 个字符的编码，比如空格“SPACE”是 32（二进制 00100000）， 大写的字母 A\n65（二进制 01000001）。这 128 个符号（包括 32 个不能打印出来的控制符号），只占用了一\n个字节的后面 7 位，最前面的 1 位统一规定为 0。\n3、 看一个完整的 ASCII 码表\nASCII 码表参考地址：https://tool.oschina.net/commons?type=4\nC 语言标准库 – 参考手册 介绍：C 标准库是一组 C 内置函数、常量和头文件，比如 \u0026lt;stdio.h\u0026gt;、\u0026lt;stdlib.h\u0026gt;、\u0026lt;math.h\u0026gt;，等等。这个标\n准库可以作为 C 程序员的参考手册。\n标准库参考地址：https://www.runoob.com/cprogramming/c-data-types.html\n以上内容课后习题 题目1：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include \u0026lt;stdio.h\u0026gt; void main(){ /*要求： a、用变量将姓名、年龄、成绩、性别、爱好存储 b、添加适当的注释 c、添加转义字符*/ //分析：使用不同的变量来保存对应的数据 char name[10] = \u0026#34;张三\u0026#34;; //字符数组，存放字符串 short age = 23; float score = 78.5f; char gender = \u0026#39;M\u0026#39;; //男生尚硅谷高校大学生 C 语言课程 char hobby[20] = \u0026#34;篮球，足球\u0026#34;; printf(\u0026#34;姓名\\t 年龄\\t 成绩\\t 性别 \\t 爱好\\n%s\\t%d\\t%.2f\\t%c\\t%s\u0026#34;, name,age,score,gender,hobby); getchar(); } 题目2：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include \u0026lt;stdio.h\u0026gt; void main(){ int number1; int number2; int number3; int number4 = 50; int number5; number1 = 10; number2 = 20; number3 = number1 + number2; //30 printf(\u0026#34;\\nNumber3 = %d\u0026#34; , number3);//30 number5 = number4 - number3;//20 printf(\u0026#34;\\nNumber5 = %d\u0026#34; , number5);//20 getchar(); } 题目3、4：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include \u0026lt;stdio.h\u0026gt; void main() { // ************************************* // 小小计算器 //************************************* //10 + 5 =15 //10 - 5 = 5 //10 * 5 = 50 //10 / 5 = 2 //分析 //1. 定义两个 int //2. 根据要求进行计算，得到不同结果，可以再定义变量 int n1 = 10; int n2 = 5; int sum = n1 + n2; int sub = n1 - n2; int mul = n1 * n2; int div = n1 / n2; int mod = n1 % n2; int num = 11; //输出 printf(\u0026#34;\\n*************************************\u0026#34;); printf(\u0026#34;\\n 小小计算器\u0026#34;); printf(\u0026#34;\\n*************************************\u0026#34;); printf(\u0026#34;\\n %d + %d = %d\u0026#34;, n1, n2, sum); printf(\u0026#34;\\n %d - %d = %d\u0026#34;, n1, n2, sub); printf(\u0026#34;\\n %d * %d = %d\u0026#34;, n1, n2, mul); printf(\u0026#34;\\n %d / %d = %d\u0026#34;, n1, n2, div); printf(\u0026#34;\\n %d 模 %d = %d\u0026#34;, n1, n2, mod); //判断 num 是不是偶数还是基数 //if-else 后面要学习的分支结构，后面会详解讲解 if(num % 2 == 0) { //偶数 printf(\u0026#34;\\n%d 是偶数\u0026#34;, num); } else { printf(\u0026#34;\\n%d 是奇数\u0026#34;, num); } getchar(); } 常量 整数常量 整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数：0x 或 0X 表示十六进制，0 表示八进制，不带前缀则默认表示十\n进制。整数常量也可以带一个后缀，后缀是 U 和 L 的组合，U 表示无符号整数（unsigned），L 表示长整数（long）。后缀可以是大\n写，也可以是小写，U 和 L 的顺序任意\n整数常量举例说明：\n1 2 3 4 5 6 7 8 9 10 11 12 13 85 /* 十进制 */ 0213 /* 八进制 */ 0x4b /* 十六进制 */ 八进制和十六进制后面解释 30 /* 整数 */ 30u /* 无符号整数 */ 30l /* 长整数 */ 30ul /* 无符号长整数 */ 浮点常量 浮点常量由整数部分、小数点、小数部分和指数部分组成。您可以使用小数形式或者指数形式来表示浮点常量\n1 2 3 3.14159; //double 常量 314159E-5; // 科学计数法 3.1f; //float常量 字符常量 字符常量是括在单引号中，例如，\u0026lsquo;x\u0026rsquo; 可以存储在 char 类型的变量中。字符常量可以是一个普通的字符（例如 \u0026lsquo;x\u0026rsquo;）、一个转义序列（例如 \u0026lsquo;\\t\u0026rsquo;）\n1 2 3 4 5 6 \u0026#39;X\u0026#39; \u0026#39;Y\u0026#39; \u0026#39;A\u0026#39; \u0026#39;b\u0026#39; \u0026#39;1\u0026#39; \u0026#39;\\t 字符串常量 1 2 3 4 \u0026#34;hello, world\u0026#34; \u0026#34;北京\u0026#34; \u0026#34;hello \\ world\u0026#34; 常量的定义 1、使用 #define 预处理器 2、使用 const 关键字\n#define 预处理器\n1 #define 常量名 常量值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include \u0026lt;stdio.h\u0026gt; #define PI 3.14 //定义常量 PI 常量值 3.14 int main() { //PI = 3.1415 可以吗?=》 不可以修改，因为 PI 是常量 //可以修改 PI 值? //PI = 3.1415; //提示 = 左值 必须是可修改的值 double area; double r = 1.2;//半径 area = PI * r * r; printf(\u0026#34;area=%.2f\u0026#34;,area); getchar(); return 0; } const\n可以使用 const 声明指定类型的常量\n1 const 数据类型 常量名 = 常量值; //即就是一个语句 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include \u0026lt;stdio.h\u0026gt; //1. const 是一个关键字，规定好，表示后面定义了一个常量 //2. PI 是常量名，即是一个常量，常量值就是 3.14 //3. PI 因为是常量，因此不可以修改 //4. const 定义常量时，需要加 分号 const double PI = 3.14; int main() { //PI = 3.1415 可以吗? double area; double r = 1.2; area = PI * r * r; printf(\u0026#34;面积 : %.2f\u0026#34;, area); getchar(); return 0; } const 和 #define 的区别\n1、const 定义的常量时，带类型，define 不带类型\n2、const 是在 ==编译、运行==的时候起作用，而 define 是在编译的==预处理==阶段起作用\n3、define 只是简单的替换，没有类型检查。简单的字符串替换会导致==边界效应==\n4、const 常量可以进行调试的，define 是不能进行调试的，主要是预编译阶段就已经替换掉了，调试的时候就没它\n了\n5、const 不能重定义，不可以定义两个一样的，而 define 通过 undef 取消某个符号的定义，再重新定义\n6、define 可以配合#ifdef、 #ifndef、 #endif 来使用， 可以让代码更加灵活，比如我们可以通过#define 来 启动\n或者关闭 调试信息。\n区别6的案例：\n1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;stdio.h\u0026gt; #define DEBUG void main() { #ifdef DEBUG //如果定义过 DEBUF printf(\u0026#34;ok, 调试信息\u0026#34;); #endif #ifndef DEBUG //如果没有定义过 DEBUF printf(\u0026#34;hello, 另外的信息\u0026#34;); #endif getchar(); } 运算符 优先级 运算符优先级和结合性一览表\n==上表中可以总结出如下规律：==\n结合方向只有三个是从右往左，其余都是从左往右。 所有双目运算符中只有赋值运算符的结合方向是从右往左。 另外两个从右往左结合的运算符也很好记，因为它们很特殊：一个是单目运算符，一个是三目运算符。 C语言中有且只有一个三目运算符。 逗号运算符的优先级最低，要记住。 此外要记住，对于优先级：算术运算符 \u0026gt; 关系运算符 \u0026gt; 逻辑运算符 \u0026gt; 赋值运算符。逻辑运算符中“逻辑非 !”除外。 运算符是一种特殊的符号，用以表示数据的运算、赋值和比较等。\n算术运算符 (+, -, * , / , %)\n关系运算符（比较运算符）(比如 \u0026gt; \u0026gt;= \u0026lt; \u0026lt;= == 等等)\n逻辑运算符 (\u0026amp;\u0026amp; 逻辑与 || 逻辑或 ! 逻辑非)\n赋值运算符 (= += -= ..)\n位运算符 (\u0026amp; 按位与 | 按位或 ^ 按位异或 ~ 按位取反等等)\n三元运算符 ( 表达式 ? 表达1 : 表达2)\n算术运算符 算术运算符是对==数值类型的变量==进行运算的，在C程序中使用的非常多。\n案例演示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include \u0026lt;stdio.h\u0026gt; void main() { //处理的流程 10 / 4 = 2.5 ==截取整数==\u0026gt; 2 =\u0026gt; 2.00000 double d1 = 10 / 4; // double d2 = 10.0 / 4; //如果希望保留小数，参与运算数必须有浮点数 //给大家一个取模的公式 // a % b = a - a / b * b int res1 = 10 % 3; // 求 10/3 的余数 1 int res2 = -10 % 3; // = -10 - (-10) / 3 * 3 = -10- (-3) * 3 = -10 + 9 = -1 int res3 = 10 % -3; // 10 - 10 / (-3) * (-3) = 10 - 9 = 1 int res4 = -10 % -3; // ? -1 //++ 的使用 int i = 10; int j = i++; // 运算规则等价是 int j = i; i = i + 1; ==\u0026gt; j = 10, i=11 int k = ++i; // 运算规则等价 i = i + 1; int k = i; ===\u0026gt; i=12, k =12 printf(\u0026#34;\\n i=%d j=%d\u0026#34;, i, j); //i=12 j=10; printf(\u0026#34;\\n i=%d k=%d\u0026#34;, i ,k );// i = 12 k = 12 printf(\u0026#34;\\nd1=%f d2=%f res1=%d res2=%d res3=%d res4=%d\u0026#34;, d1, d2, res1, res2, res3, res4); //++ 或者 -- 还可以独立使用, 就相当于自增 //k++ 等价于 k = k +1 //++k 等价于 k= k +1 //如果独立使用 ++k 和 k++ 完全等价 k++; // k = 13 ++k;// k = 14 printf(\u0026#34;\\nk=%d\u0026#34;, k); //k = 14 getchar(); } 细节说明\n对于除号“/”，它的整数除和小数除是有区别的：整数之间做除法时，只保留整数部分而舍弃小数部分。 例如：int x= 10/3 ,结果是 当对一个数取模时，可以等价 a%b=a-a/b*b ， 这样我们可以看到 取模的一个本质运算。 当 自增 当做一个独立语言使用时，不管是 ++i; 还是 i++; 都是一样的，等价 当 自增 当做一个 表达式使用时 j = ++i 等价 i = i + 1; j = i; 当 自增 当做一个 表达式使用时 j = i++ 等价 j = i; i = i + 1; 关系运算符 1、关系运算符的结果要么是==真(非0 表示)==，要么是==假(0 表示)== 2、关系表达式 经常用在 if结构的条件中或循环结构的条件中\n细节说明\n1、 关系运算符的结果要么是真(非0 表示, 默认使用1)，要么是 假(0 表示)\n2、 关系运算符组成的表达式，我们称为关系表达式。 a \u0026gt; b\n3、比较运算符 \u0026ldquo;==\u0026quot;(关系运算符) 不能误写成 \u0026ldquo;=\u0026rdquo; (赋值)\n逻辑运算符 用于连接多个条件（一般来讲就是关系表达式），最终的结果要么是真(非0 表示)，要么是 假(0 表示) 。\n逻辑运算符一览\n下表显示了 C 语言支持的所有逻辑运算符。假设变量 A 的值为 1，变量 B 的值为 0\n演示案例：\n1、\u0026amp;\u0026amp;的使用\neq01\n1 2 3 4 5 6 7 8 9 10 # include \u0026lt;stdio.h\u0026gt; void main() { double score = 70;//成绩 if(score\u0026gt;=60 \u0026amp;\u0026amp; score\u0026lt;=80){ printf(\u0026#34;ok1\u0026#34;); } else { printf(\u0026#34;ok2\u0026#34;); } getchar(); } eq02\n1 2 3 4 5 6 7 8 9 # include \u0026lt;stdio.h\u0026gt; void main() { int a = 10, b = 99; if(a \u0026lt; 20 \u0026amp;\u0026amp; b++\u0026gt;100) { printf(\u0026#34;ok100\u0026#34;); } printf(\u0026#34;b=%d\\n\u0026#34;, b); //b=100 getchar(); } 3、||的使用\n1 2 3 4 5 6 7 8 9 # include \u0026lt;stdio.h\u0026gt; void main() { int a = 10, b = 99; if(a \u0026lt; 5 || b++\u0026gt;100) { printf(\u0026#34;ok100\u0026#34;); } printf(\u0026#34;b=%d\\n\u0026#34;, b); getchar(); } 赋值运算符 赋值运算符就是将=某个运算后的值=，赋给==指定的变量==。\n赋值运算符特点\n1、运算顺序从右往左\n2、赋值运算符的左边 只能是变量,右边 可以是变量、表达式、常量值\n3、复合赋值运算符等价于下面的效果\n比如：a+=3;等价于a=a+3;\n位运算符 位运算符作用于位， 并逐位执行操作。\n三目运算符 基本语法：\n==条件表达式 ? 表达式1: 表达式2;==\n1)、如果条件表达式为非0 (真)，运算后的结果是表达式1；\n2)、如果条件表达式为0 (假)，运算后的结果是表达式2；\n3)、口诀: 一灯大师 =》 一真大师\n演示案例：\n1 2 3 4 5 6 7 8 # include \u0026lt;stdio.h\u0026gt; void main() { int a = 10; int b = 99; int res = a \u0026gt; b ? a++ : b--; printf(\u0026#34;res=%d\u0026#34;,res); ///res =99 getchar(); } 演示2：求3个数之间的最大值\n1 2 3 4 5 int a = 10; int b = 100; int c = 199; int max = a \u0026gt; b ? a : b int max2 = max\u0026gt;c ? max : c // max2为三个数中的最大值 运算符优先级 运算符优先级和结合性一览表\n表地址：https://blog.csdn.net/u013630349/article/details/47444939\n上表中可以总结出如下规律：\n结合方向只有三个是从右往左，其余都是从左往右。 所有==赋双目运算符中只有值运算符==的结合方向是从右往左。 另外两个从右往左结合的运算符也很好记，因为它们很特殊：一个是==单目运算符==，一个是==三目运算符==。 C语言中有且只有一个三目运算符。 ==逗号运算符的优先级最低==，要记住。 此外要记住，对于优先级：==算数运算符 \u0026gt; 关系运算符 \u0026gt; 逻辑运算符 \u0026gt; 赋值运算符 \u0026gt; 逗号运算符==。逻辑运算符中“逻辑非 !”除外。 不需要刻意去记忆，在使用中去熟练它！ ==一些容易出错的优先级问题==\n上表中，优先级同为1 的几种运算符如果同时出现，那怎么确定表达式的优先级呢？这是很多初学者迷糊的地方。下表就整理了这些容易出错的情况：\n优先级问题 表达式 经常误认为的结果 实际结果 . 的优先级高于 *（-\u0026gt; 操作符用于消除这个问题） *p.f p 所指对象的字段 f，等价于： (*p).f 对 p 取 f 偏移，作为指针，然后进行解除引用操作，等价于： *(p.f) [] 高于 * int *ap[] ap 是个指向 int 数组的指针，等价于： int (*ap)[] ap 是个元素为 int 指针的数组，等价于： int *(ap []) 函数 () 高于 * int *fp() fp 是个函数指针，所指函数返回 int，等价于： int (*fp)() fp 是个函数，返回 int*，等价于： int* ( fp() ) == 和 != 高于位操作 (val \u0026amp; mask != 0) (val \u0026amp;mask) != 0 val \u0026amp; (mask != 0) == 和 != 高于赋值符 c = getchar() != EOF (c = getchar()) != EOF c = (getchar() != EOF) 算术运算符高于位移 运算符 msb \u0026laquo; 4 + lsb (msb \u0026laquo; 4) + lsb msb \u0026laquo; (4 + lsb) 逗号运算符在所有运 算符中优先级最低 i = 1, 2 i = (1,2) (i = 1), 2 这些容易出错的情况，希望读者好好在编译器上调试调试，这样印象会深一些。一定要多调试，光靠看代码，水平是很难提上来的。调试代码才是最长水平的。\n运算符总结案例： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # include \u0026lt;stdio.h\u0026gt; void main() { //定义变量保存 秒数，打印输出 xx 小时 xx 分钟 xx 秒 //思路 //1. 定义变量保存 秒数 second //2. 定义变量保存 小时 hour //3. 定义变量保存 分钟 min //4. 定义变量保存 剩余描述 leftSecond int second = 894567; int hour = second / 3600 ; // 一个小时有 3600 秒 int min = second % 3600 / 60; // int leftSecond = second % 60; printf(\u0026#34;%d 秒 合%d 小时%d 分钟%d 秒\u0026#34;, second, hour, min, leftSecond); getchar(); } 二进制和位运算 （1）、其他进制转成进制 二进制转换成十进制示例\n1 0123 = 3 * 8 ^ 0 + 2 * 8 ^ 1 + 1 * 8^2 = 3+16+64=83 十六进制转换成十进制示例\n1 0x34A = 10 * 16 ^ 0 + 4 * 16 ^1 + 3 * 16^2 = 10+64+768=842 （2）、十进制转其他进制 十进制转换成二进制\n==规则：==将该数不断除以2，直到商为0为止，然后将每步得到的余数倒过来，就是对应的二进制。\n案例：请将 56 转成二进制\n1 56 =\u0026gt; 111000 如图：\n十进制转换成八进制\n规则：将该数不断除以8，直到商为0为止，然后将每步得到的余数倒过来，就是对 应的八进制。 案例：请将 156 转成八进制\n156 =\u0026gt;234\n十进制转换成十六进制\n规则：将该数不断除以16，直到商为0为止，然后将每步得到的余数倒过来，就是 对应的十六进制。 案例：请将 356 转成十六进制\n356 =\u0026gt; 164\n（3）、二进制转换成八进制、十六进制 二进制转换成八进制\n规则：从低位开始,将二进制数每==三位一组==，转成对应的==8进制==数即可。 案例：请将 11 010 101 转成八进制\n1 11010101 =\u0026gt; 0 3 2 5 二进制转换成十六进制\n规则：低位开始，将二进制数每四位一组，转成对应的==16进制==数即可。 案例：请将 11010101 转成十六进制\n1 11010101 = 0xD5 （4）、八进制、十六进制转成二进制 八进制转换成二进制\n规则：将八进制数每1位，转成对应的一个3位的二进制数即可。 案例：请将 0237 转成二进制\n1 0237 = 10011111 十六进制转换成二进制\n规则：将十六进制数每1位，转成对应的4位的一个二进制数即可。 案例：请将 0x23B 转成二进制\n1 2 B=11=\u0026gt; 1011 0x23B = 1000111011 位运算的思考题\n==请看下面的代码段，回答 a,b,c,d,e 结果是多少?==\n1 2 3 4 5 6 7 8 9 10 # include \u0026lt;stdio.h\u0026gt; void main() { int a=1\u0026gt;\u0026gt;2; // 1 向右位移2位 , 这里还涉及到二进制中 原码，反码，补码 int b=-1\u0026gt;\u0026gt;2; int c=1\u0026lt;\u0026lt;2;// int d=-1\u0026lt;\u0026lt;2;// //a,b,c,d,e结果是多少 printf(\u0026#34;a=%d b=%d c=%d d=%d \u0026#34;,a,b,c,d); getchar(); } ==请回答在C中，下面的表达式运算的结果是: (位操作)==\n1 2 3 4 5 6 7 ~2=? // 按位取反 2\u0026amp;3=? 2|3=? ~-5=? 13\u0026amp;7=? 5|4=? -3^3=? 二进制再运算中的说明\n二进制是逢2进位的进位制，0、1是基本算符。现代的电子计算机技术全部采用的是二进制，因为它只使用0、1两个数字符号，非常简单方便，易于用==电子方式实现==。计算机内部处理的信息，都是采用二进制数来表示的。二进制（Binary）数用0和1两个数字及其组合来表示任何数。进位规则是“逢2进1”，数字1在不同的位上代表不同的值，按从右至左的次序，这个值以二倍递增\n原码、反码、补码 二进制的最高位是符号位: 0表示正数,1表示负数 正数的原码，反码，补码都一样 (三码合一) ==负数的反码===它的原码符号位不变，其它位取反(0-\u0026gt;1,1-\u0026gt;0) ==负数的补码===它的反码+1 0的反码，补码都是0 在计算机运算的时候，都是==以补码的方式==来运算的 位运算符 中位运算符介绍\n它们的运算规则是：\n按位与\u0026amp; ： 两位全为１，结果为1，否则为0 按位或| : 两位有一个为1，结果为1，否则为0 按位异或 ^ : 两位一个为0,一个为1，结果为1，否则为0 按位取反 : 0-\u0026gt;1 ,1-\u0026gt;0 比如：~2=? ~-5=? 2\u0026amp;-3=? 2|3=? 2^3=?\n~2=?\n求-3的补码：\n程序的控制结构 在程序中，程序运行的流程控制决定程序是如何执行的，是我们必须掌握的，主要有三大流程控制语句。\n顺序结构 程序从上到下逐行地执行，中间没有任何判断和跳转\n选择结构 分支控制if-else介绍：单分支、双分支、多分支\n单分支\n基本语法：\n1 2 3 if(条件表达式){ 执行代码块； } ==说明：当条件表达式为真 (非0) 时，就会执行 { } 的代码，返回假(0) 时，不会执行{ } 的代码==\n流程图演示：\n双分支\n基本语法：\n1 2 3 4 5 6 if(条件表达式){ 执行代码块1; } else{ 执行代码块2; } ==说明：当条件表达式成立(为真)，执行代码块1，否则执行代码块2==\n双分支对应的流程图:\n多分支\n基本语法：\n1 2 3 4 5 6 7 8 9 10 if(条件表达式1){ 执行代码块1; } else if (条件表达式2){ 执行代码块2; } …… else{ 执行代码块n; } 嵌套分支\n在一个分支结构中又完整的嵌套了另一个完整的分支结构，里面的分支的结构称为内层分支外面的分支结构称为外层分支。\n嵌套分支不适合过多，最多不要超过3层\n基本语法：\n1 2 3 4 5 6 7 if(){ if(){ //被包含的可以是单分支，双分支，多分支 }else{ } } 案例：\n参加百米运动会，如果用时8秒以内进入决赛，否则提示淘汰。并且根据性别提示进入男子组或女子组。【可以让学员先练习下】, 输入成绩和性别，进行判断。1分钟思考 思路\ndouble second; char gender;\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # include \u0026lt;stdio.h\u0026gt; void main() { /* 参加百米运动会，如果用时8秒以内进入决赛，否则提示淘汰。并且根据性别提示进入男子组或女子组。 【可以让学员先练习下】, 输入成绩和性别，进行判断。1分钟思考 分析： 1、用double second;保存用的时间 2、用char gender 保存性别\tdouble second; char gender; */ double second = 0.0; char gender = \u0026#39; \u0026#39;; printf(\u0026#34;请输入运动成绩-时间(s)：\u0026#34;); scanf(\u0026#34;%lf\u0026#34;,\u0026amp;second); if(second \u0026lt;=8 ){ printf(\u0026#34;请输入性别(m/f)\u0026#34;); // 接收到上次回车 scanf(\u0026#34;%c\u0026#34;, \u0026amp;gender); scanf(\u0026#34;%c\u0026#34;, \u0026amp;gender); // 这次才接收到性别 if(gender == \u0026#39;m\u0026#39;){ printf(\u0026#34;请进入男子组\u0026#34;); }else{ printf(\u0026#34;请进入女子组\u0026#34;); } }else{ printf(\u0026#34;你被淘汰了！\u0026#34;); } getchar(); getchar(); } switch分支结构\n基本语法:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 switch(表达式){ case 常量1： //当表达式 值等于常量1 语句块1; break; //退出switch case 常量2; // 含义一样 语句块2; break; ... case 常量n; 语句块n; break; default: default语句块; break; } 流程图：\n演示案例：\n请编写一个程序，该程序可以接收一个字符，比如: a,b,c,d,e,f,g\na 表示星期一，b 表示星期\n二 … 根据用户的输入显\n示相依的信息.要求使用\nswitch 语句完成\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 # include \u0026lt;stdio.h\u0026gt; void main() { char c1 = \u0026#39; \u0026#39;; char c2 = \u0026#39;a\u0026#39;; printf(\u0026#34;请输入一个字符(a,b,c,d)\u0026#34;); scanf(\u0026#34;%c\u0026#34;, \u0026amp;c1); //switch //表达式： 任何有值都可以看成是一个表达式 switch(c1) { case \u0026#39;a\u0026#39; : //\u0026#39;a\u0026#39; =\u0026gt; 97 printf(\u0026#34;今天星期一, 猴子穿新衣\u0026#34;); break; //退出 switch case \u0026#39;b\u0026#39; : printf(\u0026#34;今天星期二, 猴子当小二\u0026#34;); break; case \u0026#39;c\u0026#39; : printf(\u0026#34;今天星期二, 猴子当小二\u0026#34;); break; case\t\u0026#39;d\u0026#39;: printf(\u0026#34;今天星期四，猴子当小四\u0026#34;); break; case \u0026#39;e\u0026#39;: printf(\u0026#34;今天星期五，猴子当小五\u0026#34;); break; case \u0026#39;f\u0026#39;: printf(\u0026#34;今天星期六，猴子当小六\u0026#34;); break; case \u0026#39;g\u0026#39;: printf(\u0026#34;今天星期天，猴子当小天\u0026#34;); break; default: printf(\u0026#34;输入错误！\u0026#34;); } getchar(); getchar(); } switch 细节讨论\nswitch 语句中的 expression 是一个常量表达式，必须是一个整型(charshort, int, long 等) 或枚举类型 case 子句中的值必须是常量,而不能是变量 default 子句是可选的，当没有匹配的 case 时，执行 default break 语句用来在执行完一个 case 分支后使程序跳出 switch 语句块； 如果没有写 break，会执行下一个 case 语句块，直到遇到 break 或者执行到 switch 结尾, 这个现象称为穿透. 循环结构 基本介绍:听其名而知其意,就是让你的代码可以循环的执行.\nfor 循环 循环变量定义:\n1 2 3 4 5 for(①循环变量初始化;②循环条件;④循环变量迭代){ ③循环操作(多条语句); } ==注意事项和细节说明==\n1、循环条件是 返回一个表示真(非0)假(0) 的表达式 2、or(;循环判断条件;) 中的初始化和变量迭代可以不写（写到其它地方），但是两边的分号不能省略。 3、 循环初始值可以有多条初始化语句，但要求类型一样，并且中间用逗号隔开，循环变量迭代也可以有多条变量迭代语句，中间用逗号隔开。\nfor(i = 0, j = 0; j \u0026lt; count; i++, j += 2)\n例题：\n1、打印1~100之间所有是9的倍数的整数的个数及总和. [使用for完成]\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 # include \u0026lt;stdio.h\u0026gt; void main() { int count = 0; int sum = 0; int i\t; for(i = 1; i\u0026lt;=100; i++){ if(i%9 == 0){ sum+=i; count++; } } printf(\u0026#34;个数：%d,总和：%d\u0026#34;,count,sum); getchar(); } 2、完成下面的表达式输出\n1 2 3 4 5 6 7 8 9 10 11 12 13 # include \u0026lt;stdio.h\u0026gt; void main() { int i = 0; int j = 0; for(i=0; i\u0026lt;=6; i++){ for(j=0; j\u0026lt;=6; j++){ if(i+j == 6){ printf(\u0026#34;%d+%d\\n\u0026#34;,i,j); } } } getchar(); } while循环 1 2 3 4 5 6 ①循环变量初始化; while(②循环条件){ ③循环体(多条语句); ④循环变量迭代; } 案例：输出5个你好！贺晶晶\n1 2 3 4 5 6 7 8 9 # include \u0026lt;stdio.h\u0026gt; void main() { int i = 1; while(i\u0026lt;=5){ printf(\u0026#34;你好！贺晶晶 \\n\u0026#34;); i++; } getchar(); } 流程图分析：\n联系案例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 # include \u0026lt;stdio.h\u0026gt; # include \u0026lt;string.h\u0026gt; void main() { char name[10] = \u0026#34;\u0026#34;; while(strcmp(name,\u0026#34;exit\u0026#34;) != 0){ printf(\u0026#34;请输入名字：\u0026#34;); scanf(\u0026#34;%s\u0026#34;,name);// 这里不需要加\u0026amp;，因为数组的名称就是地址 printf(\u0026#34;你输入的名字是=%s \\n\u0026#34;,name); } getchar(); getchar(); } do while循环 1 2 3 4 5 6 7 ①循环变量初始化; do{ ②循环体(多条语句); ③循环变量迭代; } while(④循环条件); 注意：do – while 后面有一个 分号，不能省略 案例：输出5个你好！贺猪猪\n1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;stdio.h\u0026gt; void main() { //输出 int i = 1; //循环变量初始化 while( i \u0026lt;= 5) { //循环条件 printf(\u0026#34;你好 贺猪猪 i=%d \\n\u0026#34; , i); //循环语句 i++; // 变量迭代 } getchar(); } 案例2：\n如果老公同意老婆购物，则老婆将一直购物，直到老公说不同意为止 [printf(\u0026ldquo;老婆问：我可以继续购物吗？y/n\u0026rdquo;)]\n1 2 3 4 5 6 7 8 9 10 11 12 #include \u0026lt;stdio.h\u0026gt; void main() { char answer = \u0026#39; \u0026#39;; do{ printf(\u0026#34;老婆问：我可以继续购物吗？y/n：\u0026#34;); scanf(\u0026#34;%c\u0026#34;,\u0026amp;answer); getchar(); //其回车 }while(answer == \u0026#39;y\u0026#39; ); printf(\u0026#34;我的天，老婆终于不购物了~~~~\u0026#34;); getchar(); } 测试运行结果：\n多重循环控制 1、 将一个循环放在另一个循环体内，就形成了嵌套循环。其中，for ,while ,do…while均可以作为外层循环和内层循环。【建议一般使用两层，最多不要超过3层】, 如果嵌套循环过多，会造成可读性降低\n2、 实质上，嵌套循环就是把内层循环当成外层循环的循环体。当只有内层循环的循环条件为false时，才会完全跳出内层循环，才可结束外层的当次循环，开始下一次的循环, 举例说明。\n3、设外层循环次数为m次，内层为n次，则内层循环体实际上需要执行m*n次。\n案例：\n1、统计3个班成绩情况，每个班有5名同学，求出各个班的平均分和所有班级的平均分==[学生的成绩从键盘输入]==。\n2、统计三个班及格人数，每个班有5名同学。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include \u0026lt;stdio.h\u0026gt; void main() { int classNumber = 3; // 班级个数 int stuNumber = 5; // 学生个数 double classTotalScore =0.0; // 个人班级总分 double allClassTotalScore =0.0; // 个人班级总分 double score = 0.0; // 接收学生的成绩 int count = 0; // 及格人数 int i,j; for(i=1;i\u0026lt;=classNumber;i++){ //每次给一个班级输出成绩时，需要清零 classTotalScore=0.0; for(j=1; j\u0026lt;=stuNumber; j++){ printf(\u0026#34;\\n请输入第%d班级的第%d个学生的成绩\u0026#34;,i,j); scanf(\u0026#34;%lf\u0026#34;,\u0026amp;score); if(score\u0026gt;=60){ count++; } classTotalScore +=score; } allClassTotalScore += classTotalScore; printf(\u0026#34;第%d班的平均分是%.2f\u0026#34;,i,classTotalScore/stuNumber); } printf(\u0026#34;所有班级的平均分%.2f\u0026#34;,allClassTotalScore/(stuNumber*classNumber)); printf(\u0026#34;所有班级的及格人数%d\u0026#34;,count); getchar();// 去回车 getchar(); } 3、打印九九乘法表\n1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;stdio.h\u0026gt; void main(){ int i,j; for(i=1;i\u0026lt;=9;i++){ for(j=1;j\u0026lt;=i;j++){ printf(\u0026#34;%d * %d = %d \u0026#34;,j,i,i*j); } printf(\u0026#34;\\n\u0026#34;); } getchar(); } 4、打印金字塔\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include \u0026lt;stdio.h\u0026gt; void main(){ int i,j,k; int totalLevel = 10; for(i=1;i\u0026lt;=totalLevel;i++){ //输出空格 for(k=1;k\u0026lt;=totalLevel-i;k++){ printf(\u0026#34; \u0026#34;); } // 输出* for(j=1;j\u0026lt;=2*i-1;j++){ printf(\u0026#34;*\u0026#34;); } //换行 printf(\u0026#34;\\n\u0026#34;); } getchar(); } 4、打印空心金字塔\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include \u0026lt;stdio.h\u0026gt; void main(){ int i,j,k; int totalLevel = 10; for(i=1;i\u0026lt;=totalLevel;i++){ //输出空格 for(k=1;k\u0026lt;=totalLevel-i;k++){ printf(\u0026#34; \u0026#34;); } // 输出* for(j=1;j\u0026lt;=2*i-1;j++){ if(j==1 || j==2*i-1 || i ==totalLevel){ printf(\u0026#34;*\u0026#34;); }else{ printf(\u0026#34; \u0026#34;); } } //换行 printf(\u0026#34;\\n\u0026#34;); } getchar(); } ==作业：打印空心菱形：==\n实现思路：\n第一步：先实现打印实心矩形\n直接通过双重for循环实现\n1 2 3 4 5 6 7 ******* ******* ******* ******* ******* ******* ******* 第二步：打印实心金字塔\n1 2 3 4 5 6 7 8 9 10 11 * // 3个空格 *** // 2个空格 ***** //1个空格 *******//0个空格 规律totalLevel -i 个* * // 1个* *** // 2个* ***** //5个* *******/ 7个* 规律：2*i-1个* 代码实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include \u0026lt;stdio.h\u0026gt; void main(){ printArc(10); } void printArc(int num){ int i,j,k; for(i=1;i\u0026lt;=num;i++){ //输出空格 for(k=1;k\u0026lt;=num-i;k++){ printf(\u0026#34; \u0026#34;); } // 输出* for(j=1;j\u0026lt;=2*i-1;j++){ printf(\u0026#34;*\u0026#34;); } //换行 printf(\u0026#34;\\n\u0026#34;); } getchar(); } 第三步：打印实心菱形\n思路：就是再写一个循环打印对称三角形\n1 2 3 4 5 6 7 * *** ***** ******* ***** *** * break break只是跳出当前循环\n==假如：随机生成一个数，直到生成了97这个数，看看你一共用了几次?==\n解决：\n在执行循环的过程中，当满足某个条件时，可以提前退出该循环, 这时，就可能使用break\n代码实现：\n1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;stdio.h\u0026gt; void main(){ int i = 0; while(1) { printf(\u0026#34;\\n输出%d\u0026#34;,i++); if(i==97) { break; } } getchar(); } continue continue语句用于==结束本次循环==，继续执行下一次循环。\n流程图：\n演示案例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include \u0026lt;stdio.h\u0026gt; void main() { int i,j; for( j = 0; j \u0026lt; 4; j++){ for( i = 0; i \u0026lt; 10; i++){ if(i == 2){ //看看分别输出什么值，并分析 continue ; } printf(\u0026#34;i = %d\\n\u0026#34; , i); } printf(\u0026#34;================\\n\u0026#34;); } getchar(); }// 输出 4次 i=0, 1 ,3,4,5,6,7,8,9 结果如图：\n注意事项和细节说明:\ncontinue语句，==只能配合循环语言使用==，不能单独和switch/if使用\n练习题：\n(1)、从键盘读入个数不确定的整数，并判断读入的正数和负数的个数，输入为0时结束程序【使用for循环 ，break, continue完成】 ==【positive 正数，negative】==\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include \u0026lt;stdio.h\u0026gt; void main() { int positive = 0; int negative = 0; int num = 0; //从控制台输入的个数 for(; ;) { //死循环 等效 while(1){} printf(\u0026#34;请输入一个整数：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;num); if(num==0){ break; // 跳出for循环 } if(num\u0026gt;0){ positive++; continue; } negative++; } printf(\u0026#34;正数个数%d，负数个数%d\u0026#34;,positive,negative); getchar(); getchar(); } (2)、某人有 100,000 元,每经过一次路口，需要交费,规则如下:\n当现金\u0026gt;50000 时,每次交 5%\n当现金\u0026lt;=50000 时,每次交 1000\n编程计算该人可以经过多少次路口,使用 while break 方式完成\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include \u0026lt;stdio.h\u0026gt; void main() { /* 某人有 100,000 元,每经过一次路口，需要交费,规则如下: 当现金\u0026gt;50000 时,每次交 5% 当现金\u0026lt;=50000 时,\t每次交 1000 编程计算该人可以经过多少次路口, 使用 while 方式完成 */ //分析：money\u0026lt;1000的时候就过不了 double money = 100000; int count = 0; while(1){ //判断money是后小于1000\tif(money\u0026lt;1000){ break; } if(money\u0026gt;50000){ money=money*(1-0.05); count++; }else if(money\u0026lt;=50000){ money-=1000; count++; } } printf(\u0026#34;过路口次数为：%d，剩余%.2f\u0026#34;,count,money); getchar(); } go to 1、C 语言的 goto 语句可以无条件地转移到程序中指定的行。\n2、goto语句通常与条件语句配合使用。可用来实现条件转移，跳出循环体等功能。\n3、在C程序设计中==一般不主张使用==goto语句， 以免造成程序流程的混乱，使理解\n和调试程序都产生困难\n基本语法\n1 2 3 4 5 goto label .. . label: statement 演示：\n1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;stdio.h\u0026gt; void main() { printf(\u0026#34;start\\n\u0026#34;); goto lable1; //lable1 称为标签 printf(\u0026#34;ok1\\n\u0026#34;); printf(\u0026#34;ok2\\n\u0026#34;); lable1: printf(\u0026#34;ok3\\n\u0026#34;); printf(\u0026#34;ok4\\n\u0026#34;); getchar(); } 综合练习： (1)、判断一个年份是否是闰年\n分析：\n闰年的判断条件：1、能被400整除的 2、能被4整数且不能被100整除\n程序语句实现\nif(year%400==0)\nif(year%4==0 \u0026amp;\u0026amp; year%100 != 0)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include \u0026lt;stdio.h\u0026gt; void main() { int year; printf(\u0026#34;请输人年份：\\n\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;year); if(year%400 == 0 || (year%4==0 \u0026amp;\u0026amp; year%100 != 0)){ printf(\u0026#34;%d 此年是闰年\\n\u0026#34;,year); } else{ printf(\u0026#34;%d 此年不是闰年\\n\u0026#34;,year); } getchar(); getchar(); } （2）、1000以内水仙花数：==（例如：153=1^3+5^3+3^3）==\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include \u0026lt;stdio.h\u0026gt; int main() { int i; for(i = 0; i\u0026lt;1000; i++){ int ge = i%10; // 个位 int shi = i%100/10; // 十位 int bai= i/100; //百位 if(i = ge*ge*ge + shi*shi*shi + bai*bai*bai){ printf(\u0026#34;%d是水仙花数\\n\u0026#34;,i); } } getchar(); } 运行结果如图：\n(3)、编写程序，根据输入的月份和年份，求出该月的天数（1-12）==注意：需要考虑闰年(2 月份 29)和平年(2 月份 28)==\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include \u0026lt;stdio.h\u0026gt; void main(){ int year = 2019; int month = 2; switch(month) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: printf(\u0026#34;%d 年的 %d 月是%d天\u0026#34;, year, month, 31); break; case 2: //判断 year 是闰年还是平年 if( (year % 4 == 0 \u0026amp;\u0026amp; year % 100 !=0) || year % 400 == 0) { printf(\u0026#34;%d 年的 %d 月是%d天\u0026#34;, year, month, 29); } else{ printf(\u0026#34;%d 年的 %d 月是%d天\u0026#34;, year, month, 28); } break; default: printf(\u0026#34;%d 年的 %d 月是%d天\u0026#34;, year, month, 30); break; } getchar(); } 运行结果：\n（4）、看下面代码输出什么? ==重点：优先级问题==\n1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;stdio.h\u0026gt; void main(){ int b1=0,b2=0; // 将 b2==5\u0026gt;0 改成 b2=5\u0026gt;0 又输出什么 // 充分考虑运算符的优先级问题 if((b1==2\u0026gt;3) \u0026amp;\u0026amp; (b2=5\u0026gt;0)){ printf(\u0026#34;\\n(b1=2\u0026gt;3) \u0026amp;\u0026amp; (b2=5\u0026gt;0)为真\u0026#34;); //输出 } printf(\u0026#34;\\nb1= %d ;b2= %d\u0026#34;, b1,b2);// b1=0 b2=1 getchar(); } 输出结果：\n(5)、输出小写的 a-z 以及大写的 Z—A\n1 2 3 4 5 6 7 8 9 10 11 12 #include \u0026lt;stdio.h\u0026gt; void main() { char c1; for(c1 = \u0026#39;a\u0026#39;; c1\u0026lt;\u0026#39;z\u0026#39;;c1++){ printf(\u0026#34;%c \u0026#34;,c1); } for(c1 = \u0026#39;A\u0026#39;; c1\u0026lt;\u0026#39;Z\u0026#39;;c1++){ printf(\u0026#34;%c \u0026#34;,c1); } getchar(); } (6)、求出 1-1/2+1/3-1/4…..1/100 的和\n找规律分析解题：\n1-1/2+1/3-1/4…..1/100 = (1/1)-(1/2)+(1/3)-(1/4)…..(1/100)\n分母为奇数：符号为+\n分母为偶数：符号为-\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include \u0026lt;stdio.h\u0026gt; void main() { //定义一个变量 sum 来统计和 double sum = 0.0; int i ; for(i = 1; i \u0026lt;= 100; i++) { //如果 i 是奇数 if(i % 2 != 0) { sum += 1.0/i; // 注意，考虑保留小数 1.0 而不是 1 } else { sum -= 1.0/i; } } printf(\u0026#34;sum=%.2f\u0026#34;, sum); getchar(); } 枚举（enum） 1、枚举是 C 语言中的一种构造数据类型，它可以让数据更简洁，更易读, 对于只有几个有限的特定数据，可以使用枚举. 2、枚举对应英文(enumeration, 简写 enum) 3、枚举是一组常量的集合，包含一组有限的特定的数据 4、枚举语法定义格式为\n快速入门案例： 1 2 3 4 5 6 7 8 9 10 11 12 #include \u0026lt;stdio.h\u0026gt; int main() { enum DAY { MON=1, TUE=2, WED=3, THU=4, FRI=5, SAT=6, SUN=7\t// 这里DAY 就是枚举类型, 包含了7个枚举元素 }; enum DAY day; // enum DAY 是枚举类型， day 就是枚举变量 day = WED; //\t给枚举变量 day 赋值，值就是某个枚举元素 printf(\u0026#34;%d\u0026#34;,day);\t// 3 ， 每个枚举元素对应一个值 getchar(); return 0; } 枚举的遍历 C 语言中，枚举类型是被当做 int 或者 unsigned int 类型来处理的，枚举类型必须连续是可以实现有条件的 遍历。以下实例使用 for 来遍历枚举的元素\n1 2 3 4 5 6 7 8 9 10 11 12 13 #include \u0026lt;stdio.h\u0026gt; enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN //如果没有给赋值，就会按照顺序赋值 } day; // 表示 定义了一个枚举类型 enum Day ,同时定义了一个变量 day(类型是 enum DAY) int main() { // 遍历枚举元素 //day++ 会给出警告，但是可以运行 for (day = MON; day \u0026lt;= SUN; day++) { // 要求枚举元素是连续赋值 printf(\u0026#34;枚举元素：%d \\n\u0026#34;, day); } getchar(); return 0; } 枚举在switch中的应用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include \u0026lt;stdio.h\u0026gt; int main() { enum SEASONS {SPRING=1, SUMMER, AUTUMN, WINTER}; //定义枚举类型 enum SEASONS enum SEASONS season;//定义了一个枚举类型变量 season(类型 enum SEASONS ) printf(\u0026#34;请输入你喜欢的季节: (1. spring, 2. summer, 3. autumn 4 winter): \u0026#34;); scanf(\u0026#34;%d\u0026#34;, \u0026amp;season); switch (season) { case SPRING: printf(\u0026#34;你喜欢的季节是春天\u0026#34;); break; case SUMMER: printf(\u0026#34;你喜欢的季节是夏天\u0026#34;); break; case AUTUMN: printf(\u0026#34;你喜欢的季节是秋天\u0026#34;); break; case WINTER: printf(\u0026#34;你喜欢的季节是冬天\u0026#34;); break; } getchar(); getchar(); } ==枚举类型使用注意事项和细节==\n1、第一个枚举成员的默认值为整型0，后续枚举成员的值在前一个成员上加1我们在这个实例中把第一个枚举成员的值定义为 1，第二个就为 2，以此类推 2、 在定义枚举类型时改变枚举元素的值 [案例] 3、 枚举变量的定义的形式1-先定义枚举类型，再定义枚举变量 4、 枚举变量的定义的形式2-定义枚举类型的同时定义枚举变量 5、枚举变量的定义的形式3-省略枚举名称，直接定义枚举变量\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include \u0026lt;stdio.h\u0026gt; int main() { //定义方式1 -定义枚举类型的同时定义枚举变量 enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN } day; // 定义方式2 -省略枚举名称，直接定义枚举变量 enum { MON=1, TUE, WED, THU, FRI, SAT, SUN } day; // 这样使用枚举，该枚举类型只能使用一次. //定义方式3 先定义枚举类型，再定义枚举变量 enum DAY { MON=1, TUE, WED, THU, FRI, SAT, SUN }; enum DAY day; } 6、可以将整数转换为对应的枚举值\n1 2 3 4 5 6 7 8 9 10 #include \u0026lt;stdio.h\u0026gt; int main() { enum SEASONS {SPRING=1, SUMMER, AUTUMN, WINTER}; enum SEASONS season; int n = 4; season = (enum SEASONS) n; // 将整型int 4 变成了 SEASONS类型 printf(\u0026#34;season=:%d\u0026#34;,season); getchar(); return 0; } 函数 1、为完成某一功能的程序指令(语句)的集合,称为函数。 2、在C语言中,函数分为: 自定义函数、系统函数(查看C语言函数手册) 3、函数还有其它叫法，比如方法等，在本视频课程中，我们统一称为 函数。\n函数的定义 函数的形式：\n1 2 3 4 返回类型 函数名（形参列表）{ 执行语句...; // 函数体 return 返回值; // 可选 } 头文件 在实际的开发中，我们往往需要在不同的文件中，去调用其它文件的定义的函数，比如hello.c中，去使用myfuns.c 文件中的函数，如何实现？ -\u0026gt;==使用头文件==\n头文件基本概念\n1、 头文件是扩展名为 .h 的文件，包含了 C 函数声明和宏定义，被多个源文件中引用共享。有两种类型的头文件：==程序员编写的头文件==和==C标准库自带的头文件== 2、 在程序中要使用头文件，需要使用 C ==预处理指令 #include==来引用它。前面我们已经看过 stdio.h 头文件，它是C标准库自带的头文件 3、 ==#include叫做文件包含命令==，用来引入对应的头文件（.h文件）。#include 也是C语言预处理命令的一种。\n#include 的处理过程很简单，就是将头文件的内容插入到该命令所在的位置，从而把头文件和当前源文件连接成一个源文件，这与复制粘贴的效果相同。但是我们不会直接在源文件中复制头文件的内容，因为这么做很容易出错，特别在程序是由多个源文件组成的时候。 5、建议把所有的常量、宏、系统全局变量和函数原型写在头文件中，在需要的时候随时引用这些头文件 工作原理图：\n头文件快速入门 说明：头文件快速入门-C 程序相互调用函数，我们将 cal 声明到文件 myFun.h , 在 myFun.c 中定义 cal 函数， 当其它文件需要使用到 myFun.h 声明 的函数时，可以#include 该头文件，就可以使用了.\n1、创建myfun.h并声明int add(int a,int b)函数\n1 2 //声明函数int add(int a,int b); int add(int a ,int b); 2、创建myfun.c并实现int add(int a,int b)函数\n1 2 3 4 5 6 #include \u0026lt;stdio.h\u0026gt; // 实现函数int add(int a,int b); int add(int a,int b){ return a+b; } 3、创建测试类Test.c去测试int add(int a,int b)函数\n1 2 3 4 5 6 #include \u0026lt;stdio.h\u0026gt; // 实现函数int add(int a,int b); int add(int a,int b){ return a+b; } 头文件的注意事项和细节说明 1、引用头文件相当于复制头文件的内容\n2、源文件的名字 可以不和头文件一样，但是为了好管理，一般头文件名和源文件名一样.\n3、==C 语言中 include \u0026lt;\u0026gt; 与include \u0026quot;\u0026rdquo; 的区别==\ninclude \u0026lt;\u0026gt;：引用的是编译器的类库路径里面的头文件，用于引用系统头文件\ninclude \u0026ldquo;\u0026quot;：引用的是你程序目录的相对路径中的头文件，如果在程序目录没有找到引用的头文件则到编译器的类库路径的目录下找该头文件，用于引用用户头文件\n说明：\n==引用 系统头文件，两种形式都会可以，include \u0026lt;\u0026gt; 效率高==\n==引用 用户头文件，只能使用 include \u0026ldquo;\u0026quot;==\n4、一个 #include 命令只能包含一个头文件，多个头文件需要多个 #include 命令\n5、同一个头文件如果被多次引入，多次引入的效果和一次引入的效果相同，因为头文件在代码层面有防止重复引入的机制 [举例]\n6、在一个被包含的文件(.c)中又可以包含另一个文件头文件(.h)\n7、不管是标准头文件，还是自定义头文件，都只能包含变量和函数的声明，不 能包含定义，否则在多次引入时会引起重复定义错误(!!!!)\n函数-调用机制 如何理解方法这个概念,给大家举个通俗的示例:\n举例说明+图解\n无返回值型：\n有返回值型：\n==4、如果有函数有返回值，则将返回值赋给接收的变量==\n==5、当一个函数返回后，该函数对应的栈空间也会销毁==\n举例说明：\n函数的递归 一个函数在函数体内又调用了本身，我们称为==递归调用==\n递归调用快速入门 1 2 3 4 5 6 7 8 9 10 11 12 #include \u0026lt;stdio.h\u0026gt; void test(int n) { if(n \u0026gt; 2) { test(n -1); } printf(\u0026#34;\\n n=%d\u0026#34;, n); } void main() { test(4); // 输出什么呢？ getchar(); } 结果如图：\n图解分析：\n函数递归需要遵守的重要原则: 1、执行一个函数时，就创建一个新的受保护的独立空间(新函数栈) 2、函数的局部变量是独立的，不会相互影响 3、递归必须向退出递归的条件逼近，否则就是无限递归，死龟了:) 4、当一个函数执行完毕，或者遇到return，就会返回，遵守谁调用，就将结果返回给谁\n递归函数练习题 1、斐波那契数\n请使用递归的方式，求出斐波那契数1,1,2,3,5,8,13\u0026hellip;给你一个整数n，求出它的斐波那契数是多少？\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include \u0026lt;stdio.h\u0026gt; /* 题目：求出斐波那契数1,1,2,3,5,8,13...给你一个整数n 分析： 当n=1和n=2时候，返回值为1 当n\u0026gt;2的时候，斐波那契数=前两个数的和：fbn(n-1)+fbn(n-2) */ int fbn(int n){ if(n ==1 || n ==2 ){ return 1; }else{ return fbn(n-1)+fbn(n-2); } } void main(){ int result = fbn(4); printf(\u0026#34;%d\u0026#34;,result); getchar(); } 2、求函数值\n已知 f(1)=3; f(n) = 2*f(n-1)+1; 请使用递归的思想编程，求出 f(n)的值?\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include \u0026lt;stdio.h\u0026gt; /* 题目：已知 f(1)=3; f(n) = 2*f(n-1)+1; 请使用递归的思想编程，求出 f(n)的值? */ int f(int n){ if(n ==1){ return 3; }else{ return 2*f(n-1)+1; } } void main(){ int result = f(10); printf(\u0026#34;%d\u0026#34;,result); getchar(); } 3、猴子吃桃子问题\n有一堆桃子，猴子第一天吃了其中的一半，并再多吃了一个！以后每天猴子都吃其中的一半，然后再多吃一个。当到第十天时，想再吃时（还没吃），发现只有1个桃子了。==问题：最初共多少个桃子？==\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include\u0026lt;stdio.h\u0026gt; /* 有一堆桃子，猴子第一天吃了其中的一半，并再多吃了一个！ 以后每天猴子都吃其中的一半，然后再多吃一个。 当到第十天时，想再吃时（还没吃），发现只有1个桃子了。 问题：最初共多少个桃子？ 分析：day10 只有1个桃子 推出 day9 = (day10+1)*2 = 4 day8 = (4+1)*2= 10; day7 = (10+1)*2= 22; */ int peach(int day){ if(day == 10){ return 1; }else { return (peach(day+1)+1)*2; } } void main(){ int peachNum = peach(1); printf(\u0026#34;第一天有%d个桃子\u0026#34;,peachNum); getchar(); } 函数注意事项和细节讨论 1、 函数的形参列表可以是多个。 2、C语言传递参数可以是值传递（pass by value），也可以传递指针（a pointer passed by value）也叫引用传递。 3、函数的命名遵循标识符命名规范，首字母不能是数字，可以采用 驼峰法 或者 下 划线法 ，比如 getMax() get_max()。 4、函数中的变量是局部的，函数外不生效【案例说明】\n5、 基本数据类型默认是值传递的，即进行值拷贝。在函数内修改，不会影响到原来 的值。【案例演示】\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include\u0026lt;stdio.h\u0026gt; void f2(int n) { n++; printf(\u0026#34;\\nf2 中的 n=%d\u0026#34;, n); // n=10 } void main() { //函数中的变量是局部的，函数外不生效 //printf(\u0026#34;num=%d\u0026#34;, num); int n = 9; f2(n); printf(\u0026#34;\\nmain 函数中 n=%d\u0026#34;, n); //9 getchar(); } 6、如果希望函数内的变量能修改函数外的变量，可以传入变量的地址\u0026amp;，函数内以指针的方式操作变量。从效果上看类似引用(即传递指针) 【案例演示:】\n==指针传递==\n1 2 3 4 5 6 7 8 9 10 11 12 13 #include\u0026lt;stdio.h\u0026gt; void f3(int *p) { (*p)++;// 修改会对函数外的变量有影响 } void main() { //函数中的变量是局部的，函数外不生效 //printf(\u0026#34;num=%d\u0026#34;, num); int n = 9; f3(\u0026amp;n); printf(\u0026#34;\\nmain 函数中 n=%d\u0026#34;, n); //10 getchar(); } 内存图分析：\n7、C语言==不支持函数重载==，C语言支持==可变参数函数==\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include\u0026lt;stdio.h\u0026gt; /* 请编写一个函数 swap(int \\*n1, int \\*n2) 可以交换 n1 和 n2的值 */ void swap(int *n1, int *n2){ int temp = *n1; // 表示将n1 指针指向的变量的值赋值给temp *n1 = *n2; // 将n2指针指向的值赋值给n1指针指向的变量的值 *n2 = temp; // 将temp 的值赋值给 n2指针指向的变量的值 } void main(){ int a = 3; int b = 4; swap(\u0026amp;a,\u0026amp;b); printf(\u0026#34;a=%d,b=%d\u0026#34;,a,b); // 此时 a = 4 , b = 3 getchar(); } 练习题：请编写一个函数 swap(int *n1, int *n2) 可以交换 n1 和 n2的值\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include\u0026lt;stdio.h\u0026gt; /* 请编写一个函数 swap(int \\*n1, int \\*n2) 可以交换 n1 和 n2的值 */ void swap(int *n1, int *n2){ int temp = *n1; // 表示将n1 指针指向的变量的值赋值给temp *n1 = *n2; // 将n2指针指向的值赋值给n1指针指向的变量的值 *n2 = temp; // 将temp 的值赋值给 n2指针指向的变量的值 } void main(){ int a = 3; int b = 4; swap(\u0026amp;a,\u0026amp;b); printf(\u0026#34;a=%d,b=%d\u0026#34;,a,b); getchar(); } 函数参数的传递方式 我们在讲解函数注意事项和使用细节时，已经讲过C语言传递参数可以是==值传递（pass by value）==，也可以==传递指针（a pointer passed by value）==也叫==传递地址==或者 ==引用传递==\n两种传递方式:\n1、值传递\n2、引用传递(传递指针、地址)\n其实，不管是值传递还是引用传递，传递给函数的都是变量的副本，不同的是，值传递的是值的拷贝，引用传递的是地址的拷贝，一般来说，地址拷贝效率高，因为数据量小，而值拷贝决定拷贝的数据大小，数据越大，效率越低。\n值传递和引用传递使用特点 1、值传递：变量直接存储值，内存通常在栈中分配【案例: 示意图】 2、==默认是值传递的数据类型有== 1. 基本数据类型 2. 结构体 3. 共用体 4. 枚举类型 3、引用传递：变量存储的是一个地址，这个地址对应的空间才真正存储数据(值)。 4、==默认引用传递的数据类型有==：指针和数组\n5、如果希望函数内的变量能修改函数外的变量，可以传入变量的地址\u0026amp;，函数内以指针的方式操作变量(*指针)。从效果上看类似引用 【案例演示: 画出示意图】, 比如修改结构体的属性.\n变量作用域 所谓变量作用域（Scope），就是指变量的有效范围\n1、函数内部声明定义的局部变量，作用域仅限于函数内部。\n2、 函数的参数，形式参数，被当作该函数内的局部变量，如果与全局变量同名它们会优先使用局部变量\n3、 在一个代码块，比如 for / if中 的局部变量，那么这个变量的的作用域就在该代码块\n4、在所有函数外部定义的变量叫全局变 量，作用域在整个程序有效。\n初始化局部变量和全局变量\n1、局部变量，系统不会对其默认初始化，必须对局部变量初始化后才能使用，否则，程序运行后可能会异常退出\n2、全局变量，系统会自动对其初始化，如下所示\n作用域的注意事项和细节\n1、全局变量(Global Variable)保存在内存的全局存储区中，占用静态的存储单元，它的作用域默认是整个程序，也就是所有的代码文件，包括源文件（.c文件）和头文件（.h文件）。【c程序内存布局图!!!】 2、局部变量(Local Variable)保存在栈中，函数被调用时才动态地为变量分配存储单元，它的作用域仅限于函数内部。\n参考：↓↓↓↓↓\n3、C语言规定，只能从小的作用域向大的作用域中去寻找变量，而不能反过来，使用更小的作用域中的变量 4、在同一个作用域，变量名不能重复，在不同的作用域，变量名可以重复，使用时编译器采用就近原则. 5、由{ }包围的代码块也拥有独立的作用域\nc程序内存布局图\nstatic关键字 static关键字在c语言中比较常用，使用恰当能够大大提高程序的模块化特性，有利于扩展和维护。\n局部变量使用static修饰\n1、局部变量被 static 修饰后，我们称为静态局部变量\n2、对应静态局部变量在声明时未赋初值，编译器也会把它初始化为0。\n3、静态局部变量存储于进程的静态存储区**(全局性质)，**只会被初始一次，即使函数返回，它的值也会保持不变 [案例+图解]\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include\u0026lt;stdio.h\u0026gt; void fn(void){ int n = 10; //普通变量 printf(\u0026#34;n=%d\\n\u0026#34;, n); n++; printf(\u0026#34;n++=%d\\n\u0026#34;, n); } void fn_static(void) { static int n = 10; //静态局部变量 -只初始化一次 printf(\u0026#34;static n=%d\\n\u0026#34;, n); n++; printf(\u0026#34;n++=%d\\n\u0026#34;, n); } int main(void) { //fn(); printf(\u0026#34;--------------------\\n\u0026#34;); fn_static(); printf(\u0026#34;--------------------\\n\u0026#34;); //fn(); printf(\u0026#34;--------------------\\n\u0026#34;); fn_static(); getchar(); return 0; } 全局变量使用static修饰\n1、普通全局变量对整个工程可见，其他文件可以使用extern外部声明后直接使用。也就是说其他文件不能再定义一个与其相同名字的变量了（否则编译器会认为它们是同一个变量），静态全局变量仅对当前文件可见，其他文件不可访问，其他文件可以定义与其同名的变量，两者互不影响\n(1)、创建带有全局变量的类file01.c，代码如下：\n1 2 int n = 10; //普通全局变量 //static int n = 20; //静态全局变量，只能在本文件中使用，而不能在其他文件中使用！ (2)、创建测试类file02.c，代码如下：\n1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;stdio.h\u0026gt; //#include \u0026#34;file01.c\u0026#34;; //方式一：通过头文件引入 // 在一个文件，使用另外一个文件的全局变量，使用extern来引入即可 extern int n; extern int m; void main(){ printf(\u0026#34;%d\u0026#34;,n); getchar(); } 2、定义不需要与其他文件共享的全局变量时，加上static关键字能够有效地降低程序模块之间的耦合，避免不同文件同名变量的冲突，且不会误使用\n函数使用static修饰\n(1)、创建带有普通函数类file03.c，代码如下：\n1 2 3 4 5 6 7 #include \u0026lt;stdio.h\u0026gt; void fun1(void) {//普通函数(非静态函数) printf(\u0026#34;hello from fun1.\\n\u0026#34;); } static void fun2(void) {//静态函数 - 只能在本文件中使用 printf(\u0026#34;hello from fun2.\\n\u0026#34;); } (2)、创建测试类file04.c，代码如下：\n1 2 3 4 5 6 7 8 #include \u0026lt;stdio.h\u0026gt; extern void fun1(void) //extern void fun2(void); //肯定会报错 void main(){ fun1(); //fun2(); //肯定会报错 getchar(); } ==发现：static函数只能在本类中使用被调用，而不能被外部的类调用==\n系统函数 标准库参考地址：https://www.runoob.com/cprogramming/c-data-types.html\n字符串系统函数\n1、得到字符串的长度\nsize_t strlen(const char *str) 计算字符串 str 的长度，直到空结束字符，但不包括空结束字符。\n2、拷贝字符串 char *strcpy(char *dest, const char *src)\n把 src 所指向的字符串复制到 dest。 3、 连接字符串 char *strcat(char *dest, const char *src)\n把 src 所指向的字符串追加到 dest 所指向的字符串的结尾。\n测试案例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include\u0026lt;stdio.h\u0026gt; #include\u0026lt;string.h\u0026gt; void main(){ char src[50], dest[50]; char * str = \u0026#34;abcdff\u0026#34;; printf(\u0026#34;str.len=%d\u0026#34;, strlen(str)); //返回6 strcpy(src, \u0026#34;hello \u0026#34;); // 输出【src = hello】，将原来的abcdff给覆盖了 strcpy(dest, \u0026#34;叶仁平\u0026#34;); strcat(dest, src); // 将src的内容追加到desc中去，不会覆盖原有的内容 【结果为：叶仁平，hello】 printf(\u0026#34;最终的目标字符串： |%s|\u0026#34;, dest);//【结果为：叶仁平，hello】 getchar(); } 时间和日期相的函数\n(1)、获取当前时间\nchar *ctime(const time_t *timer)\n返回一个表示当地时间的字符串，当地时间是基于参数 timer。\n案例演示1：\n1 2 3 4 5 6 7 8 9 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;time.h\u0026gt; int main () { time_t curtime; // time_t是一个结构体类型 time(\u0026amp;curtime); // time() 可以完成初始化任务 printf(\u0026#34;当前时间 = %s\u0026#34;, ctime(\u0026amp;curtime)); // 返回一个表示当地时间的字符串，当地时间是基于参数 timer。 getchar(); return(0); } 结果：显示当地当前时间\n演示案例2：\n通过执行两个for循环花费的时间\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;time.h\u0026gt; void test() { int i = 0; int sum = 0; int j = 0; for(i = 0; i \u0026lt; 77777777;i++) { sum = 0; for (j = 0; j\u0026lt; 10;j++) { sum += j; } } } void main () { // 定义两个变量star_t表示开始时间，end_t表示结束时间 time_t start_t,end_t; double diff_t;\t// 两者的时间差 printf(\u0026#34;*********程序启动************\\n\u0026#34;); time(\u0026amp;start_t);\t//获取当前时间\ttest(); // 进行测试 time(\u0026amp;end_t);\t//获取结束时的时间 diff_t = diff_t = difftime(end_t,start_t); // 两者的时间差即为当前时间 printf(\u0026#34;执行test函数耗时%.2fs\u0026#34;,diff_t); getchar(); } 结果：\n数学函数math.h\n1、double exp(double x) 返回 e 的 x 次幂的值。 1、double log(double x) 返回 x 的自然对数（基数为 e 的对数） 3、 double pow(double x, double y) 返回 x 的 y 次幂。 4、 double sqrt(double x) 返回 x 的平方根。 5、 double fabs(double x)\n返回 x 的绝对值。\n案例：\n1 2 3 4 5 6 7 8 9 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;math.h\u0026gt; void main (){ double d1 = pow(2.0,3.0); // 2的3次方 double d2 = sqrt(5.0); // 返回5.0的平方根 printf(\u0026#34;d1=%.2f\u0026#34;, d1); printf(\u0026#34;d2=%.2f\u0026#34;, d2); getchar(); } 基本数据类型和字符串类型的转换\n在程序开发中，我们经常需要将基本数据类型转成字符串类型(即 ==char数组==)。 或者将字符串类型转成基本数据类型。\nsprintf函数的用法:\n1、 sprintf和平时我们常用的printf函数的功能很相似。sprintf函数打印到字符串中，而printf函数打印输出到屏幕上。sprintf函数在我们完成其他数据类型转换成字符串类型的操作中应用广泛 2、该函数包含在stdio.h的头文件中\n演示案例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 #include\u0026lt;stdio.h\u0026gt; void main() { char str1[20]; //字符数组，即字符串 char str2[20]; char str3[20]; int a=20984,b=48090; double d=14.309948; sprintf(str1,\u0026#34;%d %d\u0026#34;,a,b); sprintf(str2, \u0026#34;%.2f\u0026#34;, d); sprintf(str3, \u0026#34;%8.2f\u0026#34;, d); printf(\u0026#34;str1=%s str2=%s str3=%s\u0026#34;, str1, str2, str3); getchar(); } 字符串类型转基本数据类型\natoi (字符串)\n将字符串数组转成整数\natof (字符串)\n将字符串数组转成小数\n案例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include\u0026lt;stdio.h\u0026gt; #include\u0026lt;stdlib.h\u0026gt; void main() { char str[10] = \u0026#34;123456\u0026#34;; char str2[10] = \u0026#34;12.67423\u0026#34;; char str3[3] = \u0026#34;ab\u0026#34;; char str4[4] = \u0026#34;111\u0026#34;; int num1 = atoi(str); // 将str转成一个int整数 short s1 = atoi(str4); double d = atof(str2);// 将str2转成double类型 char c = str3[0]; // 获取字符串的第一个元素a printf(\u0026#34;num1=%d d=%f c=%c s1=%d\u0026#34;, num1, d, c, s1); getchar(); } ==注意：==\n在将char 数组 类型转成 基本数据类型时，要确保能够转成有效的数据，比如 我们可以把 \u0026ldquo;123\u0026rdquo; , 转成一个整数，但是不能把 \u0026ldquo;hello\u0026rdquo; 转成一个整数；如果格式不正确，会默认转成 0 或者 0.0 [案例演示]\n函数的练习题 （1）、函数可以没有返回值案例，编写一个函数,从终端输入一个整数打印出对应的金子塔。 【课后练习】 -\u0026gt; 将原 来的代码封装到函数中即可！\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include \u0026lt;stdio.h\u0026gt; void printfA(int totalLevel){ int i,j,k; for(i=1;i\u0026lt;=totalLevel;i++){ //输出空格 for(k=1;k\u0026lt;=totalLevel-i;k++){ printf(\u0026#34; \u0026#34;); } // 输出* for(j=1;j\u0026lt;=2*i-1;j++){ if(j==1 || j==2*i-1 || i ==totalLevel){ printf(\u0026#34;*\u0026#34;); }else{ printf(\u0026#34; \u0026#34;); } } //换行 printf(\u0026#34;\\n\u0026#34;); } } void main(){ int totalLevel = 0; printf(\u0026#34;请输入金字塔层数：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;totalLevel); printfA(totalLevel); getchar(); getchar(); } （2）、编写一个函数,从终端输入一个整数(1—9),打印出对应的乘法表\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include \u0026lt;stdio.h\u0026gt; void print99(int num){ int i,j; for(i = 1;i \u0026lt;= num; i++){ for(j = 1; j\u0026lt;=i; j++){ printf(\u0026#34;%d * %d = %d\u0026#34;,j,i,i*j); } printf(\u0026#34;\\n\u0026#34;); } getchar(); } void main(){ int num = 0; printf(\u0026#34;请输入：\u0026#34;); scanf(\u0026#34;%d\u0026#34;,\u0026amp;num); print99(num); } (3)、定义函数，实现求两个 double 数字的最大值，并返回\n1 2 3 4 5 6 7 8 9 double getMax(double a,double b){ return a\u0026gt;=b?a:b; } void main(){ printf(\u0026#34;最大值：%.2f\u0026#34;,getMax(3.2,2.1)); getchar(); } 预处理命令 1、使用库函数之前，应该用#include 引入对应的头文件。这种==以#号开头的命令称为预处理命令==。\n2、这些在编译之前对源文件进行简单加工的过程，就称为预处理（即预先处理、提前处理）\n3、预处理主要是处理以#开头的命令，例如#include \u0026lt;stdio.h\u0026gt;等。预处理命令要放在所有函数之外，而且一般都放 在源文件的前面\n4、 预处理是 C 语言的一个重要功能，由预处理程序完成。当对一个源文件进行编译时，系统将自动调用预处理程 序对源程序中的预处理部分作处理，处理完毕自动进入对源程序的编译\n5、C 语言提供了多种预处理功能，如宏定义、文件包含、条件编译等，合理地使用它们会使编写的程序便于阅读、 修改、移植和调试，也有利于模块化程序设计\n预处理命令快速入门 问题：\n开发一个C语言程序，让它暂停 5 秒以后再输出内容 \u0026ldquo;helllo, 叶仁平!~\u0026quot;，并且要求跨平台，在 Windows 和 Linux 下都能运行，如何处理\n==提示==\n1、 Windows 平台下的暂停函数的原型是void Sleep(DWORD dwMilliseconds)，参数的单位是“毫\n秒”，位于 \u0026lt;windows.h\u0026gt; 头文件。\nLinux 平台下暂停函数的原型是unsigned int sleep (unsigned int seconds)，参数的单位是 “秒”，位于 \u0026lt;unistd.h\u0026gt; 头文件\n#if、#elif、#endif 就是预处理命令，它们都是在编译之前由预处理程序来执行的。 解决问题：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include\u0026lt;stdio.h\u0026gt; #if _WIN32 // 如果是win32平台，就执行#include \u0026lt;windows.h\u0026gt;，引入：windows.h #include \u0026lt;windows.h\u0026gt; #elif __linux__ // 否则如果是linux平台，就执行#include \u0026lt;unistd.h\u0026gt;，引入：unistd.h #include \u0026lt;unistd.h\u0026gt; #endif //结束 int main() { //不同的平台下调用不同的函数 #if _WIN32 //识别windows平台 Sleep(5000); #elif __linux__ //识别linux平台 sleep(5); #endif //结束 // 正文 puts(\u0026#34;hello, 叶仁平~\u0026#34;); // 5秒后 -输出hello,叶仁平~ getchar(); return 0; } ==说明：==\n在Windows操作系统和Linux操作系统下，生成的源码不一样！\n在Windows下生成的源码是： 1 2 3 4 5 6 7 #include\u0026lt;stdio.h\u0026gt; include \u0026lt;windows.h\u0026gt; int main() { puts(\u0026#34;hello, 叶仁平~\u0026#34;); // 5秒后 -输出hello,叶仁平~ getchar(); return 0; } 在Linux下生成的源码是 1 2 3 4 5 6 7 #include\u0026lt;stdio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; int main() { puts(\u0026#34;hello, 叶仁平~\u0026#34;); // 5秒后 -输出hello,叶仁平~ getchar(); return 0; } 宏定义define #define 叫做宏定义命令，它也是 C 语言预处理命令的一种。\n所谓宏定义，就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符，那么就全 部替换成指定的字符串\n快速回顾\n==宏定义注意事项和细节==\n1、 宏定义是用宏名来表示一个字符串，在宏展开时又以该字符串取代宏名，这只是一种简单的替换。字符串中可以含任何字符，它可以是常数、表达式、if 语句、函数等，预处理程序对它不作任何检查，如有错误，只能在编译已被宏展开后的源程序时发现。 2、宏定义不是说明或语句，在行末不必加分号，如加上分号则连分号也一起替换 3、宏定义必须写在函数之\t外，其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令 案例\n↓案例如下\n1 2 3 4 5 6 7 8 9 10 11 #include\u0026lt;stdio.h\u0026gt; #define PI 3.14159 int main(){ printf(\u0026#34;PI=%f\u0026#34;, PI); return 0; } #undef PI //取消宏定义 void func(){ // Code printf(\u0026#34;PI=%f\u0026#34;, PI);//错误,这里不能使用到PI了 } 4、代码中的宏名如果被引号包围，那么预处理程序不对其作宏代替 案例4\n5、宏定义允许嵌套，在宏定义的字符串中可以使用已经定义的宏名，在宏展开时由预处理程序层层代换 案例5\n6、习惯上宏名用大写字母表示，以便于与变量区别。但也允许用小写字母\n7、可用宏定义==表示数据类型==，使书写方便 [案例]\n1 2 3 4 #define UINT unsigned int void main() { UINT a, b; // 宏替换 unsigned int a, b; } 8、宏定义表示数据类型和用 typedef 定义数据说明符的区别：宏定义只是简单的字符串替换**，由预处理器来处理；而 typedef是在编译阶段由编译器处理的**，它并不是简单的字符串替换，而给原有的数据类型起一个新的名字，将它作为一种新的数据类型。\n带参宏定义define 1、 C语言允许宏带有参数。在宏定义中的参数称为“形式参数”，在宏调用中的参数称为“实际参数”，这点和函数有些类似 2、对带参数的宏，在展开过程中不仅要进行字符串替换，还要用实参去替换形参 3、带参宏定义的一般形式为#define 宏名(形参列表) 字符串 ,在字符串中可以含有各个形参 4、带参宏调用的一般形式为 : 宏名 (实参列表); [案例+说明 ]\n举例说明：\n创建一个带参的宏定义，代码如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #define MAX(a,b) (a\u0026gt;b) ? a : b int main(){ int x , y, max; printf(\u0026#34;input two numbers: \u0026#34;); scanf(\u0026#34;%d %d\u0026#34;, \u0026amp;x, \u0026amp;y); // 1、MAX(x,y)；调用带参宏定义 // 2、在宏替换时（预处理，由预处理完成），会进行字符串的替换，同时会使用实参替换形参 // 即 MAX(x, y) //宏替换后 (x\u0026gt;y) ? x : y\tmax = MAX(x, y); printf(\u0026#34;max=%d\\n\u0026#34;, max); getchar(); getchar(); return 0; } 带参宏定义的注意事项和细节\n1、带参宏定义中，形参之间可以出现空格，但是宏名和形参列表之间不能有空格出现\n1 2 3 #define MAX(a,b) (a\u0026gt;b)?a:b 如果写成了 #define MAX (a, b) (a\u0026gt;b)?a:b 将被认为是无参宏定义，宏名 MAX 代表字符串(a,b) (a\u0026gt;b)?a:b 而不是 : MAX(a,b) 代表 (a\u0026gt;b) ? a: b 了 2、在带参宏定义中，不会为形式参数分配内存，因此不必指明数据类型。而在宏调用中，实参包含了具体的数据，要用它们去替换形参，因此实参必须要指明数据类型\n3、在宏定义中，字符串内的形参通常要用括号括起来以避免出错。【案例+说明 】\n1 2 3 4 5 6 7 8 9 10 11 12 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define SQ(y) (y)*(y) // 带参宏定义,字符串内的形参通常要用括号括起来以避免出错 int main(){ int a, sq; printf(\u0026#34;input a number: \u0026#34;); scanf(\u0026#34;%d\u0026#34;, \u0026amp;a); sq = SQ(a+1); // 宏替换 (a+1) * (a+1) printf(\u0026#34;sq=%d\\n\u0026#34;, sq); system(\u0026#34;pause\u0026#34;); return 0; } 带参宏定义和函数的区别\n1、宏展开仅仅是字符串的替换，不会对表达式进行计算；宏在编译之前就被处理掉了，它没有机会参与编译，也不会占用内存。\n2、函数是一段可以重复使用的代码，会被编译，会给它分配内存，每次调用函数，就是执行这块内存中的代码\n3、案例说明 ：要求 使用函数计算平方值， 使用宏计算平方值， 并总结二者的区别\n案例分析区别：==Fuction VS Define==\n函数：function\n1 2 3 4 5 6 7 8 9 10 11 12 13 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; int SQ(int y){ return ((y)*(y)); } int main(){ int i=1; while(i\u0026lt;=5){ // 1, 4, 9, 16, 25 printf(\u0026#34;%d^2 = %d\\n\u0026#34;, (i-1), SQ(i++)); //为什么写i-1 ，因为想执行\tSQ(i++) } system(\u0026#34;pause\u0026#34;); return 0; } 宏定义define \u0026ndash;\u0026gt; ==其实就是替换==\n1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define SQ(y) ((y)*(y)) int main(){ int i=1; while(i\u0026lt;=5){ // 这里相当于计算了 1,3,5的平方\ti = 1 3 5 printf(\u0026#34;%d^2 = %d\\n\u0026#34;, (i-2), SQ(i++)); //SQ(i++) = ((i++)*(i++)) 1 9 25 } system(\u0026#34;pause\u0026#34;); return 0; } C语言预处理命令总结 预处理指令是以#号开头的代码行，# 号必须是该行除了任何空白字符外的第一个字符。# 后是指令关键字，在关键字和 # 号之间允许存在任意个数的空白字符，整行语句构成了一条预处理指令，该指令将在编译器进行编译之前对源代码做某些转\n指令 说明 # 空指令，无任何效果 #include 包含一个源代码文件 #define 定义宏 #undef 取消已定义的宏 #if 如果给定条件为真，则编译下面代码 #ifdef 如果宏已经定义，则编译下面代码 #ifndef 如果宏没有定义，则编译下面代码 #elif 如果前面的#if给定条件不为真，当前条件为真，则编译下面代码 #endif 结束一个#if……#else条件编译块 预处理指令使用注意事项\n1、预处理功能是C语言特有的功能，它是在对源程序正式编译前由预处理程序完成的，程序员在程序中用预处理命令来调用这些功能。 2、 宏定义可以带有参数，宏调用时是以实参代换形参，而不是“值传送”。 3、为了避免宏代换时发生错误，宏定义中的字符串应加括号，字符串中出现的形式参数两边也应加括号。\n4、文件包含是预处理的一个重要功能，它可用来把多个源文件连接成一个源文件进行编译，结果将生成一个目标文件。\n5、条件编译允许只编译源程序中满足条件的程序段，使生成的目标程序较短，从而减少了内存的开销并提高了程序的效率。 6、使用预处理功能便于程序的修改、阅读、移植和调试，也便于实现模块化程序设计\n数组 数组可以存放==多个同一类型数据==。数组也是==一种数据类型==，是==构造类型==。传递是以引用的方式传递(即传递的是地址)\n快速入门\n一个养鸡场有 6 只鸡，它们的体重分别是 3kg,5kg,1kg, 3.4kg,2kg,50kg 。请问这六只鸡的总体重是多少?平 均体重是多少? 请你编一个程序\n代码实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #define SQ(y) ((y)*(y)) int main(){ // 定义一个6个鸡的数组 double hens[6]; double totalWeight = 0.0; double avrageWeight = 0.0; int i; // 初始每只鸡的体重 hens[0] = 3; hens[1] = 3.3; hens[2] = 2.3; hens[3] = 2; hens[4] = 1; hens[5] = 4.5; for(i=0; i\u0026lt;6; i++){ totalWeight+=hens[i]; } printf(\u0026#34;六只鸡的总体重是%.2f kg,均体重是%.2f kg\u0026#34;,totalWeight,totalWeight/6); getchar(); } 数组的定义和内存分布 数组的定义\n1 2 3 4 5 数据类型 数组名 [数组大小]; int a [5]; // a 数组名，类型 int , [5] 大小， 即 a 数组最多存放 5个int 数据 赋初值 a[0] = 1; a[1] = 30; .... ==说明== 数组名 就代表 该数组的首地址 ，即 a[0]地址 数组的各个元素是 连续分布的， 假如 a[0] 地址 0x1122 a[1] 地址= a[0]的地址+int字节数(4) = 0x1122 + 4 = 0x1126 ,后面 a[2] 地址 = a[1]地址 + int 字节数(4) = 0x1126 + 4 = 0x112A, 依次类推 3种初始化数组的方式\n1 2 3 4 5 6 7 8 9 int arr[3]; //先定义，再复制，给定长度 arr[0] = 1; arr[1] = 1; arr[2] = 1; int arr2[3] = {4,5,6};// 边定义，边赋值，给定长度 int arr3[] = {7,8,9,10};// 边定义，边赋值，不限定长度 数组的使用\n问题：从终端循环输入5个成绩，保存到double数组,并输出\n代码实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include \u0026lt;stdio.h\u0026gt; /* 从终端循环输入5个成绩，保存到double数组,并输出 */ int main(){ double arr[5]; // 总字节数/单个double所占字节数 = 数组长度 int arrLen = sizeof(arr)/sizeof(double); int i; for(i=0; i\u0026lt;5; i++){ printf(\u0026#34;\\n请输入一个小数：\u0026#34;); scanf(\u0026#34;%lf\u0026#34;,\u0026amp;arr[i]); } for(i=0; i\u0026lt;5; i++){ printf(\u0026#34;arr[%d] =%.2f\u0026#34;,i,arr[i]); } getchar();//过滤回车 getchar(); } 效果如图：\n数组使用注意事项和细节\n1、数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化。\n2、数组创建后，如果没有赋值，则遵守如下规则\n全局数组默认值 0 非全局数组初值是机器垃圾值==(即：原来系统分配给这块空间的值)== 3、使用数组的步骤 1. 定义数组 2 给数组各个元素赋值 3 使用数组, 也可以一步到位\n4、数组的下标是从 0 开始的, 不是从 1 开始\n5、数组下标必须在指定范围内使用，编译通过，在运行时会因为数组越界而异常中断: 比如 int arr [5] 有效下标为 0-4\n6、C 的数组属==构造类型==， 是引用传递(传递的是地址)，因此当把一个数组传递给一个函数时/或者变量，函数/变\n量操作数组会影响到原数组\n数组的应用案例： 1、创建一个char类型的26个元素的数组，分别 放置\u0026rsquo;A\u0026rsquo;-\u0026lsquo;Z‘。使用for循环访问所有元素并打印出来。==提示：==字符数据运算 \u0026lsquo;A\u0026rsquo;+1 -\u0026gt; \u0026lsquo;B\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include \u0026lt;stdio.h\u0026gt; int main(){ char arr[26]; int i; //存入 for(i = 0; i \u0026lt;26; i++){ arr[i] = \u0026#39;A\u0026#39;+i; } // 打印 for(i = 0; i \u0026lt;26; i++){ printf(\u0026#34;\\n %c\u0026#34;,arr[i]); } getchar(); } 2、请求出一个数组的最大值，并得到对应的下标\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include \u0026lt;stdio.h\u0026gt; int main(){ int a[] = {1,2,3,7,4}; int max = a[0]; int index = 0; int i; // 数组长度= 总字节数/单个double所占字节数 int len = sizeof(a)/sizeof(int); for(i=0; i\u0026lt;len; i ++){ if(a[i]\u0026gt;max){ max = a[i]; index = i; } } printf(\u0026#34;数组中最大的数是%d,下标是%d\u0026#34;,max,index); getchar(); } 字符数组与字符串 用来存放字符的数组称为字符数组,\n字符数组实际上是一系列字符的集合，也就是字符串（String）。在 C 语言中，没有专门的字符串变量，没有 string 类型，通常就用一个字符数组来存放一个字符串\n案例：\n1 2 3 4 5 1) char a[10]; //一维字符数组, 长度为 10 2) char b[5][10]; //二维字符数组, 后面我们详细介绍二维数组 3) char c[20]={\u0026#39;c\u0026#39;, \u0026#39; \u0026#39;, \u0026#39;p\u0026#39;, \u0026#39;r\u0026#39;, \u0026#39;o\u0026#39;, \u0026#39;g\u0026#39;, \u0026#39;r\u0026#39;, \u0026#39;a\u0026#39;,\u0026#39;m\u0026#39;}; // 给部分数组元素赋值 字符串注意事项：\n1、在 C 语言中，字符串实际上是使用 null 字符 (\u0026rsquo;\\0\u0026rsquo;) 终止的一维字符数组。因此，一个以 null 结尾的字符串， 包含了组成字符串的字符。\n2、\u0026rsquo;\\0\u0026rsquo;是 ASCII 码表中的第 0 个字符，用 NUL 表示，称为空字符。该字符既不能显示，也不是控制字符，输出该 字符不会有任何效果，它在 C 语言中仅作为字符串的结束标志。\n3、字符数组(字符串)在内存中的布局分析\n4、思考char str[3] = {'a','b','c'} 输出什么？ 为什么?\n输出 abc #$%$#$%；\n==原因：结束字符\\0没有地方放==\n解决：给数组扩容成str[4]\n结论：\n如果在给某个字符数组赋值时，(1)赋给的元素的个数小于该数组的长度，则会自动在后面加 \u0026lsquo;\\0\u0026rsquo;, 表示 字符串结束，(2)赋给的元素的个数等于该数组的长度，则不会自动添加 \u0026lsquo;\\0\u0026rsquo; char str2[] = {\u0026rsquo;t\u0026rsquo;,\u0026rsquo;m\u0026rsquo;,\u0026lsquo;o\u0026rsquo;} 输出什么? 输出的是 tmo 乱码\n使用字符指针变量和字符数组两种方法表示字符串的讨论\n1、字符数组由若干个元素组成，每个元素放一个字符；而字符指针变量中存放的是地址（字符串/字符数组的首地址），绝不是将字符串放到字符指针变量中（是字符串首地址） [图]\n2、对字符数组只能对各个元素赋值，不能用以下方法对字符数组赋值\n1 2 3 char str[14]; str=\u0026#34; hello tom\u0026#34;; //错误 str[0] = \u0026#39;i\u0026#39;; //ok 3、对字符指针变量，采用下面方法赋值, 是可以的\n1 2 char* a=\u0026#34;yes\u0026#34;; a=\u0026#34; hello tom\u0026#34;; 4、如果定义了一个字符数组，那么它有确定的内存地址(即字符数组名是一个常量)；而定义一个字符指针变量时，它并未指向某个确定的字符数据，并且可以多次赋值 [代 码+图解]\n字符串相关函数 常用字符串函数一览\n字符串(字符数组)使用注意事项和细节\n1、程序中往往依靠检测 \u0026lsquo;\\0\u0026rsquo; 的位置来判定字符串是否结束，而不是根据数组的长度来决定字符串长度。因此，字符串长度不会统计 \u0026lsquo;\\0\u0026rsquo;, 字符数组长度会统计 [案例]\n2、在定义字符数组时应估计实际字符串长度，保证数组长度始终大于字符串实际长度，否则，在输出字符数组时可能出现未知字符.\n3、系统对字符串常量也自动加一个\u0026rsquo;\\0\u0026rsquo;作为结束符。例如\u0026quot;C Program”共有9个字符，但在内存中占10个字节，最后一个字节\u0026rsquo;\\0\u0026rsquo;是系统自动加上的。（通过sizeof()函数可验证）\n4定义字符数组时，如果 给的字符个数 比 数组的长度小，则系统会默认将剩余的元素空间，全部设置为 \u0026lsquo;\\0\u0026rsquo;, 比如 char str[6] = \u0026ldquo;ab\u0026rdquo; , str内存布局就是[a][b][\\0][\\0][\\0][\\0]\n5、字符数组定义和初始化的方式比较多，比如\n1 2 3 4 5 6 7 8 9 10 11 #include \u0026lt;stdio.h\u0026gt; int main(){ char str1[ ]={\u0026#34;I am happy\u0026#34;}; // 默认后面加 \u0026#39;\\0\u0026#39; char str2[ ]=\u0026#34;I am happy\u0026#34;; // 省略{}号 ,默认后面加 \u0026#39;\\0\u0026#39; char str3[ ]={\u0026#39;I\u0026#39;,\u0026#39; \u0026#39;,\u0026#39;a\u0026#39;,\u0026#39;m\u0026#39;,\u0026#39; \u0026#39;,\u0026#39;h\u0026#39;,\u0026#39;a\u0026#39;,\u0026#39;p\u0026#39;,\u0026#39;p\u0026#39;,\u0026#39;y\u0026#39;}; // 字符数组后面不会加 \u0026#39;\\0\u0026#39;, 可能有乱码 char str4[5]={\u0026#39;C\u0026#39;,\u0026#39;h\u0026#39;,\u0026#39;i\u0026#39;,\u0026#39;n\u0026#39;,\u0026#39;a\u0026#39;}; //字符数组后面不会加 \u0026#39;\\0\u0026#39;, 可能有乱码 char * pStr = \u0026#34;hello\u0026#34;; //可以，正确！ printf(\u0026#34;str=%s\u0026#34;,pStr); getchar(); } 排序和查找 排序也称排序算法(Sort Algorithm)，排序是将一组数据，依指定的顺序进行排列的过程。\n排序的分类：\n1、内部排序:\n指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。\n2、 外部排序法：\n数据量过大，无法全部加载到内存中，需要借助外部存储进行排序\n冒泡排序 冒泡排序（Bubble Sorting）的基本思想是：通过对待排序序列从前向后（从下标较小的元素开始）,依次比较相邻元素的值，若发现逆序则交换，使值较大的元素逐渐从前移向后部，就象水底下的气泡一样逐渐 向上冒\n因为排序的过程中，各元素不断接近自己的位置，==如果一趟比较下来没有进行过交换，就说明序列有序==，因此要在排序过程中设置 一个标志flag判断元素是否进行过交换。从而减少不必要的比较\n规律探寻：\n代码实现，第一尝试\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #include\u0026lt;stdio.h\u0026gt; void main(){ int arr[] = {3,9,-1,10,-2}; // 第1轮排序 int j; int t; //临时变量 for(j=0;j\u0026lt;4;j++){ if(arr[j]\u0026gt;arr[j+1]){ //如果前面的数大于后面的就交换 t = arr[j]; arr[j] = arr[j+1]; arr[j+1] = t; } } // 第1轮排序后，结果 for(j=0;j\u0026lt;5;j++){ printf(\u0026#34;%d \u0026#34;,arr[j]); } printf(\u0026#34;\\n------------------------\\n\u0026#34;); // 第2轮排序 for(j=0;j\u0026lt;3;j++){ if(arr[j]\u0026gt;arr[j+1]){ //如果前面的数大于后面的就交换 t = arr[j]; arr[j] = arr[j+1]; arr[j+1] = t; } } // 第2轮排序后，结果 for(j=0;j\u0026lt;5;j++){ printf(\u0026#34;%d \u0026#34;,arr[j]); } printf(\u0026#34;\\n------------------------\\n\u0026#34;); // 第3轮排序 for(j=0;j\u0026lt;2;j++){ if(arr[j]\u0026gt;arr[j+1]){ //如果前面的数大于后面的就交换 t = arr[j]; arr[j] = arr[j+1]; arr[j+1] = t; } } // 第3轮排序后，结果 for(j=0;j\u0026lt;5;j++){ printf(\u0026#34;%d \u0026#34;,arr[j]); } printf(\u0026#34;\\n------------------------\\n\u0026#34;); // 第4轮排序 for(j=0;j\u0026lt;1;j++){ if(arr[j]\u0026gt;arr[j+1]){ //如果前面的数大于后面的就交换 t = arr[j]; arr[j] = arr[j+1]; arr[j+1] = t; } } // 第4轮排序后，结果 for(j=0;j\u0026lt;5;j++){ printf(\u0026#34;%d \u0026#34;,arr[j]); } getchar(); } 代码实现，进一步优化，实现==最终结果==：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include\u0026lt;stdio.h\u0026gt; void main(){ int arr[] = {3,9,-1,10,-2}; int j; int i; int t; //临时变量 int arrLen = sizeof(arr)/sizeof(int); //数组的长度 for(i=0;i\u0026lt;arrLen-1;i++){ for(j=0;j\u0026lt;arrLen-1-i;j++){ //4是递减 if(arr[j]\u0026gt;arr[j+1]){ //如果前面的数大于后面的就交换 t = arr[j]; arr[j] = arr[j+1]; arr[j+1] = t; } } } //结果 for(j=0;j\u0026lt;5;j++){ printf(\u0026#34;%d \u0026#34;,arr[j]); } getchar(); } 顺序查找 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include\u0026lt;stdio.h\u0026gt; int queryNum (int arr[], int len, int val){ int i; for(i=0; i\u0026lt;len; i++){ if(arr[i]==val){ return i; } } return 1; } void main(){ int arr[] = {1,2,3,4}; int len = sizeof(arr)/sizeof(int); int index = queryNum(arr,len,3); if(index == -1){ printf(\u0026#34;抱歉！找不到啊\u0026#34;); }else{ printf(\u0026#34;找到了，下标为%d\u0026#34;,arr[index]); } getchar(); } 二分查找 前提条件：必须是有序数组\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include\u0026lt;stdio.h\u0026gt; // 二分查找 int queryNum (int arr[], int leftIndex, int rightIndex\t,int findVal){ // 先找到中间这个数midVal; int midIndex = (leftIndex + rightIndex)/2; int midVal = arr[midIndex]; // 如果leftIndex \u0026gt; rightIndex ，说明这个数组都比较过了，没有找到 if(leftIndex\u0026gt;rightIndex){ return -1; } // 如果midVal \u0026gt; findVal 说明应该在左边查找 if(midVal \u0026gt; findVal){ queryNum(arr,leftIndex,midIndex-1,findVal); // 如果midVal \u0026gt; findVal 说明应该在左边查找 }else if(midVal \u0026gt; findVal){ queryNum(arr,midIndex + 1,rightIndex,findVal); }else{ return midIndex; // 找到了，就是中间这个数，返回中间这个数的下标 } } void main(){ int arr[] = {1,2,3,4,6,10,11}; int len = sizeof(arr)/sizeof(int); int index = queryNum(arr,0,len-1,51111); if(index != -1){ printf(\u0026#34;找到了，数组下标为%d\u0026#34;,index); }else{ printf(\u0026#34;没有找到！\u0026#34;); } getchar(); } 多维数组-二维数组 快速入门案例： 请用二维数组输出如下图形\n1 2 3 4 5 6 7 0 0 0 0 0 0 0 0 1 0 0 0 0 2 0 3 0 0 0 0 0 0 0 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include \u0026lt;stdio.h\u0026gt; void main() { //a[4][6] : 表示一个 4 行 6 列的二维数组 int a[4][6]; // 没有初始化，则是分配的内存垃圾值 int i, j; //全部初始化 0 for(i = 0; i \u0026lt; 4; i++) { //先遍历行 for(j = 0; j \u0026lt; 6; j++) {//遍历列 a[i][j] = 0; } } a[1][2] = 1; a[2][1] = 2; a[2][3] = 3; //输出二维数组 for(i = 0; i \u0026lt; 4; i++) { for(j = 0; j \u0026lt; 6; j++) { printf(\u0026#34;%d \u0026#34;, a[i][j]); } printf(\u0026#34;\\n\u0026#34;); } ////看看二维数组的内存布局 printf(\u0026#34;\\n 二维数组 a 的首地址=%p\u0026#34;, a); printf(\u0026#34;\\n 二维数组 a[0]的地址=%p\u0026#34;, a[0]); printf(\u0026#34;\\n 二维数组 a[0][0]的地址=%p\u0026#34;, \u0026amp;a[0][0]); printf(\u0026#34;\\n 二维数组 a[0][1]的地址=%p\u0026#34;, \u0026amp;a[0][1]); printf(\u0026#34;\\n\u0026#34;); for(i = 0; i \u0026lt; 4; i++) { printf(\u0026#34;a[%d]的地址=%p \u0026#34;, i, a[i]); for(j=0; j \u0026lt; 6; j++) { printf(\u0026#34;a[%d][%d]的地址=%p \u0026#34;, i, j , \u0026amp;a[i][j]); } printf(\u0026#34;\\n\u0026#34;); } getchar(); } 使用方式 2\n1 2 类型 数组名[大小][大小] = {{值 1,值 2..},{值 1,值 2..},{值 1,值 2..}}; 2) 或者 类型 数组名[大小][大小] = { 值 1,值 2,值 3,值 4,值 5,值 6 ..}; 注意事项：\n1、可以只对部分元素赋值，未赋值的元素自动取“零”值\n2、如果对全部元素赋值，那么第一维的长度可以不给出\n1 2 3 int a[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; int a[][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; 3、二维数组可以看作是由一维数组嵌套而成的；如果一个数组的每个元素又是一个数组，那么它就是二维数组。\n1 2 3 二维数组a[3][4]可看成三个一维数组，它们的数组名分别为 a[0]、a[1]、a[2]。 这三个一维数组都有 4 个元素，如，一维数组 a[0] 的元素为 a[0][0]、a[0][1]、a[0][2]、a[0][3] 断点调试 一个实际需求:\n在开发中，程序员发现一个非常诡异的错误，怎么看源代码都发现不了这个错误，这时老程序员就会温馨提示，可以使用断点调试，一步一步的看源码执行的过程，从而发现错误所在。\n什么是断点调试\n百度百科：断点调试是指自己在程序的某一行设置一个断点，调试时，程序运行到这一行就会停住，然后你可以一步一步往下调试，调试过程中可以看各个变量当前的值，出错的话，调试到出错的代码行即显示错误，停下。然后程序可以进行分析从而找到这个。\n快捷键\n1 2 3 4 5 f5： 开始调试 、执行到下一个断点 f11: 逐句执行代码, 会进入到函数体中 f10: 逐过程执行(遇到函数，不会进入到函数体) shift+f11: 跳出(跳出某个函数, 跳出前，会将该函数执行完) shift+f5: 终止调试 指针 指针表示一个地址（存放的是地址），如下图\n1 2 3 4 5 6 7 8 9 10 11 //指针入门 # include \u0026lt;stdio.h\u0026gt; void main(){ int num = 1; int *ptr = \u0026amp;num; // num的地址 // 如果要输出一个变量的地址，使用的格式是%p printf(\u0026#34;num 的地址为：%p\u0026#34;,\u0026amp;num); printf(\u0026#34;\\n ptr 的地址为：%p , ptr存放值的一个地址为：%p ， ptr指向的地址的值是多少%d：\u0026#34;,\u0026amp;ptr,ptr,*ptr); getchar(); } 运行结果：\n练习案例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //指针入门 # include \u0026lt;stdio.h\u0026gt; void main(){ //写一个程序，获取一个 int 变量 num 的地址，并显示到终端尚硅谷高校大学生 C 语言课程 //将 num 的地址赋给指针 ptr , 并通过 ptr 去修改 num 的值. //分析 ptr 的类型 是 int * , 注意指针的类型和 该指针指向的变量类型是对应关系 //并画出案例的内存布局图 //int num = 88; //int * ptr = # ////先输出 num 没有修改的情况 //printf(\u0026#34;\\nnum 的值=%d num 的地址=%p\u0026#34;, num, \u0026amp;num); // num= 88 //*ptr = 99; // 通过 ptr 去修改 num 的值, num 变量的值也就相应的被修改 //printf(\u0026#34;\\n num 的值=%d num 的地址=%p\u0026#34;, num, \u0026amp;num);// num = 99 int a = 300; // a = 300 int b = 400; // b = 400 int * ptr = \u0026amp;a; //ok ptr 指向 a *ptr = 100; // a = 100 ptr = \u0026amp;b; // ok ptr 指向 b *ptr = 200; // b = 200 printf(\u0026#34;\\n a=%d,b=%d,*ptr=%d\u0026#34;, a, b, *ptr); //a = 100, b = 200, *ptr = 200 getchar(); } 指针细节说明 1、基本类型，都有对应的指针类型， 形式为 数据类型 *，比如 int 的对应的指针就是 int *, float 对应的指针类型就是 float * , 依次类推。\n2、 此外还有指向数组的指针、指向结构体的指针，指向共用体的指针，(二级指针，多级指针)后面我们再讲到数\n组、结构体和共用体时，还会详细讲解。\n值传递和地址传递 1、 默认==传递值==的类型：基本数据类型 (整型类型、小数类型，字符类型), 结构体, 共用体。\n2、默认==地址传递（指针传递）== 的类似：指针、数组\n指针的运算符 指针是一个用数值表示的地址。可以对指针执行算术运算。可以对指针进行四种算术运算：++、\u0026ndash;、+、-。\n指针递增操作(++)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include\u0026lt;stdio.h\u0026gt; const int MAX = 3; int main () { int var[] = {10, 100, 200}; // int i, *ptr; // ptr = var; for ( i = 0; i \u0026lt; MAX; i++) { printf(\u0026#34;var[%d] 地址= %p \\n\u0026#34;, i, ptr ); printf(\u0026#34;存储值：var[%d] = %d\\n\u0026#34;, i, *ptr ); ptr++; } getchar(); return 0; } 运行结果：\n指针递增操作(\u0026ndash;)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include\u0026lt;stdio.h\u0026gt; const int MAX = 3; int main () { int var[] = {10, 100, 200}; int i, *ptr; /* 指针中最后一个元素的地址 */ ptr = \u0026amp;var[MAX-1]; for ( i = MAX; i \u0026gt; 0; i--) { printf(\u0026#34;ptr存放的地址=%p, var[%d] 的地址= %p\\n\u0026#34;, ptr, i \u0026amp;var[i-1] ); printf(\u0026#34;存储值：var[%d] = %d\\n\u0026#34;, i-1, *ptr ); ptr--; } getchar(); return 0; } 运行结果：\n指针+\n1 2 3 4 5 6 7 8 9 10 11 #include\u0026lt;stdio.h\u0026gt; int main () { int var[] = {10, 100, 200}; int i, *ptr; ptr = var; ptr += 2; // printf(\u0026#34;var[2]=%d var[2]的地址=%p ptr=%p ptr指向的地址的内容=%d\u0026#34; ,var[2], \u0026amp;var[2], ptr, *ptr); getchar(); return 0; } 运行结果：\n指针数组 要让数组的元素 指向 int 或其他数据类型的地址(指针)。可以使用指针数组\n指针数组定义\n1 2 数据类型 *指针数组名[大小]; 比如： int *ptr[3]; 1、 ptr 声明为一个指针数组 2、由 3 个整数指针组成。因此，ptr 中的每个元素，都是一个指向 int 值的指针。\n快速入门 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include\u0026lt;stdio.h\u0026gt; void main (){ int var[] = {10, 100, 200}; int i, *ptr[3]; int MAX = 3; for ( i = 0; i \u0026lt; MAX; i++) { ptr[i] = \u0026amp;var[i]; /* 赋值为整数的地址 */ } for ( i = 0; i \u0026lt; MAX; i++) { printf(\u0026#34;Value of var[%d] = %d\\n\u0026#34;, i, *ptr[i] ); } getchar(); } 字符串指针数组\n请编写程序，定义一个指向字符的指针数组来存储字符串列表(四大名著书名)， 并通过遍历 该指针数组，显示字符串信息 ， (即：定义一个指针数组，该数组的每个元素，指向的是一个字符串)\n代码实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include\u0026lt;stdio.h\u0026gt; void main(){ char *books[] = { \u0026#34;水浒传\u0026#34;, \u0026#34;三国演义\u0026#34;, \u0026#34;西游记\u0026#34;, \u0026#34;红楼梦\u0026#34;, \u0026#34;水浒传\u0026#34; }; int i ,len = 4; for(i=0; i\u0026lt;len; i++){ printf(\u0026#34;\\nboos[%d]指向的字符串是《%s》\u0026#34;,i,books[i] ); // 不用加* } getchar(); } 多重指针 指向指针的指针是一种多级间接寻址的形式，或者说是一个指针链。通常，一个指针包含一个变量的地址。当我们定义一个指向指针的指针时，第一个指针包含了第二个指针的地址，第二个指针指向包含实际值的位置(如下图)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include\u0026lt;stdio.h\u0026gt; int main () { int var; int *ptr; int **pptr; var = 3000; ptr = \u0026amp;var; pptr = \u0026amp;ptr; printf(\u0026#34;var的地址=%p var = %d \\n\u0026#34;, \u0026amp;var, var ); printf(\u0026#34;ptr 的本身的地址=%p ptr存放的地址=%p *ptr = %d \\n\u0026#34;, \u0026amp;ptr, ptr, *ptr ); printf(\u0026#34;pptr 本身地址 = %p pptr存放的地址=%p **pptr = %d\\n\u0026#34;, \u0026amp;pptr, pptr, **pptr); getchar(); return 0; } 返回指针的函数 请编写一个函数 strlong()，返回两个字符串中较长的一个。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include\u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; char *strlong(char *str1, char *str2){ //函数返回的char * (指针) printf(\u0026#34;\\nstr1的长度%d str2的长度%d\u0026#34;, strlen(str1), strlen(str2)); if(strlen(str1) \u0026gt;= strlen(str2)){ return str1; }else{ return str2; } } int main(){ char str1[30], str2[30], *str; printf(\u0026#34;\\n请输入第1个字符串\u0026#34;); gets(str1); printf(\u0026#34;\\n请输入第2个字符串\u0026#34;); gets(str2); str = strlong(str1, str2); printf(\u0026#34;\\nLonger string: %s \\n\u0026#34;, str); getchar(); return 0; } 结果如图：\n1、用指针作为函数返回值时需要注意，函数运行结束后会销毁在它内部定义的所有局部数据，包括局部变量、局部数组和形式参数，函数返回的指针不能指向这些数据【案例演示】\n2、函数运行结束后会销毁该函数所有的局部数据， 这里所谓的销毁并不是将局部数据所占用的内****存全部清零，而是程序放弃对它的使用权限，后面的代码可以使用这块内存\n3、C 语言不支持在调用函数时返回局部变量的地址，如果确实有这样的需求，需要定义局部变量为 static 变量\n函数指针 1、 一个函数总是占用一段连续的内存区域，函数名在表达式中有时也会被转换为该函数所在内存区域的首地址，这和数组名非常类似。\n2、把函数的这个首地址（或称入口地址）赋予一个指针变量，使指针变量指向函数所在的内存区域，然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针\nreturnType (*pointerName)(param list);\n1 2 3 4 5 6 7 8 9 1、 `returnType `为函数返回值类型 2、`pointerName `为指针名称 3、`param list` 为函数参数列表 4、参数列表中可以同时给出参数的类型和名称，也可以只给出参数的类型，省略参数的名称 5、注意( )的优先级高于*，第一个括号不能省略，如果写作r`eturnType *pointerName(param list);`就成了函数原型，它表明函数的返回值类型为`returnType ` 演示案例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include\u0026lt;stdio.h\u0026gt; int max(int a, int b); int main(){ int x, y, maxVal; // 说明：函数指针\t// 函数指针的名字：pmax // int 表示：函数指针指向的方式返回值是int类型 // (int, int) 表示该函数指针指向的函数形参是接受两个int // 在定义指针函数时，也可以写上形参名：例如-int (*pmax)(int x, int y) =max int (*pmax)(int, int) = max; printf(\u0026#34;Pleace input two numbers:\u0026#34;); scanf(\u0026#34;%d %d\u0026#34;, \u0026amp;x, \u0026amp;y); maxVal = (*pmax)(x, y); printf(\u0026#34;Max value: %d\\n\u0026#34;, maxVal); getchar(); getchar(); return 0; } int max(int a, int b){ return a\u0026gt;b ? a : b; } 回调函数\n1、 函数指针变量可以作为某个函数的参数来使用的，回调函数就是一个通过函数指针调用的函数。\n2、简单的讲：回调函数是由别人的函数执行时调用你传入的函数（通过函数指针完成）\n案例演示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include\u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; /* 回调函数) f 就是一个函数指针，他可以接收的函数式 (返回 int，没有形参的函数) */ void initArray(int *array, int arraySize, int (*f)(void)) { int i ; for ( i=0; i\u0026lt;arraySize; i++) array[i] = f(); // 循环10次，回调了我们的getNextRandomValue(void)函数 } // 获取随机值 int getNextRandomValue(void) { return rand(); //系统函数，可以返回一个随机整数！ } int main(void) { int myarray[10],i; /* 说明： 1、调用initArray函数 2、传入一个函数名getNextRandomValue(地址)，需要使用函数指针介绍 initArray(myarray, 10, getNextRandomValue); */ initArray(myarray, 10, getNextRandomValue); for(i = 0; i \u0026lt; 10; i++) { printf(\u0026#34;%d \u0026#34;, myarray[i]); } printf(\u0026#34;\\n\u0026#34;); getchar(); return 0; } 1 指针变量存放的是地址，从这个角度看指针的本质就是地址。\n2变量声明的时候，如果没有确切的地址赋值，为指针变量赋一个 NULL 值是好的编程习惯。\n3 赋为 NULL 值的指针被称为空指针，NULL 指针是一个定义在标准库 \u0026lt;stdio.h\u0026gt;中的值为零的常量 #define NULL 0\n1 2 3 4 5 6 7 8 9 #include\u0026lt;stdio.h\u0026gt; void main(){ int *p = NULL; // 空指针p int num =33; p = \u0026amp;num; printf(\u0026#34;p= %d\u0026#34;,*p); getchar(); } 4、 指针使用一览 (见后)\n空指针 赋为 NULL 值的指针被称为空指针，NULL 指针是一个定义在标准库 \u0026lt;stdio.h\u0026gt;中的值为零的常量 #define NULL 0\n1 2 3 4 5 6 7 8 9 #include\u0026lt;stdio.h\u0026gt; void main(){ int *p = NULL; // 空指针p int num =33; p = \u0026amp;num; printf(\u0026#34;p= %d\u0026#34;,*p); getchar(); } 动态内存分配 1、全局变量——内存中的静态存储区\n2、非静态的局部变量——内存中的动态存储区——stack 栈\n3、临时使用的数据\u0026mdash;建立动态内存分配区域，需要时随时开辟，不需要时及时释放——heap 堆\n4、根据需要向系统申请所需大小的空间，由于未在声明部分定义其为变量或者数组，不能通过变量名或者数组名来引用这些数据，只能通过指针来引用\n内存动态分配的相关函数\n1、头文件 #Include \u0026lt;stdlib.h\u0026gt; 声明了四个关于内存动态分配的函数\n2、函数原型\n1 void * malloc（usigned int size） //malloc = memory allocation 作用——在内存的动态存储区(堆区)中分配一个长度为size的连续空间。 形参size的类型为无符号整型，函数返回值是所分配区域的第一个字节的地址，即此函数是一个指针型函数，返回的指针指向该分配域的开头位置。 malloc(100); 开辟100字节的临时空间，返回值为其第一个字节的地址 3、函数原型\n1 void *calloc（unsigned n,unsigned size） 作用——在内存的动态存储区中分配n个长度为size的连续空间，这个空间一般比较大，足以保存一个数组 用calloc函数可以为一维数组开辟动态存储空间，n为数组元素个数，每个元素长度为size. 函数返回指向所分配域的起始位置的指针；分配不成功，返回NULL。 p = calloc(50, 4); //开辟 50*4 个字节临时空间，把起始地址分配给指针变量 p 4、函数原型：void free（void *p）\n作用——释放变量p所指向的动态空间，使这部分空间能重新被其他变量使用。 p是最近一次调用calloc或malloc函数时的函数返回值 free函数无返回值 free(p); // 释放p 所指向的已分配的动态空间 5、函数原型 void *realloc（void *p，unsigned int size)\n作用——重新分配malloc或calloc函数获得的动态空间大小，将p指向的动态空间大小改变为size，p的值不变，分配失败返回NULL realloc(p, 50); // 将 p 所指向的已分配的动态空间 改为50字节 6、返回类型说明\nC99标准把malloc，calloc，relloc函数的基本类型定位void类型，这种指针称为无类型指针(typeless pointer),就是指不指向某一种特定的类型的数据，仅提供一个存地址，而不指向任何具体的对象。\n应用实例\n动态创建数组，输入 5 个学生的成绩，另外一个函数检测成绩低于 60 分的，输出不合格的成绩\n代码实现：\n结构体 张老太养了两只猫猫:一只名字叫小白,今年3岁,白色。还有一只叫小花,今年100岁,花色。请编写一个程序，当用户输入小猫的名字时，就显示该猫的名字，年龄，颜色。如果用户输入的小猫名错误，则显示 张老太没有这只猫猫\n目前所学知识-传统方案：\n1 2 3 4 5 6 7 8 9 10 11 void main() { char cat1Name[10] = \u0026#34;小白\u0026#34;; int cat1Age = 3; char cat1Color[10] = \u0026#34;白色\u0026#34;; char *catsName[2] = { \u0026#34;小白\u0026#34;, \u0026#34;小花\u0026#34; } ; int catsAge[] = {3,100} ==缺点：==\n不利于数据管理和维护，因为本身的猫的三个属性（名字，年龄，颜色）是一个整体，传统方式会将其拆解，这是就需要用到结构体！\n结构体快速入门 结构体与结构体变量的关系示意图\n==说明：==除了可以有Cat 结构体，还可以有 Person 结构体， 有 Fish 结构体等等\n结构体与结构体变量的关系示意图\n快速入门-面向对象的方式(struct)解决养猫问题\n代码实现：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include\u0026lt;stdio.h\u0026gt; void main(){ struct Cat{ //结构体名为Cat，Cat是我们自己构成的一个数据类型！ char *name; // 名字，使用指针，指向一个字符串 int age; // 年龄 char *color; // 颜色 }; // 使用结构体Cat，创建对应的变量 struct Cat cat1; cat1.name =\u0026#34;小白\u0026#34;; cat1.age = 3; cat1.color = \u0026#34;白色\u0026#34;; // 使用结构体Cat，创建对应的变量 struct Cat cat2; cat2.name =\u0026#34;小花\u0026#34;; cat2.age = 100; cat2.color = \u0026#34;花色\u0026#34;; // 输出两只猫的信息 printf(\u0026#34;\\n第一只猫 name = %s age= %d color = %s\u0026#34;,cat1.name,cat1.age,cat1.color); printf(\u0026#34;\\n第二只猫 name = %s age= %d color = %s\u0026#34;,cat2.name,cat2.age,cat2.color); getchar(); } 结构体和结构体变量的区别和联系\n结构体是==自定义的数据类型==，表示的是一种==数据类型==.\n结构体变量代表一个具体变量，好比\n1 2 nt num1 ; // int 是数据类型, 而num1 是一个具体的int变量 struct Cat cat1; // Cat 是结构体数据类型， 而cat1 是一个Cat变量 Cat 就像一个“模板”，定义出来的结构体变量都含有相同的成员。也可以将结构体比作“图纸”，将结构体变量比作“零件”，根据同一张图纸生产出来的零件的特性都是一样的\n结构体内存分析：\n声明结构体 1 2 3 struct 结构体名称 { // 结构体名首字母大写，比如Cat, Person 成员列表; } 举例:\n1 2 3 4 5 6 7 8 9 10 11 12 #include\u0026lt;stdio.h\u0026gt; void main(){ struct Student{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在学习小组 float score; //成绩 //成员也可以是结构体 }; } 注意事项和细节说明 1、成员声明语法同变量，示例： 数据类型成员名; 2、 字段的类型可以为：基本类型、数组或指针、结构体等 3、 在创建一个结构体变量后，需要给成员赋值，如果没有赋值就使用可能导致程序异常终止。[ 案例演示 ]：\n1 2 3 4 5 6 7 8 9 10 11 #include\u0026lt;stdio.h\u0026gt; void main(){ struct Cat{ // 结构体名字建议首写字母大写 char *name; //名字 , 这里需要使用指针类型 int age; //年龄 char *color; // 颜色 }; struct Cat cat1; printf(\u0026#34;\\n名字=%s 年龄=%d 颜色=%s \u0026#34;, cat1.name, cat1.age, cat); getchar(); } 4、 不同结构体变量的成员是独立，互不影响，一个结构体变量的成员 更改，不影响另外一个。[案例演示+图(Monster)]\n创建结构体和结构体变量 方式1-先定义结构体，然后再创建结构体变量\n1 2 3 4 5 6 7 8 9 10 11 12 13 #include\u0026lt;stdio.h\u0026gt; void main(){ struct Stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在学习小组 float score; //成绩 }; struct Stu stu1, stu2; //定义了两个变量 stu1 和 stu2，它们都是 Stu 类型，都由 5 个成员组成 //注意关键字struct不能少 } 方式2-在定义结构体的同时定义结构体变量\n1 2 3 4 5 6 7 8 9 10 11 #include\u0026lt;stdio.h\u0026gt; void main(){ struct Stu{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在学习小组 float score; //成绩 } stu1, stu2; //在定义结构体Stu 的同时，创建了两个结构体变量 stu1 和 stu2 } 方式3-如果只需要 stu1、stu2 两个变量，后面不需要再使用结构体数据类型，定义其他变量，在定义时也可以不给出结构体名\n1 2 3 4 5 6 7 8 9 10 struct { //没有写 Stu char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在学习小组 float score; //成绩 } stu1, stu2; stu1.name = \u0026#34;tom\u0026#34;; stu1.num = 100;.... //1. 该结构体数据类型，没有名, 匿名结构体 //2. stu1 和 stu2 就是 该结构体的两个变量 案例2：\n1 2 3 4 5 6 7 8 9 10 #include\u0026lt;stdio.h\u0026gt; void main(){ struct{ char *name; //姓名 int num; //学号 int age; //年龄 char group; //所在小组 float score; //成绩 } stu1 = {\u0026#34;贾宝玉\u0026#34;, 11, 18, \u0026#39;B\u0026#39;, 90.50}, stu2 = { \u0026#34;林黛玉\u0026#34;, 12, 16, \u0026#39;A\u0026#39;, 100 }; } 应用案例 1、 编写一个Dog结构体，包含name(char[10])、age(int)、weight(double)属性\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include\u0026lt;stdio.h\u0026gt; struct Dog{ char *name; int age; double weight; }; // say函数 char *say(struct Dog dog){ // 将这个信息放入一个字符串中 static char info[100]; // 局部变量 sprintf(info, \u0026#34;name=%s age = %d weight = %.2f\u0026#34;,dog.name,dog.age,dog.weight); return info; } void main(){ //定义结构体变量 struct Dog dog = {\u0026#34;贺晶晶\u0026#34;,18,99.9}; char* info = NULL; info = say(dog); printf(\u0026#34;小狗信息为：%s\u0026#34;,info); getchar(); } 景区门票案例：\n2.请编写游人结构体(Visitor)，根据年龄段决定能够购买的门票价格并输出\n3.规则：年龄\u0026gt;18 , 门票为 20 元，其它情况免费。\n4.可以循环从控制台输入名字和年龄，打印门票收费情况, 如果名字输入 n ,则退出程序。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #include\u0026lt;stdio.h\u0026gt; #include\u0026lt;string.h\u0026gt; //定义结构体 struct Visitor { char name[10]; int age; double pay; //应付票价 }; void ticket(struct Visitor * visitor) { //判断 if( (*visitor).age \u0026gt; 18) { (*visitor).pay = 20; } else { (*visitor).pay = 0; } } void main() { //测试 //创建结构体变量(创建一个游客) struct Visitor visitor; //循环的输入名字和年龄 while(1) { printf(\u0026#34;\\n 请输入游客名字\u0026#34;); scanf(\u0026#34;%s\u0026#34;, visitor.name); //判断如果名字输入 n ,则退出程序 if(!strcmp(\u0026#34;n\u0026#34;, visitor.name) ) { break; } printf(\u0026#34;\\n 请输入游客年龄\u0026#34;); scanf(\u0026#34;%d\u0026#34;, \u0026amp;visitor.age); //调用函数 ticket ， 获取应付的票价 ticket(\u0026amp;visitor); printf(\u0026#34;\\n 该游客应付票价=%.2f\u0026#34;, visitor.pay); } printf(\u0026#34;退出程序\u0026#34;); getchar(); getchar(); } 盒子案例：\n编程创建一个 Box 结构体，在其中定义三个成员表示一个立方体的长、宽和高，长宽高可以通过控制台输入。 定义一个函数获取立方体的体积(volume)。 创建一个结构体，打印给定尺寸的立方体的体积。\n共用体 现有一张关于学生信息和教师信息的表格。学生信息包括姓名、编号、性别、职业、分数，教师的信息包括姓名、编号、性别、职业、教学科目。请看下面的表格：\n传统的方式来解决：\n1 2 3 4 5 6 7 8 struct Person{ char name[20]; int num; char sex; char profession; float score; // 学生使用 score char course[20]; // 老师使用course }; ==会造成 空间的浪费 , 比如学生只使用 score ,但是 也占用了course 成员的20个字节==\n解决方案\n(1)、做 struct Stu 和 struct Teacher [但如果职业很多，就会对应多个结构体类型, 不利于管理\n(2)、使用共用体\n什么是共用体 1、共用体（Union）属于 构造类型，它可以包含多个类型不同的成员。和结构体非常类似, 但是也有不同的地方. 共用体有时也被称为==联合==或者==联合体==, 定义格式如下：\n1 2 3 union 共用体名{ 成员列表 }; 结构体和共用体的区别在于：结构体的各个成员会占用不同的内存，互相之间没有影响；而共用体的所有成员占用同一段内存，修改一个成员会影响其余所有成员\n快速入门 定义共用体类型和共用体变量的三种方式(和结构体一样)\n1：先定义共用体，然后再创建共用体变量\n1 2 3 4 5 6 union data{ int n; char ch; double f; }; union data a, b, c; 2: 定义共用体的同时也创建共用体变量\n1 2 3 4 5 union data{ int n; char ch; double f; } a, b, c; 3：匿名共用体\n1 2 3 4 5 union{ int n; char ch; double f; } a, b, c; 案例演示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include\u0026lt;stdio.h\u0026gt; union data{ // data就是一个共用体数据类型，三个成员公用数据空间，该空间的大小以占用最大的成员为准 int n; // 4个字节 char ch; // 1 个字节 short m; // 2个字节 }; void main(){ union data a; // 定义一个共用体变量 a printf(\u0026#34;%d -- %d\\n\u0026#34;,sizeof(a),sizeof(union data)); // 4个字节 a.n = 0x40; // 16进制 printf(\u0026#34;%d, %c, %d\\n\u0026#34;, a.n, a.ch, a.m); a.ch = \u0026#39;9\u0026#39;; printf(\u0026#34;%d, %c, %d\\n\u0026#34;, a.n, a.ch, a.m); a.m = 0x2059; printf(\u0026#34;%d, %c, %d\\n\u0026#34;, a.n, a.ch, a.m); a.n = 0x3E25AD54; printf(\u0026#34;%d, %c, %d\\n\u0026#34;, a.n, a.ch, a.m); getchar(); } 共用体内存布局分析 要深入理解为什么前面的案例输出的结果，就需要剖析共用体在内存中是如何布局的\n案例实践 现有一张关于学生信息和教师信息的表格。学生信息包括姓名、编号、性别、职业、分数，教师的信息包括姓名、编号、性别、职业、教学科目。请看下面的表格-请使用共用体编程完成.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include\u0026lt;stdio.h\u0026gt; #define TOTAL 2 //人员总数 // 定义了一个结构体Persion，包含了一个匿名共用体sc struct Person{ char name[20]; // name int num;// 编号 char sex; // 性别 f = 女 m = 男 char profession; // 职业 s=\u0026gt;学生 t=\u0026gt;老师 union{ //sc是一个共用体变量 float score; char course[20]; } sc; }; void main(){ int i; struct Person persons[TOTAL]; // 定义了一个结构体数组 //输入人员信息 for(i=0; i\u0026lt;TOTAL; i++){ printf(\u0026#34;Please input infomation: \u0026#34;); scanf(\u0026#34;%s %d %c %c\u0026#34;, persons[i].name, \u0026amp;(persons[i].num), \u0026amp;(persons[i].sex), \u0026amp;(persons[i].profession)); // 对于int 和char字符，需要在前面加\u0026amp; 用于取地址 if(persons[i].profession == \u0026#39;s\u0026#39;){ //如果是学生 printf(\u0026#34;Please input the student\u0026#39;s score:\u0026#34;); scanf(\u0026#34;%f\u0026#34;, \u0026amp;persons[i].sc.score); }else{ //如果是老师 printf(\u0026#34;Please input the teacher\u0026#39;s course\u0026#34;); scanf(\u0026#34;%s\u0026#34;, persons[i].sc.course); } fflush(stdin); // 刷新输入 } // 输出成员信息 printf(\u0026#34;\\nName\\t\\tNum\\tSex\\tProfession\\tScore / Course\\n\u0026#34;); for(i=0; i\u0026lt;TOTAL; i++){ if(persons[i].profession==\u0026#39;s\u0026#39;){ printf(\u0026#34;\\n%s\\t\\t%d\\t%c\\t%c\\t%f\\n\u0026#34;,persons[i].name,persons[i].num,persons[i].sex,persons[i].profession,persons[i].sc.score); }else{ printf(\u0026#34;\\n%s\\t\\t%d\\t%c\\t%c\\t%s\\n\u0026#34;,persons[i].name,persons[i].num,persons[i].sex,persons[i].profession,persons[i].sc.course); } } getchar(); // 去回车 getchar(); } CRM系统（项目） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; struct Customer { int id; int age; char name[10]; char gender; char phone[10]; char email[10]; }; char key ; char loop=1; int customerNum=1; //客户结构体数组 struct Customer customers[20]; //得到一个客户的信息 void getInfo(struct Customer *customer) { /*sprintf(info, \u0026#34;\\n%d\\t%s\\t%c\\t%d\\t%s\\t%s\u0026#34;, (*customer).id, (*customer).name, (*customer).gender, (*customer).age, (*customer).phone,(*customer).email);*/ printf(\u0026#34;\\n%d\\t%s\\t%c\\t%d\\t%s\\t%s\u0026#34;, (*customer).id, (*customer).name, (*customer).gender, (*customer).age, (*customer).phone,(*customer).email); } //提供各种操作 //1. 添加 void add(){ //编号自动增长 customers[customerNum].id = customerNum + 1; printf(\u0026#34;\\n---------------------添加客户---------------------\u0026#34;); printf(\u0026#34;\\n姓名：\u0026#34;); scanf(\u0026#34;%s\u0026#34;, customers[customerNum].name); getchar(); printf(\u0026#34;\\n性别：\u0026#34;); scanf(\u0026#34;%c\u0026#34;, \u0026amp;(customers[customerNum].gender)); getchar(); printf(\u0026#34;\\n年龄：\u0026#34;); scanf(\u0026#34;%d\u0026#34;, \u0026amp;(customers[customerNum].age)); getchar(); printf(\u0026#34;\\n电话：\u0026#34;); scanf(\u0026#34;%s\u0026#34;, customers[customerNum].phone); getchar(); printf(\u0026#34;\\n邮箱：\u0026#34;); scanf(\u0026#34;%s\u0026#34;,customers[customerNum].email); getchar(); printf(\u0026#34;\\n---------------------添加完成---------------------\u0026#34;); customerNum++; } //根据输入的id去找对应的下标，如果找不到返回-1 int findIndex(int id){ int index = -1; int i; for (i = 0; i \u0026lt; customerNum ; i++) { if (customers[i].id == id) { index = i; break; } } return index; } //2. 删除客户 int del(int id){ //找到id对应的元素下标 int index = findIndex(id); int i; if (index == -1) { return 0;//说明这个客户不存在.. }else { //找到,就从index+1开始整体前移 for (i = index + 1; i \u0026lt; customerNum; i++) { customers[i - 1] = customers[i]; } --customerNum; return 1; } } //显示部分 //1. 显示所有 void showList(){ int i = 0; printf(\u0026#34;\\n---------------------------客户列表---------------------------\u0026#34;); printf(\u0026#34;\\n编号\\t姓名\\t性别\\t年龄\\t电话\\t邮箱\u0026#34;); for (i = 0; i \u0026lt; customerNum; i++) { getInfo(\u0026amp;customers[i]); } } //2. 完成删除 界面 //---------------------删除客户--------------------- //请选择待删除客户编号(-1退出)：1 //确认是否删除(Y/N)：y //---------------------删除完成--------------------- void delView(){ int id; char choice = \u0026#39; \u0026#39;; printf(\u0026#34;\\n---------------------删除客户---------------------\u0026#34;); printf(\u0026#34;\\n请选择待删除客户编号(-1退出)：\u0026#34;); scanf(\u0026#34;%d\u0026#34;, \u0026amp;id); getchar(); if (id == -1) { printf(\u0026#34;\\n---------------------删除没有完成---------------------\u0026#34;); return; } printf(\u0026#34;确认是否删除(Y/N)：\u0026#34;); scanf(\u0026#34;%c\u0026#34;, \u0026amp;choice); getchar(); if (choice == \u0026#39;Y\u0026#39;) { if(del(id)){ printf(\u0026#34;\\n---------------------删除完成---------------------\u0026#34;); }else{ printf(\u0026#34;\\n---------------------删除没有完成，无此id---------------------\u0026#34;); } } } //3. 主菜单 void mainMenu() { do { printf(\u0026#34;\\n-----------------客户信息管理软件-----------------\u0026#34;); printf(\u0026#34;\\n 1 添 加 客 户\u0026#34;); printf(\u0026#34;\\n 2 修 改 客 户\u0026#34;); printf(\u0026#34;\\n 3 删 除 客 户\u0026#34;); printf(\u0026#34;\\n 4 客 户 列 表\u0026#34;); printf(\u0026#34;\\n 5 退 出\u0026#34;); printf(\u0026#34;\\n请选择(1-5):\u0026#34;); scanf(\u0026#34;%c\u0026#34;, \u0026amp;key); getchar(); switch (key) { case \u0026#39;1\u0026#39;: add(); break; case \u0026#39;2\u0026#39;: break; case \u0026#39;3\u0026#39;: delView(); break; case \u0026#39;4\u0026#39;: showList(); break; case \u0026#39;5\u0026#39;: loop = 0; break; default: printf(\u0026#34;\\n输入错误，请重新输入\u0026#34;); break; } } while (loop); printf(\u0026#34;\\n你已经成功的退出了系统....\u0026#34;); getchar(); } void main() { ////为了测试方便 customers[0].id = 1; customers[0].age = 18; strcpy(customers[0].email , \u0026#34;shiye13@foxmail.com\u0026#34;); customers[0].gender = \u0026#39;f\u0026#39;; strcpy(customers[0].name , \u0026#34;叶十三\u0026#34;); strcpy(customers[0].phone , \u0026#34;110\u0026#34;); mainMenu(); return ; } 文件操作 文件，对我们并不陌生,文件是数据源(保存数据的地方)的一种,比如大家经常使用的word文档，txt文件，excel文件\u0026hellip;都是文件。文件最\n主要的作用就是保存数据,它既可以保存一张图片,也可以保持视频,声音\u0026hellip;\n文件在程序中是以流的形式来操作\n流：数据在数据源(文件)和程序(内存)之间经历的路径\n输入流：数据从数据源(文件)到程序(内存)的路径\n输出流：数据从程序(内存)到数据源(文件)的路径\n==C 标准库 - stdio .h==该头文件定义了三个变量类型、一些宏和各种函数来执行输入和输出, 在开发过程中，可以来查询\nC 输入\u0026amp; 输出 1、当我们提到==输入==时，这意味着要向程序写入一些数据。输入可以是以文件的形式或从命令行中进行。C 语言提供了一系列内置的函\n数来读取给定的输入，并根据需要写入到程序中。\n2、当我们提到==输出==时，这意味着要在屏幕上、打印机上或任意文件中显示一些数据。C 语言提供了一系列内置的函数来输出数据到计\n算机屏幕上和保存数据到文本文件或二进制文件中\n标准文件 ==C 语言把所有的设备都当作文件。所以设备（比如显示器）被处理的方式与文件相同。==以下三个文件会在程序执行时自动打开，以便访问键盘和屏幕\n2、==文件指针是访问文件的方式==，我们会讲解如何从屏幕读取值以及如何把结果输出到屏幕上。\n3、C 语言中的 I/O (输入/输出) 通常使用printf()和 scanf() 两个函数。scanf() 函数用于从标准输入（键盘）读取并格式\n化，printf()函数发送格式化输出到标准输出（屏幕）\n案例演示, 将内容输出到屏幕\n1 2 3 4 5 6 #include \u0026lt;stdio.h\u0026gt; // 执行 printf() 函数需要该库 int main() { printf(\u0026#34;hello\u0026#34;); //显示引号中的内容 return 0; } getchar()\u0026amp;putchar() getchar()\nint getchar(void) 函数从屏幕读取下一个可用的字符，并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字\n符。您可以在循环内使用这个方法，以便从屏幕上读取多个字符。\nputchar()\nint putchar(int c) 函数把字符输出到屏幕上，并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符。\n您可以在循环内使用这个方法，以便在屏幕上输出多个字符\n测试案例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include \u0026lt;stdio.h\u0026gt; int main() {\tint c; printf(\u0026#34;Please enter a value:\u0026#34;); c = getchar(); // 读取一个char，并返回一个int printf(\u0026#34;\\nYou entered：\u0026#34;); putchar( c );// 可以在屏幕上显示 printf(\u0026#34;\\n\u0026#34;); getchar(); // 过滤回车 getchar(); // 停止界面 return 0; } gets() \u0026amp; puts() gets()\n1、char *gets(char *s) 函数从 stdin 读取一行到 s 所指向的缓冲区，直到一个终止符或 EOF。\nputs()\n2、 int puts(const char *s) 函数把字符串 s 和一个尾随的换行符写入到 stdout。\n应用案例：\n1 2 3 4 5 6 7 8 9 10 11 12 #include \u0026lt;stdio.h\u0026gt; int main( ) { char str[100]; printf( \u0026#34;Please enter a character :\u0026#34;); gets( str ); printf( \u0026#34;\\nYou entered: \u0026#34;); puts( str ); getchar(); return 0; } scanf() 和 printf() 1、int scanf(const char *format, ...) 函数从标准输入流 stdin 读取输入，并根据提供的 format 来浏览输入。\n2、 int printf(const char *format, ...) 函数把输出写入到标准输出流 stdout ，并根据提供的格式产生输出。\n3、format 可以是一个简单的常量字符串，但是您可以分别指定 %s、%d、%c、%f 等来输出或读取字符串、整数、字符或浮点数。还有许多其他可用的格式选项，可以根据需要使用。如需了解完整的细节，可以查看这些函数的参考手册。现在让我们通过下面这个简单的实例来加深理解\n应用实例：\n==您输入一个文本并按下回车键时，程序读取输入， 但是要求格式要匹配==\n1 2 3 4 5 6 7 8 9 10 11 12 #include \u0026lt;stdio.h\u0026gt; int main( ) { char str[100]; int i; printf( \u0026#34;please input a value :\u0026#34;); scanf(\u0026#34;%s %d\u0026#34;, str, \u0026amp;i); getchar(); // 过滤回车 printf( \u0026#34;\\nYou entered: %s %d \u0026#34;, str, i); printf(\u0026#34;\\n\u0026#34;); getchar(); // 界面停止 return 0; } C文件读写 1、讲解了 C 语言处理的标准输入和输出设备。我们将介绍 如何创建、打开、关闭文本文件或二进制文件。\n2、一个文件，无论它是文本文件还是二进制文件，都是代表了一系列的字节。C 语言不仅提供了访问顶层的函数，也提供了底层（OS）调用来处理存储设备上的文件。==（重要）==\n打开文件 使用fopen( )函数来创建一个新的文件或者打开一个已有的文件，这个调用会初始化类型 FILE 的一个对象，类型 FILE 包含了所\n有用来控制流的必要的信息。下面是这个函数调用的原型：\n1 FILE *fopen( const char * filename, const char * mode ); 说明：\n1、ilename 是字符串,用来命名文件\n2、访问模式 mode 的值可以是下列值中的一个：\n如果处理的是二进制文件(图片，视频..)，则需使用下面的访问模式: \u0026ldquo;rb\u0026rdquo;, \u0026ldquo;wb\u0026rdquo;, \u0026ldquo;ab\u0026rdquo;, \u0026ldquo;rb+\u0026rdquo;, \u0026ldquo;r+b\u0026rdquo;, \u0026ldquo;wb+\u0026rdquo;, \u0026ldquo;w+b\u0026rdquo;, \u0026ldquo;ab+\u0026rdquo;, \u0026ldquo;a+b\u0026rdquo; //b :binary 二进制\n关闭文件 1 int fclose( FILE *fp ); 1、 如果成功关闭文件，fclose( ) 函数返回零，如果关闭文件时发生错误，函数返回 EOF。这个函数实际上，会清空缓冲区中的数\n据，关闭文件，并释放用于该文件的所有内存。EOF 是一个定义在头文件 stdio.h 中的常量。\n2、 C 标准库提供了各种函数来按字符或者以固定长度字符串的形式读写文件。\n3、==使用完文件后(读，写)，一定要将该文件关闭==\n写入文件 下面是把字符写入到流中的函数\n1 int fputc( int c, FILE *fp ); 说明：\n函数 fputc() 把参数 c 的字符值写入到 fp 所指向的输出流中。如果写入成功，它会返回写入的字符，如果发生错误，则会返回 EOF。\n您可以使用下面的函数来把一个以 null 结尾的字符串写入到流中：\n1 int fputs( const char *s, FILE *fp ); 说明：\n函数 fputs() 把字符串 s 写入到 fp 所指向的输出流中。如果写入成功，它会返回一个非负值，如果发生错误，则会返回 EOF。您也可\n以使用 int fprintf(FILE *fp,const char *format, \u0026hellip;) 函数来写把一个字符串写入到文件中\n应用案例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 #include \u0026lt;stdio.h\u0026gt; int main( ) { FILE *fp = NULL; fp = fopen(\u0026#34;d:/a.txt\u0026#34;,\u0026#34;w+\u0026#34;); //w+覆盖原先内容，非追加 // 将内容写入到文件中 fprintf(fp,\u0026#34;你好，是叶十三\\n\u0026#34;); fputs(\u0026#34;你好，胡佳妮\\n\u0026#34;,fp); //close file ，if we not close the file,our content is not saved to the file fclose(fp); printf(\u0026#34;创建，写入信息完成\u0026#34;); getchar(); } 读取文件 下面是从文件读取单个字符的函数\n1 int fgetc( FILE * fp ); ==说明：==\nfgetc() 函数从 fp 所指向的输入文件中读取一个字符。返回值是读取的字符，如果发生错误则返回 EOF。\n下面的函数从流中读取一个字符串：\n1 char *fgets( char *buf, int n, FILE *fp ); 1、 说明：函数 fgets() 从 fp 所指向的输入流中读取 n - 1 个字符。它会把读取的字符串复制到缓冲区 buf，并在最后追加一个 null 字符来终止字符串。如果这个函数在读取最后一个字符之前就遇到一个换行符 \u0026lsquo;\\n\u0026rsquo; 或文件的末尾 EOF，则只会返回读取到的字符，包括换行符。\n2、也可以使用int fscanf(FILE *fp, const char *format, ...)函数来从文件中读取字符串，但是在遇到第一个空格字符时，它会停止读取\n应用案例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include \u0026lt;stdio.h\u0026gt; int main( ) { // 创建一个文件指针 FILE *fp = NULL; // 定义一个缓冲区 char buff[1024]; // 打开文件 fp = fopen(\u0026#34;d:/a.txt\u0026#34;,\u0026#34;r\u0026#34;); // 方法1 ：从fp所指向的文件读取一行到buff中 //fscanf(fp,\u0026#34;%s\u0026#34;,buff); //printf(\u0026#34;%s\\n\u0026#34;,buff); //getchar(); // 方法2 ：从fp所指向的文件读取整个文件到buff中 /* 说明：循环读取fp指向的文件内容，如果读取NULL，就结束 */ while(fgets(buff,1024,fp)!=NULL){ printf(\u0026#34;%s\u0026#34;,buff); } // closed file fclose(fp); getchar(); } 参考 C语言中文网\n","permalink":"https://ktzxy.top/posts/24qrt1w3je/","summary":"学习C语言的笔记","title":"C语言学习"},{"content":"通讯录管理系统 1、系统需求 通讯录是一个可以记录亲人、好友信息的工具。\n本教程主要利用C++来实现一个通讯录管理系统\n系统中需要实现的功能如下：\n添加联系人：向通讯录中添加新人，信息包括（姓名、性别、年龄、联系电话、家庭住址）最多记录1000人 显示联系人：显示通讯录中所有联系人信息 删除联系人：按照姓名进行删除指定联系人 查找联系人：按照姓名查看指定联系人信息 修改联系人：按照姓名重新修改指定联系人 清空联系人：清空通讯录中所有信息 退出通讯录：退出当前使用的通讯录 2、创建项目 创建项目步骤如下：\n创建新项目 添加文件 3、菜单功能 功能描述： 用户选择功能的界面\n菜单界面效果如下图：\n步骤：\n封装函数显示该界面 如 void showMenu() 在main函数中调用封装好的函数 代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include\u0026lt;iostream\u0026gt; using namespace std; //1.打印菜单 void showMenu() { cout \u0026lt;\u0026lt; \u0026#34;***************************\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;***** 1、添加联系人 *****\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;***** 2、显示联系人 *****\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;***** 3、删除联系人 *****\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;***** 4、查找联系人 *****\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;***** 5、修改联系人 *****\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;***** 6、清空联系人 *****\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;***** 0、退出通讯录 *****\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;***************************\u0026#34; \u0026lt;\u0026lt; endl; } int main() { showMenu(); system(\u0026#34;pause\u0026#34;); return 0; } 4、退出功能 功能描述：退出通讯录系统\n思路：根据用户不同的选择，进入不同的功能，可以选择switch分支结构，将整个架构进行搭建\n当用户选择0时候，执行退出，选择其他先不做操作，也不会退出程序\n代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 int main() { int select = 0; while (true) { showMenu(); cin \u0026gt;\u0026gt; select; switch (select) { case 1: //添加联系人 break; case 2: //显示联系人 break; case 3: //删除联系人 break; case 4: //查找联系人 break; case 5: //修改联系人 break; case 6: //清空联系人 break; case 0: //退出通讯录 cout \u0026lt;\u0026lt; \u0026#34;欢迎下次使用\u0026#34; \u0026lt;\u0026lt; endl; system(\u0026#34;pause\u0026#34;); return 0; break; default: break; } } system(\u0026#34;pause\u0026#34;); return 0; } 5、添加联系人 功能描述：\n实现添加联系人功能，联系人上限为1000人，联系人信息包括（姓名、性别、年龄、联系电话、家庭住址）\n添加联系人实现步骤：\n设计联系人结构体 设计通讯录结构体 main函数中创建通讯录 封装添加联系人函数 测试添加联系人功能 5.1 设计联系人结构体 联系人信息包括：姓名、性别、年龄、联系电话、家庭住址\n设计如下：\n1 2 3 4 5 6 7 8 9 10 #include \u0026lt;string\u0026gt; //string头文件 //联系人结构体 struct Person { string m_Name; //姓名 int m_Sex; //性别：1男 2女 int m_Age; //年龄 string m_Phone; //电话 string m_Addr; //住址 }; 5.2 设计通讯录结构体 设计时候可以在通讯录结构体中，维护一个容量为1000的存放联系人的数组，并记录当前通讯录中联系人数量\n设计如下\n1 2 3 4 5 6 7 8 #define MAX 1000 //最大人数 //通讯录结构体 struct Addressbooks { struct Person personArray[MAX]; //通讯录中保存的联系人数组 int m_Size; //通讯录中人员个数 }; 5.3 main函数中创建通讯录 添加联系人函数封装好后，在main函数中创建一个通讯录变量，这个就是我们需要一直维护的通讯录\n1 2 3 4 5 6 mian函数起始位置添加： //创建通讯录 Addressbooks abs; //初始化通讯录中人数 abs.m_Size = 0; 5.4 封装添加联系人函数 思路：添加联系人前先判断通讯录是否已满，如果满了就不再添加，未满情况将新联系人信息逐个加入到通讯录\n添加联系人代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 //1、添加联系人信息 void addPerson(Addressbooks *abs) { //判断电话本是否满了 if (abs-\u0026gt;m_Size == MAX) { cout \u0026lt;\u0026lt; \u0026#34;通讯录已满，无法添加\u0026#34; \u0026lt;\u0026lt; endl; return; } else { //姓名 string name; cout \u0026lt;\u0026lt; \u0026#34;请输入姓名：\u0026#34; \u0026lt;\u0026lt; endl; cin \u0026gt;\u0026gt; name; abs-\u0026gt;personArray[abs-\u0026gt;m_Size].m_Name = name; cout \u0026lt;\u0026lt; \u0026#34;请输入性别：\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;1 -- 男\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;2 -- 女\u0026#34; \u0026lt;\u0026lt; endl; //性别 int sex = 0; while (true) { cin \u0026gt;\u0026gt; sex; if (sex == 1 || sex == 2) { abs-\u0026gt;personArray[abs-\u0026gt;m_Size].m_Sex = sex; break; } cout \u0026lt;\u0026lt; \u0026#34;输入有误，请重新输入\u0026#34;; } //年龄 cout \u0026lt;\u0026lt; \u0026#34;请输入年龄：\u0026#34; \u0026lt;\u0026lt; endl; int age = 0; cin \u0026gt;\u0026gt; age; abs-\u0026gt;personArray[abs-\u0026gt;m_Size].m_Age = age; //联系电话 cout \u0026lt;\u0026lt; \u0026#34;请输入联系电话：\u0026#34; \u0026lt;\u0026lt; endl; string phone; cin \u0026gt;\u0026gt; phone; abs-\u0026gt;personArray[abs-\u0026gt;m_Size].m_Phone = phone; //家庭住址 cout \u0026lt;\u0026lt; \u0026#34;请输入家庭住址：\u0026#34; \u0026lt;\u0026lt; endl; string address; cin \u0026gt;\u0026gt; address; abs-\u0026gt;personArray[abs-\u0026gt;m_Size].m_Addr = address; //更新通讯录人数 abs-\u0026gt;m_Size++; cout \u0026lt;\u0026lt; \u0026#34;添加成功\u0026#34; \u0026lt;\u0026lt; endl; system(\u0026#34;pause\u0026#34;); system(\u0026#34;cls\u0026#34;); } } 5.5 测试添加联系人功能 选择界面中，如果玩家选择了1，代表添加联系人，我们可以测试下该功能\n在switch case 语句中，case1里添加：\n1 2 3 case 1: //添加联系人 addPerson(\u0026amp;abs); break; 6、显示联系人 功能描述：显示通讯录中已有的联系人信息\n显示联系人实现步骤：\n封装显示联系人函数 测试显示联系人功能 6.1 封装显示联系人函数 思路：判断如果当前通讯录中没有人员，就提示记录为空，人数大于0，显示通讯录中信息\n显示联系人代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 //2、显示所有联系人信息 void showPerson(Addressbooks * abs) { if (abs-\u0026gt;m_Size == 0) { cout \u0026lt;\u0026lt; \u0026#34;当前记录为空\u0026#34; \u0026lt;\u0026lt; endl; } else { for (int i = 0; i \u0026lt; abs-\u0026gt;m_Size; i++) { cout \u0026lt;\u0026lt; \u0026#34;姓名：\u0026#34; \u0026lt;\u0026lt; abs-\u0026gt;personArray[i].m_Name \u0026lt;\u0026lt; \u0026#34;\\t\u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;性别：\u0026#34; \u0026lt;\u0026lt; (abs-\u0026gt;personArray[i].m_Sex == 1 ? \u0026#34;男\u0026#34; : \u0026#34;女\u0026#34;) \u0026lt;\u0026lt; \u0026#34;\\t\u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;年龄：\u0026#34; \u0026lt;\u0026lt; abs-\u0026gt;personArray[i].m_Age \u0026lt;\u0026lt; \u0026#34;\\t\u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;电话：\u0026#34; \u0026lt;\u0026lt; abs-\u0026gt;personArray[i].m_Phone \u0026lt;\u0026lt; \u0026#34;\\t\u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;住址：\u0026#34; \u0026lt;\u0026lt; abs-\u0026gt;personArray[i].m_Addr \u0026lt;\u0026lt; endl; } } system(\u0026#34;pause\u0026#34;); system(\u0026#34;cls\u0026#34;); } 6.2 测试显示联系人功能 在switch case语句中，case 2 里添加\n1 2 3 case 2: //显示联系人 showPerson(\u0026amp;abs); break; 7、删除联系人 功能描述：按照姓名进行删除指定联系人\n删除联系人实现步骤：\n封装检测联系人是否存在 封装删除联系人函数 测试删除联系人功能 7.1 封装检测联系人是否存在 设计思路：\n删除联系人前，我们需要先判断用户输入的联系人是否存在，如果存在删除，不存在提示用户没有要删除的联系人\n因此我们可以把检测联系人是否存在封装成一个函数中，如果存在，返回联系人在通讯录中的位置，不存在返回-1\n检测联系人是否存在代码：\n1 2 3 4 5 6 7 8 9 10 11 12 //判断是否存在查询的人员，存在返回在数组中索引位置，不存在返回-1 int isExist(Addressbooks * abs, string name) { for (int i = 0; i \u0026lt; abs-\u0026gt;m_Size; i++) { if (abs-\u0026gt;personArray[i].m_Name == name) { return i; } } return -1; } 7.2 封装删除联系人函数 根据用户输入的联系人判断该通讯录中是否有此人\n查找到进行删除，并提示删除成功\n查不到提示查无此人。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 //3、删除指定联系人信息 void deletePerson(Addressbooks * abs) { cout \u0026lt;\u0026lt; \u0026#34;请输入您要删除的联系人\u0026#34; \u0026lt;\u0026lt; endl; string name; cin \u0026gt;\u0026gt; name; int ret = isExist(abs, name); if (ret != -1) { for (int i = ret; i \u0026lt; abs-\u0026gt;m_Size; i++) { abs-\u0026gt;personArray[i] = abs-\u0026gt;personArray[i + 1]; } abs-\u0026gt;m_Size--; cout \u0026lt;\u0026lt; \u0026#34;删除成功\u0026#34; \u0026lt;\u0026lt; endl; } else { cout \u0026lt;\u0026lt; \u0026#34;查无此人\u0026#34; \u0026lt;\u0026lt; endl; } system(\u0026#34;pause\u0026#34;); system(\u0026#34;cls\u0026#34;); } 7.3 测试删除联系人功能 在switch case 语句中，case3里添加：\n1 2 3 case 3: //删除联系人 deletePerson(\u0026amp;abs); break; 8、查找联系人 功能描述：按照姓名查看指定联系人信息\n查找联系人实现步骤\n封装查找联系人函数 测试查找指定联系人 8.1 封装查找联系人函数 实现思路：判断用户指定的联系人是否存在，如果存在显示信息，不存在则提示查无此人。\n查找联系人代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 //4、查找指定联系人信息 void findPerson(Addressbooks * abs) { cout \u0026lt;\u0026lt; \u0026#34;请输入您要查找的联系人\u0026#34; \u0026lt;\u0026lt; endl; string name; cin \u0026gt;\u0026gt; name; int ret = isExist(abs, name); if (ret != -1) { cout \u0026lt;\u0026lt; \u0026#34;姓名：\u0026#34; \u0026lt;\u0026lt; abs-\u0026gt;personArray[ret].m_Name \u0026lt;\u0026lt; \u0026#34;\\t\u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;性别：\u0026#34; \u0026lt;\u0026lt; abs-\u0026gt;personArray[ret].m_Sex \u0026lt;\u0026lt; \u0026#34;\\t\u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;年龄：\u0026#34; \u0026lt;\u0026lt; abs-\u0026gt;personArray[ret].m_Age \u0026lt;\u0026lt; \u0026#34;\\t\u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;电话：\u0026#34; \u0026lt;\u0026lt; abs-\u0026gt;personArray[ret].m_Phone \u0026lt;\u0026lt; \u0026#34;\\t\u0026#34;; cout \u0026lt;\u0026lt; \u0026#34;住址：\u0026#34; \u0026lt;\u0026lt; abs-\u0026gt;personArray[ret].m_Addr \u0026lt;\u0026lt; endl; } else { cout \u0026lt;\u0026lt; \u0026#34;查无此人\u0026#34; \u0026lt;\u0026lt; endl; } system(\u0026#34;pause\u0026#34;); system(\u0026#34;cls\u0026#34;); } 8.2 测试查找指定联系人 在switch case 语句中，case4里添加：\n1 2 3 case 4: //查找联系人 findPerson(\u0026amp;abs); break; 9、修改联系人 功能描述：按照姓名重新修改指定联系人\n修改联系人实现步骤\n封装修改联系人函数 测试修改联系人功能 9.1 封装修改联系人函数 实现思路：查找用户输入的联系人，如果查找成功进行修改操作，查找失败提示查无此人\n修改联系人代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 //5、修改指定联系人信息 void modifyPerson(Addressbooks * abs) { cout \u0026lt;\u0026lt; \u0026#34;请输入您要修改的联系人\u0026#34; \u0026lt;\u0026lt; endl; string name; cin \u0026gt;\u0026gt; name; int ret = isExist(abs, name); if (ret != -1) { //姓名 string name; cout \u0026lt;\u0026lt; \u0026#34;请输入姓名：\u0026#34; \u0026lt;\u0026lt; endl; cin \u0026gt;\u0026gt; name; abs-\u0026gt;personArray[ret].m_Name = name; cout \u0026lt;\u0026lt; \u0026#34;请输入性别：\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;1 -- 男\u0026#34; \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; \u0026#34;2 -- 女\u0026#34; \u0026lt;\u0026lt; endl; //性别 int sex = 0; while (true) { cin \u0026gt;\u0026gt; sex; if (sex == 1 || sex == 2) { abs-\u0026gt;personArray[ret].m_Sex = sex; break; } cout \u0026lt;\u0026lt; \u0026#34;输入有误，请重新输入\u0026#34;; } //年龄 cout \u0026lt;\u0026lt; \u0026#34;请输入年龄：\u0026#34; \u0026lt;\u0026lt; endl; int age = 0; cin \u0026gt;\u0026gt; age; abs-\u0026gt;personArray[ret].m_Age = age; //联系电话 cout \u0026lt;\u0026lt; \u0026#34;请输入联系电话：\u0026#34; \u0026lt;\u0026lt; endl; string phone = \u0026#34;\u0026#34;; cin \u0026gt;\u0026gt; phone; abs-\u0026gt;personArray[ret].m_Phone = phone; //家庭住址 cout \u0026lt;\u0026lt; \u0026#34;请输入家庭住址：\u0026#34; \u0026lt;\u0026lt; endl; string address; cin \u0026gt;\u0026gt; address; abs-\u0026gt;personArray[ret].m_Addr = address; cout \u0026lt;\u0026lt; \u0026#34;修改成功\u0026#34; \u0026lt;\u0026lt; endl; } else { cout \u0026lt;\u0026lt; \u0026#34;查无此人\u0026#34; \u0026lt;\u0026lt; endl; } system(\u0026#34;pause\u0026#34;); system(\u0026#34;cls\u0026#34;); } 9.2 测试修改联系人功能 在switch case 语句中，case 5里添加：\n1 2 3 case 5: //修改联系人 modifyPerson(\u0026amp;abs); break; 10、清空联系人 功能描述：清空通讯录中所有信息\n清空联系人实现步骤\n封装清空联系人函数 测试清空联系人 10.1 封装清空联系人函数 实现思路： 将通讯录所有联系人信息清除掉，只要将通讯录记录的联系人数量置为0，做逻辑清空即可。\n清空联系人代码：\n1 2 3 4 5 6 7 8 //6、清空所有联系人 void cleanPerson(Addressbooks * abs) { abs-\u0026gt;m_Size = 0; cout \u0026lt;\u0026lt; \u0026#34;通讯录已清空\u0026#34; \u0026lt;\u0026lt; endl; system(\u0026#34;pause\u0026#34;); system(\u0026#34;cls\u0026#34;); } 10.2 测试清空联系人 在switch case 语句中，case 6 里添加：\n1 2 3 case 6: //清空联系人 cleanPerson(\u0026amp;abs); break; 至此，通讯录管理系统完成！\n","permalink":"https://ktzxy.top/posts/n3588i3me7/","summary":"学习C++语言做项目案例","title":"通讯录管理系统"},{"content":"MyBatis笔记 1、什么是MyBatis 1.1 环境说明 jdk 8 +\nMySQL 5.7.19\nmaven-3.6.1\nIDEA\n1.2 学习前需要掌握 JDBC MySQL Java 基础 Maven Junit 1.3 什么是MyBatis MyBatis 是一款优秀的持久层框架 MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集的过程 MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息，将接口和 Java 的 实体类 【Plain Old Java Objects,普通的 Java对象】映射成数据库中的记录。 MyBatis 本是apache的一个开源项目ibatis, 2010年这个项目由apache 迁移到了google code，并且改名为MyBatis 。 2013年11月迁移到Github . Mybatis官方文档 : http://www.mybatis.org/mybatis-3/zh/index.html GitHub : https://github.com/mybatis/mybatis-3 1.4 持久化 持久化是将程序数据在持久状态和瞬时状态间转换的机制。\n即把数据（如内存中的对象）保存到可永久保存的存储设备中（如磁盘）。持久化的主要应用是将内存中的对象存储在数据库中，或者存储在磁盘文件中、XML数据文件中等等。 JDBC就是一种持久化机制。文件IO也是一种持久化机制。 在生活中 : 将鲜肉冷藏，吃的时候再解冻的方法也是。将水果做成罐头的方法也是。 为什么需要持久化服务呢？那是由于内存本身的缺陷引起的\n内存断电后数据会丢失，但有一些对象是无论如何都不能丢失的，比如银行账号等，遗憾的是，人们还无法保证内存永不掉电。 内存过于昂贵，与硬盘、光盘等外存相比，内存的价格要高2~3个数量级，而且维持成本也高，至少需要一直供电吧。所以即使对象不需要永久保存，也会因为内存的容量限制不能一直呆在内存中，需要持久化来缓存到外存。 1.5 持久层 什么是持久层？\n完成持久化工作的代码块 . \u0026mdash;-\u0026gt; dao层 【DAO (Data Access Object) 数据访问对象】\n大多数情况下特别是企业级应用，数据持久化往往也就意味着将内存中的数据保存到磁盘上加以固化，而持久化的实现过程则大多通过各种关系数据库来完成。\n不过这里有一个字需要特别强调，也就是所谓的“层”。对于应用系统而言，数据持久功能大多是必不可少的组成部分。也就是说，我们的系统中，已经天然的具备了“持久层”概念？也许是，但也许实际情况并非如此。之所以要独立出一个“持久层”的概念,而不是“持久模块”，“持久单元”，也就意味着，我们的系统架构中，应该有一个相对独立的逻辑层面，专注于数据持久化逻辑的实现.\n与系统其他部分相对而言，这个层面应该具有一个较为清晰和严格的逻辑边界。【说白了就是用来操作数据库存在的！】\n1.6 为什么需要Mybatis Mybatis就是帮助程序猿将数据存入数据库中 , 和从数据库中取数据 .\n传统的jdbc操作 , 有很多重复代码块 .比如 : 数据取出时的封装 , 数据库的建立连接等等\u0026hellip; , 通过框架可以减少重复代码,提高开发效率 .\nMyBatis 是一个半自动化的ORM框架 (Object Relationship Mapping) \u0026ndash;\u0026gt;对象关系映射\n所有的事情，不用Mybatis依旧可以做到，只是用了它，所有实现会更加简单！技术没有高低之分，只有使用这个技术的人有高低之别\nMyBatis的优点\n简单易学：本身就很小且简单。没有任何第三方依赖，最简单安装只要两个jar文件+配置几个sql映射文件就可以了，易于学习，易于使用，通过文档和源代码，可以比较完全的掌握它的设计思路和实现。 灵活：mybatis不会对应用程序或者数据库的现有设计强加任何影响。sql写在xml里，便于统一管理和优化。通过sql语句可以满足操作数据库的所有需求。 解除sql与程序代码的耦合：通过提供DAO层，将业务逻辑和数据访问逻辑分离，使系统的设计更清晰，更易维护，更易单元测试。sql和代码的分离，提高了可维护性。 提供xml标签，支持编写动态sql。 \u0026hellip;\u0026hellip;. 最重要的一点，使用的人多！公司需要！\n2、第一个MyBatis程序 思路流程：搭建环境\u0026ndash;\u0026gt;导入Mybatis\u0026mdash;\u0026gt;编写代码\u0026mdash;\u0026gt;测试\n2.1 搭建实验数据库 2.2 导入MyBatis相关 jar 包 GitHub上找 1 2 3 4 5 6 7 8 9 10 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.2\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.1.47\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 2.3 编写MyBatis核心配置文件 编写MyBatis核心配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE configuration PUBLIC \u0026#34;-//mybatis.org//DTD Config 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-config.dtd\u0026#34;\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;environments default=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;environment id=\u0026#34;development\u0026#34;\u0026gt; \u0026lt;transactionManager type=\u0026#34;JDBC\u0026#34;/\u0026gt; \u0026lt;dataSource type=\u0026#34;POOLED\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driver\u0026#34; value=\u0026#34;com.mysql.jdbc.Driver\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;jdbc:mysql://localhost:3306/mybatis?useSSL=true\u0026amp;amp;useUnicode=true\u0026amp;amp;characterEncoding=utf8\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;root\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;root\u0026#34;/\u0026gt; \u0026lt;/dataSource\u0026gt; \u0026lt;/environment\u0026gt; \u0026lt;/environments\u0026gt; \u0026lt;mappers\u0026gt; \u0026lt;mapper resource=\u0026#34;com/stuy/dao/userMapper.xml\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; \u0026lt;/configuration\u0026gt; 2.4 编写MyBatis工具类 查看帮助文档 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; public class MybatisUtils { private static SqlSessionFactory sqlSessionFactory; static { try { String resource = \u0026#34;mybatis-config.xml\u0026#34;; InputStream inputStream = Resources.getResourceAsStream(resource); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } catch (IOException e) { e.printStackTrace(); } } //获取SqlSession连接 public static SqlSession getSession(){ return sqlSessionFactory.openSession(); } } 2.5 创建实体类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.study.entity; import java.io.Serializable; public class User implements Serializable { private Integer id; private String username; private String password; public User() { } public User(Integer id, String username, String password) { this.id = id; this.username = username; this.password = password; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return \u0026#34;User{\u0026#34; + \u0026#34;id=\u0026#34; + id + \u0026#34;, username=\u0026#39;\u0026#34; + username + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, password=\u0026#39;\u0026#34; + password + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } 2.6 编写mapper接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.study.dao; import com.study.entity.User; import java.util.List; import java.util.Map; /** * @author huang */ //@Mapper public interface UserMapper { //@Select(\u0026#34;select * from t_user\u0026#34;) List\u0026lt;User\u0026gt; getUserList(); //模糊查询 List\u0026lt;User\u0026gt; getUserLike(String value); } 2.7 编写Mapper.xml配置文件 1 2 3 4 5 6 7 8 9 10 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;http://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;!--namespace 绑定一个对应的Dao/Mapper接口--\u0026gt; \u0026lt;mapper namespace=\u0026#34;com.study.dao.UserMapper\u0026#34;\u0026gt; \u0026lt;!--select 查询语句--\u0026gt; \u0026lt;select id=\u0026#34;getUserList\u0026#34; resultType=\u0026#34;com.study.entity.User\u0026#34;\u0026gt; select * from t_user \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; 2.8 编写测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public static void test(){ SqlSession sqlSession = null; try { //1.获取SqlSession对象 sqlSession = MybatisUtils.getSqlSession(); //2.获取mapper对象 UserMapper mapper = sqlSession.getMapper(UserMapper.class); List\u0026lt;User\u0026gt; userList = mapper.getUserList(); //不推荐使用 //List\u0026lt;User\u0026gt; userList2 = sqlSession.selectList(\u0026#34;com.study.dao.UserMapper.getUserList\u0026#34;); for (User user : userList) { System.out.println(user); } } catch (Exception e) { e.printStackTrace(); }finally { //3.关闭资源sqlSession sqlSession.close(); } } 2.9 问题说明 可能出现问题说明：Maven静态资源过滤问题（xml配置文件不在resoures目录下的问题，会使xml文件访问不到）\n在pom.xml文件加上(内容包裹在build标签下)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;build\u0026gt; \u0026lt;resources\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;directory\u0026gt;src/main/java\u0026lt;/directory\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;include\u0026gt;**/*.properties\u0026lt;/include\u0026gt; \u0026lt;include\u0026gt;**/*.xml\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;filtering\u0026gt;false\u0026lt;/filtering\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;resource\u0026gt; \u0026lt;directory\u0026gt;src/main/resources\u0026lt;/directory\u0026gt; \u0026lt;includes\u0026gt; \u0026lt;include\u0026gt;**/*.properties\u0026lt;/include\u0026gt; \u0026lt;include\u0026gt;**/*.xml\u0026lt;/include\u0026gt; \u0026lt;/includes\u0026gt; \u0026lt;filtering\u0026gt;false\u0026lt;/filtering\u0026gt; \u0026lt;/resource\u0026gt; \u0026lt;/resources\u0026gt; \u0026lt;/build\u0026gt; 3、CRUD（增删改查） 3.1 namespace namespace中的包名要和mapper接口中的报名一致！\n3.2 select 选择、查询语句\nid: 就是对应 namespace中的方法名；\nresultType ：sql 语句执行的返回值\nparameterType ： 参数类型\n1.编写接口\n1 2 3 4 5 6 7 8 //根据id查询用户 User getUserById(Integer id); //添加用户 int addUser(User user); //更改用户信息 int updateUser(User user); //删除用户 int deleteUser(Integer integer); 2.编写mapper中的SQL语句\n1 2 3 \u0026lt;select id=\u0026#34;getUserById\u0026#34; resultType=\u0026#34;com.study.entity.User\u0026#34; parameterType=\u0026#34;integer\u0026#34;\u0026gt; select * from t_user where id= #{id} \u0026lt;/select\u0026gt; 3.测试\n1 2 3 4 5 6 7 public static void testSelect(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); User user = mapper.getUserById(1); System.out.println(user); sqlSession.close(); } 3.3 insert 1 2 3 \u0026lt;insert id=\u0026#34;addUser\u0026#34; parameterType=\u0026#34;com.study.entity.User\u0026#34; \u0026gt; insert into t_user (id,username,password) values (#{id},#{username},#{password}); \u0026lt;/insert\u0026gt; 3.4 update 1 2 3 \u0026lt;update id=\u0026#34;updateUser\u0026#34; parameterType=\u0026#34;com.study.entity.User\u0026#34;\u0026gt; update t_user set username=#{username},password=#{password} where id=#{id}; \u0026lt;/update\u0026gt; 3.5 delete 1 2 3 \u0026lt;delete id=\u0026#34;deleteUser\u0026#34; parameterType=\u0026#34;integer\u0026#34;\u0026gt; delete from t_user where id = #{id} \u0026lt;/delete\u0026gt; 注意点：\n增删改需要提交事务！ 3.6 分析错误 标签不要匹配错误\nresource 绑定 mapper的时候需要使用路径 /而不是.\n1 2 3 \u0026lt;mappers\u0026gt; \u0026lt;mapper resource=\u0026#34;com/study/dao/UserMapper.xml\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; 程序配置文件必须符合规范\nmaven资源没有导出的问题\n3.7 万能的Map 假设我们的实体类，或者是数据库中的表，字段过多，我们应当考虑使用Map！\n1 2 //使用map来控制参数 int addUser2(Map map); 1 2 3 \u0026lt;insert id=\u0026#34;addUser2\u0026#34; parameterType=\u0026#34;map\u0026#34;\u0026gt; insert into t_user (id,username,password) values (#{id},#{name},#{pwd}); \u0026lt;/insert\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void addUser2(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); Map\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;id\u0026#34;,7); map.put(\u0026#34;name\u0026#34;,\u0026#34;clearLove7\u0026#34;); map.put(\u0026#34;pwd\u0026#34;,\u0026#34;7777777\u0026#34;); int res = mapper.addUser2(map); if (res \u0026gt; 0) { System.out.println(\u0026#34;增加成功\u0026#34;); sqlSession.commit(); System.out.println(\u0026#34;事务已经提交\u0026#34;); } else { System.out.println(\u0026#34;增加失败，事务回滚\u0026#34;); sqlSession.rollback(); } sqlSession.close(); } Map传递参数，直接在sql中取出key即可！ 【parameterType=\u0026ldquo;map\u0026rdquo;】\n对象传递参数，直接在sql中取对象的属性即可！【parameterType=\u0026ldquo;com.study.entity.User\u0026rdquo;】\n只有一个基本数据类型参数的情况下，可以直接在sql取到！ 可以不写\n多个参数用Map，或者注解\n3.8 模糊查询 在java代码执行的时候，传递通配符% %\n1 List\u0026lt;User\u0026gt; userList = mapper.getUserLike(\u0026#34;%李%\u0026#34;); 1 2 3 \u0026lt;select id=\u0026#34;getUserLike\u0026#34; resultType=\u0026#34;com.study.entity.User\u0026#34;\u0026gt; select * from t_user where username like #{value} \u0026lt;/select\u0026gt; 在sql拼接中使用通配符\n1 List\u0026lt;User\u0026gt; userList = mapper.getUserLike(\u0026#34;李\u0026#34;); 1 2 3 \u0026lt;select id=\u0026#34;getUserLike\u0026#34; resultType=\u0026#34;com.study.entity.User\u0026#34;\u0026gt; select * from t_user where username like \u0026#34;%\u0026#34;#{value}\u0026#34;%\u0026#34; \u0026lt;/select\u0026gt; 4、配置解析 4.1 核心配置文件 mybatis-config.xml\n1 2 3 4 5 6 7 8 9 10 11 12 13 • configuration（配置） • properties（属性） • settings（设置） •typeAliases（类型别名） • typeHandlers（类型处理器） • objectFactory（对象工厂） • plugins（插件） • environments（环境配置） ▪ environment（环境变量） ▪ transactionManager（事务管理器） ▪ dataSource（数据源） • databaseIdProvider（数据库厂商标识） • mappers（映射器） 4.2 环境配置（environments） Mybatis可以配置成适应多种环境\n不过要记住：尽管可以配置多种环境，但每个SqlSessionFactory实例只能选择一种环境\n学会使用配置多套运行环境！\nMyBatis默认的事务管理器就是JDBC ,连接池：POOLED\n4.3 属性（properties） 我们可以通过properties属性来实现引用配置文件\n这些属性可以在外部进行配置，并可以进行动态替换。你既可以在典型的 Java 属性文件中配置这些属性，也可以在 properties 元素的子元素中设置。【db.properties】【log4j.properties】\n编写一个配置文件db.properties\n1 2 3 4 driver=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/mybatis username=root password=root 在核心配置文件中引入\n1 2 \u0026lt;!--引入外部配置文件--\u0026gt; \u0026lt;properties resource=\u0026#34;db.properties\u0026#34;/\u0026gt; 注意点：\n可以直接引入外部配置文件 可以在其中增加一些属性配置 如果两个文件有相同字段，优先使用外部配置文件的！ 4.4 类型别名（typeAliases） 类型别名可为 Java 类型设置一个缩写名字。\n它仅用于 XML 配置，意在降低冗余的全限定类名书写 比如：\n1 2 3 4 \u0026lt;!--给实体类起别名--\u0026gt; \u0026lt;typeAliases\u0026gt; \u0026lt;typeAlias type=\u0026#34;com.study.entity.User\u0026#34; alias=\u0026#34;User\u0026#34;/\u0026gt; \u0026lt;/typeAliases\u0026gt; 也可以指定一个包名，MyBatis 会在包名下面搜索需要的 Java Bean 比如：\n扫描实体类的包，它的默认别名就是这个类的 类名，首字母小写！\n1 2 3 4 \u0026lt;!--扫描指定的包--\u0026gt; \u0026lt;typeAliases\u0026gt; \u0026lt;package name=\u0026#34;com.study.entity\u0026#34;/\u0026gt; \u0026lt;/typeAliases\u0026gt; 在实体类比较少的时候，使用第一种方式。\n如果实体类比较多，建议使用第二种。\n第一种别名可以DIY，第二种不行，如果非要改，需要在实体类中写上注解\n1 2 @Alias(\u0026#34;user\u0026#34;) public class User{} 4.5 设置（settings） 缓存开启关闭 懒加载 日志实现 4.6 映射器（mappers） MapperRegistry：注册绑定我们的Mapper文件；\n方式一：【推荐使用】\n1 2 3 4 \u0026lt;!-- 使用相对于类路径的资源引用 --\u0026gt; \u0026lt;mappers\u0026gt; \u0026lt;mapper resource=\u0026#34;com/study/dao/UserMapper.xml\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; 方式二：\n1 2 3 4 \u0026lt;!-- 使用映射器接口实现类的完全限定类名 --\u0026gt; \u0026lt;mappers\u0026gt; \u0026lt;mapper class=\u0026#34;com.study.dao.UserMapper\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; 注意点：\n接口和它的Mapper配置文件必须在同一个包下！ 接口和它的Mapper配置文件必须同名！ 方式三：\n1 2 3 4 \u0026lt;!-- 将包内的映射器接口实现全部注册为映射器 --\u0026gt; \u0026lt;mappers\u0026gt; \u0026lt;package name=\u0026#34;com.study.dao\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; 注意点：\n接口和它的Mapper配置文件必须在同一个包下！ 接口和它的Mapper配置文件必须同名！ 常见错误：\norg.apache.ibatis.binding.BindingException: Type interface com.study.dao.UserMapper is not known to the MapperRegistry.\n说明你的mapper配置文件没有在核心配置文件中绑定\n4.7作用域（Scope）和生命周期 作用域和生命周期是至关重要的，因为错误的使用会导致非常严重的并发问题。\nSqlSessionFactoryBuilder\n一旦创建了 SqlSessionFactory，就不再需要它了 局部变量 SqlSessionFactory\n说白了就是可以想象成： 数据库连接池 SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在，没有任何理由丢弃它或重新创建另一个实例 因此SqlSessionFactory 的最佳作用域是应用作用域 最简单的就是使用单例模式或者静态单例模式 SqlSession\nSqlSession的实例不是线程安全的，因此是不能被共享的，所以它的最佳的作用域是请求或方法作用域 连接到连接池的一个请求！ 用完之后需要赶紧关闭，否则资源被占用！ 这里的每一个mapper都代表一个业务！\n5、ResultMap（解决属性名和字段名不一致的问题） 5.1 测试问题： 1、 数据库的字段名\n2、Java中的实体类\n1 2 3 4 5 6 7 8 9 10 public class User { private int id; //id private String name; //姓名 private String password; //密码和数据库不一样！ //构造 //set/get //toString() } 3、接口\n1 2 //根据id查询用户 User selectUserById(int id); 4、mapper映射文件\n1 2 3 \u0026lt;select id=\u0026#34;selectUserById\u0026#34; resultType=\u0026#34;user\u0026#34;\u0026gt; select * from user where id = #{id} \u0026lt;/select\u0026gt; 5、测试\n1 2 3 4 5 6 7 8 @Test public void testSelectUserById() { SqlSession session = MybatisUtils.getSession(); //获取SqlSession连接 UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.selectUserById(1); System.out.println(user); session.close(); } 结果：\nUser{id=1, name=\u0026lsquo;hhwww\u0026rsquo;, password=\u0026lsquo;null\u0026rsquo;} 查询出来发现 password 为空 . 说明出现了问题！ 分析：\nselect * from user where id = #{id} 可以看做 select id,name,pwd from user where id = #{id} mybatis会根据这些查询的列名(会将列名转化为小写,数据库不区分大小写) , 去对应的实体类中查找相应列名的set方法设值 , 由于找不到setPwd() , 所以password返回null ; 【自动映射】 5.2 解决问题 方案一：为列名指定别名 , 别名和java实体类的属性名一致\n1 2 3 \u0026lt;select id=\u0026#34;selectUserById\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; select id , name , pwd as password from user where id = #{id} \u0026lt;/select\u0026gt; 方案二：使用结果集映射-\u0026gt;ResultMap 【推荐】\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;!-- id就是resultMap的名字 type是要映射的类型 --\u0026gt; \u0026lt;resultMap id=\u0026#34;UserMap\u0026#34; type=\u0026#34;User\u0026#34;\u0026gt; \u0026lt;!-- id为主键 --\u0026gt; \u0026lt;id column=\u0026#34;id\u0026#34; property=\u0026#34;id\u0026#34;/\u0026gt; \u0026lt;!-- column是数据库表的列名 , property是对应实体类的属性名 --\u0026gt; \u0026lt;result column=\u0026#34;name\u0026#34; property=\u0026#34;name\u0026#34;/\u0026gt; \u0026lt;result column=\u0026#34;pwd\u0026#34; property=\u0026#34;password\u0026#34;/\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;select id=\u0026#34;selectUserById\u0026#34; resultMap=\u0026#34;UserMap\u0026#34;\u0026gt; select id , name , pwd from user where id = #{id} \u0026lt;/select\u0026gt; 5.3 resultMap 结果集映射\n1 2 id\tname\tpwd (数据的) id\tname\tpassword (实体类的) resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来。\nresultMap 的设计思想是，对于简单的语句根本不需要配置显式的结果映射，而对于复杂一点的语句只需要描述它们的关系就行了\nresultMap 最优秀的地方在于，虽然你已经对它相当了解了，但是根本就不需要显式地用到他们。(什么不一样，才需要映射什么)\n6、日志 6.1 日志工厂 如果一个数据库操作，出现了异常，我们需要排错。日志就是最好的助手！\n曾经：sout打印、debug跑日志\n现在：日志工厂！\nLOG4J【掌握】 STDOUT_LOGGING 【掌握】 其他的需要了解即可\n在Mybatis中具体需要使用哪个日志实现，在设置中设定！\n1 2 3 \u0026lt;settings\u0026gt; \u0026lt;setting name=\u0026#34;logImpl\u0026#34; value=\u0026#34;STDOUT_LOGGING\u0026#34;/\u0026gt; \u0026lt;/settings\u0026gt; STDOUT_LOGGING 标准日志输出\n6.2 Log4j 什么是Log4j？\nLog4j是Apache的一个开源项目，通过使用Log4j，我们可以控制日志信息输送的目的地：控制台，文本，GUI组件\u0026hellip;. 我们也可以控制每一条日志的输出格式； 通过定义每一条日志信息的级别，我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是，这些可以通过一个配置文件来灵活地进行配置，而不需要修改应用的代码。 1、先导入Log4j的包\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;log4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;log4j\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.17\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 2、log4j.properties\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #将等级为DEBUG的日志信息输出到console和file这两个目的地，console和file的定义在下面的代码 log4j.rootLogger=DEBUG,console,file #控制台输出的相关设置 log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.Target = System.out log4j.appender.console.Threshold=DEBUG log4j.appender.console.layout = org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern=[%c]-%m%n #文件输出的相关设置 log4j.appender.file = org.apache.log4j.RollingFileAppender log4j.appender.file.File=./log/kuang.log log4j.appender.file.MaxFileSize=10mb log4j.appender.file.Threshold=DEBUG log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n #日志输出级别 log4j.logger.org.mybatis=DEBUG log4j.logger.java.sql=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.ResultSet=DEBUG log4j.logger.java.sql.PreparedStatement=DEBUG 3、配置log4j为日志的实现\n1 2 3 4 \u0026lt;settings\u0026gt; \u0026lt;!--Log4j实现--\u0026gt; \u0026lt;setting name=\u0026#34;logImpl\u0026#34; value=\u0026#34;LOG4J\u0026#34;/\u0026gt; \u0026lt;/settings\u0026gt; 4、Log4j日志输出\n5、简单使用log4j\n1、在要使用Log4j的类中，导入包 import org.apache.log4j.Logger;\n2、日志对象，参数为当前类的反射(class)\n1 static Logger logger = Logger.getLogger(TestUserMapper.class); 3、日志级别\n1 2 3 logger.info(\u0026#34;info：进入selectUser方法\u0026#34;); logger.debug(\u0026#34;debug：进入selectUser方法\u0026#34;); logger.error(\u0026#34;error: 进入selectUser方法\u0026#34;); 7、分页 ​\t思考：为什么分页？\n核心 ：减少数据的处理量 在学习mybatis等持久层框架的时候，会经常对数据进行增删改查操作，使用最多的是对数据库进行查询操作，如果查询大量数据的时候，我们往往使用分页进行查询，也就是每次处理小部分数据，这样对数据库压力就在可控范围内。 7.1 SQL底层使用limit进行分页 1 2 3 4 5 6 #语法 SELECT * FROM table LIMIT stratIndex，pageSize SELECT * FROM table LIMIT 5,10; // 检索记录行 6-15 SELECT * FROM table LIMIT 5; [0,5] 7.2使用Mybatis分页，核心SQL 万能Map实现步骤：\n1、写接口\n1 List\u0026lt;User\u0026gt; getUserByLimit(HashMap\u0026lt;String,Integer\u0026gt; map); 2、mapper.xml\n1 2 3 \u0026lt;select id=\u0026#34;getUserByLimit\u0026#34; resultMap=\u0026#34;userMap\u0026#34; parameterType=\u0026#34;map\u0026#34;\u0026gt; select * from t_user limit #{startIndex},#{pageSize} \u0026lt;/select\u0026gt; 3、测试\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testLimit(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); HashMap\u0026lt;String, Integer\u0026gt; map = new HashMap\u0026lt;String, Integer\u0026gt;(); map.put(\u0026#34;startIndex\u0026#34;,0); map.put(\u0026#34;pageSize\u0026#34;,2); List\u0026lt;User\u0026gt; userList = mapper.getUserByLimit(map); for (User user : userList) { System.out.println(user); } sqlSession.close(); } 7.3 使用RowBounds进行分页 不在使用SQL进行分页\n1、接口\n1 2 3 4 5 /** * 使用RowBounds进行分页 * @return */ List\u0026lt;User\u0026gt; getUserbyRowBounds(); 2、mapper.xml\n1 2 3 \u0026lt;select id=\u0026#34;getUserbyRowBounds\u0026#34; resultMap=\u0026#34;userMap\u0026#34;\u0026gt; select * from t_user \u0026lt;/select\u0026gt; 3、测试\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void testRowBounds(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); //RowBounds实现 RowBounds rowBounds = new RowBounds(1,2); //通过java代码层面实现分页 List\u0026lt;User\u0026gt; userList = sqlSession.selectList(\u0026#34;com.study.dao.UserMapper.getUserbyRowBounds\u0026#34;,null,rowBounds); for (User user : userList) { System.out.println(user); } sqlSession.close(); } 7.4 Mybatis分页插件 了解即可，可以自己尝试使用\n官方文档：https://pagehelper.github.io/\n8、使用注解开发 8.1 面向接口编程 大家之前都学过面向对象编程，也学习过接口，但在真正的开发中，很多时候我们会选择面向接口编程\n根本原因 : ==解耦== , 可拓展 , 提高复用 , 分层开发中 , 上层不用管具体的实现 , 大家都遵守共同的标准 , 使得开发变得容易 , 规范性更好\n在一个面向对象的系统中，系统的各种功能是由许许多多的不同对象协作完成的。在这种情况下，各个对象内部是如何实现自己的,对系统设计人员来讲就不那么重要了；\n而各个对象之间的协作关系则成为系统设计的关键。小到不同类之间的通信，大到各模块之间的交互，在系统设计之初都是要着重考虑的，这也是系统设计的主要工作内容。面向接口编程就是指按照这种思想来编程。\n关于接口的理解\n接口从更深层次的理解，应是定义（规范，约束）与实现（名实分离的原则）的分离。\n接口的本身反映了系统设计人员对系统的抽象理解。\n接口应有两类：\n第一类是对一个个体的抽象，它可对应为一个抽象体(abstract class)； 第二类是对一个个体某一方面的抽象，即形成一个抽象面（interface）； 一个体有可能有多个抽象面。抽象体与抽象面是有区别的。\n三个面向区别\n面向对象是指，我们考虑问题时，以对象为单位，考虑它的属性及方法 . 面向过程是指，我们考虑问题时，以一个具体的流程（事务过程）为单位，考虑它的实现 . 接口设计与非接口设计是针对复用技术而言的，与面向对象（过程）不是一个问题.更多的体现就是对系统整体的架构 8.2 使用注解开发 1、注解在接口上实现\n1 2 @Select(\u0026#34;select * from t_user\u0026#34;) List\u0026lt;User\u0026gt; getUsers(); 2、需要在核心配置文件中绑定接口！\n1 2 3 \u0026lt;mappers\u0026gt; \u0026lt;mapper class=\u0026#34;com.study.dao.UserMapper\u0026#34;/\u0026gt; \u0026lt;/mappers\u0026gt; 3、测试\n4、利用dubug查看本质：反射机制实现\n5、底层：本质上利用了jvm的动态代理机制！\n8.3 Mybatis详细执行流程 9、注解CRUD 9.1 步骤： 1、改造MybatisUtils工具类的getSession()方法，重载实现\n作用：自动提交事务\n1 2 3 4 5 6 7 8 //获取SqlSession连接 public static SqlSession getSession(){ return getSession(true); //事务自动提交 } public static SqlSession getSession(boolean flag){ return sqlSessionFactory.openSession(flag); } 2、编写接口方法和注解\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface UserMapper { @Select(\u0026#34;select * from t_user\u0026#34;) List\u0026lt;User\u0026gt; getUsers(); @Insert(\u0026#34;insert into t_user values (#{id},#{username},#{password})\u0026#34;) int addUser(User user); @Update(\u0026#34;update t_user set username=#{username},password=#{password} where id=#{id}\u0026#34;) int updateUser(User user); //如果方法存在多个参数，所有参数前面必须加上@param 注解 （只基本类型的需要加吧） @Delete(\u0026#34;delete from t_user where id=#{uid}\u0026#34;) int deleteUser(@Param(\u0026#34;uid\u0026#34;) int id); } 3、测试\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class TestUserMapper { @Test public void getUsers(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); //底层主要应用反射 UserMapper mapper = sqlSession.getMapper(UserMapper.class); List\u0026lt;User\u0026gt; users = mapper.getUsers(); for (User user : users) { System.out.println(user); } } @Test public void addUser(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); mapper.addUser(new User(8,\u0026#34;hw\u0026#34;,\u0026#34;123456\u0026#34;)); } @Test public void updateUser(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); mapper.updateUser(new User(2,\u0026#34;zzf\u0026#34;,\u0026#34;0601\u0026#34;)); } @Test public void deleteUser(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); mapper.deleteUser(5); } } 9.2 关于@Param()注解 基本类型的参数和String类型，需要加上 引用类型不需要加上 如果只有一个基本类型的话，可以忽略，但是建议大家都加上！ 我们在SQL中引用的就是我们在这里@Param(\u0026quot;\u0026quot;)中设定的属性名！ #{} 和${}\n#{} 相当 预编译的preperedstatment 可以有效的防止sql注入\n${} 就是普通的statment 会造成外界可以拼接SQL，从而导致SQL注入，不安全\n10、Lombok 10.1 使用步骤 1、在IDEA中安装Lombok插件!\n2、在项目中导入lombok的 jar包\n1 2 3 4 5 6 \u0026lt;!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.projectlombok\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;lombok\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.16.10\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 3、在实体类上加注解即可\n1 2 3 4 5 6 7 8 9 @Data @AllArgsConstructor @NoArgsConstructor public class User implements Serializable { private Integer id; private String username; private String password; } @Data ： 无参构造，get，set，tostring，hashcode，equals\n@AllArgsConstructor : 全部参数的构造器 @NoArgsConstructor：无参构造器\n10、多对一处理 10.1数据库设计 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 CREATE TABLE `teacher` ( `id` INT(10) NOT NULL, `name` VARCHAR(30) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 INSERT INTO teacher(`id`, `name`) VALUES (1, \u0026#39;秦老师\u0026#39;); CREATE TABLE `student` ( `id` INT(10) NOT NULL, `name` VARCHAR(30) DEFAULT NULL, `tid` INT(10) DEFAULT NULL, PRIMARY KEY (`id`), KEY `fktid` (`tid`), CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`) ) ENGINE=INNODB DEFAULT CHARSET=utf8 INSERT INTO `student` (`id`, `name`, `tid`) VALUES (\u0026#39;1\u0026#39;, \u0026#39;小明\u0026#39;, \u0026#39;1\u0026#39;); INSERT INTO `student` (`id`, `name`, `tid`) VALUES (\u0026#39;2\u0026#39;, \u0026#39;小红\u0026#39;, \u0026#39;1\u0026#39;); INSERT INTO `student` (`id`, `name`, `tid`) VALUES (\u0026#39;3\u0026#39;, \u0026#39;小张\u0026#39;, \u0026#39;1\u0026#39;); INSERT INTO `student` (`id`, `name`, `tid`) VALUES (\u0026#39;4\u0026#39;, \u0026#39;小李\u0026#39;, \u0026#39;1\u0026#39;); INSERT INTO `student` (`id`, `name`, `tid`) VALUES (\u0026#39;5\u0026#39;, \u0026#39;小王\u0026#39;, \u0026#39;1\u0026#39;); 10.2 测试运行环境 1、导入lombok\n2、新建实体类\n3、建立Mapper接口\n4、建立Mapper.xml文件\n5、在核心配置文件中绑定注册我们的Mapper接口或者文件\n6、测试\n10.3 多对一按照查询嵌套处理 实体类\n1 2 3 4 5 6 7 8 9 10 11 /** * 多个学生对应一个老师 */ @Data @AllArgsConstructor @NoArgsConstructor public class Student { private int id; private String name; private Teacher teacher; } 1 2 3 4 5 6 7 8 @Data @AllArgsConstructor @NoArgsConstructor public class Teacher { int id; private String name; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;!-- 思路:1.查询所有的学生信息 2.根据查询出来的学生的tid，寻找对应的老师！ --\u0026gt; \u0026lt;select id=\u0026#34;getStudentList\u0026#34; resultMap=\u0026#34;StudentTeacher\u0026#34;\u0026gt; select * from student \u0026lt;/select\u0026gt; \u0026lt;resultMap id=\u0026#34;StudentTeacher\u0026#34; type=\u0026#34;Student\u0026#34;\u0026gt; \u0026lt;result property=\u0026#34;id\u0026#34; column=\u0026#34;id\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;name\u0026#34; column=\u0026#34;name\u0026#34;/\u0026gt; \u0026lt;!--复杂的属性需要单独处理 association:对象 collection：集合--\u0026gt; \u0026lt;association property=\u0026#34;teacher\u0026#34; column=\u0026#34;tid\u0026#34; javaType=\u0026#34;Teacher\u0026#34; select=\u0026#34;getTeacher\u0026#34;/\u0026gt; \u0026lt;/resultMap\u0026gt; \u0026lt;select id=\u0026#34;getTeacher\u0026#34; resultType=\u0026#34;Teacher\u0026#34;\u0026gt; select * from teacher where id = #{id}; \u0026lt;/select\u0026gt; 10.4 按照结果嵌套处理 起别名，不改变sql，进行查询 (一步到位) 主要是配置结果集的映射\n1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;!--按照结果嵌套查询--\u0026gt; \u0026lt;select id=\u0026#34;getStudentList2\u0026#34; resultMap=\u0026#34;StudentTeacher2\u0026#34;\u0026gt; select s.id as sid,s.name as sname,t.name as tname from student s,teacher t where s.tid = t.id \u0026lt;/select\u0026gt; \u0026lt;resultMap id=\u0026#34;StudentTeacher2\u0026#34; type=\u0026#34;Student\u0026#34;\u0026gt; \u0026lt;result property=\u0026#34;id\u0026#34; column=\u0026#34;sid\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;name\u0026#34; column=\u0026#34;sname\u0026#34;/\u0026gt; \u0026lt;association property=\u0026#34;teacher\u0026#34; javaType=\u0026#34;Teacher\u0026#34;\u0026gt; \u0026lt;result property=\u0026#34;name\u0026#34; column=\u0026#34;tname\u0026#34;/\u0026gt; \u0026lt;/association\u0026gt; \u0026lt;/resultMap\u0026gt; 10.5 回顾Mysql多对一查询方式 子查询 联表查询 11、一对多处理 比如：一个老师拥有多个学生！\n对于老师而言，就是一对多的关系。\n实体类\n1 2 3 4 5 6 7 8 9 10 11 12 @Data @AllArgsConstructor @NoArgsConstructor public class Teacher { int id; private String name; /** * 一个老师对应多个学生 */ private List\u0026lt;Student\u0026gt; students; } 1 2 3 4 5 6 7 8 9 10 11 /** * 一个老师对应多个学生 */ @Data @AllArgsConstructor @NoArgsConstructor public class Student { private int id; private String name; private int tid; } 11.1 按照结果进行嵌套查询 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 \u0026lt;select id=\u0026#34;getTeacherStudent\u0026#34; resultMap=\u0026#34;TeacherStudent\u0026#34;\u0026gt; select s.id as sid, s.name as sname,t.name as tname,t.id as tid from teacher t ,student s where (t.id=#{tid}) and (t.id=s.tid); \u0026lt;/select\u0026gt; \u0026lt;resultMap id=\u0026#34;TeacherStudent\u0026#34; type=\u0026#34;Teacher\u0026#34;\u0026gt; \u0026lt;result property=\u0026#34;id\u0026#34; column=\u0026#34;tid\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;name\u0026#34; column=\u0026#34;tname\u0026#34;/\u0026gt; \u0026lt;!-- 复杂的属性需要单独处理 association:对象 collection：集合 javaType：指定属性的类型 集合中的泛型信息，我们使用ofType获取 --\u0026gt; \u0026lt;collection property=\u0026#34;students\u0026#34; ofType=\u0026#34;Student\u0026#34;\u0026gt; \u0026lt;result property=\u0026#34;id\u0026#34; column=\u0026#34;sid\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;name\u0026#34; column=\u0026#34;sname\u0026#34;/\u0026gt; \u0026lt;result property=\u0026#34;tid\u0026#34; column=\u0026#34;tid\u0026#34;/\u0026gt; \u0026lt;/collection\u0026gt; \u0026lt;/resultMap\u0026gt; 接口\n1 2 3 4 /** * 获取指定老师下面的所有学生的信息 */ Teacher getTeacherStudent(@Param(\u0026#34;tid\u0026#34;) int id); 10.2 总结： 1、关联：association 【多对一】\n2、集合：collection 【一对多】\n3、javaType \u0026amp; ofType\n​\tjavaType ：用来指定实体类中属性的类型\n​\tofType：用来指定映射到List或者集合中的pojo类型，泛型中的约束类型！\n注意点：\n保证sql的可读性，尽量保证通俗易懂 注意一对多和多对一的 属性名和字段名的问题 如果问题不好排查错误，可以使用日志，建议使用 [log4j] 面视高频:\nMysql 引擎 InnoDB 和 MyISAM InnoDB 底层原理 索引 索引优化！ 12、动态SQL 什么是动态SQL：动态SQL指的是根据不同的查询条件 , 生成不同的Sql语句.\n1 2 3 4 5 6 7 8 动态 SQL 元素和 JSTL 或基于类似 XML 的文本处理器相似。在 MyBatis 之前的版本中，有很多元素需要花时间了解。MyBatis 3 大大精简了元素种类，现在只需学习原来一半的元素便可。MyBatis 采用功能强大的基于 OGNL 的表达式来淘汰其它大部分元素。 ------------------------------- - if - choose (when, otherwise) - trim (where, set) - foreach ------------------------------- 12.1 搭建环境 1、数据库\n1 2 3 4 5 6 7 CREATE TABLE `blog` ( `id` varchar(50) NOT NULL COMMENT \u0026#39;博客id\u0026#39;, `title` varchar(100) NOT NULL COMMENT \u0026#39;博客标题\u0026#39;, `author` varchar(30) NOT NULL COMMENT \u0026#39;博客作者\u0026#39;, `create_time` datetime NOT NULL COMMENT \u0026#39;创建时间\u0026#39;, `views` int(30) NOT NULL COMMENT \u0026#39;浏览量\u0026#39; ) ENGINE=InnoDB DEFAULT CHARSET=utf8 2、实体类\n1 2 3 4 5 6 7 8 9 10 @Data @NoArgsConstructor @AllArgsConstructor public class Blog { private int id; private String title; private String author; private Date create_time; private int views; } 3、编写接口\n4、接口的mappe.xml文件\n5、插入数据\n12.2 IF(where) 1 2 3 4 5 6 7 8 9 10 11 \u0026lt;select id=\u0026#34;queryBlogIF\u0026#34; parameterType=\u0026#34;map\u0026#34; resultType=\u0026#34;Blog\u0026#34;\u0026gt; select * from blog \u0026lt;where\u0026gt; \u0026lt;if test=\u0026#34;title != null\u0026#34;\u0026gt; title = #{title} \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;author != null\u0026#34;\u0026gt; and author = #{author} \u0026lt;/if\u0026gt; \u0026lt;/where\u0026gt; \u0026lt;/select\u0026gt; 这个“where”标签会知道如果它包含的标签中有返回值的话，它就插入一个‘where’。此外，如果标签返回的内容是以AND 或OR 开头的，则它会剔除掉。 12.3 Choose 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;select id=\u0026#34;queryBlogChoose\u0026#34; parameterType=\u0026#34;map\u0026#34; resultType=\u0026#34;Blog\u0026#34;\u0026gt; select * from blog \u0026lt;where\u0026gt; \u0026lt;choose\u0026gt; \u0026lt;when test=\u0026#34;title != null\u0026#34;\u0026gt; title = #{title} \u0026lt;/when\u0026gt; \u0026lt;when test=\u0026#34;author != null\u0026#34;\u0026gt; and author = #{author} \u0026lt;/when\u0026gt; \u0026lt;otherwise\u0026gt; and views = #{views} \u0026lt;/otherwise\u0026gt; \u0026lt;/choose\u0026gt; \u0026lt;/where\u0026gt; \u0026lt;/select\u0026gt; 12.4 trim（where，set） 1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;update id=\u0026#34;updateBlog\u0026#34; parameterType=\u0026#34;map\u0026#34;\u0026gt; update blog \u0026lt;set\u0026gt; \u0026lt;if test=\u0026#34;title != null\u0026#34;\u0026gt; title = #{title}, \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;author != null\u0026#34;\u0026gt; author = #{author} \u0026lt;/if\u0026gt; \u0026lt;/set\u0026gt; where id = #{id} \u0026lt;/update\u0026gt; 12.5 SQL片段 有时候可能某个 sql 语句我们用的特别多，为了增加代码的重用性，简化代码，我们需要将这些代码抽取出来，然后使用时直接调用。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;sql id=\u0026#34;if-title-author\u0026#34;\u0026gt; \u0026lt;if test=\u0026#34;title != null\u0026#34;\u0026gt; title = #{title} \u0026lt;/if\u0026gt; \u0026lt;if test=\u0026#34;author != null\u0026#34;\u0026gt; and author = #{author} \u0026lt;/if\u0026gt; \u0026lt;/sql\u0026gt; \u0026lt;!-- 引用 sql 片段，如果refid 指定的不在本文件中，那么需要在前面加上 namespace --\u0026gt; \u0026lt;select id=\u0026#34;queryBlogIF\u0026#34; parameterType=\u0026#34;map\u0026#34; resultType=\u0026#34;Blog\u0026#34;\u0026gt; select * from blog \u0026lt;where\u0026gt; \u0026lt;include refid=\u0026#34;if-title-author\u0026#34;/\u0026gt; \u0026lt;!-- 在这里还可以引用其他的 sql 片段 --\u0026gt; \u0026lt;/where\u0026gt; \u0026lt;/select\u0026gt; 引用 sql 片段，如果refid 指定的不在本文件中，那么需要在前面加上 namespace\n注意：\n①、最好基于 单表来定义 sql 片段，提高片段的可重用性\n②、在 sql 片段中不要包括 where\n12.6 Foreach 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;!--select * from blog where 1=1 and (id=1 or id=2 or id=3)--\u0026gt; \u0026lt;!-- collection:要遍历的集合 item：每次遍历生成的对象 open:开始遍历时的拼接字符串 close:结束时拼接的字符串 separator:遍历对象之间需要拼接的字符串 --\u0026gt; \u0026lt;select id=\u0026#34;queryBlogForeach\u0026#34; resultType=\u0026#34;Blog\u0026#34; parameterType=\u0026#34;map\u0026#34;\u0026gt; select * from blog \u0026lt;where\u0026gt; \u0026lt;foreach collection=\u0026#34;ids\u0026#34; item=\u0026#34;id\u0026#34; open=\u0026#34;and (\u0026#34; separator=\u0026#34;or\u0026#34; close=\u0026#34;)\u0026#34;\u0026gt; id = #{id} \u0026lt;/foreach\u0026gt; \u0026lt;/where\u0026gt; \u0026lt;/select\u0026gt; 测试\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void selectForeach(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); BlogMapper mapper = sqlSession.getMapper(BlogMapper.class); Map\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;String, Object\u0026gt;(); ArrayList\u0026lt;Integer\u0026gt; ids = new ArrayList\u0026lt;Integer\u0026gt;(); ids.add(1); ids.add(2); map.put(\u0026#34;ids\u0026#34;,ids); List\u0026lt;Blog\u0026gt; blogs = mapper.queryBlogForeach(map); for (Blog blog : blogs) { System.out.println(blog); } sqlSession.close(); } 所谓的动态SQL，本质上还是SQL，只是我们在SQL层面，去执行了一个逻辑代码\n==动态SQL就是在拼接SQL语句，我们只要保证SQL的正确性，按照SQL的格式，去排列组合就可以了==\n建议\n先在Mysql中写出完整的SQL，再去对应的去修改成为我们的动态SQL实现通用即可！ 动态SQL在开发中大量的使用，一定要熟练掌握！ 13、缓存 13.1 简介 1、什么是缓存 [ Cache ]？\n存在内存中的临时数据。 将用户经常查询的数据放在缓存（内存）中，用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询，从缓存中查询，从而提高查询效率，解决了高并发系统的性能问题。 2、为什么使用缓存？\n减少和数据库的交互次数，减少系统开销，提高系统效率。 3、什么样的数据能使用缓存？\n经常查询并且不经常改变的数据。 13.2 Mybatis缓存 MyBatis包含一个非常强大的查询缓存特性，它可以非常方便地定制和配置缓存。缓存可以极大的提升查询效率。\nMyBatis系统中默认定义了两级缓存：一级缓存和二级缓存\n默认情况下，只有一级缓存开启。（SqlSession级别的缓存，也称为本地缓存） 二级缓存需要手动开启和配置，他是基于namespace级别的缓存。（mapper缓存） 为了提高扩展性，MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存 13.3 一级缓存 一级缓存也叫本地缓存：\n与数据库同一次会话期间查询到的数据会放在本地缓存中。 以后如果需要获取相同的数据，直接从缓存中拿，没必须再去查询数据库； 测试:\n1、开启日志\n2、编写接口方法和对应的Mapper.xml\n1 2 3 public interface UserMapper { User queryUser(@Param(\u0026#34;id\u0026#34;) int id); } 1 2 3 \u0026lt;select id=\u0026#34;queryUser\u0026#34; resultType=\u0026#34;User\u0026#34;\u0026gt; select * from t_user where id = #{id}; \u0026lt;/select\u0026gt; 3、测试\n1 2 3 4 5 6 7 8 9 10 11 @Test public void testQueryUser(){ SqlSession sqlSession = MybatisUtils.getSqlSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); User user = mapper.queryUser(2); System.out.println(user); System.out.println(\u0026#34;===========================\u0026#34;); User user2 = mapper.queryUser(2); System.out.println(user2); sqlSession.close(); } 13.4 一级缓存失效的四种情况 一级缓存是SqlSession级别的缓存，是一直开启的，我们关闭不了它；\n一级缓存失效情况：没有使用到当前的一级缓存，效果就是，还需要再向数据库中发起一次查询请求！\n1、sqlSession不同**\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void testQueryUserById(){ SqlSession session = MybatisUtils.getSession(); SqlSession session2 = MybatisUtils.getSession(); UserMapper mapper = session.getMapper(UserMapper.class); UserMapper mapper2 = session2.getMapper(UserMapper.class); User user = mapper.queryUserById(1); System.out.println(user); User user2 = mapper2.queryUserById(1); System.out.println(user2); System.out.println(user==user2); session.close(); session2.close(); } 观察结果：发现发送了两条SQL语句！\n结论：每个sqlSession中的缓存相互独立\n2、sqlSession相同，查询条件不同\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testQueryUserById(){ SqlSession session = MybatisUtils.getSession(); UserMapper mapper = session.getMapper(UserMapper.class); UserMapper mapper2 = session.getMapper(UserMapper.class); User user = mapper.queryUserById(1); System.out.println(user); User user2 = mapper2.queryUserById(2); System.out.println(user2); System.out.println(user==user2); session.close(); } 观察结果：发现发送了两条SQL语句！很正常的理解\n结论：当前缓存中，不存在这个数据\n3、sqlSession相同，两次查询之间执行了增删改操作\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void testQueryUserById(){ SqlSession session = MybatisUtils.getSession(); UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.queryUserById(1); System.out.println(user); HashMap map = new HashMap(); map.put(\u0026#34;name\u0026#34;,\u0026#34;kuangshen\u0026#34;); map.put(\u0026#34;id\u0026#34;,4); mapper.updateUser(map); User user2 = mapper.queryUserById(1); System.out.println(user2); System.out.println(user==user2); session.close(); } 观察结果：查询在中间执行了增删改操作后，重新执行了\n结论：因为增删改操作可能会对当前数据产生影响\n4、sqlSession相同，手动清除一级缓存\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void testQueryUserById(){ SqlSession session = MybatisUtils.getSession(); UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.queryUserById(1); System.out.println(user); session.clearCache();//手动清除缓存 User user2 = mapper.queryUserById(1); System.out.println(user2); System.out.println(user==user2); session.close(); } 一级缓存就是一个map\n13.5 二级缓存 只有一级缓存不起作用的时候，才使用二级缓存\n二级缓存也叫全局缓存，一级缓存作用域太低了，所以诞生了二级缓存\n基于namespace级别的缓存，一个名称空间，对应一个二级缓存；\n工作机制\n一个会话查询一条数据，这个数据就会被放在当前会话的一级缓存中； 如果当前会话关闭了，这个会话对应的一级缓存就没了；但是我们想要的是，会话关闭了，一级缓存中的数据被保存到二级缓存中； 新的会话查询信息，就可以从二级缓存中获取内容； 不同的mapper查出的数据会放在自己对应的缓存（map）中； 使用步骤：\n1、开启全局缓存 【mybatis-config.xml】\n1 \u0026lt;setting name=\u0026#34;cacheEnabled\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; 2、去每个mapper.xml中配置使用二级缓存\n1 2 3 4 5 6 \u0026lt;cache eviction=\u0026#34;FIFO\u0026#34; flushInterval=\u0026#34;60000\u0026#34; size=\u0026#34;512\u0026#34; readOnly=\u0026#34;true\u0026#34;/\u0026gt; 这个更高级的配置创建了一个 FIFO 缓存，每隔 60 秒刷新，最多可以存储结果对象或列表的 512 个引用，而且返回的对象被认为是只读的，因此对它们进行修改可能会在不同线程中的调用者产生冲突。 3、测试\n所有的实体类先实现序列化接口 测试代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void testQueryUserById(){ SqlSession session = MybatisUtils.getSession(); SqlSession session2 = MybatisUtils.getSession(); UserMapper mapper = session.getMapper(UserMapper.class); UserMapper mapper2 = session2.getMapper(UserMapper.class); User user = mapper.queryUserById(1); System.out.println(user); session.close(); User user2 = mapper2.queryUserById(1); System.out.println(user2); System.out.println(user==user2); session2.close(); } 结论：\n只要开启了二级缓存，我们在同一个Mapper中的查询，可以在二级缓存中拿到数据\n查出的数据都会被默认先放在一级缓存中\n只有会话提交或者关闭以后，一级缓存中的数据才会转到二级缓存中\n13.6 缓存原理 注意：\n1、先看二级缓存中有没有\n2、再看一级缓存用有没有\n3、最后查询数据库\n","permalink":"https://ktzxy.top/posts/y6shmghekz/","summary":"MyBatis笔记2","title":"MyBatis笔记2"},{"content":" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 #!/bin/sh yum clean all yum makecache find . -type f -exec touch {} \\; echo 删除现有的用户和组 userdel oracle groupdel dba groupdel oinstall echo 创建orale用户 groupadd dba groupadd oinstall useradd -g oinstall -G dba oracle echo \u0026#34;1234\u0026#34; | passwd --stdin \u0026#34;oracle\u0026#34; echo orale用户创建完成 echo 创建oracle安装目录 mkdir -p /opt/oracle/product/11.2/db_1 mkdir -p /opt/oraInventory mkdir -p /opt/oracle/oradata mkdir -p /var/opt/oracle echo oracle安装目录完成 echo 给目录赋予权限 chown -R oracle.oinstall /opt/oracle chown -R oracle.oinstall /opt/oracle/oradata chown -R oracle.oinstall /opt/oracle/product/11.2/db_1 chown -R oracle.dba /opt/oraInventory chown oracle.dba /var/opt/oracle chmod -R 775 /opt/oracle chmod -R 755 /var/opt/oracle echo 目录所有组赋予完成 echo 设置oracle环境变量 echo \u0026#39;export ORACLE_BASE=/opt/oracle export ORACLE_HOME=$ORACLE_BASE/product/11.2/db_1 export ORACLE_SID=orcl export ORACLE_OWNER=oracle export ORACLE_TERM=vt100 export PATH=$PATH:$ORACLE_HOME/bin:$HOME/bin export PATH=$ORACLE_HOME/bin:$PATH LD_LIBRARY_PATH=$ORACLE_HOME/lib:/lib:/usr/lib:/usr/local/lib export LD_LIBRARY_PATH CLASSPATH=$ORACLE_HOME/JRE:$ORACLE_HOME/jlib:$ORACLE_HOME/rdbms/jlib CLASSPATH=$CLASSPATH:$ORACLE_HOME/network/jlib export CLASSPATH NLS_LANG=\u0026#34;AMERICAN_AMERICA.AL32UTF8\u0026#34; PATH=$PATH:/usr/sbin; export PATH PATH=$PATH:/usr/bin; export PATH export ORA_NLS33=$ORACLE_HOME/nls/admin/data\u0026#39; \u0026gt;\u0026gt;/home/oracle/.bash_profile source /home/oracle/.bash_profile echo 设置oracle环境变量完成 echo 安装依赖包，可能提示没有成功安装，最后跳过即可 yum install -y libaio-* yum install -y gcc-* yum install -y glibc-* yum install -y compat-libstdc* yum install -y elfutils-libelf-devel* yum install -y libstdc++* yum install -y unixODBC-* yum install -y unixODBC-devel-* echo 依赖包安装完成 echo 开始安装jdk 7.0 rpm -e --nodeps jdk-1.7.0_80-fcs.x86_64 rpm -ivh jdk-7u80-linux-x64.rpm echo jdk安装完成 路径为:/usr/java echo 设置jdk环境变量 echo \u0026#39; export JAVA_HOME=/usr/java/jdk1.7.0_80 export PATH=$PATH:$JAVA_HOME/bin export JRE_HOME=$JAVA_HOME/jre export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib\u0026#39;\u0026gt;\u0026gt;/etc/profile echo jdk环境变量设置完成 echo 设置软限制和硬限制 echo \u0026#39;oracle soft nproc 2047 oracle hard nproc 16384 oracle soft nofile 1024 oracle hard nofile 65536 oracle hard stack 10240\u0026#39; \u0026gt;\u0026gt;/etc/security/limits.conf echo 设置限制完成 echo 修改内核参数 echo \u0026#39; fs.aio-max-nr = 1048576 fs.file-max = 6815744 kernel.shmall = 2097152 kernel.shmmax = 8405194752 kernel.shmmni = 4096 kernel.sem = 250 32000 100 128 net.ipv4.ip_local_port_range = 9000 65500 net.core.rmem_default = 262144 net.core.rmem_max = 4194304 net.core.wmem_default = 262144 net.core.wmem_max = 1048586\u0026#39; \u0026gt;\u0026gt;/etc/sysctl.conf cd /etc sysctl -p echo 修改内核完成 echo -先设置开启启动装完自己修改即可 echo \u0026#34;su - oracle -lc \u0026#39;dbstart \\$ORACLE_HOME\u0026#39;\u0026#34; \u0026gt;\u0026gt;/etc/rc.local chmod -R 777 /home/database echo -oracle自启完成 echo -重启电脑 reboot ","permalink":"https://ktzxy.top/posts/bk1y3q3yqo/","summary":"部署脚本","title":"部署脚本"},{"content":"1. 泛型(generics)概述 Java 泛型（generics）是 JDK 1.5 中引入的一个新特性，提供了编译时类型安全检测机制，该机制允许程序员在定义类和接口的时候使用类型参数，从而在编译时可检测到非法的类型。声明的类型参数在使用时会用具体的类型来替换。泛型的本质是参数化类型，也就是说所操作的数据类型被指定为一个参数。\n比如要写一个排序方法，能够对整型数组、字符串数组甚至其他任何类型的数组进行排序，就可以使用 Java 泛型。\n1.1. 泛型的应用场景 在定义类（方法、接口）的时候不确定类型，在使用类（方法、接口）的时候才确定类型。\n1.2. 泛型定义的位置 类 方法 接口 参数 1.3. 泛型的特点 泛型只在编译阶段有效。 泛型类型必须是引用类型。 2. 泛型变量（标识） 泛型变量可以理解为数据类型的占位符，也可以理解为某种数据类型的变量。\n泛型变量的命名规则：只要是合法的标识符即可，一般使用一个大写字母。常用的泛型标识：T、E、K、V\n泛型标识 代表的元素名称 说明 E Element 通常用在定义集合的泛型标识，表示在集合中存放的元素 T Type 表示Java类，包括基本的类和自定义的类 K Key 表示键，比如Map集合中的key V Value 表示值 N Number 表示数值类型 ? \\ 表示不确定的Java类型 3. 泛型类 3.1. 泛型类的概念 泛型类的声明和非泛型类的声明类似，除了在类名后面添加了类型参数声明部分。泛型类的类型参数声明部分也包含一个或多个类型参数，参数间用英文逗号（,）隔开。一个泛型参数，也被称为一个类型变量，是用于指定一个泛型类型名称的标识符。因为他们接受一个或多个参数，这些类被称为参数化的类或参数化的类型。\n泛型类的特点：\n在类定义上使用了泛型变量的类就是泛型类。 当创建该类的对象的时候，传入类型，此时类上的泛型被确定。 3.2. 泛型类定义语法 1 2 3 public class 类名\u0026lt;泛型标识1, 泛型标识2, ...\u0026gt; { // ... } 语法说明：\n泛型标识：可以定义任意的标识号，用于标识指定的泛型的类型 在 \u0026lt;\u0026gt; 中的定义的泛型类型数量不限 在 \u0026lt;\u0026gt; 中的定义泛型标识，如；E, T, ... 能够做为类型在该类内部被使用 3.3. 泛型类使用语法 创建泛型类对象时指定泛型变量的具体数据类型\n1 泛型类\u0026lt;具体的数据类型\u0026gt; 对象名 = new 泛型类\u0026lt;具体的数据类型\u0026gt;(); 在 java 1.7 以后，后面的\u0026lt;\u0026gt;中的具体的数据类型可以省略不写\n1 泛型类\u0026lt;具体的数据类型\u0026gt; 对象名 = new 泛型类\u0026lt;\u0026gt;(); 3.4. 使用示例 定义泛型类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.moon.basic; /** * 泛型类的定义 * * @param \u0026lt;T\u0026gt; 此处 T 可以随便写为任意标识，常见的如 T、E、K、V 等形式的参数常用于表示泛型 * 在实例化泛型类时，必须指定T的具体类型 */ public class Generic\u0026lt;T\u0026gt; { // 此成员变量的类型为T，T 的类型在创建实例时指定 private T key; // 泛型构造方法形参的类型也为T，T 的类型在创建实例时指定 public Generic(T key) { this.key = key; } // 泛型方法的返回值类型为 T，T 的类型在创建实例时指定 public T getKey() { return key; } // 泛型方法的形参类型也为T，T 的类型在创建实例时指定 public void setKey(T key) { this.key = key; } } 使用泛型类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 在使用泛型类的时候去确定泛型类形参的数据类型\u0026lt;数据类型\u0026gt;，Java 1.7以后，右边\u0026lt;\u0026gt;的数据类型可以省略 Generic\u0026lt;String\u0026gt; strGen = new Generic\u0026lt;\u0026gt;(\u0026#34;abc\u0026#34;); String str = strGen.getKey(); System.out.println(str); // 泛型的类型参数只能是类类型（包括自定义类），不能是基本数据类型 Generic\u0026lt;Integer\u0026gt; intGen = new Generic\u0026lt;Integer\u0026gt;(123); int num = intGen.getKey(); System.out.println(num); // 泛型类如果没有指定数据类型，那么会按 Object 类型来处理 Generic generic = new Generic(\u0026#34;abc\u0026#34;); Object key = generic.getKey(); System.out.println(key); 3.5. 泛型类的注意事项 泛型的类型参数只能是引用类型，不能是基本数据类型 泛型类的泛型变量的具体数据类型是在创建对象时确定。 如果在创建泛型类对象时没有指定泛型变量的具体数据时，即原始类型，默认是 Object 类型。(不推荐)。原始类型和带参数类型之间的主要区别是，在编译时编译器不会对原始类型进行类型安全检查，却会对带参数的类型进行检查。 静态方法中不能使用类定义的泛型变量，如果静态中要使用到泛型变量，则需要将该方法定义成泛型方法。不要使用类上使用的泛型变量。(因为静态方法不需要对象就可以调用。) 4. 泛型类派生子类 4.1. 语法定义 如果定义的类，继承了一个泛型父类，则出现以下几种情况：\n子类也是泛型类，则子类和父类的泛型类型要一致 1 class 子类\u0026lt;T\u0026gt; extends 父类\u0026lt;T\u0026gt; 子类不是泛型类，则父类要明确泛型的数据类型 1 class 子类 extends 父类\u0026lt;数据类型\u0026gt; Notes: 子类重写父类的方法，需要和父类类型保持一致\n4.2. 使用示例 泛型父类 1 2 3 4 5 6 7 8 9 10 11 12 public class Parent\u0026lt;E\u0026gt; { private E value; public E getValue() { return value; } public void setValue(E value) { this.value = value; } } 泛型类派生子类情况一：子类也是泛型类，那么子类的泛型标识要和父类一致。 1 2 3 4 5 6 public class ChildFirst\u0026lt;T\u0026gt; extends Parent\u0026lt;T\u0026gt; { @Override public T getValue() { return super.getValue(); } } 泛型类派生子类情况二：如果子类不是泛型类，那么父类要明确数据类型 1 2 3 4 5 6 7 8 9 10 11 public class ChildSecond extends Parent\u0026lt;Integer\u0026gt; { @Override public Integer getValue() { return super.getValue(); } @Override public void setValue(Integer value) { super.setValue(value); } } 测试 1 2 3 4 5 6 7 8 9 10 11 12 // 子类是泛型类，则需要与父类类型保持一致 ChildFirst\u0026lt;String\u0026gt; childFirst = new ChildFirst\u0026lt;\u0026gt;(); childFirst.setValue(\u0026#34;abc\u0026#34;); String value = childFirst.getValue(); System.out.println(value); System.out.println(\u0026#34;---------------------------------\u0026#34;); // 子类非泛型类，将泛型父类需要指定类型 ChildSecond childSecond = new ChildSecond(); childSecond.setValue(100); Integer value1 = childSecond.getValue(); System.out.println(value1); 5. 泛型接口 5.1. 泛型接口概念 在接口定义上使用了泛型变量的接口\n5.2. 泛型接口语法 1 2 3 public interface 接口名 \u0026lt;泛型变量\u0026gt; { // 接口方法 } 5.3. 泛型接口实现 实现类不是泛型类，则泛型接口要明确泛型变量的具体数据类型。 1 2 public class BaseDao implements Dao\u0026lt;Student\u0026gt;{ } 实现类也是泛型类，则实现类和接口的泛型类型要一致。泛型变量具体数据类型在创建该类的对象时指定。(推荐) 1 2 public class BaseDao\u0026lt;T\u0026gt; implements Dao\u0026lt;T\u0026gt;{ } Tips: 指定泛型变量时只写接口\u0026lt;\u0026gt;处，不指定泛型变量需要类名和接口名两边都写\u0026lt;\u0026gt;\n5.4. 使用示例 泛型接口 1 2 3 public interface GenericInterface\u0026lt;T\u0026gt; { T getKey(); } 实现泛型接口的类情况一：不是泛型类，需要明确实现泛型接口的数据类型。 1 2 3 4 5 6 public class GenericInterfaceImplFirst implements GenericInterface\u0026lt;String\u0026gt; { @Override public String getKey() { return \u0026#34;Hello GenericInterface implement\u0026#34;; } } 实现泛型接口的实现类情况二：是一个泛型类，那么要保证实现接口的泛型类泛型标识包含泛型接口的泛型标识 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class GenericInterfaceImplSecond\u0026lt;T, E\u0026gt; implements GenericInterface\u0026lt;T\u0026gt; { private T key; private E value; public GenericInterfaceImplSecond(T key, E value) { this.key = key; this.value = value; } @Override public T getKey() { return key; } public E getValue() { return value; } } 测试 1 2 3 4 5 6 7 8 // 实现类非泛型类，需要明确实现泛型接口的数据类型。 GenericInterfaceImplFirst genericImpl1 = new GenericInterfaceImplFirst(); System.out.println(genericImpl1.getKey()); System.out.println(\u0026#34;---------------------------------\u0026#34;); // 实现类是泛型类，则要保证实现接口的泛型类泛型标识包含泛型接口的泛型标识 GenericInterfaceImplSecond\u0026lt;String, Integer\u0026gt; genericImpl2 = new GenericInterfaceImplSecond\u0026lt;\u0026gt;(\u0026#34;count\u0026#34;, 100); System.out.println(genericImpl2.getKey() + \u0026#34;=\u0026#34; + genericImpl2.getValue()); 6. 泛型方法 6.1. 泛型方法的概念 泛型方法，是在调用方法的时候指明泛型的具体类型。在方法的修改符与返回值之间是否存在 \u0026lt;\u0026gt; 标识，是判断泛型方法的唯一依据。\n6.2. 泛型方法定义 6.2.1. 语法格式 1 2 3 修饰符 \u0026lt;泛型变量\u0026gt; 返回值类型 方法名(参数列表) { // 方法上的泛型定义在返回值的前面 } 修饰符与返回值中间的泛型标识\u0026lt;泛型变量\u0026gt;非常重要，可以理解成以此为标识，声明此方法为泛型方法。 只有声明了的方法才是泛型方法，泛型类中的使用了泛型的成员方法并不是泛型方法。 \u0026lt;泛型变量\u0026gt; 表明该方法将使用泛型类型，此时才可以在方法中使用泛型类型。 与泛型类的定义一样，此处\u0026lt;泛型变量\u0026gt;可以定义任意标识，常见的如 T、E、K、V 等形式的参数常用于表示泛型。 6.2.2. 定义示例 1 2 3 4 5 6 7 8 9 10 11 public \u0026lt;T\u0026gt; T 方法名(T 参数变量名) { // 方法上的泛型定义在返回值的前面 } public \u0026lt;T\u0026gt; void 方法名(T 参数变量名) { // 返回值也可以是void } public static \u0026lt;T,E,K\u0026gt; void printType(T t, E e, K k) { // 静态的泛型方法，采用多个泛型类型 } 6.3. 泛型使用格式 调用方法时，由参数类型确定泛型的类型\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // 例1：API中的ArrayList集合中的方法： public \u0026lt;T\u0026gt; T[] toArray(T[] a){ } // 该方法，用来把集合元素存储到指定数据类型的数组中，返回已存储集合元素的数组 // 例2： ArrayList\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;String\u0026gt;(); String[] arr = new String[100]; String[] result = list.toArray(arr); // 此时，变量T的值就是String类型。变量T，可以与定义集合的泛型不同 public \u0026lt;String\u0026gt; String[] toArray(String[] a){ } // 例3： ArrayList\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;String\u0026gt;(); Integer[] arr = new Integer[100]; Integer[] result = list.toArray(arr); // 此时，变量T的值就是Integer类型。变量T，可以与定义集合的泛型不同 public \u0026lt;Integer\u0026gt; Integer[] toArray(Integer[] a){ } 6.4. 泛型方法须知 泛型方法上泛型变量的具体数据类型是什么，取决于方法调用时传入的参数。 泛型变量具体数据类型不能是基本数据类型，如果要使用基本数据类型，要使用对应的包装类类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import java.util.Arrays; public class Test2_04 { public static void main(String[] args) { // 定义一个数组 String[] arr = { \u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;d\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;, \u0026#34;D\u0026#34;, \u0026#34;E\u0026#34; }; // 泛型方法必须放基本数据类型对应的包装类型，否则报错。 Integer[] arrInt = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 11, 12 }; // 调用方法测试String数组 swap(arr, 0, 9); // 调用方法测试Integer数组 swap(arrInt, 0, 9); // 如果传入错误的索引就输出错误信息 swap(arr, 0, 19); } public static \u0026lt;E\u0026gt; void swap(E[] arr, int i, int j) { if ((i \u0026gt;= 0 \u0026amp;\u0026amp; i \u0026lt; arr.length) \u0026amp;\u0026amp; (j \u0026gt;= 0 \u0026amp;\u0026amp; j \u0026lt; arr.length)) { E temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; System.out.println(Arrays.toString(arr)); } else { System.out.println(\u0026#34;你输入的索引错误！\u0026#34;); } } } 6.5. 泛型方法与泛型类的成员方法 泛型类中可以定义泛型方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class GenericMethod\u0026lt;T\u0026gt; { private T key; // 此方法非泛型方法，这是一个普通方法，它的数据类型遵从泛型类的类型 public T getKey() { return key; } // 此方法是泛型方法，不会依赖父类的泛型类型，即使以 \u0026lt;T\u0026gt; 作为泛型变量，也与泛型类无关 public \u0026lt;E\u0026gt; void show(E[] e) { for (int i = 0; i \u0026lt; e.length; i++) { System.out.println(e[i]); } } } 在泛型类中，public T getKey() 是采用泛型类类型的普通方法，不是泛型方法，并且不支持 static；public \u0026lt;E\u0026gt; void show(E[] e) 是泛型方法，也可以定义为静态的\n6.6. 泛型方法与可变参数 在泛型方法中，可以使用泛型的可变参数。\n1 2 3 4 5 public \u0026lt;E\u0026gt; void print(E... e) { for (E e1 : e) { System.out.println(e); } } 7. 泛型通配符 ? 7.1. 概述 泛型通配符：代表可以匹配任意类型，一般用来表示泛型的上下限。具有以下特点：\n可以直接使用，不用定义。 不能使用泛型类、泛型方法和泛型接口定义上。 一般不会单独使用，一般会结合泛型上下限使用。 Notes: 类型通配符一般是使用 ? 代替具体的类型实参。所以类型通配符 ? 是类型实参，而不是类型形参。\n定义方式：参考 ArrayList 类的构造方法，无法在类中使用 使用方式：调用方法时可以给予任意类型。参照 Arraylist 的构造方法 7.2. 泛型的上限 语法：\n1 类/接口\u0026lt;? extends 形参类型\u0026gt; 如 \u0026lt;? extends E\u0026gt;，代表传递 E 类型或 E 的子类，即该通配符所代表的类型是 E 类型的子类。还有一种特殊的形式，可以指定其不仅要是指定类型的子类，而且还要实现某些接口。\n例如：List\u0026lt;? extends A\u0026gt; 表明这是 A 某个具体子类的 List，保存的对象必须是 A 或 A 的子类。对于 List\u0026lt;? extends A\u0026gt; 列表，由于只知道父类但无法得知使用哪个子类，因此不能添加 A 或 A 的子类对象，只能获取 A 的对象。\n示例代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class GenericityUpTest { // 泛型类基础使用测试 @Test public void test01() { List\u0026lt;Animal\u0026gt; animals = new ArrayList\u0026lt;\u0026gt;(); List\u0026lt;Cat\u0026gt; cats = new ArrayList\u0026lt;\u0026gt;(); List\u0026lt;MiniCat\u0026gt; miniCats = new ArrayList\u0026lt;\u0026gt;(); cats.addAll(cats); cats.addAll(miniCats); // showAnimal(animals); // 报错 showAnimal(cats); showAnimal(miniCats); } /** * 泛型上限通配符，调用该方法时，传递的集合类型只能是Cat或Cat的子类类型。 */ public void showAnimal(List\u0026lt;? extends Cat\u0026gt; list) { // 这里泛型形参集合不能添加元素。 // 因为 \u0026lt;? extends Cat\u0026gt; 表示未知的子类，程序无法确定这个类型是什么，所以无法将任何对象添加到集合中 // list.add(new Animal()); // 报错 // list.add(new Cat()); // 报错 // list.add(new MiniCat()); // 报错 // 因此，这种指定通配符上限的集合，只能从集合中读取元素（取出的元素总是上限类型），不能向集合中添加元素。 for (int i = 0; i \u0026lt; list.size(); i++) { Cat cat = list.get(i); System.out.println(cat); } } } 7.3. 泛型的下限 语法：\n1 类/接口\u0026lt;? super 形参类型\u0026gt; 如 \u0026lt;? super E\u0026gt;，可以传递 E 类型或 E 的父类，即该通配符所代表的类型是 E 类型的父类。\n例如：List\u0026lt;? super A\u0026gt; 表明这是 A 某个具体父类的 List，保存的对象必须是 A 或 A 的超类。对于 List\u0026lt;? super A\u0026gt; 列表，由于编译器已知集合元素是下限类型，因此能够添加 A 或 A 的子类对象；而具体是那种父类型不能确定，所以获取内容时只能通过 Object 类型来接收。\n示例代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class GenericityDownTest { // 泛型类基础使用测试 @Test public void test01() { List\u0026lt;Animal\u0026gt; animals = new ArrayList\u0026lt;\u0026gt;(); List\u0026lt;Cat\u0026gt; cats = new ArrayList\u0026lt;\u0026gt;(); List\u0026lt;MiniCat\u0026gt; miniCats = new ArrayList\u0026lt;\u0026gt;(); showAnimal(animals); showAnimal(cats); // showAnimal(miniCats); // 报错 } /** * 泛型下限通配符，调用该方法时，要求集合只能是Cat或Cat的父类类型 */ public void showAnimal(List\u0026lt;? super Cat\u0026gt; list) { // 对于指定下限的泛型集合来说，编译器只知道集合元素是下限的父类型，但具体是那种父类型不确定。因此，这种泛型集合能向其中添加元素 list.add(new Cat(\u0026#34;小白\u0026#34;, 3)); list.add(new MiniCat(\u0026#34;小黑\u0026#34;, 2, 1)); // 而指定泛型下限的集合元素，循环读取的内容只能通过 Object 类型来接收 for (Object o : list) { System.out.println(o); } } } 7.4. PECS（Producer Extends Consumer Super）原则 作为生产者提供数据（往外读取）时，适合用上界通配符（extends） 作为消费者消费数据（往里写入）时，适合用下界通配符（super） Notes: 在日常编码中，比较常用的是上界通配符（extends），用于限定泛型类型的父类。\n8. 类型擦除 8.1. 概念 Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数，会被编译器在编译的时候去掉。这个过程就称为类型擦除。\n如在代码中定义的 List\u0026lt;Object\u0026gt; 和 List\u0026lt;String\u0026gt; 等类型，在编译之后都会变成 List。JVM 看到的只是 List，而由泛型附加的类型信息对 JVM 来说是不可见的。类型擦除的基本过程也比较简单，首先是找到用来替换类型参数的具体类，这个具体类一般是 Object，如果指定了类型参数的上界的话，则使用这个上界的类型，最后把代码中的类型参数都替换成具体的类。\n8.2. 无限制类型擦除 8.3. 有限制类型擦除 8.4. 擦除方法中类型定义的参数 8.5. 桥接方法 如果类型擦除和多态性发生了冲突时，则在子类中生成桥方法解决。\n9. 集合中使用泛型-常用案例 9.1. 集合中泛型的使用 在创建集合的同时指定集合要存储的对象的数据类型。集合类\u0026lt;数据类型\u0026gt; 对象名 = new 集合类\u0026lt;数据类型\u0026gt;();\n9.2. 集合使用泛型的好处 强制只能存储一种数据类型对象，提高程序的安全性 将运行时错误转换为编译时错误。(及早发现错误) 省去类型强制转换的麻烦。 9.3. 集合使用泛型的注意事项 在指定泛型变量时，要么指定左边，要么两边都指定，但两边指定的类型一定要一致。 泛型中没有多态的概念，要么两边一致，要么指定左边(JDK1.7后可以不指定右边) 强烈推荐两边都指定，并且数据类型要一致。 10. 泛型的总结 泛型用来灵活地将数据类型应用到不同的类、方法、接口当中。将数据类型作为参数传递。 泛型是数据类型的一部分，将类名与泛型合并一起看做数据类型 1 2 ArrayList\u0026lt;String\u0026gt; array = new ArrayList\u0026lt;String\u0026gt;(); // ArrayList\u0026lt;String\u0026gt; 看作数据类型 泛型的定义：定义泛型可以在类中预知地使用未知的类型。 泛型的使用：一般在创建对象时，将未知的类型确定具体的类型。当没有指定泛型时，默认类型为 Object 类型。 1 2 3 4 ArrayList array = new ArrayList(); array.add(\u0026#34;abc\u0026#34;); array.add(1); // 由于在定义集合时没有指定泛型，add()方法的形参为 Object 类型，所以可以往集合中添加任意任意类型的数据(多态特点)。 可以声明带泛型的数组，但不能直接创建带泛型的数组，必须强制转型 1 2 3 4 5 ArrayList[] list = new ArrayList[5]; // 可以声明带泛型的数组，但能直接创建其对象 // ArrayList\u0026lt;String\u0026gt;[] listArr = new ArrayList\u0026lt;String\u0026gt;[5]; // 报错 // 但可以创建数组，强制类型 ArrayList\u0026lt;String\u0026gt;[] listArr = new ArrayList[5]; 可以通过 java.lang.reflect.Array 的静态方法 newInstance(Class\u0026lt;T\u0026gt;，int) 创建 T 类型的数组，但需要强制转型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class Fruit\u0026lt;T\u0026gt; { private T[] array; public Fruit(Class\u0026lt;T\u0026gt; clz, int length){ // 通过Array.newInstance创建泛型数组 array = (T[])Array.newInstance(clz, length); } /** * 填充数组 */ public void put(int index, T item) { array[index] = item; } /** * 获取数组元素 */ public T get(int index) { return array[index]; } public T[] getArray() { return array; } } ","permalink":"https://ktzxy.top/posts/jxb8sz061l/","summary":"Java基础 泛型","title":"Java基础 泛型"},{"content":"﻿\n版本控制 什么是版本控制 版本控制（Revision control）是一种在开发的过程中用于管理我们对文件、目录或工程等内容的修改历史，方便查看更改历史记录，备份以便恢复以前的版本的软件工程技术。\n实现跨区域多人协同开发 追踪和记载一个或者多个文件的历史记录 组织和保护你的源代码和文档 统计工作量 并行开发、提高开发效率 跟踪记录整个软件的开发过程 减轻开发人员的负担，节省时间，同时降低人为错误 简单说就是用于管理多人协同开发项目的技术。\n没有进行版本控制或者版本控制本身缺乏正确的流程管理，在软件开发过程中将会引入很多问题，如软件代码的一致性、软件内容的冗余、软件过程的事物性、软件开发过程中的并发性、软件源代码的安全性，以及软件的整合等问题。\n多人开发就必须要使用版本控制！\n常见的版本控制工具 主流的版本控制器有如下这些：\nGit SVN（Subversion） CVS（Concurrent Versions System） VSS（Micorosoft Visual SourceSafe） TFS（Team Foundation Server） Visual Studio Online 版本控制产品非常的多（Perforce、Rational ClearCase、RCS（GNU Revision Control System）、Serena Dimention、SVK、BitKeeper、Monotone、Bazaar、Mercurial、SourceGear Vault），现在影响力最大且使用最广泛的是Git与SVN\n为什么要使用版本控制?\n软件开发中采用版本控制系统是个明智的选择。 有了它你就可以将某个文件回溯到之前的状态,甚至将整个项目都回退到过去某个时间点的状态。 就算你乱来一气把整个项目中的文件改的改删的删，你也照样可以轻松恢复到原先的样子。 但额外增加的工作量却微乎其微。你可以比较文件的变化细节,查出最后是谁修改了哪个地方,从而找出导致怪异问题出现的原因，又是谁在何时报告了某个功能缺陷等等。\n版本控制分类\n密集中化的版本控制系统:\n集中化的版本控制系统诸如CVS, SVN以及Perforce等,都有一-个单一-的集中管理的服务器保存所有文件的修订版本，而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。多年以来,这已成为版本控制系统的标准做法，这种做法带来了许多好处,现在,每个人都可以在一定程度上看到项目中的其他人正在做些什么。而管理员也可以轻松掌控每个开发者的权限，并且管理一个集中化的版本控制系统;要远比在各 个客户端上维护本地数据库来得轻松容易。事分两面，有好有坏。这么做最显而易见的缺点是中央服务器的单点故障。如果服务器宕机- -小时，那么在这- -小时内，谁都无法提交更新,也就无法协同工作。\n密分布式的版本控制系统\n由于上面集中化版本控制系统的那些缺点，于是分布式版本控制系统面世了- 在这类系统中，像Git, BitKeeper等,==客户端并不只提取最新版本的文件快照，而是把代码仓库完整地镜像下来。==\n更进-步，许多这类系统都可以指定和若干不同的远端代码仓库进行交互。这样,你就可以在同一一个项目中分别和不同工作小组的人相互协作。\n分布式的版本控制系统在管理项目时存放的不是项目版本与版本之间的差异.它存的是索引(所需磁盘空间很少所以每个客户端都可以放下整个项目的历史记录)\nGit与SVN的主要区别 SVN是集中式版本控制系统，版本库是集中放在中央服务器的，而工作的时候，用的都是自己的电脑，所以首先要从中央服务器得到最新的版本，然后工作，完成工作后，需要把自己做完的活推送到中央服务器。集中式版本控制系统是必须联网才能工作，对网络带宽要求较高。\nGit是分布式版本控制系统，没有中央服务器，每个人的电脑就是一个完整的版本库，工作的时候不需要联网了，因为版本都在自己电脑上。协同的方法是这样的：比如说自己在电脑上改了文件A，其他人也在电脑上改了文件A，这时，你们两之间只需把各自的修改推送给对方，就可以互相看到对方的修改了。Git可以直接看到更新了哪些代码和文件！\nGit是目前世界上最先进的分布式版本控制系统。\nGit结构 三个区域 Git本地有三个工作区域：工作目录（Working Directory）、暂存区(Stage/Index)、资源库(Repository或Git Directory)。如果在加上远程的git仓库(Remote Directory)就可以分为四个工作区域。文件在这四个区域之间的转换关系如下：\nWorkspace：工作区，就是你平时存放项目代码的地方 Index / Stage：暂存区，用于临时存放你的改动，事实上它只是一个文件，保存即将提交到文件列表信息 Repository：仓库区（或本地仓库），就是安全存放数据的位置，这里面有你提交到所有版本的数据。其中HEAD指向最新放入仓库的版本 Remote：远程仓库，托管代码的服务器，可以简单的认为是你项目组中的一台电脑用于远程数据交换 本地的三个区域确切的说应该是git仓库中HEAD指向的版本：\nDirectory：使用Git管理的一个目录，也就是一个仓库，包含我们的工作空间和Git的管理空间。 WorkSpace：需要通过Git进行版本控制的目录和文件，这些目录和文件组成了工作空间。 .git：存放Git管理信息的目录，初始化仓库的时候自动创建。 Index/Stage：暂存区，或者叫待提交更新区，在提交进入repo之前，我们可以把所有的更新放在暂存区。 Local Repo：本地仓库，一个存放在本地的版本库；HEAD会只是当前的开发分支（branch）。 Stash：隐藏，是一个工作状态保存栈，用于保存/恢复WorkSpace中的临时状态。 代码托管中心 我们已经有了本地库，本地库可以帮我们进行版本控制，为什么还需要代码托管中心呢?\n它的任务是帮我们维护远程库,\n下面说一下本地库和远程库的交互方式， 也分为两种:\n(1)团队内部协作\n(2)跨团队协作\n托管中心种类:\n​\t局域网环境下:可以搭建 GitLab服务器作为代码托管中心，GitLab可以自己去搭建 ​\t外网环境下:可以由GitHub或者Gitee作为代码托管中心，GitHub或者Gitee是现成的托管中心，不用自己去搭建\n工作流程 git的工作流程一般是这样的：\n１、在工作目录中添加、修改文件；\n２、将需要进行版本管理的文件放入暂存区域；\n３、将暂存区域的文件提交到git仓库。\n因此，git管理的文件有三种状态：已修改（modified）,已暂存（staged）,已提交(committed)\nGit下载安装 软件下载 打开 [git官网] https://git-scm.com/，下载git对应操作系统的版本。\n所有东西下载慢的话就可以去找镜像！\n官网下载太慢，我们可以使用淘宝镜像下载：http://npm.taobao.org/mirrors/git-for-windows/\n下载对应的版本即可安装！\n安装 启动Git 鼠标右键\u0026mdash;\u0026gt;点击Git Bash here，打开Git终端\nGit Bash： Unix与Linux风格的命令行，使用最多，推荐最多\nGit CMD： Windows风格的命令行\nGit GUI ：图形界面的Git，不建议初学者使用，尽量先熟悉常用命令\n查看Git配置 所有的配置文件，其实都保存在本地！\n1 git config -l #查看配置 查看不同级别的配置文件：\n1 2 3 4 #查看系统config git config --system --list　#查看当前用户（global）配置 git config --global --list 初始化本地仓库 创建一个文件夹，名为GitResp，作为本地仓库\n打开Git终端：\n点击Git Bash Here:\n再右键\u0026ndash;\u0026gt;选择options\n进入以后先对字体和编码进行设置：\n在Git中命令跟Linux是一样的：\n（1）查看git安装版本\n1 git --version (2) 清屏 1 clear (3)设置签名 ​ 设置用户名与邮箱\n1 2 git config --global user.name \u0026#34;XXX\u0026#34; #名称 git config --global user.email 1354353@qq.com #邮箱 初始化仓库操作\n用cd命令进入之前创建文件夹所在路径或者直接进入该文件夹右击打开Git Bash here\n执行下列命令\n1 git init .git文件隐藏\n查看.git下内容\n1 ll 注意事项：.git目录下的本地库相关的子目录和子文件不要删除和修改。\nGit常用命令 add和commit命令 1.先创建一个文件\n2.将文件提交到暂存区：\n1 git add demo.txt 3.将暂存区的内容提交到本地库：\n1 git commit -m \u0026#34;这是我提交的第一个文件 demo.txt\u0026#34; demo.txt 注意事项：\n​\t1.不放在本地仓库中的文件，git是不进行管理\n​\t2.即使放在本地仓库的文件。git也不管理，必须通过add，commit命令操作才可以将内容提交到本地库。\nstatus命令 1 git status #看的是工作区和暂存区的状态 创建一个文件，然后查看状态：\n然后将demo02.txt通过git add命令提交至：暂存区\n查看状态： 利用git commit 命令将文件提交至：本地库\n现在修改demo.txt文件中的内容，然后再查看状态：\n重新添加至：暂存区并查看状态： 然后将暂存区的文件提交至本地库，并查看状态：\nlog命令 1 2 3 git log #查看提交历史 git log -p -2 #-p 显示每次提交所引入的差异（按补丁的格式输出） -2 限制显示的日志条目数量 git log --stat #可以看到每次提交的简略统计信息 当历史记录过多的时候，查看日志的时候，有分页效果，分屏效果，一页展示不下\n下一页：空格\n上一页：b\n到页尾显示END\n退出：q\n日志展示方式：\n1.方式一：git log 分页\n2.方式二：git log \u0026ndash;pretty=oneline\n3.方式三：git log \u0026ndash;oneline\n4.方式四：git reflog\n多了信息：HEAD@{数字}\n这个数字的含义：指针回到当前这个历史版本需要走多少步\nreset命令 1 2 3 4 git reset --hard 索引 #前进或者后退历史版本 hard参数 #本地库的指针移动的同时，重置暂存区，重置工作区 mixed参数 #本地库的指针移动的同时，重置暂存区，但是工作区不动 soft参数 #本地库的指针移动的时候，暂存区，工作区都不动 删除/找回文件 1.新建一个demo3.txt文件\n2.添加到暂存区\n3.提交到本地库\n4.删除工作区中的Test2.txt\n5.删除操作同步到暂存区\n6.删除操作同步到本地库\n7.查看日志\n8.找回本地库中删除的文件，实际上就是将历史版本切换到刚才添加文件的那个版本即可：\n1.删除工作区数据：\n2.同步到缓存区：\n3.恢复暂存区中数据：\n上述命令相当于将2c0518a指针回滚了一次，下列命令也可以达到目的\ndiff命令 1 2 3 git diff [文件名] #将工作区中的文件和暂存区中文件进行比较 git diff #比较工作区中和暂存区中所有文件的差异 git diff [历史版本][文件名] #比较暂存区和工作区中的内容 1.先创建一个文件，编写内容，然后添加到暂存区，在提交到本地库：\n2.更改工作区中demo4.txt中内容，增加内容：\n导致：工作区 和 暂存区 不一致，比对：\n多个文件的比对\n更改工作区中demo3.txt中内容，增加内容：ccc，然后比对\n比较暂存区和工作区中的内容\n更改工作区中demo4.txt中内容，增加内容：ssss，然后添加到暂存区，然后比对：\n获取帮助 使用Git时需要获取帮助，有三种方法：\n1 git help 1 git help branch -h 打标签 列出标签\n1 git tag 创建标签 Git 支持两种标签：轻量标签（lightweight）与附注标签（annotated）。\n轻量标签很像一个不会改变的分支——它只是某个特定提交的引用。\n而附注标签是存储在 Git 数据库中的一个完整对象， 它们是可以被校验的，其中包含打标签者的名字、电子邮件地址、日期时间， 此外还有一个标签信息，并且可以使用 GNU Privacy Guard （GPG）签名并验证。\n附注标签 1 2 git tag -a v1.4 -m \u0026#34;my version 1.4\u0026#34; #-m 选项指定了一条将会存储在标签中的信息。 如果没有为附注标签指定一条信息，Git 会启动编辑器要求你输入信息。 轻量标签 1 git tag 标签名 后期打标签 你也可以对过去的提交打标签。\n1 git tag -a 标签名 索引 共享标签 默认情况下，git push 命令并不会传送标签到远程仓库服务器上。 在创建完标签后你必须显式地推送标签到共享服务器上。\n1 git push origin 标签名 如果想要一次性推送很多标签，也可以使用带有 --tags 选项的 git push 命令。 这将会把所有不在远程仓库服务器上的标签全部传送到那里。\n1 git push origin --tags 删除标签 1 2 3 4 git tag -d 标签名 #注意上述命令并不会从任何远程仓库中移除这个标签 git push origin --delete 标签名 #可以移除远程库标签 分支 什么是分支 在版本控制过程中，使用多条线同时推进多个任务。这里面说的多条线，就是多个分支。\n通过一张图展示 分支的好处 同时多个分支可以并行开发，互相不耽误，互相不影响，提高开发效率\n如果有一个分支功能开发失败，直接删除这个分支就可以了，不会对其它分支产生任何影响。\n操作分支 1.在工作区创建一个Test2.txt文件，编写内容，然后提交到暂存区，提交到本地库：\n2.查看分支\n3.创建分支\n再查看\n4.切换分支\n再查看\n冲突问题及解决 1.进入分支，增加内容，提交到本地库\n2.将分支切换到master,并查看分支\n1 git checkout -b 分支名 #创建分支并切换到该分支，相当于git branch和git checkout两个操作 然后在主分支下 加入内容：\n3.再次切换到branch01分支查看：\n4.将branch01分支 合并到 主分支：\n（1）进入主分支：\n（2）将branch01中的内容和主分支的内容进行合并：\n查看文件：出现冲突：\n解决：\n公司内部决定，或者认为决定，留下想要的即可：\n将工作区中内容添加到暂存区:\n然后进行commit命令提交\n远程管理 注册GIthub https://github.com/\n创建一个远程仓库 创建远程库地址别名 远程库的地址：\n点击进入\n在Git本地将地址保存，通过别名\n查看别名\n起别名：（按照需求起，这里为origin）\n再查看别名：\n推送操作 1 git push 远程库别名 推送分支 推送成功以后，查看自己的远程库：\n克隆操作 1.获取远程库地址\n2.任意盘下新建一个文件夹，用于存放克隆的文件,然后打开Git Bash Here进行克隆操作:\n3.进入文件夹，查看克隆到本地的文件\n克隆操作可以帮我们完成：\n（1）初始化本地库\n（2）将远程库内容完整的克隆到本地\n（3）替我们创建远程库的别名：\n邀请加入团队 1.创建一个文件，编辑内容，并更新本地库信息：\n发现push内容到远程仓库中并没有要求录入账号密码或者提示错误。\n原因：git使用的时候在本地有缓存：\n将缓存删除：\n重新测试，发现出错\n必须要加入团队：\n登录项目经理的账号，邀请普通成员：\n登录被邀请者的账号，接受邀请：（在地址栏录入邀请链接即可）\n再重新执行push操作，会发现成功推送到远程库：\n远程库修改的拉取操作 1.拉取操作pull操作，相当于fetch+merge\n2.项目经理先确认远程库内容是否更新了：\n3.项目经理进行拉取：\n（1）先是抓取操作：fetch：\n1 git fetch 远程库别名 远程库上对应的分支 在抓取操作执行后，只是将远程库的内容下载到本地，但是工作区中的文件并没有更新。工作区中还是原先的内容：\n抓取后可以去远程库看看内容是否正确：\n然后发现内容都正确，就可以进行合并了：\n（2）进行合并：merge：（合并之前切换回来）\n远程库的拉取可以直接利用pull命令来完成：\n1 git pull origin master fetch+merge \u0026mdash;\u0026gt;为了保险起见\npull \u0026mdash;\u0026ndash;\u0026gt;简单代码\n协同开发合作时冲突以及解决 1.向远程库推送数据：（项目经理）\n2.删除凭据，做拉取操作：（普通开发人员）\n到现在为止，现在远程合作没有任何问题。\n现在操作同一个文件的同一个位置的时候，就会引起冲突：\n3.再次做了推送操作：（普通开发人员）\n改动位置\n4.改动demo6.txt中内容，然后进行推送：（项目经理）\n==注意：所编辑的文件demo6.txt不在同一个文件夹下==\n发现推送失败！！！\n在冲突的情况下，先应该拉取下来，然后修改冲突，然后再推送到远程服务器：\n人为解决冲突：（仅留下需要的那个）\n解决完冲突以后，向服务器推送：\n1 2 3 git add demo6.txt git commit -m \u0026#34;解决了冲突\u0026#34; #注意在提交冲突时不可以带文件名，否则提交失败 git push origin master 跨团队合作 1.得到项目经理的远程库的地址：\n地址：https://github.com/solitude114/GitResp.git\n2.开发人员B进行fork操作：\n登录开发人员B账号：复制上面地址，然后点击其中的fork操作：\n3.然后就可以克隆到本地，并且进行修改：\n然后更改数据：添加到暂存区，然后提交到本地库，然后push到远程库：\n然后重新推送：\n4.进行pull request操作：\n5.项目经理进行审核操作：\n登录项目经理账号，然后审核：\n可以互相留言：\n登录开发人员B账号查看： 并回复：\n登录项目经理账号：\n确定通过以后，可以进行合并： 免密操作 1.进入用户的主目录中：\n2.执行命令，生成一个.ssh的目录：\n1 2 ssh-keygen -t rsa -C Github邮箱地址 然后三次回车确认默认值即可 发现.ssh目录下有俩个文件：\n3.打开id_rsa.pub文件，复制里面的内容：\n4.打开Github账号：\n5.生成密钥以后，就可以正常进行push操作了：\n对ssh远程地址起别名：\n展示别名：\n6.测验：\nssh方式的好处：不用每次都进行身份验证\n缺陷：只能针对一个账号\nIDEA集成Git 初始化-添加-提交 本地库的初始化操作:\n然后生成一个.git文件\n创建一个文件会自动提示是否添加到暂存区：\n将整个模块添加到暂存区：\n可以看到全部变成绿色：\n还可以提交到本地库：\n拉取-推送 因为他们是两个不同的项目，要把两个不同的项目合并，git需要添加一句代码，在git pull之后，这句代码是在git 2.9.2版本发生的,最新的版本需要添加 \u0026ndash;allow-unrelated-histories 告诉 git允许不相关历史合并。 假如我们的源是origin，分支是master，那么我们需要这样写 git pull origin master \u0026ndash;allow-unrelated-histories 这个方法只解决因为两个仓库有不同的开始点，也就是两个仓库没有共同的commit出现的无法提交。如果使用本文的方法还无法提交，需要看一下是不是发生了冲突，解决冲突再提交 push推送: git push -u origin master -f\n在IDEA中进行推送：\n也可以提交和推送一起执行：\n一般在开发中先pull操作，再push操作，不会直接进行push操作！！\n克隆 克隆到本地后：\n这个目录既变成了一个本地仓库，又变成了工作空间。\n冲突以及解决 1.在你push以后，有冲突的时候提示合并操作：\n2.合并即可\n如何避免冲突 1.团队开发的时候避免在一个文件中改代码\n2.在修改一个文件前，在push之前，先pull操作\n附加 忽略文件 一般我们总会有些文件无需纳入 Git 的管理，也不希望它们总出现在未跟踪文件列表。 通常都是些自动生成的文件，比如日志文件，或者编译过程中创建的临时文件等。 在这种情况下，我们可以创建一个名为 .gitignore 的文件，列出要忽略的文件的模式。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 cat .gitignore # 忽略所有的 .a 文件 *.a # 但跟踪所有的 lib.a，即便你在前面忽略了 .a 文件 !lib.a # 只忽略当前目录下的 TODO 文件，而不忽略 subdir/TODO /TODO # 忽略任何目录下名为 build 的文件夹 build/ # 忽略 doc/notes.txt，但不忽略 doc/server/arch.txt doc/*.txt # 忽略 doc/ 目录及其所有子目录下的 .pdf 文件 doc/**/*.pdf 文件 .gitignore 的格式规范如下：\n所有空行或者以 # 开头的行都会被 Git 忽略。\n可以使用标准的 glob 模式匹配，它会递归地应用在整个工作区中。\n匹配模式可以以（/）开头防止递归。\n匹配模式可以以（/）结尾指定目录。\n要忽略指定模式以外的文件或目录，可以在模式前加上叹号（!）取反。\n常见问题解决办法 Git Gui 查看分支历史中文乱码解决 在Git Gui工具栏上选择-编辑-选项。 选择：Default File Contents Encoding， 改为UTF-8。 Git warning: LF will be replaced by CRLF git config --global core.autocrlf false .git 文件太大时怎样处理 clone的时候，可以指定深度，如下，为1即表示只克隆最近一次commit.\ngit clone git://xxoo --depth 1 Git 生成变更历史文件 一些项目要求生成变更日志changelog，通过键入[1]：\ngit log \u0026gt; ChangeLog 利用Dropbox或者Google Drive建立私人仓库 cd /Dropbox/repo.git git init --bare cd ~/local_repository git remote add origin ~/Dropbox/repo.git git add . git commit -a -m \u0026quot;init repo.\u0026quot; git push origin master 参考文献及其它内容 Git 魔法（教程） Astral: 整理Starred项目 ","permalink":"https://ktzxy.top/posts/len2vr2ppm/","summary":"Git学习","title":"Git学习"},{"content":"1. 函数概述 在 MySQL 中，为了提高代码重用性和隐藏实现细节，MySQL 提供了很多内置函数。函数可以理解为封装好的模板程序或代码，可以直接被另一段程序调用的。函数主要可以分为以下几类：\n聚合函数 数学函数 字符串函数 日期函数 控制流函数 窗口函数 TODO: 实际使用时再追加\n2. 聚合函数 2.1. GROUP_CONCAT() 函数 GROUP_CONCAT() 首先根据 group by 指定的列进行分组，并且用分隔符分隔，将同一个分组中的非 NULL 的值连接起来，返回一个字符串结果。语法：\n1 group_concat([distinct] 字段名 [order by 排序字段 asc/desc] [separator \u0026#39;分隔符\u0026#39;]) 参数说明：\n使用 distinct 可以排除重复值 如果需要对结果中的值进行排序，可以使用 order by 子句 separator 是一个字符串值，指定返回结果拼接的分隔符，默认为逗号 示例：\n1 2 3 4 5 6 7 8 SELECT t.spec_id, GROUP_CONCAT(t.option_name) FROM tb_specification_option t GROUP BY t.spec_id; -- 将所有员工的名字合并成一行 select group_concat(emp_name) from emp; -- 指定分隔符合并 select department,group_concat(emp_name separator \u0026#39;;\u0026#39; ) from emp group by department; -- 指定排序方式和分隔符 select department,group_concat(emp_name order by salary desc separator \u0026#39;;\u0026#39; ) from emp group by department; 表数据：\n查询结果：\n3. 数学函数 函数 功能说明 CEIL(x) 向上取整 FLOOR(x) 向下取整 MOD(x,y) 返回x/y的模 RAND() 返回0~1内的随机数 ROUND(x,y) 求参数x的四舍五入的值，保留y位小数 Notes: MySQL中，数学函数如果发生错误，都会返回 NULL\n3.1. ABS 1 ABS(X) 返回 X 的绝对值。\n1 2 SELECT ABS(2); -- 返回 2 SELECT ABS(-32); -- 返回 32 3.2. CEIL 1 2 3 CEIL(X) -- 或者 CEILING(X) 返回大于或等于 x 的最小整数\n1 2 SELECT CEILING(1.23); -- 返回 2 SELECT CEIL(-1.23); -- 返回 -1 3.3. FLOOR 1 FLOOR(X) 返回不大于X的最大整数值\n1 2 SELECT FLOOR(1.23); -- 返回 1 SELECT FLOOR(-1.23); -- 返回 -2 3.4. GREATEST 1 GREATEST(value1, value2,...) 当有2或多个参数时，返回值列表中最大值\n1 2 SELECT GREATEST(34.0,3.0,5.0,767.0); -- 返回 767.0 SELECT GREATEST(\u0026#39;B\u0026#39;,\u0026#39;A\u0026#39;,\u0026#39;C\u0026#39;); -- 返回 C 3.5. LEAST 1 LEAST(value1, value2,...) 在有两个或多个参数的情况下， 返回值为最小(最小值)参数\n假如返回值被用在一个 INTEGER 语境中，或是所有参数均为整数值，则将其作为整数值进行比较。 假如返回值被用在一个 REAL 语境中，或所有参数均为实值，则将其作为实值进行比较。 假如任意一个参数是一个区分大小写的字符串，则将参数按照区分大小写的字符串进行比较。 在其它情况下，将参数作为区分大小写的字符串进行比较 1 2 3 SELECT LEAST(2,0); -- 返回 0 SELECT LEAST(34.0,3.0,5.0,767.0); -- 返回 3.0 SELECT LEAST(\u0026#39;B\u0026#39;,\u0026#39;A\u0026#39;,\u0026#39;C\u0026#39;); -- 返回 \u0026#39;A\u0026#39; 3.6. MAX / MIN 1 2 3 4 -- 获取最大值 MAX([DISTINCT] expr) -- 获取最小值 MIN([DISTINCT] expr) 返回字段 expression 中的最大值/最小值。\nMIN() 和 MAX() 的取值可以是一个字符串参数，返回的是最小或最大字符串值 DISTINCT关键词可以被用来查找 expr 的不同值的最小或最大值，可以省略不写 若找不到匹配的行，MIN()和MAX()返回 NULL 1 2 3 SELECT student_name, MIN(test_score), MAX(test_score) FROM student GROUP BY student_name; 3.7. MOD 1 2 3 4 5 MOD(N,M) -- 等价于 N % M -- 等价于 N MOD M 模操作。返回 N 被 M 除后的余数。\n1 2 3 4 SELECT MOD(234, 10); -- 返回 4 SELECT 253 % 7; -- 返回 1 SELECT MOD(29,9); -- 返回 2 SELECT 29 MOD 9; -- 返回 2 3.8. PI 1 PI() 返回圆周率(3.141593)，默认的显示小数位数是7位\n1 SELECT PI(); -- 返回 3.141593 3.9. POW 1 2 3 POW(X,Y) -- 或者 POWER(X,Y) 返回 X 的 Y 次方的结果值。\n1 2 SELECT POW(2,2); -- 返回 4 SELECT POW(2,-2); -- 返回 0.25 3.10. RAND 1 2 3 RAND() -- 指定一个整数参数 N ，则它被用作种子值，用来产生重复序列。 RAND(N) 返回一个随机浮点数，范围在0到1之间 (即其范围为 0 ≤ v ≤ 1.0)。若已指定一个整数参数 N，则它被用作种子值，用来产生重复序列。\n1 2 SELECT RAND(); -- 返回 0.9233482386203 (随机) SELECT RAND(20); -- 返回 0.15888261251047 (随机) 3.10.1. 使用示例：随机样本 注：在 ORDER BY 语句中，不能使用一个带有 RAND() 值的列，原因是 ORDER BY 会计算列的多重时间。然而可按照如下的随机顺序检索数据行，多用于测试：\n1 SELECT * FROM tbl_name ORDER BY RAND(); ORDER BY RAND()同 LIMIT 的结合从一组列中选择随机样本很有用。\n3.11. ROUND 1 2 ROUND(X) ROUND(X,D) 返回离 x 最接近的整数（遵循四舍五入）。有两个参数的情况时，返回 X ，其值保留到小数点后D位，而第D位的保留方式为四舍五入。若要接保留X值小数点左边的D 位，可将 D 设为负值。\n1 2 3 4 SELECT ROUND(-1.23); -- 返回 -1 SELECT ROUND(1.58); -- 返回 2 SELECT ROUND(1.298, 1); -- 返回 1.3 SELECT ROUND(23.298, -1); -- 返回 20 MYSQL 的随机抽取实现方法。如：要从 tablename 表中随机提取一条记录，一般的写法就是：\n1 SELECT * FROM tablename ORDER BY RAND() LIMIT 1 Tips: 此方式效率不高，不推荐使用。\n3.12. TRUNCATE 1 TRUNCATE(X,D) 返回数值 X 保留到小数点后 D 位的值。若 D 的值为 0，则结果不带有小数点或不带有小数部分。可以将 D 设为负数，若要截去(归零) X小数点左起第D位开始后面所有低位的值。（与 ROUND 最大的区别是不会进行四舍五入）\n1 2 3 4 5 6 SELECT TRUNCATE(1.223,1); -- 返回 1.2 SELECT TRUNCATE(1.999,0); -- 返回 1 SELECT TRUNCATE(-1.999,1); -- 返回 -1.9 SELECT TRUNCATE(122,-2); -- 返回 100 -- 通过 concat() 函数拼接两个字符串，实现将小数转换成百分比格式 concat(truncate(0.55754 * 100,2),\u0026#39;%\u0026#39;) -- 结果：55.75% 3.13. FORMAT 1 FORMAT(X, D) 将数值 X 设置为格式 '#,###,###.##'，以四舍五入的方式保留到小数点后 D 位，而返回结果为一个字符串。\n1 select FORMAT(123456789.12345, 2); -- 结果：123,456,789.12 注：使用 mysql format 函数的时候数字超过以前之后得到的查询结果会以逗号分割，此时如果程序接收还是数字类型将会转换异常。所以如果属性是数字类型那么就使用这两个函数\n1 2 select cast(字段, decimal(12,2)) convert(字段, decimal(12,2)) Notes: 经测试，如果FORMAT函数的参数X如果数据库表字段类型是Bigint或者其他数字类型，内容长度超过17位是不会出现精度丢失；如果参数X是字符类型（varchar）的话，使用FORMAT函数后，超出17位后会进行四舍五入，精度丢失。\n4. 字符串函数 MySQL 中内置了很多字符串函数，常用的几个如下：\n函数 功能说明 CONCAT(S1,S2,...Sn) 字符串拼接，将S1，S2，\u0026hellip; Sn拼接成一个字符串 LOWER(str) 将字符串str全部转为小写 UPPER(str) 将字符串str全部转为大写 LPAD(str,n,pad) 左填充，用字符串pad对str的左边进行填充，达到n个字符串长度 RPAD(str,n,pad) 右填充，用字符串pad对str的右边进行填充，达到n个字符串长度 TRIM(str) 去掉字符串头部和尾部的空格 SUBSTRING(str,start,len) 返回从字符串str从start位置起的len个长度的字符串 4.1. CHAR_LENGTH/CHARACTER_LENGTH 1 2 3 CHAR_LENGTH(str) -- 或 CHARACTER_LENGTH(str) 返回值为字符串 str 的长度，长度的单位为字符。\n1 2 SELECT CHAR_LENGTH(\u0026#34;RUNOOB\u0026#34;) -- 返回 6 SELECT CHARACTER_LENGTH(\u0026#34;RUNOOB\u0026#34;) -- 返回 6 4.2. CONCAT 1 CONCAT(str1, str2,...) 将参数列表中的字符串合并为一个字符串。如有任何一个参数为NULL ，则返回值为 NULL。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 mysql\u0026gt; SELECT CONCAT(\u0026#39;My\u0026#39;, \u0026#39;S\u0026#39;, \u0026#39;QL\u0026#39;); +-------------------------+ | CONCAT(\u0026#39;My\u0026#39;, \u0026#39;S\u0026#39;, \u0026#39;QL\u0026#39;) | +-------------------------+ | MySQL | +-------------------------+ mysql\u0026gt; SELECT CONCAT(\u0026#39;My\u0026#39;, NULL, \u0026#39;SQL\u0026#39;); +---------------------------+ | CONCAT(\u0026#39;My\u0026#39;, NULL, \u0026#39;SQL\u0026#39;) | +---------------------------+ | NULL | +---------------------------+ mysql\u0026gt; SELECT CONCAT(14.3); +--------------+ | CONCAT(14.3) | +--------------+ | 14.3 | +--------------+ 4.3. CONCAT_WS 1 CONCAT_WS(separator, str1, str2,...) 与CONCAT函数一样，用于拼接字符串，第一个参数是指定分隔符。分隔符的位置放在要连接的两个字符串之间。分隔符可以是一个字符串，也可以是其它参数。如果分隔符为 NULL，则结果为 NULL。\n1 2 3 SELECT CONCAT_WS(\u0026#39;,\u0026#39;,\u0026#39;First name\u0026#39;,\u0026#39;Second name\u0026#39;,\u0026#39;Last Name\u0026#39;); -- 返回 \u0026#39;First name,Second name,Last Name\u0026#39; SELECT CONCAT_WS(\u0026#39;,\u0026#39;,\u0026#39;First name\u0026#39;,NULL,\u0026#39;Last Name\u0026#39;); -- 返回 \u0026#39;First name,Last Name\u0026#39; SELECT CONCAT_WS(NULL,\u0026#39;First name\u0026#39;,\u0026#39;Second name\u0026#39;,\u0026#39;Last Name\u0026#39;); -- 返回 NULL 4.4. FIELD 1 FIELD(str, str1, str2, str3,...) 返回第一个字符串 str 在字符串列表(str1,str2,str3\u0026hellip;)中的位置。在找不到 str 的情况下，返回值为0。\n1 2 SELECT FIELD(\u0026#39;ej\u0026#39;, \u0026#39;Hej\u0026#39;, \u0026#39;ej\u0026#39;, \u0026#39;Heja\u0026#39;, \u0026#39;hej\u0026#39;, \u0026#39;foo\u0026#39;); -- 返回 2 SELECT FIELD(\u0026#39;fo\u0026#39;, \u0026#39;Hej\u0026#39;, \u0026#39;ej\u0026#39;, \u0026#39;Heja\u0026#39;, \u0026#39;hej\u0026#39;, \u0026#39;foo\u0026#39;); -- 返回 0 4.5. LTRIM 1 LTRIM(str) 去掉字符串 str 开始处的空格\n1 SELECT LTRIM(\u0026#39; barbar\u0026#39;); -- 返回 \u0026#39;barbar\u0026#39; 4.6. RTRIM 1 RTRIM(str) 去掉字符串 str 结尾处的空格。\n1 SELECT RTRIM(\u0026#39;barbar \u0026#39;); -- 返回 \u0026#39;barbar\u0026#39; 4.7. TRIM 1 TRIM(str) 去掉字符串 str 开始和结尾处的空格\n1 2 3 4 SELECT TRIM(\u0026#39; bar \u0026#39;); -- 返回 \u0026#39;bar\u0026#39; SELECT TRIM(LEADING \u0026#39;x\u0026#39; FROM \u0026#39;xxxbarxxx\u0026#39;); -- 返回 \u0026#39;barxxx\u0026#39; SELECT TRIM(BOTH \u0026#39;x\u0026#39; FROM \u0026#39;xxxbarxxx\u0026#39;); -- 返回 \u0026#39;bar\u0026#39; SELECT TRIM(TRAILING \u0026#39;xyz\u0026#39; FROM \u0026#39;barxxyz\u0026#39;); -- 返回 \u0026#39;barx\u0026#39; 4.8. SUBSTRING / SUBSTR / MID 1 2 3 4 5 SUBSTRING(str, pos, len) -- 或 SUBSTR(str, pos, len) -- 或 MID(str, pos, len) 返回从字符串 str 的 pos 位置截取长度为 len 的子字符串。\nNotes: len 参数可以忽略，即截图 pos 位置后全部内容。如果位数是负数 如-5则是从后倒数位数，到字符串结束或截取的长度\n1 2 3 4 5 6 SELECT SUBSTRING(\u0026#39;Quadratically\u0026#39;,5); -- 返回 \u0026#39;ratically\u0026#39; SELECT SUBSTRING(\u0026#39;foobarbar\u0026#39; FROM 4); -- 返回 \u0026#39;barbar\u0026#39; SELECT SUBSTRING(\u0026#39;Quadratically\u0026#39;,5,6); -- 返回 \u0026#39;ratica\u0026#39; SELECT SUBSTRING(\u0026#39;Sakila\u0026#39;, -3); -- 返回 \u0026#39;ila\u0026#39; SELECT SUBSTRING(\u0026#39;Sakila\u0026#39;, -5, 3); -- 返回 \u0026#39;aki\u0026#39; SELECT SUBSTRING(\u0026#39;Sakila\u0026#39; FROM -4 FOR 2); -- 返回 \u0026#39;ki\u0026#39; 4.9. SUBSTRING_INDEX 1 SELECT SUBSTRING_INDEX(str, delim, count); 根据指定的关键字符截取字符串，参数说明：\n参数str：被截取字段 参数delim：关键字（分隔符） 参数count：关键字出现的次数 1 SELECT SUBSTRING_INDEX(\u0026#39;www.moon.com\u0026#39;, \u0026#39;.\u0026#39;, 2); -- 返回 www.moon 如果在字符串中找不到 delim 参数指定的值，就返回整个字符串，count 是正数时，是截取第几次出现的关键字前的字符；count是负数时，是截取第几次出现的关键字后的字符\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 substring_index(\u0026#39;www.baidu.com\u0026#39;, \u0026#39;.\u0026#39;, 1); -- 结果是：www substring_index(\u0026#39;www.baidu.com\u0026#39;, \u0026#39;.\u0026#39;, 2); -- 结果是：www.baidu /* 也就是说，如果count是正数，那么就是从左往右数，第N个分隔符的左边的全部内容 相反，如果是负数，那么就是从右边开始数，第N个分隔符右边的所有内容，如： */ substring_index(\u0026#39;www.baidu.com\u0026#39;, \u0026#39;.\u0026#39;, -2); -- 结果为：baidu.com /* 如果要中间的的baidu？则有两个方向： 从右数第二个分隔符的右边全部，再从左数的第一个分隔符的左边： /* substring_index(substring_index(\u0026#39;www.baidu.com\u0026#39;, \u0026#39;.\u0026#39;, -2), ‘.’, 1); 4.9.1. 将字符串按指定的分隔符转成多行数据 SQL案例：\n1 2 3 4 5 6 7 8 9 -- 查询影片与主演（如果出演者Id定义为“A00001,A00002,..”这种形式，则前端查询列表则需要以下语句才能查询到对应的出演者信息） SELECT * FROM jactor ta WHERE ta.id in ( SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(t.actor_ids, \u0026#39;,\u0026#39;, b.help_topic_id + 1), \u0026#39;,\u0026#39;, -1) FROM movie t JOIN mysql.help_topic b ON b.help_topic_id \u0026lt; ( LENGTH(t.actor_ids) - LENGTH(REPLACE(t.actor_ids, \u0026#39;,\u0026#39;, \u0026#39;\u0026#39;)) + 1 ) WHERE t.id = \u0026#39;124\u0026#39;); on条件后面(length(t.actor_ids) - length(replace(t.actor_ids,',',''))+1)这个语法是得到被逗号分隔的字段一共有几个。为什么后面加1？可以这样理解，就是如果有3个逗号（分隔符），那个转换的内容就必然有4个，即确认了要转成的行数\n提示：mysql.help_topic这张表只用到了它的 help_topic_id，可以看到这个 help_topic_id 是从0开始一直连续的，join 这张表只是为了确定数据行数。现在假设 mysql.help_topic 只有5条数据，那么最多可转成5行数据，若果现在主演的名字有6个就不能用 mysql.help_topic 这张表了。由此看出我们完全可以找其他表来替代 mysql.help_topic，只要满足表的id是连续的，且数据条数超过了你要转换的行数即可。\n4.10. POSITION 1 POSITION(substr IN str) 从字符串 str 中获取 substr 的第一次出现的位置。如若 substr 不在 str 中，则返回值为0。\n1 2 SELECT POSITION(\u0026#39;b\u0026#39; in \u0026#39;abc\u0026#39;); -- 返回 2 SELECT POSITION(\u0026#39;e\u0026#39; in \u0026#39;abc\u0026#39;); -- 返回 0 4.11. REPLACE 1 REPLACE(str, from_str, to_str) 将字符串 str 中的字符串 to_str 替代成字符串 from_str，并返回\n1 SELECT REPLACE(\u0026#39;www.mysql.com\u0026#39;, \u0026#39;w\u0026#39;, \u0026#39;Ww\u0026#39;); -- 返回 \u0026#39;WwWwWw.mysql.com\u0026#39; 4.12. REVERSE 1 REVERSE(str) 返回字符串 str 的反转顺序\n1 SELECT REVERSE(\u0026#39;abc\u0026#39;); -- 返回 \u0026#39;cba\u0026#39; 4.13. LEFT 1 left(str, length) 从左开始截取字符串str，返回前面 length 个字符。\n1 SELECT LEFT(\u0026#39;foobarbar\u0026#39;, 4); -- 返回 \u0026#39;foob\u0026#39; 4.14. RIGHT 1 RIGHT(str,len) 从右开始截取字符串，返回字符串 str 的后 len 个字符。参数说明：\n参数str：被截取字段 参数length：截取长度 1 SELECT RIGHT(\u0026#39;foobarbar\u0026#39;, 4); -- 返回 \u0026#39;rbar\u0026#39; 4.15. STRCMP 1 STRCMP(expr1, expr2) 比较字符串 expr1 和 expr2，如果 expr1 与 expr2 相等返回0，如果 expr1 \u0026gt; expr2 返回 1，如果 expr1 \u0026lt; expr2 返回 -1\n1 2 3 SELECT STRCMP(\u0026#39;text\u0026#39;, \u0026#39;text2\u0026#39;); -- 返回 -1 SELECT STRCMP(\u0026#39;text2\u0026#39;, \u0026#39;text\u0026#39;); -- 返回 1 SELECT STRCMP(\u0026#39;text\u0026#39;, \u0026#39;text\u0026#39;); -- 返回 0 4.16. UCASE / UPPER 1 2 3 UPPER(str) -- 或 UCASE(str) 将字符串 str 所有字母转换为大写\n1 2 3 4 5 6 mysql\u0026gt; SELECT UPPER(\u0026#39;Hej\u0026#39;); +--------------+ | UPPER(\u0026#39;Hej\u0026#39;) | +--------------+ | HEJ | +--------------+ 4.17. LCASE / LOWER 1 2 3 LOWER(str) -- 或 LCASE(str) 将字符串 str 的所有字母变成小写字母\n1 2 3 4 5 6 mysql\u0026gt; SELECT LOWER(\u0026#39;QUADRATICALLY\u0026#39;); +------------------------+ | LOWER(\u0026#39;QUADRATICALLY\u0026#39;) | +------------------------+ | quadratically | +------------------------+ 4.18. LPAD / RPAD 前后补位 LPAD / RPAD 函数是用于对字段内容补位(补零为例)，语法结构：\n1 2 LPAD(str, len, padstr) RPAD(str, len, padstr) 参数说明：\nstr：需要补充的原数据 len：补充后字符的总位数 padstr：补充的内容 注意：如果字符串 str 的长度大于 len，则返回值被缩短成 len 长度的字符串返回。\n示例\n1 2 3 4 5 6 7 8 9 10 11 -- 前补内容(LPAD) select LPAD(uid, 8, 0),username from uc_members where uid = \u0026#39;100015\u0026#39;; -- 结果：uid: 00100015 username:MooN -- 后补内容(RPAD) select RPAD(uid, 8, 0),username from uc_members where uid = \u0026#39;100015\u0026#39;; -- 结果：uid: 10001500 username:MooN -- 原字符串被压缩 SELECT RPAD(\u0026#39;hi\u0026#39;,1,\u0026#39;?\u0026#39;); -- 返回结果：\u0026#39;h\u0026#39; -- 修改企业员工的工号统一为5位数，目前不足5位数的全部在前面补0。比如：1号员工的工号应该为00001。 update emp set workno = lpad(workno, 5, \u0026#39;0\u0026#39;); 4.19. LENGTH/CHAR_LENGTH/CHARACTER_LENGTH/BIT_LENGTH LENGTH(str)：用于函数获取某个字段数据长度，计算字段的长度规则是：一个汉字是算三个字符，一个数字或字母算一个字符 CHAR_LENGTH(str)：返回值为字符串str 的长度，长度的单位为字符。一个多字节字符算作一个单字符。对于一个包含五个二字节字符集，LENGTH返回值为10，而CHAR_LENGTH的返回值为5 CHARACTER_LENGTH(str)：与 CHAR_LENGTH 函数一样 BIT_LENGTH(str)：返回2进制长度 1 SELECT LENGTH(\u0026#39;www.moon.com\u0026#39;); -- 结果：12 4.19.1. 查询某一个字段是否包含中文字符 在使用 mysql 时候，某些字段会存储中文字符，或是包含中文字符的串，查询出来的方法是：\n1 SELECT col FROM table WHERE length(col) != char_length(col) 此现实原理：当字符集为 UTF-8，并且字符为中文时，length() 和 char_length() 两个函数返回的结果是不相同的。\nlength()：计算字段的长度，一个汉字算3个字符，一个数字或者字母按1个字符 char_length()：计算字段的长度，不论是汉字、数字还是字母，均按1个字符来算 5. 日期函数 函数 功能说明 CURDATE() 返回当前日期 CURTIME() 返回当前时间 NOW() 返回当前日期和时间 YEAR(date) 获取指定date的年份 MONTH(date) 获取指定date的月份 DAY(date) 获取指定date的日期 DATE_ADD(date, INTERVAL exprtype) 返回一个日期/时间值加上一个时间间隔expr后的时间值 DATEDIFF(date1,date2) 返回起始时间date1和结束时间date2之间的天数 5.1. CURDATE / CURRENT_DATE 1 2 CURDATE() CURRENT_DATE() 以'YYYY-MM-DD'的格式返回当前的日期，可以直接存到DATE字段中。\n1 2 3 4 5 6 7 8 9 10 11 12 13 mysql\u0026gt; select curdate(); +------------+ | curdate() | +------------+ | 2022-07-29 | +------------+ mysql\u0026gt; select CURRENT_DATE(); +-----------------+ | CURRENT_DATE() | +-----------------+ | 2022-07-29 | +-----------------+ 5.2. CURTIME 1 CURTIME() 以'HH:MM:SS'的格式返回当前的时间，可以直接存到TIME字段中。\n1 2 3 4 5 6 mysql\u0026gt; select curtime(); +-----------+ | curtime() | +-----------+ | 18:29:21 | +-----------+ 5.3. NOW 1 NOW() 以'YYYY-MM-DD HH:MM:SS'或'YYYYMMDDHHMMSS'的格式返回当前的日期和时间，可以直接存到DATETIME字段中。\n1 2 3 4 5 6 7 mysql\u0026gt; SELECT NOW(); +---------------------+ | NOW() | +---------------------+ | 2022-07-29 17:24:59 | +---------------------+ 1 row in set (0.02 sec) 5.4. YEAR 1 YEAR(date) 获取指定date的年份\n1 2 3 4 5 6 mysql\u0026gt; select YEAR(now()); +-------------+ | YEAR(now()) | +-------------+ | 2022 | +-------------+ 5.5. MONTH 1 MONTH(date) 获取指定date的月份\n1 2 3 4 5 6 mysql\u0026gt; select MONTH(now()); +--------------+ | MONTH(now()) | +--------------+ | 7 | +--------------+ 5.6. DAY 1 DAY(date) 获取指定date的日期\n1 2 3 4 5 6 mysql\u0026gt; select DAY(now()); +------------+ | DAY(now()) | +------------+ | 29 | +------------+ 5.7. DATE_ADD 1 DATE_ADD(date, INTERVAL exprtype) 返回一个日期/时间值加上一个时间间隔expr后的时间值\n1 2 3 4 5 6 mysql\u0026gt; select date_add(now(), INTERVAL 70 YEAR); +-----------------------------------+ | date_add(now(), INTERVAL 70 YEAR) | +-----------------------------------+ | 2092-07-29 18:35:38 | +-----------------------------------+ 5.8. DATEDIFF 1 DATEDIFF(date1,date2) 返回起始时间date1和结束时间date2之间的天数\n1 2 3 4 5 6 mysql\u0026gt; select datediff(\u0026#39;2021-10-01\u0026#39;, \u0026#39;2021-12-01\u0026#39;); +--------------------------------------+ | datediff(\u0026#39;2021-10-01\u0026#39;, \u0026#39;2021-12-01\u0026#39;) | +--------------------------------------+ | -61 | +--------------------------------------+ 5.9. 日期函数的对比 5.9.1. NOW 和 CURRENT_DATE 的区别 NOW 函数用于显示当前年份，月份，日期，小时，分钟和秒。 CURRENT_DATE 函数仅显示当前年份，月份和日期。 6. 控制流程函数 流程函数也是很常用的一类函数，可以在 SQL 语句中实现条件筛选，从而提高语句的效率。\n函数 功能说明 IF(value , t , f) 如果value为true，则返回t，否则返回f IFNULL(value1 , value2) 如果value1不为空，返回value1，否则返回value2 CASE WHEN [ val1 ] THEN [res1] … ELSE [ default ] END 如果val1为true，返回res1，\u0026hellip; 否则返回default默认值 CASE [ expr ] WHEN [ val1 ] THEN [res1] … ELSE [ default ] END 如果expr的值等于val1，返回res1，\u0026hellip; 否则返回default默认值 6.1. IF 1 IF(expr1, expr2, expr3) 如果expr1为true，则返回expr2，否则返回expr3\n1 2 3 SELECT IF(1\u0026gt;2,2,3); -- 返回 3 SELECT IF(1\u0026lt;2,\u0026#39;yes \u0026#39;,\u0026#39;no\u0026#39;); -- 返回 \u0026#39;yes\u0026#39; SELECT IF(STRCMP(\u0026#39;test\u0026#39;,\u0026#39;test1\u0026#39;),\u0026#39;no\u0026#39;,\u0026#39;yes\u0026#39;); -- 返回 \u0026#39;no\u0026#39; 6.2. IFNULL 1 IFNULL(expr1, expr2) IFNULL 函数作用是，假如 expr1 不为 NULL，则返回值为 expr1，否则其返回值为 expr2。IFNULL() 函数的返回值是数字或是字符串取决于实际的sql\n1 2 3 4 SELECT IFNULL(1, 0); -- 返回 1 SELECT IFNULL(NULL, 10); -- 返回 10 SELECT IFNULL(1/0, 10); -- 返回 10 SELECT IFNULL(1/0, \u0026#39;yes\u0026#39;); -- 返回 \u0026#39;yes\u0026#39; 6.3. ISNULL 1 ISNULL(expr) 如 expr 为 NULL，那么 ISNULL() 的返回值为 1，否则返回值为 0。\n1 2 SELECT ISNULL(1+1); -- 返回 0 SELECT ISNULL(1/0); -- 返回 1 6.4. NULLIF 1 2 3 NULLIF(expr1, expr2) -- 效果等价于 CASE WHEN expr1 = expr2 THEN NULL ELSE expr1 END 如果 expr1 = expr2，则返回 NULL，否则返回 expr1。等价于下面的：\n1 2 SELECT NULLIF(1,1); -- 返回 NULL SELECT NULLIF(1,2); -- 返回 1 6.5. case when 语句 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 方式1: CASE value WHEN compare-value1 THEN result1 [WHEN compare-value2 THEN result2] ... [WHEN compare-valueN THEN resultN] [ELSE result] END -- 方式2: CASE WHEN [condition1] THEN result1 [WHEN [condition2] THEN result2] ... [WHEN [conditionN] THEN resultN] [ELSE result] END CASE 表示函数开始，END 表示函数结束。\n方式1中，如果 value = compare-value1，则返回 result1，如果 value = compare-value2，则返回 result2，\u0026hellip;. 方式2中，如果 condition1 成立，则返回 result1，如果 condition2 成立，则返回 result2，\u0026hellip;. 如果没有匹配的结果值，则返回结果为 ELSE 后的结果，如果没有 ELSE 部分，则返回值为 NULL。而当有一个成立条件之后，后面的就不再执行了。\n一个CASE表达式的默认返回值类型是任何返回值的相容集合类型，但具体情况视其所在语境而定。如果用在字符串语境中，则返回结果味字符串。如果用在数字语境中，则返回结果为十进制值、实值或整数值。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 SELECT CASE 1 WHEN 1 THEN \u0026#39;one\u0026#39; WHEN 2 THEN \u0026#39;two\u0026#39; ELSE \u0026#39;more\u0026#39; END; -- 返回 \u0026#39;one\u0026#39; SELECT CASE WHEN 1\u0026gt;0 THEN \u0026#39;true\u0026#39; ELSE \u0026#39;false\u0026#39; END; -- 返回 \u0026#39;true\u0026#39; SELECT CASE BINARY \u0026#39;B\u0026#39; WHEN \u0026#39;a\u0026#39; THEN 1 WHEN \u0026#39;b\u0026#39; THEN 2 END; -- 返回 NULL -- 方式1：值比较方式 SELECT *, CASE payType WHEN 1 THEN \u0026#39;微信支付\u0026#39; WHEN 2 THEN \u0026#39;支付宝支付\u0026#39; WHEN 3 THEN \u0026#39;银行卡支付\u0026#39; ELSE \u0026#39;其他支付方式\u0026#39; END AS payTypeStr FROM orders; -- 方式2：条件表达式 SELECT *, CASE WHEN payType = 1 THEN \u0026#39;微信支付\u0026#39; WHEN payType = 2 THEN \u0026#39;支付宝支付\u0026#39; WHEN payType = 3 THEN \u0026#39;银行卡支付\u0026#39; ELSE \u0026#39;其他支付方式\u0026#39; END AS payTypeStr FROM orders; 7. 窗口函数/分析函数（Window Functions，8.0版本新增） 7.1. 简述 MySQL 8.0 新增窗口函数，又被称为开窗函数、分析函数，与 Oracle 窗口函数类似，属于 MySQL 的一大特点。它可以用来实现若干新的查询方式。窗口函数与 SUM()、COUNT() 这种分组聚合函数类似，在聚合函数后面加上over()就变成窗口函数了，在括号里可以加上 partition by 等分组关键字指定如何分组，窗口函数即便分组也不会将多行查询结果合并为一行，而是将结果放回多行当中，即窗口函数不需要再使用GROUP BY。\n非聚合窗口函数是相对于聚合函数来说的，区别如下：\n聚合函数是对一组数据计算后返回单个值（即分组） 非聚合函数一次只会处理一行数据。 窗口聚合函数在行记录上计算某个字段的结果时，可将窗口范围内的数据输入到聚合函数中，并不改变行数。 7.2. 窗口函数分类 序号函数：ROW_NUMBER()、RANK()、DENSE_RANK() 分布函数：PERCENT_RANK()、CUME_DIST() 前后函数：LAG()、LEAD() 头尾函数：FIRST_VALUE()、LAST_VALUE() 其它函数：NTH_VALUE()、NTILE() 另外还有开窗聚合函数: SUM, AVG, MIN, MAX\n7.3. 窗口函数定义语法(通用) 语法结构：\n1 2 3 4 5 window_function (expr) OVER ( PARTITION BY ... ORDER BY ... frame_clause ) 参数解析：\nwindow_function 是窗口函数的名称； expr 是函数的参数，有些函数不需要参数； OVER 子句包含三个选项： PARTITION BY：分区选项。用于将数据行拆分成多个分区（组），它的作用类似于GROUP BY分组。如果省略了 PARTITION BY，所有的数据作为一个组进行计算 ORDER BY：排序选项。用于指定分区内的排序方式，与 ORDER BY 子句的作用类似 frame_clause：窗口大小选项。用于在当前分区内指定一个计算窗口，也就是一个与当前行相关的数据子集。 Notes: 在聚合函数后面加上 over() 就变成窗口函数了，后面可以不用再加 group by 制定分组，因为在 over 里的 partition 关键字指明了如何分组计算，这种可以保留原有表数据的结构，不会像分组聚合函数那样每组只返回一条数据\n7.4. 序号函数（ROW_NUMBER,RANK,DENSE_RANK） 序号函数有三个：ROW_NUMBER()、RANK()、DENSE_RANK()，可以用来实现分组排序，并添加序号。\n语法格式：\n1 2 3 4 row_number()|rank()|dense_rank() over ( partition by ... order by ... ) 示例：\n1 2 3 4 5 6 7 8 9 10 SELECT dname, ename, salary, -- row_number() 按指定的列表排序并添加序号，相同的值也按顺序给序号 row_number() over ( PARTITION BY dname ORDER BY salary DESC ) AS \u0026#39;rn1\u0026#39;, -- rank() 按指定的列表排序并添加序号，相同的值给相同的序号，后面的值接相同序号的个数+1 继续给序号 rank() over ( PARTITION BY dname ORDER BY salary DESC ) AS \u0026#39;rn2\u0026#39;, -- dense_rank() 按指定的列表排序并添加序号，相同的值给相同的序号，后面的值按序号+1 继续给序号 dense_rank() over ( PARTITION BY dname ORDER BY salary DESC ) AS \u0026#39;rn3\u0026#39; FROM employee; 使用子查询的方式，获取分组的前3的记录\n1 2 3 4 5 6 7 8 9 10 SELECT * FROM ( SELECT dname, ename, salary, dense_rank() over ( PARTITION BY dname ORDER BY salary DESC ) AS rn FROM employee ) t WHERE t.rn \u0026lt;= 3; 不加partition by表示全局排序\n1 2 3 4 5 select dname, ename, salary, dense_rank() over(order by salary desc) as rn from employee; 7.5. 开窗聚合函数（SUM,AVG,MIN,MAX） 在窗口中每条记录动态地应用聚合函数 SUM()、AVG()、MAX()、MIN()、COUNT()，可以动态计算在指定的窗口内的各种聚合函数值。\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 SELECT dname, ename, salary, sum( salary ) over ( PARTITION BY dname ORDER BY hiredate ) AS pv1 FROM employee; SELECT dname, ename, salary, sum( salary ) over ( PARTITION BY dname ORDER BY hiredate rows BETWEEN unbounded preceding AND current ROW ) AS c1 FROM employee; SELECT dname, ename, salary, sum( salary ) over ( PARTITION BY dname ORDER BY hiredate rows BETWEEN 3 preceding AND current ROW ) AS c1 FROM employee; SELECT dname, ename, salary, sum( salary ) over ( PARTITION BY dname ORDER BY hiredate rows BETWEEN 3 preceding AND 1 following ) AS c1 FROM employee; SELECT dname, ename, salary, sum( salary ) over ( PARTITION BY dname ORDER BY hiredate rows BETWEEN current ROW AND unbounded following ) AS c1 FROM employee; 7.6. 分布函数（CUME_DIST,PERCENT_RANK） 7.6.1. CUME_DIST CUME_DIST 函数用途：分组内小于、等于当前(rank值的行数/分组内总行数)\n示例应用场景：查询小于等于当前薪资（salary）的比例\n1 2 3 4 5 6 7 8 9 10 11 12 13 /* rn1: 没有partition,所有数据均为1组，总行数为12， 第一行：小于等于3000的行数为3，因此，3/12=0.25 第二行：小于等于4000的行数为5，因此，5/12=0.4166666666666667 rn2: 按照部门分组，dname=\u0026#39;研发部\u0026#39;的行数为6, 第一行：研发部小于等于3000的行数为1，因此，1/6=0.16666666666666666 */ SELECT dname, ename, salary, cume_dist() over ( ORDER BY salary ) AS rn1,-- 没有partition语句 所有的数据位于一组 cume_dist() over ( PARTITION BY dept ORDER BY salary ) AS rn2 FROM employee; 7.6.2. PERCENT_RANK PERCENT_RANK 函数用途：每行按照公式 (rank-1) / (rows-1) 进行计算。其中，rank为RANK()函数产生的序号，rows为当前窗口的记录总行数。\n此函数很少应用场景\n1 2 3 4 5 6 7 8 9 10 11 12 /* rn2: 第一行: (1 - 1) / (6 - 1) = 0 第二行: (1 - 1) / (6 - 1) = 0 第三行: (3 - 1) / (6 - 1) = 0.4 */ SELECT dname, ename, salary, rank() over ( PARTITION BY dname ORDER BY salary DESC ) AS rn, percent_rank() over ( PARTITION BY dname ORDER BY salary DESC ) AS rn2 FROM employee; 7.7. 前后函数（LAG,LEAD） 前后函数用途：返回位于当前行的前 n 行（LAG(expr, n)）或后 n 行（LEAD(expr, n)）的 expr 的值\n示例应用场景：查询前1名同学的成绩和当前同学成绩的差值\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- lag 函数的用法 /* last_1_time: 指定了往上第1行的值，default为\u0026#39;2000-01-01\u0026#39; 第一行，往上1行为null,因此取默认值 \u0026#39;2000-01-01\u0026#39; 第二行，往上1行值为第一行值，2021-11-01 第三行，往上1行值为第二行值，2021-11-02 last_2_time: 指定了往上第2行的值，为指定默认值 第一行，往上2行为null 第二行，往上2行为null 第四行，往上2行为第二行值，2021-11-01 第七行，往上2行为第五行值，2021-11-02 */ SELECT dname, ename, hiredate, salary, lag( hiredate, 1, \u0026#39;2000-01-01\u0026#39; ) over ( PARTITION BY dname ORDER BY hiredate ) AS last_1_time, lag( hiredate, 2 ) over ( PARTITION BY dname ORDER BY hiredate ) AS last_2_time FROM employee; 1 2 3 4 5 6 7 -- lead 函数的用法 SELECT dname, ename, hiredate, salary, lead( hiredate, 1, \u0026#39;2000-01-01\u0026#39; ) over ( PARTITION BY dname ORDER BY hiredate ) AS last_1_time, lead( hiredate, 2 ) over ( PARTITION BY dname ORDER BY hiredate ) AS last_2_time FROM employee; 7.8. 头尾函数（FIRST_VALUE,LAST_VALUE） 头尾函数用途：返回第一个（FIRST_VALUE(expr)）或最后一个（LAST_VALUE(expr)）expr 的值\n示例应用场景：截止到当前，按照日期排序查询第1个入职和最后1个入职员工的薪资\n1 2 3 4 5 6 7 -- 注意, 如果不指定ORDER BY，则进行排序混乱，会出现错误的结果 SELECT dname, ename, hiredate, salary, first_value( salary ) over ( PARTITION BY dname ORDER BY hiredate ) AS FIRST, last_value( salary ) over ( PARTITION BY dname ORDER BY hiredate ) AS last FROM employee; 7.9. 其他函数（NTH_VALUE,NTILE） 7.9.1. NTH_VALUE NTH_VALUE(expr,n) 函数用途：返回窗口中第n个expr的值。expr可以是表达式，也可以是列名\n示例应用场景：截止到当前薪资，显示每个员工的薪资中排名第2或者第3的薪资\n1 2 3 4 5 6 SELECT dname, ename, hiredate, salary, nth_value( salary, 2 ) over ( PARTITION BY dname ORDER BY hiredate ) AS second_score, nth_value( salary, 3 ) over ( PARTITION BY dname ORDER BY hiredate ) AS third_score FROM employee 7.9.2. NTILE NTILE(n) 函数用途：将分区中的有序数据分为n个等级，记录等级数\n应用场景：将每个部门员工按照入职日期分成3组\n1 2 3 4 5 SELECT dname, ename, hiredate, salary, ntile( 3 ) over ( PARTITION BY dname ORDER BY hiredate ) AS rn FROM employee; 8. 其他函数 8.1. IP 地址转化函数 IPV4 地址可以使用 INT UNSIGNED 类型进行存储，因为用 UNSINGED INT 存储 IP 地址占用 4 字节，CHAR(15) 则占用 15 字节。另外，计算机处理整数类型比字符串类型快。\nMySQL 提供函数 inet_ntoa 和 inet_aton 来对 ip 地址在 int 与 char 之间相互转化。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 mysql\u0026gt; SELECT INET_ATON( \u0026#39;192.168.172.3\u0026#39; ); +------------------------------+ | INET_ATON( \u0026#39;192.168.172.3\u0026#39; ) | +------------------------------+ | 3232279555 | +------------------------------+ mysql\u0026gt; SELECT INET_NTOA( 3232279555 ); +-------------------------+ | INET_NTOA( 3232279555 ) | +-------------------------+ | 192.168.172.3 | +-------------------------+ Notes: IPv6 地址目前没有转化函数，需要使用 DECIMAL 或两个 BIGINT 来存储。\n","permalink":"https://ktzxy.top/posts/c3sx17ejec/","summary":"MySQL 函数","title":"MySQL 函数"},{"content":"Day-14-JVM入门 JVM入门 面试常见：\n请你谈谈你对JVM的理解? java8虚拟机和之前的变化更新? 什么是OOM，什么是栈溢出StackOverFlowError? 怎么分析? JVM的常用调优参数有哪些? 内存快照如何抓取？怎么分析Dump文件？ 谈谈JVM中，类加载器你的认识？ 1.JVM的位置 三种JVM:\nSun公司：HotSpot 用的最多 BEA：JRockit IBM：J9VM 我们学习都是：HotSpot\n2.JVM的体系结构 jvm调优：99%都是在方法区和堆，大部分时间调堆。 JNI（java native interface）本地方法接口。 3.类加载器 作用：加载Class文件——如果new Student();（具体实例在堆里，引用变量名放栈里） 。 先来看看一个类加载到 JVM 的一个基本结构： 类是模板，对象是具体的，通过new来实例化对象。car1，car2，car3，名字在栈里面，真正的实例，具体的数据在堆里面，栈只是引用地址。 虚拟机自带的加载器 启动类（根）加载器 扩展类加载器 应用程序加载器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package github.JVM.Demo01; /** * * @create 2021-06-08 07:42 */ public class Test01 { public static void main(String[] args) { Test01 test01 = new Test01(); Test01 test02 = new Test01(); Test01 test03 = new Test01(); System.out.println(test01.hashCode()); System.out.println(test02.hashCode()); System.out.println(test03.hashCode()); /* 1836019240 325040804 1173230247 */ Class\u0026lt;? extends Test01\u0026gt; aClass1 = test01.getClass(); ClassLoader classLoader = aClass1.getClassLoader(); System.out.println(classLoader); System.out.println(classLoader.getParent()); System.out.println(classLoader.getParent().getParent()); /* sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@330bedb4 null */ Class\u0026lt;? extends Test01\u0026gt; aClass2 = test02.getClass(); Class\u0026lt;? extends Test01\u0026gt; aClass3 = test03.getClass(); System.out.println(aClass1.hashCode()); System.out.println(aClass2.hashCode()); System.out.println(aClass3.hashCode()); /* 2133927002 2133927002 2133927002 */ } } 类加载器的分类\nBootstrap ClassLoader 启动类加载器 Extention ClassLoader 标准扩展类加载器 Application ClassLoader 应用类加载器 User ClassLoader 用户自定义类加载器 4.双亲委派机制 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package java.lang; /** * * @create 2021-06-08 08:06 */ public class String { /* 双亲委派机制:安全 1.APP--\u0026gt;EXC--\u0026gt;BOOT(最终执行) BOOT EXC APP */ public String toString() { return \u0026#34;Hello\u0026#34;; } public static void main(String[] args) { String s = new String(); System.out.println(s.getClass()); s.toString(); } /* 1.类加载器收到类加载的请求 2.将这个请求向上委托给父类加载器去完成，一直向上委托，知道启动类加载 3.启动加载器检查是否能够加载当前这个类，能加载就结束，使用当前的加载器，否则，抛出异常，适知子加载器进行加载 4.重复步骤3 */ } idea报了一个错误： 这是因为，在运行一个类之前，首先会在应用程序加载器(APP)中找，如果APP中有这个类，继续向上在扩展类加载器EXC中找，然后再向上，在启动类( 根 )加载器BOOT中找。如果在BOOT中有这个类的话，最终执行的就是根加载器中的。如果BOOT中没有的话，就会倒找往回找。\n过程总结\n1.类加载器收到类加载的请求\n2.将这个请求向上委托给父类加载器去完成，一直向上委托，直到启动类加载器\n3.启动类加载器检查是否能够加载当前这个类，能加载就结束，使用当前的加载器，否则，抛出异常，一层一层向下，通知子加载器进行加载\n4.重复步骤3\n关于双亲委派机制的博客：\n你确定你真的理解“双亲委派“了吗？！\n面试官：java双亲委派机制及作用\n==概念==：当某个类加载器需要加载某个.class文件时，它首先把这个任务委托给他的上级类加载器，递归这个操作，如果上级的类加载器没有加载，自己才会去加载这个类。\n==例子==：当一个Hello.class这样的文件要被加载时。不考虑我们自定义类加载器，首先会在AppClassLoader中检查是否加载过，如果有那就无需再加载了。如果没有，那么会拿到父加载器，然后调用父加载器的loadClass方法。父类中同理也会先检查自己是否已经加载过，如果没有再往上。注意这个类似递归的过程，直到到达Bootstrap classLoader之前，都是在检查是否加载过，并不会选择自己去加载。直到BootstrapClassLoader，已经没有父加载器了，这时候开始考虑自己是否能加载了，如果自己无法加载，会下沉到子加载器去加载，一直到最底层，如果没有任何加载器能加载，就会抛出ClassNotFoundException。\n==作用==：\n防止重复加载同一个.class。通过委托去向上面问一问，加载过了，就不用再加载一遍。保证数据安全。 保证核心.class不能被篡改。通过委托方式，不会去篡改核心.class，即使篡改也不会去加载，即使加载也不会是同一个.class对象了。不同的加载器加载同一个.class也不是同一个Class对象。这样保证了Class执行安全。 比如：如果有人想替换系统级别的类：String.java。篡改它的实现，在这种机制下这些系统的类已经被Bootstrap classLoader加载过了（为什么？因为当一个类需要加载的时候，最先去尝试加载的就是BootstrapClassLoader），所以其他类加载器并没有机会再去加载，从一定程度上防止了危险代码的植入。\n5.沙箱安全机制 Java安全模型的核心就是Java沙箱(sandbox)，什么是沙箱?沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中，并且严格限制代码对本地系统资源访问，通过这样的措施来保证对代码的有效隔离，防止对本地系统造成破坏。沙箱**主要限制系统资源访问**，那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一样。 所有的Java程序运行都可以指定沙箱，可以定制安全策略。 在]ava中将执行程序分成本地代码和远程代码两种，本地代码默认视为可信任的，而远程代码则被看作是不受信的。对于授信的本地代码，可以访问一切本地资源。而对于非授信的远程代码在早期的ava实现中，安全依赖于沙箱(Sandbox)机制。如下图所示JDK1.0安全模型。 但如此严格的安全机制也给程序的功能扩展带来障碍，比如当用户希望远程代码访问本地系统的文件时候，就无法实现。因此在后续的Java1.1 版本中，针对安全机制做了改进，增加了安全策略，允许用户指定代码对本地资源的访问权限。如下图所示JDK1.1安全模型。 在Java1.2版本中，再次改进了安全机制，增加了代码签名。不论本地代码或是远程代码，都会按照用户的安全策略设定，由类加载器加载到虚拟机中权限不同的运行空间，来实现差异化的代码执行权限控制。如下图所示JDK1.2安全模型。 当前最新的安全机制实现，则引入了域(Domain)的概念。虚拟机会把所有代码加载到不同的系统域和应用域，系统域部分专门负责与关键资源进行交互，而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain)，对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限，如下图所示最新的安全模型(jdk 1.6)。 组成沙箱的基本组件:\n字节码校验器(bytecode verifier)︰确保Java类文件遵循lava语言规范。这样可以帮助lava程序实现内存保护。但并不是所有的类文件都会经过字节码校验，比如核心类。\n类装载器(class loader) :其中类装载器在3个方面对Java沙箱起作用：\n。它防止恶意代码去干涉善意的代码; 。它守护了被信任的类库边界; 。它将代码归入保护域，确定了代码可以进行哪些操作。\n虚拟机为不同的类加载器载入的类提供不同的命名空间，命名空间由一系列唯一的名称组成，每一个被装载的类将有一个名字，这个命名空间是由Java虚拟机为每一个类装载器维护的，它们互相之间甚至不可见。 类装载器采用的机制是双亲委派模式。\n1.从最内层VM自带类加载器开始加载，外层恶意同名类得不到加载从而无法使用;\n2.由于严格通过包来区分了访问域，外层恶意的类通过内置代码也无法获得权限访问到内层类，破坏代码就自然无法生效。\n存取控制器(access controller)︰存取控制器可以控制核心API对操作系统的存取权限，而这个控制的策略设定，可以由用户指定。 安全管理器(security manager)︰是核心API和操作系统之间的主要接口。实现权限控制，比存取控制器优先级高。 安全软件包(security package) : java.security下的类和扩展包下的类，允许用户为自己的应用增加新的安全特性，包括: 安全提供者 消息摘要 数字签名 加密 鉴别 6.Native 编写一个多线程类启动。 1 2 3 public static void main(String[] args) { new Thread(()-\u0026gt;{ },\u0026#34;your thread name\u0026#34;).start(); } 点进去看start方法的源码： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public synchronized void start() { if (threadStatus != 0) throw new IllegalThreadStateException(); group.add(this); boolean started = false; try { start0();\t// 调用了一个start0方法 started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } // 这个Thread是一个类，这个方法定义在这里是不是很诡异！看这个关键字native； private native void start0(); 凡是带了native关键字的，说明 java的作用范围达不到，去调用底层C语言的库！\nJNI：Java Native Interface（Java本地方法接口）\n凡是带了native关键字的方法就会进入本地方法栈；\nNative Method Stack 本地方法栈\n本地接口的作用是融合不同的编程语言为Java所用，它的初衷是融合C/C++程序，Java在诞生的时候是C/C++横行的时候，想要立足，必须有调用C、C++的程序，于是就在内存中专门开辟了一块区域处理标记为native的代码，它的具体做法是 在 Native Method Stack 中登记native方法，在 ( ExecutionEngine ) 执行引擎执行的时候加载Native Libraies。\n目前该方法使用的越来越少了，除非是与硬件有关的应用，比如通过Java程序驱动打印机或者Java系统管理生产设备，在企业级应用中已经比较少见。因为现在的异构领域间通信很发达，比如可以使用Socket通信，也可以使用Web Service等等，不多做介绍！\n7.PC寄存器 **程序计数器：**Program Counter Register\n每个线程都有一个程序计数器，是线程私有的，就是一个指针，指向方法区中的方法字节码(用来存储指向像一条指令的地址，也即将要执行的指令代码)，在执行引擎读取下一条指令，是一个非常小的内存空间，几乎可以忽略不计。 8.方法区 Method Area 方法区\n方法区是被所有线程共享，所有字段和方法字节码，以及一些特殊方法，如构造函数，接口代码也在此定义，简单说，所有定义的方法的信息都保存在该区域，此区域属于共享区间;\n==静态变量、常量、类信息(构造方法、接口定义)、运行时的常量池存在方法区中，但是实例变量存在堆内存中，和方法区无关==。\nstatic ，final ，Class ，常量池~\n9.栈 在计算机流传有一句废话： 程序 = 算法 + 数据结构\n但是对于大部分同学都是： 程序 = 框架 + 业务逻辑\n栈：后进先出 / 先进后出\n队列：先进先出（FIFO : First Input First Output）\n栈管理程序运行\n存储一些基本类型的值、对象的引用、方法等。\n栈的优势是，存取速度比堆要快，仅次于寄存器，栈数据可以共享。\n思考：为什么main方法最后执行！为什么一个test() 方法执行完了，才会继续走main方法！\n喝多了吐就是栈，吃多了拉就是队列。\n说明：\n1、栈也叫栈内存，主管Java程序的运行，是在线程创建时创建，它的生命期是跟随线程的生命期，线程结束栈内存也就释放。\n2、对于栈来说不存在垃圾回收问题，只要线程一旦结束，该栈就Over，生命周期和线程一致，是线程私有的。\n3、方法自己调自己就会导致栈溢出（递归死循环测试）。\n栈里面会放什么东西那？\n8大基本类型 + 对象的引用 + 实例的方法 栈运行原理\nJava栈的组成元素——栈帧。\n栈帧是一种用于帮助虚拟机执行方法调用与方法执行的数据结构。他是独立于线程的，一个线程有自己的一个栈帧。封装了方法的局部变量表、动态链接信息、方法的返回地址以及操作数栈等信息。\n第一个方法从调用开始到执行完成，就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。\n当一个方法A被调用时就产生了一个栈帧F1，并被压入到栈中，A方法又调用了B方法，于是产生了栈帧F2也被压入栈中，B方法又调用了C方法，于是产生栈帧F3也被压入栈中\t执行完毕后，先弹出F3， 然后弹出F2，在弹出F1\u0026hellip;\u0026hellip;..\n遵循 “先进后出” / \u0026ldquo;后进先出\u0026rdquo; 的原则。 栈满了，抛出异常：stackOverflowError 对象实例化的过程。 10.三种JVM Sun公司HotSpot java Hotspot™64-Bit server vw (build 25.181-b13，mixed mode) BEA JRockit IBM 39 VM 我们学习都是：Hotspot 11.堆 Java7之前\nHeap 堆，一个JVM实例只存在一个堆内存，堆内存的大小是可以调节的。\n类加载器读取了类文件后，需要把类，方法，常变量放到堆内存中，保存所有引用类型的真实信息，以方便执行器执行。\n堆内存分为三部分：\n新生区 Young Generation Space Young/New\n养老区 Tenure generation space Old/Tenure\n永久区 Permanent Space Perm\n堆内存逻辑上分为三部分：新生，养老，永久（元空间 : JDK8 以后名称）。\n谁空谁是to\nGC垃圾回收主要是在新生区和养老区，又分为轻GC 和 重GC，如果内存不够，或者存在死循环，就会导致 在JDK8以后，永久存储区改了个名字(元空间)。 12.新生区、养老区 新生区是类诞生，成长，消亡的区域，一个类在这里产生，应用，最后被垃圾回收器收集，结束生命。\n新生区又分为两部分：伊甸区（Eden Space）和幸存者区（Survivor Space），所有的类都是在伊甸区被new出来的，幸存区有两个：0区 和 1区，当伊甸园的空间用完时，程序又需要创建对象，JVM的垃圾回收器将对伊甸园区进行垃圾回收（Minor GC）。将伊甸园中的剩余对象移动到幸存0区，若幸存0区也满了，再对该区进行垃圾回收，然后移动到1区，那如果1区也满了呢？（这里幸存0区和1区是一个互相交替的过程）再移动到养老区，若养老区也满了，那么这个时候将产生MajorGC（Full GC），进行养老区的内存清理，若养老区执行了Full GC后发现依然无法进行对象的保存，就会产生OOM异常 “OutOfMemoryError ”。如果出现 java.lang.OutOfMemoryError：java heap space异常，说明Java虚拟机的堆内存不够，原因如下：\n1、Java虚拟机的堆内存设置不够，可以通过参数 -Xms（初始值大小），-Xmx（最大大小）来调整。\n2、代码中创建了大量大对象，并且长时间不能被垃圾收集器收集（存在被引用）或者死循环。\n13.永久区（Perm） 永久存储区是一个常驻内存区域，用于存放JDK自身所携带的Class，Interface的元数据，也就是说它存储的是运行环境必须的类信息，被装载进此区域的数据是不会被垃圾回收器回收掉的，关闭JVM才会释放此区域所占用的内存。 如果出现 java.lang.OutOfMemoryError：PermGen space，说明是 Java虚拟机对永久代Perm内存设置不够。一般出现这种情况，都是程序启动需要加载大量的第三方jar包， 例如：在一个Tomcat下部署了太多的应用。或者大量动态反射生成的类不断被加载，最终导致Perm区被占满。 注意：\nJDK1.6之前： 有永久代，常量池1.6在方法区； JDK1.7： 有永久代，但是已经逐步 “去永久代”，常量池1.7在堆； JDK1.8及之后：无永久代，常量池1.8在元空间。 熟悉三区结构后方可学习JVM垃圾回收机制\n实际而言，方法区（Method Area）和堆一样，是各个线程共享的内存区域，它用于存储虚拟机加载的：类信息+普通常量+静态常量+编译器编译后的代码，虽然JVM规范将方法区描述为堆的一个逻辑部分，但它却还有一个别名，叫做Non-Heap（非堆），目的就是要和堆分开。\n对于HotSpot虚拟机，很多开发者习惯将方法区称之为 “永久代（Parmanent Gen）”，但严格本质上说两者不同，或者说使用永久代实现方法区而已，永久代是方法区（相当于是一个接口interface）的一个实现，Jdk1.7的版本中，已经将原本放在永久代的字符串常量池移走。\n常量池（Constant Pool）是方法区的一部分，Class文件除了有类的版本，字段，方法，接口描述信息外，还有一项信息就是常量池，这部分内容将在类加载后进入方法区的运行时常量池中存放！\n14.堆内存调优 ==-Xms==：设置初始分配大小，默认为物理内存的 “1/64”。 ==-Xmx==：最大分配内存，默认为物理内存的 “1/4”。 ==-XX:+PrintGCDetails==：输出详细的GC处理日志。 测试1\n代码测试\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Demo01 { public static void main(String[] args) { // 返回虚拟机试图使用的最大内存 long max = Runtime.getRuntime().maxMemory(); // 字节：1024*1024 // 返回jvm的总内存 long total = Runtime.getRuntime().totalMemory(); System.out.println(\u0026#34;max=\u0026#34; + max + \u0026#34;字节\\t\u0026#34; + (max/(double)1024/1024) + \u0026#34;MB\u0026#34;); System.out.println(\u0026#34;total=\u0026#34; + total + \u0026#34;字节\\t\u0026#34; + (total/(double)1024/1024) + \u0026#34;MB\u0026#34;); // 默认情况下:分配的总内存是电脑内存的1/4,初始化的内存是电脑的1/64 } } IDEA中进行VM调优参数设置，然后启动。 发现，默认的情况下分配的内存是总内存的 1/4，而初始化的内存为 1/64 ！ 1 -Xms1024m -Xmx1024m -XX:+PrintGCDetails VM参数调优：把初始内存，和总内存都调为 1024M，运行，查看结果！ 来大概计算分析一下！ 再次证明：元空间并不在虚拟机中，而是使用本地内存。 测试2\n代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package github.JVM.Demo02; import java.util.Random; /** * * @create 2021-06-08 10:22 */ public class Demo02 { public static void main(String[] args) { String str = \u0026#34;suneiLY\u0026#34;; while (true) { str += str + new Random().nextInt(88888888) + new Random().nextInt(999999999); } } } vm参数： 1 -Xms8m -Xmx8m -XX:+PrintGCDetails 测试，查看结果！ 这是一个young 区域撑爆的JAVA 内存日志，其中 PSYoungGen 表示 youngGen分区的变化1536k 表示 GC 之前的大小。\n488k 表示GC 之后的大小。\n整个Young区域的大小从 1536K 到 672K , young代的总大小为 7680K。\nuser – 总计本次 GC 总线程所占用的总 CPU 时间。\nsys – OS 调用 or 等待系统时间。\nreal – 应用暂停时间。\n如果GC 线程是 Serial Garbage Collector 串行搜集器的方式的话（只有一条GC线程,）， real time 等于user 和 system 时间之和。\n通过日志发现Young的区域到最后 GC 之前后都是0，old 区域 无法释放，最后报堆溢出错误。\n其他文章链接\n一文读懂 - 元空间和永久代 Java方法区、永久代、元空间、常量池详解 15.GC 1.Dump内存快照 在运行java程序的时候，有时候想测试运行时占用内存情况，这时候就需要使用测试工具查看了。在eclipse里面有 **Eclipse Memory Analyzer tool(MAT)**插件可以测试，而在idea中也有这么一个插件，就是**JProfiler**，一款性能瓶颈分析工具！ 作用：\n分析Dump文件，快速定位内存泄漏；\n获得堆中对象的统计数据\n获得对象相互引用的关系\n采用树形展现对象间相互引用的情况\n安装JProﬁler\nIDEA插件安装 安装JProﬁler监控软件 下载地址：https://www.ej-technologies.com/download/jproﬁler/version_92 下载完双击运行，选择自定义目录安装，点击Next。 注意：安装路径，建议选择一个文件名中没有中文，没有空格的路径 ，否则识别不了。然后一直点Next。 注册 1 2 3 4 5 6 // 注册码仅供大家参考 L-Larry_Lau@163.com#23874-hrwpdp1sh1wrn#0620 L-Larry_Lau@163.com#36573-fdkscp15axjj6#25257 L-Larry_Lau@163.com#5481-ucjn4a16rvd98#6038 L-Larry_Lau@163.com#99016-hli5ay1ylizjj#27215 L-Larry_Lau@163.com#40775-3wle0g1uin5c1#0674 配置IDEA运行环境 Settings–Tools–JProﬂier–JProﬂier executable选择JProﬁle安装可执行文件。（如果系统只装了一个版本， 启动IDEA时会默认选择）保存。 代码测试： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package github.JVM.Demo02; import java.util.ArrayList; /** * * @create 2021-06-08 11:13 */ public class Demo03 { byte[] byteArray = new byte[1*1024*1024]; // 1M = 1024K public static void main(String[] args) { ArrayList\u0026lt;Demo03\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); int count = 0; try { while (true) { list.add(new Demo03()); // 问题所在 count = count + 1; } } catch (Error e) { System.out.println(\u0026#34;count:\u0026#34; + count); e.printStackTrace(); } } } vm参数 ： -Xms1m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError 寻找文件：\n使用 Jproﬁler 工具分析查看\n双击这个文件默认使用 Jproﬁler 进行 Open大的对象！\n从软件开发的角度上，dump文件就是当程序产生异常时，用来记录当时的程序状态信息（例如堆栈的状态），用于程序开发定位问题。 2.GC四大算法 1.引用计数法 每个对象有一个引用计数器，当对象被引用一次则计数器加1，当对象引用失效一次，则计数器减1，对于计数器为0的对象意味着是垃圾对象，可以被GC回收。\n目前虚拟机基本都是采用可达性算法，从GC Roots 作为起点开始搜索，那么整个连通图中的对象边都是活对象，对于GC Roots 无法到达的对象变成了垃圾回收对象，随时可被GC回收。\n2.复制算法 年轻代中使用的是Minor GC，采用的就是复制算法（Copying）。 什么是复制算法？\nMinor GC 会把Eden中的所有活的对象都移到Survivor区域中，如果Survivor区中放不下，那么剩下的活的对象就被移动到Old generation中，也就是说，一旦收集后，Eden就是变成空的了\n当对象在Eden（包括一个Survivor区域，这里假设是From区域）出生后，在经过一次Minor GC后，如果对象还存活，并且能够被另外一块Survivor区域所容纳 （上面已经假设为from区域，这里应为to区域，即to区域有足够的内存空间来存储Eden 和 From 区域中存活的对象），则使用复制算法将这些仍然还活着的对象复制到另外一块Survivor区域（即 to 区域）中，然后清理所使用过的Eden 以及Survivor 区域（即form区域），并且将这些对象的年龄设置为1，以后对象在Survivor区，每熬过一次MinorGC，就将这个对象的年龄 + 1，当这个对象的年龄达到某一个值的时候（默认是15岁，通过- XX:MaxTenuringThreshold 设定参数）这些对象就会成为老年代。\n-XX:MaxTenuringThreshold\t任期门槛=\u0026gt;设置对象在新生代中存活的次数\n面试题：如何判断哪个是to区呢？一句话：谁空谁是to\n原理解释：\n年轻代中的GC，主要是复制算法（Copying）\nHotSpot JVM 把年轻代分为了三部分：一个 Eden 区 和 2 个Survivor区（from区 和 to区）。默认比例为 8:1:1，一般情况下，新创建的对象都会被分配到Eden区（一些大对象特殊处理），这些对象经过第一次Minor GC后，如果仍然存活，将会被移到Survivor区，对象在Survivor中每熬过一次Minor GC ， 年龄就会增加1岁，当它的年龄增加到一定程度时，就会被移动到年老代中，因为年轻代中的对象基本上 都是朝生夕死，所以在年轻代的垃圾回收算法使用的是复制算法！复制算法的思想就是将内存分为两块，每次只用其中一块，当这一块内存用完，就将还活着的对象复制到另外一块上面。复制算法不会产 生内存碎片！\n在GC开始的时候，对象只会在Eden区和名为 “From” 的Survivor区，Survivor区“TO” 是空的，紧接着进行GC，Eden区中所有存活的对象都会被复制到 “To”，而在 “From” 区中，仍存活的对象会更具他们的年龄值来决定去向。 年龄达到一定值的对象会被移动到老年代中，没有达到阈值的对象会被复制到 “To 区域”，经过这次GC后，Eden区和From区已经被清空，这个时候， “From” 和 “To” 会交换他们的角色， 也就是新的 “To” 就是GC前的“From” ， 新的 “From” 就是上次GC前的 “To”。 不管怎样，都会保证名为To 的Survicor区域是空的。 Minor GC会一直重复这样的过程。直到 To 区 被填满 ，“To” 区被填满之后，会将所有的对象移动到老年代中。 因为Eden区对象一般存活率较低，一般的，使用两块10%的内存作为空闲和活动区域，而另外80%的内存，则是用来给新建对象分配内存的。一旦发生GC，将10%的from活动区间与另外80%中存活的Eden 对象转移到10%的to空闲区域，接下来，将之前的90%的内存，全部释放，以此类推；\n好处：没有内存碎片；坏处：浪费内存空间。\n劣势：\n复制算法它的缺点也是相当明显的。 1、他浪费了一半的内存，这太要命了。 2、如果对象的存活率很高，我们可以极端一点，假设是100%存活，那么我们需要将所有对象都复制一遍，并将所有引用地址重置一遍。复制这一工作所花费的时间，在对象存活率达到一定程度时，将会变的不可忽视，所以从以上描述不难看出。复制算法要想使用，最起码对象的存活率要非常低才行，而且 最重要的是，我们必须要克服50%的内存浪费。 标记清除（Mark-Sweep）\n回收时，对需要存活的对象进行标记；\n回收不是绿色的对象。\n当堆中的有效内存空间被耗尽的时候，就会停止整个程序（也被称为stop the world），然后进行两项工作，第一项则是标记，第二项则是清除。\n标记：从引用根节点开始标记所有被引用的对象，标记的过程其实就是遍历所有的GC Roots ，然后将所有GC Roots 可达的对象，标记为存活的对象。\n清除： 遍历整个堆，把未标记的对象清除。\n缺点：这个算法需要暂停整个应用，会产生内存碎片。两次扫描，严重浪费时间。\n用通俗的话解释一下 标记/清除算法，就是当程序运行期间，若可以使用的内存被耗尽的时候，GC线程就会被触发并将程序暂停，随后将依旧存活的对象标记一遍，最终再将堆中所有没被标记的对象全部清 除掉，接下来便让程序恢复运行。\n劣势：\n首先、它的缺点就是效率比较低（递归与全堆对象遍历），而且在进行GC的时候，需要停止应用 程序，这会导致用户体验非常差劲\n其次、主要的缺点则是这种方式清理出来的空闲内存是不连续的，这点不难理解，我们的死亡对象 都是随机的出现在内存的各个角落，现在把他们清除之后，内存的布局自然乱七八糟，而为了应付 这一点，JVM就不得不维持一个内存空间的空闲列表，这又是一种开销。而且在分配数组对象的时 候，寻找连续的内存空间会不太好找。\n3.标记压缩 标记整理说明：老年代一般是由标记清除或者是标记清除与标记整理的混合实现。 什么是标记压缩？\n原理：\n在整理压缩阶段，不再对标记的对象作回收，而是通过所有存活对象都像一端移动，然后直接清除边界以外的内存。可以看到，标记的存活对象将会被整理，按照内存地址依次排列，而未被标记的内存会被 清理掉，如此一来，当我们需要给新对象分配内存时，JVM只需要持有一个内存的起始地址即可，这比维护一个空闲列表显然少了许多开销。\n标记、整理算法 不仅可以弥补 标记、清除算法当中，内存区域分散的缺点，也消除了复制算法当中，内存减半的高额代价；\n4.标记清除压缩 先标记清除几次，再压缩。 3.总结 内存效率：复制算法 \u0026gt; 标记清除算法 \u0026gt; 标记压缩算法 （时间复杂度）；\n内存整齐度：复制算法 = 标记压缩算法 \u0026gt; 标记清除算法；\n内存利用率：标记压缩算法 = 标记清除算法 \u0026gt; 复制算法；\n可以看出，效率上来说，复制算法是当之无愧的老大，但是却浪费了太多内存，而为了尽量兼顾上面所 提到的三个指标，标记压缩算法相对来说更平滑一些 ， 但是效率上依然不尽如人意，它比复制算法多了一个标记的阶段，又比标记清除多了一个整理内存的过程。\n难道就没有一种最优算法吗？\n答案： 无，没有最好的算法，只有最合适的算法 。\t\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;\u0026gt; 分代收集算法\n年轻代：（Young Gen）\n年轻代特点是区域相对老年代较小，对象存活低。 这种情况复制算法的回收整理，速度是最快的。复制算法的效率只和当前存活对象大小有关，因而很适 用于年轻代的回收。而复制算法内存利用率不高的问题，通过hotspot中的两个survivor的设计得到缓解。 老年代：（Tenure Gen）\n老年代的特点是区域较大，对象存活率高！ 这种情况，存在大量存活率高的对象，复制算法明显变得不合适。一般是由标记清除或者是标记清除与标记整理的混合实现。Mark阶段的开销与存活对象的数量成正比，这点来说，对于老年代，标记清除或 者标记整理有一些不符，但可以通过多核多线程利用，对并发，并行的形式提标记效率。Sweep阶段的 开销与所管理里区域的大小相关，但Sweep “就地处决” 的 特点，回收的过程没有对象的移动。使其相对其他有对象移动步骤的回收算法，仍然是是效率最好的，但是需要解决内存碎片的问题。 16.JMM 什么是JMM？\nJMM：（java Memory Model的缩写） 他干嘛的？官方，其他人的博客，对应的视频！\n作用：缓存一致性协议，用于定义数据读写的规则(遵守，找到这个规则)。\nJMM定义了线程工作内存和主内存之间的抽象关系∶线程之间的共享变量存储在主内存(Main Memory)中，每个线程都有一个私有的本地内存（Local Memory)。\n解决共享对象可见性这个问题：volilate 它该如何学习？\nJMM：抽象的概念，理论。 JMM对这八种指令的使用，制定了如下规则： 不允许read和load、store和write操作之一单独出现。即使用了read必须load，使用了store必须write。 不允许线程丢弃他最近的assign操作，即工作变量的数据改变了之后，必须告知主存。 不允许一个线程将没有assign的数据从工作内存同步回主内存。 一个新的变量必须在主内存中诞生，不允许工作内存直接使用一个未被初始化的变量。就是怼变量实施use、store操作之前，必须经过assign和load操作。 一个变量同一时间只有一个线程能对其进行lock。多次lock后，必须执行相同次数的unlock才能解锁。 如果对一个变量进行lock操作，会清空所有工作内存中此变量的值，在执行引擎使用这个变量前，必须重新load或assign操作初始化变量的值。 如果一个变量没有被lock，就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量。 对一个变量进行unlock操作之前，必须把此变量同步回主内存。 JMM对这八种操作规则和对volatile的一些特殊规则就能确定哪里操作是线程安全，哪些操作是线程不安全的了。但是这些规则实在复杂，很难在实践中直接分析。所以一般我们也不会通过上述规则进行分析。更多的时候，使用java的happen-before规则来进行分析。\n","permalink":"https://ktzxy.top/posts/f2rec7hxpi/","summary":"Day 14 JVM入门","title":"Day 14 JVM入门"},{"content":"1. 标准 SQL 语言没有规范以下功能 ID自示增长 分页 函数 编程语言 服务端的数据软件 2. 数据库命名规范 所有数据库对象名称必须使用小写字母并用下划线分割； 所有数据库对象名称禁止使用mysql保留关键字（如果表名中包含关键字查询时，需要将其用单引号括起来）； 数据库对象的命名要能做到见名识意，并且最后不要超过32个字符； 临时库表必须以tmp_为前缀并以日期为后缀，备份表必须以bak_为前缀并以日期(时间戳)为后缀； 所有存储相同数据的列名和列类型必须一致（一般作为关联列，如果查询时关联列类型不一致会自动进行数据类型隐式转换，会造成列上的索 引失效，导致查询效率降低）。 3. SQL 语句规范 SQL 语句可以单行或多行书写，以分号结尾 可使用空格和缩进来增强语句的可读性 同样可以使用/**/的方式完成注释 MySQL 数据库的 SQL 语句不区分大小写，建议关键字使用大写，自定义的使用小写，例如： 1 SELECT * FROM user; 4. 命名规范 数据库、表、字段的命名要遵守可读性原则，尽可能少使用或者不使用缩写 表名、字段名必须使用小写字母或数字，禁止出现数字开头，禁止两个下划线中间只出现数字。 说明：MySQL在Windows下不区分大小写，但在Linux下默认是区分大小写。因此，数据库名、表名、字段名，都不允许出现任何大写字母，避免节外生枝。 表名使用单数形式。如：员工表使用 EMPLOYEE，而不要使用 EMPLOYEES 采用有意义的名字，一般不超过三个英文单词，单词之间使用下划线分隔 数据库、表、字段的命名禁用保留字，如desc、range、match之类 对象的名字应该能够描述它所表示的对象。 表的名称应该能够体现表中存储的数据内容，最好是遵循“业务名称_表的作用”； 对于存储过程存储过程应该能够体现存储过程的功能。 库名与应用名称尽量一致。 主键索引名为pk_字段名；唯一索引名为uk_字段名；普通索引名则为idx_字段名 表达是与否概念的字段，应该使用is_xxx的方式命名，数据类型是unsigned tinyint（1 表示是，0 表示否）。 5. 数据库基本设计规范 所有表必须使用Innodb存储引擎 没有特殊要求（即Innodb无法满足的功能如：列存储，存储空间数据等）的情况下，所有表必须使用Innodb存储引擎（mysql5.5之前默认使用Myisam，5.6以后默认的为Innodb）Innodb 支持事务，支持行级锁，更好的恢复性，高并发下性能更好。 数据库和表的字符集统一使用UTF8 兼容性更好，统一字符集可以避免由于字符集转换产生的乱码，不同的字符集进行比较前需要进行转换会造成索引失效。 所有表和字段都需要添加注释 使用comment从句添加表和列的备注 从一开始就进行数据字典的维护。 尽量控制单表数据量的大小，建议控制在500万以内 500万并不是MySQL数据库的限制，过大会造成修改表结构，备份，恢复都会有很大的问题。 可以用历史数据归档（应用于日志数据），分库分表（应用于业务数据）等手段来控制数据量大小。 谨慎使用MySQL分区表 分区表在物理上表现为多个文件，在逻辑上表现为一个表，谨慎选择分区键，跨分区查询效率可能更低 建议采用物理分表的方式管理大数据。 尽量做到冷热数据分离，减小表的宽度 MySQL限制每个表最多存储4096列，并且每一行数据的大小不能超过65535字节 减少磁盘IO，保证热数据的内存缓存命中率（表越宽，把表装载进内存缓冲池时所占用的内存也就越大，也会消耗更多的IO） 更有效的利用缓存，避免读入无用的冷数据 经常一起使用的列放到一个表中（避免更多的关联操作）。 禁止在表中建立预留字段 预留字段的命名很难做到见名识义 预留字段无法确认存储的数据类型，所以无法选择合适的类型 对预留字段类型的修改，会对表进行锁定。 禁止在数据库中存储图片，文件等大的二进制数据 通常文件很大，会短时间内造成数据量快速增长，数据库进行数据库读取时，通常会进行大量的随机IO操作，文件很大时，IO操作很耗时，通常存储于文件服务器，数据库只存储文件地址信息。 禁止在线上做数据库压力测试 禁止从开发环境，测试环境直接连接生成环境数据库 6. 数据库表字段类型设计规范 6.1. 基本原则 6.1.1. 优先选择符合存储需要的最小的数据类型（更小的通常更好） 原因：列的字段越大，建立索引时所需要的空间也就越大，这样一页中所能存储的索引节点的数量也就越少也越少，在遍历时所需要的IO次数也就越多， 索引的性能也就越差。\n一些优化的示例：\n将字符串转换成数字类型存储，如：将IP地址转换成整形数据。mysql提供了两个方法来处理ip地址： inet_aton：把ip转为无符号整形（4-8位） inet_ntoa：把整型的ip转成地址 插入数据前，先用inet_aton把ip地址转为整型，可以节省空间。显示数据时，使用inet_ntoa把整型的ip地址转为地址显示即可。 对于非负型的数据（如自增ID、整型IP）来说，要优先使用无符号整型来存储。因为，无符号相对于有符号可以多出一倍的存储空间 VARCHAR(N)中的N代表的是字符数，而不是字节数 使用UTF8存储255个汉字Varchar(255)=765个字节。过大的长度会消耗更多的内存 6.1.2. 简单的数据类型更好 简单数据类型的操作通常需要更少的CPU周期。例如：\n整型比字符操作代价更低，因为字符集和校对规则(排序规则)使字符比较比整型比较更复杂。 应该使用 MySQL 内建的类型而不是字符串来存储日期和时间 6.1.3. 尽量避免 NULL 尽可能把所有列定义为NOT NULL。原因如下：\n索引NULL列需要额外的空间来保存，所以要占用更多的空间； 进行比较和计算时要对NULL值做特别的处理。 6.2. 整数类型（int） MySQL存储整数的数据类型如下表：\n整数类型 存储空间（位） 字节数 TINYINT 8 1 SMALLINT 16 2 MEDIUMINT 24 3 INT 32 4 BIGINT 64 8 同时整数类型有可选的 UNSIGNED 属性，表示不允许负值可以使正数的上限提高一倍。如：TINYINT UNSIGNED可以存储的范围是0~255，而TINYINT的存储范围是-128~127。\n有符号和无符号类型使用相同的存储空间，并具有相同的性能，因此可以根据实际情况选择合适的类型。\nMySQL 可以为整数类型指定宽度，例如INT(11)，对大多数应用这是没有意义的，它不会限制值的合法范围，只是规定了MySQL的一些交互工具（例如 MySQL命令行客户端)用来显示字符的个数。对于存储和计算来说，INT(1)和INT(20)是相同的。\n在整数字段类型选择上，遵循着更小的通常更好的原则，在业务许可的情况下，尽量选择位数小的\n6.3. 实数类型 实数是带有小数部分的数字。MySQL 既支持精确浮点类型的存储DECIMAL类型，也支持不精确浮点类型存储FLOAT和DOUBLE类型。\nDECIMAL 类型用于存储精确的小数，本质上 MySQL 是以字符串形式存放的。所以CPU不支持对DECIMAL类型的直接计算，而MySQL会实现了DECIMAL的高精度计算。相对而言，CPU 直接支持原生浮点计算，所以浮点运算明显更快。 浮点类型在存储同样范围的值时，通常比DECIMAL使用更少的空间。FLOAT使用4个字节存储，DOUBLE使用8个字节，所以DOUBLE比FLOAT有更高的精度和更大的范围。 浮点和 DECIMAL 类型都可以指定精度。对于 DECIMAL 列，可以指定小数点前后所允许的最大位数。这会影响列的空间消耗。MySQL 5.0 和更高版本将数字打包保存到一个二进制字符串中（每 4 个字节存 9 个数字)。例如，DECIMAL(18,9)小数点两边将各存储 9 个数字，一共使用 9 个字节：小数点前的数字用 4 个字节，小数点后的数字用 4 个字节，小数点本身占 1 个字节。\n一般涉及财务相关的金额类数据必须使用decimal类型。因为Decimal类型为精准浮点数，在计算时不会丢失精度。占用空间由定义的宽度决定，每4个字节可以存储9位数字，并且小数点要占用一个字节。可用于存储比bigint更大的整型数据。MySQL 5.0 和更高版本中的DECIMAL类型允许最多65个数字。在精度不敏感和需要快速运算的时候，选择FLOAT和DOUBLE。\n实际项目运用中，如果在数据量比较大的而且要求精度时，可以考虑使用BIGINT代替DECIMAL，将需要存储的货币单位根据小数的位数乘以相应的倍数即可。假设要存储财务数据精确到万分之一分，则可以把所有金额乘以一百万，然后将结果存储在BIGINT里，这样可以同时避免浮点存储计算不精确和DECIMAL精确计算代价高的问题。\n6.4. 字段串类型 MySQL 支持多种字符串类型，包括VARCHAR和CHAR类型、BLOB和TEXT类型、ENUM（枚举）和SET类型。\n6.4.1. VARCHAR 和 CHAR 类型 VARCHAR 和 CHAR 是两种最主要的字符串类型。\nVARCHAR\nVARCHAR 类型用于存储可变长字符串，它比定长类型更节省空间，因为它仅使用必要的空间（例如，越短的字符串使用越少的空间)。\n在内部实现上，VARCHAR 需要使用 1 或 2 个额外字节记录字符串的长度，如果列的最大长度小于或等于 255 字节，则只使用 1 个字节表示，否则使用 2 个字节。\nVARCHAR 节省了存储空间，所以对性能也有帮助。但是，由于行是变长的，在 UPDATE 时新值比旧值长时，使行变得比原来更长，这就肯能导致需要做额外的工作。如果一个行占用的空间增长，并且在页内没有更多的空间可以存储，在这种情况下，MyISAM 会将行拆成不同的片段存储，InnoDB 则需要分裂页来使行可以放进页内。\nCHAR\nCHAR 类型是定长的，MySQL 总是根据定义的字符串长度分配足够的空间。当存储 CHAR 值时，MySQL 会删除所有的末尾空格，CHAR 值会根据需要采用空格进行填充以方便比较。\n适合使用VARCHAR的情况\n字符串列的最大长度比平均长度大很多。 列的更新很少； 使用了像 UTF-8 这样复杂的字符集，每个字符都使用不同的字节数进行存储。 适合使用CHAR的情况\n适合存储很短的字符串，或者所有值定长或都接近同一个长度。如存储密码MD5值，因为它是定长的 适合长度非常短的列。如CHAR(1)来存储只有 Y 和 N 的值，如果采用单字节字符集只需要一个字节，但是VARCHAR(1)却需要两个字节，因为还有一个记录长度的额外字节。 使用VARCHAR(5)和VARCHAR(200)存储\u0026rsquo;hello\u0026rsquo;在磁盘空间上开销是一样的。应该使用更短的列。最好的策略是只分配真正需要的空间。因为更长的列会消耗更多的内存，MySQL 通常会分配固定大小的内存块来保存内部值。尤其是使用内存临时表进行排序或操作时会特别糟糕。\n6.4.2. BLOB 和 TEXT 类型 BLOB 和 TEXT 都是为存储很大的数据而设计的字符串数据类型，分别采用二进制和字符方式存储。与其他类型不同，MySQL 把每个 BLOB 和 TEXT 值当作一个独立的对象处理，存储引擎在存储时通常会做特殊处理。当 BLOB 和 TEXT 值太大时，InnoDB 会使用专门的“外部”存储区域来进行存储，此时每个值在行内需要 1~4 个字节存储一个指针，然后在外部存储区域存储实际的值。\nBLOB 和 TEXT 之间的区别是 BLOB 类型存储的是二进制数据，没有排序规则或字符集，而 TEXT 类型有字符集和排序规则。\nBLOB和TEXT类型使用规范\nBLOB和TEXT值会引起一些性能问题，所以尽量避免使用BLOB和TEXT类型。最常见的TEXT类型可以存储64k的数据。 如必须使用此两种类型，建议把 BLOB 或 TEXT 的列分离到单独的表中。 Mysql内存临时表不支持TEXT、BLOB这样的大数据类型，如果查询中包含这样的数据，在排序等操作时，就不能使用内存临时表，必须使用磁盘临时表进行。 而且对于这种数据，Mysql还是要进行二次查询，会使sql性能变得很差，但是不是说一定不能使用这样的数据类型。 在不必要的时候避免检索大型的 BLOB 或 TEXT 值。避免在包含此两种类型的表使用select *查询，导致网络上传输大量的值。建议可以搜索索引列，决定需要的哪些数据行，然后从符合条件的数据行中检索 BLOB 或 TEXT 值； 可以使用合成的(Synthetic)索引来提高大文本字段(BLOB 或 TEXT)的查询性能。合成索引就是根据大文本字段的内容建立一个散列值，并把这个值存储在单独的数据列中，接下来就可以通过检索散列值找到数据行。注意这种技术只能用于精确匹配的查询（散列值对于类似“\u0026lt;”或“\u0026gt;=”等范围搜索操作符是没有用处的)。 TEXT或BLOB类型只能使用前缀索引。因为MySQL对索引字段长度是有限制的，所以TEXT类型只能使用前缀索引，并且TEXT列上是不能有默认值的。 6.5. ENUM 类型 枚举列可以把一些不重复的字符串存储成一个预定义的集合。MySQL 在存储枚举时非常紧凑，会根据列表值的数量压缩到一个或者两个字节中，MySQL 在内部会将每个值在列表中的位置保存为整数，这样的话可以让表的大小大为缩小。\n1 2 CREATE TABLE enum_test(e ENUM(\u0026#39; fish\u0026#39;, \u0026#39;apple\u0026#39;, \u0026#39;dog\u0026#39;) NOT NULL); INSERT INTO enum_test(e) VALUES(\u0026#39;fish\u0026#39;),(\u0026#39;dog\u0026#39;),(\u0026#39;apple\u0026#39;); 枚举类型的使用规范：\n一般是避免使用ENUM类型。因为修改ENUM值需要使用ALTER语句；ENUM类型的ORDER BY操作效率低，需要额外操作 必要时可使用枚举代替字符串。如果表中的字段的取值是固定几个字符串，可以使用枚举列代替常用的字符串类型。 禁止使用数值作为ENUM的枚举值。因为枚举列实际存储为整数，而不是字符串，所以不要使用数字作为ENUM枚举常量，这种双重性很容易导致混乱，例如ENUN('1','2''3')。 枚举字段是按照内部存储的整数而不是定义的字符串进行排序的，所以尽量按照需要的顺序来定义枚举列。 6.6. 日期和时间类型 MySQL 可以使用许多类型来保存日期和时间值，分别是：DATETIME、DATE、TIMESTAMP、YEAR、TIME。MySQL 能存储的最小时间粒度为秒。\n大部分时间类型都没有替代品，因此没有什么是最佳选择的问题。唯一就是DATETIME和TIMESTAMP比较相似，需要做些适当的选择\nTIMESTAMP（占用空间是4个字节），存储的时间范围1970-01-01 00:00:01 ~ 2038-01-19 03:14:07。TIMESTAMP显示的值也依赖于时区。从空间效率来说，TIMETAMP 比 DATETIME 更高。 DATETIME（占用空间是8个字节），存储的时间范围从 1001 年到 9999 年，精度为秒。它把日期和时间封装到格式为 YYYYMMDDHHMMSS 的整数中，与时区无关。 日期时间类型使用规范：\nTIMESTAMP 占用4字节和INT相同，但比INT可读性高。超出TIMESTAMP取值范围的使用DATETIME类型存储。 如果需要存储比秒更小粒度的日期和时间值，MySQL目前没有提供合适的数据类型，但是可以使用自定义的存储格式：可以使用 BIGINT 类型存储微秒级别的时间截，或者使用 DOUBLE 存储秒之后的小数部分。 不应该用字符串存储日期型的数据。 缺点1：无法用日期函数进行计算和比较 缺点2：用字符串存储日期要占用更多的空间 6.7. 其他规范 字段名称不能包含数据类型，不能使用关键字 建议使用 INT UNSIGNED 类型存储 IPV4 地址。用 UNSINGED INT 存储 IP 地址占用 4 字节，CHAR(15) 则占用 15 字节。另外，计算机处理整数类型比字符串类型快。 Tips: MySQL 提供函数 inet_ntoa 和 inet_aton 来对 ip 地址在 int 与 char 之间相互转化。IPv6 地址目前没有转化函数，需要使用 DECIMAL 或两个 BIGINT 来存储。\n7. 索引设计规范 7.1. 索引的数量 限制每张表上的索引数量，建议单张表索引不超过5个\n索引并不是越多越好！索引可以提高效率同样可以降低效率。 索引可以增加查询效率，但同样也会降低插入和更新的效率，甚至有些情况下会降低查询效率。 因为mysql优化器在选择如何优化查询时，会根据统一信息，对每一个可以用到的索引来进行评估，以生成出一个最好的执行计划，如果同时有很多个索引都可以用于查询，就会增加mysql优化器生成执行计划的时间，同样会降低查询性能。 7.2. 禁止给表中的每一列都建立单独的索引 5.6版本之前，一个sql只能使用到一个表中的一个索引，5.6 以后，虽然有了合并索引的优化方式，但是还是远远没有使用一个联合索引的查询方式好。 7.3. 主键索引的注意事项 Innodb 是一种索引组织表：数据的存储的逻辑顺序和索引的顺序是相同的。每个表都可以有多个索引，但是表的存储顺序只能有一种 Innodb 是按照主键索引的顺序来组织表的。\n不要使用更新频繁的列作为主键，不适用多列主键（相当于联合索引），不要使用UUID、MD5、HASH、字符串列作为主键（无法保证数据的顺序增长）。主键建议使用自增ID值。\n8. 数据库 SQL 开发规范 建议使用预编译语句进行数据库操作 预编译语句可以重复使用这些计划，减少SQL编译所需要的时间，还可以解决动态SQL所带来的 SQL 注入的问题 只传参数，比传递SQL语句更高效 相同语句可以一次解析，多次使用，提高处理效率。 避免数据类型的隐式转换 隐式转换会导致索引失效。如：select name,phone from customer where id = '111'; 充分利用表上已经存在的索引 避免使用双%号的查询条件。如 a like '%123%'（如果无前置%，只有后置%，是可以用到列上的索引的） 一个SQL只能利用到复合索引中的一列进行范围查询 如：有 a,b,c列的联合索引，在查询条件中有a列的范围查询，则在b,c列上的索引将不会被用到，在定义联合索引时，如果a列要用到范围查找的话，就要把a列放到联合索引的右侧。 使用left join或 not exists来优化not in操作，因为not in 也通常会使用索引失效。 数据库设计时，应该要对以后扩展进行考虑 程序连接不同的数据库使用不同的账号，进制跨库查询 为数据库迁移和分库分表留出余地 降低业务耦合度 避免权限过大而产生的安全风险 禁止使用 SELECT * 必须使用 SELECT \u0026lt;字段列表\u0026gt; 查询。原因如下： 消耗更多的CPU和IO以网络带宽资源 无法使用覆盖索引 可减少表结构变更带来的影响 禁止使用不含字段列表的INSERT语句 如：insert into values (\u0026lsquo;a\u0026rsquo;,\u0026lsquo;b\u0026rsquo;,\u0026lsquo;c\u0026rsquo;); 应使用insert into t(c1,c2,c3) values (\u0026lsquo;a\u0026rsquo;,\u0026lsquo;b\u0026rsquo;,\u0026lsquo;c\u0026rsquo;); 避免使用子查询，可以把子查询优化为join操作 通常子查询在in子句中，且子查询中为简单SQL(不包含union、group by、order by、limit从句)时，才可以把子查询转化为关联查询进行优化。 子查询性能差的原因： 子查询的结果集无法使用索引，通常子查询的结果集会被存储到临时表中，不论是内存临时表还是磁盘临时表都不会存在索引，所以查询性能 会受到一定的影响； 特别是对于返回结果集比较大的子查询，其对查询性能的影响也就越大； 由于子查询会产生大量的临时表也没有索引，所以会消耗过多的CPU和IO资源，产生大量的慢查询。 避免使用JOIN关联太多的表 对于Mysql来说，是存在关联缓存的，缓存的大小可以由join_buffer_size参数进行设置。 在Mysql中，对于同一个SQL多关联（join）一个表，就会多分配一个关联缓存，如果在一个SQL中关联的表越多，所占用的内存也就越大。 如果程序中大量的使用了多表关联的操作，同时join_buffer_size设置的也不合理的情况下，就容易造成服务器内存溢出的情况，就会影响到服务器数据库性能的稳定性。 同时对于关联操作来说，会产生临时表操作，影响查询效率Mysql最多允许关联61个表，建议不超过5个。 减少同数据库的交互次数 数据库更适合处理批量操作 合并多个相同的操作到一起，可以提高处理效率 对应同一列进行or判断时，使用in代替or in的值不要超过500个in操作可以更有效的利用索引，or大多数情况下很少能利用到索引。 禁止使用order by rand()进行随机排序 会把表中所有符合条件的数据装载到内存中，然后在内存中对所有数据根据随机生成的值进行排序，并且可能会对每一行都生成一个随机值，如果满足条件的数据集非常大，就会消耗大量的CPU和IO及内存资源。 推荐在程序中获取一个随机值，然后从数据库中获取数据的方式。 WHERE从句中禁止对列进行函数转换和计算 对列进行函数转换或计算时会导致无法使用索引。 不推荐：where date(create_time)='20190101' 推荐：where create_time \u0026gt;= '20190101 and create_time \u0026lt; '20190102' 在明显不会有重复值时使用UNION ALL而不是UNION UNION会把两个结果集的所有数据放到临时表中后再进行去重操作 UNION ALL不会再对结果集进行去重操作 拆分复杂的大SQL为多个小SQL 大SQL：逻辑上比较复杂，需要占用大量CPU进行计算的SQL MySQL：一个SQL只能使用一个CPU进行计算 SQL拆分后可以通过并行执行来提高处理效率 9. 数据库操作行为规范 超100万行的批量写（UPDATE、DELETE、INSERT）操作，要分批多次进行操作 大批量操作可能会造成严重的主从延迟 主从环境中，大批量操作可能会造成严重的主从延迟，大批量的写操作一般都需要执行一定长的时间，而只有当主库上执行完成后，才会在其他从库上执行，所以会造成主库与从库长时间的延迟情况 binlog日志为row格式时会产生大量的日志 大批量写操作会产生大量日志，特别是对于row格式二进制数据而言，由于在row格式中会记录每一行数据的修改，我们一次修改的数据越多，产生的日志量也就会越多，日志的传输和恢复所需要的时间也就越长，这也是造成主从延迟的一个原因。 避免产生大事务操作 大批量修改数据，一定是在一个事务中进行的，这就会造成表中大批量数据进行锁定，从而导致大量的阻塞，阻塞会对MySQL的性能产生非常大的影响。 特别是长时间的阻塞会占满所有数据库的可用连接，这会使生产环境中的其他应用无法连接到数据库，因此一定要注意大批量写操作要进行分批。 对于大表使用pt-online-schema-change修改表结构 避免大表修改产生的主从延迟 避免在对表字段进行修改时进行锁表 对大表数据结构的修改一定要谨慎，会造成严重的锁表操作，尤其是生产环境，是不能容忍的。 pt-online-schema-change它会首先建立一个与原表结构相同的新表，并且在新表上进行表结构的修改，然后再把原表中的数据复制到新表中，并在原表中增加一些触发器。 把原表中新增的数据也复制到新表中，在行所有数据复制完成之后，把新表命名成原表，并把原来的表删除掉。 把原来一个DDL操作，分解成多个小的批次进行。 禁止为程序使用的账号赋予super权限 当达到最大连接数限制时，还运行1个有super权限的用户连接super权限只能留给DBA处理问题的账号使用。 对于程序连接数据库账号，遵循权限最小原则 程序使用数据库账号只能在一个DB下使用，不准跨库 程序使用的账号原则上不准有drop权限。 ","permalink":"https://ktzxy.top/posts/k2yhizsqk4/","summary":"MySQL 开发规范","title":"MySQL 开发规范"},{"content":"Go的运算符 算数运算符 +：相加 -：相减 *：相乘 /：相除 %：求余 在golang中， ++ 和 \u0026ndash; 只能单独使用，错误的写法如下\n1 2 3 4 var i int = 8 var a int a = i++ // 错误，i++只能单独使用 a = i-- // 错误，i--只能单独使用 同时在golang中，没有 ++i这样的操作\n1 2 var i int = 1 ++i // 错误 正确的写法\n1 2 var i int = 1 i++ //正确 ","permalink":"https://ktzxy.top/posts/znzw94r6va/","summary":"4 Go的运算符","title":"4 Go的运算符"},{"content":"1. IO 流 1.1. 数据在计算机的表现形式 所有数据(视频、音频、图片、文本文件)在计算机中都是由0和1组成。计算机只能识别0和1；\n数据在计算机的表现形式就是：二进制数据。在数据传输过程中，一切数据(文本、图像、声音等)最终存储的均为一个个字节，即二进制数字。所以数据传输过程中使用二进制数据可以完成任意数据的传递，任何数据在传输过程中都是以0和1格式传输。\n向一个文件中存储一定的数据(一些数字)，如果使用文本方式打开，则会以文本的方式解释数据。如果以视频的方式打开，则会以视频的方式解释数据。音频、可行执行文件等亦是如此。所以，在文件传输过程中，要时刻明确，传输的始终为二进制数据。\n1.2. IO 的概念 1.2.1. IO 操作 IO 操作，是指输入和输出操作。\nInput(输入操作)：数据从文件到程序的过程。 Output(输出操作)：数据从程序到文件的过程。 例如：当使用集合保存数据时，这些数据都存在于内存中，一旦程序运行结束，这些数据将会从内存中清除，下次再想使用这些数据，已经没有了。如果希望将运算结果永久地保存下来，可以使用 IO，将这些数据持久化存储起来。要把数据持久化存储就需要把内存中的数据存储到内存以外的其他持久化设备(硬盘、光盘、U 盘等)上。此时需要数据的输入(in)输出(out)。数据输入输出相关的类均在 io 包下。\n1.2.2. IO 流 按流向可分为两种：输入流（I），输出流（O）。\nI 表示 Intput，是数据从硬盘进内存的过程，称之为读。 O 表示 Output，是数据从内存到硬盘的过程。称之为写。 例如：将数据写到文件中，实现数据永久化存储；读取文件中已经存在的数据。\n1.3. IO 模型分类 1.3.1. 阻塞 IO 模型（Blocking IO） 最传统的一种 IO 模型，即在读写数据过程中用户线程会发生阻塞现象。\n当用户线程发出 IO 请求之后，内核会去查看数据是否就绪，如果没有就绪就会等待数据就绪，而用户线程就会处于阻塞状态，用户线程交出 CPU。当数据就绪之后，内核会将数据拷贝到用户线程，并返回结果给用户线程，用户线程才解除 block 状态。典型的阻塞 IO 模型的例子为：data = socket.read();，如果数据没有就绪，就会一直阻塞在 read 方法的位置。\n在客户端连接数量不高的情况下，性能上是没问题的。但是当面对十万甚至百万级连接的时候，传统的 BIO 模型就会突显性能上的缺陷。\n1.3.2. 非阻塞 IO 模型（Nonblocking IO） 在非阻塞 I/O 模型中，当用户线程发起一个 read 操作后，并不需要等待，而是马上就得到了一个结果。如果结果是一个 error 时，它就知道数据还没有准备好，于是它可以再次发送 read 操作。期间用户线程会继续不断重试直到数据准备好为止。一旦内核中的数据准备好了，并且又再次收到了用户线程的请求，那么它马上就将数据拷贝到了用户线程，然后返回。\n所以事实上，在非阻塞 IO 模型中，用户线程需要不断地询问内核数据是否就绪，也就说非阻塞 IO 不会交出 CPU，但会一直占用 CPU。典型的非阻塞 IO 模型实现一般如下：\n1 2 3 4 5 6 7 while (true) { data = socket.read(); if (data != error) { // 处理数据 break; } } 因此，非阻塞 IO 有一个非常严重的缺点，在 while 循环中需要不断地去询问内核数据是否就绪，这样会导致 CPU 占用率非常高，因此一般情况下很少使用 while 循环这种方式来读取数据。\n非阻塞 IO 可以一个线程处理多个流事件，只要不停地询所有流事件即可。当然这种方式也不好，当大多数流没数据时，也是会大量浪费 CPU 资源。为了避免 CPU 空转，引进代理(select 和 poll，两种方式相差不大)，代理可以观察多个流 I/O 事件，空闲时会把当前线程阻塞掉，当有一个或多个 I/O 事件时，就从阻塞态醒过来，把所有 IO 流都轮询一遍，于是没有 IO 事件时程序就阻塞在 select 方法处，即便这样依然存在问题，但从 select 处只是知道是否有 IO 事件发生，却不知道是哪几个流，还是只能轮询所有流，epoll 这样的代理就可以把哪个流发生怎样的 IO 事件通知用户线程。\n1.3.3. 多路复用 IO 模型（IO multiplexing） 多路复用 IO 模型是目前使用得比较多的模型。Java NIO 实际上就是多路复用 IO 模型。\n在 Java NIO 中，是通过 selector.select() 去查询每个通道是否有到达事件，如果没有事件，则一直阻塞在那里，因此这种方式会导致用户线程的阻塞。在多路复用 IO 模型中，会有一个线程不断去轮询多个 socket 的状态，只有当 socket 真正有读写事件时，才真正调用实际的 IO 读写操作。\n因此IO 多路复用的特点是：通过一种机制一个进程能同时等待多个文件描述符，而这些文件描述符(套接字描述符)其中任意一个进入就绪状态，select 函数就可以返回。\n在多路复用 IO 模型中，只需要使用一个线程就可以管理多个 socket，系统不需要建立新的进程或者线程，也不必维护这些线程和进程，并且只有在真正有 socket 读写事件进行时，才会使用 IO 资源，所以它大大减少了资源占用。因此，多路复用 IO 比较适合连接数比较多的情况。\n另外多路复用 IO 之所以比非阻塞 IO 模型的效率高，是因为在非阻塞 IO 中，不断地询问 socket 状态时通过用户线程去进行；而在多路复用 IO 中，轮询每个 socket 状态是内核在进行的，这个效率要比用户线程要高的多。\n不过要注意的是，多路复用 IO 模型是通过轮询的方式来检测是否有事件到达，并且对到达的事件逐一进行响应。因此对于多路复用 IO 模型来说，一旦事件响应体很大，那么就会导致后续的事件迟迟得不到处理，并且会影响新的事件轮询。\n目前支持 IO 多路复用的系统调用，有 select，epoll 等等。select 系统调用，目前几乎在所有的操作系统上都有支持。\nselect 调用：内核提供的系统调用，它支持一次查询多个系统调用的可用状态。几乎所有的操作系统都支持。 epoll 调用：linux 2.6 内核，属于 select 调用的增强版本，优化了 IO 的执行效率。 1.3.4. 信号驱动 IO 模型 在信号驱动 IO 模型中，当用户线程发起一个 IO 请求操作，会给对应的 socket 注册一个信号函数，然后用户线程会继续执行；当内核数据就绪时会发送一个信号给用户线程，用户线程接收到信号之后，便在信号函数中调用 IO 读写操作来进行实际的 IO 请求操作。\n信号驱动 IO 模型的特点是：等待数据报到达期间进程不被阻塞。主循环可以继续执行，只要等待来自信号处理函数的通知。既可以是数据已准备好被处理，也可以是数据报已准备好被读取。\n1.3.5. 异步 IO 模型（asynchronous IO） 异步 IO 模型是最理想的 IO 模型，在异步 IO 模型中，当用户线程发起 read 操作之后，立刻就可以开始去做其它的事。而另一方面，从内核的角度，当它受到一个 asynchronous read 之后，它会立刻返回，说明 read 请求已经成功发起了，因此不会对用户线程产生任何 block。然后，内核会等待数据准备完成，再将数据拷贝到用户线程，当这一切都完成之后，内核会给用户线程发送一个信号，告诉它 read 操作完成了。也就说用户线程完全不需要实际的整个 IO 操作是如何进行的，只需要先发起一个请求，当接收内核返回的成功信号时表示 IO 操作已经完成，可以直接去使用数据了。\n在异步 IO 模型中，IO 操作的两个阶段都不会阻塞用户线程，这两个阶段都是由内核自动完成，然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用 IO 函数进行具体的读写。这点是和信号驱动模型有所不同的，在信号驱动模型中，当用户线程接收到信号表示数据已经就绪，然后需要用户线程调用 IO 函数进行实际的读写操作；而在异步 IO 模型中，收到信号表示 IO 操作已经完成，不需要再在用户线程中调用 IO 函数进行实际的读写操作。\nTips: 异步 IO 是需要操作系统的底层支持，在 Java 7 中，提供了 Asynchronous IO。\n2. File 类 2.1. File 简述 File 类：文件和目录路径名的抽象表示形式。即，Java 中把文件或者目录（文件夹）都封装成 File 对象。\n可以用来操作硬盘上的文件或文件夹。也就是说如果要去操作硬盘上的文件，或者文件夹只要找到 File 这个类即可。\n文件可以持久化地存储数据 File 的一个对象就代表一个文件或文件夹(自己简单地定义，方便记忆的说法) 文档上说明 File 类代表文件或文件夹路径，但是我们可以通过路径找到对应的文件或文件夹。 可以认为 File 类就代表文件或文件夹(通过路径找到) 2.2. 相对路径与绝对路径 绝对路径\n以盘符开始到文件的全路径 在整个系统中，具有唯一性 相对路径\n从某个参照目录开始到指定文件所经过的路径 在整个系统中，不具有唯一性 相对路径一般是在 Eclipse 中的某个项目当中创建一个文件夹(目录)开始。如 a.txt 相对于 myIO 项目根目录经过了 a/b/a.txt，则 a/b/a.txt 就是该文件的相对路径 2.3. File 类的使用 2.3.1. 成员变量 与系统有关的路径分隔符，window是“;”，mac与lunix是“:” 1 public static final String pathSeparator 与系统有关的默认名称分隔符(目录分隔符)，跨平台的。window是“\\”，mac与lunix是“/”。应该是需要跨平台，所以不能直接将分隔符写成一种类型。目前JDK版本已经可以自动识别。 1 public static final String separator 注：静态成员变量，直接用file.成员变量名使用\n2.3.2. 构造方法 1 public File(String pathname) 根据文件或文件夹路径字符串创建文件对象，通过路径找到对应的文件或文件夹 API描述：通过将给定路径名字符串转换为抽象路径名来创建一个新 File 实例。如果给定字符串是空字符串，那么结果是空抽象路径名。\n例：File f = new File(\u0026quot;E:\\\\documents\\\\aaa.txt\u0026quot;); 1 public File(String parent, String child); parent 指的是父级目录路径字符串，child 指的是子级目录（子级文件）路径字符串。根据父文件夹路径和子文件夹（子文件）路径创建文件对象。 例：File f = new File(\u0026quot;E:\\\\documents\\\\\u0026quot;, \u0026quot;aaa.txt\u0026quot;); 1 public File(File parent, String child); parent 为 File 类型，为了使用 File 类中的方法 例：File f = new File(new File(\u0026quot;E:\\\\documents\\\\\u0026quot;), \u0026quot;aaa.txt\u0026quot;); 注意：File的构造方法不会去判断路径是否存在，需要自己去调用方法处理\n2.3.3. 文件创建 1 public boolean createNewFile() throws IOException; 根据构造方法指定的路径创建文件，如果文件已经存在，则什么不做。如果文件不存在，则创建文件。\n只能用来创建文件，不能创建文件夹。在创建文件时，如果文件所在的文件夹不存在，则报错系统找不到指定的路径。创建文件时，必须确保文件夹已经存在。\n2.3.4. 文件夹创建 1 public boolean mkdir(); 根据路径字符串创建文件夹（单级目录，下面不能再创建新的目录） 如果文件夹存在，则什么不做；如果文件夹不存在，则创建。创建成功返回true，否则返回false 需要注意：只能用来创建文件夹，不能创建文件 使用mkdir方法创建文件夹时，必须保证其所在文件夹已经存在，否则创建失败(不会报错) 1 public boolean mkdirs(); 一次性创建多级文件夹（最常用）。如果父文件夹不存在，则会先创建父文件夹。 创建成功返回true，否则返回false 需要注意：只能用来创建文件夹，不能创建文件 2.3.5. 文件/文件夹删除 1 public boolean delete(); 删除此抽象路径名表示的文件或目录。如果删除成功返回true；如果不成功则返回false。\nFile对象是文件：直接删除文件(Java 删除时，不会使用 windows 的回收站) File对象是文件夹：只删定义路径中最后一个文件夹且只能删除空文件夹，如果不是空文件夹，即不能删除。 2.3.6. 获取文件/文件夹信息 1 public long length() 获得文件大小，单位：字符。只能是文件，不能是文件夹。 API: 返回由此抽象路径名表示的文件的长度（单位：字符）。如果此路径名表示一个目录，则返回值是不确定的(垃圾值)。\n1 public String getName(); 获取文件/文件夹的名称 API: 返回由此抽象路径名表示的文件或目录的名称。该名称是路径名名称序列中的最后一个名称。如果路径名名称序列为空，则返回空字符串。\n1 public String getAbsolutePath(); 获取绝对路径字符串，返回此抽象路径名的绝对路径名字符串。 1 public File getAbsoluteFile() 获取绝对路径的对象 API: 返回此抽象路径名的绝对路径名形式。等同于new File(this.getAbsolutePath())。\n1 public String getParent(); 获得上一级文件路径字符串； API: 返回所在文件夹路径(根据创建对象时是否为绝对路径/相对路径)\n1 public File getParentFile() 获得上一级文件路径对象；（应该是用于返回上一级目录再继续进行其他操作） API: 返回此抽象路径名父目录的抽象路径名；如果此路径名没有指定父目录，则返回 null。\n1 public String getPath(); 获取路径(用什么方式创建的对象,就返回什么方式的路径(绝对路径/相对路径)) API: 将此抽象路径名转换为一个路径名字符串。所得字符串使用默认名称分隔符分隔名称序列中的名称。\n获取文件/文件夹信息示例代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /* * 获取 E 盘 aaa 文件夹中 b.txt 文件的文件名，文件大小，文件的绝对路径和父路径等信息， * 并将信息输出在控制台。 */ @Test public void testGetFileInfo() { // 获取 E 盘 aaa 文件夹中 b.txt对象 File file = new File(\u0026#34;e:\\\\aaa\\\\b.txt\u0026#34;); // 判断对象是否文件 System.out.println(\u0026#34;对象是否为文件：\u0026#34; + file.isFile()); // 获取文件的大小,单位：字节 System.out.println(\u0026#34;对象文件的大小 ：\u0026#34; + file.length()); // 获取文件名 System.out.println(\u0026#34;对象文件的名称：\u0026#34; + file.getName()); // 获取文件的绝对路径 System.out.println(\u0026#34;对象的绝对路径是：\u0026#34; + file.getAbsolutePath()); // 获取父路径信息 System.out.println(\u0026#34;对象的父路径是：\u0026#34; + file.getParent()); // 获取路径 System.out.println(\u0026#34;对象的路径是(用什么方式创建的对象,就返回什么方式的路径)：\u0026#34; + file.getPath()); File file2 = new File(\u0026#34;qq.txt\u0026#34;); // 获取这个相对路径的对象的绝对路径 System.out.println(\u0026#34;相对路径对象的绝对路径是：\u0026#34; + file2.getAbsolutePath()); } 输出结果\n1 2 3 4 5 6 7 对象是否为文件：true 对象文件的大小 ：44 对象文件的名称：b.txt 对象的绝对路径是：e:\\aaa\\b.txt 对象的父路径是：e:\\aaa 对象的路径是(用什么方式创建的对象,就返回什么方式的路径)：e:\\aaa\\b.txt 相对路径对象的绝对路径是：D:\\code\\java-technology-stack\\java-basic-api\\qq.txt 2.3.7. 判断文件/文件夹 此部分的API是用于判断该 File 对象是否存在或者判断该 File 对象代表一个文件还是代表一个文件夹\n1 public boolean exists(); 判断文件/文件夹是否存在。存在则返回true，否则返回false 1 public boolean isDirectory(); 判断File对象是否为文件夹，是文件夹则则返回true，否则返回false 1 public boolean isFile(); 判断File对象是否为文件，是文件则返回true，否则返回false 2.3.8. 获取文件/文件夹列表（重点） 需要注意：File 对象必须是文件夹\n1 public String[] list() 获得当前文件夹对象下所有文件（子文件和子文件夹），返回字符串数组。 API: 返回一个字符串数组，这些字符串指定此抽象路径名表示的目录中的文件和目录。\n1 public File[] listFiles(); 获得当前文件夹对象下所有文件（子文件和子文件夹），返回 File 对象数组。 API: 返回一个抽象路径名数组，这些路径名表示此抽象路径名表示的目录中的文件。\n注意：如果是File对象是文件，则返回去的数组为null。所以在使用List获取方法前，需要判断File对象是否是文件夹。\n示例代码：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void testGetFileList() { // 创建文件夹对象 File file = new File(\u0026#34;E:\\\\00-Downloads\\\\test\\\\\u0026#34;); String[] list = file.list(); // 输出该文件夹里的文件列表 System.out.println(Arrays.toString(list)); // 创建文件对象（非文件夹） File file2 = new File(\u0026#34;E:\\\\00-Downloads\\\\a.txt\u0026#34;); String[] list2 = file2.list(); // 输出null System.out.println(Arrays.toString(list2)); } 输出结果：\n1 2 [a - 副本 (2).txt, a - 副本 (3).txt, a - 副本 (4).txt, a - 副本 (5).txt, a - 副本.txt, a.txt] null 2.4. File 类基础应用案例 2.4.1. 读取指定文件夹下的所有文件（多个文件夹） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import java.io.File; public class MoonZero { public static void main(String[] args) { // 创建文件夹对象 File file = new File(\u0026#34;E:\\\\temp\u0026#34;); // 调用读取所有文件的方法 printAllFile(file); } // 创建读取当前文件夹下的的有文件递归方法 public static void printAllFile(File file) { // 获取当前文件夹下所有文件的list对象数组 File[] list = file.listFiles(); // 利用增强for遍历File对象数组 for (File f : list) { // 再判断是否是文件，如果是文件，直接输出（递归的出口） if (f.isFile()) { // 直接输出对像是绝对路径，是因为File重写了toString方法 System.out.println(f); } else { // 不是文件，则就是文件夹，就进行递归，继续执行。 printAllFile(f); } } } } 2.4.2. 统计指定文件夹所有文件的大小总和（文件夹下有多个文件夹） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package day10; import java.io.File; import java.util.ArrayList; import java.util.Iterator; import java.util.Scanner; /* * 关卡2训练案例2 * 键盘录入一个文件路径，根据文件路径创建文件对象，判断是文件还是文件夹 * 如果是文件，则输出文件的大小 * 如果是文件夹则计算该文件夹下所有文件大小之和并输出(不包含子文件夹)。 */ public class Test2_02 { public static void main(String[] args) { Scanner input = new Scanner(System.in); System.out.println(\u0026#34;请录入一个文件路径：\u0026#34;); File file = new File(input.nextLine()); // 判断是文件还是文件夹 if (file.isFile()) { System.out.println(\u0026#34;输入的文件路径是文件！\u0026#34;); System.out.println(\u0026#34;\\\u0026#34;\u0026#34; + file.getName() + \u0026#34;\\\u0026#34;的大小是：\u0026#34; + file.length()); } else { System.out.println(\u0026#34;输入的文件路径是文件夹！\u0026#34;); System.out.println(\u0026#34;输入的文件夹里的所有文件清单：\u0026#34;); // 增加将子文件夹都算进去，调用递归方法来统计所有文件的大小 // 创建一个新的集合，用来存放递归所有文件的File对像。 ArrayList\u0026lt;File\u0026gt; array = new ArrayList\u0026lt;File\u0026gt;(); findAllFile(file, array); // 将递归接收所有的文件的集合再使用迭代器遍历 Iterator\u0026lt;File\u0026gt; it = array.iterator(); // 定义一个long变量用来统计所有文件的大小 long sum = 0; while (it.hasNext()) { File f = it.next(); System.out.println(f.getName()); sum += f.length(); } System.out.println(\u0026#34;文件夹中的所有文件的大小之和是：\u0026#34; + sum); } input.close(); } public static void findAllFile(File file, ArrayList\u0026lt;File\u0026gt; array) { // 使用listFiles方法，获取File对象数组。 File[] fileArr = file.listFiles(); for (File f : fileArr) { // 判断，如果是文件，直接输出统计大小，如果是文件夹，再进行递归遍历 if (f.isFile()) { array.add(f); } else { findAllFile(f, array); } } } } 2.4.3. 删除指定文件夹下的所有文件（多个文件夹） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package day10; import java.io.File; import java.util.Scanner; /* * 关卡3训练案例2 * 键盘录入一个文件夹路径，删除该路径下的文件夹。 * 要求：文件夹中包含有子文件夹 */ public class Test3_02 { public static void main(String[] args) { // 创建键盘录入对象 Scanner input = new Scanner(System.in); System.out.println(\u0026#34;请输入一个文件夹路径：\u0026#34;); File file = new File(input.nextLine()); // 调用递归方法，对当前文件夹进行删除操作。 deleteFile(file); input.close(); } public static void deleteFile(File file) { // 获取file对象数组 File[] list = file.listFiles(); // 遍历数组 for (File f : list) { // 进行判断是否为文件，如果文件直接删除 if (f.isFile()) { // 将文件删除后才能将文件夹删除 f.delete(); } else { // 如果是文件夹，再递归，然后里面的文件都删除。 deleteFile(f); } } // 再对文件夹进行删除，上一步递归后，已经将里面的文件夹和文件都删除 file.delete(); } } 3. FileFilter 文件过滤器(难点、重点) 3.1. FileFilter 概述 FileFilter 过滤器是一个函数式接口，用于抽象路径名的过滤器。使用时候要创建一个接口的实现类。（通常使用匿名内部类或者lambda表达式来完成，因为一般该接口只适用于本次的过滤需求，没有广泛的适用性。）\n1 2 3 4 5 6 7 8 9 10 11 12 13 @FunctionalInterface public interface FileFilter { /** * Tests whether or not the specified abstract pathname should be * included in a pathname list. * * @param pathname The abstract pathname to be tested * @return \u0026lt;code\u0026gt;true\u0026lt;/code\u0026gt; if and only if \u0026lt;code\u0026gt;pathname\u0026lt;/code\u0026gt; * should be included */ boolean accept(File pathname); } FileFilter 接口的唯一方法accept，作用是测试指定抽象路径名是否应该包含在某个路径名列表中。\n3.2. 接口的调用时机 每当遍历获得一个子文件或子文件夹时，系统内部会创建一个文件对象，然后将该文件对象作为参数调用，文件过滤的accept方法，由accept的返回值决定该文件是否要过滤。返回false表示过滤该文件，ture则不过滤。\n3.3. File 类使用过滤器的方法 根据指定文件过滤器获得当前文件夹下的过滤后的所有文件的File对象数组\n1 public File[] listFiles(FileFilter filter) API: 返回抽象路径名数组，这些路径名表示此抽象路径名表示的目录中满足指定过滤器的文件和目录。除了返回数组中的路径名必须满足过滤器外，此方法的行为与 listFiles() 方法相同。如果给定 filter 为 null，则接受所有路径名。否则，当且仅当在路径名上调用过滤器的 FileFilter.accept(java.io.File) 方法返回 true 时，该路径名才满足过滤器。\n3.4. 文件过滤器的使用步骤与示例 定义一个类实现FileFilter接口**（通常使用匿名内部类或者lambda表达式来完成）** 重写accept方法,满足条件的返回true,不满足条件的返回false 创建FileFilter接口的实现类对象 file.listFiles()方法参数中传入过滤器 使用文件过滤器示例:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import java.io.File; import java.io.FileFilter; /* * 需求：只输出文件夹中所有的.pptx文件 */ public class MoonZero { public static void main(String[] args) { // 创建文件夹对象 File file = new File(\u0026#34;E:\\\\temp\u0026#34;); // 调用读取所有文件的方法 printAllFile(file); } // 创建读取当前文件夹下的的有文件递归方法 public static void printAllFile(File file) { // 创建FileFilter接口的实现类对象 FileFilterDemo ff = new FileFilterDemo(); // 使用File[] listFiles(FileFilter filter)方法，获取过滤后的File对象数组 File[] list = file.listFiles(ff); // 利用增强for遍历File对象数组 for (File f : list) { // 再判断是否是文件，如果是文件，直接输出（递归的出口） if (f.isFile()) { // 直接输出对像是绝对路径，是因为File重写了toString方法 System.out.println(f); } else { // 不是文件，则就是文件夹，就进行递归，继续执行。 printAllFile(f); } } } } // 定义一个类实现FileFilter接口 class FileFilterDemo implements FileFilter { // 重写过滤器 FileFilter的accept抽象方法， // 根据需求定义过滤的条件 @Override public boolean accept(File pathname) { // 要先判断传入的File对象是文件还是文件夹 // 如果是文件夹则直接不过滤，如果是文件，则进行判断过滤。 if (pathname.isDirectory()) { return true; } else { return pathname.getName().endsWith(\u0026#34;.pptx\u0026#34;); } // 另一种简单写法 // return (pathname.isDirectory()) || (pathname.getName().endsWith(\u0026#34;.pptx\u0026#34;)); } } 使用文件过滤器示例2:(使用匿名内部类)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import java.io.File; import java.io.FileFilter; /* * 需求：只输出文件夹中所有的.pptx文件 */ public class MoonZero { public static void main(String[] args) { // 创建文件夹对象 File file = new File(\u0026#34;E:\\\\temp\u0026#34;); // 调用读取所有文件的方法 printAllFile(file); } // 创建读取当前文件夹下的的有文件递归方法 public static void printAllFile(File file) { // 使用File[] listFiles(FileFilter filter)方法，获取过滤后的File对象数组 File[] list = file.listFiles(new FileFilter() { // 创建FileFilter接口的匿名内部类，直接重写accept抽象方法 @Override public boolean accept(File pathname) { // 要先判断传入的File对象是文件还是文件夹 // 如果是文件夹则直接不过滤，如果是文件，则进行判断过滤。 return (pathname.isDirectory()) || (pathname.getName().endsWith(\u0026#34;.pptx\u0026#34;)); } }); // 利用增强for遍历File对象数组 for (File f : list) { // 再判断是否是文件，如果是文件，直接输出（递归的出口） if (f.isFile()) { // 直接输出对像是绝对路径，是因为File重写了toString方法 System.out.println(f); } else { // 不是文件，则就是文件夹，就进行递归，继续执行。 printAllFile(f); } } } } 4. 字符流 4.1. 字符流概述 IO 流用来处理设备之间的数据传输。Java 对数据的操作是通过流的方式，用于操作流的类都在 IO 包中。\n在 IO 开发过程中，传输最频繁的数据为字符，而以字节方式传输字符需要每次将字符串转换成字节再处理，而且也丧失了程序员对数据内容的判断。所以，为了方便对字符进行操作，Java 提供了专门以字符作为操作单位的类——『字符流』，但其底层仍然为字节流。\n注意：字符流只能操作字符，无法操作其他数据，如声音、视频等。\n4.2. FileWriter 输出字符流 FileWriter 输出字符流在 java.io 包中，用于写数据，属于输出流。\n1 public class FileWriter extends OutputStreamWriter 4.2.1. FileWriter 向文件中写数据操作步骤 使用 FileWriter 流关联文件 利用 FileWriter 的写方法写数据 利用 FileWriter 的刷新方法将数据从内存刷到硬盘上 利用 FileWriter 的关流方法将释放占用的系统底层资源 4.2.2. 构造方法 创建输出流对象时，做了哪些事情:\n调用系统资源创建了一个文件 创建输出流对象 把输出流对象指向文件 1 public FileWriter(String fileName) throws IOException 传入一个文件的路径和名称创建输出流，此类输出流无法追加写入。如：FileWriter fw = new FileWriter(\u0026quot;d:\\\\a.txt\u0026quot;) 1 public FileWriter(String fileName, boolean append) throws IOException 传入一个文件的路径和名称创建输出流，根据 append 参数判断是否可追加写入数据。true 表示可以追加写入。默认是 false 无追加。 1 public FileWriter(File file) throws IOException 根据给定的 File 对象构造一个 FileWriter 对象。 1 public FileWriter(File file, boolean append) throws IOException 根据给定的 File 对象构造一个 FileWriter 对象。并且可以通过 append 参数指定是否追加写入。 4.2.3. 常用方法 Notes: 以下 5 种 write 写数据方法，均调用输出流对象的写数据的方法，向文件对象写入数据，但数据没有直接写到文件，只是写到了内存缓冲区。\n1 public void write(String str) throws IOException 向文件写入一个字符串数据。 1 public void write(String str, int off, int len) throws IOException 向文件写入指定字符串中的一部分数据。 str：待写入的字符串数据 off：指定待写入的字符串开始截取的位置索引 len：写入的长度（注意不是索引） 1 public void write(int c) throws IOException 写入一个字符数据，这里写 int 类型的好处是既可以写 char类 型的数据，也可以写 char 对应的 int 类型的值。如 'a' 就是 97 1 public void write(char cbuf[]) throws IOException 写入一个字符数组数据。继承自 Writer 类的方法 1 public void write(char cbuf[], int off, int len) throws IOException 写入一个字符数组的一部分数据 cbuf[]：待写入的字符数组 off：指定待写入的字符数组开始截取的位置索引 len：写入的长度（注意不是索引） 1 public void flush() throws IOException 将内存中的数据刷新到文件中 1 public void close() throws IOException 通知系统释放和该文件相关的资源关闭流，释放系统底层资源。 4.2.4. close() 和 flush() 方法的区别 flush(): 刷新缓冲区。流对象还可以继续使用。 close(): 先刷新缓冲区，然后通知系统释放资源。流对象不可以再被使用了。 4.3. FileReader 输入字符流 FileReader 输出字符流在 java.io 包中，从文件中读数据，属于输入流。\n1 public class FileReader extends InputStreamReader 4.3.1. 输入流读文件的步骤 创建输入流对象 调用输入流对象的读数据方法 释放资源 4.3.2. 构造方法 1 public FileReader(String fileName) throws FileNotFoundException 根据传递文件名称，创建文件输入流 1 public FileReader(File file) throws FileNotFoundException 在给定从中读取数据的 File 的情况下创建一个新 FileReader。 4.3.3. 常用方法 1 public int read() throws IOException 一次读取一个字符，如果读取数据的返回值是-1的时候，就说明没有可读取的数据了。 1 public int read(char cbuf[]) throws IOException 一次读取一个字符数组的数据并保存到 cbuf 数组中，返回的是实际读取的字符个数 4.3.4. 读数据方式1：一次读取一个字符 调用输入流对象的读数据方法，一次读取一个字符。循环去就读取文件的数据，通过测试，如果读取数据的返回值是-1的时候，就说明没有数据了，这也作为循环的结束条件。读取出来的是字符的 ASCII 码，所以需要(char)强转。\n1 2 3 4 5 6 7 8 FileReader fr = new FileReader(\u0026#34;C:\\\\a.txt\u0026#34;); // 定义 ch 变量，是实际读取的数据，也是当返回-1的时候代表没有可读取数据 int ch; while ((ch = fr.read()) != -1) { // 额外内容 // Thread.sleep(100); // 这个可以减缓读取的速度 System.out.print((char) ch); // 示例打印输出是不换行的字符，不要输出\u0026#34;ln\u0026#34;换行。因为如果文档有换行的话，一样可以读取到换行的信息 } 4.3.5. 读数据方式2：一次读取一个字符数组 调用输入流对象的读数据方法，一次读取一个字符数组，通常读取的字符数量为1024及其整数倍\n1 2 3 4 5 6 7 8 FileReader fr = new FileReader(\u0026#34;C:\\\\a.txt\u0026#34;); // 1.初始化读取的字符数数组，一般可以是1024及其整数倍 char[] chs = new char[1024]; // 定义 len 变量，是实际读取的数据数量，也是当返回-1的时候代表没有可读取数据 int len; while ((len = fr.read(chs)) != -1) { System.out.print(new String(chs, 0, len)); // 如果文档有换行的话，一样可以读取到换行的信息 } 4.3.6. read() 和 read(char cbuf[]) 的区别 如果文件中的数据是\u0026quot;a\u0026quot;，两种方法区别如下：\nint len = fr.read();，结果是 len = 97，数据保存在 len 中 int len = fr.read(arr);，结果是 len = 1，代表的是只读一个数据，实际的数据是保存在 arr 数组中。 4.4. BufferedWriter / BufferedReader（缓冲字符流） 4.4.1. BufferedWriter BufferedWriter 带缓冲的输出字符流在 java.io 包中，是文本写入字符输出流，缓冲各个字符，从而提供单个字符、数组和字符串的高效写入。\n1 public class BufferedWriter extends Writer Tips: 缓冲流一样是用基本流的方法，只是创建对象的比较麻烦，但缓冲流的效率会比较高，一般都是使用缓冲流。\n4.4.1.1. 构造方法 1 public BufferedWriter(Writer out) 创建一个使用默认大小输出缓冲区的缓冲字符输出流。 BufferedWriter 用法与 FileWriter 是一样的，只是创建对象的时候不一样。\n1 BufferedWriter bw = new BufferedWriter(new FileWriter(\u0026#34;xxx.txt\u0026#34;)); 4.4.1.2. 特有方法 1 public void newLine() throws IOException 写一个换行符，此换行符由系统决定，不同的操作系统使用的换行符不同。 4.4.2. BufferedReader BufferedReader 带缓冲的输出字符流在 java.io 包中，从字符输入流中读取文本，缓冲各个字符，从而实现字符、数组和行的高效读取。\n1 public class BufferedReader extends Reader 4.4.2.1. 构造方法 1 public BufferedReader(Reader in) 创建一个使用默认大小输入缓冲区的缓冲字符输入流。 BufferedReader 用法与 FileReader 是一样的，但创建对象的时候不一样。\n1 BufferedReader br = new BufferedReader(new FileReader(\u0026#34;xxx.txt\u0026#34;)); 4.4.2.2. 特有方法 1 public String readLine() throws IOException 一次读取一行数据，但是不读取换行符。基础使用示例如下： 1 2 3 4 5 6 7 BufferedReader br = new BufferedReader(new FileReader(\u0026#34;xxx.txt\u0026#34;)); String line; // 将 br.readLine() 方法返回是 null，说明已经无数据可读取 while ((line = br.readLine()) != null) { // readLine是不读取换行符的，所以示例打印加“ln” System.out.println(line); } 4.5. IO 字符流复制文本文件5种实现方式示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 package com.moon; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; /* * 复制文本文件(5种方式) */ public class CopyFileTest { public static void main(String[] args) throws IOException { String srcFileName = \u0026#34;FileWriterDemo.java\u0026#34;; String destFileName = \u0026#34;Copy.java\u0026#34;; //\tmethod1(srcFileName,destFileName); //\tmethod2(srcFileName,destFileName); method3(srcFileName,destFileName); //\tmethod4(srcFileName,destFileName); //\tmethod5(srcFileName,destFileName); } //缓冲流一次读写一行字符串 public static void method5(String srcFileName,String destFileName) throws IOException { //创建输入缓冲流对象 BufferedReader br = new BufferedReader(new FileReader(srcFileName)); //创建输出缓冲流对象 BufferedWriter bw = new BufferedWriter(new FileWriter(destFileName)); //一次读写一个字符串 String line; while((line=br.readLine())!=null){ bw.write(line); bw.newLine(); bw.flush(); } //释放资源 bw.close(); br.close(); } //缓冲流一次读写一个字符数组 public static void method4(String srcFileName,String destFileName) throws IOException { //创建输入缓冲流对象 BufferedReader br = new BufferedReader(new FileReader(srcFileName)); //创建输出缓冲流对象 BufferedWriter bw = new BufferedWriter(new FileWriter(destFileName)); //一次读写一个字符数组 char[] chs = new char[1024]; int len; while((len=br.read(chs))!=-1) { bw.write(chs,0,len); } //释放资源 bw.close(); br.close(); } //缓冲流一次读写一个字符 public static void method3(String srcFileName,String destFileName) throws IOException { //创建输入缓冲流对象 BufferedReader br = new BufferedReader(new FileReader(srcFileName)); //创建输出缓冲流对象 BufferedWriter bw = new BufferedWriter(new FileWriter(destFileName)); //一次读写一个字符 int ch; while((ch=br.read())!=-1) { bw.write(ch); } //释放资源 bw.close(); br.close(); } //基本流一次读写一个字符数组 public static void method2(String srcFileName,String destFileName) throws IOException { //创建输入流对象 FileReader fr = new FileReader(srcFileName); //创建输出流对象 FileWriter fw = new FileWriter(destFileName); //一次读写一个字符数组 char[] chs = new char[1024]; int len; while((len=fr.read(chs))!=-1) { fw.write(chs,0,len); } //释放资源 fw.close(); fr.close(); } //基本流一次读写一个字符 public static void method1(String srcFileName,String destFileName) throws IOException { //创建输入流对象 FileReader fr = new FileReader(srcFileName); //创建输出流对象 FileWriter fw = new FileWriter(destFileName); //一次读写一个字符 int ch; while((ch=fr.read())!=-1) { fw.write(ch); } //释放资源 fw.close(); fr.close(); } } 5. 字节流 5.1. 字符流存在的问题 字符输入和输出流只能操作文本文件，如果操作的是非文本文件（图片，视频，音频\u0026hellip;）就会出现数据丢失的问题。\n5.2. OutputStream 字节输出流 5.2.1. 概述 1 public abstract class OutputStream implements Closeable, Flushable OutputStream 字节输出流在 java.io 包中，是一个抽象类。字节输出流的根类，定义了所有字节输出流应该具备的方法。\n此抽象类表示是所有字节输出流类的超类/父类/基类，常用子类有：\njava.io.FileOutputStream java.io.BufferedOutputStream 5.2.2. 常用方法 1 public abstract void write(int b) throws IOException; 输出一个字节。如果整数b超出一个字节，也是写入一个字节的内容。 1 public void write(byte b[]) throws IOException 输出一个字节数组。如果写入字符串，将字符串转成字节数组。如：new FileOutputStream(\u0026quot;a.txt\u0026quot;).write(\u0026quot;你好\u0026quot;.getByte()); 1 public void write(byte b[], int off, int len) throws IOException 输出字节数组的一部分。 byte b[]：待写出的字节数组 int off：从字节数组的哪个位置开始 int len：写出多少个字节 1 public void close() throws IOException 关闭流释放资源。释放 IO 占用的 windows 底层资源 5.2.3. 常用子类：FileOutputStream (文件字节输出流) 1 public class FileOutputStream extends OutputStream java.io.FileOutputStream 是 OutputStream 的一个常用子类，其构造方法如下：\n1 public FileOutputStream(String name) throws FileNotFoundException 通过字符串路径创建 FileOutputStream 对象。创建一个向具有指定名称的文件中写入数据的输出文件流。默认是以覆盖方式写入内容。 1 public FileOutputStream(String name, boolean append) throws FileNotFoundException 通过字符串路径创建 FileOutputStream 对象。参数 append 为 true 代表每次写入都向文件末尾追加，默认为 false 则每次都以覆盖方式写入。 1 public FileOutputStream(File file) throws FileNotFoundException 通过 File 对象创建 FileOutputStream 对象。默认是以覆盖方式写入内容。 1 public FileOutputStream(File file, boolean append) throws FileNotFoundException 通过 File 对象创建 FileOutputStream 对象。参数 append 为 true 代表每次写入都向文件末尾追加，默认为 false 则每次都以覆盖方式写入。 Notes: 直接 new FileOutputStream(file) 创建对象，写入数据，会覆盖原有的文件。\n5.2.4. 字节输出流的使用步骤 创建字节输出流对象并关联目标文件 调用 write() 方法写出数据：写一个字节，写一个字节数组，写一个字节数组的一部分。 关闭流释放资源。 5.2.5. 字节输出流注意事项 如果文件不存在，则会自动创建该文件。 如果不是追加写出，则默认会先将文件内容清空再输出新内容。 如果需要给文件追加写出，则在构造方法指定参数 append 为 true，实现追加输入的效果。 实现内容换行，可以使用 String 类的方法，将字符串转成 byte 数组，在写完数据后加上\u0026quot;\\r\\n\u0026quot;（这里的换行方式是windows系统）。 1 2 3 FileOutputStream fos = new FileOutputStream(\u0026#34;a.txt\u0026#34;, true); // ...写入相关内容后再进行换行， fos.write(\u0026#34;\\r\\n\u0026#34;.getBytes()); 5.2.6. 异常的处理 假设在 FileOutputStream fos = new FileOutputStream(\u0026quot;c.txt\u0026quot;); 出现异常 使用 try-catch 捕获 FileNotFoundException 异常 在 finally 中关流，此时可能会访问不到 fos 变量，因此需要在 try 外面定义 fos 在 finally 要先判断 fos 是否为空，只有 fos 不等于空才需要关流 调用 fos.close(); 时又有异常，接着再进行 try-catch 捕获 IOException 异常 在 try 中调用 fos.write(); 会有 IOException，添加一个新的 catch 分支捕获即可。其实可以合并成一个 IOException 分支即可，分开只是为了更清晰问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 FileOutputStream fos = null; try { fos = new FileOutputStream(\u0026#34;c.txt\u0026#34;); fos.write(\u0026#34;写点东西\u0026#34;.getBytes()); } catch (FileNotFoundException e) { // do something... } catch (IOException e) { // do something... } finally { try { if (fos != null) { fos.close(); } } catch (IOException e) { // 关闭流失败 } } 5.3. InputStream 字节输入流 5.3.1. 概述 1 public abstract class InputStream implements Closeable InputStream 字节输入流在 java.io 包中，是一个抽象类。字节输入流的根类，定义了所有字节输入流应该具备的方法。\n此抽象类表示是所有字节输入流类的超类/父类/基类，常用子类有：\njava.io.FileInputStream java.io.BufferedInputStream 5.3.2. 常用方法 1 public abstract int read() throws IOException; 读取一个字节，返回的是字节内容本身，读取到末尾返回 -1。 1 public int read(byte b[]) throws IOException 将读取到字节输出存储到字节数组b 中，返回实际读取的字节个数。返回 -1 表示读到文件末尾。 1 public int read(byte b[], int off, int len) throws IOException 将读取到字节输出存储的字节数组b 中，返回实际读取的字节个数 byte b[]：存储读取的内容字节数组 int off：从哪个位置开始存储 int len：存几个字符 1 public void close() throws IOException 释放 IO 占用的系统底层资源 5.3.3. 常用子类：FileInputStream (文件字节输入流) 1 public class FileInputStream extends InputStream java.io.FileInputStream 是 InputStream 的一个常用子类，用于从文件中读取字节数据。其构造方法如下：\n1 public FileInputStream(String name) throws FileNotFoundException 通过字符串路径创建 FileInputStream 1 public FileInputStream(File file) throws FileNotFoundException 通过 File 对象创建 FileInputStream 5.3.4. 字节输入流的使用步骤 创建字节输入流对象并关联目标文件 调用 read() 方法读取数据：读一个字节，读一个字节数组，读一个字节数组的一部分。 关闭流释放资源。 5.3.5. 字节输入流注意事项 如果输入流关联的文件不存在，则会抛出异常。 5.3.6. 基础示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.moon; import java.io.FileInputStream; import java.io.IOException; /* * 关卡1训练案例5 * 利用字节输入流读取 C 盘文件 b.txt 的内容， * 使用循环读取，一次读取一个字节数组，直到读取到文件末尾， * 将读取到的字节数组转换成字符串输出到控制台。 */ public class Test1_05 { public static void main(String[] args) throws IOException { // 创建字节输入流对象 String src = \u0026#34;D:\\\\b.txt\u0026#34;; FileInputStream fis = new FileInputStream(src); // 定义一个字节数组 byte[] b = new byte[1024]; int len; while ((len = fis.read(b)) != -1) { System.out.print(new String(b, 0, len)); } // 释放资源 fis.close(); } } 5.4. BufferedOutputStream / BufferedInputStream（字节缓冲流） 5.4.1. 缓冲流高效原理 利用缓冲区临时存储多个数据，统一调用底层资源将数据写入到目标文件中。Java 在常规 IO 流的基础上，提供了更为高效的缓冲流，如下：\n高效流使用普通流对象作为构造方法参数。将普通流包装，提供高效的装饰。即在读写还是用到普通流来实现，高效流只提供了缓冲区（缓冲区就新建一个字节数组，而默认的数组长度是8192）。 高效流 write 写出数据时，写出位置为缓冲区，并非目标资源。需要通过 flush 刷新方法将缓冲区的内容写出到目标文件中。 高效输出流的关闭 close 方法先会自动调用 flush 方法，再关闭流。 都通减少调用底层资源的使用资料来达到高效 高效缓冲流除了创建对象的时候与普通字节流不一样，其他使用方式都和普通字节流一样。JDK1.5 后，高效缓冲流就高于普通字节流。\nTips: 凡是字节流都没有 write.newLine() 和 readLine() 这个方法。\n5.4.2. BufferedOutputStream（缓冲输出流、写数据） 1 public class BufferedOutputStream extends FilterOutputStream BufferedOutputStream 继承了 FilterOutputStream 最终是继承 OutputStream。\n5.4.2.1. 构造方法 1 public BufferedOutputStream(OutputStream out) 通过 OutputStream 对象来创建 BufferedOutputStream。如：new BufferedOutputStream(new FileOutputStream(\u0026quot;xxx\u0026quot;)); 1 public BufferedOutputStream(OutputStream out, int size) 可以传递任意的字节输出流对象，可以指定缓冲区大小。 5.4.2.2. 普通方法 1 public synchronized void write(int b) throws IOException 写一个字节 1 public void write(byte b[]) throws IOException 写字节数组，继承自 FilterOutputStream 类的方法 1 public synchronized void write(byte b[], int off, int len) throws IOException 写字节数组的一部分 5.4.3. BufferedInputStream（缓冲输入流、读数据） 1 public class BufferedInputStream extends FilterInputStream BufferedInputStream 继承了 FilterInputStream 最终是继承 InputStream。\n5.4.3.1. 构造方法 1 public BufferedInputStream(InputStream in) 通过 InputStream 对象来创建一个 BufferedInputStream。如：new BufferedInputStream(new FileInputStream(\u0026quot;xxx\u0026quot;)); 1 public BufferedInputStream(InputStream in, int size) 可以传递任意的字节输入流对象，可以指定缓冲区大小。 5.4.3.2. 普通方法 1 public synchronized int read() throws IOException 读取一个字节 1 public int read(byte b[]) throws IOException 读取一个字节数组，继承自 FilterInputStream 类的方法 1 private int read1(byte[] b, int off, int len) throws IOException 读取数组的部分 5.5. 字节流综合案例 5.5.1. 案例1：4 种字节流复制文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 package com.moon; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; /* * 关卡1训练案例12 * 四种复制文件方式比较 * 1.字节流复制文件一次读写一个字节 * 2.字节流复制文件一次读写一个字节数组 * 3.高效流复制文件一次读写一个字节 * 4.高效流复制文件一次读取一个字节数组 * 利用上面四种方式复制同一个文件，输出每一种复制方式花费的时间。 */ public class Test1_12 { public static void main(String[] args) throws IOException { // 创建复制的源文件路径对象和目标文件路径对象 File src = new File(\u0026#34;E:\\\\abc.zip\u0026#34;); File copy2 = new File(\u0026#34;e:\\\\我是复制品，Please kill me~2.zip\u0026#34;); File copy3 = new File(\u0026#34;e:\\\\我是复制品，Please kill me~3.zip\u0026#34;); File copy4 = new File(\u0026#34;e:\\\\我是复制品，Please kill me~4.zip\u0026#34;); method04(src, copy4); method03(src, copy3); method02(src, copy2); // method01(src, copy); } // 高效流复制文件一次读取一个字节数组 public static void method04(File src, File copy4) throws IOException { // 记录当前系统时间毫秒值 long start = System.currentTimeMillis(); // 创建高效字节输入输出流对象 BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src)); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(copy4)); // 高效流复制文件一次读取一个字节数组 byte[] b = new byte[1024]; int len; while ((len = bis.read(b)) != -1) { bos.write(b, 0, len); } // 释放资源 bos.close(); bis.close(); // 输出完成复制需要的时间 System.out.println(\u0026#34;高效流复制文件一次读取一个字节数组耗时：\u0026#34; + (System.currentTimeMillis() - start)); } // 高效流复制文件一次读写一个字节 public static void method03(File src, File copy) throws IOException { // 记录当前系统时间毫秒值 long start = System.currentTimeMillis(); // 创建高效字节输入输出流对象 BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src)); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(copy)); // 高效流复制文件一次读写一个字节 int b; while ((b = bis.read()) != -1) { bos.write(b); } // 释放资源 bos.close(); bis.close(); // 输出完成复制需要的时间 System.out.println(\u0026#34;高效流复制文件一次读写一个字节耗时：\u0026#34; + (System.currentTimeMillis() - start)); } // 字节流复制文件一次读写一个字节数组 public static void method02(File src, File copy) throws IOException { // 记录当前系统时间毫秒值 Long start = System.currentTimeMillis(); // 创建字节输入输出流对象 FileInputStream fis = new FileInputStream(src); FileOutputStream fos = new FileOutputStream(copy); // 字节流复制文件一次读写一个字节数组 byte[] b = new byte[1024]; int len; while ((len = fis.read(b)) != -1) { fos.write(b, 0, len); } // 释放资源 fos.close(); fis.close(); // 输出完成复制需要的时间 System.out.println(\u0026#34;字节流复制文件一次读写一个字节数组耗时：\u0026#34; + (System.currentTimeMillis() - start)); } // 字节流复制文件一次读写一个字节 public static void method01(File src, File copy) throws IOException { // 记录当前系统时间毫秒值 Long start = System.currentTimeMillis(); // 创建字节输入输出流对象 FileInputStream fis = new FileInputStream(src); FileOutputStream fos = new FileOutputStream(copy); // 字节流复制文件一次读写一个字节 int b; while ((b = fis.read()) != -1) { fos.write(b); } // 释放资源 fos.close(); fis.close(); // 输出完成复制需要的时间 System.out.println(\u0026#34;字节流复制文件一次读写一个字节耗时：\u0026#34; + (System.currentTimeMillis() - start)); } } 5.5.2. 案例2：字节流组合 File 类，复制文件夹下所有文件（包括文件夹与文件） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 package com.moon; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; /* * 关卡2训练案例3(增加需要，指定文件夹下有多个文件夹，将所有文件（含文件夹复制到目标文件夹中）) * 在 C 盘下创建一个目录 d1,在目录 d1 下创建创建若干个文本文件， * 并在每一个文本文件中添加若干内容。利用字节高效流将该目录下的所有文件复制到 C 盘下的 d2 目录下。 */ public class Test2_03 { public static void main(String[] args) throws IOException { // 创建复制源和目标路径对象 File src = new File(\u0026#34;e:\\\\srcFile\u0026#34;); File copy = new File(\u0026#34;e:\\\\copyFile\u0026#34;); // 调用复制所有文件的方法 copyAllfile(src, copy); } /** * 递归方法读取源路径中的所有文件 * * @param src * 源文件路径 * @param copy * 目标文件路径 * @throws IOException */ public static void copyAllfile(File src, File copy) throws IOException { // 先判断目标路径的文件是否存在，如果不存在，就创建文件夹 if (!copy.exists()) { copy.mkdirs(); } // 使用listFiles方法获取当前文件下所有file对象的数组 File[] list = src.listFiles(); // 遍历数组，判断如果是文件，直接判断到目标文件路径中 for (File f : list) { // 将目标路径修改成需要的路径 File copyNewFile = new File(copy, f.getName()); if (f.isFile()) { // 调用复制文件的方法进行文件复制 copyFile(f, copyNewFile); } else { copyAllfile(f, copyNewFile); } } } /** * 复制文件到目标路径的方法 * * @param src * 源文件路径 * @param copy * 目标文件路径 * @throws IOException */ public static void copyFile(File src, File copy) throws IOException { // 创建高效字节输入输出流对象 BufferedInputStream bis = new BufferedInputStream(new FileInputStream(src)); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(copy)); // 使用一次读取数组的方式复制文件 byte[] b = new byte[1024]; int len; while ((len = bis.read(b)) != -1) { bos.write(b, 0, len); } // 释放资源 bis.close(); bos.close(); } } 6. 转换流 6.1. 概念 为什么要使用转换流？因为使用 FileReader 或 BufferedReader 读取文件时，默认使用的编码是 GBK。如果读取的文件的编码格式是 UTF-8 时，则不能使用 FileReader 或 BufferedReader 读取。此时就需要利用转换流读取文件的内容。\n转换流的作用：字节流和字符流相互转换的桥梁。\n转换流是字符流的一种，创建对象时传入对应字节流对象即可完成转换动作。转换流同样使用了包装的思想，其构造方法接收的同样为 IO 流对象，并非某个文件资源。关闭转换流的同时即关闭了对应的字节流。\n构造方法传入字节流对象自身调用字符流的方法\n6.2. 字符编码表 6.2.1. 什么是编码表 将现实生活中的文字对应数字，存储的是数字的二进制。\n编码表：就是生活中字符和计算机二进制的对应关系表。\n6.2.2. ASCII 码表 ASCII 码表：American Standard Code for Information Interchange/美国信息交换标准代码\n一个字节中的 7 位就可以表示。对应的字节都是正数。0 ~ xxxxxx\n其他常见码表有：ASCII, ISO-8859-1, GBK, UTF-8\n6.2.3. 支持中文的码表 GB2312：简体中文码表。兼容 ASCII 码表，并加入了中文字符，包含 6000~7000 中文和符号。用两个字节表示。两个字节第一个字节是负数,第二个字节可能是正数 GBK：目前最常用的中文码表，兼容 GB2312 码表，2 万的中文和符号。用两个字节表示，其中的一部分文字，第一个字节开头是 1，第二字节开头是 0 GB18030：最新的中文码表，目前还没有正式使用。 Unicode码表：国际码表，包含各国大多数常用字符，每个字符都占 2 个字节，因此有 65536 个字符映射关系。Java 语言使用的 char 类型就是使用 Unicode 码表，如 char c = 'a'; 占两个字节。 UTF-8：基于 unicode，一个字节就可以存储数据，不要用两个字节存储，而且这个码表更加的标准化 中文一般使用 3 个字节表示 6.2.4. 编码/解码 编码：将文字对应到数字，如：a -\u0026gt; 97 解码: 将数字对应到文字，如：97 -\u0026gt; a 6.2.5. 乱码 乱码，是指因为文本在存储时使用的映射码表和在读取时使用的码表不一致造成的。\n当将字符串转为对应的数字字节时，需要指定码表，则存储了为该字符该码表对应的数字字节，如果使用了其他码表重写翻译回字符串，则拼写的新字符串会乱码。\n对于 IO 操作，与字符串编码表使用类似，当以某个码表写出字节数据时，又使用另外码表展示，会出现乱码。\n6.3. OutputStreamWriter（输出转换流） 6.3.1. 继承体系 1 public class OutputStreamWriter extends Writer OutputStreamWriter 是字符流通向字节流的桥梁：可使用指定的 charset 将要写入流中的字符编码成字节。\n6.3.2. 构造方法 通过构造函数看出，可以完成字节输出流转换为字符输出流。\n1 public OutputStreamWriter(OutputStream out) 使用默认的编码表创建 OutputStreamWriter。默认是 GBK 1 public OutputStreamWriter(OutputStream out, String charsetName) throws UnsupportedEncodingException 使用指定的编码表创建 OutputStreamWrite，参数 charsetName 可选值如：gbk/GBK, utf-8/UTF-8 等等。默认是 GBK，通常在使用 UTF-8 的时候才需要指定。 示例：\n1 2 3 4 5 6 7 // 构造方式1: // 1.创建字节输出流 FileOutputStream fos = new FileOutputStream(\u0026#34;a.txt\u0026#34;); // 2.将字节流转为字符流，即通过字节流对象创建转换流对象 OutputStreamWriter osw1 = new OutputStreamWriter(fos); // 构造方式2（常用）: OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream(\u0026#34;a.txt\u0026#34;), StandardCharsets.UTF_8); 6.3.3. 常用方法 1 public void write(int c) throws IOException 写入单个字符。 1 public void write(char cbuf[]) throws IOException 写入字符数组。继承自 Writer 1 public void write(char cbuf[], int off, int len) throws IOException 写入字符数组的某一部分。 1 public void write(String str) throws IOException 写入字符串。继承自 Writer 1 public void write(String str, int off, int len) throws IOException 写入字符串的某一部分。 6.3.4. 字符流转字节流的过程 首先通过 OutputStreamWriter 查询指定码表，将要输出的内容转换成对应的字节。 然后将转换的字节交给 FileOutputStream 输出到文件。 最后关闭流释放资源。 6.4. InputStreamReader（输入转换流） 6.4.1. 继承体系 1 public class InputStreamReader extends Reader InputStreamReader 是字节流通向字符流的桥梁：它使用指定的 charset 读取字节并将其解码为字符\n6.4.2. 构造方法 通过构造函数看出，可以完成字节输入流转换为字符输入流。\n1 public InputStreamReader(InputStream in) 使用默认的编码表创建 InputStreamReader 1 public InputStreamReader(InputStream in, String charsetName) throws UnsupportedEncodingException 使用指定的编码表创建 InputStreamReader，参数 charsetName 可选值：GBK/UTF-8 示例：\n1 2 3 4 5 6 7 // 构造方式1: // 1.创建字节输入流 FileInputStream fis = new FileInputStream(\u0026#34;a.txt\u0026#34;); // 2.将字节流转为字符流，即通过字节流对象创建转换流对象 InputStreamReader isr1 = new InputStreamReader(fis); // 构造方式2（常用）: InputStreamReader isr2 = new InputStreamReader(new FileInputStream(\u0026#34;a.txt\u0026#34;), StandardCharsets.UTF_8); 6.4.3. 常用方法 1 public int read() throws IOException 读取单个字符。 1 public int read(char cbuf[]) throws IOException 将字符读入数组。继承自 Reader。 1 public int read(char cbuf[], int offset, int length) throws IOException 将字符读入数组中的某一部分。 6.4.4. 字节流转换字符流的过程 由字节输入流去目标文件中读取数据，获得对应的字节。 然后将字节交给转换流去查询对应的编码表，得到对应的字符。 最后关闭流释放资源。 6.5. 转换流与字符流子类 6.5.1. 两者的区别 Writer 字符输出流：\nOutputStreamWriter：转换流写入数据，可以指定字符编码表。 FileWriter：字符输出流，采用默认的字符编码表（GBK）。 Reader 字符输入流：\nInputStreamReader：转换流读取数据，可以指定字符编码表。 FileReader：字符输入流，采用默认的字符编码表（GBK）。 6.5.2. FileReader / FileWriter 原理 FileReader / FileWriter 构造方法实际上使用的是 InputStreamReader / OutputStreamWriter 的构造方法中的默认码表。\n字符流其实用的就是转换流，只是使用默认码表，不能指定编码表而已。\n6.5.3. 转换流与字符流使用选择 什么时候使用转换流，什么时候使用字符流 FileReader / FileWriter？\n如果需要修改默认的码表，必须使用转换流（默认的码表是：GBK）。可以使用转换流包装字节缓冲输入输出流。 如果不需要指定码表，使用 FileReader / FileWriter (代码简单一点) 7. 打印流 7.1. 打印流的概念与分类 打印流的作用是：为其他流添加功能，使其能方便输出各种数据类型的值。其最大的特点是：只有输出数据的流，没有读取数据的流。分成以下两类：\n字节打印流: java.io.PrintStream，继承了 FilterOutputStream，顶层父类是 OutputStream 1 public class PrintStream extends FilterOutputStream implements Appendable, Closeable 字符打印流: java.io.PrintWriter， 继承 Writer 1 public class PrintWriter extends Writer Tips: 以上两种流的方法使用是一样的。\n7.2. 打印流使用方法 PrintWrite 与 PrintStream 使用方法一样，下面以 PrintStream 为示例说明\n7.2.1. PrintStream 类构造方法 PrintStream 类有很多构造方法，以下是常用的构造方法介绍：\n1 public PrintStream(String fileName) throws FileNotFoundException 创建具有指定文件名称且不带自动行刷新的新打印流。 1 public PrintStream(File file) throws FileNotFoundException 创建具有指定文件且不带自动行刷新的新打印流。 1 public PrintStream(OutputStream out, boolean autoFlush) 根据字节输入流出，并可以指定是否自动行刷新，创建打印流 1 public PrintStream(OutputStream out, boolean autoFlush, String encoding) 根据字节输入流出，并可以指定是否自动行刷新、指定字符编码，创建打印流 示例：在创建字节流时指定可以追加输出\n1 2 // 如果需要追加输出，也可以在创建 FileOutputStream 对象时指定 append 参数为 true PrintStream ps = new PrintStream(new FileOutputStream(\u0026#34;ps.txt\u0026#34;,true)); 7.2.2. PrintStream 类成员方法 PrintStream 类常用的成员方法主要是 print 与 println，并有大量的重载方法\n1 public void print(数据类型 变量名); 将指定数据类型的值打印到流关联目标文件中，不换行。 1 public void println(数据类型 变量名); 将指定数据类型的值打印到流关联目标文件中，换行。 8. IO 流总结（字符流和字节流） 8.1. Java IO 体系图 8.2. 字节流与字符流的区别 字节流：以字节为单位输入输出数据，按照 8 位传输。 字符流：以字符为单位输入输出数据，按照 16 位传输。 字节流可以处理所有格式的文件。 字符流在处理文本的效率比字节流高。 在 Java 中，可以根据结尾来判断是字节流还是字符流。\nInputStream/OutputStream：字节流 Writer/Reader：字符流 8.2.1. 字节流和字符流的选择 绝大多数情况下使用字节流会更好，因为字节流是字符流的包装，而大多数时候 IO 操作都是直接操作磁盘文件，所以这些流在传输时都是以字节的方式进行的（图片等都是按字节存储的）。 如果操作需要通过 IO 在内存中频繁处理字符串的情况，使用字符流会比较好，因为字符流具备缓冲区，提高了性能。 9. BIO 编程 BIO 有的称之为 basic(基本) IO，有的称之为 block(阻塞) IO，主要应用于文件 IO 和网络 IO。\nBIO 主要的 API 在 java.io 包中，其中重点包含 5 个类（File、OutputStream、InputStream、Writer、Reader）和 1 个接口（Serializable）。\n9.1. 基于 BIO 的网络 IO 在 JDK1.4 之前，我们建立网络连接的时候只能采用 BIO，需要先在服务端启动一个 ServerSocket，然后在客户端启动 Socket 来对服务端进行通信，默认情况下服务端需要对每个请求建立一个线程等待请求，而客户端发送请求后，先咨询服务端是否有线程响应，如果没有则会一直等待或者遭到拒绝，如果有的话，客户端线程会等待请求结束后才继续执行，这就是阻塞式 IO\n9.2. 基本用法示例（基于 TCP） 编写TCP服务端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.moon.system.testmodule.bio; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; /** * BIO 编程测试 - TCP服务器端程序 */ public class TCPServer { public static void main(String[] args) throws IOException { // 1. 创建ServerSocket对象，设置端口号为9999 ServerSocket serverSocket = new ServerSocket(9999); while (true) { // 2. 监听客户端 System.out.println(\u0026#34;serverSocket.accept()执行前\u0026#34;); Socket socket = serverSocket.accept(); // 阻塞，等客户端启动 System.out.println(\u0026#34;serverSocket.accept()执行完\u0026#34;); // 3. 从连接中取出输入流来接收消息 InputStream inputStream = socket.getInputStream(); // 阻塞，等待接收客户端发出的消息 byte[] bytes = new byte[1024]; // 读取数据 inputStream.read(bytes); String clientIP = socket.getInetAddress().getHostAddress(); System.out.println(String.format(\u0026#34;%s说：%s\u0026#34;, clientIP, new String(bytes).trim())); // 4. 从连接中取出输出流并回话 OutputStream outputStream = socket.getOutputStream(); outputStream.write(\u0026#34;TCPServer收到消息\u0026#34;.getBytes()); // 5. 关闭socket socket.close(); } } } 上述代码编写了一个服务器端程序，绑定端口号 9999，accept 方法用来监听客户端连接，如果没有客户端连接，就一直等待，程序会阻塞在serverSocket.accept()方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.moon.system.testmodule.bio; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.util.Scanner; /** * BIO 编程测试 - TCP客户端程序 */ public class TCPClient { public static void main(String[] args) throws Exception { while (true) { // 1.创建Socket对象，连接9999端口 Socket socket = new Socket(\u0026#34;127.0.0.1\u0026#34;, 9999); // 2.从连接中取出输出流并发消息 OutputStream outputStream = socket.getOutputStream(); System.out.println(\u0026#34;请输入:\u0026#34;); Scanner sc = new Scanner(System.in); String msg = sc.nextLine(); outputStream.write(msg.getBytes()); // 3.从连接中取出输入流并接收回话 InputStream is = socket.getInputStream(); // 阻塞，一直等待服务端的响应 byte[] bytes = new byte[20]; is.read(bytes); System.out.println(\u0026#34;TCPServer回复：\u0026#34; + new String(bytes).trim()); // 4.关闭 socket.close(); } } } 上述代码编写了一个客户端程序，通过 9999 端口连接服务器端，getInputStream 方法用来等待服务器端返回数据，如果没有返回，就一直等待，程序会阻塞在socket.getInputStream()方法 10. NIO 编程 10.1. 概述 java.nio 全称 java non-blocking IO，是指 JDK 提供的新 API。从 JDK1.4 开始，Java 提供了一系列改进的输入/输出的新特性，被统称为 NIO(即 New IO)。新增了许多用于处理输入输出的类，这些类都被放在 java.nio 包及子包下，并且对原 java.io 包中的很多类进行改写，新增了满足 NIO 的功能。\n10.1.1. Java NIO 和传统 I/O 的区别 Java NIO 和 传统的 BIO 有着相同的目的和作用，但还是有以下的区别：\n实现方式不同：传统 I/O 是面向流的，以流的方式处理数据；NIO 是面向缓冲区的，以块的方式处理数据。在流的操作中，数据只能在一个流中连续进行读写，数据没有缓冲；而面向缓冲区的操作，数据可以从一个 Channel 读取到一个 Buffer 中，再从 Buffer 写入 CHannel 中，可以方便地在缓冲区中进行数据的前后移动等操作。块 I/O 的效率比流 I/O 高很多，这种功能在应用层主要用于数据的粘包、拆包等操作。 传统 I/O 的流操作是阻塞模式的，NIO 是基于多路复用 I/O 模型实现非阻塞模式的。传统的 I/O 中，用户线程调用 read() 或者 write() 进行 I/O 读写操作时，该线程将一直阻塞，直到数据读写完成；而 NIO 通过 Selector 监听 Channel 上事件的变化，在 Channel 上有数据变化时通知该线程进行读写操作。 10.1.2. NIO 三大核心组件 NIO 主要有三大核心部分：Channel(通道)，Buffer(缓冲区), Selector(选择器)。\n传统的 BIO 基于字节流和字符流进行操作，而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作，数据总是从通道读取到缓冲区中，或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件（比如：连接请求，数据到达等），因此使用单个线程就可以监听多个客户端通道。\n10.1.3. NIO 的非阻塞 传统 IO 的各种流是阻塞的。即当一个线程调用 read() 或 write() 时，该线程被阻塞，直到有一些数据被读取，或数据完全写入。该线程在此期间不能再做任何事情了。\nNIO 的非阻塞模式，使一个线程从某通道发送请求读取数据，但是它仅能得到目前可用的数据，如果目前没有数据可用时，就什么都不会获取，而不会保持线程阻塞。所以直至数据变的可以读取之前，该线程可以继续做其他的事情。非阻塞写数据也一样，一个线程请求写入一些数据到某通道，但不需要等待它完全写入，这个线程同时可以去做别的事情。\n线程通常将非阻塞 IO 的空闲时间用于在其它通道上执行 IO 操作，所以一个单独的线程现在可以管理多个输入和输出通道（channel）。\n10.1.4. NIO 和 IO 适用场景 NIO 是为弥补传统 IO 的不足而诞生的，但 NIO 也有自身的缺点。因为 NIO 是面向缓冲区的操作，每一次的数据处理都是对缓冲区进行的，那么在数据处理之前必须要判断缓冲区的数据是否完整或者已经读取完毕，如果没有，假设数据只读取了一部分，那么对不完整的数据处理没有任何意义。所以每次数据处理之前都要检测缓冲区数据。\nNIO 和 IO 各适用的场景：\n如果需要管理同时打开的成千上万个连接，这些连接每次只是发送少量的数据，例如聊天服务器，此时使用 NIO 处理数据可能是个很好的选择。 而如果只有少量的连接，而这些连接每次要发送大量的数据，这时候传统的 IO 更合适。 使用哪种类型来处理数据，需要在数据的响应等待时间和检查缓冲区数据的时间上作比较来权衡选择。\n10.2. 通道（Channel） 10.2.1. 概述 通道（Channel）类似于 BIO 中的 Stream(流)。只是 Stream(流)是单向，分为 InputStream(输入流)和 OutputStream(输出流)；而 NIO 中的通道(Channel)是双向的，既可以用来进行读操作，也可以用来进行写操作。\nNotes: BIO 中的 stream 是单向的，例如 FileInputStream 用来建立到目标（文件，网络套接字，硬件设备等）的一个连接，对象只能进行读取数据的操作。\n10.2.2. Channel 接口实现类 NIO 中常用的 Channel 实现类有：\nChannel 实现类 作用 FileChannel 用于文件的数据读写 DatagramChannel 用于 UDP 协议网络通信的数据读写 ServerSocketChannel Socket Server 用于 TCP 的数据读写 SocketChannel Socket Client 用于 TCP 的数据读写 10.2.3. FileChannel（文件的数据读写） 1 2 3 public abstract class FileChannel extends AbstractInterruptibleChannel implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel 该类主要用来对本地文件进行 IO 操作，主要方法如下\n从通道读取数据并放到缓冲区中 1 public int read(ByteBuffer dst); 把缓冲区的数据写到通道中 1 public int write(ByteBuffer src); 从目标通道中复制数据到当前通道 1 public long transferFrom(ReadableByteChannel src, long position, long count); 把数据从当前通道复制给目标通道 1 public long transferTo(long position, long count, WritableByteChannel target); 10.2.4. ServerSocketChannel（服务 TCP 的数据读写） 1 2 3 public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel ServerSocketChannel，用来在服务器端监听新的客户端 Socket 连接。常用方法如下\n得到一个 ServerSocketChannel 通道 1 public static ServerSocketChannel open() 设置服务器端端口号 1 public final ServerSocketChannel bind(SocketAddress local) 设置阻塞或非阻塞模式，取值 false 表示采用非阻塞模式 1 public final SelectableChannel configureBlocking(boolean block) 接受一个连接，返回代表这个连接的通道对象 1 public SocketChannel accept() 注册一个选择器并设置监听事件 1 public final SelectionKey register(Selector sel, int ops) 10.2.5. SocketChannel（客户端 TCP 的数据读写） 1 2 3 public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel SocketChannel，网络 IO 通道，具体负责进行读写操作。NIO 总是把缓冲区的数据写入通道，或者把通道里的数据读到缓冲区。常用方法如下所示\n得到一个 SocketChannel 通道 1 public static SocketChannel open() 设置阻塞或非阻塞模式，取值 false 表示采用非阻塞模式 1 public final SelectableChannel configureBlocking(boolean block) 连接服务器 1 public boolean connect(SocketAddress remote) 如果上面的方法连接失败，接下来就要通过该方法完成连接操作 1 public boolean finishConnect() 往通道里写数据 1 public int write(ByteBuffer src) 从通道里读数据 1 public int read(ByteBuffer dst) 注册一个选择器并设置监听事件，最后一个参数可以设置共享数据 1 public final SelectionKey register(Selector sel, int ops, Object att) 关闭通道 1 public final void close() 10.3. 缓冲区（Buffer） 10.3.1. 概述 Java IO 面向流意味着每次从流中读一个或多个字节，直至读取所有字节，它们没有被缓存在任何地方。此外，它也不能前后移动流中的数据。如果需要前后移动从流中读取的数据，需要先将它缓存到一个缓冲区。\n而 NIO 的缓冲导向方法不同。数据读取到一个它稍后处理的缓冲区，需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是，还需要检查是否该缓冲区中包含所有需要处理的数据。而且，需确保当更多的数据读入缓冲区时，不要覆盖缓冲区里尚未处理的数据。\n缓冲区（Buffer）：实际上是一个容器，其内部通过一个连续的字节数组存储 I/O 上的数据。缓冲区对象内置了一些机制，能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道，但是读取或写入的数据都必须经由 Buffer，如下图所示（以文件读写为例）：\n10.3.2. Buffer 抽象实现类 在 NIO 中，java.nio.Buffer 是一个顶层抽象类，对于 Java 中的不同的基本数据类型都有一个 Buffer 类型实现与之相对应。常用的 Buffer 子类如下：\nBuffer 子类 作用类型 ByteBuffer 存储字节数据到缓冲区 ShortBuffer 存储字符串数据到缓冲区 CharBuffer 存储字符数据到缓冲区 IntBuffer 存储整数数据到缓冲区 LongBuffer 存储长整型数据到缓冲区 DoubleBuffer 存储小数到缓冲区 FloatBuffer 存储小数到缓冲区 10.3.3. ByteBuffer（存储字节数据） 最常用的自然是 ByteBuffer 类（二进制数据），该类的主要方法如下所示\n存储字节数据到缓冲区 1 public abstract ByteBuffer put(byte[] b); 从缓冲区获得字节数据 1 public abstract byte[] get(); 把缓冲区数据转换成字节数组 1 public final byte[] array(); 设置缓冲区的初始容量 1 public static ByteBuffer allocate(int capacity); 把一个现成的数组放到缓冲区中使用 1 public static ByteBuffer wrap(byte[] array); 翻转缓冲区，重置位置到初始位置。相当于切换模式，如：『写模式切换成读模式』或者『读模式切换成写模式』 1 public final Buffer flip(); 清空整个缓冲区数据。 1 public Buffer clear(); 清空缓冲区部分数据。只清除已经读取的数据，未读取的数据会被移到 buffer 的开头，此时写入数据会从当前数据的末尾开始。 1 public abstract ByteBuffer compact(); 10.4. 选择器（Selector） 10.4.1. 概述 一般的 IO 操作，如果用阻塞 I/O，需要多线程（浪费内存）；如果用非阻塞 I/O，需要不断重试（耗费CPU）。\nSelector（选择器），能够检测多个注册的 Channel 通道上是否有 I/O 事件发生，如果有事件发生，便获取事件然后针对每个事件进行相应的响应和处理。因此只用一个 Selector 单线程去管理多个通道，也就是管理多个连接，并且不必为每个连接都创建一个线程，避免了多线程之间的上下文切换导致的开销。同时，Selector 只有在 Channel 有真正有读写事件发生时，才会调用 I/O 函数来进行读写，从而大大地减少了系统开销。\n10.4.2. Selector 类关系图 Selector 类关系图如下：\n10.4.3. Selector 常用方法 得到一个选择器对象 1 public static Selector open(); 监控所有注册的通道，当其中有 IO 操作可以进行时，将对应的 SelectionKey 加入到内部集合中并返回，参数用来设置超时时间 1 public int select(long timeout); 从内部集合中得到所有的 SelectionKey 1 public Set\u0026lt;SelectionKey\u0026gt; selectedKeys(); 获取所有准备就绪的网络通道 1 public abstract Set\u0026lt;SelectionKey\u0026gt; keys(); 10.4.4. SelectionKey 类(网络通道key) SelectionKey，代表了 Selector 和网络通道的注册关系，一共四种：\nint OP_ACCEPT：有新的网络连接可以接受，值为 16 int OP_CONNECT：代表连接已经建立，值为 8 int OP_READ：代表读操作，值为 1 int OP_WRITE：代表写操作，值为 4 该类的常用方法如下所示：\n得到与之关联的 Selector 对象 1 public abstract Selector selector() 得到与之关联的通道 1 public abstract SelectableChannel channel() 得到与之关联的共享数据 1 public final Object attachment() 设置或改变监听事件 1 public abstract SelectionKey interestOps(int ops) 是否可以 accept 1 public final boolean isAcceptable() 是否可以读 1 public final boolean isReadable() 是否可以写 1 public final boolean isWritable() 10.5. 文件 NIO 示例 测试使用 NIO 进行本地文件的读、写和复制操作，和 BIO 进行对比\n10.5.1. 往本地文件中写数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /* 往本地文件中写数据 */ @Test public void testWrite() throws Exception { // 1. 创建输出流 FileOutputStream fileOutputStream = new FileOutputStream(\u0026#34;E:\\\\moon.txt\u0026#34;); // 2. 从流中得到一个通道 FileChannel fileChannel = fileOutputStream.getChannel(); // 3. 提供一个缓冲区 ByteBuffer buffer = ByteBuffer.allocate(1024); // 4. 往缓冲区中存入数据 String str = \u0026#34;hello,nio\u0026#34;; buffer.put(str.getBytes()); // 5. 翻转缓冲区 buffer.flip(); // 6. 把缓冲区写到通道中 fileChannel.write(buffer); // 7. 关闭 fileOutputStream.close(); } NIO 中的通道是从输出流对象里通过 getChannel 方法获取到的，该通道是双向的，既可以读，又可以写。在往通道里写数据之前，必须通过 put 方法把数据存到 ByteBuffer 中，然后通过通道的 write 方法写数据。在 write 之前，需要调用 flip 方法翻转缓冲区，把内部重置到初始位置，这样在接下来写数据时才能把所有数据写到通道里\n10.5.2. 从本地文件中读数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /* 从本地文件中读取数据 */ @Test public void test2() throws Exception { File file = new File(\u0026#34;E:\\\\moon.txt\u0026#34;); // 1. 创建输入流 FileInputStream fileInputStream = new FileInputStream(file); // 2. 得到一个通道 FileChannel fileChannel = fileInputStream.getChannel(); // 3. 准备一个缓冲区 ByteBuffer buffer = ByteBuffer.allocate((int) file.length()); // 4. 从通道里读取数据并存到缓冲区中 fileChannel.read(buffer); System.out.println(new String(buffer.array())); // 5. 关闭 fileInputStream.close(); } 上面示例从输入流中获得一个通道，然后提供 ByteBuffer 缓冲区，该缓冲区的初始容量和文件的大小一样，最后通过通道的 read 方法把数据读取出来并存储到了 ByteBuffer 中\n10.5.3. 复制本地文件 以下示例通过传统的 BIO 复制一个文件，分别通过输入流和输出流实现了文件的复制\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /* 使用 BIO 实现文件复制 */ @Test public void testBioCopy() throws Exception { // 1. 创建两个流 FileInputStream fileInputStream = new FileInputStream(\u0026#34;E:\\\\moon.txt\u0026#34;); FileOutputStream fileOutputStream = new FileOutputStream(\u0026#34;E:\\\\moon_copy.txt\u0026#34;); // 2. 定义字节数组，使用一次读取数组方式复制文件 byte[] bytes = new byte[1024]; int len; while ((len = fileInputStream.read(bytes)) != -1) { fileOutputStream.write(bytes, 0, len); } // 3. 关闭资源 fileInputStream.close(); fileOutputStream.close(); } 以下示例使用 NIO 实现文件复制，分别从两个流中得到两个通道，sourceCh 负责读数据，destCh 负责写数据，然后直接调用 transferFrom 方法一步到位实现了文件复制\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* 使用 NIO 实现文件复制 */ @Test public void testNioCopy() throws Exception { // 1. 创建两个流 FileInputStream fileInputStream = new FileInputStream(\u0026#34;E:\\\\moon.txt\u0026#34;); FileOutputStream fileOutputStream = new FileOutputStream(\u0026#34;E:\\\\moon_copy.txt\u0026#34;); // 2. 得到两个通道 FileChannel fileInChannel = fileInputStream.getChannel(); FileChannel fileOutChannel = fileOutputStream.getChannel(); // 3. 复制 fileOutChannel.transferFrom(fileInChannel, 0, fileInChannel.size()); // 4. 关闭 fileInputStream.close(); fileOutputStream.close(); } 10.6. 网络 IO 10.6.1. 概述 Java NIO 中的网络通道是非阻塞 IO 的实现，基于事件驱动，非常适用于服务器需要维持大量连接，但是数据交换量不大的情况，例如一些即时通信的服务等等\u0026hellip;\n如下图描述，从一个客户端向服务端发送数据，然后服务端接收数据的过程。客户端发送数据时，必须先将数据存入 Buffer 中，然后将 Buffer 中的内容写入通道。服务端这边接收数据必须通过 Channel 将数据读入到 Buffer 中，然后再从 Buffer 中取出数据来处理。\n在 Java 中编写 Socket 服务器，通常有以下几种模式：\n一个客户端连接用一个线程。 优点：程序编写简单。 缺点：如果连接非常多，分配的线程也会非常多，服务器可能会因为资源耗尽而崩溃。 将每一个客户端连接交给一个拥有固定数量线程的连接池。 优点：程序编写相对简单，可以处理大量的连接。 缺点：线程的开销非常大，连接如果非常多，排队现象会比较严重。 【推荐】使用 Java 的 NIO，用非阻塞的 IO 方式处理。这种模式可以用一个线程，处理大量的客户端连接 10.6.2. 基础示例 需求分析：实现服务器端和客户端之间的数据通信（非阻塞）。\n网络服务器端程序。用 NIO 实现了一个服务器端程序，能不断接受客户端连接并读取客户端发过来的数据。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package com.moon.system.testmodule.nio.socket; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; /** * NIO案例 - 网络服务器端程序 */ public class NIOServer { public static void main(String[] args) throws Exception { // 1. 得到一个ServerSocketChannel对象 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 2. 得到一个Selector对象 Selector selector = Selector.open(); // 3. 绑定一个端口号 serverSocketChannel.bind(new InetSocketAddress(9999)); // 4. 设置非阻塞方式 serverSocketChannel.configureBlocking(false); // 5. 把ServerSocketChannel对象注册给Selector对象 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 6. 处理逻辑 while (true) { // 6.1 监控客户端 if (selector.select(2000) == 0) { // nio非阻塞式的优势 System.out.println(\u0026#34;Server:没有客户端搭理我，我就干点别的事\u0026#34;); continue; } // 6.2 得到SelectionKey,判断通道里的事件 Iterator\u0026lt;SelectionKey\u0026gt; keyIterator = selector.selectedKeys().iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); // 客户端连接请求事件 if (key.isAcceptable()) { System.out.println(\u0026#34;OP_ACCEPT\u0026#34;); SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); // 将每个新连接的通道注册给Selector对象 socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); } // 读取客户端数据事件 if (key.isReadable()) { SocketChannel channel = (SocketChannel) key.channel(); // 获取客户端发送的附件，读取数据放到缓冲区 ByteBuffer buffer = (ByteBuffer) key.attachment(); channel.read(buffer); System.out.println(\u0026#34;客户端发来数据：\u0026#34; + new String(buffer.array())); } // 6.3 手动从集合中移除当前key,防止重复处理 keyIterator.remove(); } } } } 网络客户端程序。通过 NIO 实现了一个客户端程序，连接上服务器端后发送了一条数据。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.moon.system.testmodule.nio.socket; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; /** * NIO案例 - 网络客户端程序 */ public class NIOClient { public static void main(String[] args) throws Exception { // 1. 得到一个网络通道 SocketChannel socketChannel = SocketChannel.open(); // 2. 设置非阻塞方式 socketChannel.configureBlocking(false); // 3. 提供服务器端的IP地址和端口号 InetSocketAddress address = new InetSocketAddress(\u0026#34;127.0.0.1\u0026#34;, 9999); // 4. 连接服务器端 if (!socketChannel.connect(address)) { while (!socketChannel.finishConnect()) { // nio作为非阻塞式的优势 System.out.println(\u0026#34;Client:连接服务器端的同时，我还可以干别的一些事情\u0026#34;); } } // 5. 得到一个缓冲区并存入数据 String msg = \u0026#34;hello,Server\u0026#34;; ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes()); // 6. 发送数据 socketChannel.write(byteBuffer); System.in.read(); // 为了不让程序停止（因为客户端停止，服务端会抛出异常，暂时不想多做处理），特意设置等待输入，让程序阻塞在此处 } } NIO 示例运行效果：\n10.6.3. 网络聊天案例 需求：使用NIO实现多人聊天\n使用 NIO 编写了一个聊天程序的服务器端，可以接受客户端发来的数据，并能把数据广播给所有客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 package com.moon.system.testmodule.nio.chat; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.Channel; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Iterator; /** * NIO案例 - 聊天程序服务器端 */ public class ChatServer { /* 定义监听通道 */ private ServerSocketChannel listenerChannel; /* 选择器对象 */ private Selector selector; /* 服务器端口 */ private static final int PORT = 9999; /* 缓冲区字节数组大小 */ private static final int BYTE_SIZE = 1024; // 创建基于JDK1.8的DateTimeFormatter（线程安全） private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(\u0026#34;yyyy-MM-dd HH:mm:ss\u0026#34;); /** * 定义构造方法，初始化相关设置 */ public ChatServer() { try { // 1. 得到监听通道 listenerChannel = ServerSocketChannel.open(); // 2. 得到选择器 selector = Selector.open(); // 3. 绑定端口 listenerChannel.bind(new InetSocketAddress(PORT)); // 4. 设置为非阻塞模式 listenerChannel.configureBlocking(false); // 5. 将选择器绑定到监听通道并监听accept事件 listenerChannel.register(selector, SelectionKey.OP_ACCEPT); printInfo(\u0026#34;Chat Server is ready.......\u0026#34;); } catch (IOException e) { e.printStackTrace(); } } /** * 服务端的相关业务逻辑 * * @throws Exception */ public void start() throws Exception { try { /* 循环不停的监控 */ while (true) { // 判断是否有新的客户端连接 if (selector.select(2000) == 0) { System.out.println(\u0026#34;Server:暂无新客户端连接，可进行其他业务逻辑\u0026#34;); continue; } // 获取所有的网络通道key Iterator\u0026lt;SelectionKey\u0026gt; keyIterator = selector.selectedKeys().iterator(); // 迭代所有网络通道 while (keyIterator.hasNext()) { SelectionKey selectionKey = keyIterator.next(); // 客户端连接请求事件 if (selectionKey.isAcceptable()) { // 获取客户端连接通道对象 SocketChannel socketChannel = listenerChannel.accept(); // 设置非阻塞方式 socketChannel.configureBlocking(false); // 将每个新连接的通道注册给Selector对象 socketChannel.register(selector, SelectionKey.OP_READ); // 做客户端连接成功后的相关业务逻辑... System.out.println(socketChannel.getRemoteAddress().toString().substring(1) + \u0026#34;上线了...\u0026#34;); } // 读取客户端数据事件 if (selectionKey.isReadable()) { readMsg(selectionKey); } // 手动从集合中移除当前key,防止重复处理 keyIterator.remove(); } } } catch (IOException e) { e.printStackTrace(); } } /** * 读取客户端发来的消息并广播出去 * * @param selectionKey 网络通道key */ private void readMsg(SelectionKey selectionKey) throws Exception { // 根据key获取客户端连接通道 SocketChannel channel = (SocketChannel) selectionKey.channel(); // 创建缓冲区 ByteBuffer buffer = ByteBuffer.allocate(BYTE_SIZE); // 获取客户端发送的附件，读取数据放到缓冲区 int count = channel.read(buffer); // 判断是否读取到客户端消息 if (count \u0026gt; 0) { String msg = new String(buffer.array()); // 打印消息 this.printInfo(msg); // 将消息发送广播 broadCast(channel, msg); } } /** * 给所有的客户端发广播 * * @param channel 客户端连接通道 * @param msg 消息字符串 */ private void broadCast(SocketChannel channel, String msg) { System.out.println(\u0026#34;服务器发送了广播...\u0026#34;); /* * 过Selector对象以下方法，获取所有准备就绪的网络，循环所有客户端网络通道key * public abstract Set\u0026lt;SelectionKey\u0026gt; keys(); */ this.selector.keys().forEach(key -\u0026gt; { // 获取其他客户端的连接通道对象 Channel targetChannel = key.channel(); // 判断排除本身以内的其他客户端连接通道 if (targetChannel instanceof SocketChannel \u0026amp;\u0026amp; targetChannel != channel) { SocketChannel destChannel = (SocketChannel) targetChannel; // 获取缓冲区 ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes()); try { // 通过连接通道，发送信息 destChannel.write(buffer); } catch (IOException e) { e.printStackTrace(); } } }); } /** * 往控制台打印消息 * * @param str 输入的信息 */ private void printInfo(String str) { System.out.println(\u0026#34;[\u0026#34; + DATE_TIME_FORMATTER.format(LocalDateTime.now()) + \u0026#34;] -\u0026gt; \u0026#34; + str); } public static void main(String[] args) throws Exception { // 测试 new ChatServer().start(); } } 通过 NIO 编写了一个聊天程序的客户端，可以向服务器端发送数据，并能接收服务器广播的数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 package com.moon.system.testmodule.nio.chat; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; /** * NIO案例 - 聊天程序客户端 */ public class ChatClient { /* 定义服务器地址 */ private static final String HOST = \u0026#34;127.0.0.1\u0026#34;; /* 定义服务器端口 */ private static final int PORT = 9999; /* 定义网络通道 */ private SocketChannel socketChannel; /* 聊天用户名 */ private String userName; /* 缓冲区字节数组大小 */ private static final int BYTE_SIZE = 1024; /** * 定义构造方法，初始化业务设置 * * @throws Exception */ public ChatClient() throws Exception { // 1. 得到一个网络通道 socketChannel = SocketChannel.open(); // 2. 设置非阻塞方式 socketChannel.configureBlocking(false); // 3. 提供服务器端的IP地址和端口号 InetSocketAddress address = new InetSocketAddress(HOST, PORT); // 4. 连接服务器端 if (!socketChannel.connect(address)) { while (!socketChannel.finishConnect()) { System.out.println(\u0026#34;Client:连接服务器端的同时，我还可以干别的一些事情\u0026#34;); } } // 5. 得到客户端IP地址和端口信息，作为聊天用户名使用 userName = socketChannel.getLocalAddress().toString().substring(1); System.out.println(\u0026#34;---------------Client(\u0026#34; + userName + \u0026#34;) is ready---------------\u0026#34;); } /** * 向服务器端发送数据 * * @param msg 消息字符串 * @throws Exception */ public void sendMsg(String msg) throws Exception { // 定义结束聊天的信息 if (msg.equalsIgnoreCase(\u0026#34;bye\u0026#34;)) { socketChannel.close(); return; } // 给服务端发送数据 msg = userName + \u0026#34;说：\u0026#34; + msg; ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes()); socketChannel.write(buffer); } /** * 从服务器端接收数据 * * @throws Exception */ public void receiveMsg() throws Exception { // 获取字节缓冲区 ByteBuffer buffer = ByteBuffer.allocate(BYTE_SIZE); // 读取数据 int size = socketChannel.read(buffer); if (size \u0026gt; 0) { // 如果有接收数据，进行相关业务逻辑处理 String msg = new String(buffer.array()); System.out.println(msg.trim()); } } } 运行了聊天程序的客户端，并在主线程中发送数据，在另一个线程中不断接收服务器端的广播数据，该代码运行一次就是一个聊天客户端，可以同时运行多个聊天客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class TestChat { public static void main(String[] args) throws Exception { // 启动客户端 ChatClient chatClient = new ChatClient(); // 单独开一个线程不断的接收服务器端广播的数据 new Thread(() -\u0026gt; { while (true) { try { // 接收服务端发送的数据 chatClient.receiveMsg(); // 休眠2秒 Thread.currentThread().sleep(2000); } catch (Exception e) { e.printStackTrace(); } } }).start(); // 模拟客户端输入消息 Scanner scanner = new Scanner(System.in); while (scanner.hasNextLine()) { String msg = scanner.nextLine(); // 向服务端发送消息 chatClient.sendMsg(msg); } } } 11. AIO 编程 JDK 7 引入了 Asynchronous I/O，即 AIO（也称 NIO 2.0），叫做异步不阻塞的 IO 模型，是基于事件和回调机制实现。AIO 引入异步通道的概念，采用了 Proactor 模式，简化了程序编写，一个有效的请求才启动一个线程，它的特点是先由操作系统完成后才通知服务端程序启动线程去处理，一般适用于连接数较多且连接时间较长的应用。\n在进行 I/O 编程中，常用到两种模式：Reactor 和 Proactor。Java 的 NIO 就是 Reactor，当有事件触发时，服务器端得到通知，进行相应的处理。\n目前 AIO 还没有广泛应用。Netty 之前也尝试使用过 AIO，不过又放弃了。这是因为 Netty 使用了 AIO 之后，在 Linux 系统上的性能并没有多少提升。\n12. 不同类型的 IO 对比总结 IO 的方式通常分为几种：同步阻塞的 BIO、同步非阻塞的 NIO、异步非阻塞的 AIO。\nBIO 方式适用于连接数目比较小且固定的架构，这种方式对服务器资源要求比较高，并发局限于应用中，JDK1.4 以前的唯一选择，但程序直观简单易理解。 NIO 方式适用于连接数目多且连接比较短（轻操作）的架构，比如聊天服务器，并发局限于应用中，编程比较复杂，JDK1.4 开始支持。 AIO 方式使用于连接数目多且连接比较长（重操作）的架构，比如相册服务器，充分调用 OS 参与并发操作，编程比较复杂，JDK7 开始支持。 对比总结 BIO NIO AIO IO 方式 同步阻塞 同步非阻塞（多路复用） 异步非阻塞 API 使用难度 简单 复杂 复杂 可靠性 差 好 好 吞吐量 低 高 高 举个例子再理解一下：\n同步阻塞：你到饭馆点餐，然后在那等着，啥都干不了，饭馆没做好，你就必须等着！ 同步非阻塞：你在饭馆点完餐，就去玩儿了。不过玩一会儿，就回饭馆问一声：好了没啊！ 异步非阻塞：饭馆打电话说，我们知道您的位置，一会给你送过来，安心玩儿就可以了，类似于现在的外卖。 13. Properties 类 13.1. Properties 概述 1 public class Properties extends Hashtable\u0026lt;Object,Object\u0026gt; java.util.Properties 类继承 Hashtable，实现了 Map 接口，是属性集合（完全可以当成双列集合使用）。\n使用时不需要指定泛型变量，键和值默认都是字符串类型。可以与 IO 流技术相结合，实现从文件中读取数据到集合中，也可以直接将集合的数据保存到文件中。\nProperies 类特点：键和值必须是 String 类型，不支持泛型。与 IO 有关的集合类，对文件进行操作，文件就叫属性文件。\n13.2. Properties 属性文件 属性文件是 Java 中常用的一种文件类型，其扩展名必须是 properties。如：applicatioin.properties\n属性文件内容格式要求：\n一个键值对占一行，格式是：键=值。 1 2 name=MooNkirA age=18 文件中可以使用注释，以 # 开头就是注释行。 1 2 # 我是一行注释 key=abc 文件中如果有空行，则会被忽略。 13.3. 构造方法 1 public Properties() 创建一个无默认值的空属性列表。 1 public Properties(Properties defaults) 创建一个有默认值的空属性列表。 13.4. 属性值操作相关方法 1 public synchronized Object setProperty(String key, String value) 存储键值对，如果键存在，则替换旧的键值对，并返回旧的value值。也可以使用 put 方法设置键值，但一般建议使用 setProperty 方法。 1 public String getProperty(String key) 根据key(键)得到相应的属性值，如果key(键)不存在，则返回 null。 1 public String getProperty(String key, String defaultValue) 通过属性名（key 键）得到属性值，如果属性值不存在，则返回方法参数的 defaultValue，从而保证一定可以得到一个属性值。 1 public synchronized V remove(Object key) 根据key(键)删除对应的值。继承自 HashTable 1 public synchronized int size() 得到 Properties 集合的键值对个数。继承自 HashTable 1 public Set\u0026lt;String\u0026gt; stringPropertyNames() 返回此属性列表中的所有属性名（key 键）的 Set 集合 13.5. 将集合中内容存储到文件 1 public void store(OutputStream out, String comments) throws IOException 通过字节流，将集合中的数据(属性列表)保存到流关联的目标文件中，并加上注释（comments），还会加上保存的时间，汉字使用的是 Unicode 编码。参数 comments 是描述信息，一般给 null 即可 1 public void store(Writer writer, String comments) throws IOException 保存属性到字符流中，并加上注释。还会加上保存的时间。字符流汉字直接写入。 13.6. 读取文件中的数据并保存到属性集合 1 public synchronized void load(InputStream inStream) throws IOException 从字节输入流中读取属性列表（属性名和属性值） 1 public synchronized void load(Reader reader) throws IOException 从字符输入流中读取属性列表（属性名和属性值） 14. 序列化与反序列化 14.1. 概述 引用维基百科对于“序列化”的介绍：\n序列化（serialization）在计算机科学的数据处理中，是指将数据结构或对象状态转换成可取用格式（例如存成文件，存于缓冲，或经由网络中发送），以留待后续在相同或另一台计算机环境中，能恢复原先状态的过程。依照序列化格式重新获取字节的结果时，可以利用它来产生与原始对象相同语义的副本。对于许多对象，像是使用大量引用的复杂对象，这种序列化重建的过程并不容易。面向对象中的对象序列化，并不概括之前原始对象所关系的函数。这种过程也称为对象编组（marshalling）。从一系列字节提取数据结构的反向操作，是反序列化（也称为解编组、deserialization、unmarshalling）。\n一般情况下，只有当 JVM 处于运行时，创建的对象就存活在内存中，但对象会随着 JVM 的关闭而消失（即对象的生命周期不会比 JVM 的生命周期更长）。在有些实际情况需要在JVM停止运行之后能够保存(持久化)指定的对象，并在将来重新读取被保存的对象。另外对象并不只是存在内存中，还需要在传输网络或者持久化到文件，下次再加载出来用，这些场景都需要用到 Java 序列化技术。\nJava 序列化技术正是将对象转变成一串由二进制字节组成的数组，可以通过将二进制数据保存到磁盘或者传输网络，磁盘或者网络接收者可以在对象的属类的模板上来反序列化类的对象，达到对象持久化的目的。\n序列化：将数据结构或对象转换成二进制字节流的过程。要实现对象的序列化需要使用的流：ObjectOutputStream 继承 OutputStream 反序列化：将在序列化过程中所生成的二进制字节流的过程转换成数据结构或者对象的过程。要实现对象的反序列化需要使用的流：ObjectInputStream 继承 InputStream 14.1.1. 序列化协议对应于 TCP/IP 四层模型中的层级 网络通信的双方必须要采用和遵守相同的协议。TCP/IP 四层模型如下：\n应用层 传输层 网络层 网络接口层 如上图所示，OSI 七层协议模型中，表示层做的事情主要就是对应用层的用户数据进行处理转换为二进制流。反过来的话，就是将二进制流转换成应用层的用户数据。因此，OSI 七层协议模型中的应用层、表示层和会话层对应的都是 TCP/IP 四层模型中的应用层，所以序列化协议属于 TCP/IP 协议应用层的一部分。\n14.1.2. 实际开发中序列化和反序列化的应用场景 对象在进行网络传输（比如远程方法调用 RPC 的时候）之前需要先被序列化，接收到序列化的对象之后需要再进行反序列化 将对象存储到文件中的时候需要进行序列化，将对象从文件中读取出来需要进行反序列化 将对象存储到缓存数据库（如 Redis）时需要用到序列化，将对象从缓存数据库中读取出来需要反序列化 14.1.3. 常见序列化协议 常见的序列化协议有：JDK 自带的序列化，比较常用第三方的序列化协议：hessian、kyro、protostuff。\n其中 JDK 自带的序列化一般很少用，因为序列化效率低并且部分版本有安全漏洞，主要原因有两个：\n不支持跨语言调用：如果调用的是其他语言开发的服务的时候就不支持了。 性能差：相比于其他序列化框架性能更低，主要原因是序列化之后的字节数组体积较大，导致传输成本加大。 14.2. Serializable 接口 14.2.1. 概述 1 2 3 4 package java.io; public interface Serializable { } Serializable接口，没有任何方法，该接口属于标记性接口，仅用于标识可序列化的语义。接口的作用是，能够保证实现了该接口的类的对象可以直接被序列化到文件中\nNotes: 被保存的对象要求实现 Serializable 接口，否则不能直接保存到文件中。否则会出现java.io.NotSerializableException。\n14.2.2. serialVersionUID 序列化是将对象的状态信息转换为可存储或传输的形式的过程。虚拟机是否允许反序列化，不仅取决于类路径和功能代码是否一致，一个非常重要的一点是两个类的序列化 ID 是否一致，这个所谓的序列化 ID，就是在代码中定义的 serialVersionUID。\n序列化号 serialVersionUID 属于版本控制的作用。序列化的时候 serialVersionUID 也会被写入二级制序列，当反序列化时会检查 serialVersionUID 是否和当前类的 serialVersionUID 一致。如果 serialVersionUID 不一致则会抛出 InvalidClassException 异常。强烈推荐每个序列化类都手动指定其 serialVersionUID，如果不手动指定，那么编译器会动态生成默认的序列化号。\n14.2.3. Externalizable Java 中还提供了 Externalizable 接口，也可以实现它来提供序列化能力。\n1 2 3 4 5 6 7 8 package java.io; public interface Externalizable extends java.io.Serializable { void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; } Externalizable 继承自 Serializable，该接口中定义了两个抽象方法：writeExternal() 与 readExternal()。当使用 Externalizable 接口来进行序列化与反序列化的时候需要开发人员重写该方法。否则所有变量的值都会变成默认值。\n14.3. ObjectOutputStream（对象序列化流） 14.3.1. ObjectOutputStream 的作用 对象输出流，将 Java 的对象保存到文件中\n14.3.2. 构造方法 1 public ObjectOutputStream(OutputStream out); 根据指定的字节输出OutputStream对象来创建ObjectOutputStream。如：\n1 ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(\u0026#34;stu.txt\u0026#34;)); 14.3.3. 相关方法 1 public final void writeObject(Object obj) 将对象Obj写出到流关联的目标文件中\n14.3.4. 序列化步骤 定义类，实现 Serializable 接口，自定义一个 serialVersionUID 1 2 3 public class Student implements Serializable { private static final long serialVersionUID = -6286083484798000168L; } 创建对象 使用一个输出流(如：FileOutputStream)来构建 ObjectOutputStream （对象流）对象 调用 ObjectOutputStream 对象的 writeObject 将对象写入文件中(即保存其状态) 关流 14.4. ObjectInputStream（对象反序列化流） 14.4.1. ObjectInputStream 的作用 将文件中的对象读取到程序中，将对象从文件中读取出来，实现对象的反序列化操作。\n14.4.2. 构造方法 1 ObjectInputStream(InputStream in) 通过字节输入InputStream对象创建ObjectInputStream\n14.4.3. 普通方法 1 public final Object readObject() 从流关联的的文件中读取对象\n14.4.4. 反序列化步骤 创建对象输入流 调用readObject()方法读取对象 关流 14.5. 自定义对象输出流(了解) 14.5.1. 概念 要自定义对象输出流，就让新建的类继承 ObjectOutputStream\n14.5.2. WriteStreamHeader 方法的调用时机 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class ObjectOutputStream extends OutputStream implements ObjectOutput, ObjectStreamConstants { // ...省略 public ObjectOutputStream(OutputStream out) throws IOException { verifySubclass(); bout = new BlockDataOutputStream(out); handles = new HandleTable(10, (float) 3.00); subs = new ReplaceTable(10, (float) 3.00); enableOverride = false; writeStreamHeader(); bout.setBlockDataMode(true); if (extendedDebugInfo) { debugInfoStack = new DebugTraceInfoStack(); } else { debugInfoStack = null; } } protected void writeStreamHeader() throws IOException { bout.writeShort(STREAM_MAGIC); bout.writeShort(STREAM_VERSION); } // ...省略 } WriteStreamHeader() 方法中 ObjectOutputStream 类中的成员方法，每当创建 ObjectOutputStream 对象时，在 ObjectOutputStream 的构造方法中调用。\n14.5.3. WriteStreamHeader 方法的作用 WriteStreamHeader() 方法作用是，写入一个头部信息，如果是要追加写入，则要求第一个对象写入一个头部信息，其他对象则不能写入头部信息。\n判断文件是否存在，或文件长度是0，如果是就表示第一次保存对象。调用了 ObjectInputStream 中的方法：\n1 public int available() throws IOException 返回可以不受阻塞地读取的字节数。\n14.6. 瞬态关键字 transient 14.6.1. transient 的作用 序列化对象时，如果不想保存某一个成员变量的值，该如何处理？transient 关键字作用就是用于指定序列化对象时不保存某个成员变量的值。\n用 transient 修饰成员变量，能够保证该成员变量的值不能被序列化到文件中。当对象被反序列化时，被 transient 修饰的变量值会设为初始值，如 int 型的是 0，对象型的是 null。\n14.6.2. 使用 static 修饰的成员变量（不建议使用） 可以将该成员变量定义为静态的成员变量。因为对象序列化只会保存对象自己的信息，静态成员变量是属于类的信息，所有不会被保存。但不建议使用。\n14.6.3. 注意点 transient 只能修饰变量，不能修饰类和方法\n14.7. 序列化的常见问题与注意事项 14.7.1. InvalidClassException 异常 java.io.InvalidClassException: 无效的类异常。此异常是序列号冲突。\n出错的核心问题：类改变后，类的序列化号也改变，就和文件中的序列化号不一样 解决方法：修改类的时候，让序列化号不变，自定义一个序列号，不要系统随机生成序列号。 14.7.2. 注意事项总结 序列化对象必须实现序列化接口。 序列化对象里面的属性是对象的话也要实现序列化接口。 类的对象序列化后，类的序列化ID(serialVersionUID)不能轻易修改，不然反序列化会失败。 类的对象序列化后，类的属性有增加或者删除不会影响序列化，只是值会丢失。 如果父类实现了序列化接口，子类会继承父类的序列化，子类无需添加序列化接口。 如果父类没有实现序列化接口，而子类序列化了，子类中的属性能正常序列化，但父类的属性会丢失，不能序列化。 用 Java 序列化的二进制字节数据只能由 Java 反序列化，不能被其他语言反序列化。如果要进行前后端或者不同语言之间的交互一般需要将对象转变成 Json/Xml 通用格式的数据，再恢复原来的对象。 如果某个字段不想被序列化，在该字段前加上 transient 关键字即可。在被反序列化后，transient 修饰的变量值会被设为对应类型的初始值，例如，int 类型变量的值是 0，对象类型变量的值是 null。 序列化不会保存静态变量。 序列化对象会将其状态保存为一组字节；反序列化时，再将这些字节组装成对象。 14.8. 扩展 14.8.1. 其他序列化转换对象的方式 除了使用 Java 自带的序列化机制，通过实现 Serializable 接口并重写 readObject 和 writeObject 方法，将对象转换成字节序列，或将字节序列转换成对象。使用 ObjectInputStream 和 ObjectOutputStream 进行读写操作。还有以下序列化转换对象的方式：\n使用 JSON 序列化框架，如 Jackson、Gson 等，通过将对象转换成 JSON 字符串，或将 JSON 字符串转换成对象。使用 ObjectMapper 进行读写操作。 使用 XML 序列化框架，如 JAXB、XStream 等，通过将对象转换成 XML 格式，或将 XML 格式转换成对象。使用 Marshaller 和 Unmarshaller 进行读写操作。 使用 Protobuf 序列化框架，通过定义.proto文件来描述数据结构，然后使用编译器生成 Java 代码，使用这些生成的代码进行读写操作。 14.8.2. 序列化对象案例 要序列化一个对象，这个对象所在类就必须实现Java序列化的接口：java.io.Serializable。\n14.8.2.1. 类添加序列化接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 import java.io.Serializable; public class User implements Serializable { private static final long serialVersionUID = -8475669200846811112L; private String username; private String address; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } @Override public String toString() { return \u0026#34;User{\u0026#34; + \u0026#34;username=\u0026#39;\u0026#34; + username + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, address=\u0026#39;\u0026#34; + address + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } 14.8.2.2. 序列化/反序列化 可以借助commons-lang3工具包里面的类实现对象的序列化及反序列化，无需自己写\n1 2 3 4 5 6 7 8 9 10 11 12 13 import org.apache.commons.lang3.SerializationUtils; public class Test { public static void main(String[] args) { User user = new User(); user.setUsername(\u0026#34;Java\u0026#34;); user.setAddress(\u0026#34;China\u0026#34;); byte[] bytes = SerializationUtils.serialize(user); User u = SerializationUtils.deserialize(bytes); System.out.println(u); } } 输出结果：\n1 User{username=\u0026#39;Java\u0026#39;, address=\u0026#39;China\u0026#39;} 上例通过序列化对象字节到内存然后反序列化，当然里面也提供了序列化磁盘然后再反序列化的方法，原理都是一样的，只是目标地不一样。\n","permalink":"https://ktzxy.top/posts/6ohxm8u9pr/","summary":"Java基础 IO编程","title":"Java基础 IO编程"},{"content":"Docker学习笔记 Docker介绍 Docker简介 Docker简介以及Docker历史 http://c.biancheng.net/view/3118.html\ndocker官网 https://www.docker.com/\nDocker是一个开源的应用容器引擎，基于LXC（Linux Container）内核虚拟化技术实现，提供一系列更强的功能，比如镜像、 Dockerfile等；\nDocker理念是将应用及依赖包打包到一个可移植的容器中，可发布到任意Linux发行版Docker引擎上。使用沙箱机制运行程序， 程序之间相互隔离； Docker使用Go语言开发。\nDocker架构与内部组件 Docker采用C/S架构，Dcoker daemon作为服务端接受来自客户端请求，并处理这些请求，比如创建、运行容器等。客户端为用 户提供一系列指令与Docker daemon交互。\nDocker的架构图 Docker Client：客户端 Ddocker Daemon：守护进程 Docker Images：镜像 Docker Container：容器 Docker Registry：镜像仓库 LXC：Linux容器技术，共享内核，容器共享宿主机资源，使用namespace和cgroups对资源限制与隔离。 Cgroups（control groups）：Linux内核提供的一种限制单进程或者多进程资源的机制；比如CPU、内存等资源的使用限制。 NameSpace：命名空间，也称名字空间，Linux内核提供的一种限制单进程或者多进程资源隔离机制；一个进程可以属于多个命 名空间。Linux内核提供了六种NameSpace：UTS、IPC、PID、Network、Mount和User。\nAUFS（advanced multi layered unification filesystem）：高级多层统一文件系统，是UFS的一种，每个branch可以指定readonly（ro 只读）、readwrite（读写）和whiteout-able（wo隐藏）权限；一般情况下，aufs只有最上层的branch才有读写权限，其他branch 均为只读权限。\nUFS（UnionFS）：联合文件系统，支持将不同位置的目录挂载到同一虚拟文件系统，形成一种分层的模型；成员目录称为虚拟 文件系统的一个分支（branch）。 Docker 包括三个基本概念\n镜像（Image） 容器（Container） 仓库（Repository） 理解了这三个概念，就理解了 Docker 的整个生命周期。\n镜像( image)： Docker镜像(工mage)就是一个只读的模板。镜像可以用来创建 Docker容器镜像可以创建很多容器。容器从镜像启动的时候，会在镜像的最上层创建一个可写层。就好似]ava中的类和对象,类就是镜像,容器就是对象。\n容器( container):\n1.Docker利用容器( Container)独立运行的一个或一组应用。容器是用镜像创建的运行实例 2.它可以被启动、开始、停止、除。每个容器都是相互隔高的,保证安全的平台 3.可以把音器看做是一个简易版的Linux环境(色括root用户权限、进程空间、用户空间和网空间等)和运行在具其中的应用程序 4.容器的定义和镜像几手ー模一样,也是一堆层的统一视角,唯一区别在于容器的最上面那一层是可读可写的\n仓库( repository): 1.仓库( Repository)是集中存放镜像文件的场所 2.仓库( Repository)和仓库注册服务器( Registry)是有区别的。仓库注册服务器上往往存放着多个仓库,每个仓库中又包含了多个镜像,每个镜像有不同的标签(tag) 3.仓库分为公开仓库(Pub1ic)和私有仓库( Private)两种形式 4.最大的公开仓库是DockerHub(https://hub.docker.com/)存放了数量度大的镜像供用户下载 5.国内的公开仓库包括阿里云、网易云等 *注：Docker 仓库的概念跟 Git 类似，注册服务器可以理解为 GitHub 这样的托管服务。\nDocker有什么优点 持续集成 在项目快速迭代情况下，轻量级容器对项目快速构建、环境打包、发布等流程就能提高工作效率。\n版本控制 每个镜像就是一个版本，在一个项目多个版本时可以很方便管理。\n可移植性 容器可以移动到任意一台Docker主机上，而不需要过多关注底层系统。\n标准化 应用程序环境及依赖、操作系统等问题，增加了生产环境故障率，容器保证了所有配置、依赖始终不变。\n隔离性与安全 容器之间的进程是相互隔离的，一个容器出现问题不会影响其他容器。\n虚拟机与容器区别 以KVM举例，与Docker对比\n启动时间 Docker秒级，KVM分钟级。\n轻量级 容器镜像大小通常以M为单位，虚拟机以G为单位。 容器资源占用小，要比虚拟机部署更快速。\n性能 容器共享宿主机内核，系统级虚拟化，占用资源少，没有Hypervisor层开销，容器性能基本接近物理机； 虚拟机需要Hypervisor层支持，虚拟化一些设备，具有完整的GuestOS，虚拟化开销大，因而降低性能，没有容器性能好。\n安全性 由于共享宿主机内核，只是进程级隔离，因此隔离性和稳定性不如虚拟机，容器具有一定权限访问宿主机内核，存在一定安全 隐患。\n使用要求 KVM基于硬件的完全虚拟化，需要硬件CPU虚拟化技术支持； 容器共享宿主机内核，可运行在主流的Linux发行版，不用考虑CPU是否支持虚拟化技术。\n应用打包与部署自动化 构建标准化的运行环境； 现在大多方案是在物理机和虚拟机上部署运行环境，面临问题是环境杂乱、完整性迁移难度高等问题，容器即开即用。\n自动化测试和持续集成/部署 自动化构建镜像和良好的REST API，能够很好的集成到持续集成/部署环境来。\n部署与弹性扩展 由于容器是应用级的，资源占用小，弹性扩展部署速度要更快。\n微服务 Docker这种容器华隔离技术，正式应对了微服务理念，将业务模块放到容器中运行，容器的可复用性大大增加了业务模块扩展 性。 底层原理 Docker是怎么工作的\nDocker是一个Client-Server结构的系统，Docker的守护进程运行在主机上。通过Socket从客户端访问。\nDockerServer接受到Docker-Client的指令，就会执行这个命令！\nDocker为什么比VM快？\n1.Docker有着比虚拟机更少的抽象层\n2.Docker利用的是宿主机的内核，vm需要的时Guest OS。 Docker安装 安装Docker 查看Linux版本内核\n1 2 3 4 5 1.查看Linux版本内核 #uname -a Docker最低支持CentOS 7，Docker 需要安装在 64 位的平台，并且内核版本不低于 3.10。 CentOS 7 满足最低内核的要求，但由于内核版本比较低，部分功能（如 overlay2 存储层驱动）无法使用，并且部分功能可能不太稳定。 2.yum安装gcc相关环境（确保虚拟机可以上外网） #yum -y install gcc 安装\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 1.卸载旧的版本 $sudo yum remove docker \\ docker-client \\ docker-client-latest \\ docker-common \\ docker-latest \\ docker-latest-logrotate \\ docker-logrotate \\ docker-engine 2.需要的安装包 $sudo yum install -y yum-utils 3.设置镜像的仓库（官方） $sudo yum-config-manager \\ --add-repo \\ https://download.docker.com/linux/centos/docker-ce.repo （官网速度慢） $sudo yum-config-manager \\ --add-repo \\ http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo（推荐使用，阿里云速度快） 4.更新yum软件包索引 $sudo yum makecache fast 5.安装docker docker-ce 社区版 docker-ee 企业版 $sudo yum install docker-ce docker-ce-cli containerd.io 6.启动docker $sudo systemctl start docker 7.查看docker是否安装成功 $sudo docker version 卸载Docker\n1 2 3 4 5 1.卸载依赖 $sudo yum remove docker-ce docker-ce-cli containerd.io 2.删除资源 $sudo rm -rf /var/lib/docker # /var/lib/docker docker的默认工作路径 阿里云镜像加速 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #配置镜像加速器 #方法一 #通过shell脚本追加文本内容 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json \u0026lt;\u0026lt;-\u0026#39;EOF\u0026#39; { \u0026#34;registry-mirrors\u0026#34;: [ \u0026#34;https://registry.docker-cn.com\u0026#34;, \u0026#34;http://hub-mirror.c.163.com\u0026#34;, \u0026#34;https://docker.mirrors.ustc.edu.cn\u0026#34;, \u0026#34;https://mirror.baidubce.com\u0026#34; ] } EOF sudo systemctl daemon-reload sudo systemctl restart docker #方法二 #通过修改daemon配置文件/etc/docker/daemon.json来使用加速器 vi /etc/docker/daemon.json { \u0026#34;registry-mirrors\u0026#34;: [ \u0026#34;https://registry.docker-cn.com\u0026#34;, \u0026#34;http://hub-mirror.c.163.com\u0026#34;, \u0026#34;https://docker.mirrors.ustc.edu.cn\u0026#34; ] } $sudo systemctl daemon-reload $sudo systemctl restart docker 设置开机自动启动 1 #systemctl enable docker 查看docker服务状态 1 #systemctl status docker 查看docker具体信息 1 #docker info 镜像使用 列出镜像 1 #docker images REPOSITORY**：表示镜像的仓库源 TAG****：**镜像的标签 IMAGE ID**：**镜像ID CREATED**：**镜像创建时间 SIZE**：**镜像大小 只显示镜像ID 1 #docker images –q 直接列出镜像结果，并且只包含镜像ID和仓库名 1 #docker images --format \u0026#34;{{.ID}}: {{.Repository}}\u0026#34; 获取镜像 1 #docker pull REPOSITORY:TAG 查找镜像 1 #docker search REPOSITORY:TAG NAME: 镜像仓库源的名称 DESCRIPTION: 镜像的描述 OFFICIAL: 是否 docker 官方发布 stars: 类似 Github 里面的 star，表示点赞、喜欢的意思。 AUTOMATED: 自动构建。 删除镜像 1 #docker rmi REPOSITORY 使用某镜像来启动一个容器 1 #docker run -t -i REPOSITORY:TAG /bin/bash -i: 交互式操作\n-t: 终端。\n/bin/bash：放在镜像名后的是命令，这里我们希望有个交互式 Shell，因此用的是 /bin/bash。\n如果你不指定一个镜像的版本标签，docker 将默认使用 REPOSITORY:latest 镜像。\n更新镜像（更新前需要创建一个容器） 在运行的容器内使用 apt-get update 命令进行更新。\n在完成操作之后，输入 exit 命令来退出这个容器。\n通过commit命令来提交（将容器保存为镜像）\n1 #docker commit -m=\u0026#34;描述信息\u0026#34; -a=\u0026#34;镜像作者\u0026#34; 容器名/容器ID REPOSITORY：创建的目标镜像名 设置镜像标签 1 #docker tag 镜像ID REPOSITORY:新的标签名 镜像的导入导出 如果因为网络原因可以通过硬盘的方式传输镜像，虽然不规范，但是有效，但是这种方式导出的镜像名称和版本都是null，需要手动修改\n将本地的镜像导出\n1 #docker save -o 导出的路径 镜像id 加载本地的镜像文件\n1 #docker load -i 镜像文件 虚悬镜像 镜像既没有仓库名，也没有标签，均为 ：这类无标签镜像也被称为虚悬镜像（dangling image），可以用下面的命令专门显示这类镜像：\n1 #docker images -f dangling=true 可以用下面的命令删除\n1 #docker rmi $（docker images -q -f dangling=true） 利用 commit 理解镜像构成 现在以定制一个 Web 服务器为例子，来讲解镜像是如何构建的。\n1 #docker run --name webserver -d -p 80:80 nginx 这条命令会用 nginx 镜像启动一个容器，命名为 webserver，并且映射了 80 端口，这样可以用浏览器去访问这个 nginx 服务器。直接用浏览器访问的话，会看到默认的 Nginx 欢迎页面。\n现在，假设非常不喜欢这个欢迎页面，希望改成欢迎 Docker 的文字，可以使用 docker exec命令进入容器，修改其内容。\n1 2 3 #docker exec -it webserver bash #cd /usr/share/nginx/html/ #echo \u0026#39;\u0026lt;h1\u0026gt;Hello, Docker!\u0026lt;/h1\u0026gt;\u0026#39; \u0026gt;index.html 上传本地镜像到共有仓库 在 https://hub.docker.com 免费注册一个 Docker 账号。\n登录\n启动 docker 服务\n1 #systemctl start docker 1 2 #docker login #docker logout(退出) 查看本地镜像\n通过docker push 命令将自己的镜像推送到Docker HuB\n使用tag标记镜像需要上传到的仓库\n1 #docker tag hello-world:latest username/hello-world:latest 使用push 上传镜像\n1 #docker push username/hello-world:latest 登陆hub.docker.com 查看上传结果\n私有仓库的搭建并上传镜像至私有仓库 查找并下载 私有仓库的镜像 registry\n1 2 #docker search registry #docker pull registry 创建镜像的容器\n1 #docker run -d -v /opt/registry:/var/lib/registry -p 5000:5000 --name myregistry registry:2 Registry服务默认会将上传的镜像保存在容器的/var/lib/registry，我们将主机的/opt/registry目录挂载到该目录，即可实现将镜像保存到主机的/opt/registry目录了。\n浏览器访问http://127.0.0.1:5000/v2，出现下面情况说明registry运行正常。\n上传镜像至私有仓库\n要通过docker tag将该镜像标志为要推送到私有仓库：\n1 #docker tag nginx:latest localhost:5000/nginx:latest 通过 docker push 命令将 nginx 镜像 push到私有仓库中：\n1 #docker push localhost:5000/nginx:latest 访问 http://127.0.0.1:5000/v2/_catalog 查看私有仓库目录，可以看到刚上传的镜像了：\n可能出现的异常\nreceived unexpected HTTP status:500 Internal Server Error\n解决方案：设置防火墙的权限\n1 2 3 #setenforce 0 #getenforce Permissive 容器的操作 运行容器 运行容器需要定制具体镜像，如果镜像不存在，会直接下载\n命令\n1 #docker run -d -p 宿主机端口:容器端口 --name 容器名称 镜像的标识|镜像名称[:tag] 当利用 docker run 来创建容器时，Docker 在后台运行的标准操作包括：\n检查本地是否存在指定的镜像，不存在就从公有仓库下载 利用镜像创建并启动一个容器 分配一个文件系统，并在只读的镜像层外面挂载一层可读写层 从宿主主机配置的网桥接口中桥接一个虚拟接口到容器中去 从地址池配置一个 ip 地址给容器 执行用户指定的应用程序 执行完毕后容器被终止 常用的参数\n-d:代表后台运行容器\n-it 使用交互方式运行，进入容器查看内容\n-p 宿主机端口:容器端口：为了映射当前Linux的端口和容器的端口\n\u0026ndash;name 容器名称:指定容器的名称\n我们也可以使用 -p 标识来指定容器端口绑定到主机端口。\n两种方式的区别是:\n-P :是容器内部端口随机映射到主机的高端口。\n-p : 是容器内部端口绑定到指定的主机端口。\n查看正在运行的容器 查看全部正在运行的容器信息\n1 #docker ps [-qa] -a 查看全部的容器，包括没有运行\n-q 只查看容器的标识\n查看容器日志 查看容器日志，以查看容器运行的信息\n1 #docker logs -f 容器id -f：可以滚动查看日志的最后几行 \u0026ndash;tail number 要显示日志条数 进入容器的内部 可以进入容器的内部进行操作\n1 #docker exec -it 容器id /bin/bash 复制内容到容器 将宿主机的文件复制到容器内部的指定目录\n1 #docker cp 文件名称 容器id:容器内部路径 重启\u0026amp;启动\u0026amp;停止\u0026amp;删除容器\u0026amp;退出容器 重新启动容器 1 #docker restart 容器id 启动停止运行的容器 1 #docker start/run 容器id -d 后台运行 -i 交换式运行 \u0026ndash;name 添加名字 -t 添加标签 -v 添加数据卷 -rm 容器删除后清除缓存 停止指定的容器(删除容器前，需要先停止容器) 1 #docker stop 容器id 停止全部容器 1 #docker stop $(docker ps -qa) 删除指定容器 1 #docker rm 容器id 强制停止当前容器 1 #docker kill 容器id 退出容器 1 2 #exit #直接容器停止并退出 Ctrl +P + Q #容器不停止退出 删除全部容器 1 #docker rm $(docker ps -qa) 导出容器 1 #docker export 容器ID \u0026gt; 存储路径 导入容器 1 #docker import 容器快照/指定URL/某个目录 *注：用户既可以使用 docker load 来导入镜像存储文件到本地镜像库，也可以使用 docker import 来导入一个容器快照到本地镜像库。这两者的区别在于容器快照文件将丢弃所有的历史记录和元数据信息（即仅保存容器当时的快照状态），而镜像存储文件将保存完整记录，体积也要大。此外，从容器快照文件导入时可以重新指定标签等元数据信息。\n将website1发到nginx的容器中去 1 2 3 4 5 #docker volume create 卷名 #docker run -d -p 5000:80 -v 卷名：/usr/share/nginx/html --name 容器名 docker.io/nginx:latest #cd /var/lib/docker/volumes/卷名/_data/ #vi index.html web访问ip:5000 docker常用命令 Docker图形界面管理 DockerUI DockerUI是一个基于Docker API提供图形化页面简单的容器管理系统，支持容器管理、镜像管理。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 docker run \\ -d \\ -p 9000:9000 \\ -v /var/run/docker.sock:/docker.sock \\ --name dockerui abh1nav/dockerui:latest \\ -e=\u0026#34;/docker.sock\u0026#34; 也可以通过Rest API管理： docker run \\ -d \\ -p 9000:9000 \\ --name dockerui \\ -e \u0026#34;http://\u0026lt;dockerd host ip\u0026gt;:2375\u0026#34; abh1nav/dockerui:latest http://\u0026lt;dockerd host ip\u0026gt;:9000 Shipyard Shipyard也是基于Docker API实现的容器图形管理系统，支持container、images、engine、cluster等功能，可满足我 们基本的容器部署需求。 Shipyard分为手动部署和自动部署。\n官方部署文档：https://www.shipyard-project.com/docs/deploy/\n可视化 http://www.yunweipai.com/34991.html\nDocker镜像加载原理 UnionFS（联合文件系统） 我们下载的时候看到的一层层就是这个！\nUnionFS（联合文件系统）：Union文件系统（UnionFS）是一种分层、轻量级并且高性能的文件系统，它支持对文件系统得修改，作为一次提交来一层层的叠加，同时可以将不同目录挂载到同一个虚拟文件系统下（unite several directories into a single virtual filesystem）。Union文件系统是Docker镜像的基础。镜像可以通过分层来进行继承，基于基础镜像（没有父镜像），可以制作各种具体的应用镜像。\n特性：一次同时加载多个文件系统，但从外面看来，只能看到一个文件系统，联合加载会把各层文件系统叠加起来，这样最终的文件系统对包含所有底层的文件和目录。\nDocker镜像加载原理 docker的镜像实际上由一层一层的文件系统组成，这种层次的文件系统UnionFS。\nboots（boot file system）主要包含bootloader和kernel，bootloader主要是引导加载kernel，Linux刚启动时加载bootfs文件系统，在Docker镜像的最底层是bootfs。这一层与我们典型的Linux/Unix系统是一样的，包含boot加载器和内核。当boot加载完成之后整个内核就都在内存中了，此时内存的使用权已由bootfs转交给内核，此时系统也会加载bootfs。\nrootfs（root file system），在bootfs之上。包含的就是典型Linux系统中的/dev/，/proc，/bin，/etc等标准目录和文件。rootfs就是各种不同的操作系统发行版，比如Ubuntu，Centos等等。\n分层理解 所有的 Docker镜像都起始于一个基础镜像层,当进行修改或增加新的内容时,就会在当前镜像层之上,创建新的镜像层。\n举一个简单的例子,假如基于 Ubuntu Linux16.04创建一个新的镜像,这就是新镜像的第一层;如果在该镜像中添加 Python\u0026rsquo;包,就会在基础镜像层之上创建第二个镜像层;如果继续添加一个安补丁,就会创建第三个镜像层该镜像当前已经包含3个镜像层,如下图所示(这只是一个用于演示的很简单的例子)。\n在添加额外的镜像层的同时,镜像始终保持是当前所有镜像的组合,理解这一点非常重要。下图中举了一个简单的例子,每个镜像层包含3个文件,而镜像包含了来自两个镜像层的6个文件。\n上图中的镜像层跟之前图中的略有区别,主要目的是便于展示文件。下图中展示了一个稍微复杂的三层镜像,在外部看来整个镜像只有6个文件,这是因为最上层中的文件7是文件5的一个更新版本\n这种情况下,上层镜像层中的文件覆盖了底层镜像层中的文件。这样就使得文件的更新版本作为—个新镜像层添加到镜像当中。Docker通过存储引擎(新版本采用快照机制)的方式来实现镜像层堆栈,并保证多镜像层对外展示为统-的文件系统。\nLinux上可用的存储引擎有AUFS、 Overlay2、 Device Mapper、Bts以及zFS。顾名思义,每种存储引擎都基于 Linux中对应的又仵系统或者块设备技术,并且每种存储引擎都有其独有的性能特点。\nDocker在 Windows上仅支持 windowsfilter—种存储引擎,该引擎基于NTFS文件系统之上实现了分层和CoW[1]。下图展示了与系统显示相同的三层镜像。所有镜像层堆并合并,对外提供统-的视图.\n特点：\nDocker镜像都是只读的,当容器启动时,—个新的可写层被加载到镜像的顶部这一层就是我们通常说的容器层,容器之下的都叫镜像层!\n数据卷 数据卷是一个可供一个或多个容器使用的特殊目录，它绕过 UFS，可以提供很多有用的特性：\n数据卷可以在容器之间共享和重用 对数据卷的修改会立马生效 对数据卷的更新，不会影响镜像 卷会一直存在，直到没有容器使用 *数据卷的使用，类似于 Linux 下对目录或文件进行 mount。\n创建数据卷 创建数据卷后，默认会存放在一个目录下/var/lib/docker/volumes/数据卷名称/_data\n1 #docker volume create 数据卷名称 查看全部数据卷 查看全部数据卷信息\n1 #docker volume ls 查看数据卷详情 查看数据卷的详细信息，可以查询到存放的路径，创建时间等等\n1 #docker volume inspect 数据卷名称 删除数据卷 删除指定的数据卷\n1 #docker volume rm 数据卷名称 Docker数据卷容器 如果你有一些持续更新的数据需要在容器之间共享，最好创建数据卷容器。\n数据卷容器，其实就是一个正常的容器，专门用来提供数据卷供其它容器挂载的。\n首先，创建一个命名的数据卷容器 dbdata：\n1 $sudo docker run -d -v /数据卷容器名称 --name 数据卷容器名称 镜像的标识|镜像名称[:tag] 然后，在其他容器中使用 \u0026ndash;volumes-from 来挂载 dbdata 容器中的数据卷。\n1 2 $sudo docker run -d --volumes-from 数据卷容器名称 --name db1 $sudo docker run -d --volumes-from 数据卷容器名称 --name db2 还可以使用多个 \u0026ndash;volumes-from 参数来从多个容器挂载多个数据卷。\n也可以从其他已经挂载了数据卷的容器来挂载数据卷。\n1 $sudo docker run -d --name db3 --volumes-from db1 *注意：使用 \u0026ndash;volumes-from 参数所挂载数据卷的容器自己并不需要保持在运行状态。\n如果删除了挂载的容器（包括 dbdata、db1 和 db2），数据卷并不会被自动删除。如果要删除一个数据卷，必须在删除最后一个还挂载着它的容器时使用 docker rm -v 命令来指定同时删除关联的容器。\n容器映射数据卷 通过数据卷名称映射，如果数据卷不存在。Docker会帮你自动创建，会将容器内部自带的文件，存储在默认的存放路径中。\n1 #docker run -d -p 8080:8080 --name tomcat -v 数据卷名称:容器内部的路径 镜像id 通过路径映射数据卷，直接指定一个路径作为数据卷的存放位置。但是这个路径下是空的。\n1 #docker run -d -p 8080:8080 --name tomcat -v 路径(/root/自己创建的文件夹):容器内部的路径 镜像id 映射所有接口地址 使用 hostPort:containerPort 格式本地的 5000 端口映射到容器的 5000 端口，可以执行\n1 $ sudo docker run -d -p 5000:5000 镜像的标识|镜像名称 此时默认会绑定本地所有接口上的所有地址。\n映射到指定地址的指定端口 可以使用 ip:hostPort:containerPort 格式指定映射使用一个特定地址，比如 localhost 地址 127.0.0.1\n1 $ sudo docker run -d -p 127.0.0.1:5000:5000 镜像的标识|镜像名称 映射到指定地址的任意端口 使用 ip::containerPort 绑定 localhost 的任意端口到容器的 5000 端口，本地主机会自动分配一个端口。\n1 $ sudo docker run -d -p 127.0.0.1::5000 镜像的标识|镜像名称 还可以使用 udp 标记来指定 udp 端口\n1 $ sudo docker run -d -p 127.0.0.1:5000:5000/udp 镜像的标识|镜像名称 查看映射端口配置 使用 docker port 来查看当前映射的端口配置，也可以查看到绑定的地址\n1 2 $ docker port nostalgic_morse 5000 127.0.0.1:49155. 注意：\n容器有自己的内部网络和 ip 地址（使用 docker inspect 可以获取所有的变量，Docker 还可以有一个可变的网络配置。）\n-p 标记可以多次使用来绑定多个端口\n例如\n1 $ sudo docker run -d -p 5000:5000 -p 3000:80 镜像的标识|镜像名称 具名和匿名挂载 匿名挂载 1 #docker run -d -P --name 容器名 -v 容器内路径 镜像名 具名挂载 1 #docker run -d -P --name 容器名 -v 卷名：容器内路径 镜像名 所有的docker容器内的卷，没有指定目录的情况下都是在/var/lib/docker/volumes/xxx/_data\n1 2 3 4 #如何确定是具名挂载还是匿名挂载，还是指定路径挂载 -v 容器内路径 #匿名挂载 -v 卷名：容器内路径 #具名挂载 -v /宿主机路径：：容器内路径 #指定路径挂载 1 2 3 4 #通过 -v 容器内路径，ro rw 改变读写权限 ro readonly #只读 rw readwrite #可读可写 docker run -d -p --name nginx2 -v mynginx:/etc/nginx:ro nginx 将数据从宿主机挂载到容器中的三种方式\nDocker提供三种方式将数据从宿主机挂载到容器中：\n• volumes：Docker管理宿主机文件系统的一部分（/var/lib/docker/volumes）。保存数据的最佳方式。\n• bind mounts-：将宿主机上的任意位置的文件或者目录挂载到容器中。\n• tmpfs：挂载存储在主机系统的内存中，而不会写入主机的文件系统。如果不希望将数据持久存储在任何位置，可以使用 tmpfs，同时避免写入容器可写层提高性能。\nVolume特点：\n• 多个运行容器之间共享数据，多个容器可以同时挂载相同的卷。\n• 当容器停止或被移除时，该卷依然存在。\n• 当明确删除卷时，卷才会被删除。\n• 将容器的数据存储在远程主机或其他存储上（间接）\n• 将数据从一台Docker主机迁移到另一台时，先停止容器，然后备份卷的目录（/var/lib/docker/volumes/）\nBind Mounts特点：\n• 从主机共享配置文件到容器。默认情况下，挂载主机/etc/resolv.conf到每个容器，提供DNS解析。\n• 在Docker主机上的开发环境和容器之间共享源代码。例如，可以将Maven target目录挂载到容器中，每次在Docker主机 上构建Maven项目时，容器都可以访问构建的项目包。\n• 当Docker主机的文件或目录结构保证与容器所需的绑定挂载一致时\nDockerfile自定义镜像 实战一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #创建一个dockerfile文件，名字可以随机 建议Dockerfile #文件中的内容 指令（大写） 参数 FROM centos VOLUME [\u0026#34;volume01\u0026#34;,\u0026#34;volume02\u0026#34;] CMD echo \u0026#34;---end---\u0026#34; CMD /bin/bash #FROM:指定当前自定义镜像依赖的环境 #MAINTAINER：镜像作者 姓名+邮箱 #COPY：将相对路径下的内容复制到自定义镜像中 #WORKDIR:声明镜像的默认工作目录 #VOLUME：挂载的目录 #EXPOSE：保留端口配置 #RUN：执行的命令，可以编写多个 #CMD：指定这个容器启动的时候要运行的命令，只有最后一个会生效，可被替代 类似于 RUN 指令，用于运行程序，但二者运行的时间点不同: CMD 在docker run 时运行。 RUN 是在 docker build。 #ENTRYPOINT：指定这个容器启动的时候要运行的命令，可以追加命令 #ONBUILD：当构建一个被继承Dockerfile 这个时候就会运行ONBUILD的指令，触发指令 #ENV：构建的时候设置环境变量 #docker build -f 文件存储路径 -t 容器名[：TAG] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 FROM centos MAINTAINER zzz\u0026lt;2251@qq.com\u0026gt; ENV MYPATH /usr/local WORKDIR $MYPATH RUN yum -y install vim RUN yum -y install net-tools EXPOSE 80 CMD echo $MYPATH CMD echo \u0026#34;----end----\u0026#34; CMD echo /bin/bash 实战二（Tomcat） 准备镜像文件tomcat压缩包，jdk的压缩包\n编写dockerfile文件，官方命名Dockerfile，build会自动寻找这个文件，就不需要-f 制定了！\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 FROM centos MAINTAINER zzz\u0026lt;2251@qq.com\u0026gt; COPY readme.txt /usr/Local/readme.txt ADD jdk-8u11-linux-x64.tar.gz /usr/local/ ADD apache-tomcat-9.0.22.tar.gz /usr/Local/ RUN yum-y install vim ENV MYPATH /usr/Local WORKDIR $MYPATH ENV JAVA_HOME /usr/Local/idk.8.0_11 ENV CLASSPATH $JAVA_HOME/Lib/dt.jar:$JAVA_HOME/Lib/tools.jar ENV CATALINA_HOME /usr/Local/apache-tomcat-9.0.22 ENV CATALINA_BASH /usr/Local/apache-tomcat-9.0.22 ENV PATH $PATH: $JAVA_HOME/bin:$CATALINA_HOME/Lib:$CATALINA_HOME/bin EXPOSE 8080 CMD /usr/Local/apache-tomcat-90.22/bin/startup.sh \u0026amp;\u0026amp; tail -F /usr/Local/apache-tomcat-9022/bin/logs/catalina.out 构建镜像\n1 2 3 4 5 #docker build -t diytomcat . . 是上下文路径 上下文路径，是指 docker 在构建镜像，有时候想要使用到本机的文件（比如复制），docker build 命令得知这个路径后，会将路径下的所有内容打包。 由于 docker 的运行模式是 C/S。我们本机是 C，docker 引擎是 S。实际的构建过程是在 docker 引擎下完成的，所以这个时候无法用到我们本机的文件。这就需要把我们本机的指定目录下的文件一起打包提供给 docker 引擎使用。 如果未说明最后一个参数，那么默认上下文路径就是 Dockerfile 所在的位置。 启动镜像\n1 2 3 #docker run -d 9090：8080 --name zzzcomcat -v 本地目录路径：容器内部目录路径 本地目录路径：容器内部目录路径（日志挂载）镜像名 #-d 后台运行 #-p 指定端口 访问测试\n发布项目\n出现资源拒绝访问，解决办法，增加一个tag\n构建PHP网站环境镜像 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 FROM centos:6 MAINTAINER zzz RUN yum install -y httpd php php-gd php-mysql mysql mysql-server ENV MYSQL_ROOT_PASSWORD 123456 RUN echo \u0026#34;\u0026lt;?php phpinfo()?\u0026gt;\u0026#34; \u0026gt; /var/www/html/index.php ADD start.sh /start.sh RUN chmod +x /start.sh ADD https://cn.wordpress.org/wordpress-4.7.4-zh_CN.tar.gz /var/www/html COPY wp-config.php /var/www/html/wordpress VOLUME [\u0026#34;/var/lib/mysql\u0026#34;] CMD /start.sh EXPOSE 80 3306 # cat start.sh service httpd start service mysqld start mysqladmin -uroot password $MYSQL_ROOT_PASSWORD tail -f 构建JAVA网站环境镜像 1 2 3 4 5 6 7 8 FROM centos:6 MAINTAINER zzz ADD jdk-8u45-linux-x64.tar.gz /usr/local ENV JAVA_HOME /usr/local/jdk1.8.0_45 ADD http://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-8/v8.0.45/bin/apachetomcat-8.0.45.tar.gz /usr/local WORKDIR /usr/local/apache-tomcat-8.0.45 ENTRYPOINT [\u0026#34;bin/catalina.sh\u0026#34;, \u0026#34;run\u0026#34;] EXPOSE 8080 构建支持SSH服务的镜像 1 2 3 4 5 6 7 8 9 FROM centos:6 MAINTAINER zzz ENV ROOT_PASSWORD 123456 RUN yum install -y openssh-server RUN echo $ROOT_PASSWORD |passwd --stdin root RUN ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key CMD [\u0026#34;/usr/sbin/sshd\u0026#34;, \u0026#34;-D\u0026#34;] EXPOSE 22 Docker网络 当 Docker 启动时，会自动在主机上创建一个 docker0 虚拟网桥，实际上是 Linux 的一个 bridge，可以理解为一个软件交换机。它会在挂载到它的网口之间进行转发。\n同时，Docker 随机分配一个本地未占用的私有网段（在 RFC1918 中定义）中的一个地址给 docker0 接口。比如典型的 172.17.42.1，掩码为 255.255.0.0。此后启动的容器内的网口也会自动分配一个同一网段（172.17.0.0/16）的地址。\n当创建一个 Docker 容器的时候，同时会创建了一对 veth pair 接口（当数据包发送到一个接口时，另外一个接口也可以收到相同的数据包）。这对接口一端在容器内，即 eth0；另一端在本地并被挂载到 docker0 网桥，名称以 veth 开头（例如 vethAQI2QT）。通过这种方式，主机可以跟容器通信，容器之间也可以相互通信。Docker 就创建了在主机和所有容器之间一个虚拟共享网络。\n接下来的部分将介绍在一些场景中，Docker 所有的网络定制配置。以及通过 Linux 命令来调整、补充、甚至替换 Docker 默认的网络配置。\n快速配置指南 下面是一个跟 Docker 网络相关的命令列表。\n其中有些命令选项只有在 Docker 服务启动的时候才能配置，而且不能马上生效。\n-b BRIDGE or \u0026ndash;bridge=BRIDGE —指定容器挂载的网桥 \u0026ndash;bip=CIDR —定制 docker0 的掩码 -H SOCKET\u0026hellip; or \u0026ndash;host=SOCKET\u0026hellip; —Docker 服务端接收命令的通道 \u0026ndash;icc=true|false —是否支持容器之间进行通信 \u0026ndash;ip-forward=true|false —请看下文容器之间的通信 \u0026ndash;iptables=true|false —禁止 Docker 添加 iptables 规则 \u0026ndash;mtu=BYTES —容器网络中的 MTU 下面2个命令选项既可以在启动服务时指定，也可以 Docker 容器启动（docker run）时候指定。在 Docker 服务启动的时候指定则会成为默认值，后面执行 docker run 时可以覆盖设置的默认值。\n\u0026ndash;dns=IP_ADDRESS\u0026hellip; —使用指定的DNS服务器 \u0026ndash;dns-search=DOMAIN\u0026hellip; —指定DNS搜索域 最后这些选项只有在 docker run 执行时使用，因为它是针对容器的特性内容。\n-h HOSTNAME or \u0026ndash;hostname=HOSTNAME —配置容器主机名 \u0026ndash;link=CONTAINER_NAME:ALIAS —添加到另一个容器的连接 \u0026ndash;net=bridge|none|container:NAME_or_ID|host —配置容器的桥接模式 -p SPEC or \u0026ndash;publish=SPEC —映射容器端口到宿主主机 -P or \u0026ndash;publish-all=true|false —映射容器所有端口到宿主主机 Docker配置DNS Docker 没有为每个容器专门定制镜像，那么怎么自定义配置容器的主机名和 DNS 配置呢？\n秘诀就是它利用虚拟文件来挂载到来容器的 3 个相关配置文件。\n在容器中使用 mount 命令可以看到挂载信息：\n1 2 3 4 5 6 $ mount ... /dev/disk/by-uuid/1fec...ebdf on /etc/hostname type ext4 ... /dev/disk/by-uuid/1fec...ebdf on /etc/hosts type ext4 ... tmpfs on /etc/resolv.conf type tmpfs ... ... 这种机制可以让宿主主机 DNS 信息发生更新后，所有 Docker 容器的 dns 配置通过 /etc/resolv.conf 文件立刻得到更新。\n如果用户想要手动指定容器的配置，可以利用下面的选项。\n-h HOSTNAME or \u0026ndash;hostname=HOSTNAME\n设定容器的主机名，它会被写到容器内的 /etc/hostname 和 /etc/hosts。但它在容器外部看不到，既不会在 docker ps 中显示，也不会在其他的容器的 /etc/hosts 看到。\n\u0026ndash;link=CONTAINER_NAME:ALIAS\n选项会在创建容器的时候，添加一个其他容器的主机名到 /etc/hosts 文件中，让新容器的进程可以使用主机名 ALIAS 就可以连接它。\n\u0026ndash;dns=IP_ADDRESS\n添加 DNS 服务器到容器的 /etc/resolv.conf 中，让容器用这个服务器来解析所有不在 /etc/hosts 中的主机名。\n\u0026ndash;dns-search=DOMAIN\n设定容器的搜索域，当设定搜索域为 .example.com 时，在搜索一个名为 host 的主机时，DNS 不仅搜索host，还会搜索 host.example.com。\n注意：如果没有上述最后 2 个选项，Docker 会默认用主机上的 /etc/resolv.conf 来配置容器。\nDocker容器访问控制 容器的访问控制，主要通过 Linux 上的 iptables 防火墙来进行管理和实现。iptables 是 Linux 上默认的防火墙软件，在大部分发行版中都自带。\n容器访问外部网络 容器要想访问外部网络，需要本地系统的转发支持。在Linux 系统中，检查转发是否打开。\n1 2 $sysctl net.ipv4.ip_forward net.ipv4.ip_forward = 1 如果为 0，说明没有开启转发，则需要手动打开。\n1 $sysctl -w net.ipv4.ip_forward=1 如果在启动 Docker 服务的时候设定 \u0026ndash;ip-forward=true, Docker 就会自动设定系统的 ip_forward 参数为 1。\n容器之间访问 容器之间相互访问，需要两方面的支持。\n容器的网络拓扑是否已经互联。默认情况下，所有容器都会被连接到 docker0 网桥上。\n本地系统的防火墙软件 — iptables 是否允许通过。\n访问所有端口 当启动 Docker 服务时候，默认会添加一条转发策略到 iptables 的 FORWARD 链上。策略为通过（ACCEPT）还是禁止（DROP）取决于配置\u0026ndash;icc=true（缺省值）还是 \u0026ndash;icc=false。当然，如果手动指定 \u0026ndash;iptables=false 则不会添加 iptables 规则。\n可见，默认情况下，不同容器之间是允许网络互通的。如果为了安全考虑，可以在 /etc/default/docker 文件中配置 DOCKER_OPTS=\u0026ndash;icc=false 来禁止它。\n访问指定端口 在通过 -icc=false 关闭网络访问后，还可以通过 \u0026ndash;link=CONTAINER_NAME:ALIAS 选项来访问容器的开放端口。\n例如，在启动 Docker 服务时，可以同时使用 icc=false \u0026ndash;iptables=true 参数来关闭允许相互的网络访问，并让 Docker 可以修改系统中的 iptables 规则。\n此时，系统中的 iptables 规则可能是类似\n1 2 3 4 5 6 $ sudo iptables -nL ... Chain FORWARD (policy ACCEPT) target prot opt source destination DROP all -- 0.0.0.0/0 0.0.0.0/0 ... 之后，启动容器（docker run）时使用 \u0026ndash;link=CONTAINER_NAME:ALIAS 选项。Docker 会在 iptable 中为 两个容器分别添加一条 ACCEPT 规则，允许相互访问开放的端口（取决于 Dockerfile 中的 EXPOSE 行）。\n当添加了 \u0026ndash;link=CONTAINER_NAME:ALIAS 选项后，添加了 iptables 规则。\n1 2 3 4 5 6 7 $ sudo iptables -nL ... Chain FORWARD (policy ACCEPT) target prot opt source destination ACCEPT tcp -- 172.17.0.2 172.17.0.3 tcp spt:80 ACCEPT tcp -- 172.17.0.3 172.17.0.2 tcp dpt:80 DROP all -- 0.0.0.0/0 0.0.0.0/0 注意：\u0026ndash;link=CONTAINER_NAME:ALIAS 中的 CONTAINER_NAME 目前必须是 Docker 分配的名字，或使用 \u0026ndash;name 参数指定的名字。主机名则不会被识别。\nDocker端口映射实现 默认情况下，容器可以主动访问到外部网络的连接，但是外部网络无法访问到容器。\n容器访问外部实现 容器所有到外部网络的连接，源地址都会被NAT成本地系统的IP地址。这是使用 iptables 的源地址伪装操作实现的。\n查看主机的 NAT 规则。\n1 2 3 4 5 6 $ sudo iptables -t nat -nL ... Chain POSTROUTING (policy ACCEPT) target prot opt source destination MASQUERADE all -- 172.17.0.0/16 !172.17.0.0/16 ... 其中，上述规则将所有源地址在 172.17.0.0/16 网段，目标地址为其他网段（外部网络）的流量动态伪装为从系统网卡发出。MASQUERADE 跟传统 SNAT 的好处是它能动态从网卡获取地址。\n外部访问容器实现 容器允许外部访问，可以在 docker run 时候通过 -p 或 -P 参数来启用。\n不管用那种办法，其实也是在本地的 iptable 的 nat 表中添加相应的规则。\n使用 -P 时：\n1 2 3 4 5 $ iptables -t nat -nL ... Chain DOCKER (2 references) target prot opt source destination DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:49153 to:172.17.0.2:80 使用 -p 80:80 时：\n1 2 3 4 $ iptables -t nat -nL Chain DOCKER (2 references) target prot opt source destination DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.2:80 注意：\n这里的规则映射了 0.0.0.0，意味着将接受主机来自所有接口的流量。用户可以通过 -p IP:host_port:container_port 或 -p IP::port 来指定允许访问容器的主机上的 IP、接口等，以制定更严格的规则。 如果希望永久绑定到某个固定的 IP 地址，可以在 Docker 配置文件 /etc/default/docker 中指定 DOCKER_OPTS=\u0026quot;\u0026ndash;ip=IP_ADDRESS\u0026quot;，之后重启 Docker 服务即可生效。 配置docker0网桥 Docker 服务默认会创建一个 docker0 网桥（其上有一个 docker0 内部接口），它在内核层连通了其他的物理或虚拟网卡，这就将所有容器和本地主机都放到同一个物理网络。\nDocker 默认指定了 docker0 接口 的 IP 地址和子网掩码，让主机和容器之间可以通过网桥相互通信，它还给出了 MTU（接口允许接收的最大传输单元），通常是 1500 Bytes，或宿主主机网络路由上支持的默认值。这些值都可以在服务启动的时候进行配置。\n\u0026ndash;bip=CIDR — IP 地址加掩码格式，例如 192.168.1.5/24 \u0026ndash;mtu=BYTES — 覆盖默认的 Docker mtu 配置 也可以在配置文件中配置 DOCKER_OPTS，然后重启服务。\n由于目前 Docker 网桥是 Linux 网桥，用户可以使用 brctl show 来查看网桥和端口连接信息。\n1 2 3 4 $ sudo brctl show bridge name bridge id STP enabled interfaces docker0 8000.3a1d7362b4ee no veth65f9 vethdda6 *注：brctl 命令在 Debian、Ubuntu 中可以使用 sudo apt-get install bridge-utils 来安装。\n每次创建一个新容器的时候，Docker 从可用的地址段中选择一个空闲的 IP 地址分配给容器的 eth0 端口。使用本地主机上 docker0 接口的 IP 作为所有容器的默认网关。\n1 2 3 4 5 6 7 8 9 10 11 12 $ sudo docker run -i -t --rm base /bin/bash $ ip addr show eth0 24: eth0: \u0026lt;BROADCAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 32:6f:e0:35:57:91 brd ff:ff:ff:ff:ff:ff inet 172.17.0.3/16 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::306f:e0ff:fe35:5791/64 scope link valid_lft forever preferred_lft forever $ ip route default via 172.17.42.1 dev eth0 172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.3 $ exit Docker自定义网桥 除了默认的 docker0 网桥，用户也可以指定网桥来连接各个容器。\n在启动 Docker 服务的时候，使用 -b BRIDGE或\u0026ndash;bridge=BRIDGE 来指定使用的网桥。\n如果服务已经运行，那需要先停止服务，并删除旧的网桥。\n1 2 3 $ sudo service docker stop $ sudo ip link set dev docker0 down $ sudo brctl delbr docker0 然后创建一个网桥 bridge0。\n1 2 3 $ sudo brctl addbr bridge0 $ sudo ip addr add 192.168.5.1/24 dev bridge0 $ sudo ip link set dev bridge0 up 查看确认网桥创建并启动。\n1 2 3 4 5 $ ip addr show bridge0 4: bridge0: \u0026lt;BROADCAST,MULTICAST\u0026gt; mtu 1500 qdisc noop state UP group default link/ether 66:38:d0:0d:76:18 brd ff:ff:ff:ff:ff:ff inet 192.168.5.1/24 scope global bridge0 valid_lft forever preferred_lft forever 配置 Docker 服务，默认桥接到创建的网桥上。\n1 2 $ echo \u0026#39;DOCKER_OPTS=\u0026#34;-b=bridge0\u0026#34;\u0026#39; \u0026gt;\u0026gt; /etc/default/docker $ sudo service docker start 启动 Docker 服务。\n新建一个容器，可以看到它已经桥接到了 bridge0 上。\n可以继续用 brctl show 命令查看桥接的信息。另外，在容器中可以使用 ip addr 和 ip route 命令来查看 IP 地址配置和路由信息。\n容器网络访问原理 Docker 网络模式\nDocker支持五种网络模式\n bridge\n默认网络，Docker启动后创建一个docker0网桥，默认创建的容器也是添加到这个网桥中；IP地址段是172.17.0.1/16\n host\n容器不会获得一个独立的network namespace，而是与宿主机共用一个。\n none\n获取独立的network namespace，但不为容器进行任何网络配置。\n container\n与指定的容器使用同一个network namespace，网卡配置也都是相同的。\n 自定义\n自定义网桥，默认与bridge网络一样。\n\u0026ndash;link 通过容器名来实现互ping\n1 2 #docker exec -it 容器名2 --link 容器名1 容器名 #docker exec -it 容器名2 ping 容器名1 反过来，容器1 ping不通容器2。 其实这个容器2就是在本地配置了容器1的配置。\n1 2 #查看hosts配置， #docker exec -it 容器2 cat /etc/hosts 本质探究：\u0026ndash;link就是在hosts配置中增加了一个地址\n自定义网络，不适用docker0\ndocker问题:不支持容器名连接访问\n自定义网络 查看所有的网络\n1 #docker network ls 网络模式\nbridge：桥接docker（默认）\nnone：不配置网络\nhost：和宿主机共享网络\ncontainer：容器网络连通（用的少，局限很大）\n自定义一个网络\n1 #docker network create --driver bridge --subnet 子网 --gateway 网关 网络名 网络连通 实现tomcat-01 连接tomcat-net-01\n1 #docker network connect 网络名 容器名 Docker安全 评估 Docker 的安全性时，主要考虑三个方面:\n由内核的名字空间和控制组机制提供的容器内在安全 Docker程序（特别是服务端）本身的抗攻击性 内核安全性的加强机制对容器安全性的影响 内核名字空间 Docker 容器和 LXC 容器很相似，所提供的安全特性也差不多。当用 docker run 启动一个容器时，在后台 Docker 为容器创建了一个独立的名字空间和控制组集合。\n名字空间提供了最基础也是最直接的隔离，在容器中运行的进程不会被运行在主机上的进程和其它容器发现和作用。\n每个容器都有自己独有的网络栈，意味着它们不能访问其他容器的 sockets 或接口。不过，如果主机系统上做了相应的设置，容器可以像跟主机交互一样的和其他容器交互。当指定公共端口或使用 links 来连接 2 个容器时，容器就可以相互通信了（可以根据配置来限制通信的策略）。\n从网络架构的角度来看，所有的容器通过本地主机的网桥接口相互通信，就像物理机器通过物理交换机通信一样。\n那么，内核中实现名字空间和私有网络的代码是否足够成熟？\n内核名字空间从 2.6.15 版本（2008 年 7 月发布）之后被引入，数年间，这些机制的可靠性在诸多大型生产系统中被实践验证。\n实际上，名字空间的想法和设计提出的时间要更早，最初是为了在内核中引入一种机制来实现 OpenVZ 的特性。\n而 OpenVZ 项目早在 2005 年就发布了，其设计和实现都已经十分成熟。\n控制组 控制组是 Linux 容器机制的另外一个关键组件，负责实现资源的审计和限制。\n它提供了很多有用的特性；以及确保各个容器可以公平地分享主机的内存、CPU、磁盘 IO 等资源；当然，更重要的是，控制组确保了当容器内的资源使用产生压力时不会连累主机系统。\n尽管控制组不负责隔离容器之间相互访问、处理数据和进程，它在防止拒绝服务（DDOS）攻击方面是必不可少的。尤其是在多用户的平台（比如公有或私有的 PaaS）上，控制组十分重要。例如，当某些应用程序表现异常的时候，可以保证一致地正常运行和性能。\n控制组机制始于 2006 年，内核从 2.6.24 版本开始被引入。\nDocker服务端的防护 运行一个容器或应用程序的核心是通过 Docker 服务端。Docker 服务的运行目前需要 root 权限，因此其安全性十分关键。\n首先，确保只有可信的用户才可以访问 Docker 服务。Docker 允许用户在主机和容器间共享文件夹，同时不需要限制容器的访问权限，这就容易让容器突破资源限制。例如，恶意用户启动容器的时候将主机的根目录/映射到容器的 /host 目录中，那么容器理论上就可以对主机的文件系统进行任意修改了。这听起来很疯狂？但是事实上几乎所有虚拟化系统都允许类似的资源共享，而没法禁止用户共享主机根文件系统到虚拟机系统。\n这将会造成很严重的安全后果。因此，当提供容器创建服务时（例如通过一个 web 服务器），要更加注意进行参数的安全检查，防止恶意的用户用特定参数来创建一些破坏性的容器\n为了加强对服务端的保护，Docker 的 REST API（客户端用来跟服务端通信）在 0.5.2 之后使用本地的 Unix 套接字机制替代了原先绑定在 127.0.0.1 上的 TCP 套接字，因为后者容易遭受跨站脚本攻击。现在用户使用 Unix 权限检查来加强套接字的访问安全。\n用户仍可以利用 HTTP 提供 REST API 访问。建议使用安全机制，确保只有可信的网络或 VPN，或证书保护机制（例如受保护的 stunnel 和 ssl 认证）下的访问可以进行。此外，还可以使用 HTTPS 和证书来加强保护。\n最近改进的 Linux 名字空间机制将可以实现使用非 root 用户来运行全功能的容器。这将从根本上解决了容器和主机之间共享文件系统而引起的安全问题。\n终极目标是改进 2 个重要的安全特性：\n将容器的 root 用户映射到本地主机上的非 root 用户，减轻容器和主机之间因权限提升而引起的安全问题； 允许 Docker 服务端在非 root 权限下运行，利用安全可靠的子进程来代理执行需要特权权限的操作。这些子进程将只允许在限定范围内进行操作，例如仅仅负责虚拟网络设定或文件系统管理、配置操作等。 最后，建议采用专用的服务器来运行 Docker 和相关的管理服务（例如管理服务比如 ssh 监控和进程监控、管理工具 nrpe、collectd 等）。其它的业务服务都放到容器中去运行。\n内核能力机制 能力机制（Capability）是 Linux 内核一个强大的特性，可以提供细粒度的权限访问控制。\nLinux 内核自 2.2 版本起就支持能力机制，它将权限划分为更加细粒度的操作能力，既可以作用在进程上，也可以作用在文件上。\n例如，一个 Web 服务进程只需要绑定一个低于 1024 的端口的权限，并不需要 root 权限。那么它只需要被授权 net_bind_service 能力即可。此外，还有很多其他的类似能力来避免进程获取 root 权限。\n默认情况下，Docker 启动的容器被严格限制只允许使用内核的一部分能力。\n使用能力机制对加强 Docker 容器的安全有很多好处。通常，在服务器上会运行一堆需要特权权限的进程，包括有 ssh、cron、syslogd、硬件管理工具模块（例如负载模块）、网络配置工具等等。容器跟这些进程是不同的，因为几乎所有的特权进程都由容器以外的支持系统来进行管理。\nssh 访问被主机上ssh服务来管理； cron 通常应该作为用户进程执行，权限交给使用它服务的应用来处理； 日志系统可由 Docker 或第三方服务管理； 硬件管理无关紧要，容器中也就无需执行 udevd 以及类似服务； 网络管理也都在主机上设置，除非特殊需求，容器不需要对网络进行配置。 从上面的例子可以看出，大部分情况下，容器并不需要“真正的” root 权限，容器只需要少数的能力即可。为了加强安全，容器可以禁用一些没必要的权限。\n完全禁止任何 mount 操作； 禁止直接访问本地主机的套接字； 禁止访问一些文件系统的操作，比如创建新的设备、修改文件属性等； 禁止模块加载。 这样，就算攻击者在容器中取得了 root 权限，也不能获得本地主机的较高权限，能进行的破坏也有限。\n默认情况下，Docker采用 白名单 机制，禁用 必需功能 之外的其它权限。\n当然，用户也可以根据自身需求来为 Docker 容器启用额外的权限。\n其它安全特性 除了能力机制之外，还可以利用一些现有的安全机制来增强使用 Docker 的安全性，例如 TOMOYO, AppArmor, SELinux, GRSEC 等。\nDocker 当前默认只启用了能力机制。用户可以采用多种方案来加强 Docker 主机的安全，例如：\n在内核中启用 GRSEC 和 PAX，这将增加很多编译和运行时的安全检查；通过地址随机化避免恶意探测等。并且，启用该特性不需要 Docker 进行任何配置。 使用一些有增强安全特性的容器模板，比如带 AppArmor 的模板和 Redhat 带 SELinux 策略的模板。这些模板提供了额外的安全特性。 用户可以自定义访问控制机制来定制安全策略。 跟其它添加到 Docker 容器的第三方工具一样（比如网络拓扑和文件系统共享），有很多类似的机制，在不改变 Docker 内核情况下就可以加固现有的容器。\n总体来看，Docker 容器还是十分安全的，特别是在容器内不使用 root 权限来运行进程的话。\n另外，用户可以使用现有工具，比如 Apparmor, SELinux, GRSEC 来增强安全性；甚至自己在内核中实现更复杂的安全机制。\nPrometheus监控Docker主机 Prometheus 概述 Prometheus（普罗米修斯）是一个最初在SoundCloud上构建的监控系统。自2012年成为社区 开源项目，拥有非常活跃的开发人员和用户社区。为强调开源及独立维护，Prometheus于2016 年加入云原生云计算基金会（CNCF），成为继Kubernetes之后的第二个托管项目。\nhttps://prometheus.io\nhttps://github.com/prometheus\nPrometheus 特点：\n• 多维数据模型：由度量名称和键值对标识的时间序列数据\n• PromQL：一种灵活的查询语言，可以利用多维数据完成复杂的查询\n• 不依赖分布式存储，单个服务器节点可直接工作\n• 基于HTTP的pull方式采集时间序列数据\n• 推送时间序列数据通过PushGateway组件支持\n• 通过服务发现或静态配置发现目标\n• 多种图形模式及仪表盘支持（grafana）\n•Prometheus Server：收集指标和存储时间序列数据，并提供查询接口\n• ClientLibrary：客户端库\n• Push Gateway：短期存储指标数据。主要用于临时性的任务\n• Exporters：采集已有的第三方服务监控指标并暴露metrics\n• Alertmanager：告警\n• Web UI：简单的Web控制台\nPrometheus 部署 1 2 3 4 5 6 Docker部署：https://prometheus.io/docs/prometheus/latest/installation/ docker run -d \\ --name=prometheus \\ -p 9090:9090 \\ -v /tmp/prometheus.yml:/etc/prometheus/prometheus.yml \\ prom/prometheus 构建容器监控系统 cAdvisor+InfluxDB+Grafana cAdvisor：Google开源的工具，用于监控Docker主机和容器系统资源，通过图形页面实时显示数据，但不存储；它通过 宿主机/proc、/sys、/var/lib/docker等目录下文件获取宿主机和容器运行信息。 InfluxDB：是一个分布式的时间序列数据库，用来存储cAdvisor收集的系统资源数据。 Grafana：可视化展示平台，可做仪表盘，并图表页面操作很方面，数据源支持zabbix、Graphite、InfluxDB、 OpenTSDB、Elasticsearch等\n它们之间关系： cAdvisor容器数据采集-\u0026gt;InfluxDB容器数据存储-\u0026gt;Grafana可视化展示\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 部署 influxdb docker run \\ -d \\ -p 8083:8083 \\ -p 8086:8086 \\ --name influxdb tutum/influxdb cadvisor docker run -d \\ --volume=/:/rootfs:ro \\ --volume=/var/run:/var/run:rw \\ --volume=/sys:/sys:ro \\ --volume=/var/lib/docker/:/var/lib/docker:ro \\ --link influxdb:influxdb \\ -p 8081:8080 \\ --name=cadvisor \\ google/cadvisor:latest \\ -storage_driver=influxdb \\ -storage_driver_db=cadvisor \\ -storage_driver_host=influxdb:8086 grafana docker run -d \\ -p 3000:3000 \\ -e INFLUXDB_HOST=influxdb \\ -e INFLUXDB_PORT=8086 \\ -e INFLUXDB_NAME=cadvisor \\ -e INFLUXDB_USER=cadvisor \\ -e INFLUXDB_PASS=cadvisor \\ --link influxdb:influxsrv \\ --name grafana \\ grafana/grafana 企业级镜像仓库Harbor Harbor概述 Habor是由VMWare公司开源的容器镜像仓库。事实上，Habor是在Docker Registry上进行了相应的 企业级扩展，从而获得了更加广泛的应用，这些新的企业级特性包括：管理用户界面，基于角色的访 问控制 ，AD/LDAP集成以及审计日志等，足以满足基本企业需求。 官方地址：https://vmware.github.io/harbor/cn/\nHarbor部署 Harbor安装有3种方式：\n• 在线安装：从Docker Hub下载Harbor相关镜像，因此安装软件包非常小\n• 离线安装：安装包包含部署的相关镜像，因此安装包比较大\n• OVA安装程序：当用户具有vCenter环境时，使用此安装程序，在部署OVA后启动Harbor\n1 2 3 4 5 6 7 8 # tar zxvf harbor-offline-installer-v1.6.1.tgz # cd harbor # vi harbor.cfg hostname = 10.206.240.188 ui_url_protocol = http harbor_admin_password = 123456 # ./prepare # ./install.sh 基本使用 1 2 3 4 5 6 7 8 9 10 1、配置http镜像仓库可信任 # vi /etc/docker/daemon.json {\u0026#34;insecure-registries\u0026#34;:[\u0026#34;reg.ctnrs.com\u0026#34;]} # systemctl restart docker 2、打标签 # docker tag centos:6 reg.ctnrs.com/library/centos:6 3、上传 # docker push reg.ctnrs.com/library/centos:6 4、下载 # docker pull reg.ctnrs.com/library/centos:6 Docker-Compose 常见的Docker Compose脚本 https://gitee.com/zhengqingya/docker-compose\nDocker-compose是用于定义和运行多容器 Docker 应用程序的工具。使用\u0026quot;Docker-compose\u0026quot;，您可以使用 YAML 文件来配置应用程序的服务。然后，通过单个命令，从配置创建和启动所有服务。\n在所有环境中Docker-compose作品：生产、暂存、开发、测试以及 CI 工作流。\n使用Docker-compose基本上是一个三步过程：\n使用 定义应用的环境，以便可以在任何地方复制。Dockerfile 定义在中管理应用的服务，以便它们可以在隔离环境中一起运行。docker-compose.yml 运行和Docker-compose启动并运行整个应用。docker-compose up 下载并安装Docker-Compose 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 1.\t官方下载（下载很慢） #curl -L \u0026#34;https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)\u0026#34; -o /usr/local/bin/docker-compose 2.快速下载 #curl -L https://get.daocloud.io/docker/compose/releases/download/1.25.5/docker-compose-`uname -s`-`uname -m` \u0026gt; /usr/local/bin/docker-compose 3.Docke-compose设置权文件的权限 #chmod +x /usr/local/bin/docker-compose 4。测试 #docker-compose version 5.配置环境变量（可以省略） 方便后期操作，配置一个环境变量 将docker-compose文件移动到了/usr/local/bin，修改了/etc/profile文件，给/usr/local/bin配置到了PATH中 #mv docker-compose /usr/local/bin #vi /etc/profile 添加内容：export PATH=$JAVA_HOME:/usr/local/bin:$PATH #source /etc/profile 6.卸载 #rm /usr/local/bin/docker-compose 体验 python应用：计数器。\n1.应用app.py\n2.Dockerfile应用打包为镜像\n3.Docker-compose yaml文件（定义整个服务依赖的环境，web，redis，完整的上线服务）\n4.启动compose项目\n准备工作 1 2 #yum install python-pip #pip是python包管理工具 #yum install epel-release #报错的话执行 为项目创建目录 1 2 #mkdir composetest #cd composetest 在项目目录中创建一个名为app.py文件，内容如下 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import time import redis from flask import Flask app = Flask(__name__) cache = redis.Redis(host=\u0026#39;redis\u0026#39;, port=6379) def get_hit_count(): retries = 5 while True: try: return cache.incr(\u0026#39;hits\u0026#39;) except redis.exceptions.ConnectionError as exc: if retries == 0: raise exc retries -= 1 time.sleep(0.5) @app.route(\u0026#39;/\u0026#39;) def hello(): count = get_hit_count() return \u0026#39;Hello World! I have been seen {} times.\\n\u0026#39;.format(count) 在项目目录中创建一个名为requirements.txt文件，内容如下 1 2 flask redis 在项目目录中创建一个名为Dockerfile文件 1 2 3 4 5 6 7 8 9 10 11 12 FROM python:3.6-alpine ADD . /code WORKDIR /code RUN pip install -r requirements.txt CMD [\u0026#34;python\u0026#34;, \u0026#34;app.py\u0026#34;] #这告诉docker # 从Python 3.6镜像开始构建镜像。 #\t将当前目录添加. 到/code镜像中的路径中。 #\t将工作目录设置为/code #\t安装Python依赖项 #\t将容器的默认命令设置为python app.py. 在项目目录中创建一个名为docker-compose.yml文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 version: \u0026#34;3.8\u0026#34; services: web: build: . ports: - \u0026#34;5000:5000\u0026#34; volumes: - .:/code redis: image: \u0026#34;redis:alpine\u0026#34; #此Compose文件定义了俩个服务，web和redis。 #使用从Dockerfile当前目录中构建的图像 #将容器上的公共端口5000转发到主机上的端口5000。我们使用Flask Web服务器的默认端口5000 #该redis服务使用从Docker Hub注册表中提取的公共 Redis镜像 使用Compose构建和运行应用程序，在项目目录中，通过运行docker-compose up 启动应用程序 YAML文件格式及编写注意事项 YAML是一种标记语言很直观的数据序列化格式，可读性高。类似于XML数据描述语言，语法比XML简单的很多。 YAML数据结构通过缩进来表示，连续的项目通过减号来表示，键值对用冒号分隔，数组用中括号括起来，hash用花括号括起 来。\nYAML文件格式注意事项：\n不支持制表符tab键缩进，需要使用空格缩进 通常开头缩进2个空格 字符后缩进1个空格，如冒号、逗号、横杆 用井号注释 如果包含特殊字符用单引号引起来 布尔值（true、false、yes、no、on、off）必须用引号括起来，这样分析器会将他们解释为字符串。 Docker-Compose管理MySQL和Tomcat容器 yaml文件编辑\n官方案例（https://docs.docker.com/compose/compose-file/#compose-file-structure-and-examples）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 yml文件以key:value方式来指定配置信息 多个配置信息以换行+缩进的方式来区分 在docker-compose.yml文件中，不要使用制表符 version: \u0026#39;3.1\u0026#39; services: mysql: # 服务的名称 restart: always # 代表只要docker启动，那么这个容器就跟着一起启动 image: daocloud.io/library/mysql:5.7.4 # 指定镜像路径 container_name: mysql # 指定容器名称 ports: - 3306:3306 # 指定端口号的映射 environment: MYSQL_ROOT_PASSWORD: root # 指定MySQL的ROOT用户登录密码 TZ: Asia/Shanghai # 指定时区 volumes: - /opt/docker_mysql_tomcat/mysql_data:/var/lib/mysql # 映射数据卷 tomcat: restart: always image: daocloud.io/library/tomcat:8.5.15-jre8 container_name: tomcat ports: - 8080:8080 environment: TZ: Asia/Shanghai volumes: - /opt/docker_mysql_tomcat/tomcat_webapps:/usr/local/tomcat/webapps - /opt/docker_mysql_tomcat/tomcat_logs:/usr/local/tomcat/logs 使用docker-compose命令管理容器 在使用docker-compose的命令时，默认会在当前目录下找docker-compose.yml文件\n基于docker-compose.yml启动管理的容器 1 #docker-compose up -d 关闭并删除容器 1 #docker-compose down 开启|关闭|重启容器 1 #docker-compose start|stop|restart 查看由docker-compose管理的容器 1 #docker-compose ps 查看日志 1 #docker-compose logs -f docker-compose配合Dockerfile使用（统一在/opt目录下操作） 使用docker-compose.yml文件以及Dockerfile文件在生成自定义镜像的同时启动当前镜像，并且由docker-compose去管理容器\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1.首先生成一个文件夹，在其内编写docker-compose.yml文件 version: \u0026#39;3.1\u0026#39; services: ssm: restart: always build: # 构建自定义镜像 context: ../ # 指定dockerfile文件的所在路径 dockerfile: Dockerfile # 指定Dockerfile文件名称 image: ssm:1.0.1 container_name: ssm ports: - 8081:8080 environment: TZ: Asia/Shanghai 2.编写Dockerfile文件 from docker.io/nginx:latest copy index.html /usr/share/nginx/html 3.编写index.html(随意编写一个静态网页文件) 4.运行 docker-compose up -d docker-compose说明 管理控制容器或是镜像的工具\n管理控制容器 1 2 3 4 5 6 7 8 9 10 11 12 直接使用docker-compose.yml文件 文件的结构 version：版本 services： 服务内容（可以同时存在多个服务） servicesname: 服务名称，下级放的是服务相关的属性和设置（如 myweb） restart: 服务启动的时机 image: 创建容器时使用的镜像 container_name：创建后容器的名称 ports: 服务涉及到端口（不能和系统中的端口冲突） - 宿主机端口：容器端口 volumes：指定容器挂载的数据卷 - 宿主机路径：容器内的路径 管理控制镜像（需要Dockerfile文件的支撑） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 创建 Dockerfile文件 from：指定当前自定义镜像依赖的环境 copy：将相对路径下的内容复制到自定义镜像中 workdir：声明镜像的默认工作目录 run：执行的命令，可以编写多个 cmd：需要执行的命令（在workdir下执行的，cmd可以写多个，只以最后一个为准） 2 创建Docker-compose.yml文件去调用Dockerfile生成镜像 version:\u0026#39;3.1\u0026#39; 版本 services: 服务 ssm: 服务名称 restart: 启动时机 build: # 构建自定义镜像时使用 context:../# 指定dockerfile文件的所在路径（相对） dockerfile:Dockerfile# 指定Dockerfile文件名称 image: ssm:1.0.1 #生成后自定义镜像的名称 container_name:ssm #容器名称 ports: -8081:8080 environment: #系统环境 （语言，时区等的设置） TZ:Asia/Shanghai 开源项目 搭建博客（https://docs.docker.com/compose/wordpress/）\nDocker Compose项目 术语 首先介绍几个术语。\n服务（service）：一个应用容器，实际上可以运行多个相同镜像的实例。\n项目(project)：由一组关联的应用容器组成的一个完整业务单元。\n可见，一个项目可以由多个服务（容器）关联而成，Compose 面向项目进行管理。\n场景 下面，我们创建一个经典的 Web 项目：一个 Haproxy，挂载三个 Web 容器。\n创建一个 compose-haproxy-web 目录，作为项目工作目录，并在其中分别创建两个子目录：haproxy 和 web。\nWeb 子目录 这里用 Python 程序来提供一个简单的 HTTP 服务，打印出访问者的 IP 和 实际的本地 IP。\nindex.py 编写一个 index.py 作为服务器文件，代码为\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 #!/usr/bin/python #authors: yeasy.github.com #date: 2013-07-05 import sys import BaseHTTPServer from SimpleHTTPServer import SimpleHTTPRequestHandler import socket import fcntl import struct import pickle from datetime import datetime from collections import OrderedDict class HandlerClass(SimpleHTTPRequestHandler): def get_ip_address(self,ifname): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) return socket.inet_ntoa(fcntl.ioctl( s.fileno(), 0x8915, # SIOCGIFADDR struct.pack(\u0026#39;256s\u0026#39;, ifname[:15]) )[20:24]) def log_message(self, format, *args): if len(args) \u0026lt; 3 or \u0026#34;200\u0026#34; not in args[1]: return try: request = pickle.load(open(\u0026#34;pickle_data.txt\u0026#34;,\u0026#34;r\u0026#34;)) except: request=OrderedDict() time_now = datetime.now() ts = time_now.strftime(\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;) server = self.get_ip_address(\u0026#39;eth0\u0026#39;) host=self.address_string() addr_pair = (host,server) if addr_pair not in request: request[addr_pair]=[1,ts] else: num = request[addr_pair][0]+1 del request[addr_pair] request[addr_pair]=[num,ts] file=open(\u0026#34;index.html\u0026#34;, \u0026#34;w\u0026#34;) file.write(\u0026#34;\u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html\u0026gt; \u0026lt;body\u0026gt;\u0026lt;center\u0026gt;\u0026lt;h1\u0026gt;\u0026lt;font color=\\\u0026#34;blue\\\u0026#34; face=\\\u0026#34;Georgia, Arial\\\u0026#34; size=8\u0026gt;\u0026lt;em\u0026gt;HA\u0026lt;/em\u0026gt;\u0026lt;/font\u0026gt; Webpage Visit Results\u0026lt;/h1\u0026gt;\u0026lt;/center\u0026gt;\u0026#34;); for pair in request: if pair[0] == host: guest = \u0026#34;LOCAL: \u0026#34;+pair[0] else: guest = pair[0] if (time_now-datetime.strptime(request[pair][1],\u0026#39;%Y-%m-%d %H:%M:%S\u0026#39;)).seconds \u0026lt; 3: file.write(\u0026#34;\u0026lt;p style=\\\u0026#34;font-size:150%\\\u0026#34; \u0026gt;#\u0026#34;+ str(request[pair][1]) +\u0026#34;: \u0026lt;font color=\\\u0026#34;red\\\u0026#34;\u0026gt;\u0026#34;+str(request[pair][0])+ \u0026#34;\u0026lt;/font\u0026gt; requests \u0026#34; + \u0026#34;from \u0026amp;lt\u0026lt;font color=\\\u0026#34;blue\\\u0026#34;\u0026gt;\u0026#34;+guest+\u0026#34;\u0026lt;/font\u0026gt;\u0026amp;gt to WebServer \u0026amp;lt\u0026lt;font color=\\\u0026#34;blue\\\u0026#34;\u0026gt;\u0026#34;+pair[1]+\u0026#34;\u0026lt;/font\u0026gt;\u0026amp;gt\u0026lt;/p\u0026gt;\u0026#34;) else: file.write(\u0026#34;\u0026lt;p style=\\\u0026#34;font-size:150%\\\u0026#34; \u0026gt;#\u0026#34;+ str(request[pair][1]) +\u0026#34;: \u0026lt;font color=\\\u0026#34;maroon\\\u0026#34;\u0026gt;\u0026#34;+str(request[pair][0])+ \u0026#34;\u0026lt;/font\u0026gt; requests \u0026#34; + \u0026#34;from \u0026amp;lt\u0026lt;font color=\\\u0026#34;navy\\\u0026#34;\u0026gt;\u0026#34;+guest+\u0026#34;\u0026lt;/font\u0026gt;\u0026amp;gt to WebServer \u0026amp;lt\u0026lt;font color=\\\u0026#34;navy\\\u0026#34;\u0026gt;\u0026#34;+pair[1]+\u0026#34;\u0026lt;/font\u0026gt;\u0026amp;gt\u0026lt;/p\u0026gt;\u0026#34;) file.write(\u0026#34;\u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt;\u0026#34;); file.close() pickle.dump(request,open(\u0026#34;pickle_data.txt\u0026#34;,\u0026#34;w\u0026#34;)) if __name__ == \u0026#39;__main__\u0026#39;: try: ServerClass = BaseHTTPServer.HTTPServer Protocol = \u0026#34;HTTP/1.0\u0026#34; addr = len(sys.argv) \u0026lt; 2 and \u0026#34;0.0.0.0\u0026#34; or sys.argv[1] port = len(sys.argv) \u0026lt; 3 and 80 or int(sys.argv[2]) HandlerClass.protocol_version = Protocol httpd = ServerClass((addr, port), HandlerClass) sa = httpd.socket.getsockname() print \u0026#34;Serving HTTP on\u0026#34;, sa[0], \u0026#34;port\u0026#34;, sa[1], \u0026#34;...\u0026#34; httpd.serve_forever() except: exit() index.html 生成一个临时的 index.html 文件，其内容会被 index.py 更新。\n1 $ touch index.html Dockerfile 生成一个 Dockerfile，内容为\n1 2 3 4 5 FROM python:2.7 WORKDIR /code ADD . /code EXPOSE 80 CMD python index.py haproxy 目录 在其中生成一个 haproxy.cfg 文件，内容为\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 global log 127.0.0.1 local0 log 127.0.0.1 local1 notice defaults log global mode http option httplog option dontlognull timeout connect 5000ms timeout client 50000ms timeout server 50000ms listen stats :70 stats enable stats uri / frontend balancer bind 0.0.0.0:80 mode http default_backend web_backends backend web_backends mode http option forwardfor balance roundrobin server weba weba:80 check server webb webb:80 check server webc webc:80 check option httpchk GET / http-check expect status 200 docker-compose.yml 编写 docker-compose.yml 文件，这个是 Compose 使用的主模板文件。内容十分简单，指定 3 个 web 容器，以及 1 个 haproxy 容器。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 weba: build: ./web expose: - 80 webb: build: ./web expose: - 80 webc: build: ./web expose: - 80 haproxy: image: haproxy:latest volumes: - haproxy:/haproxy-override - haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro links: - weba - webb - webc ports: - \u0026#34;80:80\u0026#34; - \u0026#34;70:70\u0026#34; expose: - \u0026#34;80\u0026#34; - \u0026#34;70\u0026#34; 运行 compose 项目 现在 compose-haproxy-web 目录长成下面的样子。\n1 2 3 4 5 6 7 8 compose-haproxy-web ├── docker-compose.yml ├── haproxy │ └── haproxy.cfg └── web ├── Dockerfile ├── index.html └── index.py 在该目录下执行 docker-compose up 命令，会整合输出所有容器的输出。\n1 2 3 4 5 6 $sudo docker-compose up Recreating composehaproxyweb_webb_1... Recreating composehaproxyweb_webc_1... Recreating composehaproxyweb_weba_1... Recreating composehaproxyweb_haproxy_1... Attaching to composehaproxyweb_webb_1, composehaproxyweb_webc_1, composehaproxyweb_weba_1, composehaproxyweb_haproxy_1 此时访问本地的 80 端口，会经过 haproxy 自动转发到后端的某个 web 容器上，刷新页面，可以观察到访问的容器地址的变化。\n访问本地 70 端口，可以查看到 haproxy 的统计信息。\n当然，还可以使用 consul、etcd 等实现服务发现，这样就可以避免手动指定后端的 web 容器了，更为灵活。\nDocker API 三种API 在Docker的生态系统中，存在下列三种API：\nReistry API：与存储Docker镜像的Registry相关的功能。\nDocker Hub API：与Docker Hub相关的功能\nDocker Remote API：与Docker守护进程相关的功能。\n其中，Docker Remote API是使用最为频繁的API类型\n启动Remote API Remote API主要用于远程访问Docker守护进程从而下达指令的。\n因此，我们在启动Docker守护进程时，需要添加-H参数并指定开启的访问端口。\n通常，我们可以通过编辑守护进程的配置文件来实现。\n不过对于不同操作系统而言，守护进程启动的配置文件也不尽相同：\n1 2 3 4 5 6 Ubuntu系统：/etc/default/docker文件 Centos系统：/etc/sysconfig/docker文件 在该配置文件最后，添加内容如下： OPTIONS=\u0026#39;-H=tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock\u0026#39; 修改完成后执行如下命令，重启Docker守护进程： #systemctl restart docker 测试Remote API 1 #docker -H 127.0.0.1:2375 info 上面的试验中，我们已经确认了与Docker守护进程之间的连通性。\n下面，我们来使用一些Remote API。\n1 #curl http://127.0.0.1:2375/info 从返回结果看，我们可以得到类似的docker info时的JSON格式的数据。\n1 2 3 通过API管理Docker镜像 调用/images/json接口可以获取镜像列表： #curl http://127.0.0.1:2375/images/json | python -mjson.tool ==Ps：通过python -mjson.tool可以将JSON数据格式化显示。==\n通过API管理Docker容器 调用/containers/json接口可以获取正在运行中的容器列表：\n1 #curl http://127.0.0.1:2375/containers/json | python -mjson.tool 如果想要查询全部的容器（包含不是正在运行的容器）时，可以调用如下接口：\n1 #curl http://127.0.0.1:2375/containers/json?all=1 | python -mjson.tool 此外，我们还可以使用/containers/create以及/containers/start来创建和启动容器，从而实现docker run的功能。\n创建容器： 1 #curl -X POST -H \u0026#34;Content-Type:application/json\u0026#34; http://127.0.0.1:2375/containers/create -d \u0026#39;{\u0026#34;Image\u0026#34;: \u0026#34;docker.io/nginx\u0026#34;}\u0026#39; Consul 安装consul 1 docker pull consul 启动服务:consul1 1 docker run --name consul1 -d -p 8500:8500 -p 8300:8300 -p 8301:8301 -p 8302:8302 -p 8600:8600 consul agent -server -bootstrap-expect 2 -ui -bind=0.0.0.0 -client=0.0.0.0 8500 http 端口，用于 http 接口和 web ui\n8300 server rpc 端口，同一数据中心 consul server 之间通过该端口通信\n8301 serf lan 端口，同一数据中心 consul client 通过该端口通信\n8302 serf wan 端口，不同数据中心 consul server 通过该端口通信\n8600 dns 端口，用于服务发现\n-bbostrap-expect 2: 集群至少两台服务器，才能选举集群leader\n-ui：运行 web 控制台\n-bind： 监听网口，0.0.0.0 表示所有网口，如果不指定默认未127.0.0.1，则无法和容器通信\n-client ： 限制某些网口可以访问\n获取 consul server1 的 ip 地址 1 docker inspect --format =\u0026#39;{{ .NetworkSettings.IPAddress }}\u0026#39; consul1 启动服务：consul2 1 docker run --name consul2 -d -p 8501:8500 consul agent -server -ui -bind=0.0.0.0 -client=0.0.0.0 -join 172.17.0.2 启动服务：consul3 1 docker run --name consul3 -d -p 8502:8500 consul agent -server -ui -bind=0.0.0.0 -client=0.0.0.0 -join 172.17.0.2 第四第五略 访问 宿主机浏览器访问：http://localhost:8500 或者 http://localhost:8501 或者 http://localhost:8502\n提升\n创建test.json文件，以脚本形式注册服务到consul：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 test.json文件内容如下： { \u0026#34;ID\u0026#34;: \u0026#34;test-service1\u0026#34;, \u0026#34;Name\u0026#34;: \u0026#34;test-service1\u0026#34;, \u0026#34;Tags\u0026#34;: [ \u0026#34;test\u0026#34;, \u0026#34;v1\u0026#34; ], \u0026#34;Address\u0026#34;: \u0026#34;127.0.0.1\u0026#34;, \u0026#34;Port\u0026#34;: 8000, \u0026#34;Meta\u0026#34;: { \u0026#34;X-TAG\u0026#34;: \u0026#34;testtag\u0026#34; }, \u0026#34;EnableTagOverride\u0026#34;: false, \u0026#34;Check\u0026#34;: { \u0026#34;DeregisterCriticalServiceAfter\u0026#34;: \u0026#34;90m\u0026#34;, \u0026#34;HTTP\u0026#34;: \u0026#34;http://zhihu.com\u0026#34;, \u0026#34;Interval\u0026#34;: \u0026#34;10s\u0026#34; } } 通过 http 接口注册服务（端口可以是8500. 8501， 8502等能够正常访问consul的就行）：\n1 curl -X PUT --data @test.json http://localhost:8500/v1/agent/service/register 控制台如下所示：\n宿主机浏览器访问以下链接可以看到所有通过健康检查的可用test-service1服务列表\n（任意正常启动consul的端口皆可）：\nhttp://localhost:8501/v1/health/service/test-service1?passing\n输出json格式的内容，如下所示：\n其它应用程序可以通过这种方式轮询获取服务列表，这就是微服务能够动态知道其依赖微服务可用列表的原理。\n解绑定：\n1 curl -i -X PUT http://127.0.0.1:8501/v1/agent/service/deregister/test-service1 集群方式需要至少启动两个consul server，本机调试web应用时，为了方便可以用 -dev 参数方式仅启动一个consul server\n1 docker run --name consul0 -d -p 8500:8500 -p 8300:8300 -p 8301:8301 -p 8302:8302 -p 8600:8600 consul:1.2.2 agent -dev -bind=0.0.0.0 -client=0.0.0.0 Rancher 安装Rancher 1 2 3 4 docker search rancher docker pull docker.io/rancher/server docker pull docker.io/rancher/rancher docker pull docker.io/rancher/agent 启动容器 1 sudo docker run -d --restart=always -p 8080:8080 rancher/server 查看ip地址 1 ip a 访问 1 ip:8080 登录到rancher官网，为安全起见，设置管理账户\n然后进行添加主机操作，根据网站指引操作，生成一条命令，在docker中运行\n1 docker run -e CATTLE_AGENT_IP=\u0026#34;192.168.200.131\u0026#34; --rm --privileged -v /var/run/docker.sock:/var/run/docker.sock -v /var/lib/rancher:/var/lib/rancher rancher/agent:v1.2.11 http://192.168.200.131:8080/v1/scripts/6A5FD96A5C2385D994EF:1577750400000:HeMhHq8vWHgMCIHBJoFHGh8U 当在宿主机中运行完成后，网站中会显示成功添加主机\nEnvironment在Rancher中被定义为主要用于容器编排和管理的环境，比如Dev或者TEST或者PROD环境等等。 目前Rancher支持如下四种：Cattle/Kubernetes/Mesos/Swarm, Cattle是Rancher自己内置的缺省的编排环境，缺省的Default的即为Cattle类型的。\n案例：博客部署 （1）在rancher平台导航栏中，点击应用商店按钮，找到wordpress应用，点击查看详情，示例如图15所示： （2）更改WordPress应用的端口号为8088，然后点击页面下方的启动按钮，示例如图16所示： （3）等待一段时间后出现Active即创建成功，创建成功如图17所示，创建成功后，点击端口8088直接访问WordPress应用，如图18所示：\nSwarm Swarm介绍 Swarm是什么？ Swarm是Docker公司自研发的容器集群管理系统，Swarm在早期是作为一个独立服务存在，在Docker Engine v1.12中集成了 Swarm的集群管理和编排功能。可以通过初始化Swarm或加入现有Swarm来启用Docker引擎的Swarm模式。 Docker Engine CLI和API包括了管理Swarm节点命令，比如添加、删除节点，以及在Swarm中部署和编排服务。 也增加了服务栈（Stack）、服务（Service）、任务（Task）概念。\nSwarm两种角色： Manager：接收客户端服务定义，将任务发送到worker节点；维护集群期望状态和集群管理功能及Leader选举。默认情况下 manager节点也会运行任务，也可以配置只做管理任务。 Worker：接收并执行从管理节点分配的任务，并报告任务当前状态，以便管理节点维护每个服务期望状态。\nSwarm特点：\nDocker Engine集成集群管理 使用Docker Engine CLI 创建一个Docker Engine的Swarm模式，在集群中部署应用程序服务。 去中心化设计 Swarm角色分为Manager和Worker节点，Manager节点故障不影响应用使用。 扩容缩容 可以声明每个服务运行的容器数量，通过添加或删除容器数自动调整期望的状态。 期望状态协调 Swarm Manager节点不断监视集群状态，并调整当前状态与期望状态之间的差异。例如，设置一个服务运行10个副本容器，如果两个副本的服 务器节点崩溃，Manager将创建两个新的副本替代崩溃的副本。并将新的副本分配到可用的worker节点。 多主机网络 可以为服务指定overlay网络。当初始化或更新应用程序时，Swarm manager会自动为overlay网络上的容器分配IP地址。 服务发现 Swarm manager节点为集群中的每个服务分配唯一的DNS记录和负载均衡VIP。可以通过Swarm内置的DNS服务器查询集群中每个运行的容器。 负载均衡 实现服务副本负载均衡，提供入口访问。也可以将服务入口暴露给外部负载均衡器再次负载均衡。 安全传输 Swarm中的每个节点使用TLS相互验证和加密，确保安全的其他节点通信。 滚动更新 升级时，逐步将应用服务更新到节点，如果出现问题，可以将任务回滚到先前版本。 集群部署及节点管理 阿里云购买四台服务器 4台机器安装docker 工作模式 搭建集群 初始化docker swarm init\ndocker swarm join 加入一个节点\n1 2 3 #获取令牌 #docker swarm join-token manager #docker swarm join-token worker 把其他节点也搭建进去\nDocker swarm项目 概念学习 swarm\n集的管理和编号, docker可以初始化一个 swarm集群,其他节点可以加入。(管理、工作者)\ndocker\nNode就是一个节点。多个节点就组成了一个网路集,(管理、工作者)\nService\n任务,可以在管理节点或者工作节点来运行。核心！用户访问。\nTask\n容器内的命令,细节任务\n命令-\u0026gt;管理-\u0026gt;API-\u0026gt;调度-\u0026gt;工作节点（创建Task容器维护创建）\nDocker Swarm项目安装 安装swarm的最简单的方式是使用Docker官方的swarm镜像\n1 $ sudo docker pull swarm 可以使用下面的命令来查看swarm是否成功安装。\n1 $ sudo docker run —rm swarm -v 输出下面的形式则表示成功安装(具体输出根据swarm的版本变化)\n1 swarm version 0.2.0 (48fd993) Docker Swarm项目使用 在使用 swarm 管理集群前，需要把集群中所有的节点的 docker daemon 的监听方式更改为 0.0.0.0:2375。\n可以有两种方式达到这个目的，第一种是在启动docker daemon的时候指定\n1 sudo docker -H 0.0.0.0:2375\u0026amp; 第二种方式是直接修改 Docker 的配置文件(Ubuntu 上是 /etc/default/docker，其他版本的 Linux 上略有不同)\n在文件的最后添加下面这句代码：\n1 DOCKER_OPTS=\u0026#34;-H 0.0.0.0:2375 -H unix:///var/run/docker.sock\u0026#34; 需要注意的是，一定要在所有希望被 Swarm 管理的节点上进行的。修改之后要重启 Docker\n1 sudo service docker restart Docker 集群管理需要使用服务发现(Discovery service backend)功能，Swarm支持以下的几种方式：DockerHub 提供的服务发现功能，本地的文件，etcd，counsel，zookeeper 和 IP 列表，本文会详细讲解前两种方式，其他的用法都是大同小异的。\n先说一下本次试验的环境，本次试验包括三台机器，IP地址分别为192.168.1.84,192.168.1.83和192.168.1.124.利用这三台机器组成一个docker集群，其中83这台机器同时充当swarm manager节点。\n使用 DockerHub 提供的服务发现功能 创建集群 token 在上面三台机器中的任何一台机器上面执行 swarm create 命令来获取一个集群标志。这条命令执行完毕后，Swarm 会前往 DockerHub 上内置的发现服务中获取一个全球唯一的 token，用来标识要管理的集群。\n1 sudo docker run --rm swarm create 我们在84这台机器上执行这条命令，输出如下：\n1 2 rio@084:~$ sudo docker run --rm swarm create b7625e5a7a2dc7f8c4faacf2b510078e 可以看到我们返回的 token 是 b7625e5a7a2dc7f8c4faacf2b510078e，每次返回的结果都是不一样的。这个 token 一定要记住，后面的操作都会用到这个 token。\n加入集群 在所有要加入集群的节点上面执行 swarm join 命令，表示要把这台机器加入这个集群当中。在本次试验中，就是要在 83、84 和 124 这三台机器上执行下面的这条命令：\n1 sudo docker run --rm swarm join addr=ip_address:2375 token://token_id 其中的 ip_address 换成执行这条命令的机器的 IP，token_id 换成上一步执行 swarm create 返回的 token。\n在83这台机器上面的执行结果如下：\n1 2 rio@083:~$ sudo docker run --rm swarm join --addr=192.168.1.83:2375 token://b7625e5a7a2dc7f8c4faacf2b510078e time=\u0026#34;2015-05-19T11:48:03Z\u0026#34; level=info msg=\u0026#34;Registering on the discovery service every 25 seconds...\u0026#34; addr=\u0026#34;192.168.1.83:2375\u0026#34; discovery=\u0026#34;token://b7625e5a7a2dc7 f8c4faacf2b510078e\u0026#34; 这条命令不会自动返回，要我们自己执行 Ctrl+C 返回。\n启动swarm manager 因为我们要使用 83 这台机器充当 swarm 管理节点，所以需要在83这台机器上面执行 swarm manage 命令：\n1 sudo docker run -d -p 2376:2375 swarm manage token://b7625e5a7a2dc7f8c4faacf2b510078e 执行结果如下：\n1 2 rio@083:~$ sudo docker run -d -p 2376:2375 swarm manage token://b7625e5a7a2dc7f8c4faacf2b510078e 83de3e9149b7a0ef49916d1dbe073e44e8c31c2fcbe98d962a4f85380ef25f76 这条命令如果执行成功会返回已经启动的 Swarm 的容器的 ID，此时整个集群已经启动起来了。\n现在通过 docker ps 命令来看下有没有启动成功。\n1 2 3 rio@083:~$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 83de3e9149b7 swarm:latest \u0026#34;/swarm manage token 4 minutes ago Up 4 minutes 0.0.0.0:2376-\u0026gt;2375/tcp stupefied_stallman 可以看到，Swarm 已经成功启动。\n在执行 Swarm manage 这条命令的时候，有几点需要注意的：\n这条命令需要在充当 swarm 管理者的机器上执行 Swarm 要以 daemon 的形式执行 映射的端口可以使任意的除了 2375 以外的并且是未被占用的端口，但一定不能是 2375 这个端口，因为 2375 已经被 Docker 本身给占用了。 集群启动成功以后，现在我们可以在任何一台节点上使用 swarm list 命令查看集群中的节点了，本实验在 124 这台机器上执行 swarm list 命令：\n1 2 3 4 rio@124:~$ sudo docker run --rm swarm list token://b7625e5a7a2dc7f8c4faacf2b510078e 192.168.1.84:2375 192.168.1.124:2375 192.168.1.83:2375 输出结果列出的IP地址正是我们使用 swarm join 命令加入集群的机器的IP地址。\n现在我们可以在任何一台安装了 Docker 的机器上面通过命令(命令中要指明swarm manager机器的IP地址)来在集群中运行container了。\n本次试验，我们在 192.168.1.85 这台机器上使用 docker info 命令来查看集群中的节点的信息。\n其中 info 也可以换成其他的 Docker 支持的命令。\n1 2 3 4 5 6 7 8 9 10 11 12 13 rio@085:~$ sudo docker -H 192.168.1.83:2376 info Containers: 8 Strategy: spread Filters: affinity, health, constraint, port, dependency Nodes: 2 sclu083: 192.168.1.83:2375 └ Containers: 1 └ Reserved CPUs: 0 / 2 └ Reserved Memory: 0 B / 4.054 GiB sclu084: 192.168.1.84:2375 └ Containers: 7 └ Reserved CPUs: 0 / 2 └ Reserved Memory: 0 B / 4.053 GiB 结果输出显示这个集群中只有两个节点，IP地址分别是 192.168.1.83 和 192.168.1.84，结果不对呀，我们明明把三台机器加入了这个集群，还有 124 这一台机器呢？\n经过排查，发现是忘了修改 124 这台机器上面改 docker daemon 的监听方式，只要按照上面的步骤修改写 docker daemon 的监听方式就可以了。\n在使用这个方法的时候，使用swarm create可能会因为网络的原因会出现类似于下面的这个问题：\n1 2 3 rio@227:~$ sudo docker run --rm swarm create [sudo] password for rio: time=\u0026#34;2015-05-19T12:59:26Z\u0026#34; level=fatal msg=\u0026#34;Post https://discovery-stage.hub.docker.com/v1/clusters: dial tcp: i/o timeout\u0026#34; 使用文件 第二种方法相对于第一种方法要简单得多，也不会出现类似于上面的问题。\n第一步：在 swarm 管理节点上新建一个文件，把要加入集群的机器 IP 地址和端口号写入文件中，本次试验就是要在83这台机器上面操作：\n1 2 3 4 5 6 7 rio@083:~$ echo 192.168.1.83:2375 \u0026gt;\u0026gt; cluster rio@083:~$ echo 192.168.1.84:2375 \u0026gt;\u0026gt; cluster rio@083:~$ echo 192.168.1.124:2375 \u0026gt;\u0026gt; cluster rio@083:~$ cat cluster 192.168.1.83:2375 192.168.1.84:2375 192.168.1.124:2375 第二步：在083这台机器上面执行 swarm manage 这条命令：\n1 2 rio@083:~$ sudo docker run -d -p 2376:2375 -v $(pwd)/cluster:/tmp/cluster swarm manage file:///tmp/cluster 364af1f25b776f99927b8ae26ca8db5a6fe8ab8cc1e4629a5a68b48951f598ad 使用docker ps来查看有没有启动成功：\n1 2 3 rio@083:~$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 364af1f25b77 swarm:latest \u0026#34;/swarm manage file: About a minute ago Up About a minute 0.0.0.0:2376-\u0026gt;2375/tcp happy_euclid 可以看到，此时整个集群已经启动成功。\n在使用这条命令的时候需要注意的是注意：这里一定要使用-v命令，因为cluster文件是在本机上面，启动的容器默认是访问不到的，所以要通过-v命令共享。\n接下来的就可以在任何一台安装了docker的机器上面通过命令使用集群，同样的，在85这台机器上执行docker info命令查看集群的节点信息：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 rio@s085:~$ sudo docker -H 192.168.1.83:2376 info Containers: 9 Strategy: spread Filters: affinity, health, constraint, port, dependency Nodes: 3 atsgxxx: 192.168.1.227:2375 └ Containers: 0 └ Reserved CPUs: 0 / 4 └ Reserved Memory: 0 B / 2.052 GiB sclu083: 192.168.1.83:2375 └ Containers: 2 └ Reserved CPUs: 0 / 2 └ Reserved Memory: 0 B / 4.054 GiB sclu084: 192.168.1.84:2375 └ Containers: 7 └ Reserved CPUs: 0 / 2 └ Reserved Memory: 0 B / 4.053 GiB Docker Swarm项目调度器 swarm支持多种调度策略来选择节点。每次在swarm启动container的时候，swarm会根据选择的调度策略来选择节点运行container。目前支持的有:spread,binpack和random。\n在执行swarm manage命令启动 swarm 集群的时候可以通过 \u0026ndash;strategy 参数来指定，默认的是spread。\nspread和binpack策略会根据每台节点的可用CPU，内存以及正在运行的containers的数量来给各个节点分级，而random策略，顾名思义，他不会做任何的计算，只是单纯的随机选择一个节点来启动container。这种策略一般只做调试用。\n使用spread策略，swarm会选择一个正在运行的container的数量最少的那个节点来运行container。这种情况会导致启动的container会尽可能的分布在不同的机器上运行，这样的好处就是如果有节点坏掉的时候不会损失太多的container。\nbinpack 则相反，这种情况下，swarm会尽可能的把所有的容器放在一台节点上面运行。这种策略会避免容器碎片化，因为他会把未使用的机器分配给更大的容器，带来的好处就是swarm会使用最少的节点运行最多的容器。\nspread 策略 先来演示下 spread 策略的情况。\n1 2 3 4 5 rio@083:~$ sudo docker run -d -p 2376:2375 -v $(pwd)/cluster:/tmp/cluster swarm manage --strategy=spread file:///tmp/cluster 7609ac2e463f435c271d17887b7d1db223a5d696bf3f47f86925c781c000cb60 ats@sclu083:~$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7609ac2e463f swarm:latest \u0026#34;/swarm manage --str 6 seconds ago Up 5 seconds 0.0.0.0:2376-\u0026gt;2375/tcp focused_babbage 三台机器除了83运行了 Swarm之外，其他的都没有运行任何一个容器，现在在85这台节点上面在swarm集群上启动一个容器\n1 2 3 4 5 rio@085:~$ sudo docker -H 192.168.1.83:2376 run --name node-1 -d -P redis 2553799f1372b432e9b3311b73e327915d996b6b095a30de3c91a47ff06ce981 rio@085:~$ sudo docker -H 192.168.1.83:2376 ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2553799f1372 redis:latest /entrypoint.sh redis 24 minutes ago Up Less than a second 192.168.1.84:32770-\u0026gt;6379/tcp 084/node-1 启动一个 redis 容器，查看结果\n1 2 3 4 5 6 rio@085:~$ sudo docker -H 192.168.1.83:2376 run --name node-2 -d -P redis 7965a17fb943dc6404e2c14fb8585967e114addca068f233fcaf60c13bcf2190 rio@085:~$ sudo docker -H 192.168.1.83:2376 ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7965a17fb943 redis:latest /entrypoint.sh redis Less than a second ago Up 1 seconds 192.168.1.124:49154-\u0026gt;6379/tcp 124/node-2 2553799f1372 redis:latest /entrypoint.sh redis 29 minutes ago Up 4 minutes 192.168.1.84:32770-\u0026gt;6379/tcp 084/node-1 再次启动一个 redis 容器，查看结果\n1 2 3 4 5 6 7 rio@085:~$ sudo docker -H 192.168.1.83:2376 run --name node-3 -d -P redis 65e1ed758b53fbf441433a6cb47d288c51235257cf1bf92e04a63a8079e76bee rio@085:~$ sudo docker -H 192.168.1.83:2376 ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7965a17fb943 redis:latest /entrypoint.sh redis Less than a second ago Up 4 minutes 192.168.1.227:49154-\u0026gt;6379/tcp 124/node-2 65e1ed758b53 redis:latest /entrypoint.sh redis 25 minutes ago Up 17 seconds 192.168.1.83:32770-\u0026gt;6379/tcp 083/node-3 2553799f1372 redis:latest /entrypoint.sh redis 33 minutes ago Up 8 minutes 192.168.1.84:32770-\u0026gt;6379/tcp 084/node-1 可以看到三个容器都是分布在不同的节点上面的。\nbinpack 策略 现在来看看binpack策略下的情况。在083上面执行命令：\n1 2 rio@083:~$ sudo docker run -d -p 2376:2375 -v $(pwd)/cluster:/tmp/cluster swarm manage --strategy=binpack file:///tmp/cluster f1c9affd5a0567870a45a8eae57fec7c78f3825f3a53fd324157011aa0111ac5 现在在集群中启动三个 redis 容器，查看分布情况：\n1 2 3 4 5 6 7 8 9 10 11 rio@085:~$ sudo docker -H 192.168.1.83:2376 run --name node-1 -d -P redis 18ceefa5e86f06025cf7c15919fa64a417a9d865c27d97a0ab4c7315118e348c rio@085:~$ sudo docker -H 192.168.1.83:2376 run --name node-2 -d -P redis 7e778bde1a99c5cbe4701e06935157a6572fb8093fe21517845f5296c1a91bb2 rio@085:~$ sudo docker -H 192.168.1.83:2376 run --name node-3 -d -P redis 2195086965a783f0c2b2f8af65083c770f8bd454d98b7a94d0f670e73eea05f8 rio@085:~$ sudo docker -H 192.168.1.83:2376 ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 2195086965a7 redis:latest /entrypoint.sh redis 24 minutes ago Up Less than a second 192.168.1.83:32773-\u0026gt;6379/tcp 083/node-3 7e778bde1a99 redis:latest /entrypoint.sh redis 24 minutes ago Up Less than a second 192.168.1.83:32772-\u0026gt;6379/tcp 083/node-2 18ceefa5e86f redis:latest /entrypoint.sh redis 25 minutes ago Up 22 seconds 192.168.1.83:32771-\u0026gt;6379/tcp 083/node-1 可以看到，所有的容器都是分布在同一个节点上运行的。\nDocker Swarm项目过滤器 swarm 的调度器(scheduler)在选择节点运行容器的时候支持几种过滤器 (filter)：Constraint,Affinity,Port,Dependency,Health\n可以在执行 swarm manage 命令的时候通过 \u0026ndash;filter 选项来设置。\nConstraint Filter constraint 是一个跟具体节点相关联的键值对，可以看做是每个节点的标签，这个标签可以在启动docker daemon的时候指定，比如\n1 sudo docker -d --label label_name=label01 也可以写在docker的配置文件里面（在ubuntu上面是 /etc/default/docker）。\n在本次试验中，给083添加标签—label label_name=083,084添加标签—label label_name=084,124添加标签—label label_name=084,\n以083为例，打开/etc/default/docker文件，修改DOCKER_OPTS：\n1 DOCKER_OPTS=\u0026#34;-H 0.0.0.0:2375 -H unix:///var/run/docker.sock --label label_name=083\u0026#34; 在使用docker run命令启动容器的时候使用 -e constarint:key=value 的形式，可以指定container运行的节点。\n比如我们想在84上面启动一个 redis 容器。\n1 2 rio@085:~$ sudo docker -H 192.168.1.83:2376 run --name redis_1 -d -e constraint:label_name==084 redis fee1b7b9dde13d64690344c1f1a4c3f5556835be46b41b969e4090a083a6382d 注意，是两个等号，不是一个等号，这一点会经常被忽略。\n接下来再在084这台机器上启动一个redis 容器。\n1 rio@085:~$ sudo docker -H 192.168.1.83:2376 run --name redis_2 -d -e constraint:label_name==084 redis 4968d617d9cd122fc2e17b3bad2f2c3b5812c0f6f51898024a96c4839fa000e1 然后再在083这台机器上启动另外一个 redis 容器。\n1 rio@085:~$ sudo docker -H 192.168.1.83:2376 run --name redis_3 -d -e constraint:label_name==083 redis 7786300b8d2232c2335ac6161c715de23f9179d30eb5c7e9c4f920a4f1d39570 现在来看下执行情况：\n1 2 3 4 5 rio@085:~$ sudo docker -H 192.168.1.83:2376 ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7786300b8d22 redis:latest \u0026#34;/entrypoint.sh redi 15 minutes ago Up 53 seconds 6379/tcp 083/redis_3 4968d617d9cd redis:latest \u0026#34;/entrypoint.sh redi 16 minutes ago Up 2 minutes 6379/tcp 084/redis_2 fee1b7b9dde1 redis:latest \u0026#34;/entrypoint.sh redi 19 minutes ago Up 5 minutes 6379/tcp 084/redis_1 可以看到，执行结果跟预期的一样。\n但是如果指定一个不存在的标签的话来运行容器会报错。\n1 2 rio@085:~$ sudo docker -H 192.168.1.83:2376 run --name redis_0 -d -e constraint:label_name==0 redis FATA[0000] Error response from daemon: unable to find a node that satisfies label_name==0 Affinity Filter 通过使用 Affinity Filter，可以让一个容器紧挨着另一个容器启动，也就是说让两个容器在同一个节点上面启动。\n现在其中一台机器上面启动一个 redis 容器。\n1 2 3 4 5 rio@085:~$ sudo docker -H 192.168.1.83:2376 run -d --name redis redis ea13eddf667992c5d8296557d3c282dd8484bd262c81e2b5af061cdd6c82158d rio@085:~$ sudo docker -H 192.168.1.83:2376 ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ea13eddf6679 redis:latest /entrypoint.sh redis 24 minutes ago Up Less than a second 6379/tcp 083/redis 然后再次启动两个 redis 容器。\n1 2 3 4 rio@085:~$ sudo docker -H 192.168.1.83:2376 run -d --name redis_1 -e affinity:container==redis redis bac50c2e955211047a745008fd1086eaa16d7ae4f33c192f50412e8dcd0a14cd rio@085:~$ sudo docker -H 192.168.1.83:2376 run -d --name redis_1 -e affinity:container==redis redis bac50c2e955211047a745008fd1086eaa16d7ae4f33c192f50412e8dcd0a14cd 现在来查看下运行结果,可以看到三个容器都是在一台机器上运行\n1 2 3 4 5 rio@085:~$ sudo docker -H 192.168.1.83:2376 ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 449ed25ad239 redis:latest /entrypoint.sh redis 24 minutes ago Up Less than a second 6379/tcp 083/redis_2 bac50c2e9552 redis:latest /entrypoint.sh redis 25 minutes ago Up 10 seconds 6379/tcp 083/redis_1 ea13eddf6679 redis:latest /entrypoint.sh redis 28 minutes ago Up 3 minutes 6379/tcp 083/redis 通过 -e affinity:image=image_name 命令可以指定只有已经下载了image_name镜像的机器才运行容器\n1 sudo docker –H 192.168.1.83:2376 run –name redis1 –d –e affinity:image==redis redis redis1 这个容器只会在已经下载了 redis 镜像的节点上运行。\n1 sudo docker -H 192.168.1.83:2376 run -d --name redis -e affinity:image==~redis redis 这条命令达到的效果是：在有 redis 镜像的节点上面启动一个名字叫做 redis 的容器，如果每个节点上面都没有 redis 容器，就按照默认的策略启动 redis 容器。\nPort Filter Port 也会被认为是一个唯一的资源\n1 sudo docker -H 192.168.1.83:2376 run -d -p 80:80 nginx 执行完这条命令，之后任何使用 80 端口的容器都是启动失败。\n实战Redis shell脚本\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #创建网卡 #docker network create redis --subnet 172.38.0.0/16 #通过脚本创建六个redis配置 #for port in $(seq 1 6); \\ do \\ mkdir -p /mydata/redis/node-${port}/conf touch /mydata/redis/node-${port}/conf/redis.conf cat \u0026lt;\u0026lt; EOF \u0026gt;\u0026gt;/mydata/redis/node-${port}/conf/redis.conf port 6379 bind 0.0.0.0 Cluster-enabled yes Cluster-config-file nodes.conf Cluster-node-timeout 5000 Cluster-announce-ip 172.38.0.1${port} Cluster-announce-port 6379 Cluster-announce-bus-port 16379 appendonly yes EOF done #docker run -p 637${port}:6379 -p 1637${port}:16379 --name redis -${port} \\ -v /mydata/redis/node-${port}/data:/data \\ -v /mydata/redis/node-${port}/conf/redis.conf:/etc/redis/redis.conf \\ -d --net redis --ip 172.38.0.1${port} redis:5.0.9-alpine3.11 redis-server /etc/redis/redis. conf; \\ 第一个，在创建5个(6处更改) #docker run -p 6371:6379 -p 16371:16379 --name redis-1 \\ -v /mydata/redis/node-1/data:/data \\ -v /mydata/redis/node-1/conf/redis.conf:/etc/redis/redis.conf \\ -d --net redis --ip 172.38.0.11 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf #创建集群 #redis-cli --cluster create 172.38.0.11:6379 172.38.0.12:6379 172.38.0.13:6379 172.38.0.14:6379 172.38.0.15:6379 172.38.0.16:6379 --cluster-replicas 1 #redis-cli -c #cluster info #cluster nodes #get a b docker 搭建redis集群完成! 构建持续集成环境 docker + jenkins + git + maven****自动化构建与部署\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 (1)安装本次部署需要的软件 1.清除防火墙规则和配置yum.repo [root@docke-all yum.repos.d]# iptables -F [root@docke-all yum.repos.d]# iptables -X [root@docke-all yum.repos.d]# iptables -Z [root@docke-all yum.repos.d]# /usr/sbin/iptables-save \\# Generated by iptables-save v1.4.21 on Thu Jan 19 05:46:41 2017 *filter :INPUT ACCEPT [4:280] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [3:308] COMMIT \\# Completed on Thu Jan 19 05:46:41 2017 添加yum文件 [root@docke-all yum.repos.d]# vi /etc/yum.repos.d/docker.repo [dockerrepo] name=Docker Repository baseurl=https://yum.dockerproject.org/repo/main/centos/$releasever/ enabled=1 gpgcheck=1 gpgkey=https://yum.dockerproject.org/gpg \u0026#34;docker.repo\u0026#34; [New] 12L, 172C written 2．安装最新的docker软件包 [root@docke-all yum.repos.d]# sudo yum install docker-engine Loaded plugins: fastestmirror dockerrepo | 2.9 kB 00:00:00 dockerrepo/7/primary_db | 28 kB 00:00:00 Installed: docker-engine.x86_64 0:1.13.0-1.el7.centos Dependency Installed: docker-engine-selinux.noarch 0:1.13.0-1.el7.centos libseccomp.x86_64 0:2.3.1-2.el7 libtool-ltdl.x86_64 0:2.4.2-21.el7_2 Complete! 3. docker 分别pull 以下镜像 jenkins:2.0-beta-1 tomcat和mysql [root@docke-all yum.repos.d]# docker pull jenkins:2.0-beta-1 2.0-beta-1: Pulling from library/jenkins efd26ecc9548: Pull complete a3ed95caeb02: Pull complete bf22465a61c2: Pull complete 。。。。。。 Digest: sha256:e315b7abd7dd86dca7e5307e1deb040b4054daeaf8d9de28749a88cccc9b960f Status: Downloaded newer image for jenkins:2.0-beta-1 [root@docke-all yum.repos.d]# docker pull tomcat Using default tag: latest latest: Pulling from library/tomcat 5040bd298390: Pull complete 1638c7ffa55a: Pull complete Digest: sha256:c9c5e4d114cf547886a0c0956eebebcfc1d87216f09120e6b77df27f7c8053b0 Status: Downloaded newer image for tomcat:latest [root@docke-all yum.repos.d]# docker pull mysql Using default tag: latest latest: Pulling from library/mysql 5040bd298390: Already exists 55370df68315: Pull complete fad5195d69cc: Pull complete Digest: sha256:d433b96443b9584cdfbcc337c7ebd7cef71332c5368aba19f94177953b1fe0f6 Status: Downloaded newer image for mysql:latest 4.下载apache-maven和jdk并解压到指定文件夹下 maven 并解压： [root@docke-all ~]#mkdir -p /dockerworkspace/maven \u0026amp;\u0026amp; tar zxf apache-maven-3.3.9-bin.tar.gz -C /dockerworkspace/maven 下载jdk 并解压： [root@docke-all ~]#tar -zxvf jdk*.tar.gz -C /dockerworkspace/java/ [root@docke-all ~]# mv /dockerworkspace/java/jdk* /dockerworkspace/java/jdk 5.安装jenkins [root@docke-all dockerworkspace]# docker run -it --name jenkins -p 8080:8080 -p 50000:50000 -v /dockerworkspace/jenkins:/var/jenkins_home -v /dockerworkspace/maven/apache-maven-3.3.9:/usr/local/maven -v /dockerworkspace/java/jdk:/usr/local/jdk jenkins:latest touch: cannot touch ‘/var/jenkins_home/copy_reference_file.log’: Permission denied Can not write to /var/jenkins_home/copy_reference_file.log. Wrong volume permissions? 至此出现一个错误，是因为文件权限问题导致的，具体解释如下： 我们检查一下之前启动方式的\u0026#34;/var/jenkins_home\u0026#34;目录权限，查看Jenkins容器的当前用户: 当前用户是\u0026#34;jenkins\u0026#34;而且\u0026#34;/var/jenkins_home\u0026#34;目录是属于jenkins用户拥有的；而当映射本地数据卷时，/var/jenkins_home目录的拥有者变成了root用户。发现问题之后，相应的解决方法也很简单：把当前目录的拥有者赋值给uid 1000，再启动\u0026#34;jenkins\u0026#34;容器就一切正常了。 [root@docke-all dockerworkspace]# sudo chown -R 1000 jenkins [root@docke-all dockerworkspace]# docker start jenkins Jenkins 访问jenkins:8080(jenkins改成服务器地址) ,然后输入以下查看到的密码Unlock Jenkins [root@docke-all dockerworkspace]# cat /dockerworkspace/jenkins/secrets/initialAdminPassword 554cc3aee41841a49213e6da8507f087 Customize Jenkins步骤时选择左边的Install suggested plugins\nInstall suggested plugins安装\n插件安装完成后界面\n配置Jenkins，点击“可选插件”，安装ssh插件。\n在过滤栏搜索：ssh plugin\nJDK配置和Maven配置：\n选择JDK和Maven配置路径\n至此持续集成环境部署完成，就可以进行项目部署了。\n新建测试项目\n本教程中git管理端用的是gogs，创建jenkins中的maven 项目\n在jenkins首页点击新建选择构建一个maven项目 填写项目名称test_tomcat\n(如果没有这项请安装插件Maven Integration plugin)\n源码管理中选择git, 填写项目的git信息(Credentials点新增可以添加git的认证信息,支持帐户名密码,ssh key 等方式. 源码浏览器在构建项目时的修改记录可以直接连接到git平台)\n","permalink":"https://ktzxy.top/posts/q723p7r0gr/","summary":"Docker学习","title":"Docker学习"},{"content":" Prometheus监控Minio集群 一、概述 Minio支持集成prometheus，用以监控CPU、硬盘、网络等数据。\n二、修改docker-compose.yaml 官方的给docker-compose.yaml，默认是不能访问metric数据的。\n这里配置用是\u0026quot;public\u0026quot;类型，无身份认证。\n需要在docker-compose.yaml中，增加一个环境变量即可。\n1 MINIO_PROMETHEUS_AUTH_TYPE: public 还没有配置主机目录映射，因此，这里就一并修改了，完整内容如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 version: \u0026#39;3.7\u0026#39; # starts 4 docker containers running minio server instances. Each # minio server\u0026#39;s web interface will be accessible on the host at port # 9001 through 9004. services: minio1: image: minio/minio:RELEASE.2020-07-02T00-15-09Z volumes: - /data/minio-cluster/minio1/data1:/data1 - /data/minio-cluster/minio1/data2:/data2 ports: - \u0026#34;9001:9000\u0026#34; environment: MINIO_ACCESS_KEY: minio MINIO_SECRET_KEY: minio123 MINIO_PROMETHEUS_AUTH_TYPE: public command: server http://minio{1...4}/data{1...2} healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost:9000/minio/health/live\u0026#34;] interval: 30s timeout: 20s retries: 3 minio2: image: minio/minio:RELEASE.2020-07-02T00-15-09Z volumes: - /data/minio-cluster/minio2/data1:/data1 - /data/minio-cluster/minio2/data2:/data2 ports: - \u0026#34;9002:9000\u0026#34; environment: MINIO_ACCESS_KEY: minio MINIO_SECRET_KEY: minio123 MINIO_PROMETHEUS_AUTH_TYPE: public command: server http://minio{1...4}/data{1...2} healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost:9000/minio/health/live\u0026#34;] interval: 30s timeout: 20s retries: 3 minio3: image: minio/minio:RELEASE.2020-07-02T00-15-09Z volumes: - /data/minio-cluster/minio3/data1:/data1 - /data/minio-cluster/minio3/data2:/data2 ports: - \u0026#34;9003:9000\u0026#34; environment: MINIO_ACCESS_KEY: minio MINIO_SECRET_KEY: minio123 MINIO_PROMETHEUS_AUTH_TYPE: public command: server http://minio{1...4}/data{1...2} healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost:9000/minio/health/live\u0026#34;] interval: 30s timeout: 20s retries: 3 minio4: image: minio/minio:RELEASE.2020-07-02T00-15-09Z volumes: - /data/minio-cluster/minio4/data1:/data1 - /data/minio-cluster/minio4/data2:/data2 ports: - \u0026#34;9004:9000\u0026#34; environment: MINIO_ACCESS_KEY: minio MINIO_SECRET_KEY: minio123 MINIO_PROMETHEUS_AUTH_TYPE: public command: server http://minio{1...4}/data{1...2} healthcheck: test: [\u0026#34;CMD\u0026#34;, \u0026#34;curl\u0026#34;, \u0026#34;-f\u0026#34;, \u0026#34;http://localhost:9000/minio/health/live\u0026#34;] interval: 30s timeout: 20s retries: 3 创建主机目录\n1 mkdir -p /data/minio-cluster/minio{1,2,3,4}/data{1,2} 启动docker-compose\n1 docker-compose up -d 三、访问metric 1 http://192.168.31.34:9001/minio/prometheus/metrics 效果如下：\n将端口改为9002~9004，也是同样的结果。\n四、Prometheus配置 修改prometheus.yml，增加job_name\n1 2 3 4 5 6 - job_name: minio metrics_path: /minio/prometheus/metrics scrape_interval: 10s scheme: http static_configs: - targets: [\u0026#39;192.168.31.34:9001\u0026#39;,\u0026#39;192.168.31.34:9002\u0026#39;,\u0026#39;192.168.31.34:9003\u0026#39;,\u0026#39;192.168.31.34:9004\u0026#39;] 修改完成后，重启prometheus\n访问targets，确保都是UP状态\n五、Grafana导入模板 模板选择 推荐使用模板：https://grafana.com/grafana/dashboards/12063\n这个模板执行选择Minio节点，而且还是中文显示的。\n导入模板后，效果如下：\n但是发现关于s3相关图表，数据是空的。\n需要修改图表中的metrics计算公式才行。\n先来看S3接口总请求，对应的metrics计算公式为：\n1 sum(s3_requests_total{instance=\u0026#34;172.16.62.150:9000\u0026#34;,job=\u0026#34;minio-metrics\u0026#34;}) by (api) 它需要key为s3_requests_total的值。\n我们再去这几个metrics中去查找\n1 2 3 4 http://192.168.31.34:9001/minio/prometheus/metrics http://192.168.31.34:9002/minio/prometheus/metrics http://192.168.31.34:9003/minio/prometheus/metrics http://192.168.31.34:9004/minio/prometheus/metrics 发现只有第一个才有s3_requests_total的值。\n因此metrics的计算公式为：\n1 sum(s3_requests_total{api=\u0026#34;listobjectsv1\u0026#34;}) by (api) 修改完成之后，图表数据就有了。\n附上其他图表的正确计算公式：\n1 2 3 4 5 6 7 8 9 10 S3接口当前总请求数 sum(s3_requests_current{api=\u0026#34;listobjectsv1\u0026#34;}) by (api) S3接口总错误请求数 sum(s3_errors_total{api=\u0026#34;listobjectsv1\u0026#34;,job=\u0026#34;$job\u0026#34;}) by (api) sum(s3_requests_current{api=\u0026#34;listobjectsv1\u0026#34;,job=\u0026#34;$job\u0026#34;}) by (api) S3接口延迟统计 s3_ttfb_seconds_sum{api=\u0026#34;listobjectsv1\u0026#34;} 对于S3接口总错误请求数，需要修改一下Legend，做一下标识区分。\n最终总体效果如下：\n本文参考链接：\nhttps://www.cnblogs.com/rongfengliang/p/12017914.html\nhttps://blog.csdn.net/kuang1144/article/details/105302960\n","permalink":"https://ktzxy.top/posts/gvb7krqilo/","summary":"Prometheus监控Minio集群","title":"Prometheus监控Minio集群"},{"content":"1. 常用DOS命令 d: 回车\t盘符切换 dir(directory):列出当前目录下的文件以及文件夹 cd (change directory)改变指定目录(进入指定目录) 进入\tcd 目录；cd 多级目录\\多级目录2 ​\t回退\tcd.. ；cd\\ cls : (clear screen)清屏 exit : 退出dos命令行 ipconfig ：查询IP的命令 ipconfig /release ：释放本机现有IP ipconfig /renew ：向DHCP服务器（可以简单理解成你家的路由器）重新申领一个IP ipconfig /all ：显示完整版IP信息 telnet ：测试映射端口或远程访问主机 telnet towel.blinkenlights.nl：播放ASCII版《星球大战》 注：这项功能需要telnet支持，telnet不是Windows的默认内置组件，因此当你看到错误提示时，需要首先进入“设置” \u0026ndash;\u0026gt; “应用” \u0026ndash;\u0026gt; “程序和功能” \u0026ndash;\u0026gt; “启用或关闭Windows功能”手工安装它（Telnet Client）\nmsg ：向对方电脑发送一条文本提示 msg /server:对方电脑IP * 对方电脑屏幕要弹出的文本 net user ：查看本机账户情况 衍生的命令后缀，比方说“net user xxx 123456 /add”，输入后就会在系统中创建一个名为“xxx”的新用户，而新用户密码则是“123456”。类似的还有“net user xxx /del”（删除xxx用户）、“net user xxx /active:no”（禁用xxx用户）、“net user xxx”（查看xxx用户的详细情况）等 net share ：查看共享资源 net share 要共享的文件夹 ：指定共享文件 net share 要删除的共享文件夹 /delete ：删除共享文件 net start 服务 开启服务 net stop 服务 停止服务 nslookupn ：检查网站IP地址 nslookup 对方网站域名 netsh wlan show ：探秘Wi-Fi配置文件 netsh wlan show profile SSID key=clear，输入完成后Windows会自动返回当前已连接WIFI的详细信息，包括SSID和连接密码。当前这里有一个前提，那就是你现在已经成功连接了。 netsh dump \u0026gt; 路径 备份网络配置 set address name = \u0026ldquo;本地连接\u0026rdquo; source = static addr = 192.168.0.7 mask = 255.255.255.0 设置静态ip set address name = \u0026ldquo;本地连接\u0026rdquo; source = dhcp 设置自动获取ip set address name = \u0026ldquo;本地连接\u0026rdquo; gateway = 172.19.96.1 gwmetric = 1 设置其他（网关，DNS等） color ：更改CMD文字颜色 | ：将命令结果输出到剪贴板 具体命令是，在需要导出结果的命令后方添加“|”，再加入导出位置就可以了。比方说“| clip”是导出到剪贴板，“| xxx.txt”是导出到xxx.txt。 \u0026amp;\u0026amp; ：将多个命令“连接”起来，一步运行多组命令 \u0026amp; : 当第一个命令执行失败了，后边的命令继续执行 || 当一条命令失败后才执行第二条命令 shutdown /r -t 120 120秒关机重启 shutdown /? sfc /scannow 扫描系统并修复 for /r D:\\ %%i in (*.txt) do echo %%i 查找D盘下所有txt后缀的文件路径 1.1. windows 常用命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # 查询端口 netstat -ano # 查询指定端口 netstat -ano |findstr \u0026#34;端口号\u0026#34; # 根据进程PID查询进程名称 tasklist |findstr \u0026#34;进程PID号\u0026#34; # 根据PID杀死任务 taskkill /F /PID \u0026#34;进程PID号\u0026#34; # 根据进程名称杀死任务 taskkill -f -t -im \u0026#34;进程名称\u0026#34; convert /? 将FAT卷转换为NTFS dispart 选择磁盘1 select disk 1 格式化磁盘 clean 创建主分区 creat partition primary 定义磁盘 format fs=ntfs quick label= \u0026#34;E:\u0026#34; list disk compmgmt 1.2. 自用的系统脚本 1.2.1. 内外网IP切换（适用win10系统）.20171122 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 @echo off rem //设置变量 set NAME=\u0026#34;以太网\u0026#34; rem //以下属性值可以根据需要更改 set ADDR=192.168.14.73 set MASK=255.255.254.0 set GATEWAY=192.168.14.1 set DNS1=10.17.65.13 set DNS2=10.202.253.28 rem //以上属性依次为IP地址、子网掩码、网关、首选DNS、备用DNS echo 当前可用操作有： echo 1 设置为静态IP echo 2 设置为动态IP echo 3 退出 echo 请选择后回车： set /p operate= if %operate%==1 goto 1 if %operate%==2 goto 2 if %operate%==3 goto 3 :1 echo 正在设置静态IP，请稍等... rem //可以根据你的需要更改 echo IP地址 = %ADDR% echo 掩码 = %MASK% echo 网关 = %GATEWAY% netsh interface ipv4 set address %NAME% static %ADDR% %MASK% %GATEWAY% echo 首选DNS = %DNS1% netsh interface ipv4 set dns %NAME% static %DNS1% echo 备用DNS = %DNS2% if \u0026#34;%DNS2%\u0026#34;==\u0026#34;\u0026#34; (echo DNS2为空) else (netsh interface ipv4 add dns %NAME% %DNS2%) echo 静态IP已设置！ pause goto 3 :2 echo 正在设置动态IP，请稍等... echo 正在从DHCP自动获取IP地址... netsh interface ip set address %NAME% dhcp echo 正在从DHCP自动获取DNS地址... netsh interface ip set dns %NAME% dhcp echo 动态IP已设置！ pause goto 3 :3 exit 1.2.2. 内外网IP切换（适用win7系统） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @echo off rem //设置变量 set NAME=\u0026#34;本地连接\u0026#34; rem //以下属性值可以根据需要更改 set ADDR=192.168.14.73 set MASK=255.255.254.0 set GATEWAY=192.168.14.1 set DNS1=10.17.65.13 set DNS2=10.202.253.28 rem //以上属性依次为IP地址、子网掩码、网关、首选DNS、备用DNS echo 当前可用操作有： echo 1 设置为静态IP echo 2 设置为动态IP echo 3 退出 echo 请选择后回车： set /p operate= if %operate%==1 goto 1 if %operate%==2 goto 2 if %operate%==3 goto 3 :1 echo 正在设置静态IP,请稍等… rem //可以根据你的需要更改 echo IP地址 = %ADDR% echo 掩码 = %MASK% echo 网关 = %GATEWAY% netsh interface ipv4 set address name=%NAME% source=static addr=%ADDR% mask=%MASK% gateway=%GATEWAY% gwmetric=0 \u0026gt;nul echo 首选DNS = %DNS1% netsh interface ipv4 set dns name=%NAME% source=static addr=%DNS1% register=PRIMARY \u0026gt;nul echo 备用DNS = %DNS2% netsh interface ipv4 add dns name=%NAME% addr=%DNS2% index=2 \u0026gt;nul echo 静态IP已设置! pause goto 3 :2 echo 正在设置动态IP,请稍等… echo 正在从DHCP自动获取IP地址… netsh interface ip set address \u0026#34;本地连接\u0026#34; dhcp echo 正在从DHCP自动获取DNS地址… netsh interface ip set dns \u0026#34;本地连接\u0026#34; dhcp echo 动态IP已设置! pause goto 3 :3 exit 1.2.3. 一键删除电脑中的空文件夹脚本（未测试！！） 在任意目录中创建“xxx.bat”的批处理文件，复制以下脚本代码再双击运行即可。\n批量（循环）删除指定目录下所有空文件夹代码，例如删除F:\\盘下的所有空文件夹： 1 2 3 4 5 6 7 @echo off for /f \u0026#34;delims=\u0026#34; %%a in (\u0026#39;dir /ad /b /s F:\\^|sort /r\u0026#39;) do ( rd \u0026#34;%%a\u0026#34;\u0026gt;nul 2\u0026gt;nul \u0026amp;\u0026amp;echo 空目录\u0026#34;%%a\u0026#34;成功删除！ ) pause 批量删除多个磁盘的空文件夹，例如删除c、d、e、f区中所有的空文件夹： 1 2 3 4 5 6 7 8 9 10 11 @echo off for %%i in (c d e f) do ( if exist %%i:\\ ( for /f \u0026#34;delims=\u0026#34; %%a in (\u0026#39;dir /ad /b /s \u0026#34;%%i:\\\u0026#34;^|sort /r\u0026#39;) do ( rd \u0026#34;%%a\u0026#34; ) ) ) pause 1.2.4. 启用/禁用网络本地连接 启用/禁用网络连接脚本，注意：需要使用管理员身份运行脚本。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 @echo off :: BatchGotAdmin :------------------------------------- REM --\u0026gt; Check for permissions \u0026gt;nul 2\u0026gt;\u0026amp;1 \u0026#34;%SYSTEMROOT%\\system32\\cacls.exe\u0026#34; \u0026#34;%SYSTEMROOT%\\system32\\config\\system\u0026#34; REM --\u0026gt; If error flag set, we do not have admin. if \u0026#39;%errorlevel%\u0026#39; NEQ \u0026#39;0\u0026#39; ( echo Requesting administrative privileges... goto UACPrompt ) else ( goto gotAdmin ) :UACPrompt echo Set UAC = CreateObject^(\u0026#34;Shell.Application\u0026#34;^) \u0026gt; \u0026#34;%temp%\\getadmin.vbs\u0026#34; echo UAC.ShellExecute \u0026#34;%~s0\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;runas\u0026#34;, 1 \u0026gt;\u0026gt; \u0026#34;%temp%\\getadmin.vbs\u0026#34; \u0026#34;%temp%\\getadmin.vbs\u0026#34; exit /B :gotAdmin if exist \u0026#34;%temp%\\getadmin.vbs\u0026#34; ( del \u0026#34;%temp%\\getadmin.vbs\u0026#34; ) pushd \u0026#34;%CD%\u0026#34; CD /D \u0026#34;%~dp0\u0026#34; :-------------------------------------- cls @ECHO OFF title 启用或禁用本地连接 CLS color 0a GOTO MENU :MENU ECHO. ECHO. ==============启用禁用本地连接============== ECHO. ECHO. 1 启用本地连接 ECHO. 2 禁用本地连接 ECHO. 3 退 出 ECHO. ========================================== ECHO. ECHO. echo. 请输入选择项目的序号： set /p ID= if \u0026#34;%id%\u0026#34;==\u0026#34;1\u0026#34; goto open if \u0026#34;%id%\u0026#34;==\u0026#34;2\u0026#34; goto close if \u0026#34;%id%\u0026#34;==\u0026#34;3\u0026#34; exit PAUSE :open echo 启用本地连接 netsh interface set interface name=\u0026#34;以太网\u0026#34; admin=ENABLED GOTO MENU :close echo 禁用本地连接 netsh interface set interface name=\u0026#34;以太网\u0026#34; admin=DISABLED goto MENU 1.3. 批处理(bat)脚本命令汇总（待整理） 参考：详细的批处理文件bat脚本命令\n2. 系统运行命令 以下均为运行面板(Win+R)中输入的命令\n2.1. 如何使用 WIN+R 运行自定义命令启动程序 首先在任意盘符下建立一个文件夹，比如在D盘建立名字为shortcut的文件夹\n设置环境变量：选择计算机-\u0026gt;右键选择属性-\u0026gt;选择系统高级设置-\u0026gt;选择“环境变量-\u0026gt;双击path-\u0026gt;添加刚刚建立的文件夹D:\\shortcut(如果有多个则在每个文件夹路径后面加英文状态下的分号;)\n将桌面上所有的快捷方式都剪切到shortcut文件夹即可，以后有快捷方式也直接扔进去\n注意事项: 如果想更加简单的使用 Win+R 打开程序，可以将shortcut下的文件名称更改为自己熟悉的(支持中文哦)\n2.2. window 系统常用原生命令 快捷键 程序 cmd 命令行 regedit 注册表 services.msc 系统服务 control 所有控制面版项 calc 启动计算器 mspaint 画图 notepad 打开记事本 ncpa.cpl 打开网络连接 Shutdown -s -t 600 表示600秒后自动关机 Shutdown -a 可取消定时关机 Shutdown -r -t 600 表示600秒后自动重启 rundll32 user32.dll,LockWorkStation 表示锁定计算机 wt Microsoft.WindowsTerminal（需要手动安装） 3. windows 系统相关设置 3.1. 环境变量 (用户变量与系统变量) 参考资源：http://www.dayanzai.me/environment-variables.html\n环境变量 (environment variables) 是在操作系统中用来指定操作系统运行环境的一些参数。环境变量是在操作系统中一个具有特定名字的对象，它包含了一个或者多个应用程序所将使用到的信息。Windows 和 DOS 操作系统中的 path 环境变量，当要求系统运行一个程序而没有告诉它程序所在的完整路径时，系统除了在当前目录下面寻找此程序外，还应到 path 中指定的路径去找。用户通过设置环境变量，来更好的运行进程。 环境变量可分为用户变量与系统变量两类，在注册表中都有对应的项。\nNotes:\n环境变量不区分大小写 系统变量针对所有用户起作用，为了安全一般配置用户环境变量。 用户变量只对当前用户起作用，不建议为了省事而配置系统环境变量。 用户环境变量优先级高于系统环境变量。对于环境变量，系统会先检查用户变量，之后再检查系统变量。 3.1.1. 用户变量 注册表中用户变量所在位置：HKEY_CURRENT_USER\\Environment\n3.1.2. 系统变量 注册表中系统变量所在位置：HKEY_LOCAL_MACHINE\\SYSTEM\\ControlSet001\\Control\\Session Manager\\Environment\n在原有变量 Path 的基础上添加英文状态下的分号，然后添加路径名。不要删除原先的系统变量，只要用分号隔开，然后添加路径名，最后也要加上分号。\n3.1.3. 常用变量清单 变量名称 值 %ALLUSERSPROFILE% C:\\ProgramData %APPDATA% 列出应用程序数据的默认存放位置。C:\\Users{username}\\AppData\\Roaming %LOCALAPPDATA% C:\\Users{username}\\AppData\\Local %TEMP%或%TMP% C:\\Users{username}\\AppData\\Local\\Temp %COMMONPROGRAMFILES% C:\\Program Files\\Common Files %COMMONPROGRAMFILES(x86)% C:\\Program Files (x86)\\Common Files %CommonProgramW6432% C:\\Program Files\\Common Files %COMSPEC% C:\\Windows\\System32\\cmd.exe %HOMEDRIVE% C:\\ %HOMEPATH% 或 %USERPROFILE% 用户主目录的完整路径（当前用户的配置文件的位置）。C:\\Users{username} %WINDIR% 或 %SYSTEMROOT% 操作系统根目录。C:\\Windows %LOGONSERVER% \\{domain_logon_server} %PATH% C:\\Windows\\system32;C:\\Windows;C:\\Windows\\System32\\Wbem %PATHEXT% .com;.exe;.bat;.cmd;.vbs;.vbe;.js;.jse;.wsf;.wsh;.msc %PROGRAMDATA% C:\\ProgramData %PROGRAMFILES% 或 %ProgramW6432% C:\\Program Files %PROGRAMFILES(X86)% C:\\Program Files (x86) %PROMPT% $P$G %SYSTEMDRIVE% C: %SystemRoot% C:\\Windows %USERDOMAIN% 与当前用户相关的用户域。 %USERDOMAIN_ROAMINGPROFILE% 与漫游配置文件相关的用户域。 %USERNAME% 当前系统用户名称。{username} %PUBLIC% C:\\Users\\Public %PSMODULEPATH% %SystemRoot%\\system32\\WindowsPowerShell\\v1.0\\Modules\\ %ONEDRIVE% C:\\Users{username}\\OneDrive %DriverData% C:\\Windows\\System32\\Drivers\\DriverData %CD% 输出当前目录路径。(命令提示符) %CMDCMDLINE% 输出用于启动当前命令提示符会话的命令行。(命令提示符) %CMDEXTVERSION% 输出当前命令处理器扩展的数量。(命令提示符) %COMPUTERNAME% 输出系统名称。 %DATE% 输出当前日期。(命令提示符) %TIME% 输出时间。(命令提示符) %ERRORLEVEL% 输出上一条命令的定义退出状态的数字。(命令提示符) %PROCESSOR_IDENTIFIER% 输出处理器标识符。 %PROCESSOR_LEVEL% 输出处理器电平。 %PROCESSOR_REVISION% 输出处理器版本。 %NUMBER_OF_PROCESSORS% 输出物理和虚拟内核的数量。 %RANDOM% 输出从 0 到 32767 的随机数。 %OS% Windows_NT 3.2. hosts 文件 window 系统的 hosts 文件位置：%windir%\\System32\\drivers\\etc\n3.3. win10 锁屏壁纸位置 路径：%HOMEPATH%\\AppData\\Local\\Packages\\Microsoft.Windows.ContentDeliveryManager_cw5n1h2txyewy\\LocalState\\Assets\n3.4. C盘可清理内容 PerfLogs文件夹，系统的信息日志，文件夹可删。 Windows文件夹 C:\\Windows\\WinSxS，装载了电脑从新装到现在的所有补丁文件，不能删除。但里面有一个“backup”备份文件夹，是可删的。 C:\\Windows\\Help，帮忙文件，可删 用户文件夹：C:\\Users\\用户名称\\AppData\\Local\\Temp。这个是Windows存留安装软件时解压的源文件，方便下次安装直接调取使用，节省解压时间，可删除。 3.5. win7 系统的Temporary Internet Files清空问题 cmd.exe cd AppData\\Local\\Microsoft\\Windows\\Temporary Internet Files（或者如果有Content.IE5目录的话，cd Content.IE5） del /s/q/f *.* 3.6. 备份开始菜单 按下Win+R打开运行窗口，输入命令powershell，然后点击确定按钮 这时就会打开Windows Powershell窗口，在这里输入命令Export-startlayout –path E:\\start.xml，可以根据自己实际情况来设置相应的路径 按下回车键后，就会备份好开始菜单的布局文件 如果需要恢复开始菜单布局的话，只需要再次打开Windows Powershell命令行窗口，然后输入命令import-startlayout -layoutpath E:\\start.xml -mountpath c:，按下回车键后，就会马上把其还原回来了 3.7. 电脑护眼颜色设置 win7系统：\n桌面-\u0026gt;右键-\u0026gt;属性-\u0026gt;外观-\u0026gt;高级-\u0026gt;项目选择（窗口） 颜色1（L）选择（其它）将色调改为：85。饱和度：123。亮度：205-\u0026gt;添加到自定义颜色-\u0026gt;在自定义颜色选定点确定-\u0026gt;确定 另一种相近的颜色设置：R:204 G:232 B:207 win10系统：\nwindows+R键调出运行窗口（或者鼠标右击开始键，选择运行），在运行窗口中输入regedit调出注册表编辑器 按照如下顺序找到windows：[HKEY_CURRENT_USER\\Control Panel\\Colors] windows。双击windows 进入编辑状态 将原本数值删除并输入：202 234 206。点击确定退出注册表。 按照如下顺序找到 window：[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Themes\\DefaultColors\\Standard]。双击 window 打开编辑窗口，默认是勾选十六进制（若不是请勾选十六进制），将原始数据改为：caeace。点击确定退出注册表。 3.8. 这个可能与ACHI有关系吧。你先去修改到 compatible（兼容模式）进入系统 AHCI开启方法：\n依次展开：“开始” -\u0026gt; “运行”（或使用Win+R) -\u0026gt; 键入“regedit” -\u0026gt; “确定”后 -\u0026gt; 启动注册表编辑器 -\u0026gt; 展开到[HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\services\\msahci]分支。 在右侧双击“Start” -\u0026gt; “编辑DWORD值” -\u0026gt; 将“数值数据”的键值由“3”改为“0” -\u0026gt; 单击“确定”。 关闭“注册表编辑器”窗口，并重新启动电脑。 然后出来看看BIOS里面的硬盘模式，修改为ACHI后（如果没有就算了） 然后在把SATA Operation Mode改为 enhanced（增强模式） 3.9. NSIS：使用netsh advfirewall屏蔽某程序访问网络 关闭防火墙 1 nsExec::Exec \u0026#39;cmd /c netsh advfirewall set allprofiles state off\u0026#39; 开启防火墙 1 nsExec::Exec \u0026#39;cmd /c netsh advfirewall set allprofiles state on\u0026#39; 删除屏蔽 1 nsExec::Exec \u0026#39;cmd /c netsh advfirewall firewall Delete rule name=\u0026#34;TIM\u0026#34;\u0026#39; 添加屏蔽 1 nsExec::Exec \u0026#39;cmd /c netsh advfirewall firewall add rule name=\u0026#34;TIM\u0026#34; dir=out action=block program=\u0026#34;C:\\Program Files\\TIM Lite\\Bin\\TIM.exe\u0026#34;\u0026#39; 3.10. 删掉 WIN10 回收站右键菜单的固定到＂开始＂屏幕！ 删除：打开注册表，定位到 HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes\\Folder\\shellex\\ContextMenuHandlers，删除其子键 PintoStartScreen 恢复：在 HKEY_LOCAL_MACHINE\\SOFTWARE\\Classes\\Folder\\shellex\\ContextMenuHandlers 上单击右键，新建项 PintoStartScreen，修改其默认值为 {470C0EBD-5D73-4d58-9CED-E91E22E23282} 3.11. 限制保留宽带设置 按“WIN+R”，打开【运行】对话框； 输入“regedit”，回车，打开注册表编辑器； 依次展开“HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RunMRU” 按“WIN+R”，打开【运行】对话框，输入gpedit.msc 计算机配置－管理模板－网络－qos数据包计划程序－限制保留宽带 选择已启用。一般默认是20，直接把它改成0。 3.12. win10 系统任务栏设置时间显示秒 按“WIN+R”，打开【运行】对话框； 输入“regedit”，回车，打开注册表编辑器； 在注册表中定位到以下子健：HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced 后在Advanced上鼠标右键点击呼出菜单，选择 -\u0026gt; 新建（N） -\u0026gt; DWORD(32位)值。也可以左键点击Advanced，在右边区域点击空白处点击鼠标右键呼出菜单选择 -\u0026gt; 新建（N） -\u0026gt; DWORD(32位)值。 将新建 DWORD(32位)值，命名为 ShowSecondsInSystemClock，双击打开将数值数据改为1，并点击确定，关闭注册表。 如果想恢复不显示秒，则将创建的ShowSecondsInSystemClock删除即可\nNotes: 微软承认 win 11 系统中，删除了注册表值“ShowSecondsInSystemClock”，该值允许任务栏时钟以秒为单位显示时间。如果时间需要显示秒，需要安装第三方软件\n3.13. Win10系统删除无用的服务 运行 -\u0026gt; regedit，打开注册表编辑器 定位到【计算机\\HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services】，选择服务名称，右键删除即可 3.14. 修改 window 默认系统安装目录 Windows10 系统更改软件程序默认安装目录的方法\n运行 -\u0026gt; regedit，打开注册表编辑器 进入注册表HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion目录下，并左键单击：CurrentVersion； 在CurrentVersion对应的右侧窗口，找到ProgramFilesDir，并左键双击ProgramFilesDir打开编辑字符串对话框，把Program Files的数值数据从C:\\Program Files更改为D:\\Program Files，再点击：确定； 如果安装的是Windows10的64位系统，在CurrentVersion对应的右侧窗口，找到ProgramFilesDir（x86），并左键双击ProgramFilesDir（x86）打开编辑字符串对话框，把Program Files（x86）的数值数据从C:\\Program Files（x86）更改为D:\\Program Files（x86），再点击：确定； 修改系统存储的保存位置\n左键点击系统桌面左下角的“开始”，在开始菜单中点击：设置 在打开的设置窗口，点击：系统 \u0026ndash;\u0026gt; 窗口左侧的“存储” 在存储对应的右侧窗口，用鼠标左键按住右侧的滑块向下拖动，找到：保存位置，在保存位置下，点击：新的应用将保存到此电脑（C:）后面的小勾 修改成D盘。之后打开磁盘(D:)，可以看到磁盘(D:)中新增了三个文件夹：MoonZero（用户文件：文档、音乐、图片和视频）、Program Files（程序文件）和Windows Apps（窗口应用程序）； 3.15. win10 一般禁用的服务 运行输入【services.msc】打开服务面板，禁用以下服务 Connected User Experiences and Telemetry Diagnostic Execution Service Diagnostic Policy Service Diagnostic Service Host Diagnostic System Host SysMain（以前的 Windows Superfetch 感觉 SSD 上效果不大，不想禁用的可以改为“手动启动”） Windows Search （关联了 Win10 里的很多新功能，而且对于 SSD 影响也不大，可以不禁用） 右击“此电脑” -\u0026gt; “属性” -\u0026gt; “高级系统设置” -\u0026gt; “高级” -\u0026gt; “性能” 点击“设置” -\u0026gt; “更新与安全” -\u0026gt; “Windows预览体验计划”，退出 Windows Insider 计划。 右击任务栏空白处选择“任务管理器”，切换到“启动”标签，将没必要的自启动程序全部禁用。 3.16. 修复 win10 右键无新建 txt 文本文件 1 2 3 4 5 6 7 8 9 10 11 Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\\.txt] @=\u0026#34;txtfile\u0026#34; \u0026#34;Content Type\u0026#34;=\u0026#34;text/plain\u0026#34; [HKEY_CLASSES_ROOT\\.txt\\ShellNew] \u0026#34;NullFile\u0026#34;=\u0026#34;\u0026#34; [HKEY_CLASSES_ROOT\\txtfile] @=\u0026#34;文本文档\u0026#34; [HKEY_CLASSES_ROOT\\txtfile\\shell] [HKEY_CLASSES_ROOT\\txtfile\\shell\\open] [HKEY_CLASSES_ROOT\\txtfile\\shell\\open\\command] @=\u0026#34;NOTEPAD.EXE %1\u0026#34; 打开记事本，复制以上内容，另存为xxx.reg。点击文件，确认操作后，重启电脑生效\n3.17. 关闭cmd命令行窗口的中文输入法 运行regedit命令，打开注册表窗口，修改注册表：HKEY_CURRENT_USER\\Console\\LoadConIme 的键值由1改为0\n3.18. 修改cmd/powershell命令行窗口默认编码 临时修改\n使用chcp命令可以输出当前编码的数值，如：GBK是936，UTF-8是65001 修改注册表\n修改powershell默认编码：运行regedit命令打开注册表，展开注册表计算机\\HKEY_CURRENT_USER\\Console项。选择powershell，点击修改右边窗口中CodePage项，选择十进制，修改值为65001。修改后就每次启动都默认改成UTF-8的编码 修改cmd编码：运行regedit命令打开注册表，展开注册表计算机\\HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Command Processor项。如果右边窗口没有autorun字符串值，则右键新建字符串值，数值名称：autorun，数值数据：chcp 65001。修改后就每次启动都默认改成UTF-8的编码 3.19. 彻底关闭Cortana小娜 关闭Cortana小娜的权限 Win10的设置菜单 -\u0026gt; \u0026ldquo;应用\u0026rdquo; -\u0026gt; 在应用列表中搜索找到Cortana -\u0026gt; 高级选项 -\u0026gt; 可以将Cortana小娜的麦克风、后台以及开机启动的权限全部关闭\n彻底关闭Cortana小娜 运行regedit进入注册表 -\u0026gt; 计算机\\HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows -\u0026gt; 用右键点击“Windows”目录，选择“新建”，新建一个“项”。将这个项命名为“Windows Search” -\u0026gt; 右键点击“Windows Search”，新建一个“DWORD(32位)值” -\u0026gt; 将这个值命名为“AllowCortana”，然后双击这个值，确认它的数值为“0”，然后按下确定保存 -\u0026gt; 之后，Cortana就会被禁用了。这时候再打开Cortana，就会看到禁用的提示\n完全删除Cortana小娜 以管理员模式运行Powershell -\u0026gt; 运行以下代码删除\n1 Get-AppxPackage -allusers Microsoft.549981C3F5F10 | Remove-AppxPackage 3.20. 关闭 Win11/ Win 10 内存压缩 Win11默认开启了内存压缩功能。可以压缩内存中的数据，让内存占用更少，同时减少Swap频次，带来更高的I/O效率。但CPU性能较弱的设备，例如轻薄本，开启内存压缩可能会造成卡顿缓慢。同时，内存压缩需要消耗额外的CPU资源，带来更多耗电发热，这对注重续航的设备来说也是不合适的。\n通过任务管理器查看内存压缩的开启状态。如果开启了内存压缩，那么在任务管理器中，就会显示压缩内存的数据 通过命令行查看内存压缩的开启状态。使用系统管理员权限，打开PowerShell，然后输入命令 Get-MMAgent 后。如果看到“MemoryCompression”这一项是“Ture”，那么说明内存压缩已经开启。 关闭内存压缩。使用系统管理员权限，打开PowerShell，然后输入命令 Disable-MMAgent -mc 后，重启系统，内存压缩就关闭了。 重新打开内存压缩。使用系统管理员权限，打开PowerShell，然后输入命令 Enable-MMAgent -mc 后，重启系统，内存压缩就重新开启。 3.21. 清除电脑的运行记录 win+R 打开运行窗口，输入 regedit 打开注册表编辑器 展开 HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\RunMRU在右侧除了默认 将其他选项都删除掉 3.22. 删除资源管理器中“此电脑”下面多余的图标 WIN+R 打开运行窗口，输入 regedit 打开注册表编辑器 在注册表中定位到：HKEY_CURRENT_USER\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MyComputer\\NameSpace 项 选中“NameSpace”后，在右键窗口中删除相应的键值 退出注册表后，此电脑中多余图标消失 也可以保存以下语句为*.reg文件，运行即可移除。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Windows Registry Editor Version 5.00 ;如需还原去除上语句前减号即可 ;取消我的电脑\u0026#34;视频\u0026#34;文件夹 [-HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MyComputer\\NameSpace\\{f86fa3ab-70d2-4fc7-9c99-fcbf05467f3a}] ;取消我的电脑\u0026#34;文档\u0026#34;文件夹 [-HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MyComputer\\NameSpace\\{d3162b92-9365-467a-956b-92703aca08af}] ;取消我的电脑\u0026#34;桌面\u0026#34;文件夹 [-HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MyComputer\\NameSpace\\{B4BFCC3A-DB2C-424C-B029-7FE99A87C641}] ;取消我的电脑\u0026#34;音乐\u0026#34;文件夹 [-HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MyComputer\\NameSpace\\{3dfdf296-dbec-4fb4-81d1-6a3438bcf4de}] ;取消我的电脑\u0026#34;下载\u0026#34;文件夹 [-HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MyComputer\\NameSpace\\{088e3905-0323-4b02-9826-5d99428e115f}] ;取消我的电脑\u0026#34;图片\u0026#34;文件夹 [-HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MyComputer\\NameSpace\\{24ad3ad4-a569-4530-98e1-ab02f9417aa8}] ;取消我的电脑\u0026#34;3D对象\u0026#34;文件夹 [-HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MyComputer\\NameSpace\\{0DB7E03F-FC29-4DC6-9020-FF41B59E513A}] 4. Windows 11 系统配置 4.1. 取消显示快速访问中“文档、视频\u0026hellip;”等图标 使用快捷键 win+R 打开运行命令窗口，输入regedit命令打开注册表。在地址栏定位到以下地址：\n1 计算机\\HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FolderDescriptions 找到文件相应的代码字符串，展开并选择【PropertyBag】，选择右侧窗口中的【ThisPCPolicy】鼠标右键点击修改，将值修改为Hide。注意：首字母H必须大写\n图片：{0ddd015d-b06c-45d5-8c4c-f59713854639} 视频：{35286a68-3c57-41a1-bbb1-0eae73d76c95} 下载：{7d83ee9b-2244-4e70-b1f5-5393042af1e4} 音乐：{a0c69a99-21c8-4671-8703-7934162fcf1d} 文档：{f42ee2d3-909f-4907-8871-4c22fc0bf756} 4.2. 设置任务栏小图标 使用快捷键 win+R 打开运行命令窗口，输入regedit命令打开注册表。在地址栏定位到以下地址： 1 计算机\\HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced 右键新建【DWORD (32位)值】，命名为 TaskbarSi 修改TaskbarSi数值数据，0表示强制使用小图标；1表示使用中等图标；2表示使用大图标 但目前 win 11 不支持修改小图标的任务栏，修改后时间日期会出现下沉超出屏幕的问题。\n4.3. 开启 Windows 11 隐藏的教育主题 教育主题适用于 Windows 11 家庭版、专业版和企业版。11若要使 Windows 11 教育版主题可用，用户需要执行以下操作：\n按键盘上的 Win+R 打开运行窗口 输入 regedit 按回车打开注册表编辑器 导航到注册表中的相应路径： 1 HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\PolicyManager\\current\\device\\ 右键单击 device 文件夹，然后选择新建 -\u0026gt; 项，命名为：Education 再选择 Education 右键新建 DWORD 值（32 位），命名为：EnableEduThemes 双击 → 将值设置为 1 重新启动计算机。 或者，可以选择创建包含以下内容的文本文件，然后将其重命名为 .reg 后缀文件，并双击导入注册表。\n1 2 3 Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\PolicyManager\\current\\device\\Education] \u0026#34;EnableEduThemes\u0026#34;=dword:00000001 完成上述步骤后，计算机应该在重启后在后台自动下载其他主题。您可能需要等待一段时间，直到此过程完成。安装后，可以通过转到“设置”应用并选择“个性化” -\u0026gt; “主题”来应用新主题。\n5. Windows 11 键盘快捷键终极列表 参考：http://www.dayanzai.me/windows-11-keyboard-shortcuts.html\n5.1. Windows 11 新增快捷键 Microsoft 在 Windows 11 中添加了一些新功能。例如，Snap Layouts。如果将鼠标悬停在最大化按钮（每个窗口右上角关闭十字符号旁边的方块）上，将看到多个网格。可以使用这些网格以想要的方式排列窗口。还有一个访问 Snap Layouts 的键盘快捷键。\n操作 快捷键 打开操作中心 Win + A 打开通知面板（通知中心） Win + N 打开小部件面板 Win + W 快速访问 Snap 布局 Win + Z 打开 Microsoft Teams Win + C 5.2. 文本编辑键盘快捷键 操作 快捷键 剪切所选项目 Ctrl + X 复制所选项目 Ctrl + C 粘贴所选项目 Ctrl + V 加粗所选文本 Ctrl + B 斜体所选文本 Ctrl + I 为所选文本加下划线 Ctrl + U 移动光标到当前行的开头 Home 移动光标到当前行的结束 End 5.3. 通用 Windows 键盘快捷键 操作 快捷键 在打开的应用程序之间切换 Alt + Tab 关闭活动项，或退出活动应用程序 Alt + F4 锁定你的电脑 Win + L 显示和隐藏桌面 Win + D 打开资源管理器 Win + E 搜索 Win + S 多重剪贴板 Win + V 切“桌面” Win + Ctrl + →/← 截图 Win + Shift + S 白板（需要下载白板应用） Win + W 显示日历 Win + Alt + D 投影 Win + P 连智能电视 Win + K 执行该字母的命令 Alt + 带下划线的字母 显示所选项目的属性 Alt + Enter 打开活动窗口的快捷菜单 Alt + Spacebar 转到退回 Alt + 左箭头 转到向前 Alt + 右箭头 向上移动一屏 Alt + Page Up 向下移动一屏 Alt + Page Down 关闭活动文档 Ctrl + F4 选择文档或窗口中的所有项目 Ctrl + A 删除所选项目并将其移至回收站 Ctrl + D 刷新活动窗口 Ctrl + R 重做操作 Ctrl + Y 将光标移动到下一个单词的开头 Ctrl + 右箭头 将光标移动到上一个单词的开头 Ctrl + 左箭头 将光标移动到下一段的开头 Ctrl + 下箭头 将光标移动到上一段的开头 Ctrl + 上箭头 使用箭头键在所有打开的应用程序之间切换 Ctrl + Alt + Tab 当组或磁贴在“开始”菜单上处于焦点时，将其向指定方向移动 Alt + Shift + 箭头键 当一个磁贴在“开始”菜单上处于焦点时，将其移动到另一个磁贴中以创建文件夹 Ctrl + Shift + 箭头键 开始菜单打开时调整大小 Ctrl + 箭头键 在窗口或桌面上选择多个单独的项目 Ctrl + 箭头键 + spacebar 选择一个文本块 Ctrl + Shift 和箭头键 打开启动 Ctrl + Esc 打开任务管理器 Ctrl + Shift + Esc 当多个键盘布局可用时切换键盘布局 Ctrl + Shift 打开或关闭中文输入法编辑器 (IME) Ctrl + Spacebar 显示所选项目的快捷菜单 Shift + F10 删除所选项目而不先将其移动到回收站 Shift + Delete 打开右侧的下一个菜单，或打开一个子菜单 右箭头 打开左侧的下一个菜单，或关闭子菜单 左箭头 停止或离开当前任务 Esc 截取整个屏幕的屏幕截图并将其复制到剪贴板 PrtScn 5.4. 功能键键盘快捷键 操作 快捷键 重命名所选项目 F2 在文件资源管理器中搜索文件或文件夹 F3 在文件资源管理器中显示地址栏列表 F4 刷新活动窗口 F5 在窗口或桌面上循环浏览屏幕元素 F6 激活活动应用程序中的菜单栏 F10 最大化或最小化活动窗口 F11 5.5. 文件资源管理器键盘快捷键 操作 快捷键 选择地址栏 Alt + D 选择搜索框 Ctrl + E 打开一个新窗口 Ctrl + N 关闭活动窗口 Ctrl + W 更改文件和文件夹图标的大小和外观 Ctrl + 鼠标滚轮 显示所选文件夹上方的所有文件夹 Ctrl + Shift + E 创建一个新文件夹 Ctrl + Shift + N 显示所选文件夹下的所有子文件夹 Num Lock + asterisk (*) 显示所选文件夹的内容 Num Lock + plus (+) 折叠所选文件夹 Num Lock + minus (-) 显示预览面板 Alt + P 打开所选项目的“属性”对话框 Alt + Enter 查看下一个文件夹 Alt + 右箭头 查看文件夹所在的文件夹 Alt + 上箭头 查看上一个文件夹 Alt + 左箭头 或 Backspace 显示当前选择 右箭头 折叠当前选择 左箭头 显示活动窗口的底部 End 显示活动窗口的顶部 Home 5.6. 任务栏键盘快捷键 操作 快捷键 打开一个应用程序或快速打开另一个应用程序实例 Shift + 左键单击应用程序图标 以管理员身份打开应用 Ctrl + Shift + 左键单击应用程序图标 显示应用程序的窗口菜单 Shift + 右键单击应用程序图标 在任务栏中循环浏览应用程序 Win + T 根据固定编号在任务栏中打开应用 Win + Number 键 循环通过组的窗口 Ctrl + 单击分组的任务栏按钮 5.7. 设置键盘快捷键 操作 快捷键 打开设置 Win + I 返回设置主页 Backspace 搜索设置 在带有搜索框的任何页面上键入 5.8. 虚拟桌面键盘快捷键 操作 快捷键 打开任务视图 Win + Tab 添加虚拟桌面 Win + Ctrl + D 在右侧创建的虚拟桌面之间切换 Win + Ctrl + 右箭头 在左侧创建的虚拟桌面之间切换 Win + Ctrl + 左箭头 关闭您正在使用的虚拟桌面 Win + Ctrl + F4 5.9. 对话框快捷键 操作 快捷键 显示活动列表中的项目 F4 通过选项卡向后移动 Ctrl + Shift + Tab 移至第 n 个选项卡 Ctrl + 编号（编号 1–9） 通过选项前进 Tab 执行与该字母一起使用的命令（或选择选项） Alt + 带下划线的字母 如果活动选项是复选框，则选中或清除复选框 Spacebar 如果在“另存为”或“打开”对话框中选择了文件夹，则打开上一级文件夹 Backspace 如果活动选项是一组选项按钮，则选择一个按钮 箭头键 5.10. 命令提示符键盘快捷键 操作 快捷键 复制所选文本 Ctrl + C 粘贴所选文本 Ctrl + V 进入标记模式 Ctrl + M 在块模式下开始选择 Alt + 选择键 在指定的方向移动光标 箭头键 将光标向上移动一页 Page up 将光标向下移动一页 Page down 将光标移动到缓冲区的开头 Ctrl + Home 将光标移动到缓冲区的末尾 Ctrl + End 在输出历史中向上移动一行 Ctrl + 上箭头 在输出历史记录中下移一行 Ctrl + 下箭头 5.11. 游戏栏键盘快捷键 操作 快捷键 打开游戏栏 Win + G 截取当前游戏的截图 Win + Alt + PrtSc 记录活动游戏的最后 30 秒 Win + Alt + G 开始或停止记录活动游戏 Win + Alt + R 显示/隐藏当前游戏的录制计时器 Win + Alt + T 5.12. 辅助功能键盘快捷键 操作 快捷键 打开放大镜和缩放 Win + plus (+) 使用放大镜缩小 Win + minus (-) 在 Windows 设置中打开“轻松访问”中心 Win + U 退出放大镜 Win + Esc 在放大镜中切换到停靠模式 Alt + Ctrl + D 在放大镜中切换到全屏模式 Alt + Ctrl + F 打开或关闭粘滞键 按 Shift 五次 在放大镜中切换到镜头模式 Alt + Ctrl + L 在放大镜中反转颜色 Alt + Ctrl + I 在放大镜中循环浏览视图 Alt + Ctrl + M 在放大镜中使用鼠标调整镜头大小 Alt + Ctrl + R 在放大镜中平移 Alt + Ctrl + 箭头键 放大或缩小 Ctrl + Alt + 鼠标滚动 打开旁白 Win + Enter 打开或关闭切换键 按住 Num Lock 五秒钟 在 Windows 11 中使用此快捷方式打开屏幕键盘 Win + Ctrl + O 打开和关闭筛选键 按住右 Shift 八秒钟 打开或关闭高对比度 左 Alt 键 + 左 Shift 键 + PrtSc 打开或关闭鼠标键 左 Alt 键 + 左 Shift 键 + Num Lock 5.13. 浏览器快捷方式 操作 快捷键 在页面上查找 Ctrl + F 在地址栏中选择 URL 进行编辑 Alt + D 在 Windows 设置中打开“轻松访问”中心 Win + U 打开历史 Ctrl + H 在新选项卡中打开下载 Ctrl + J 打开一个新窗口 Ctrl + N 打印当前页面 Ctrl + P 重新加载当前页面 Ctrl + R 打开一个新选项卡并切换到它 Ctrl + T ","permalink":"https://ktzxy.top/posts/89bvjnh2zh/","summary":"Windows学习","title":"Windows学习"},{"content":"[TOC]\n如果知道查询结果只有一条或者只要最大/最小一条记录，建议用limit 1 假设现在有employee员工表，要找出一个名字叫jay的人.\n1 2 3 4 5 6 7 8 CREATE TABLE `employee` ( `id` int(11) NOT NULL, `name` varchar(255) DEFAULT NULL, `age` int(11) DEFAULT NULL, `date` datetime DEFAULT NULL, `sex` int(1) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 1 2 3 select id，name from employee where name=\u0026#39;jay\u0026#39; select id，name from employee where name=\u0026#39;jay\u0026#39; limit 1; 加上limit 1后,只要找到了对应的一条记录,就不会继续向下扫描了,效率将会大大提高。\n当然，如果name是唯一索引的话，是不必要加上limit 1了，因为limit的存在主要就是为了防止全表扫描，从而提高性能,如果一个语句本身可以预知不用全表扫描，有没有limit ，性能的差别并不大。\n应尽量避免在where子句中使用or来连接条件 新建一个user表，它有一个普通索引userId，表结构如下：\n1 2 3 4 5 6 7 8 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `userId` int(11) NOT NULL, `age` int(11) NOT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`), KEY `idx_userId` (`userId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 假设现在需要查询userid为1或者年龄为18岁的用户，很容易有以下sql 反例:\n1 2 3 4 5 6 7 8 9 select * from user where userid=1 or age =18 //使用union all select * from user where userid=1 union all select * from user where age = 18 //或者分开两条sql写： select * from user where userid=1 select * from user where age = 18 使用or可能会使索引失效，从而全表扫描。\n对于or+没有索引的age这种情况，假设它走了userId的索引，但是走到age查询条件时，它还得全表扫描，也就是需要三步过程： 全表扫描+索引扫描+合并.如果它一开始就走全表扫描，直接一遍扫描就完事。mysql是有优化器的，处于效率与成本考虑，遇到or条件，索引可能失效，看起来也合情合理。\n优化limit分页 我们日常做分页需求时，一般会用 limit 实现，但是当偏移量特别大的时候，查询效率就变得低下。\n1 2 3 4 5 6 7 8 9 10 反例： select id，name，age from employee limit 10000，10 正例： //方案一 ：返回上次查询的最大记录(偏移量) select id，name from employee where id\u0026gt;10000 limit 10. //方案二：order by + 索引 select id，name from employee order by id limit 10000，10 //方案三：在业务允许的情况下限制页数： 当偏移量最大的时候，查询效率就会越低，因为Mysql并非是跳过偏移量直接去取后面的数据，而是先把偏移量+要取的条数，然后再把前面偏移量这一段的数据抛弃掉再返回的。 如果使用优化方案一，返回上次最大查询记录（偏移量），这样可以跳过偏移量，效率提升不少。 方案二使用order by+索引，也是可以提高查询效率的。 方案三的话，建议跟业务讨论，有没有必要查这么后的分页啦。因为绝大多数用户都不会往后翻太多页。 优化你的like语句 日常开发中，如果用到模糊关键字查询，很容易想到like，但是like很可能让你的索引失效。\n1 2 3 4 反例： select userId，name from user where userId like \u0026#39;%123\u0026#39;; 正例： select userId，name from user where userId like \u0026#39;123%\u0026#39;; 把%放前面，并不走索引，如下：\n把% 放关键字后面，还是会走索引的。如下：\n使用where条件限定要查询的数据，避免返回多余的行 假设业务场景是这样：查询某个用户是否是会员。曾经看过老的实现代码是这样。。。 反例：\n1 2 3 4 5 List\u0026lt;Long\u0026gt; userIds = sqlMap.queryList(\u0026#34;select userId from user where isVip=1\u0026#34;); boolean isVip = userIds.contains(userId); 正例： Long userId = sqlMap.queryObject(\u0026#34;select userId from user where userId=\u0026#39;userId\u0026#39; and isVip=\u0026#39;1\u0026#39; \u0026#34;) boolean isVip = userId！=null; 理由：\n需要什么数据，就去查什么数据，避免返回不必要的数据，节省开销。\n尽量避免在索引列上使用mysql的内置函数 业务需求：查询最近七天内登陆过的用户(假设loginTime加了索引)\n1 2 3 4 反例： select userId,loginTime from loginuser where Date_ADD(loginTime,Interval 7 DAY) \u0026gt;=now(); 复制代码正例： explain select userId,loginTime from loginuser where loginTime \u0026gt;= Date_ADD(NOW(),INTERVAL - 7 DAY); 理由：\n索引列上使用mysql的内置函数，索引失效\n如果索引列不加内置函数，索引还是会走的。\n应尽量避免在 where 子句中对字段进行表达式操作，这将导致系统放弃使用索引而进行全表扫 1 2 3 4 反例： select * from user where age-1 =10； 复制代码正例： select * from user where age =11； 虽然age加了索引，但是因为对它进行运算，索引直接迷路了。。 Inner join 、left join、right join，优先使用Inner join，如果是left join，左边表结果尽量小 Inner join 内连接，在两张表进行连接查询时，只保留两张表中完全匹配的结果集 left join 在两张表进行连接查询时，会返回左表所有的行，即使在右表中没有匹配的记录。 right join 在两张表进行连接查询时，会返回右表所有的行，即使在左表中没有匹配的记录。 都满足SQL需求的前提下，推荐优先使用Inner join（内连接），如果要使用left join，左边表数据结果尽量小，如果有条件的尽量放到左边处理。\n1 2 3 4 反例: select * from tab1 t1 left join tab2 t2 on t1.size = t2.size where t1.id\u0026gt;2; 复制代码正例： select * from (select * from tab1 where id \u0026gt;2) t1 left join tab2 t2 on t1.size = t2.size; 如果inner join是等值连接，或许返回的行数比较少，所以性能相对会好一点。 同理，使用了左连接，左边表数据结果尽量小，条件尽量放到左边处理，意味着返回的行数可能比较少。 应尽量避免在 where 子句中使用!=或\u0026lt;\u0026gt;操作符，否则将引擎放弃使用索引而进行全表扫描。 1 2 3 4 5 6 反例： select age,name from user where age \u0026lt;\u0026gt;18; 复制代码正例： //可以考虑分开两条sql写 select age,name from user where age \u0026lt;18; select age,name from user where age \u0026gt;18; 使用!=和\u0026lt;\u0026gt;很可能会让索引失效 使用联合索引时，注意索引列的顺序，一般遵循最左匹配原则。 表结构：（有一个联合索引idx_userid_age，userId在前，age在后）\n1 2 3 4 5 6 7 8 9 10 11 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `userId` int(11) NOT NULL, `age` int(11) DEFAULT NULL, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`), KEY `idx_userid_age` (`userId`,`age`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 反例： select * from user where age = 10; 正例：\n1 2 3 4 //符合最左匹配原则 select * from user where userid=10 and age =10； //符合最左匹配原则 select * from user where userid =10; 理由：\n当我们创建一个联合索引的时候，如(k1,k2,k3)，相当于创建了（k1）、(k1,k2)和(k1,k2,k3)三个索引，这就是最左匹配原则。 联合索引不满足最左原则，索引一般会失效，但是这个还跟Mysql优化器有关的。 对查询进行优化，应考虑在 where 及 order by 涉及的列上建立索引，尽量避免全表扫描。 反例：\n1 select * from user where address =\u0026#39;深圳\u0026#39; order by age ; 正例： 添加索引\n1 alter table user add index idx_address_age (address,age) 如果插入数据过多，考虑批量插入。 1 2 3 4 5 6 7 8 9 10 反例： for(User u :list){ INSERT into user(name,age) values(#name#,#age#) } 正例： //一次500批量插入，分批进行 insert into user(name,age) values \u0026lt;foreach collection=\u0026#34;list\u0026#34; item=\u0026#34;item\u0026#34; index=\u0026#34;index\u0026#34; separator=\u0026#34;,\u0026#34;\u0026gt; (#{item.name},#{item.age}) \u0026lt;/foreach\u0026gt; 理由：\n批量插入性能好，更加省时间 在适当的时候，使用覆盖索引。 覆盖索引能够使得你的SQL语句不需要回表，仅仅访问索引就能够得到所有需要的数据，大大提高了查询效率。 反例：\n1 2 // like模糊查询，不走索引了 select * from user where userid like \u0026#39;%123%\u0026#39; 正例：\n1 2 //id为主键，那么为普通索引，即覆盖索引登场了。 select id,name from user where userid like \u0026#39;%123%\u0026#39;; 慎用distinct关键字 distinct 关键字一般用来过滤重复记录，以返回不重复的记录。在查询一个字段或者很少字段的情况下使用时，给查询带来优化效果。但是在字段很多的时候使用，却会大大降低查询效率。\n1 2 3 4 5 反例： SELECT DISTINCT * from user; 正例： select DISTINCT name from user; 理由：\n带distinct的语句cpu时间和占用时间都高于不带distinct的语句。因为当查询很多字段时，如果使用distinct，数据库引擎就会对数据进行比较，过滤掉重复数据，然而这个比较，过滤的过程会占用系统资源，cpu时间。 删除冗余和重复索引 1 2 3 4 5 6 反例： KEY `idx_userId` (`userId`) KEY `idx_userId_age` (`userId`,`age`) 正例: //删除userId索引，因为组合索引（A，B）相当于创建了（A）和（A，B）索引 KEY `idx_userId_age` (`userId`,`age`) 理由：\n重复的索引需要维护，并且优化器在优化查询的时候也需要逐个地进行考虑，这会影响性能的。 如果数据量较大，优化你的修改/删除语句。 避免同时修改或删除过多数据，因为会造成cpu利用率过高，从而影响别人对数据库的访问。\n1 2 3 4 5 6 7 8 9 10 11 反例： //一次删除10万或者100万+？ delete from user where id \u0026lt;100000; //或者采用单一循环操作，效率低，时间漫长 for（User user：list）{ delete from user； } 正例： //分批进行删除,如每次500 delete user where id\u0026lt;500 delete product where id\u0026gt;=500 and id\u0026lt;1000； 理由：\n一次性删除太多数据，可能会有lock wait timeout exceed的错误，所以建议分批操作。 where子句中考虑使用默认值代替null。 反例：\n1 select * from user where age is not null; 正例：\n1 2 //设置0为默认值 select * from user where age\u0026gt;0; 理由：\n并不是说使用了is null 或者 is not null 就会不走索引了，这个跟mysql版本以及查询成本都有关。 如果mysql优化器发现，走索引比不走索引成本还要高，肯定会放弃索引，这些条件！=，\u0026gt;is null，is not null经常被认为让索引失效，其实是因为一般情况下，查询的成本高，优化器自动放弃的。\n如果把null值，换成默认值，很多时候让走索引成为可能，同时，表达意思会相对清晰一点。 exist \u0026amp; in的合理利用 假设表A表示某企业的员工表，表B表示部门表，查询所有部门的所有员工，很容易有以下SQL:\n1 select * from A where deptId in (select deptId from B); 这样写等价于：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 先查询部门表B select deptId from B 再由部门deptId，查询A的员工 select * from A where A.deptId = B.deptId 可以抽象成这样的一个循环： List\u0026lt;\u0026gt; resultSet ; for(int i=0;i\u0026lt;B.length;i++) { for(int j=0;j\u0026lt;A.length;j++) { if(A[i].id==B[j].id) { resultSet.add(A[i]); break; } } } 显然，除了使用in，我们也可以用exists实现一样的查询功能，如下：\n1 select * from A where exists (select 1 from B where A.deptId = B.deptId); 因为exists查询的理解就是，先执行主查询，获得数据后，再放到子查询中做条件验证，根据验证结果（true或者false），来决定主查询的数据结果是否得意保留。 那么，这样写就等价于：\n1 2 3 4 5 6 7 8 9 10 11 12 13 select * from A,先从A表做循环 select * from B where A.deptId = B.deptId,再从B表做循环. 同理，可以抽象成这样一个循环： List\u0026lt;\u0026gt; resultSet ; for(int i=0;i\u0026lt;A.length;i++) { for(int j=0;j\u0026lt;B.length;j++) { if(A[i].deptId==B[j].deptId) { resultSet.add(A[i]); break; } } } 数据库最费劲的就是跟程序链接释放。假设链接了两次，每次做上百万次的数据集查询，查完就走，这样就只做了两次；相反建立了上百万次链接，申请链接释放反复重复，这样系统就受不了了。即mysql优化原则，就是小表驱动大表，小的数据集驱动大的数据集，从而让性能更优。\n因此，我们要选择最外层循环小的，也就是，如果B的数据量小于A，适合使用in，如果B的数据量大于A，即适合选择exist。\n尽量用 union all 替换 union 如果检索结果中不会有重复的记录，推荐union all 替换 union。\n1 2 3 4 5 6 7 8 反例： select * from user where userid=1 union select * from user where age = 10 复制代码正例： select * from user where userid=1 union all select * from user where age = 10 理由：\n如果使用union，不管检索结果有没有重复，都会尝试进行合并，然后在输出最终结果前进行排序。如果已知检索结果没有重复记录，使用union all 代替union，这样会提高效率。 索引不宜太多，一般5个以内。 索引并不是越多越好，索引虽然提高了查询的效率，但是也降低了插入和更新的效率。 insert或update时有可能会重建索引，所以建索引需要慎重考虑，视具体情况来定。 一个表的索引数最好不要超过5个，若太多需要考虑一些索引是否没有存在的必要。 尽量使用数字型字段，若只含数值信息的字段尽量不要设计为字符型 1 2 3 4 反例： king_id` varchar（20） NOT NULL COMMENT \u0026#39;守护者Id\u0026#39; 正例： `king_id` int(11) NOT NULL COMMENT \u0026#39;守护者Id\u0026#39;` 理由：\n相对于数字型字段，字符型会降低查询和连接的性能，并会增加存储开销。 尽量避免向客户端返回过多数据量。 假设业务需求是，用户请求查看自己最近一年观看过的直播数据。\n1 2 3 4 5 6 7 8 9 10 反例： //一次性查询所有数据回来 select * from LivingInfo where watchId =useId and watchTime \u0026gt;= Date_sub(now(),Interval 1 Y) 正例： //分页查询 select * from LivingInfo where watchId =useId and watchTime\u0026gt;= Date_sub(now(),Interval 1 Y) limit offset，pageSize //如果是前端分页，可以先查询前两百条记录，因为一般用户应该也不会往下翻太多页， select * from LivingInfo where watchId =useId and watchTime\u0026gt;= Date_sub(now(),Interval 1 Y) limit 200 ; 当在SQL语句中连接多个表时,请使用表的别名，并把别名前缀于每一列上，这样语义更加清晰。 1 2 3 4 5 6 反例： select * from A inner join B on A.deptId = B.deptId; 正例： select memeber.name,deptment.deptName from A member inner join B deptment on member.deptId = deptment.deptId; 尽可能使用varchar/nvarchar 代替 char/nchar。 1 2 3 4 反例： `deptName` char(100) DEFAULT NULL COMMENT \u0026#39;部门名称\u0026#39; 正例： `deptName` varchar(100) DEFAULT NULL COMMENT \u0026#39;部门名称\u0026#39; 理由：\n因为首先变长字段存储空间小，可以节省存储空间。 其次对于查询来说，在一个相对较小的字段内搜索，效率更高。 为了提高group by 语句的效率，可以在执行到该语句前，把不需要的记录过滤掉。 1 2 3 4 5 6 反例： select job，avg（salary） from employee group by job having job =\u0026#39;president\u0026#39; or job = \u0026#39;managent\u0026#39; 正例： select job，avg（salary） from employee where job =\u0026#39;president\u0026#39; or job = \u0026#39;managent\u0026#39; group by job； 如何字段类型是字符串，where时一定用引号括起来，否则索引失效 反例：\n1 select * from user where userid =123; 正例：\n1 select * from user where userid =\u0026#39;123\u0026#39;; 理由：\n为什么第一条语句未加单引号就不走索引了呢？ 这是因为不加单引号时，是字符串跟数字的比较，它们类型不匹配，MySQL会做隐式的类型转换，把它们转换为浮点数再做比较。 使用explain 分析你SQL的计划 日常开发写SQL的时候，尽量养成一个习惯吧。用explain分析一下你写的SQL，尤其是走不走索引这一块。\n1 explain select * from user where userid =10086 or age =18; ","permalink":"https://ktzxy.top/posts/c8cbh1lay1/","summary":"高质量Mysql","title":"高质量Mysql"},{"content":" 表结构优化是MySQL性能优化中的重要一环，性能优化从设计阶段就应该被考虑，良好的表结构设计从一开始就为系统的高性能打下了基础。本文整理了MySQL表结构优化的一些原则、经验和技巧。\n一、选择合适的存储引擎 MySQL存储引擎建议使用InnoDB，支持事务，支持行级锁，数据更安全。MySQL的其他引擎在实际应用中并不多，比如曾经风光无限的MyISAM引擎随着时代的发展逐渐没落，MySQL组复制Group Replication 只支持InnoDB存储，MySQL 8.0 元数据管理也只使用InnoDB，不再使用MyISAM。\n二、选择合适的数据类型 表中要有主键，主键字段越小越好，主键最好使用自增的整型。由于在二级索引中会存储主键的值，太大的主键会导致磁盘占用过大，占用太多的内存buffer pool，降低查询效率。不要使用md5,uuid这种类型的数据做主键，这类无序主键会会导致页频繁分裂，影响写入性能，造成数据空洞，占用过多磁盘空间。 数据类型越小越好，前提是满足业务需求。越小的数据类型运算更快，占用磁盘空间也更小。 能用固定长度的就不要使用变长类型，固定长度的类型容易被缓存，处理性能也更高。 使用简单的数据类型，能使用数值型的，就不要使用字符串，数值计算的效率远高于字符串。比如IP地址使用整型来存储比使用字符串要更快，更少占空间。 有限值字段使用enum类型，enum占用空间更少，效率更高，比如性能、民族、国家等有限固定的数据，建议使用enum。 对于小数类型，尽量不使用float,double，这两个类型存在精度丢失的问题。固定精度的小数也不建议使用decimal，建议乘以固定倍数转换成整数来存储，可以节省存储空间，提高查询效率。 对于整数类型，如果确定值不会有负数，定义字段类型时加上unsigned。 字段尽可能使用not null，尤其对于需要索引的字段，虽然null值可以节省空间，但会带来很多优化问题，另外null值在很多时候容易引起误解，比如select count(name)，如果name为null，获取的总数可能并不如预期。 字符串尽量少用text、blob等大文本类型，定长类型用char，不定长类型用varchar。 三、表的范式和反范式设计 范式化设计可以减少数据冗余，表通常更小，更新操作更快，但是在查询时需要将多个表进行关联，索引优化比较复杂。 反范式化设计可以减少查询时的表关联，可以更好的优化索引，但是存在数据冗余及多份数据的维护异常，对数据修改需要更多的成本。 范式化和反范式化设计没有绝对的好与坏，但需要遵循一定的原则。设计表结构时，以范式化设计为主，在优化表结构阶段，可允许一定的反范式设计，以减少不必要的表关联，提高某些特定场景下的查询效率。\n四、表的垂直拆分 当一个表有很多字段时，需要考虑是否把表拆小一点，解决表宽度过大的问题。垂直拆分通常基于如下原则：\n常用的字段与不常用的字段分开，把不常用的字段拆到单独的一个表中。 把text,blob等大字段单独拆到一个表中。 五、表的水平拆分 MySQL单表数据量过大时，会严重影响查询性能，此时就需要考虑对表进行水平拆分，也就是分库分表，所有子表的数据结构完全一致，根据不同的分库分表算法进行拆分。目前也有很多数据库中间件实现了分库分表的功能，比如mycat, atlas，cetus等等，使用数据库分库分表中间件，能够做到业务无感知，就像是使用一张表一样。\n","permalink":"https://ktzxy.top/posts/n4kobyv07w/","summary":"MySQL性能优化 表结构优化","title":"MySQL性能优化 表结构优化"},{"content":"[TOC]\n1、Explain诊断 1.1 select_type 常见类型及其含义 SIMPLE：不包含子查询或者 UNION 操作的查询 PRIMARY：查询中如果包含任何子查询，那么最外层的查询则被标记为 PRIMARY SUBQUERY：子查询中第一个 SELECT DEPENDENT SUBQUERY：子查询中的第一个 SELECT，取决于外部查询 UNION：UNION 操作的第二个或者之后的查询 DEPENDENT UNION：UNION 操作的第二个或者之后的查询,取决于外部查询 UNION RESULT：UNION 产生的结果集 DERIVED：出现在 FROM 字句中的子查询 1.2 type常见类型及其含义 system：这是 const 类型的一个特例，只会出现在待查询的表只有一行数据的情况下 consts：常出现在主键或唯一索引与常量值进行比较的场景下，此时查询性能是最优的 eq_ref：当连接使用的是完整的索引并且是 PRIMARY KEY 或 UNIQUE NOT NULL INDEX 时使用它 ref：当连接使用的是前缀索引或连接条件不是 PRIMARY KEY 或 UNIQUE INDEX 时则使用它 ref_or_null：类似于 ref 类型的查询，但是附加了对 NULL 值列的查询 index_merge：该联接类型表示使用了索引进行合并优化 range：使用索引进行范围扫描，常见于 between、\u0026gt; 、\u0026lt; 这样的查询条件 index：索引连接类型与 ALL 相同，只是扫描的是索引树，通常出现在索引是该查询的覆盖索引的情况 ALL：全表扫描，效率最差的查找方式 阿里编码规范要求：至少要达到 range 级别，要求是 ref 级别，如果可以是 consts 最好\n1.3 key列 实际在查询中是否使用到索引的标志字段\n1.4 如何查看Mysql优化器优化之后的SQL 1 2 3 4 5 6 7 8 # 仅在服务器环境下或通过Navicat进入命令列界面 explain extended SELECT * FROM `student` where `name` = 1 and `age` = 1; # 再执行 show warnings; # 结果如下： /* select#1 */ select `mytest`.`student`.`age` AS `age`,`mytest`.`student`.`name` AS `name`,`mytest`.`student`.`year` AS `year` from `mytest`.`student` where ((`mytest`.`student`.`age` = 1) and (`mytest`.`student`.`name` = 1)) 2、SQL优化 2.1 超大分页场景解决方案 如表中数据需要进行深度分页，如何提高效率？\n利用延迟关联或者子查询优化超多分页场景\n说明：MySQL 并不是跳过 offset 行，而是取 offset+N 行，然后返回放弃前 offset 行，返回 N 行，那当 offset 特别大的时候，效率就非常的低下，要么控制返回的总页数，要么对超过特定阈值的页数进行 SQL 改写。\n1 2 3 4 5 6 7 8 # 反例（耗时129.570s） select * from task_result LIMIT 20000000, 10; # 正例（耗时5.114s） SELECT a.* FROM task_result a, (select id from task_result LIMIT 20000000, 10) b where a.id = b.id; # 说明 task_result表为生产环境的一个表，总数据量为3400万，id为主键，偏移量达到2000万 2.2 获取一条数据时的Limit 1 如果数据表的情况已知，某个业务需要获取符合某个Where条件下的一条数据，注意使用Limit\n说明：在很多情况下我们已知数据仅存在一条，此时我们应该告知数据库只用查一条，否则将会转化为全表扫描。\n1 2 3 4 5 6 7 8 # 反例（耗时2424.612s） select * from task_result where unique_key = \u0026#39;ebbf420b65d95573db7669f21fa3be3e_861414030800727_48\u0026#39;; # 正例（耗时1.036s） select * from task_result where unique_key = \u0026#39;ebbf420b65d95573db7669f21fa3be3e_861414030800727_48\u0026#39; LIMIT 1; # 说明 task_result表为生产环境的一个表，总数据量为3400万，where条件非索引字段，数据所在行为第19486条记录 2.3 批量插入 1 2 3 4 5 6 7 # 反例 INSERT into person(name,age) values(\u0026#39;A\u0026#39;,24) INSERT into person(name,age) values(\u0026#39;B\u0026#39;,24) INSERT into person(name,age) values(\u0026#39;C\u0026#39;,24) # 正例 INSERT into person(name,age) values(\u0026#39;A\u0026#39;,24),(\u0026#39;B\u0026#39;,24),(\u0026#39;C\u0026#39;,24); 2.4 like语句的优化 like语句一般业务要求都是 \u0026lsquo;%关键字%\u0026lsquo;这种形式，但是依然要思考能否考虑使用右模糊的方式去替代产品的要求，其中阿里的编码规范提到：\n页面搜索严禁左模糊或者全模糊，如果需要请走搜索引擎来解决\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 反例（耗时78.843s） EXPLAIN select * from task_result where taskid LIKE \u0026#39;%tt600e6b601677b5cbfe516a013b8e46%\u0026#39; LIMIT 1; # 正例（耗时0.986s） select * from task_result where taskid LIKE \u0026#39;tt600e6b601677b5cbfe516a013b8e46%\u0026#39; LIMIT 1 ########################################################################## # 对正例的Explain 1\tSIMPLE\ttask_result\trange\tadapt_id\tadapt_id\t98\t99\t100.00\tUsing index condition # 对反例的Explain 1\tSIMPLE\ttask_result\tALL\t33628554\t11.11\tUsing where # 说明 task_result表为生产环境的一个表，总数据量为3400万，taskid是一个普通索引列，可见%%这种匹配方式完全无法使用索引，从而进行全表扫描导致效率极低，而正例通过索引查找数据只需要扫描99条数据即可 2.5 使用 ISNULL()来判断是否为 NULL 值 说明：NULL 与任何值的直接比较都为 NULL\n1 2 3 # 1） NULL\u0026lt;\u0026gt;NULL 的返回结果是 NULL，而不是 false。 # 2） NULL=NULL 的返回结果是 NULL，而不是 true。 # 3） NULL\u0026lt;\u0026gt;1 的返回结果是 NULL，而不是 true。 2.6 多表查询 超过三个表禁止 join。需要 join 的字段，数据类型必须绝对一致；多表关联查询时，保证被关联的字段需要有索引。\n2.7 count(*) 还是 count(id) 阿里的Java编码规范中有以下内容： 【强制】不要使用 count(列名) 或 count(常量) 来替代 count() count() 是 SQL92 定义的标准统计行数的语法，跟数据库无关，跟 NULL 和非 NULL 无关。 说明：count(*)会统计值为 NULL 的行，而 count(列名)不会统计此列为 NULL 值的行\n2.8 字段类型不同导致索引失效 阿里的Java编码规范中有以下内容：\n【推荐】防止因字段类型不同造成的隐式转换，导致索引失效\n实际上数据库在查询的时候会作一层隐式的转换，比如 varchar 类型字段通过 数字去查询。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 正例 EXPLAIN SELECT * FROM `user_coll` where pid = \u0026#39;1\u0026#39;; type：ref ref：const rows:1 Extra:Using index condition # 反例 EXPLAIN SELECT * FROM `user_coll` where pid = 1; type：index ref：NULL rows:3(总记录数) Extra:Using where; Using index # 说明 pid字段有相应索引，且格式为varchar ","permalink":"https://ktzxy.top/posts/koptgdrgcs/","summary":"SQL优化和诊断","title":"SQL优化和诊断"},{"content":"1. 网络编程概述 在计算机领域中，网络是信息传输、接收、共享的虚拟平台，将各个点、面、体的信息联系到一起，从而实现这些资源的共享。网络编程的作用：解决计算机与计算机数据传输的问题。\n网络体系大致分为三种：OSI七层模型、TCP/IP四层模型和五层模型。\nTips: 一般面试的时候考察比较多的是五层模型。\n1.1. 网络通讯三要素 1.1.1. IP地址 IP 是每台电脑在互联网上的唯一标识符。一个 IPV4 的地址是由四段 0—255 的数字组成：192.168.0.100，每一段的取值范围由8位二进制数据组成。\nIPv6 使用 16 个字节表示 IP 地址，它所拥有的地址容量约是 IPv4 的 8×1028倍，达到 2128个。\nNotes:\n127.0.0.1 为本地主机地址(本地回环地址)，与 localhost 类似，均代表本机地址 xxx.xxx.xxx.255 广播地址，即该网段下所有用户均可以被通知到 例如在 windows 系统中，可以通过以下命令来获取 ip 与网络相关内容：\n1 ipconfig 用于DOS获取计算机IP设置 1 ping ip地址 用于判断两台计算机连接是否通畅 1.1.2. 端口号 端口号是一个十进制整数，是进程的唯一标识。在计算机中，不同的应用程序是通过端口号区分的。通过IP地址可以连接到指定计算机，但如果想访问目标计算机中的某个应用程序，还需要指定端口号。\n端口号是用两个字节（16位的二进制数）表示的，它的取值范围是0~65535。其中，0~1023之间的端口号是系统保留使用的，开发人员需要使用 1024 以上的端口号，从而避免端口号被另外一个应用或服务所占用。\n1.1.3. 通讯协议 通讯协议的作用：确定数据如何传输。TCP/IP 协议中的四层分别是应用层、传输层、网络层和链路层，每层分别负责不同的通信功能：\n链路层：链路层是用于定义物理传输通道，通常是对某些网络连接设备的驱动协议，例如针对光纤、网线提供的驱动。 网络层：网络层是整个 TCP/IP 协议的核心，它主要用于将传输的数据进行分组，将分组数据发送到目标计算机或者网络。 传输层：主要使网络程序进行通信，在进行网络通信时，可以采用 TCP 协议，也可以采用 UDP 协议。 应用层：主要负责应用程序的协议，例如 HTTP 协议、FTP 协议等。 1.1.4. 小结 网络通讯三要素小结：通过IP找主机，通过端口找程序，通过协议确定如何传输数据。\n1.2. OSI 七层网络模型 OSI（Open System Interconnect），即开放式系统互联。一般都叫OSI参考模型，是ISO（国际标准化组织）组织在1985年研究的网络互连模型。ISO为了更好的使网络应用更为普及，推出了OSI参考模型，这样所有的公司都按照统一的标准来指定自己的网络，就可以互通互联了。\n网络的七层模型从上到下主要包括：\n应用层：主要是一些基于网络构建的终端应用，例如：FTP（各种文件上传下载服务）、WEB（网页浏览）、Telnet服务、HTTP服务、DNS服务、SNMP邮件服务、QQ 等等。可以理解成电脑系统中需要网络的软件都是终端应用。 表示层：主要是进行对接收的数据进行解释、加密与解密、压缩与解压缩等，也就是把计算机能够识别的内容转换成人能够能识别的内容（如图片、声音等）。 会话层：通过传输层（端口号：传输端口与接收端口）建立数据传输的连接和管理会话，主要是在系统之间发起会话或者接受会话请求，具体包括登录验证、断点续传、数据粘包与分包等。设备之间需要互相识别，依据的可以是 IP、MAC 或者主机名。 传输层：定义了一些传输数据的协议和端口号（WWW 端口 80 等），主要是将从下层接收的数据进行分段、传输，到达目的地址后再进行重组。常把这一层数据叫做段。在这一层工作的协议有 TCP 和 UDP： TCP（传输控制协议）：传输效率低，可靠性强，用于传输可靠性要求高，数据量大的数据。比如支付宝转账使用的就是 TCP UDP（用户数据报协议）：与 TCP 特性恰恰相反，用于传输可靠性要求不高，数据量小的数据，如 QQ 聊天数据、抖音等视频服务就使用了 UDP 网络层：主要将从下层接收到的数据进行 IP 地址（例 192.168.0.1)的封装与解析。常把这一层的数据叫做数据包，在这一层工作的设备是路由器、交换机、防火墙等。 数据链路层：主要将从物理层接收的数据进行 MAC 地址（网卡的地址）的封装与解析。常把这一层的数据叫做帧。在这一层工作的设备是网卡、网桥、交换机，数据通过交换机来传输。 物理层：主要定义物理设备标准，如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流（就是由 1、0 转化为电流强弱来进行传输,到达目的地后在转化为1、0，也就是我们常说的模数转换与数模转换）。这一层的数据叫做比特。 1.3. TCP/IP 四层网络模型 TCP/IP 是指因特网的整个 TCP/IP 协议簇。从协议分层模型方面来讲，TCP/IP 由 4 个层次组成：\nTCP/IP 中网络接口层、网络层、传输层和应用层的具体工作职责：\n网络接口层（Network Access Layer）：定义了主机间网络连通的协议，具体包括 Echernet、FDDI、ATM 等通信协议。 网络层（Internet Layer）：主要用于数据的传输、路由及地址的解析，以保障主机可以把数据发送给任何网络上的目标。数据经过网络传输，发送的顺序和到达的顺序可能发生变化。在网络层使用 IP（Internet Protocol）和地址解析协议（ARP）。 传输层（Transport Layer）：使源端和目的端机器上的对等实体可以基于会话相互通信。在这一层定义了两个端到端的协议 TCP 和 UDP。 TCP 是面向连接的协议，提供可靠的报文传输和对上层应用的连接服务，除了基本的数据传输，它还有可靠性保证、流量控制、多路复用、优先权和安全性控制等功能。 UDP 是面向无连接的不可靠传输的协议，主要用于不需要 TCP 的排序和流量控制等功能的应用程序。 应用层（Application Layer）：负责具体应用层协议的定义，包括以下协议： Telnet（TELecommunications NETwork，虚拟终端协议） FTP（File Transfer Protocol，文件传输协议） SMTP（Simple Mail Transfer Protocol，电子邮件传输协议） DNS（Domain Name Service，域名服务） NNTP（Net News Transfer Protocol，网上新闻传输协议） HTTP（HyperText Transfer Protocol，超文本传输协议） 1.3.1. TCP/IP 网络模型与 OSI 网络模型对比 1.4. 五层模型 五层模型：应用层、传输层、网络层、数据链路层、物理层。\n应用层：为应用程序提供交互服务。在互联网中的应用层协议很多，如域名系统 DNS、HTTP 协议、SMTP 协议等。 传输层：负责向两台主机进程之间的通信提供数据传输服务。传输层的协议主要有传输控制协议 TCP 和用户数据协议 UDP。 网络层：选择合适的路由和交换结点，确保数据及时传送。主要包括 IP 协议。 数据链路层：在两个相邻节点之间传送数据时，数据链路层将网络层交下来的 IP 数据报组装成帧，在两个相邻节点间的链路上传送帧。 物理层：实现相邻节点间比特流的透明传输，尽可能屏蔽传输介质和物理设备的差异。 2. Socket 2.1. Socket 简介 网络上的两个程序通过一个双向的通讯连接实现数据的交换，这个双向链路的一端称为一个 Socket。Socket 通常用来实现客户方和服务方的连接。Socket 连接就是所谓的长连接，客户端和服务器需要互相连接，理论上客户端和服务器端一旦建立起连接将不会主动断掉的，但是有时候网络波动还是有可能的。\nSocket 是 TCP/IP 协议的一个十分流行的编程界面，一个 Socket 由一个 IP 地址和一个端口号唯一确定。\n但是，Socket 所支持的协议种类也不光 TCP/IP、UDP，因此两者之间是没有必然联系的。在 Java 环境下，Socket 编程主要是指基于 TCP/IP 协议的网络编程。Socket 偏向于底层。一般很少直接使用 Socket 来编程，框架底层使用 Socket 比较多。\n2.2. Socket 所属网络模型的层级 Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层，它是一组接口。在设计模式中，Socket 就是一个外观模式，它把复杂的 TCP/IP 协议族隐藏在 Socket 接口后面，对用户来说，一组简单的接口就是全部，让 Socket 去组织数据，以符合指定的协议。\n2.3. Socket 通讯的过程 基于 TCP：服务器端先初始化 Socket，然后与端口绑定(bind)，对端口进行监听(listen)，调用 accept 阻塞，等待客户端连接。在这时如果有个客户端初始化一个 Socket，然后连接服务器(connect)，如果连接成功，这时客户端与服务器端的连接就建立了。客户端发送数据请求，服务器端接收请求并处理请求，然后把回应数据发送给客户端，客户端读取数据，最后关闭连接，一次交互结束。 基于 UDP：UDP 协议是用户数据报协议的简称，也用于网络数据的传输。虽然 UDP 协议是一种不太可靠的协议，但有时在需要较快地接收数据并且可以忍受较小错误的情况下，UDP 就会表现出更大的优势。客户端只需要发送，不管服务端是否接收成功。 3. UDP 通信 3.1. UDP 协议概述 UDP 是 User Datagram Protocol 的简称，称为用户数据报协议。传输层的两个重要的高级协议之一。是一个面向无连接的协议，它提供不可靠的数据传输。\n在 UDP 通信中，发送端在发送数据之前不确定接收端是否存在，也不需要与对方建立连接，数据被封装成数据包，直接发送给接收方。UDP 不提供数据校验、确认机制和拥塞控制，因此传输速度较快，但容易发生数据丢失。\n在 UDP 协议中，有一个IP地址称为广播地址，只要给广播地址发送消息，那么同一个网段的所有用户都可以接收到消息。IP 地址格式：网络号(前3段)+主机号(最后1段)。如，192.168.113.68\nTips: 如果主机号是255，则该 IP 地址就是广播地址\n3.1.1. UDP 协议的特点 面向无连接的协议。即在数据传输时，数据的发送端和接收端不建立逻辑连接。 不管对方是否能收到数据。对方收到数据之后也不会给一个反馈给发送端。 发送的数据限制在64k以内。 基于数据包来传输：将数据以及源和目的地封装到一个数据包中。 UDP的面向无连接性，不能保证数据的完整性，但效率高。是不可靠的协议。 3.1.2. UDP 协议使用场景 UDP 协议适用于实时传输要求较高的应用。\n即时通讯 在线视频 网络语音电话 3.2. DatagramPacket 类（数据报对象） 3.2.1. 作用 用于在 UDP 通信中封装发送端的数据或接收端的数据。\n3.2.2. 构造方法 1 public DatagramPacket(byte[] buf, int length) 创建 DatagramPacket 对象时，指定了封装数据的字节数组和数据的大小，没有指定 IP 地址和端口号。只能用于接收端，不能用于发送端。因为发送端一定要明确指出数据的目的地(ip 地址和端口号)，而接收端不需要明确知道数据的来源，只需要接收到数据即可 参数buf：要接收的数据数组 参数length：发送数据的长度，单位：字节 1 public DatagramPacket(byte[] buf, int length, InetAddress address, int port) 使用该构造方法在创建 DatagramPacket 对象时，不仅指定了封装数据的字节数组和数据的大小，还指定了数据包的目标 IP 地址（addr）和端口号（port）。该对象通常用于发送端，因为在发送数据时必须指定接收端的IP地址和端口号。 参数buf：要发送的数据数组 参数length：发送数据的长度，单位：字节 参数address：接收端的IP地址对象 参数port：接收端的端口号 3.2.3. 常用方法 1 public InetAddress getAddress() 返回某台机器的 IP 地址 1 public int getPort() 返回某台远程主机的端口号 1 public byte[] getData() 返回数据缓冲区。 1 public int getLength() 返回将要发送或接收到的数据的长度。 3.3. DatagramSocket 类（数据发送对象） 3.3.1. 作用 用来负责发送和接收数据包对象。\n3.3.2. 构造方法 public DatagramSocket() throws SocketException 该构造方法用于创建发送端的DatagramSocket对象，在创建DatagramSocket对象时，并没有指定端口号，此时，系统会分配一个没有被其它网络程序所使用的端口号。 API:构造数据报套接字并将其绑定到本地主机上任何可用的端口。套接字将被绑定到通配符地址，IP 地址由内核来选择。 public DatagramSocket(int port) throws SocketException 该构造方法既可用于创建接收端的DatagramSocket对象，又可以创建发送端的DatagramSocket对象，在创建接收端的DatagramSocket对象时，必须要指定一个端口号，这样就可以监听指定的端口。 3.3.3. 常用方法 1 2 3 4 5 6 7 8 public void send(DatagramPacket p) throws IOException // 从此套接字发送数据报包。 public void receive(DatagramPacket p) throws IOException // 从此套接字接收数据报包。具有线程阻塞效果，运行后等待接收 public void close() // 关闭此数据报套接字。 3.4. UDP 网络程序实现步骤 3.4.1. UDP 发送端的实现步骤 创建DatagramPacket对象，并封装数据 1 2 // 指定端口port public DatagramPacket(byte[] buf, int length, InetAddress address, int port) 创建DatagramSocket对象，使用无参构造即可 发送数据，调用send方法发送数据包 释放流资源（关闭DatagramSocket对象）。注：如果是抛异常只需抛 IOException 即可 3.4.2. UDP 接收端的实现步骤 创建 DatagramPacket 对象。接收数据存储到 DatagramPacket 对象中，创建字节数组，接收发来的数据。 1 public DatagramPacket(byte[] buf, int length) 创建 DatagramSocket 对象，**绑定端口号，要和发送端端口号一致。**s 1 public DatagramSocket(int port) 调用 DatagramSocket 对象 receive 方法，接收数据，数据放到数据包中 1 receive(DatagramPacket dp); 拆包，获取 DatagramPacket 对象的内容 发送的 IP 地址对象 接收到字节数组内容 接收到的字节个数 发送方的端口号(不重要，由系统分配的。) 释放流资源（关闭 DatagramSocket 对象） 3.4.3. UDP 发送端与接收端的基础示例 3.4.3.1. UDP 发送端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; /* * 实现UDP发送端（试验发送给自己） */ public class MoonZero { public static void main(String[] args) throws IOException { // 创建字节数组 byte[] arr = \u0026#34;试试UDP\u0026#34;.getBytes(); // 获取自己的IP地址对象，封装自己的IP地址（使用本地回环地址，目的方便日后修改成其他主机IP） InetAddress inet = InetAddress.getByName(\u0026#34;127.0.0.1\u0026#34;); // 创建数据包对象，封装要发送的数据，接收端IP，端口 // public DatagramPacket(byte[] buf, int length, InetAddress address, int port) DatagramPacket dp = new DatagramPacket(arr, arr.length, inet, 6000); // 创建DatagramSocket对象，用来发送数据包,只用发送，无参构造即可 DatagramSocket ds = new DatagramSocket(); // 调用发送的方法, ds.send(dp); // 关闭流资源 ds.close(); } } 3.4.3.2. UDP 接收端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; /* * 实现UDP接收端（试验发送给自己） */ public class UDPReceive { public static void main(String[] args) throws IOException { // 创建DatagramSocket对象，绑定端口号，端口号要一致 DatagramSocket ds = new DatagramSocket(6000); // 创建字节数组 byte[] arr = new byte[1024]; // 创建数据包对我，传递字节数据,该构造方法只用接收端 DatagramPacket dp = new DatagramPacket(arr, arr.length); // 调用Socket方法接收传递过来的数据包 ds.receive(dp); // 拆包 System.out.println(\u0026#34;接收到的数据是：\u0026#34; + new String(dp.getData(),0,dp.getLength())); System.out.println(\u0026#34;接收到的数据长度是：\u0026#34; + dp.getLength()); System.out.println(\u0026#34;发送端的IP地址是：\u0026#34; + dp.getAddress().getHostAddress()); System.out.println(\u0026#34;发送端的名称是：\u0026#34; + dp.getAddress().getHostName()); System.out.println(\u0026#34;发送端的端口号是：\u0026#34; + dp.getPort()); // 关闭流资源 ds.close(); } } 3.4.3.3. 测试效果 先运行接收端：具有线程阻塞效果，会等待发送端的数据\n再运行发送端：（注：发送端与接收对象定义的端口不是一样的）\n3.5. UDP 键盘录入发送和接收示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.util.Scanner; /* * 实现UDP发送端（试验发送给自己） */ public class MoonZero { public static void main(String[] args) throws IOException { Scanner input = new Scanner(System.in); // 获取自己的IP地址对象，封装自己的IP地址（使用本地回环地址，目的方便日后修改成其他主机IP） InetAddress inet = InetAddress.getByName(\u0026#34;127.0.0.1\u0026#34;); // 创建DatagramSocket对象，用来发送数据包,只用发送，无参构造即可 DatagramSocket ds = new DatagramSocket(); while(true) { String s = input.nextLine(); // 创建字节数组 byte[] arr = s.getBytes(); // 创建数据包对象，封装要发送的数据，接收端IP，端口 DatagramPacket dp = new DatagramPacket(arr, arr.length, inet, 6000); // 调用发送的方法, ds.send(dp); } } } import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; /* * 实现UDP接收端（试验发送给自己） */ public class UDPReceive { public static void main(String[] args) throws IOException { // 创建DatagramSocket对象，绑定端口号，端口号要一致 DatagramSocket ds = new DatagramSocket(6000); // 创建字节数组 byte[] arr = new byte[1024]; // 创建数据包对象，传递字节数据,该构造方法只用接收端 DatagramPacket dp = new DatagramPacket(arr, arr.length); while (true) { // 调用Socket方法接收传递过来的数据包 ds.receive(dp); // 拆包 System.out.print(\u0026#34;接收到的数据是：\u0026#34; + new String(dp.getData(), 0, dp.getLength())); System.out.println(\u0026#34;-----\u0026#34; + dp.getAddress().getHostAddress()); } } } 运行结果\n4. TCP 通信 4.1. TCP 协议 4.1.1. 概述 TCP 是 Transmission Control Protocol 的简称，称为传输控制协议。传输层的两个重要的高级协议之一。TCP 协议是面向连接的通信协议，即在传输数据前先在发送端和接收端建立逻辑连接，然后再传输数据，它提供了两台计算机之间可靠无差错的数据传输，保证传输数据的安全性。\n在 TCP 通信中，数据被分成多个小片段，每个片段都会被编号和校验，确保数据完整性。TCP 使用确认机制，确保数据的可靠性，如果发送方没有收到确认信息，会重新发送数据。TCP 还处理拥塞控制，根据网络条件动态调整数据传输的速率。\n4.1.2. 特点 面向连接的运输层协议，因为面向连接，效率低，可靠的协议。 点对点，每一条 TCP 连接只能有两个端点 通过 3 次握手建立连接，形成数据传输通道，开始传输。 通过 4 次挥手断开连接。 发送的数据没有大小限制 基于 IO 流进行数据传输 TCP 提供全双工通信 面向字节流 4.1.3. TCP 协议使用场景 TCP 适用于需要保证数据完整性和可靠性的应用。例如：\n文件上传和下载 发送电子邮件 远程登陆 4.1.4. TCP 和 UDP 协议的区别 连接机制不同：TCP 是面向连接，发送数据前客户端和服务器之间需要建立连接，然后再进行数据传输；而 UDP 是无连接的，发送数据之前不需要建立连接，数据包可以直接发送给目标主机。 数据传输方式不同：TCP 采用可靠的数据传输方式，即在传输过程中使用序号、确认号和重传机制等控制手段来保证数据的可靠传输；而 UDP 采用不可靠的数据传输方式，数据包可能会丢失或重复，不提供数据可靠性保障。 数据传输效率不同：由于 TCP 需要进行连接、序号确认等额外的数据包传输，因此在数据传输效率方面相对于 UDP 要低一些。 数据大小限制不同：TCP 对数据包的大小有限制，最大只能传输 64KB 的数据，而 UDP 的数据包大小没有限制。 应用场景不同：TCP 适用于要求数据传输可靠性高的场景，如网页浏览、文件下载、电子邮件等；而 UDP 适用于实时性要求较高的场景，如视频会议、在线游戏等。 TCP 面向字节流，把数据看成一连串无结构的字节流；而 UDP 是面向报文的。 TCP 有拥塞控制；而 UDP 没有拥塞控制，因此网络出现拥塞不会使源主机的发送速率降低（对实时应用很有用，如实时视频会议等）。 TCP 每一条连接只能是点到点的；而 UDP 支持一对一、一对多、多对一、多对多等通信方式。 TCP 首部开销 20 字节；UDP 的首部开销小，只有 8 个字节。 TCP 协议是没有发送端和接收端的概念，分为客户端和服务器端；而 UDP 协议是区分发送端和接收端。 TCP 协议通讯必须由客户端主动发消息给服务器端。 总结：TCP 是可靠的、有序的、面向连接的传输协议；而 UDP 是简单的、不可靠的、无连接的传输协议。选择 TCP 还是 UDP 要根据具体的应用需求来确定。\n4.2. TCP 三次握手/四次挥手 TCP 在传输之前建立连接会进行 3 次沟通，一般称为“三次握手”，在数据传输完成断开连接的时候要进行 4 次沟通，一般称为“四次挥手”。\n4.2.1. TCP 数据包结构 TCP 包的数据结构如下：\n源端口号（ 16 位）：它（连同源主机 IP 地址）标识源主机的一个应用进程。 目的端口号（ 16 位）：它（连同目的主机 IP 地址）标识目的主机的一个应用进程。这两个值加上 IP 报头中的源主机 IP 地址和目的主机 IP 地址唯一确定一个 TCP 连接。 序列号 seq（ 32 位）：用来标识从 TCP 源端向 TCP 目的端发送的数据字节流，它表示在这个报文段中的第一个数据字节的序列号。如果将字节流看作在两个应用程序间的单向流动，则 TCP 用序列号对每个字节进行计数。序号是 32bit 的无符号数，序号到达 232 － 1 后又从 0 开始。当建立一个新的连接时，SYN 标志变 1 ，序列号字段包含由这个主机选择的该连接的初始序列号 ISN （Initial Sequence Number）。 确认号 ack（ 32 位）：包含发送确认的一端所期望收到的下一个顺序号。因此，确认序号应当是上次已成功收到数据字节顺序号加 1。只有 ACK 标志为 1 时确认序号字段才有效。TCP 为应用层提供全双工服务，这意味数据能在两个方向上独立地进行传输。因此，连接的每一端必须保持每个方向上的传输数据顺序号。 TCP 报头长度（ 4 位）：给出报头中 32bit 字的数目，它实际上指明数据从哪里开始。需要这个值是因为任选字段的长度是可变的。这个字段占 4bit ，因此 TCP 最多有 60 字节的首部。然而，没有任选字段，正常的长度是 20 字节。 保留位（ 6 位）：保留给将来使用，目前必须置为 0 。 控制位（ control flags ，6 位）：在 TCP 报头中有 6 个标志比特，它们中的多个可同时被设置为 1 。依次为： URG ：为 1 表示紧急指针有效，为 0 则忽略紧急指针值。 ACK ：为 1 表示确认号有效，为 0 表示报文中不包含确认信息，忽略确认号字段。 PSH ：为 1 表示是带有 PUSH 标志的数据，指示接收方应该尽快将这个报文段交给应用层而不用等待缓冲区装满。 RST ：用于复位由于主机崩溃或其他原因而出现错误的连接。它还可以用于拒绝非法的报文段和拒绝连接请求。一般情况下，如果收到一个 RST 为 1 的报文，那么一定发生了某些问题。 SYN ：同步序号，为 1 表示连接请求，用于建立连接和使顺序号同步（ synchronize ）。 FIN ：用于释放连接，为 1 表示发送方已经没有数据发送了，即关闭本方数据流。 窗口大小（ 16 位）：数据字节数，表示从确认号开始，本报文的源方可以接收的字节数，即源方接收窗口大小。窗口大小是一个 16bit 字段，因而窗口大小最大为 65535 字节。 校验和（ 16 位）：此校验和是对整个的 TCP 报文段，包括 TCP 头部和 TCP 数据，以 16 位字进行计算所得。这是一个强制性的字段，一定是由发送端计算和存储，并由接收端进行验证。 紧急指针（ 16 位）：只有当 URG 标志置 1 时紧急指针才有效。TCP 的紧急方式是发送端向另一端发送紧急数据的一种方式。 选项：最常见的可选字段是最长报文大小，又称为 MSS(Maximum Segment Size) 。每个连接方通常都在通信的第一个报文段（为建立连接而设置 SYN 标志的那个段）中指明这个选项，它指明本端所能接收的最大长度的报文段。选项长度不一定是 32 位字的整数倍，所以要加填充位，使得报头长度成为整字数。 数据：TCP 报文段中的数据部分是可选的。在一个连接建立和一个连接终止时，双方交换的报文段仅有 TCP 首部。如果一方没有数据要发送，也使用没有任何数据的首部来确认收到的数据。在处理超时的许多情况中，也会发送不带任何数据的报文段。 4.2.2. 三次握手 TCP 是因特网的传输层协议，使用三次握手协议建立连接。在客户端主动发出 SYN 连接请求后，等待服务端回答 SYN+ACK，并最终对服务端的 SYN 执行 ACK 确认。这种建立连接的方式可以防止产生错误的连接，TCP 使用的流量控制协议是可变大小的滑动窗口协议。\nTCP 三次握手的过程如下：\n第一次握手：当客户端向服务端发起建立连接请求，客户端A会随机生成一个起始序列号x，然后客户端A会发送包含标志位 SYN＝1，序列号 seq = x(随机产生) 的数据包到服务端B，此时客户端A并进入 SYN-SEND 状态（第一次握手前客户端A的状态为CLOSE），服务端B的状态为 LISTEN。 第二次握手： 服务端B 收到客户端A请求报文后要确认联机信息，服务端B 由 SYN=1 可知，客户端A 要求建立联机。服务端B 会随机生成一个服务端的起始序列号y，向客户端A发送报文，其中包括标识位 SYN=1, ACK=1, 序列号 seq=y(随机产生), 确认号 ack=(客户端A的seq+1) 的数据包（注：其中 SYN=1 表示要和客户端建立一个连接，ACK=1 表示确认序号有效），此时服务端B进入 SYN-RCVD 状态（第二次握手前服务端B 的状态为 LISTEN，客户端A 的状态为 SYN-SENT）。 第三次握手：客户端A 收到服务端B 发来的报文后，会检查 ack 是否正确（即第一次发送的seq+1），以及标识位 ACK 是否为 1。若正确，客户端A 会再向服务端B 发送报文，其中包含 ACK=1, 序列号 seq=x+1, 确认号 ack=(服务端B的seq+1)，第三次握手后客户端和服务端的状态都进入 ESTABLISHED 状态（第三次握手前客户端A 的状态为 SYN-SENT）。在服务端B 收到后确认 seq 值与 ack=1 则连接建立成功。 在三次握手完成后，TCP 客户端和服务器端成功建立连接，可以进行数据传输。具体流程图：\nTODO: 待使用 draw.io 重新画图\n4.2.3. 四次挥手 TCP 建立连接要进行三次握手，而断开连接要进行四次。这是由于 TCP 的半关闭造成的。因为 TCP 连接是全双工的（即数据可在两个方向上同时传递），所以进行关闭时每个方向上都要单独进行关闭。这个单方向的关闭就叫半关闭。当一方完成它的数据发送任务，就发送一个 FIN 来向另一方通告将要终止这个方向的连接。\nTCP 断开连接既可以是由客户端发起，也可以是由服务器端发起。如果由客户端发起断开连接操作，则称客户端主动断开连接；如果由服务器端发起断开连接操作，则称服务端主动断开连接。下面以客户端发起关闭连接请求为例，说明 TCP 四次挥手断开连接的过程：\n客户端 A 应用进程调用断开连接的请求，向其 TCP 服务器 B 发送一个连接释放报文，其中包含终止标志位 FIN=1, seq=u 的消息，表示在客户端关闭链路前要发送的数据已经安全发送完毕并停止再发送数据，可以开始主动关闭 TCP 链路操作。此时客户端处于 FIN-WAIT-1（终止等待1）状态，然后等待服务器 B 确认关闭客户端到服务器的链路的操作。 服务器 B 收到这个 FIN (连接释放)报文段后，返回一个确认报文段 ACK=1，ack=u+1, seq=v 的消息给客户端 A，表示接收到客户端断开链路的操作请求。此时 TCP 服务器端进程通知高层应用进程释放客户端到服务器端的链路，服务器 B 处于 CLOSE-WAIT 状态，即半关闭状态（即 A 不可以发送给 B，但是 B 可以发送给 A。）。 客户端 A 在收到服务端 B 的确认释放信息后，处于 FIN-WAIT-2（终止等待2）状态，等待服务端 B 发送完数据与再次发出的连接释放报文段。 服务器端 B 将关闭链路前，再给客户端 A 进行最后的数据传送。在等待该数据发送完成后，会再次发送一个连接释放报文段，包含终止标志位 FIN=1, ACK=1, seq=w, ack=u+1 的消息给客户端 A，表示关闭链路前服务器需要向客户端发送的消息已经发送完毕，请求客户端确认关闭从服务器到客户端的链路操作。此时服务器端B 处于 LAST-ACK （最后确认）状态，等待客户端 A 最终确认断开链路。 客户端 A 在接收到这个最终 FIN 连接释放报文段后，会发送一个确认报文段 ACK=1, seq=u+1, ack=w+1 的消息给服务器端 B，表示接收到服务器端 A 的断开连接请求并准备断开服务器端 B 到客户端 A 的链路。此时客户端 A 处于 TIME-WAIT（时间等待）状态，但此时 TCP 连接还未释放，需要经过等待计时器设置的时间（2MSL，最大报文段生存时间）后，客户端 A 将进入 CLOSE 状态。服务器端 B 收到客户端 A 发出的确认报文段后关闭连接，若没收到客户端 A 发出的确认报文段，则服务器端 B 就会重传连接释放报文段。 TCP 四次挥手流程图：\nTODO: 待使用 draw.io 重新画图\n4.2.4. 相关面试题 4.2.4.1. 为什么不能两次握手就可以建立连接 第三次握手主要为了防止已失效的连接请求报文段突然又传输到了服务端，导致产生问题。\n比如客户端 A 发出连接请求，可能因为网络阻塞原因，A 没有收到确认报文，于是 A 再重传一次连接请求。连接成功，等待数据传输完毕后，就释放了连接。然后可能 A 发出的第一个连接请求等到连接释放以后，某个时间才到达服务端 B，此时 B 误认为 A 又发出一次新的连接请求，于是就向 A 发出确认报文段。\n如果不采用三次握手，只要服务端 B 发出确认，就建立新的连接了，此时客户端 A 不会响应服务端 B 的确认且不发送数据，则服务端 B 一直等待客户端 A 发送数据，造成资源的浪费。\n4.2.4.2. 第四次挥手时客户端 TIME_WAIT 状态为什么要等待 2MSL 保证 A 发送的最后一个 ACK 报文段能够到达 B。ACK 报文段有可能丢失，B 收不到该确认报文，就会超时重传连接释放报文段，然后 A 可以在 2MSL 时间内收到这个重传的连接释放报文段，接着 A 重传一次确认，重新启动 2MSL 计时器，确保最后 A 和 B 都进入到 CLOSED 状态；若 A 在 TIME-WAIT 状态不等待一段时间，而是发送完 ACK 报文段后立即释放连接，则无法收到 B 重传的连接释放报文段，所以不会再发送一次确认报文段，B 就无法正常进入到 CLOSED 状态。 防止已失效的连接请求报文段出现在本连接中。A 在发送完最后一个 ACK 报文段后，再经过 2MSL，就可以使这个连接所产生的所有报文段都从网络中消失，使下一个新的连接中不会出现旧的连接请求报文段。 4.2.4.3. 为什么释放连接时需要四次挥手 在请求连接时，当 Server 端收到 Client 端的 SYN 连接请求报文后，可以直接发送 SYN+ACK 报文。但是在关闭连接时，当 Server 端收到 Client 端发出的连接释放报文时，很可能并不会立即关闭 SOCKET，所以 Server 端先回复一个 ACK 报文，告诉 Client 端已收到连接释放报文了。只有等到 Server 端所有的报文都发送完了，这时 Server 端才能发送连接释放报文，之后两边才会真正的断开连接，因此需要四次挥手。\n因此四次挥手，目的是为了确保释放连接前所有数据全部发送完毕。\n4.3. TCP 编程 在 JDK 中提供了两个类用于实现 TCP 程序，一个是 ServerSocket 类，用于表示服务器端，一个是 Socket 类，用于表示客户端\n通信时，首先创建代表服务器端的 ServerSocket 对象，该对象相当于开启一个服务，并等待客户端的连接，然后创建代表客户端的 Socket 对象向服务器端发出连接请求，服务器端响应请求，两者建立连接开始通信。\n4.3.1. ServerSocket 类 4.3.1.1. 构造方法 1 public ServerSocket(int port) throws IOException 根据端口号创建服务器端。 API:创建绑定到特定端口的服务器套接字。\n4.3.1.2. 常用方法 1 public Socket accept() throws IOException 等待客户端连接并获得客户端的 Socket 对象。注意：此方法是同步的，即一直等待客户端连接，直到连接成功才能执行后续的代码。 API: 侦听并接受到此套接字的连接。此方法在连接传入之前一直阻塞。\n1 public InetAddress getInetAddress() 返回此服务器套接字的本地地址 1 public void close() throws IOException 关闭此套接字 4.3.2. Socket 类 4.3.2.1. Scoket 套接字 Socket 就是为网络编程提供的一种机制，又叫套接字编程。对于 Socket 需要理解以下几点内容：\n通信的两端都有 Socket。 网络通信其实就是 Socket 间的通信。 数据在两个 Socket 间通过 IO 传输。 4.3.2.2. 构造方法 1 public Socket(String host, int port); 使用该构造方法在创建 Socket 对象时，需要传递服务器字符串的IP地址和端口号。会根据参数去连接在指定地址和端口上运行的服务器程序，其中参数host接收的是一个字符串类型的IP地址。注意：构造方法只要运行，就会和服务器进行连接，如果服务器没有开启则抛出异常。 1 public Socket(InetAddress address, int port); 创建一个流套接字并将其连接到指定 IP 地址的指定端口号。参数 InetAddress address 用于接收一个 InetAddress 类型的对象，该对象用于封装一个IP地址。 4.3.2.3. 常用方法 1 public int getPort() 该方法返回一个 int 类型对象，该对象是 Socket 对象与服务器端连接的端口号。 1 public InetAddress getInetAddress() 获取客户端对象绑定的IP地址，返回套接字连接的地址。 1 public InetAddress getLocalAddress() 该方法用于获取 Socket 对象绑定的本地IP地址，并将IP地址封装成 InetAddress 类型的对象返回 1 public void close() throws IOException 该方法用于关闭 Socket 连接，结束本次通信。在关闭 Socket 之前，应将与 Socket 相关的所有的输入/输出流全部关闭，这是因为一个良好的程序应该在执行完毕时释放所有的资源 1 public InputStream getInputStream() throws IOException 该方法返回一个 InputStream 类型的字节输入流对象，如果该对象是由服务器端的 Socket 返回，就用于读取客户端发送的数据，反之，用于读取服务器端发送的数据 1 public OutputStream getOutputStream() throws IOException 该方法返回一个 OutputStream 类型的字节输出流对象，如果该对象是由服务器端的 Socket 返回，就用于向客户端发送数据，反之，用于向服务器端发送数据 1 public void shutdownOutput() throws IOException 向服务器写一个结束标记。禁用此套接字的输出流。 1 public void shutdownInput() throws IOException 此套接字的输入流置于“流的末尾”。 4.4. TCP 网络程序实现步骤 客户端服务器数据交换，必须使用套接字对象 Socket 中的获取的 IO 流，不能使用自己 new IO流的对象。\n4.4.1. TCP 客户端实现步骤 创建客户端 Socket 对象，指定要连接的服务器地址与端口号 调用 socket 对象的getOutputStream()方法获得字节输出流对象 通过字节输出流对象向服务器发送数据。 调用 socket 对象的getInputStream()方法获得字节输入流对象 通过字节输入流对象读取服务器响应的数据 关闭流资源 Socket 4.4.2. TCP 服务器端实现步骤 创建服务器 ServerSocket 对象，并指定服务器端口号 调用 ServerSocket 对象的accept()方法等待客户端连接并获得客户端的 Socket 对象 调用 socket 对象的getInputStream()方法获得字节输入流对象 通过字节输入流对象获得客户端发送的数据 调用 socket 对象的getOutputStream()方法获得字节输出流对象 通过字节输出流对象向客户端发送数据 关闭流资源 Socket 4.4.3. TCP模拟客户端与服务器代码案例 客户端上传文件到服务器 code demo\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; /* * TCP模拟客户端 */ public class TCPClient { public static void main(String[] args) throws IOException { // 创建客户端Socket对象 Socket sock = new Socket(\u0026#34;127.0.0.1\u0026#34;, 8000); // 创建上传的到服务器的字节输出流对象 OutputStream ops = sock.getOutputStream(); // 创建读取本地图片的字节输入流对象 File file = new File(\u0026#34;E:\\\\download\\\\Java学习路线图1.jpg\u0026#34;); FileInputStream fis = new FileInputStream(file); // 使用一次读取一个数组的方式将文件复制到服务器中 byte[] arr = new byte[1024]; int len; while ((len = fis.read(arr)) != -1) { ops.write(arr, 0, len); } //给服务器写终止序列 sock.shutdownOutput(); // 使用Socket对象获取字节输入流对象 InputStream ips = sock.getInputStream(); // 控制台输出接收的结果 while ((len = ips.read(arr)) != -1) { System.out.println(new String(arr, 0, len)); } // 关闭流对象 sock.close(); fis.close(); } } import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; /* * TCP模拟服务器 */ public class TCPServer { public static void main(String[] args) throws IOException { // 创建ServerSocket对象,要与客户端的端口一致 ServerSocket ss = new ServerSocket(8000); // 获取客户端端口 Socket sock = ss.accept(); // 使用Socket对象获取字节输入对象 InputStream ips = sock.getInputStream(); // 创建服务器复制文件的目标文件路径对象 File file = new File(\u0026#34;e:\\\\upload\u0026#34;); // 如果没有该文件夹就创建 if (!file.exists()) { file.mkdirs(); } System.out.println(file.isDirectory()); // 创建字节输出流对象，输出文件到服务器目标文件路径 FileOutputStream fos = new FileOutputStream(new File(file, \u0026#34;001.jpg\u0026#34;)); // 使用一次读取一个字节数组进行复制文件 byte[] arr = new byte[1024]; int len; while ((len = ips.read(arr)) != -1) { fos.write(arr, 0, len); } // 复制成功后，返回客户端消息“上传成功” // 使用Socket对象获取字节输出流对象 sock.getOutputStream().write(\u0026#34;上传成功！\u0026#34;.getBytes()); // 关闭流资源 ss.close(); sock.close(); fos.close(); } } 4.4.4. TCP服务端案例2 编写一个 TCP 的服务端，可以接受多个客户端的连接，当接收到用户的连接请求以后，就要把一张图片传回给客户端。\n增加功能：先判断客户是否要下载图片，选择后服务器端才传给客户端图片。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 package day16.test03; import java.io.BufferedOutputStream; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.net.Socket; import java.util.Scanner; /* * day16训练案例3 * 编写一个 TCP 的服务端，可以接受多个客户端的连接， * 当接收到用户的连接请求以后，就要把一张图片传回给客户端。 * 增加功能：先判断客户是否要下载图片，选择后服务器端才传给客户端图片。 */ public class TCPClient { public static void main(String[] args) { try { System.out.println(\u0026#34;客户端正在启动中........\u0026#34;); Thread.sleep(1500); // 提示用户登陆成功 System.out.println(\u0026#34;客户端成功启动！\u0026#34;); // 创建键盘录入对象 Scanner input = new Scanner(System.in); while (true) { System.out.println(\u0026#34;请选择你要的操作(1:下载资源，2:退出)：\u0026#34;); String s = input.nextLine(); // 让客户选择是否下载资源，如果不下载，服务器端不需要开启线程。 switch (s.trim()) { case \u0026#34;1\u0026#34;: // 创建客户端Socket对象 Socket socket = new Socket(\u0026#34;127.0.0.1\u0026#34;, 8000); // 创建字符输出流对象 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); bw.write(s); System.out.println(\u0026#34;正在下载资源\u0026#34;); for (int i = 0; i \u0026lt; 5; i++) { System.out.print(\u0026#34;.\u0026#34;); Thread.sleep(1000); } // 创建字节输出流，将文件保存在本机 File file = new File(\u0026#34;e:\\\\Java学习路线图\u0026#34; + System.currentTimeMillis() + \u0026#34;.jpg\u0026#34;); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); // 创建字节输入流对象 InputStream in = socket.getInputStream(); byte[] arr = new byte[1024]; int len = -1; while ((len = in.read(arr)) != -1) { bos.write(arr, 0, len); } System.out.println(\u0026#34;\\n图片下载成功到E盘中！\u0026#34;); // 关闭流资源 bos.close(); socket.close(); input.close(); System.exit(0); case \u0026#34;2\u0026#34;: // 关闭流资源 input.close(); System.exit(0); default: System.out.println(\u0026#34;你输入的信息错误，请重新输入！\u0026#34;); break; } } } catch (IOException | InterruptedException e) { e.printStackTrace(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package day16.test03; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; /* * day16训练案例3 * 编写一个 TCP 的服务端，可以接受多个客户端的连接， * 当接收到用户的连接请求以后，就要把一张图片传回给客户端。 * 增加功能：先判断客户是否要下载图片，选择后服务器端才传给客户端图片。 */ public class TCPServer { public static void main(String[] args) { // 创建服务器端对象 try { ServerSocket ss = new ServerSocket(8000); // 使用循环接受客户端的连接 while (true) { // 接收客户端Socket对象 Socket socket = ss.accept(); // 开启下载线程 new TCPServerThread(socket).start(); } } catch (IOException e) { e.printStackTrace(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package day16.test03; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.OutputStream; import java.net.Socket; /* * day16训练案例3 * 编写一个 TCP 的服务端，可以接受多个客户端的连接， * 当接收到用户的连接请求以后，就要把一张图片传回给客户端。 */ public class TCPServerThread extends Thread { // 创建集合存放ip地址 private static int count = 0; private Socket socket; public TCPServerThread(Socket socket) { this.socket = socket; } // 服务端线程 // 重写run方法 @Override public void run() { // 创建字节输入流对象读取服务器的文件 File file = new File(\u0026#34;E:\\\\Java学习路线图1.jpg\u0026#34;); try { BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); // 获取客户端的字节输出流对象，将文件输出到客户端 OutputStream out = socket.getOutputStream(); byte[] arr = new byte[1024]; int len = -1; while ((len = bis.read(arr)) != -1) { out.write(arr, 0, len); } // 给客户端写终止序列 socket.shutdownOutput(); System.out.println(\u0026#34;恭喜 \u0026#34; + socket.getInetAddress().getHostAddress() + \u0026#34; 同学，下载成功！！ 当前下载的人数是：\u0026#34; + ++count); // 释放流资源 bis.close(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } } 4.5. 滑动窗口机制（了解） TCP 利用滑动窗口实现流量控制。流量控制是为了控制发送方发送速率，保证接收方来得及接收。TCP 会话的双方都各自维护一个发送窗口和一个接收窗口。接收窗口大小取决于应用、系统、硬件的限制。发送窗口则取决于对端通告的接收窗口。\n接收方发送的确认报文中的 window 字段可以用来控制发送方窗口大小，从而影响发送方的发送速率。将接收方的确认报文window字段设置为 0，则发送方不能发送数据。\nTCP 头包含 window 字段，16bit 位，它代表的是窗口的字节容量，最大为 65535。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据，而不会导致接收端处理不过来。接收窗口的大小是约等于发送窗口的大小。\n4.6. TCP 相关问题 4.6.1. socket 分包的原因与造成问题 粘包、拆包发生的原因：\n要发送的数据大于TCP发送缓冲区剩余空间大小，将会发生拆包。 待发送数据大于MSS（最大报文长度），TCP 在传输前将进行拆包。 要发送的数据小于TCP 发送缓冲区的大小，TCP 将多次写入缓冲区的数据一次发送出去，将会发生粘包。 接收数据端的应用层没有及时读取接收缓冲区中的数据、将发生粘包。 socket 分包的情况会造成以下问题：\n数据缺失：如果接收方无法完整地接收到一个完整的数据包，就会导致数据缺失。这可能会导致应用程序出现错误或异常行为。 数据重组：如果接收方接收到了多个分包，就需要将它们组合在一起才能得到完整的数据。这个过程可能会非常复杂，尤其是在数据包非常大或复杂的情况下。 数据混淆：如果数据包被分成了多个分包并以不同的顺序到达接收方，就会导致数据混淆。这可能会导致数据被错误地解释或处理，从而导致应用程序出现错误或异常行为。 性能下降：如果数据包被分成多个分包并且接收方需要进行数据重组，就会导致性能下降。这可能会导致应用程序的响应时间变长，从而影响用户体验。 5. 网络编程模型 5.1. 阻塞 I/O 模型 阻塞 I/O 模型是最常见的 I/O 模型，在读写数据时客户端会发生阻塞。阻塞 I/O 模型的工作流程是：\n在用户线程发出 I/O 请求之后，内核会检查数据是否就绪，此时用户线程一直阻塞等待内存数据就绪。 在内存数据就绪后，内核将数据复制到用户线程中，并返回 I/O 执行结果到用户线程，此时用户线程才解除阻塞状态并开始处理数据。 典型的阻塞 I/O 模型的例子为 socket.read()，如果内核数据没有就绪，Socket 线程就会一直阻塞在 read() 中等待内核数据就绪。\n5.2. 非阻塞 I/O 模型 非阻塞 I/O 模型指用户线程在发起－个 I/O 操作后，无须阻塞便可以马上得到内核返回的一个结果。如果内核返回的结果为 false，则表示内核数据还没准备好，需要稍后再发起 I/O 操作。期间用户线程需要不断询问内核数据是否就绪，在内存数据还未就绪时，用户线程可以处理其他任务。一旦内核中的数据准备好了，并且再次收到用户线程的请求，内核就会立刻将数据复制到用户线程中并将复制的结果通知用户线程。\n5.3. 多路复用 I/O 模型 多路复用 I/O 模型是多线程并发编程用得较多的模型，Java NIO 就是基于多路复用 I/O 模型实现的。 在多路复用 I/O 模型中会有一个被称为 Selector 的线程不断轮询多个 Socket 的状态，只有在 Socket 有读写事件时，才会通知用户线程进行 I/O 读写操作。其模型有如下优势：\n阻塞 I/O 模型和非阻塞 I/O 模型需要为每个 Socket 都建立一个单独的线程处理数据；而多路复用 I/O 模型中只需一个线程就可以管理多个 Socket，并且在真正有 Socket 读写事件时才会使用操作系统的 I/O 资源，大大节约了系统资源。 非阻塞 I/O 模型在每个用户线程中都进行 Socket 状态检查；而在多路复用 I/O 模型中 是在系统内核中进行 Socket 状态检查的 多路复用 I/O 模型通过在一个 Selector 线程上以轮询方式检测在多个 Socket 上是否有事件到达，并逐个进行事件处理和响应。因此如果事件响应体（消息体）很大时，Selector 线程就可能出现性能瓶颈的问题，导致后续的事件处理很慢。在实际应用中，在多路复用方法体内一般不建议做复杂逻辑运算，只做数据的接收和转发，将具体的业务操作转发给后面的业务线程处理。\n5.4. 信号驱动 I/O 模型 在信号驱动 I/O 模型中，在用户线程发起一个 I/O 请求操作时，系统会为该请求对应的 Socket 注册一个信号函数，然后用户线程可以继续执行其他业务逻辑；在内核数据就绪时，系统会发送一个信号到用户线程，用户线程在接收到该信号后，会在信号函数中调用对应的 I/O 读写操作完成实际的 I/O 请求操作。\n5.5. 异步 I/O 模型 异步 I/O 需要操作系统的底层支持，在 Java 7 中提供了 Asynchronous I/O 操作。\n在异步 I/O 模型中，用户线程会发起一个 asynchronous read 操作到内核，内核在接收到 synchronous read 请求后会立刻返回一个状态，用于说明请求是否成功发起，在此过程中用户线程不会发生任何阻塞。然后内核会等待数据准备完成并将数据复制到用户线程中，在数据复制完成后内核会发送一个信号到用户线程，通知用户线程 asynchronous 读操作已完成。\n输入与输出操作的两个阶段（请求的发起、数据的读取）都是在内核中自动完成的，用户线程只需发起一个请求，内核最终发送一个信号告知用户线程 I/O 操作已经完成。在接收到内核返回的成功或失败信号时即说明 I/O 操作已经完成，用户直接使用内存写好的数据即可，不需要再次调用 I/O 函数进行具体的读写操作，因此在整个过程中用户线程不会发生阻塞。\n异步 I/O 模型与 信号驱动 I/O 模型的区别是：\n信号驱动模型，用户线程接收到信号便表示数据已经就绪，需要用户线程调用 I/O 函数进行实际的 I/O 读写操作，将数据读取到用户线程； 异步 I/O 模型，用户线程接收到信号便表示 I/O 操作已经完成（数据己经被复制到用户线程），用户可以开始使用该数据了 。 6. 网络编程相关 API 6.1. InetAddress 类 6.1.1. 概述 Java 中可以使用 InetAddress 类表示互联网协议（IP）地址。一个 InetAddress 对象就对应一个 IP 地址。\n6.1.2. 常用方法 6.1.2.1. 静态方法 1 2 3 4 5 public static InetAddress getLocalHost(); // 获取本地主机IP地址对象。直接输出：“主机名/ip地址” public static InetAddress getByName(String host); // 依据主机名（IP地址字符串/域名）获取主机IP地址对象。 6.1.2.2. 非静态方法 1 2 3 4 public String getHostName(); // 获取主机名称 public String getHostAddress(); // 获取主机字符串形式的IP 6.1.3. InetAddress 类 Code Demo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import java.net.InetAddress; import java.net.UnknownHostException; /* * Inetaddress 类 */ public class MoonZero { public static void main(String[] args) throws UnknownHostException { // 获取本机IP地址值对象 InetAddress inet = InetAddress.getLocalHost(); // 获取本机名称和IP地址 System.out.println(inet); // Zero/192.168.83.21 String name = inet.getHostName(); System.out.println(name); // Zero String ip = inet.getHostAddress(); System.out.println(ip); // 192.168.83.21 // 以主机的名称获取其他主机的对象 InetAddress inetOther = InetAddress.getByName(\u0026#34;2011-20120210FQ\u0026#34;); // 获取其他主机的名称和IP地址 System.out.println(inetOther); // 2011-20120210FQ/192.168.83.64 } } 6.2. InetSocketAddress 6.2.1. 概述 在使用 Socket 来连接服务器时最简单的方式就是直接使用 IP 和端口，但 Socket 类中并未提供这种方式，可以使用 SocketAddress 的子类 java.net.InetSocketAddress 来实现IP 地址+端口号的创建，不依赖任何协议。\n1 public class InetSocketAddress extends SocketAddress 7. 网络协议 7.1. 动图解释 8 种热门网络协议 HTTP（超文本传输协议）：是用于获取 HTML 等资源的协议，它使用 TCP 协议作为底层的支撑协议，它是 Web 上任何数据交换的基础，是一种客户端-服务器协议。使用场景：浏览器。 HTTP/3：是 HTTP 的第三个版本，它使用QUIC作为底层的支撑协议，QUIC 是一种为移动互联网使用而设计的新传输协议。它依赖于 UDP 而不是 TCP，这使得网页响应速度更快，可以实现虚拟现实应用，用更多的带宽来渲染虚拟场景的复杂细节。使用场景：物联网（IOT）、虚拟现实。 HTTPS（超文本传输协议安全版）：是 HTTP 协议的安全版本，扩展了 HTTP，并使用加密进行安全通信，主打一个「安全」。使用场景：浏览器、网上银行、网上支付。 WebSocket：是一种基于 TCP 协议的全双工通信协议，与传统的 HTTP 通信不同，WebSocket 允许服务器主动向客户端推送数据，而不需要等待客户端的请求。使用场景：实时聊天、视频会议、股票行情。 TCP（传输控制协议）：是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 是互联网的基础，用于在互联网上传输各种类型的数据，包括文本、图像、音频、视频等，许多应用层协议都建立在 TCP 之上。使用场景：浏览器、文件传输、邮件。 UDP（用户数据报协议）：是一种面向无连接的、不可靠的、基于数据报的传输层通信协议。UDP 是 TCP 的补充，UDP 提高了数据传输的速度，但是可能会丢失某些数据，用于那些对可靠性要求不高的应用场景。使用场景：视频流媒体、网络游戏、实时监控。 SMTP（简单邮件传输协议）：是一个标准协议，是电子邮件传递的基础，用于在互联网上发送和接收电子邮件。使用场景：电子邮件。 FTP（文件传输协议）：是文件传输协议，用于在客户端和服务器之间传输计算机文件，FTP 是文件传输的基础，用于在不同计算机之间共享文件。使用场景：文件传输。 7.2. TCP 或 UDP 的应用层协议 基于 TCP 的协议：\nHTTP（Hypertext Transfer Protocol，超文本传输协议），主要用于普通浏览。 HTTPS（HTTP over SSL，安全超文本传输协议）,HTTP协议的安全版本。 FTP（File Transfer Protocol，文件传输协议），用于文件传输。 POP3（Post Office Protocol, version 3，邮局协议），收邮件用。 SMTP（Simple Mail Transfer Protocol，简单邮件传输协议），用来发送电子邮件。 TELNET（Teletype over the Network，网络电传），通过一个终端（terminal）登陆到网络。 SSH（Secure Shell，用于替代安全性差的TELNET），用于加密安全登陆用。 基于 UDP 的协议：\nBOOTP（Boot Protocol，启动协议），应用于无盘设备。 NTP（Network Time Protocol，网络时间协议），用于网络同步。 DHCP（Dynamic Host Configuration Protocol，动态主机配置协议），动态配置IP地址。 基于 TCP 和 UDP 的协议：\nDNS（Domain Name Service，域名服务），用于完成地址查找，邮件转发等工作。 ECHO（Echo Protocol，回绕协议），用于查错及测量应答时间（运行在TCP和UDP协议上）。 SNMP（Simple Network Management Protocol，简单网络管理协议），用于网络信息的收集和网络管理。 DHCP（Dynamic Host Configuration Protocol，动态主机配置协议），动态配置IP地址。 ARP（Address Resolution Protocol，地址解析协议），用于动态解析以太网硬件的地址。 ","permalink":"https://ktzxy.top/posts/0z2x4gi6rb/","summary":"Java基础 网络编程","title":"Java基础 网络编程"},{"content":"Vimium 教程 所有快捷键可以通过?查看.\n[TOC]\n最常用的快捷键 向下/上/左/右移动 j/k/h/l 向下/上跳动 d/u 回到顶/尾部 gg/G 窗口打开模式 本窗口/新窗口 f/F 查找历史记录+书签 o/O 关闭/恢复标签 x/X 查找书签 b/B（当前/新窗口打开） 选择左/右标签 J/K 搜索剪贴板关键字 在当前/新窗口 p/P 跳转到当前url上一级/最高级 gu/gU 创建/查看标签页 t/T 将焦点聚集在第一个输入框 gi (2gi就是第二个输入框) 刷新 r 新标签中打开多个链接 \u0026lt;a-f\u0026gt; 即：alt+f 开/关静音 \u0026lt;a-m\u0026gt;即：alt+m 固定标签栏 \u0026lt;a-p\u0026gt;即 alt+p 上一个标签 ^ 其他不常用快捷键 查找（不支持中文） / 向下/上查找结果 n/N (回车后直接打开链接，不用再使用f/F定位) 复制当前链接 yy 新模式 i 查看源码 gs 查看所以快捷键 ? 编辑当前地址栏 g+e/E 并在当前/新窗口中打开 复制当前标签页 yt 移动当前标签到左/右侧边 \u0026lt;\u0026lt;/\u0026gt;\u0026gt; 滚动到页面最左/右边（在有滚动条下才有效果） zH/zL 插入模式 i（可以屏蔽掉vimium快捷键，使其不和网页默认快捷键冲突） 将标签页移动到新窗口 W 创建新标记（可创建多个 m 使用方法 设置当前/全局滚动条位置 m+小/大写字母 跳转到设置的滚动位置 ~+字母 切换到复制模式 v 删除/修改命令 删除指定命令 unmap j 删除命令j unmapAll 删除所有命令 修改命令（后面的命令参数可以在选择里面打开 show available commands找到） map a LinkHints.activateMode 把a定义原来f的功能 map f scrollPageDown 把f定义成原来d的功能 本地文件中使用vimium 打开chrome插件设置页面，勾选\u0026quot;允许访问文件网址\u0026quot; 快捷键访问任意指定网站 以快捷打开 B 站为例，实现按下**「zb」**进入 B 站。操作如下\nOptions → Custom Key Mapping 内输入 :\n命令： map \u0026lt;hotkey\u0026gt; createTab \u0026lt;yoursite\u0026gt; 举例：map zb createTab https://t.bilibili.com/ 快捷键打开指定网站的站内搜索 以知乎站内搜索为例，实现快捷键**「zh」**进入知乎站内搜索框。操作如下\n搜索引擎的配置方法：\n打开你想映射的网站 → 找到该网站搜索框 → 搜索任意内容回车后 → 复制网址 → 进入 Options → Custom Search Engine 编辑框→ 修改网址以匹配以下命令\n1 2 3 命令：\u0026lt;keyword\u0026gt;: https://yoursite.com/XXXXX=%s \u0026lt;sitename\u0026gt; 举例：zh: https://www.zhihu.com/search?type=content\u0026amp;q=%s 知乎 热键映射的配置方法（在 Options → Custom Mapping Key 编辑框）：\n1 2 3 命令：map \u0026lt;hotkey\u0026gt; Vomnibar.activateInNewTab keyword=\u0026lt;keyword\u0026gt; 举例：map zh Vomnibar.activateInNewTab keyword=zh 以上两条规则中\u0026lt; keyword\u0026gt; 是同一个值 zh 。\np.s. 如果某个键已经被赋予了命令（如“r”默认刷新网页），那么就无法设置以它开头的快捷键，如“rb”：当你按下“r”时，直接实现网页刷新，后续按键无效。\n所以建议使用以下冷门按键作为快捷键前缀：、、y、z 。使用 Alt 和 Ctrl 尤其要留意热键是否已被占用。\n选定和复制粘贴 直接使用视觉模式复制 按v进入visual mode，第一次进入 visual mode 时因为没有选中内容转入caret mode 光标模式。\n关于Caret mode光标模式：在 visual mode下， 按 c (c for caret) 就转成Caret mode 同时取消之前选中.\n按v 如果右下角显示visual mode就可以hjkl选择了, 选中待复制内容后，按 y (y for yank) 可以复制，按 \u0026lt;Enter\u0026gt;也可以复制\n使用搜索+视觉模式 搜索方法搜索起始点：/搜索内容 按 Enter。 启用视觉模式：v，按行的视觉模式：Shift + V 使用 vim 导航键选择文本：h、j、k、l、b、e、w、$（我特别喜欢 Shift + w，因为这个组合键可以按单词选择） 使用 y 将选择的文本复制到剪贴板。 切换程序，使用 Ctrl+V，将文本粘贴到其他程序中。 一些快键的修改 参考 Chrome神器Vimium的使用和配置.\n","permalink":"https://ktzxy.top/posts/8zzgdsihkq/","summary":"Vimium学习笔记","title":"Vimium"},{"content":"HTTP中的状态码 状态码的作用 状态码的职责是当客户端向服务器发送请求时，描述返回的请求结果，借助状态码，用户可以知道服务器端是正常处理了请求，还是出现了错误。\n类别 原因 1XX 信息性状态码 接收到的请求正在处理中 2XX 成功状态码 请求正常处理完毕 3XX 重定向状态码 需要附加操作以完成请求 4XX 客户端错误码 服务器无法处理请求 5XX 服务器错误码 服务器处理请求出错 2XX成功 200 OK 表示客户端发送来的请求服务端正在处理了\n204 No Content 该状态码表示客户端进行了范围请求，而服务器成功执行了这部分的GET请求。响应报文中包含Content-Range指定范围的实体内容\n3XX重定向 301 Moved Permanently 永久性重定向，该状态码表示请求的资源已经被分配了新的URI，以后应使用资源现在所指的URI\n302 Found 临时重定向，该状态码表示请求的资源已经分配了新的URI，希望用户本次使用新的URI访问。和301类似，但是该状态码表示资源不是永久性被移动，只是短暂的\n303 See other 该状态码表示由于请求对应的资源存在另一个URI，应使用GET方法定向获取请求的资源\n304 Not Modified 该状态码表示客户端发送附带条件的请求时，服务端允许访问资源，但未满足条件的情况。虽然被划分在3XX中，但是和重定向没有任何关系。\n4XX客户端错误 400 Bad Request 该状态码表示请求报文中存在语法错误，需要修改请求的内容后再次发送请求\n401 Unauthorized 该状态码表示发送的请求需要HTTP认真信息或者用户认证失败\n403 Forbidden 该状态码表示请求资源的访问被服务器拒绝了\n404 Not Found 该状态码表示服务器上无法找到请求的资源\n405 Method Not Allowed 该状态码表示请求的方法不被允许\n5XX服务器错误 500 Internal Server Error 该状态码表示服务器在执行请求时发生了错误，也可能是web应用存在BUG和某些临时的故障\n503 Service Unavailable 该状态码表示服务器暂时处于超负荷或正在停机维护，现在无法处理请求。\n","permalink":"https://ktzxy.top/posts/j3ohptqg5c/","summary":"http中的状态码","title":"http中的状态码"},{"content":"Java 集合框架支持两种不同类型的集合：\nCollection（单列集合） Map（双列集合） Notes:\n本笔记所有方法和示例基于 jdk1.8 JDK 提供的线程安全的并发容器，如：BlockingQueue 等，详见《并发编程 - 并发容器》笔记 1. Collection 接口（单列集合） 1.1. 概述 Collection 是所有单列集合的父接口（父类），集合的项层的接口。Collection 接口本身是没有索引的，但它的子体系中有支持重复的，唯一的，有序的，无序的等不同类型的实现。Collection 框架支持以下三种主要类型：\nSet（规则集）：用于存储一组不重复的元素。 List（线性表）：用于存储一个由元素构成的有序集合。 Queue（队列）：用于存储先进先出方式处理的对象。 Tips: 这些集合的通用特性都被定义在 java.util.Collection 接口中，相应的集合类型的特性定义以上各自的接口中，并提供了各自不同实现类来实现具体的功能。\n1.2. Collection 继承体系图 Collection 是所有单列集合的直接或间接接口，其指定了所有集合应该具备的基本功能。\nList 接口：元素可重复，有序，带索引。 ArrayList(重要)：底层是数组结构。ArrayList 的出现替代了 Vector，增删慢，查找快。 LinkedList(重要)：底层是链表结构。同时对元素的增删操作效率很高。 Set 接口： 元素不能重复，无序，没有索引。 HashSet(重要)：底层是哈希表结构。在不重复的基础上无序。 LinkedHashSet：底层是哈希表结构结合链表结构。在不重复的基础上可预测迭代顺序。 单列集合体系图\n1.3. Collection 集合接口的常用方法 1 Collection\u0026lt;String\u0026gt; c = new ArrayList\u0026lt;\u0026gt;(); Collection 是接口，定义了集合相关的方法。其实 ArrayList 等实现类就是实现 Collection 的以下的方法\n1.3.1. 添加元素 1 boolean add(E e); 添加一个元素 1 boolean addAll(Collection\u0026lt;? extends E\u0026gt; c); 按照指定 collection 的迭代器所返回的元素顺序，将该 collection 中的所有元素添加到此列表的尾部。 1.3.2. 删除元素 1 boolean remove(Object o); 移除此集合中首次出现的指定元素（如果存在），返回删除是否成功(true/false)。删除元素是影响本来的集合。 1 boolean removeAll(Collection\u0026lt;?\u0026gt; c); 移除此集合中所有包含在指定的集合中所有元素（如果存在），返回删除是否成功(true/false) 1.3.3. 获取集合信息 1 int size(); 返回集合中的元素的个数 1.3.4. 清空集合 1 void clear(); 移除此集合中的所有元素。此调用返回后，集合将为空。 1.3.5. 判断功能 1 boolean isEmpty(); 判断集合是否为空。如果此列表中没有元素，则返回 true。 1 boolean contains(Object o); 判断此集合中是否包含指定的元素，包含则返回 true。 1.3.6. 类型转换功能 1 Object[] toArray(); 按适当顺序（从第一个到最后一个元素）返回包含此集合中所有元素的数组。 1.4. 综合示例 Code Demo:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package day07; import java.util.ArrayList; import java.util.Collection; /* * 关卡1训练案例 1 * Collection 基本用法 */ public class Test1_01 { public static void main(String[] args) { // 创建Collection对象，因Collection是接口，只能通过子类去创建对象 Collection\u0026lt;String\u0026gt; c = new ArrayList\u0026lt;String\u0026gt;(); // 往集合中添加对象元素 c.add(\u0026#34;a\u0026#34;); c.add(\u0026#34;b\u0026#34;); c.add(\u0026#34;c\u0026#34;); c.add(\u0026#34;d\u0026#34;); c.add(\u0026#34;e\u0026#34;); // 输出集合[a, b, c, d, e] System.out.println(c); // 删除元素 c.remove(\u0026#34;c\u0026#34;); // remove不存在的元素，不报错，没有效果 c.remove(\u0026#34;f\u0026#34;); //输出集合[a, b, d, e] System.out.println(c); // 获取集合大小：4 System.out.println(\u0026#34;Collection集合的大小是：\u0026#34; + c.size()); // 清空集合+判断集合是否是空 输出 false System.out.println(\u0026#34;Collectiong集合是否为空：\u0026#34; + c.isEmpty()); c.clear(); //输出true System.out.println(\u0026#34;Collectiong集合是否为空：\u0026#34; + c.isEmpty()); // 输出集合内容 [] System.out.println(c); } } 2. List 接口 List 接口用来定义有序(存取顺序一致)，有索引，元素可重复的集合\n2.1. 接口概述 1 public interface List\u0026lt;E\u0026gt; extends Collection\u0026lt;E\u0026gt; List 是一个元素存取有序、带有索引、并且可以存储重复元素的集合。例如：存元素的顺序是11、22、33。那么集合中，元素的存储就是按照11、22、33的顺序完成的；通过索引就可以精确的操作集合中的元素（与数组的索引是一个道理）\n由于 List 集合拥有索引，因此 List 集合迭代方式除了使用迭代器之外，还可以使用索引进行迭代。遍历方法分别是：普通for，增强for，迭代器。\n2.2. List 集合存储数据的结构 List 接口下有很多个集合实现，它们存储元素所采用的结构方式是不同的，这样就导致了这些集合有它们各自的特点，供给开发者在不同的环境下进行使用。接口的常见实现类\nArrayList LinkedList Vector 2.3. 接口常用的方法 2.3.1. 增加元素 1 boolean add(E e); 向集合末尾处，添加指定的元素 1 void add(int index, E element); 向集合指定索引处，添加指定的元素，原有元素依次后移 1 boolean addAll(int index, Collection\u0026lt;? extends E\u0026gt; c); 从指定的位置开始，将指定 collection 中的所有元素插入到此列表中。向右移动当前位于该位置的元素（如果有）以及所有后续元素（增加其索引）。新元素将按照指定 collection 的迭代器所返回的元素顺序出现在列表中。 2.3.2. 删除元素 1 boolean remove(Object e); 将指定元素对象，从集合中删除，返回值为被删除的元素 1 boolean remove(int index); 从集合中删除将指定索引处的元素，并返回被删除的元素。值得注意的是，若调用方法时传入的参数 index 是 Integer 类型的，调用的是 remove(Object object) 方法，而不是 remove(int index)，所以会出现无法删除指定的索引处的元素的情况，传入的一定要是基本数据类型哦! 1 void clear(); 清空集合 2.3.3. 替换元素 1 E set(int index, E element); 将指定索引处的元素，替换成指定的元素，返回值为替换前的元素 2.3.4. 获取元素 1 E get(int index); 获取指定索引处的元素，并返回该元素。(获取元素是不会改变本来的集合) 1 int indexOf(Object o); 返回此集合中首次出现的指定元素的索引；如果此集合不包含元素，则返回 -1。 1 int lastIndexOf(Object o); 返回此集合中最后一次出现的指定元素的索引；如果此集合不包含索引，则返回 -1。 2.4. ArrayList 2.4.1. 简介 1 2 public class ArrayList\u0026lt;E\u0026gt; extends AbstractList\u0026lt;E\u0026gt; implements List\u0026lt;E\u0026gt;, RandomAccess, Cloneable, java.io.Serializable ArrayList 集合底层数据存储的结构是数组结构。数组实现的特点：元素查询快，增删慢，线程不安全（效率高）。ArrayList 是一个长度可变的高级的数组，可以在集合中存储任意对象类型的数据，集合本身也是一个对象。由于日常开发中使用最多的功能为查询数据、遍历数据，所以 ArrayList 是最常用的集合。\nNotes: 集合只能存储对象（引用类型）的数据，不能存在基本数据类型。\n需要注意线程安全性：对 ArrayList 的操作一般分为两个步骤，改变位置(size)和操作元素(e)。所以这个过程在多线程的环境下是不能保证具有原子性的，因此 ArrayList 在多线程的环境下是线程不安全的。\nTips: 开发时随意地使用 ArrayList 完成任何需求，并不严谨，这种用法不提倡。\n2.4.2. RandomAccess 接口 Java Collections 框架中提供了一个 RandomAccess 接口，用来标记 List 实现是否支持 Random Access。\n1 2 public interface RandomAccess { } 如果一个数据集合实现了该接口，就意味着它支持 Random Access，按位置读取元素的平均时间复杂度为 O(1)，如 ArrayList 如果没有实现该接口，表示不支持 Random Access，如 LinkedList 2.4.3. ArrayList 优缺点 优点：\n因为其底层是数组，所以修改和查询效率高。 可自动扩容(1.5倍)。 缺点：\n插入和删除效率不高。 线程不安全。 ArrayList 比较适合顺序添加、随机访问的场景。\n2.4.4. ArrayList 的特有方法 1 protected void removeRange(int fromIndex, int toIndex) 重写 AbstractList 抽象父类的方法，移除集合中索引在 fromIndex（包括）和 toIndex（不包括）之间的所有元素。向左移动所有后续元素（减小其索引）。此调用将列表缩短了 (toIndex - fromIndex) 个元素。如果 toIndex == fromIndex，则此操作无效。 2.4.5. 集合的遍历示例 ArrayList 集合的遍历最经典是，通过 size() 和 get() 配合实现的\n1 2 3 4 5 // 最标准的用法 for (int x = 0; x \u0026lt; array.size(); x++) { String s = array.get(x); // 目的是为了以后能再对集合里的元素进行使用 System.out.println(s); } 2.4.6. ArrayList 底层原理 2.4.6.1. ArrayList 数组实现的原理 数组实现的特点：查询快，增删慢，线程不安全（效率高）。原因：\n查询快：由于数组的索引支持，那么可以通过索引直接计算出元素的地址值，因此就可以直接通过元素的地址值获取到指定的元素 增删慢：由于在添加元素的时候，实际上底层会先创建一个新数组(新数组的长度为原数组的长度+1)，那么在添加新元素的时候，先需要对数组中原有的数据进行拷贝，其次在末尾进行添加新的元素。因此，这样操作的效率的极低的(删除元素刚好和添加的操作相反) Tips: 增删慢的情况是基于，数组的原长度不够，并且非在数组尾部插入数据的情况。若数组的长度足够并且在尾部插入新的元素，其他操作的效率甚至比链表更快。\n2.4.6.2. ArrayList 的 contains 方法判断元素（自定义类型）是否存在的原理 ArrayList 的 contains 方法，会调用方法传入的元素的 equals 方法依次与集合中的旧元素所比较，从而根据返回的布尔值判断是否有重复元素。\n当 ArrayList 存放自定义类型时，由于自定义类型在未重写 equals 方法前，判断是否重复的依据是比较对象的地址值，所以如果想根据内容判断是否为重复元素，需要重写元素的 equals 方法\n2.5. LinkedList 2.5.1. 概述 LinkedList 集合底层数据存储的实现是双向链表结构，查询慢，增删快，线程不安全（效率高）。\nNotes: 数据结构基础之双向链表。双向链表也叫双链表，是链表的一种，它的每个数据结点中都有两个指针，分别指向直接后继和直接前驱。所以，从双向链表中的任意一个结点开始，都可以很方便地访问它的前驱结点和后继结点。\nLinkedList 与 ArrayList 不同的是，在对 LinkedList 进行插入和删除操作时，只需在对应的节点上插入或删除元素，并将前后的节点元素的指针指向该节点即可，数据不需要进行复制移动，因此随机插入和删除效率很高。\nLinkedList 还提供了在 List 接口中未定义，用于操作链表头部和尾部的元素的方法，因此有时也可以被当作堆栈、队列或双向队列使用。\n2.5.2. LinkedList 链表实现的原理 链表结构：查询慢，增删快，线程不安全（效率高）。其原因：\n查询慢：由于不能直接找到元素的地址，需要上一个元素推导出下一个元素的地址，因为在进行随机访问时，需要从链表头部一直遍历到要查找的节点为止，这种查询速度较慢。 增删快：在添加的时候，只需要更改元素所记录的地址值即可 链表查询元素是判断索引是否大于集合元素个数的一半来决定从表头还是表尾开始查询。如果大于一半，则从表尾开始查找，否则从表头开始查找。\n2.5.3. LinkedList 常用特有方法 1 void addFirst(E e); 将指定元素添加到链表头 1 void addLast(E e); 将指定元素添加到链表尾 1 E removeFirst() 移除并返回此列表的第一个元素。 1 E removeLast() 移除并返回此列表的最后一个元素。 1 E getFirst(); 返回此列表的第一个元素。 1 E getLast(); 返回此列表的最后一个元素。 2.6. Vector（了解） Vector 集合数据存储的结构是数组结构，为 JDK 中最早提供的集合。\n1 2 3 public class Vector\u0026lt;E\u0026gt; extends AbstractList\u0026lt;E\u0026gt; implements List\u0026lt;E\u0026gt;, RandomAccess, Cloneable, java.io.Serializable Vector 中提供了一个独特的取出方式，就是枚举 Enumeration，它其实就是早期的迭代器。此接口 Enumeration 的功能与 Iterator 接口的功能是类似的。Vector 集合已被 ArrayList 替代。枚举 Enumeration 已被迭代器 Iterator 替代。\nVector 最大的特点是线程安全但效率低，因为 Vector 类的所有方法（如 add、set、delete 等）均是 synchronized 修饰的同步方法，在多线程访问的情况下，只允许一个线程进行增删改操作。后面已经不再建议使用，而 Arraylist 不是同步的，其效率比较高，在不需要保证线程安全时建议使用 Arraylist。\n2.7. Stack 栈结构（了解） 2.7.1. 概述 java 提供了一个专门用于栈结构的类：java.util.Stack\n1 public class Stack\u0026lt;E\u0026gt; extends Vector\u0026lt;E\u0026gt; Stack 类表示后进先出（LIFO）的对象堆栈。\n2.7.2. 常用方法 1 public E peek(); 查看堆栈顶部的对象，但不从堆栈中移除它。 1 public E push(E item); 把项压入堆栈顶部。(压栈) 1 public E pop(); 移除堆栈顶部的对象，并作为此函数的值返回该对象。(弹栈) 2.8. 集合与数组之间的转换 2.8.1. 集合转数组( List 类方法) 集合（如：ArrayList）转数组使用的是，Collection 接口的 toArray() 方法，ArrayList 和 LinkedList 都有承继该方法。\n方式一：\n1 public Object[] toArray() 将集合内容转成一个对象数组。如果需要下一步操作，需要进行向下转型。按适当顺序（从第一个到最后一个元素）返回包含此列表中所有元素的数组。 1 2 3 ArrayList\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); Object[] arr = list.toArray(); String str = (String) arr[0]; 方式二：\n1 public \u0026lt;T\u0026gt; T[] toArray(T[] a) 将集合中的元素添加至指定的数组中。按适当顺序（从第一个到最后一个元素）返回包含此列表中所有元素的数组；返回数组的运行时类型是指定数组的运行时类型。 如果传入的数组的长度大于等于集合元素的个数时，则方法内部直接将集合中元素添加到指定的数组中，并返回该数组。 如果传入的数组的长度小于集合元素的个数时，则方法内部会创建一个新的数组，新数组的类型与传入的数组类型一致，新数组的长度等于集合元素的个数，并且将集合中的元素添加到新数组中，返回新的数组。 1 2 3 ArrayList\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); String[] strs = new String[list.size()]; list.toArray(strs);\t2.8.2. 数组转集合（使用 Arrays 工具类方法） 1 public static \u0026lt;T\u0026gt; List\u0026lt;T\u0026gt; asList(T... a); Arrays 工具类的静态方法，将数组的内容添加到一个集合中，并返回集合对象。。方法的形参是可变参数类型，可变参数本质也是数组，传入实际数组，将该数组转成集合返回\nNotes: 通过 asList 方法将数组转成集合之后，获得的集合是固定大小的集合，不支持向集合中添加或删除元素。，否则会抛出 UnsupportedOperationException 异常\n可以使用有参数构造方法，将其转成可以增删的新的集合\n1 2 ArrayList\u0026lt;String\u0026gt; array = Arrays.asList(arr); ArrayList\u0026lt;String\u0026gt; newArray = new ArrayList\u0026lt;String\u0026gt;(array); // newArray 是可以增删的。 2.8.3. 集合（List）与数组（Array）的区别与选择 两者的区别是：\n长度不同：组的长度是固定的；集合的长度是可变的。 存储元素的数据类型不同：集合只能存储对象（引用类型）的数据，不能存在基本数据类型；而数组既可以存基本类型的元素，也可以存引用类型的元素。 获取长度的方法不同：数组是通过 length 属性；而集体是通过 size() 方法。 通常情况下都会选择集合(如：ArrayList)，因为数据没有提供像集合(List)的那么功能，如：addAll、removeAll、iterator 等。但也有些情况选择数组（Array）比较好用：\n如果列表的大小已经指定并且不会变化，大部分情况下是存储和遍历它们。 对于遍历基本数据类型，尽管集合会对基本类型使用自动装箱，但在处理指定大小的基本类型列表时，效率会较低。 对于需要使用多维数组，使用 [][] 比 List\u0026lt;List\u0026lt;xx\u0026gt;\u0026gt; 更容易。 2.9. List 接口相关实现类总结 2.9.1. ArrayList 和 LinkedList 的区别与选择 区别：\n数据结构实现：ArrayList 是动态数组的数据结构实现；而 LinkedList 是双向链表的数据结构实现。 随机访问效率：ArrayList 比 LinkedList 在随机访问的时候效率要高，因为 LinkedList 是线性的数据存储方式，所以需要移动指针从前往后依次查找。 增加和删除效率： 在非首尾的增加和删除操作时，LinkedList 和 ArrayList 的时间复杂度是 O(n)。LinkedList 需要遍历链表，但 LinkedList 要比 ArrayList 效率要高，因为 ArrayList 的扩容机制的存在，增删操作时超出存储长度时，需要新建数组，再将原数组中的数据复制到新数组中，并且会影响数组内的其他数据的下标。 在 ArrayList 尾部插入和删除，时间复杂度是O(1)；LinkedList 头尾节点增删时间复杂度也是 O(1)。 如果在 ArrayList 指定了合适的初始容量，并且使用尾部插入数据（没有触发数组的扩展）时，会极大提升性能，甚至超过 LinkedList（增删操作还需要创建大量的 node 对象）的效率 内存空间占用：LinkedList 比 ArrayList 更占内存，因为 LinkedList 的节点除了存储数据，还存储了两个引用，一个指向前一个元素，一个指向后一个元素。 内存存储区域：ArrayList 是连续内存存储；而 LinkedList 分散在内存中。 迭代器性能：在迭代操作时，ArrayList 使用普通迭代器或增强 for 循环的性能比 LinkedList 更优。因为 ArrayList 的数据存储在连续的内存中，迭代时可以直接访问内存，而 LinkedList 需要通过遍历链表来访问每个元素。 线程安全：ArrayList 和 LinkedList 都是不同步的，也就是不保证线程安全。如果需要保证线程安全，有两种方案： 在方法内使用，局部变量则是线程安全的 使用 Collections 工具类包装成线程安全的 ArrayList 和 LinkedList 1 2 List\u0026lt;Object\u0026gt; syncArrayList = Collections.synchronizedList(new ArrayList\u0026lt;\u0026gt;()); List\u0026lt;Object\u0026gt; syncLinkedList = Collections.synchronizedList(new LinkedList\u0026lt;\u0026gt;()); 类型选择与使用建议：\n如果需要大量非首尾增删元素，则建议使用 LinkedList 如果只是遍历查询元素，不进行增删操作，则建议使用 ArrayList 遍历 LinkedList 必须使用 Iterator 而不使用 for 循环，因为每次 for 循环体内通过 get(i) 方法获取指定元素时，需要对整个集合重新进行遍历，性能消耗极大 尽量不要试图使用 indexOf 等方法返回元素的索引，并利用其进行遍历。使用 indexOf 对集合进行遍历，当结果为空时会遍历整个集合。 2.9.2. ArrayList 和 Vector 的区别 此两个类都实现了 List 接口（List 接口继承了 Collection 接口），它们都是有序集合\n线程安全：Vector 使用了 Synchronized 来实现线程同步，是线程安全的，而 ArrayList 是非线程安全的。 性能：ArrayList 在性能方面要优于 Vector。 扩容：ArrayList 和 Vector 都会根据实际的需要动态的调整容量，只不过在 Vector 扩容每次会增加 1 倍，而 ArrayList 只会增加 50%。ArrayList 与 Vector 都可以设置初始的空间大小，但 Vector 还可以设置增长的空间大小，而 ArrayList 没有提供设置增长空间的方法。 Vector 类的所有方法都是同步的。可以由两个线程安全地访问一个 Vector 对象、但是一个线程访问 Vector 的话代码要在同步操作上耗费大量的时间；而 Arraylist 不是同步的，所以在不需要保证线程安全时时建议使用 Arraylist\n3. Iterator 迭代器 3.1. Iterator 概述 Collection 集合元素的通用获取方式：在取元素之前先要判断集合中有没有元素，如果有，就把这个元素取出来，继续在判断，如果还有就再取出出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。JDK 集合中把这种取元素的方式描述在 Iterator 接口中。\n1 public interface Iterator\u0026lt;E\u0026gt; Iterator 迭代器，是一个接口，集合迭代(集合遍历)的工具。可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration，并且迭代器允许调用者在迭代过程中移除元素。\nTips: 不同的容器完成不同方式的数据存储。不同集合的特点不同，ArrayList 有序且可重复且带索引的集合。但是有的集合不带索引。所以如果使用其他集合，可能无法通过 get+索引 的方式获取元素。所有集合的通用获取元素方法并不是通过索引获取，而是通过迭代器获取。\n3.1.1. 迭代器的好处 屏蔽了不同类型集合的内部实现，将访问逻辑抽象出来，提供了统一遍历方式。 所有的单列集合都可以使用迭代器遍历。 Iterator 的特点是只能单向遍历，但是更加安全，因为它可以确保，在当前遍历的集合元素被更改的时候，就会抛出 ConcurrentModificationException 异常。 3.1.2. 与 Enumeration 接口的区别 Enumeration 的速度是 Iterator的 两倍，也使用更少的内存。Enumeration 能满足非常基础的需求，与 Iterator 有以下区别：\nIterator 更加安全，因为当一个集合正在被遍历的时候，Iterator 会阻止其它线程去修改集合。 迭代器允许调用者从集合中移除元素，而 Enumeration 不能。迭代器已取代了 Java 集合框架中的 Enumeration。 3.2. Iterator 常用方法 1 boolean hasNext() 用来判断当前指针指向的位置是否有元素可以迭代。如果返回 true，说明可以迭代。 1 E next() 用来返回指针指向位置的元素，并把指针向后移动一位。集合用来持有数据，所有常用集合都具备了可迭代功能 Iterator 方法，该方法用于迭代集合，是最为通用的集合迭代方法。 Notes: 如果集合中没有元素可迭代了，仍然调用 next() 方法，就会抛出异常（java.util.NoSuchElementException）。真正使指针向后移动的是调用 next() 方法\n1 2 3 default void remove() { throw new UnsupportedOperationException(\u0026#34;remove\u0026#34;); } 从迭代器指向的 collection 中移除迭代器返回的最后一个元素（可选操作）。 3.3. 迭代器的基础使用(重点) 3.3.1. 集合的获取迭代器方法 通过子类调用父类 Collection 接口中的 iterator() 方法获得迭代器对象。\n1 Iterator\u0026lt;E\u0026gt; iterator(); 示例：获取某个集合的迭代器实例对象。\n1 2 ArrayList\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); Iterator\u0026lt;String\u0026gt; it = list.iterator(); 3.3.2. 集合元素的迭代 1 2 3 4 5 Iterator\u0026lt;e\u0026gt; it = list.iterator() while(it.hasNext()){ E ele = it.next(); // 返回指针指向位置的元素，并把指针向后移动一位。 // ...do something } 3.3.3. 使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import java.util.ArrayList; import java.util.Iterator; /* * 案例：往 ArrayList 添加以下元素\u0026#34;abc1\u0026#34;, \u0026#34;abc2\u0026#34;, \u0026#34;abc3\u0026#34;, \u0026#34;abc4\u0026#34;。使用迭代器获取 ArrayList 集合中的元素 */ @Test public void test() { // 创建ArrayListc对象,添加元素 ArrayList\u0026lt;String\u0026gt; array = new ArrayList\u0026lt;String\u0026gt;(); array.add(\u0026#34;abc1\u0026#34;); array.add(\u0026#34;abc2\u0026#34;); array.add(\u0026#34;abc3\u0026#34;); array.add(\u0026#34;abc4\u0026#34;); System.out.println(array); // 使用迭代器获取ArrayList集合元素 // 调用父类Collection接口中的iterator()方法创建迭代器对象Iterator Iterator\u0026lt;String\u0026gt; it = array.iterator(); // 使用Iterator迭代集合并输出结果 while (it.hasNext()) { System.out.println(it.next()); } } 3.4. 迭代器使用注意事项 3.4.1. 并发修改异常 在使用迭代器遍历集合的过程中调用了集合的 add、clear 等修改了集合元素的个数时就会出现并发修改异常(java.util.ConcurrentModificationException)。在 API 中对此异常的解释如下：\npublic class ConcurrentModificationException extends RuntimeException 此异常可能会被抛出的方法，已检测到的对象的并发修改时，这样的修改是不允许的。例如，它通常是不允许一个线程而另一个线程遍历它修改集合。在一般情况下，迭代的结果是不确定的，在这种情况下。一些迭代器实现（包括所有通用收集实现的JRE提供）可以选择如果检测行为抛出该异常。迭代器这样做被称为快速失败迭代器，因为他们不能迅速、干净，而冒着任意的，非在将来一个不确定的时间确定的行为。\n注意，这个例外并不总是表明对象已由一个不同的线程的并发性。如果一个线程问题序列的方法调用，违反合同的对象，对象可能抛出该异常。例如，如果一个线程修改直接收集的则是在一个快速失败迭代器集合的迭代，迭代器将抛出此异常。\n注意，快速失败行为不能得到保证的话，一般来说，不可能在不同步的并发修改的存在作出难以保证。快速失败的操作把 ConcurrentModificationException 尽最大努力的基础上。因此，要写一个程序，依靠这一例外的正确性错误：concurrentmodificationexception 只能用来检测错误。\n解决并发修改异常问题有如下几种方式：\n在集合有索引的情况下，使用普通 for 遍历(不使用Iterator迭代)，如： 1 2 3 4 5 ArrayList\u0026lt;String\u0026gt; array = new ArrayList\u0026lt;\u0026gt;(); // 集合添加元素... for (int i = 0; i \u0026lt; array.size(); i++) { System.out.println(array.get(i)); } 使用 ListIterator 迭代器遍历，添加元素时不要调用集合对象的 add 方法，而是调用 ListIterator 对象的 void add(E e) 方法 1 2 3 4 5 6 7 8 9 10 11 12 13 // 使用 ListIterator 解决并发修改问题 ListIterator\u0026lt;Student\u0026gt; listIt = list.listIterator(); while (listIt.hasNext()) { // 获得学生对象 Student stu = listIt.next(); // 判断是否是rose if (stu.getName().equals(\u0026#34;rose\u0026#34;)) { // 添加一个新学生对象 listIt.add(new Student(\u0026#34;老王\u0026#34;, 30)); } System.out.println(stu); } System.out.println(list); 3.4.2. 获取迭代器后遍历前不能对原集合进行结构上修改 如果在创建迭代器后不将创建时的集合输出，如果再增加或者移除元素后，集合的地址值已经改变了，迭代器的指针原来指向的地址应该没有内容，所有没有任何元素可以输出。\n3.4.3. 循环时移除集合中的元素正确方式 若需要在循环中移除集合中的元素，不能使用集合中的 remove 方法，使用 Iterrator 对象中的 remove 方法。\n1 2 3 4 5 6 7 8 9 Iterator it=list.iterator(); while(it.hasNext()){ Object e=it.next(); if(\u0026#34;b\u0026#34;.equals(e)){ it.remove(); } } System.out.println(list); 一种最常见的错误代码如下：\n1 2 3 for(Integer i : list){ list.remove(i) } 3.5. 增强 for (foreach) 3.5.1. 增强 for 概述 增强 for 循环是 JDK1.5 以后出来的一个高级 for 循环，专门用来遍历数组和集合的。\n增强 for 的本质就是迭代器，它的内部原理其实是个 Iterator 迭代器，所以在遍历的过程中，不能对集合中的元素进行增删操作。它用于遍历 Collection 和数组。通常只进行遍历元素，不要在遍历的过程中对集合元素进行增删操作。\n3.5.2. 增强 for 语法格式 1 2 3 for(元素的数据类型 变量名 : Collection 集合名或数组名){ // do something... } Code Demo：\n1 2 3 4 5 6 7 8 @Test public void test() { int[] arr = { 11, 22, 33 }; // 使用增强for遍历数组 for (int n : arr) { System.out.println(n); } } 3.5.3. 注意事项 如果使用增强 for 遍历的是引用数据类型的对象时，在循环体内部通过引用变量修改对象的成员变量值会影响集合或数组中对象的值。\n3.6. ListIterator 3.6.1. 概述 ListIterator 是一个更加强大的 Iterator 的子类型，但它只能用于各种 List 类的访问，Iterator 只能单向遍历。而 ListIterator 可以双向遍历（向前/后遍历），它还可以产生相对于迭代器在列表指向的当前位置的前一个和后一个元素的索引。并且可以使用 set() 方法替换它访问过的最后一个元素。\n1 public interface ListIterator\u0026lt;E\u0026gt; extends Iterator\u0026lt;E\u0026gt; 可以通过集合对象的 listIterator() 方法产生一个指向 List 开始处的 ListIteraor\n1 2 3 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); // ...添加元素 ListIterator\u0026lt;String\u0026gt; it = list.listIterator(); 还可以通过调用 ListIterator(n) 方法创建一个一开始就指向索引列表 n 的元素处的 ListIterator\n1 2 3 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); // ...添加元素 ListIterator\u0026lt;String\u0026gt; it = list.listIterator(list.size()); // 这里指向了 List 的最后一个元素 3.6.2. 常用方法 1 boolean hasNext(); 检查是否有下一个元素 1 E next(); 返回下一个元素 1 boolean hasPrevious(); 检查是否有前一个元素 1 E previous(); 返回前一个元素 1 int nextIndex(); 返回下一个元素的 Index 1 int previousIndex(); 返回当前元素的 Index 1 void remove(); 移除一个当前的元素 1 void set(E e); 此方法替换访问过的最后一个元素，注意用 set 设置的是 List 列表的原始值 1 void add(E e); 添加一个元素 3.6.3. 使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Test public void test1() { List\u0026lt;Pet\u0026gt; pets = Pets.arrayList(8); ListIterator\u0026lt;Pet\u0026gt; it = pets.listIterator(); while (it.hasNext()) { System.out.print(it.next() + \u0026#34;, \u0026#34; + it.nextIndex() + \u0026#34;, \u0026#34; + it.previousIndex() + \u0026#34;; \u0026#34;); } // Backwards: while (it.hasPrevious()) { System.out.print(it.previous().id() + \u0026#34; \u0026#34;); } System.out.println(pets); it = pets.listIterator(3); while (it.hasNext()) { it.next(); it.set(Pets.randomPet()); } System.out.println(pets); } @Test public void test2() { List\u0026lt;Integer\u0026gt; src = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List\u0026lt;Integer\u0026gt; dest = new LinkedList\u0026lt;\u0026gt;(src); System.out.println(\u0026#34;source: \u0026#34; + src); System.out.println(\u0026#34;destination: \u0026#34; + dest); ListIterator\u0026lt;Integer\u0026gt; fwd = dest.listIterator(); ListIterator\u0026lt;Integer\u0026gt; rev = dest.listIterator(dest.size()); // 这里将rev指向了List的最后一个元素 int mid = dest.size() \u0026gt;\u0026gt; 1; for (int i = 0; i \u0026lt; mid; i++) { Integer tmp = fwd.next(); fwd.set(rev.previous()); rev.set(tmp); } System.out.println(\u0026#34;source: \u0026#34; + src); System.out.println(\u0026#34;destination: \u0026#34; + dest); } 3.6.4. Iterator 和 ListIterator 的区别(面试题) ListIterator 接口继承 Iterator 接口，可以说是 Iterator 的增强版。该接口添加了一些额外的功能，比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置\nIterator 可以遍历所有类型的集合；而 ListIterator 只能遍历 List 及其子类。 Iterator 只能单向（向后）遍历；而 ListIterator 可以进行双向遍历（向前/后遍历），通过 previous() 和 hasPrevious() 方法进行逆向的遍历。 Iterator 不能往集合中添加元素；而 ListIterator 可以通过 add() 方法，可以向 List 集合添加对象。 Iterator 无法定位当前的索引位置；而 ListIterator 可以通过 nextIndex() 和 previousIndex() 方法来获取前/后一个元素的索引。 Iterator 仅能遍历，不能修改；而 ListIterator 可以通过 set() 方法实现对象的修改。 4. Set 接口 4.1. 概述 Set 是无序(存取顺序不一致)，无索引，元素不可重复的集合\n1 public interface Set\u0026lt;E\u0026gt; extends Collection\u0026lt;E\u0026gt; 在 Set 集合中，对象元素的相等性比较是通过元素的 equals 与 hashCode 方法来判断。本质上是 Java 依据对象的内存地址计算出对象的 HashCode 值来判断。因此如果需要自定义比较两个对象是否相等，则必须同时重写对象的 hashCode 方法和 equals 方法。\n4.2. Set 接口的实现类 HashSet\n没有索引 不能重复 存储没有顺序 LinkedHashSet(继承HashSet)\n没有索引 不能重复 存储和取出有顺序 4.3. 哈希表(数组和链表的组合体) 4.3.1. 对象的哈希值 对象的哈希值，就是一个十进制整数。通过 Object 类的 hashCode() 的方法获得。是对象存储到 HashSet 的依据。子类可以重写该方法自己计算哈希值。\n哈希表底层使用的也是数组机制，数组中也存放对象，而这些对象往数组中存放时的位置比较特殊，当需要把这些对象给数组中存放时，那么会根据这些对象的特有数据结合相应的算法，计算出这个对象在数组中的位置，然后把这个对象存放在数组中。而这样的数组就称为哈希数组，即就是哈希表。\n当向哈希表中存放元素时，需要根据元素的特有数据结合相应的算法，这个算法其实就是 Object 类中的 hashCode 方法。由于任何对象都是 Object 类的子类，所以任何对象有拥有这个方法。即就是在给哈希表中存放对象时，会调用对象的 hashCode 方法，算出对象在表中的存放位置，这里需要注意，如果两个对象 hashCode 方法算出结果一样，这样现象称为哈希冲突，这时会调用对象的 equals 方法，比较这两个对象是不是同一个对象，如果 equals 方法返回的是 true，那么就不会把第二个对象存放在哈希表中，如果返回的是 false，就会把这个值存放在哈希表中。\n总结：保证 HashSet 集合元素的唯一，其实就是根据对象的 hashCode 和 equals 方法来决定的。如果往集合中存放自定义的对象，那么保证其唯一则必须重写 hashCode 和 equals 方法，建立属于当前对象的比较方式。\n4.3.2. Object 的 hashCode 方法 Object 类的 hashCode 方法，返回值是一个十进值整数，默认是对象的内存地址值（十六进制）。默认情况下内存地址不一样 hashCode 就不一样。\nString 类重写了 hashCode，只要字符内容相同，等到的哈希值就相同。\n自定义的类如果要放到 HashSet 中，也需要重写 hashCode\n4.3.3. 哈希表结构的特点 只要看到类名上带有 Hash。说明它底层使用哈希表结构，HashSet 底层使用的是哈希表结构。\n4.3.4. 哈希表的存储元素详细过程(HashSet 判断元素唯一的原理) 调用对象的 hashCode() 方法，获得要存储元素的哈希值。 将哈希值与表的长度(即数组的长度)进行求余运算得到一个整数值，该值就是新元素要存放的位置(即是索引值)。 如果索引值对应的位置上没有存储任何元素，则直接将元素存储到该位置上。 如果索引值对应的位置上已经存储了元素，则执行第3步。 遍历该位置上的所有旧元素，依次比较每个旧元素的哈希值和新元素的哈希值是否相同。 如果有哈希值相同的旧元素，则执行第4步。 如果没有哈希值相同的旧元素，则执行第5步。 比较新元素和旧元素的地址是否相同 如果地址值相同则用新的元素替换老的元素。停止比较。 如果地址值不同，则新元素调用 equals 方法与旧元素比较内容是否相同。 如果返回true，用新的元素替换老的元素，停止比较。 如果返回false，则回到第3步继续遍历下一个旧元素。 说明没有重复，则将新元素存放到该位置上并让新元素记住之前该位置的元素。 4.3.5. hashCode 注意事项 假设有两个 Person 对象p1和p2，如果 p1.equals(p2) 为true。则 p1.hashCode() == p2.hashCode() 一定是 true 吗？ 答：一定。hashCode的官方协定：\n如果根据 equals(Object) 方法，两个对象是相等的。那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。 如果根据 equals(java.lang.Object) 方法，两个对象不相等，那么对这两个对象中的任一对象上调用 hashCode 方法“不”要求一定生成不同的整数结果。但是，程序员应该意识到，为不相等的对象生成不同整数结果可以提高哈希表的性能。 4.4. HashSet 集合 4.4.1. 概述 HashSet 集合的底层是哈希表结构，基于 HashTable 实现：查询快，增删快，线程不安全（效率高）\n1 2 3 public class HashSet\u0026lt;E\u0026gt; extends AbstractSet\u0026lt;E\u0026gt; implements Set\u0026lt;E\u0026gt;, Cloneable, java.io.Serializable HashSet 特性和基本使用\nHashSet 是 Set 接口的子类，不包含相同元素，且无序，没有索引 底层是哈希表：数组和链表的组合体。底层采用 HashMap 来保存元素，其元素值都存放在 HashMap 的 key 中，而 value 统一为 PRESENT 哈希表的特点：查询和增删都比较快。 4.4.2. HashSet 构造方法分析 1 2 3 4 5 static final float DEFAULT_LOAD_FACTOR = 0.75f; public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } 比如：加载因子是0.75，数组的长度为16，其中存入16 * 0.75 = 12个元素。如果再存入第十三个(大于12)元素。那么此时会扩充哈希表(再哈希)，底层会开辟一个长度为原长度2倍的数组。把老元素拷贝到新数组中，再把新元素添加数组中。当存入元素数量 \u0026gt; 哈希表长度 * 加载因子，就要扩容，因此加载因子决定扩容时机。\n4.4.3. HashSet 保存元素的原理 HashSet 存放的是散列值，它是按照元素的散列值来存取元素的。HashSet 中的 add 方法的底层实现是调用 HashMap 的 put 方法，将元素的散列值（即 HashCode）作为 HashMap 的 key。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class HashSet\u0026lt;E\u0026gt; extends AbstractSet\u0026lt;E\u0026gt; implements Set\u0026lt;E\u0026gt;, Cloneable, java.io.Serializable { private transient HashMap\u0026lt;E,Object\u0026gt; map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object(); public HashSet() { map = new HashMap\u0026lt;\u0026gt;(); } public boolean add(E e) { // 调用 HashMap 的 put 方法, PRESENT 是一个至始至终都相同的虚值 return map.put(e, PRESENT)==null; } // ... } 元素的散列值是通过元素的 hashCode 方法计算得到的，HashSet 首先判断两个元素的散列值是否相等，如果散列值相等，则接着通过 equals 方法返回的结果（true/false），来判断是否为同一个元素。\n4.4.4. HashSet 存储判断元素（自定义类型）重复的原理 4.4.4.1. HashSet 的 add/contains 等方法判断元素是否重复原理 Set 集合不能存放重复元素，其添加方法在添加时会判断是否有重复元素，有重复不添加，没重复则添加。\nHashSet 集合由于是无序的，其判断唯一的依据是元素类型的 hashCode 与 equals 方法的返回结果。规则如下：\n先判断新元素与集合内已经有的旧元素的 HashCode 值，如果不同，说明是不同元素，添加到集合 如果 HashCode 值相同，再判断 equals 比较结果。返回 true 则相同元素，不添加；返回 false 则不同元素，添加到集合。 4.4.4.2. 使用 HashSet 存储自定义类型 当使用HashSet存储自定义类型，如果没有重写该类的 hashCode 与 equals 方法，则判断重复时，使用的是地址值，如果想通过内容比较元素是否相同，需要重写该元素类的 hashcode 与 equals 方法\n4.5. LinkedHashSet 集合 1 2 3 public class LinkedHashSet\u0026lt;E\u0026gt; extends HashSet\u0026lt;E\u0026gt; implements Set\u0026lt;E\u0026gt;, Cloneable, java.io.Serializable LinkedHashSet 继承自 HashSet，底层也是哈希表，由 LinkedHashMap 来实现存储元素，所有的方法和操作与 HashSet 一致，不包含重复元素，没有索引。唯一不同是可预测迭代顺序的 Set 集合。\nLinkedHashSet 的实现上非常简单，只提供了四个构造方法，并通过传递一个标识参数，调用父类的构造器，底层构造一个 LinkedHashMap 来实现，在相关操作其实就是直接调用父类 HashSet 相应的方法。\n4.6. TreeSet 集合 TreeSet 基于二叉树的原理对新添加的对象按照指定的顺序排序（升序、降序），每添加一个对象都会进行排序，并将对象插入二叉树指定的位置。\nInteger 和 String 等基础对象类型可以直接根据 TreeSet 的默认排序进行存储，而对于自定义的数据类型，TreeSet 要求存放的对象必须实现 Comparable 接口，并重写该接口提供的 compareTo() 比较元素方法，当插入元素时会回调该方法比较元素的大小从而进行排序。若重写该函数，返回 -1（或负整数）则表示升序，即当前对象(this)小于指定对象；返回 1（或正整数）则表示降序，即当前对象(this)大于指定对象\n4.7. Set 接口相关实现类总结 4.7.1. HashSet、LinkedHashSet 和 TreeSet 的区别 HashSet 是 Set 接口的主要实现类，HashSet 的底层是 HashMap，线程不安全的，可以存储 null 值； LinkedHashSet 是 HashSet 的子类，能够按照添加的顺序遍历； TreeSet 底层使用红黑树，能够按照添加元素的顺序进行遍历，排序的方式可以自定义 5. Queue 接口 5.1. 概述 java 提供了一个专门用于队列结构的接口：java.util.Queue\n1 public interface Queue\u0026lt;E\u0026gt; extends Collection\u0026lt;E\u0026gt; 队列通常（但并非一定）以 FIFO（先进先出）的方式排序各个元素。\n5.2. 常用方法 1 boolean offer(E e); 将指定的元素插入此队列 1 E poll(); 获取并移除此队列的头，如果此队列为空，则返回 null。 1 E peek(); 获取但不移除此队列的头；如果此队列为空，则返回 null。 1 E remove() 获取并移除此队列的头。此方法与 poll 唯一的不同在于：当队列为空时将抛出一个 NoSuchElementException 异常。 5.3. ArrayDeque（了解） 1 2 public class ArrayDeque\u0026lt;E\u0026gt; extends AbstractCollection\u0026lt;E\u0026gt; implements Deque\u0026lt;E\u0026gt;, Cloneable, Serializable ArrayDeque 实现了双端队列，内部使用循环数组实现，默认大小为16。它的特点是：\n在两端添加、删除元素的效率较高 根据元素内容查找和删除的效率比较低。 没有索引位置的概念，不能根据索引位置进行操作。 ArrayDeque 和 LinkedList 都实现了 Deque 接口，如果只需要从两端进行操作，ArrayDeque 效率更高一些。如果同时需要根据索引位置进行操作，或者经常需要在中间进行插入和删除（LinkedList 有相应的 api，如 add(int index, E e)），则应该选 LinkedList。\nArrayDeque 和 LinkedList 都是线程不安全的，可以使用 Collections 工具类中 synchronizedXxx() 转换成线程同步。\n6. Map 接口（双列集合） 6.1. 概述 只要是 Map 接口的实现类，都属于双列集合。将键映射到值的对象。一个映射不能包含重复的键；每个键最多只能映射到一个值\n6.1.1. Map（双列集合）继承体系图 6.1.2. Map 接口的特点(重点) 每个元素是由两部分组成，一部分称为键(key)，一部分称为值(Value); 键和值称为键值对； 键必须唯一，值可以重复。 Map 集合的数据结构仅仅针对键有效，与值无关。 基于键的 HashCode 值唯一标识一条数据，同时基于键的 HashCode 值进行数据的存取。 6.1.3. Map 集合的初始化 1 public interface Map\u0026lt;K,V\u0026gt; 泛型K：此映射所维护的键的类型(key就是键) 泛型V：映射值的类型(values就是值) Tips: k和v不需要同一种类型，但必须都是引用类型，如int会报错，需要定义为 Integer); Map 集合类似词典索引，通过key可以找到value\n创建对象时，要分别制定键的泛型与值的泛型。以初始化一个HashMap对象为例：\n1 2 3 HashMap\u0026lt;String, Integer\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); // 最常用此写法 HashMap\u0026lt;String, Integer\u0026gt; map = new HashMap\u0026lt;String, Integer\u0026gt;(); HashMap\u0026lt;String, Integer\u0026gt; map = new HashMap(); // 但这种会报警告 6.1.4. Map 接口中的常用方法 1 public V put(K key, V value); 如果 map 中不存在该 key，就添加键值对元素，并返回null。相当于新增功能 如果 map 中存在该 key，就修改键值对元素，并返回旧的值。相当于修改功能 Notes: 在此映射中关联指定值与指定键。如果该映射以前包含了一个该键的映射关系，则旧值被替换。即如果 key 之前已经包含一个 value，再调用 put(k,v)方法，那么旧的value就被替换成新的value，并返回与 key 关联的旧值；如果 key 没有任何映射关系，则返回 null。（返回 null 还可能表示该映射之前将 null 与 key 关联。）\n1 public V get(Object key); 根据指定的键获取到相应的值。如果键不存在，则返回 null 1 public V remove(Object key); 根据键删除这个元素，键和值都删除。并返回被删的键对应的值。如果键不存在，则没有效果并返回 null 1 public int size(); 获取键值对的数量(元素的个数)，返回此映射中的键-值映射关系数。(即目前集合中已经存了多少对键值对元素) 1 public boolean containsKey(Object key) 如果此映射包含对于指定键的映射关系，则返回 true 1 public boolean containsValue(Object value) 如果此映射将一个或多个键映射到指定值，则返回 true 1 public boolean isEmpty() 如果Map集合为空，则返回 true 1 public void clear(); 清空Map集合 1 public Set\u0026lt;K\u0026gt; keySet() 获取 Map 中所有的键，返回此映射中所包含的键的 Set 视图。 1 public Collection\u0026lt;V\u0026gt; values(); Map 获取所有的值，返回此映射所包含的值的 Collection 视图。 6.2. Entry 接口 6.2.1. 概述 1 2 3 4 5 6 public interface Map\u0026lt;K,V\u0026gt; { // ...省略 interface Entry\u0026lt;K,V\u0026gt; { ... } } Entry\u0026lt;K,V\u0026gt; 是 Map 的内部接口(嵌套接口，看做普通接口)，将一个键和值封装成了 Entry 对象，并存储在 Set 集合中。可以使用 Map.Entry\u0026lt;K,V\u0026gt; 来定义变量，从一个 Entry 对象中获取一个键值对的键与值。\nTips: Entry 其中一个实现是 HashMap 的内部类。\n通过调用 Map 对象 entrySet 方法，返回某个集合所有的键值对对象(Entry)。\n1 Set\u0026lt;Map.Entry\u0026lt;K,V\u0026gt;\u0026gt; entrySet() 使用示例：\n1 2 3 4 5 6 Map\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); // 此方式导包 import java.util.Map.Entry; Set\u0026lt;Entry\u0026lt;K,V\u0026gt;\u0026gt; entrySet = map.entrySet(); // 此方式，不需要额外导包 Set\u0026lt;Map.Entry\u0026lt;K,V\u0026gt;\u0026gt; entrySet = map.entrySet(); 6.2.2. Entry 接口常用方法 1 k getKey(); 获得 Entry 类对象的键 1 V getValue(); 获得 Entry 类对象的值 6.3. 遍历 Map 集合的方式 Notes: 不能直接使用增强 for 或迭代器遍历，因为 Map 没有继承 Iterable\u0026lt;E\u0026gt; 接口。以下三种遍历方式，若 Map 本身的变化会影响其遍历的结果。\n6.3.1. 通过 keySet 方法遍历 Map 没有迭代器方法，最常用的遍历方法：先获取所有键的集合，迭代该集合，依次获取每一个键，通过键找值。使用 keySet() 方法遍历流程：\n使用 Set\u0026lt;K\u0026gt; keySet() 方法，获取所有的键的 Set 集合。 使用增强 for 或者迭代器遍历键的 Set 集合。 通过 V get(Object key) 方法，获取每个键对应的值。 6.3.2. 通过 entrySet 方法遍历 使用 entrySet() 方法获取某个集合所有的键值对对象，再进行遍历：\n通过 entrySet() 方法，获取所有的键值对对象的 Set 集合 使用增强 for 或者迭代器遍历获取到每一个键值对对象 通过键值对对象的 getKey() 拿到键 通过键值对对象的 getValue() 拿到值 Code Demo:\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import java.util.Collection; import java.util.HashMap; import java.util.Map.Entry; import java.util.Set; public class EntrySetTest { public static void main(String[] args) { // 创建HashMap对象 HashMap\u0026lt;String, String\u0026gt; hm = new HashMap\u0026lt;\u0026gt;(); hm.put(\u0026#34;剑圣主宰\u0026#34;, \u0026#34;敌法师\u0026#34;); hm.put(\u0026#34;风暴之灵\u0026#34;, \u0026#34;灰烬之灵\u0026#34;); hm.put(\u0026#34;炼金术士\u0026#34;, \u0026#34;剧毒术士\u0026#34;); hm.put(\u0026#34;齐天大圣\u0026#34;, \u0026#34;矮人火枪手\u0026#34;); hm.put(\u0026#34;斧王\u0026#34;, \u0026#34;幽鬼\u0026#34;); // 输出HashMap集合 System.out.println(hm); System.out.println(\u0026#34;==Collection\u0026lt;V\u0026gt; values()，直接获得HashMap集合的值==\u0026#34;); // 通过Collection\u0026lt;V\u0026gt; values()，直接获得HashMap集合的值 Collection\u0026lt;String\u0026gt; coll = hm.values(); for (String s : coll) { System.out.println(s); } System.out.println(\u0026#34;=====第1种方法遍历获取HashMap集合的键值对=====\u0026#34;); // 第1种遍历Map集合值的方法：通过keySet()方法 Set\u0026lt;String\u0026gt; key = hm.keySet(); // 输出所有key的Set集合 System.out.println(\u0026#34;HashMap集合的键：\u0026#34; + key); // 使用增强for遍历set集合，通过key拿到value值 for (String s : key) { System.out.println(s + \u0026#34;==\u0026#34; + hm.get(s)); } // 第2种遍历Map集合值的方法：通过entrySet()方法 // 创建Entry类对象的Set集合 System.out.println(\u0026#34;=====第2种方法遍历获取HashMap集合的键值对=====\u0026#34;); Set\u0026lt;Entry\u0026lt;String, String\u0026gt;\u0026gt; entrySet = hm.entrySet(); for (Entry\u0026lt;String, String\u0026gt; e : entrySet) { System.out.println(e.getKey() + \u0026#34;==\u0026#34; + e.getValue()); } } } 6.3.3. 通过 Values 方法遍历 通过 Map 对象的 Values() 方法获取所有值的 Collection 集合，再使用增加 for 循环遍历。\n6.4. HashMap 6.4.1. 概述 HashMap：基于哈希表的 Map 接口的实现类\n1 public class HashMap\u0026lt;K,V\u0026gt; extends AbstractMap\u0026lt;K,V\u0026gt; implements Map\u0026lt;K,V\u0026gt;, Cloneable, Serializable HashMap 有如下特点：\n键是唯一，基于键的 HashCode 值唯一标识一条数据，同时基于键的 HashCode 值进行数据的存取。 存储和取出无法保证顺序一致。 非线程安全。同一时间有多个线程同时对 HashMap 进行写操作，将可能导致数据的不一致。如需要满足线程安全的条件，可使用 Collections 的 synchronizedMap 方法使 HashMap 具有线程安全的能力，或者使用 ConcurrentHashMap。 允许使用 null 的值和 null 的键（HashMap 最多只允许一条记录的键为 null，允许多条记录的值为 null） Tips: 更多实现原理详见《Java扩展-集合类源码分析》笔记\n6.4.1.1. 存储结构 HashMap 在 JDK 1.7 以前的数据存储结构是『数组+链表』。\n为了减少链表遍历的开销，JDK 1.8 开始对 HashMap 进行了优化，增加了红黑树部分，将数据存储结构修改为『数组+链表+红黑树』。当链表长度大于8（TREEIFY_THRESHOLD）时，会把链表转换为红黑树，红黑树节点个数小于6（UNTREEIFY_THRESHOLD）时才转化为链表，防止频繁的转化。\nHashMap 将链表结构转换为红黑树结构后，提高了查询效率，因此其时间复杂度为 O(log N)。\n6.4.1.2. 类的主要参数 capacity：当前数组的容量，默认为 16，可以扩容，扩容后数组的大小为当前的两倍，因此该值始终为2n。 loadFactor：负载因子，默认为 0.75。 threshold：扩容的阈值，其值等于 capacity × loadFactor。 6.4.2. 一般用什么类型作为 HashMap 的 key 一般用 Integer、String 这些不可变类当 HashMap 当 key。\n因为 String 是不可变的，所以在它创建的时候hashcode就被缓存了，不需要重新计算。这就是 HashMap 中的 key 经常使用字符串的原因。获取对象的时候要用到 equals() 和 hashCode() 方法，而 Integer、String 都已经重写了此两个方法，不需要程序员去重写。\n6.4.3. HashMap 线程不安全的情况 JDK 7 时多线程下扩容会造成死循环。 多线程的 put 方法可能导致元素的丢失。 put 和 get 在并发时，可能导致 get 的值为 null。原因是 hashcode 可能发生改变，导致 put 进去的值，无法 get 获取。例如： 1 2 3 4 5 6 7 8 HashMap\u0026lt;List\u0026lt;String\u0026gt;, Object\u0026gt; changeMap = new HashMap\u0026lt;\u0026gt;(); List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); list.add(\u0026#34;hello\u0026#34;); Object objectValue = new Object(); changeMap.put(list, objectValue); System.out.println(changeMap.get(list)); // 输出结果：java.lang.Object@74a14482 list.add(\u0026#34;hello world\u0026#34;); // hashcode发生了改变 System.out.println(changeMap.get(list)); // 输出结果：null 6.5. LinkedHashMap LinkedHashMap 继承 HashMap，是 Map 接口的实现类，并允许使用 null 值和 null 键，键是唯一，存储和取出有顺序。\nLinkedHashMap 是基于哈希表(HashTable)的数据结构，该结构保证 key 唯一；使用链表(Linked)结构保存元素，从而保证元素有序性，怎么存就怎么取。注：这些约束都是针对键起作用\nTips: 其他与 HashMap 的功能与用法一样。\n6.6. TreeMap 6.6.1. 概述 1 2 3 public class TreeMap\u0026lt;K,V\u0026gt; extends AbstractMap\u0026lt;K,V\u0026gt; implements NavigableMap\u0026lt;K,V\u0026gt;, Cloneable, java.io.Serializable TreeMap 是基于二叉树数据结构存储数据。从功能上讲，有比 HashMap 更为强大的功能，它实现了 SortedMap 接口，即可以对元素进行排序，默认按键值的升序排序，也可以自定义排序比较器。\nTreeMap 要求存放的键值对所映射的键对象必须实现 Comparable 接口，重写 compareTo 方法，从而根据键对元素进行排序。否则会抛出 java.lang.ClassCastException 异常。\nTreeMap 的性能略微低于 HashMap。如果在开发中需要对元素进行排序，那么使用 HashMap 便无法实现这种功能，使用 TreeMap 的迭代输出将会以元素顺序进行。\n6.6.2. 继承结构 6.6.3. 特点 TreeMap 是有序的 key-value 集合，通过红黑树实现。根据键的自然顺序进行排序或根据提供的 Comparator 进行排序。 TreeMap 继承了 AbstractMap，实现了 NavigableMap 接口，支持一系列的导航方法，给定具体搜索目标，可以返回最接近的匹配项。如 floorEntry()、ceilingEntry() 分别返回小于等于、大于等于给定键关联的 Map.Entry() 对象，不存在则返回 null。lowerKey()、floorKey、ceilingKey、higherKey() 只返回关联的 key。 6.7. Hashtable 1 2 3 public class Hashtable\u0026lt;K,V\u0026gt; extends Dictionary\u0026lt;K,V\u0026gt; implements Map\u0026lt;K,V\u0026gt;, Cloneable, java.io.Serializable HashTable 是旧版本的遗留类，很多映射的常用功能都与 HashMap 类似，不同的是它继承自 Dictionary 类，并且是线程安全的，同一时刻只允许一个线程对 HashTable 进行写操作，并发性不如 ConcurrentHashMap。\nTips: Hashtable 不建议使用，不需要线程安全的场景可以用 HashMap 替换，需要线程安全的场景可以用 ConcurrentHashMap 替换。\n6.8. ConcurrentHashMap（网络资料，未整理） 6.8.1. 常用 API（整理中） TODO: 整理中\n6.8.2. ConcurrentHashMap 实现原理 Notes: 本章节内容详见《并发编程 - 原理篇》笔记中的『ConcurrentHashMap 原理』章节\n6.9. Map 接口相关实现类总结 6.9.1. Hashtable 和 HashMap 的区别(面试题) Hashtable 与 HashMap 都是 Map 接口的实现类。\n是否支持 null 为作为 key：HashMap 可以接受为 null 的 key 和 value，key 为 null 的键值对放在下标为 0 的头结点的链表中，并且只允许存在一个；而 Hashtable 则不能。 线程安全：HashMap 是非线程安全的；HashTable 是线程安全的。Jdk 1.5 提供了 ConcurrentHashMap，它是 HashTable 的替代。 执行效率：Hashtable 很多方法是同步方法，在单线程环境下它效率低，比 HashMap 要低。 哈希值：HashTable 直接使用对象的 hashCode；而 HashMap 重新计算 hash 值。 fail-fast 机制：HashMap 获取的 keySet 是使用 Iterator 遍历，支持 fail-fast 机制；而 HashTable 的 keySet 是使用 Enumeration 遍历，不支持 fail-fast 机制。 6.9.2. SynchronizedMap 和 ConcurrentHashMap 有什么区别 SynchronizedMap 是 Collections 集合工具类的内部类，一次锁住整张表来保证线程安全，所以每次只能有一个线程来访问 map。\nJDK1.8 ConcurrentHashMap 采用 CAS 和 synchronized 来保证并发安全。数据结构采用数组+链表/红黑二叉树。synchronized 只锁定当前链表或红黑二叉树的首节点，支持并发访问、修改。另外 ConcurrentHashMap 使用了一种不同的迭代方式。当 iterator 被创建后集合再发生改变就不再是抛出 ConcurrentModificationException，取而代之的是在改变时 new 新的数据从而不影响原有的数据，iterator 完成后再将头指针替换为新的数据，这样 iterator 线程可以使用原来老的数据，而写线程也可以并发的完成改变。\n6.9.3. TreeMap 与 LinkedHashMap 的区别 LinkedHashMap 是基于元素进入集合的顺序或者被访问的先后顺序排序。 TreeMap 则是基于元素的 key 固有顺序(由 Comparator 或者 Comparable 确定)进行排序。 7. 自定义比较器：Comparator 与 Comparable 7.1. Comparator 接口 1 2 3 4 5 6 @FunctionalInterface public interface Comparator\u0026lt;T\u0026gt; { int compare(T o1, T o2); // ...省略其他的 default 与 static 方法 } java.util.Comparator，是 JDK 提供的比较器接口，强行对某个对象 collection 进行整体排序的比较函数。它是函数式接口，可以使用 lambda 表达式来实现 compare(T o1, T o2) 方法。\n其中 int compare(T o1, T o2); 方法是比较用来排序的两个参数。根据第一个参数小于、等于或大于第二个参数分别返回负整数、零或正整数。返回值代表的含义如下：\n返回零：表示两个元素相同 返回负数：左边小于右边 返回正数：左边大于右边 Notes: 如果用两个不是整数类型的相减做为判断，需要强转。字符串可以用自带的方法 compareTo 进行比较\n7.2. Comparable 接口 1 2 3 4 public interface Comparable\u0026lt;T\u0026gt; { public int compareTo(T o); } java.lang.Comparable 接口是用于对象的自然排序。其中 compareTo 关键方法就是实现排序的规则，方法返回 int 类型数值。例如：i = x.compareTo(y);\n如果返回数值为 0，也表明两个对象排序上是相等的(并非意味 equals 方法为 true，但是jdk api上强烈建议这样处理) 如果返回数值大于0，则表示 x \u0026gt; y 如果返回数值小于0，则表示 x \u0026lt; y 7.3. Comparator 与 Comparable 的区别 Comparator 和 Comparable 接口都是用于对象集合或者数组排序，两者主要的区别是：\nComparator 接口是用于提供不同的排序算法，可以选择需要使用的该接口来对给定的对象集合进行排序。 Comparable 接口是用于提供对象的自然排序，可以使用它来提供基于单个逻辑的排序。 7.4. Collections 工具类的 sort 方法 7.4.1. 简介 Collections 工具类中的 sort 方法就是使用了 Comparator 接口来对集合中的元素进行排序。\n1 2 3 public static \u0026lt;T\u0026gt; void sort(List\u0026lt;T\u0026gt; list, Comparator\u0026lt;? super T\u0026gt; c) { list.sort(c); } 方法作用：根据指定比较器现实的顺序，对指定列表进行排序。使用此工具类的排序方法，通过自定义比较器接口 Comparator 可以对 List 类集合排序，一般通过匿名内部类使用。例如：\n1 2 3 4 5 6 7 8 9 10 11 12 13 List\u0026lt;Student\u0026gt; array = new ArrayList\u0026lt;\u0026gt;(); //...设置集合一些测试数据后 // Comparator接口排序，使用匿名内部类 Collections.sort(array, new Comparator\u0026lt;Student\u0026gt;() { // 重写compare方法，可以根据学生类的某个属性去比较 @Override public int compare(Student o1, Student o2) { return o1.getScore() - o2.getScore(); } }); // Comparator接口排序，使用 lambda 表达式 Collections.sort(array, Comparator.comparingInt(Student::getScore)); 7.4.2. 基础示例 对基本数据类型的集合进行排序\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; public class MoonZero { public static void main(String[] args) { ArrayList\u0026lt;Integer\u0026gt; array = new ArrayList\u0026lt;\u0026gt;(); Collections.addAll(array, 15, 5, 28, 3, 18, 44, 4, 145, 54, 83, 41, 8, 11, 13); System.out.println(\u0026#34;排序前：\u0026#34; + array); // Comparator接口排序,使用匿名内部类 Collections.sort(array, new Comparator\u0026lt;Integer\u0026gt;() { // 重写 compare 方法 @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); System.out.println(\u0026#34;排序后：\u0026#34; + array); } } 对象集合进行排序\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; public class MoonZero { public static void main(String[] args) { ArrayList\u0026lt;Student\u0026gt; array = new ArrayList\u0026lt;\u0026gt;(); array.add(new Student(\u0026#34;004\u0026#34;, \u0026#34;矮人火枪手\u0026#34;, 20, 99)); array.add(new Student(\u0026#34;003\u0026#34;, \u0026#34;敌法师\u0026#34;, 21, 88)); array.add(new Student(\u0026#34;005\u0026#34;, \u0026#34;剑圣主宰\u0026#34;, 20, 80)); array.add(new Student(\u0026#34;001\u0026#34;, \u0026#34;斧王\u0026#34;, 22, 70)); array.add(new Student(\u0026#34;002\u0026#34;, \u0026#34;幽鬼\u0026#34;, 22, 30)); System.out.println(\u0026#34;排序前：\u0026#34; + array); // Comparator接口排序,使用匿名内部类 Collections.sort(array, new Comparator\u0026lt;Student\u0026gt;() { // 重写compare方法，可以根据学生类的某个属性去比较 @Override public int compare(Student o1, Student o2) { // 按String类型的学号排序 字符串可以用自带的方法compareTo进行比较 // return o1.getId().compareTo(o2.getId()); // 按Double类型的成绩排序 因为返回值必须为整数，所以要做一个强转 return (int) (o1.getScore() - o2.getScore()); } }); System.out.println(\u0026#34;排序后：\u0026#34; + array); } } 8. 集合相关的工具类 API 8.1. Collections 工具类 Collection 集合的工具类，提供了大量的方法操作单列集合。里面都是静态方法。直接用类名.静态方法使用\n注：Collections是工具类，Collection是接口\n1 public static \u0026lt;T\u0026gt; boolean addAll(Collection\u0026lt;? super T\u0026gt; c, T... elements) 将数组中的所有元素添加到指定的集合中。例如： 1 2 ArrayList\u0026lt;String\u0026gt; array = new ArrayList\u0026lt;String\u0026gt;(); Collections.addAll(array, \u0026#34;a\u0026#34;,\u0026#34;b\u0026#34;,\u0026#34;c\u0026#34;,\u0026#34;d\u0026#34;,\u0026#34;e\u0026#34;); 1 public static \u0026lt;T\u0026gt; int binarySearch(List\u0026lt;?\u0026gt; list,T key) 此方法使用二分查找法(折半查找)查找指定元素 要求：二分法查询必须要求集合中的元素排好顺序(从小到大排序) 在集合List中查找指定的元素key,如果找到返回key在集合中的索引值(位置) 如果没找到，则返回= -插入点-1（插入点=-(返回值+1)） 1 public static void shuffle(List\u0026lt;?\u0026gt; list) 对集合中的元素进行乱序操作（传入是List，只针对有顺序的集合）。使用默认随机源对指定列表进行置换。所有置换发生的可能性都是大致相等的。 1 public static \u0026lt;T\u0026gt; void sort(List\u0026lt;T\u0026gt; list) 对集合元素进行排序，默认是升序排序。 有顺序(有序)：第一个元素是多少，第二个元素是多少，第几个元素对应的是第几，顺序不变。 排序：不管是第几个放的，只要到集合中(以 Integer 集合为例)，就按照一定的顺序重新排列了。 根据元素的自然顺序 对指定列表按升序进行排序。列表中的所有元素都必须实现 Comparable 接口。 1 public static \u0026lt;T\u0026gt; void sort(List\u0026lt;T\u0026gt; list, Comparator\u0026lt;? super T\u0026gt; c) 对集合元素进行排序，集合的元素不需要实现 Comparable 接口，通过第二个参数 Comparator 来定义排序的逻辑。可以使用 lambda 表达式来定义 Comparator 接口实现，更多介绍详细前面章节。 1 public static void swap(List\u0026lt;?\u0026gt; list, int i, int j); 将集合中索引i和索引j位置交换。 1 public static void reverse(List\u0026lt;?\u0026gt; list) 反转指定列表中元素的顺序。 8.1.1. 创建不能修改的集合 1 public static \u0026lt;T\u0026gt; Collection\u0026lt;T\u0026gt; unmodifiableCollection(Collection\u0026lt;? extends T\u0026gt; c) 创建一个只读集合，改变该集合的任何操作都会抛出 java.lang.UnsupportedOperationException 异常。\n示例代码如下：\n1 2 3 4 5 List\u0026lt;String\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); list.add(\u0026#34;x\u0026#34;); Collection\u0026lt;String\u0026gt; clist = Collections.unmodifiableCollection(list); clist.add(\u0026#34;y\u0026#34;); // 运行时此行报错 System.out.println(list.size()); 8.1.2. 创建线程安全的 List、Set、Map 集合 1 public static \u0026lt;T\u0026gt; List\u0026lt;T\u0026gt; synchronizedList(List\u0026lt;T\u0026gt; list) 将指定的集合转化成支持的同步（线程安全的）集合。\n参数说明：\nlist：被“包装”在同步列表中的列表。 示例：\n1 2 3 4 5 6 7 List\u0026lt;String\u0026gt; synchronizedList = Collections.synchronizedList(new ArrayList\u0026lt;\u0026gt;()); synchronizedList.add(\u0026#34;aaa\u0026#34;); synchronizedList.add(\u0026#34;bbb\u0026#34;); for (int i = 0; i \u0026lt; synchronizedList.size(); i++) { System.out.println(synchronizedList.get(i)); } Notes: Collections 工具类提供的以 synchronized 为前缀的方法，可以将 Set、Map 集合类包装成线程安全的集合类，具体用法与 synchronizedList 一样。如：\npublic static \u0026lt;K,V\u0026gt; Map\u0026lt;K,V\u0026gt; synchronizedMap(Map\u0026lt;K,V\u0026gt; m) public static \u0026lt;T\u0026gt; Set\u0026lt;T\u0026gt; synchronizedSet(Set\u0026lt;T\u0026gt; s) 8.2. Arrays 类 Arrays 类是工具类，里面都是静态方法，需要导包。Arrays是用来操作数组中的元素。\n1 public static int binarySearch(Object[] a, Object key) 使用二分搜索法来搜索指定数组，以获得指定对象。在进行此调用之前，必须根据元素的自然顺序对数组进行升序排序（通过 sort(Object[]) 方法）。如果没有对数组进行排序，则结果是不确定的。（如果数组包含不可相互比较的元素（例如，字符串和整数），则无法 根据其元素的自然顺序对数组进行排序，因此结果是不确定的。）如果数组包含多个等于指定对象的元素，则无法保证找到的是哪一个。 1 public static void sort(Object[] a) 根据元素的自然顺序对指定对象数组按升序进行排序。数组中的所有元素都必须实现 Comparable 接口。此外，数组中的所有元素都必须是可相互比较的 1 2 3 4 5 6 7 8 9 public static String toString(long[] a) public static String toString(int[] a) public static String toString(short[] a) public static String toString(char[] a) public static String toString(byte[] a) public static String toString(boolean[] a) public static String toString(float[] a) public static String toString(double[] a) public static String toString(Object[] a) 返回指定数组内容的字符串表示形式。字符串表示形式由数组的元素列表组成，括在方括号（\u0026quot;[]\u0026quot;）中。相邻元素用字符 \u0026ldquo;, \u0026ldquo;（逗号加空格）分隔。可以直接打印输出返回的遍历数组的字符串。有多个重载的方法，其中数组类型包括：boolean[], float[], double[], Object[], byte[], char[], short[], int[], long[]。 1 public static boolean equals(Object[] a, Object[] a2) 比较两个数组的元素是否完全相同(元素个数，对应的内容要一致) 如果两个指定的 Objects 数组彼此相等，则返回 true。如果两个数组包含相同数量的元素，并且两个数组中的所有相应元素对都是相等的，则认为这两个数组是相等的。 1 public static void fill(Object[] a, Object val) 将数组中所有元素赋值为指定的值val。 将指定的 Object 引用分配给指定 Object 数组的每个元素。 8.3. Array 类 Array 类是工具类，里面都是静态方法。直接用类名.使用，需要导包\n1 public final class Arrayextends Object; Array 类提供了动态创建和访问 Java 数组的方法。\n1 public static Object get(Object array, int index); 返回指定数组对象中索引组件的值。如果该值是一个基本类型值，则自动将其包装在一个对象中。 方法会throws IllegalArgumentException, ArrayIndexOutOfBoundsException; 1 public static void set(Object array, int index, Object value); 将指定数组对象中索引组件的值设置为指定的新值。如果数组的类型为基本组件类型，则新值第一个被自动解包。 方法会throws IllegalArgumentException, ArrayIndexOutOfBoundsException; 8.4. LinkedList 类(未整理完) 1 public boolean add(E e) 将指定元素添加到此列表的结尾 1 public int size() 返回此列表的元素数 ","permalink":"https://ktzxy.top/posts/uz9emtro1e/","summary":"Java基础 集合","title":"Java基础 集合"},{"content":"[TOC]\nGrafana模板：10280\n一、Prometheus监控SpringBoot 1.1 pom.xml添加依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-actuator\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-test\u0026lt;/artifactId\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.micrometer\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;micrometer-registry-prometheus\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.1.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 1.2 修改application.yml配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 server: port: 8002 # 配置启动端口号 spring: application: name: mydemo metrics: servo: enabled: false management: endpoints: web: exposure: include: info, health, beans, env, metrics, mappings, scheduledtasks, sessions, threaddump, docs, logfile, jolokia,prometheus base-path: /actuator #默认/actuator 不更改可不用配置 #CORS跨域支持 cors: allowed-origins: http：//example.com allowed-methods: GET,PUT,POST,DELETE prometheus: id: springmetrics endpoint: beans: cache: time-to-live: 10s #端点缓存响应的时间量 health: show-details: always #详细信息显示给所有用户 server: port: 8001 #默认8080 address: 127.0.0.1 #配置此项表示不允许远程连接 #监测 metrics: export: datadog: application-key: ${spring.application.name} web: server: auto-time-requests: false 这里涉及两个port，一个是server port，一个是prometheus port，其中server port则是调用接口使用的端口，而prometheus port则与该服务在prometheus.yml中的port是一致的，不一致的话则会使该服务down。\n1.3 设置启动类Application 1 2 3 4 5 6 7 8 9 10 11 12 @SpringBootApplication public class Springboot2PrometheusApplication { public static void main(String[] args) { SpringApplication.run(Springboot2PrometheusApplication.class, args); } @Bean MeterRegistryCustomizer\u0026lt;MeterRegistry\u0026gt; configurer( @Value(\u0026#34;${spring.application.name}\u0026#34;) String applicationName) { return (registry) -\u0026gt; registry.config().commonTags(\u0026#34;application\u0026#34;, applicationName); } } 开启 actuator 后要注意要防护，请勿将开启 actuator 的服务直接对外。如果你需要这么做，可以新增一个过滤器对 /actuator 进行过滤，只允许内网IP地址访问。\nSpringBoot项目到这里就配置完成了，启动项目，访问http://localhost:8080/actuator/prometheus，如图所示，可以看到一些度量指标。 包含但不限于以下接口都是在开启 actuator 之后可以访问的（默认统一前缀 /actuator）： 1.4 Prometheus配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # my global config global: scrape_interval: 15s # Set the scrape interval to every 15 seconds. Default is every 1 minute. evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute. # scrape_timeout is set to the global default (10s). # Alertmanager configuration alerting: alertmanagers: - static_configs: - targets: # - alertmanager:9093 # Load rules once and periodically evaluate them according to the global \u0026#39;evaluation_interval\u0026#39;. rule_files: # - \u0026#34;first_rules.yml\u0026#34; # - \u0026#34;second_rules.yml\u0026#34; # A scrape configuration containing exactly one endpoint to scrape: # Here it\u0026#39;s Prometheus itself. scrape_configs: - job_name: \u0026#39;prometheus\u0026#39; static_configs: - targets: [\u0026#39;127.0.0.1:9090\u0026#39;] ###以下内容为SpringBoot应用配置 - job_name: \u0026#39;springboot_prometheus\u0026#39; scrape_interval: 5s metrics_path: \u0026#39;/actuator/prometheus\u0026#39; static_configs: - targets: [\u0026#39;127.0.0.1:8080\u0026#39;] 二、Rest接口的编写 编写了一个接口，接口是http rest风格的add接口，具体代码如下所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.annotation.PostConstruct; @RestController @RequestMapping(\u0026#34;/api\u0026#34;) public class OperationController { @Autowired MeterRegistry registry; private Counter counter; private Counter failCounter; @PostConstruct private void init(){ failCounter= registry.counter(\u0026#34;requests_add_fail_total\u0026#34;,\u0026#34;save\u0026#34;,\u0026#34;carson\u0026#34;); counter = registry.counter(\u0026#34;requests_add_total\u0026#34;,\u0026#34;save\u0026#34;,\u0026#34;carson\u0026#34;); } @RequestMapping(value = \u0026#34;/add\u0026#34;,method = RequestMethod.POST) public String add(@Validated String firstName,@Validated String secondName) throws Exception { try{ String name = firstName+secondName; counter.increment(); return name; }catch (Exception e){ failCounter.increment(); throw new Exception(\u0026#34;异常\u0026#34;); } } } 其中init方法则是对prometheus中counter组件进行初始化，而在add接口中则可以直接使用，这里两个指标分别为调用成功的次数与调用失败的次数。\n2.1 模拟调用 通过postman进行调用接口，如下图所示： 2.2 Grafana监控视图的制作 在grafana页面新增dashboard之后，便进入下图所示： 然后选中数据源，并进行metrics语句编写，如下图所示，sum(request_add_total) ,其中sum函数中的字段可以模糊搜索，只要prometheus中的服务是up的。然后图就如下所示，可以看出，调用情况： 三、SpringBoot应用实现案例 3.1 在pom文件添加 1 2 3 4 5 6 7 8 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.micrometer\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;micrometer-registry-prometheus\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-actuator\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 3.2 在代码中添加如下配置： 1 2 3 4 5 6 7 8 9 10 11 12 13 private Counter requestErrorCount; private final MeterRegistry registry; @Autowired public PrometheusCustomMonitor(MeterRegistry registry) { this.registry = registry; } @PostConstruct private void init() { requestErrorCount = registry.counter(\u0026#34;requests_error_total\u0026#34;, \u0026#34;status\u0026#34;, \u0026#34;error\u0026#34;); } public Counter getRequestErrorCount() { return requestErrorCount; } 3.3 在异常处理中添加如下记录: 1 monitor.getRequestErrorCount().increment(); 3.4 在prometheus的配置中添加springboot应用服务监控 1 2 3 4 5 - job_name: \u0026#39;springboot\u0026#39; metrics_path: \u0026#39;/actuator/prometheus\u0026#39; scrape_interval: 5s static_configs: - targets: [\u0026#39;192.168.8.45:8080\u0026#39;] 3.5 Prometheu.yml配置如下: 1 2 3 4 5 - job_name: \u0026#39;springboot\u0026#39; metrics_path: \u0026#39;/actuator/prometheus\u0026#39; scrape_interval: 5s static_configs: - targets: [\u0026#39;192.168.8.45:8080\u0026#39;] 规则文件配置如下: 3.6 在prometheus监控即可查看 企业微信告警效果图: ","permalink":"https://ktzxy.top/posts/xy03pirz6a/","summary":"监控SpringBoot","title":"监控SpringBoot"},{"content":"反射 来源 https://www.liwenzhou.com/posts/Go/13_reflect/\n前言 反射在我们的编码中可能不太常用到，但是其实很多内部方法都应用了反射技术，例如\n1 2 3 4 5 6 7 8 9 10 11 type person struct { Name string `json:\u0026#34;name\u0026#34;` Age int `json:\u0026#34;age\u0026#34;` } func main() { str := `{\u0026#34;name\u0026#34;:\u0026#34;张三\u0026#34;, \u0026#34;age\u0026#34;:15}` var p person json.Unmarshal([]byte(str), \u0026amp;p) fmt.Println(p.Name, p.Age) } 变量的内在机制 Go语言中的变量是分为两部分的:\n类型信息：预先定义好的元信息。 值信息：程序运行过程中可动态变化的。 反射介绍 反射是指在程序运行期对程序本身进行访问和修改的能力。程序在编译时，变量被转换为内存地址，变量名不会被编译器写入到可执行部分。在运行程序时，程序无法获取自身的信息。\n支持反射的语言可以在程序编译期将变量的反射信息，如字段名称、类型信息、结构体信息等整合到可执行文件中，并给程序提供接口访问反射信息，这样就可以在程序运行期获取类型的反射信息，并且有能力修改它们。\nGo程序在运行期使用reflect包访问程序的反射信息。\n在上一篇博客中我们介绍了空接口。 空接口可以存储任意类型的变量，那我们如何知道这个空接口保存的数据是什么呢？ 反射就是在运行时动态的获取一个变量的类型信息和值信息。\nreflect包 在Go语言的反射机制中，任何接口值都由是一个具体类型和具体类型的值两部分组成的(我们在上一篇接口的博客中有介绍相关概念)。 在Go语言中反射的相关功能由内置的reflect包提供，任意接口值在反射中都可以理解为由reflect.Type和reflect.Value两部分组成，并且reflect包提供了reflect.TypeOf和reflect.ValueOf两个函数来获取任意对象的Value和Type\nTypeOf 在Go语言中，使用reflect.TypeOf()函数可以获得任意值的类型对象（reflect.Type），程序通过类型对象可以访问任意值的类型信息。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;reflect\u0026#34; ) func reflectType(x interface{}) { v := reflect.TypeOf(x) fmt.Printf(\u0026#34;type:%v\\n\u0026#34;, v) } func main() { var a float32 = 3.14 reflectType(a) // type:float32 var b int64 = 100 reflectType(b) // type:int64 } type name和type kind 在反射中关于类型还划分为两种：类型（Type）和种类（Kind）。因为在Go语言中我们可以使用type关键字构造很多自定义类型，而种类（Kind）就是指底层的类型，但在反射中，当需要区分指针、结构体等大品种的类型时，就会用到种类（Kind）。 举个例子，我们定义了两个指针类型和两个结构体类型，通过反射查看它们的类型和种类。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;reflect\u0026#34; ) type myInt int64 func reflectType(x interface{}) { t := reflect.TypeOf(x) fmt.Printf(\u0026#34;type:%v kind:%v\\n\u0026#34;, t.Name(), t.Kind()) } func main() { var a *float32 // 指针 var b myInt // 自定义类型 var c rune // 类型别名 reflectType(a) // type: kind:ptr reflectType(b) // type:myInt kind:int64 reflectType(c) // type:int32 kind:int32 type person struct { name string age int } type book struct{ title string } var d = person{ name: \u0026#34;沙河小王子\u0026#34;, age: 18, } var e = book{title: \u0026#34;《跟小王子学Go语言》\u0026#34;} reflectType(d) // type:person kind:struct reflectType(e) // type:book kind:struct } Go语言的反射中像数组、切片、Map、指针等类型的变量，它们的.Name()都是返回空。\n在reflect包中定义的Kind类型如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 type Kind uint const ( Invalid Kind = iota // 非法类型 Bool // 布尔型 Int // 有符号整型 Int8 // 有符号8位整型 Int16 // 有符号16位整型 Int32 // 有符号32位整型 Int64 // 有符号64位整型 Uint // 无符号整型 Uint8 // 无符号8位整型 Uint16 // 无符号16位整型 Uint32 // 无符号32位整型 Uint64 // 无符号64位整型 Uintptr // 指针 Float32 // 单精度浮点数 Float64 // 双精度浮点数 Complex64 // 64位复数类型 Complex128 // 128位复数类型 Array // 数组 Chan // 通道 Func // 函数 Interface // 接口 Map // 映射 Ptr // 指针 Slice // 切片 String // 字符串 Struct // 结构体 UnsafePointer // 底层指针 ) ValueOf reflect.ValueOf()返回的是reflect.Value类型，其中包含了原始值的值信息。reflect.Value与原始值之间可以互相转换。\nreflect.Value类型提供的获取原始值的方法如下：\n方法 说明 Interface() interface {} 将值以 interface{} 类型返回，可以通过类型断言转换为指定类型 Int() int64 将值以 int 类型返回，所有有符号整型均可以此方式返回 Uint() uint64 将值以 uint 类型返回，所有无符号整型均可以此方式返回 Float() float64 将值以双精度（float64）类型返回，所有浮点数（float32、float64）均可以此方式返回 Bool() bool 将值以 bool 类型返回 Bytes() []bytes 将值以字节数组 []bytes 类型返回 String() string 将值以字符串类型返回 通过反射获取值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 func reflectValue(x interface{}) { v := reflect.ValueOf(x) k := v.Kind() switch k { case reflect.Int64: // v.Int()从反射中获取整型的原始值，然后通过int64()强制类型转换 fmt.Printf(\u0026#34;type is int64, value is %d\\n\u0026#34;, int64(v.Int())) case reflect.Float32: // v.Float()从反射中获取浮点型的原始值，然后通过float32()强制类型转换 fmt.Printf(\u0026#34;type is float32, value is %f\\n\u0026#34;, float32(v.Float())) case reflect.Float64: // v.Float()从反射中获取浮点型的原始值，然后通过float64()强制类型转换 fmt.Printf(\u0026#34;type is float64, value is %f\\n\u0026#34;, float64(v.Float())) } } func main() { var a float32 = 3.14 var b int64 = 100 reflectValue(a) // type is float32, value is 3.140000 reflectValue(b) // type is int64, value is 100 // 将int类型的原始值转换为reflect.Value类型 c := reflect.ValueOf(10) fmt.Printf(\u0026#34;type c :%T\\n\u0026#34;, c) // type c :reflect.Value } 通过反射设置变量的值 想要在函数中通过反射修改变量的值，需要注意函数参数传递的是值拷贝，必须传递变量地址才能修改变量值。而反射中使用专有的Elem()方法来获取指针对应的值。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;reflect\u0026#34; ) func reflectSetValue1(x interface{}) { v := reflect.ValueOf(x) if v.Kind() == reflect.Int64 { v.SetInt(200) //修改的是副本，reflect包会引发panic } } func reflectSetValue2(x interface{}) { v := reflect.ValueOf(x) // 反射中使用 Elem()方法获取指针对应的值 if v.Elem().Kind() == reflect.Int64 { v.Elem().SetInt(200) } } func main() { var a int64 = 100 // reflectSetValue1(a) //panic: reflect: reflect.Value.SetInt using unaddressable value reflectSetValue2(\u0026amp;a) fmt.Println(a) } isNil()和isValid() isNil() 1 func (v Value) IsNil() bool IsNil()报告v持有的值是否为nil。v持有的值的分类必须是通道、函数、接口、映射、指针、切片之一；否则IsNil函数会导致panic。\nisValid() 1 func (v Value) IsValid() bool IsValid()返回v是否持有一个值。如果v是Value零值会返回假，此时v除了IsValid、String、Kind之外的方法都会导致panic。\n举个例子 IsNil()常被用于判断指针是否为空；IsValid()常被用于判定返回值是否有效。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func main() { // *int类型空指针 var a *int fmt.Println(\u0026#34;var a *int IsNil:\u0026#34;, reflect.ValueOf(a).IsNil()) // nil值 fmt.Println(\u0026#34;nil IsValid:\u0026#34;, reflect.ValueOf(nil).IsValid()) // 实例化一个匿名结构体 b := struct{}{} // 尝试从结构体中查找\u0026#34;abc\u0026#34;字段 fmt.Println(\u0026#34;不存在的结构体成员:\u0026#34;, reflect.ValueOf(b).FieldByName(\u0026#34;abc\u0026#34;).IsValid()) // 尝试从结构体中查找\u0026#34;abc\u0026#34;方法 fmt.Println(\u0026#34;不存在的结构体方法:\u0026#34;, reflect.ValueOf(b).MethodByName(\u0026#34;abc\u0026#34;).IsValid()) // map c := map[string]int{} // 尝试从map中查找一个不存在的键 fmt.Println(\u0026#34;map中不存在的键：\u0026#34;, reflect.ValueOf(c).MapIndex(reflect.ValueOf(\u0026#34;娜扎\u0026#34;)).IsValid()) } 结构体反射 与结构体相关的方法 任意值通过reflect.TypeOf()获得反射对象信息后，如果它的类型是结构体，可以通过反射值对象（reflect.Type）的NumField()和Field()方法获得结构体成员的详细信息。\nreflect.Type中与获取结构体成员相关的的方法如下表所示。\n方法 说明 Field(i int) StructField 根据索引，返回索引对应的结构体字段的信息。 NumField() int 返回结构体成员字段数量。 FieldByName(name string) (StructField, bool) 根据给定字符串返回字符串对应的结构体字段的信息。 FieldByIndex(index []int) StructField 多层成员访问时，根据 []int 提供的每个结构体的字段索引，返回字段的信息。 FieldByNameFunc(match func(string) bool) (StructField,bool) 根据传入的匹配函数匹配需要的字段。 NumMethod() int 返回该类型的方法集中方法的数目 Method(int) Method 返回该类型方法集中的第i个方法 MethodByName(string)(Method, bool) 根据方法名返回该类型方法集中的方法 StructField类型 StructField类型用来描述结构体中的一个字段的信息。\nStructField的定义如下：\n1 2 3 4 5 6 7 8 9 10 11 type StructField struct { // Name是字段的名字。PkgPath是非导出字段的包路径，对导出字段该字段为\u0026#34;\u0026#34;。 // 参见http://golang.org/ref/spec#Uniqueness_of_identifiers Name string PkgPath string Type Type // 字段的类型 Tag StructTag // 字段的标签 Offset uintptr // 字段在结构体中的字节偏移量 Index []int // 用于Type.FieldByIndex时的索引切片 Anonymous bool // 是否匿名字段 } 结构体反射示例 当我们使用反射得到一个结构体数据之后可以通过索引依次获取其字段信息，也可以通过字段名去获取指定的字段信息。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type student struct { Name string `json:\u0026#34;name\u0026#34;` Score int `json:\u0026#34;score\u0026#34;` } func main() { stu1 := student{ Name: \u0026#34;小王子\u0026#34;, Score: 90, } t := reflect.TypeOf(stu1) fmt.Println(t.Name(), t.Kind()) // student struct // 通过for循环遍历结构体的所有字段信息 for i := 0; i \u0026lt; t.NumField(); i++ { field := t.Field(i) fmt.Printf(\u0026#34;name:%s index:%d type:%v json tag:%v\\n\u0026#34;, field.Name, field.Index, field.Type, field.Tag.Get(\u0026#34;json\u0026#34;)) } // 通过字段名获取指定结构体字段信息 if scoreField, ok := t.FieldByName(\u0026#34;Score\u0026#34;); ok { fmt.Printf(\u0026#34;name:%s index:%d type:%v json tag:%v\\n\u0026#34;, scoreField.Name, scoreField.Index, scoreField.Type, scoreField.Tag.Get(\u0026#34;json\u0026#34;)) } } 接下来编写一个函数printMethod(s interface{})来遍历打印s包含的方法。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 // 给student添加两个方法 Study和Sleep(注意首字母大写) func (s student) Study() string { msg := \u0026#34;好好学习，天天向上。\u0026#34; fmt.Println(msg) return msg } func (s student) Sleep() string { msg := \u0026#34;好好睡觉，快快长大。\u0026#34; fmt.Println(msg) return msg } func printMethod(x interface{}) { t := reflect.TypeOf(x) v := reflect.ValueOf(x) fmt.Println(t.NumMethod()) for i := 0; i \u0026lt; v.NumMethod(); i++ { methodType := v.Method(i).Type() fmt.Printf(\u0026#34;method name:%s\\n\u0026#34;, t.Method(i).Name) fmt.Printf(\u0026#34;method:%s\\n\u0026#34;, methodType) // 通过反射调用方法传递的参数必须是 []reflect.Value 类型 var args = []reflect.Value{} v.Method(i).Call(args) } } 反射是把双刃剑 反射是一个强大并富有表现力的工具，能让我们写出更灵活的代码。但是反射不应该被滥用，原因有以下三个。\n基于反射的代码是极其脆弱的，反射中的类型错误会在真正运行的时候才会引发panic，那很可能是在代码写完的很长时间之后。 大量使用反射的代码通常难以理解。 反射的性能低下，基于反射实现的代码通常比正常代码运行速度慢一到两个数量级 练习题 编写代码利用反射实现一个ini文件的解析器程序\n首先有一个ini文件\n1 2 3 4 5 [mysql] address=127.0.0.1 port=3306 username=root password=root 然后再是一个加载配置的函数，init.go\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package main import ( \u0026#34;fmt\u0026#34; \u0026#34;reflect\u0026#34; ) // ini配置文件解析器 // MysqlConfig MySQL配置结构体 type MysqlConfig struct { Address string `ini:\u0026#34;address\u0026#34;` Port int `ini:\u0026#34;port\u0026#34;` Username string `ini:\u0026#34;username\u0026#34;` Password string `ini:\u0026#34;password\u0026#34;` } func loadIni(x interface{}) { v := reflect.ValueOf(x) fmt.Println(v) } func main() { var mc MysqlConfig loadIni(\u0026amp;mc) fmt.Println(mc.Address, mc.Port, mc.Username, mc.Password) } ","permalink":"https://ktzxy.top/posts/1b8vaow1rl/","summary":"反射","title":"反射"},{"content":" 本笔记收集与总结一些 Java 程序编写的最佳实践\n1. 代码最佳样板 1.1. 定义工具类 常见的工具类定义如下：\n1 2 3 4 5 6 7 8 9 10 /** 工具类示例 */ public class CodeSample { /** 常量值 */ public final static int CONST_VALUE = 123; /** 静态工具方法 */ public static int sum(int a, int b) { return a + b; } } 以上工具类定义存在的问题：\n修饰符顺序不规范。Java 语言规范建议使用“static final”，而不是“final static”。记住这么一条规则：静态常量，静态（static）在前，常量（final）在后。 工具类可以被继承覆盖。如果定义一个类继承 CodeSample，就可以对其工具类中的常量和方法进行覆盖，导致不能确定是否使用了原工具类中的常量和方法。例如：对于 Apache 提供的工具类，很多程序员都喜欢定义相同名称的工具类，并让这个工具类继承 Apache 的工具类，并在这个类中添加自己的实现方法。不推荐这种做法的，因为使用时不能确定调用的是 Apache 工具类提供的常量和方法，还是被覆盖的常量和方法。最好的办法，就是对工具类添加 final 关键字，让此工具类不能被继承和覆盖。 工具类可以被实例化。对于工具类来说，没有必要进行实例化。所以建议将构造方法设置为私有，并在方法中抛出 UnsupportedOperationException（不支持的操作异常）。 工具类最佳定义方式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public final class CodeSample { /** 私有构造方法 */ public CodeSample() { throw new UnsupportedOperationException(); } /** 常量值 */ public static final int CONST_VALUE = 123; /** 静态工具方法 */ public static int sum(int a, int b) { return a + b; } } 1.2. 定义枚举类 常见的枚举类定义如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /** 枚举类示例 */ public enum SampleEnum { /** 枚举实例 */ ONE(1, \u0026#34;one\u0026#34;), TWO(2, \u0026#34;two\u0026#34;), THREE(3, \u0026#34;three\u0026#34;); /** 属性 */ private Integer value; private String desc; private SampleEnum(Integer value, String desc) { this.value = value; this.desc = desc; } /** 获取属性值 */ public Integer getValue() { return value; } public String getDesc() { return desc; } } 以上枚举类定义存在的问题：\n构造方法的修饰符 private 可缺省。 成员变量建议使用基础类型。用包装类型做为枚举的属性本身并没有什么问题。但是，本着能用基础类型就用基础类型的规则，所以建议使用基础类型 int。 成员变量建议使用 final 字段。目的是为了避免枚举对外提供了修改成员属性的方法，如果被调用，就会把枚举值修改，导致应用程序出错。因此建议对字段添加 final 修饰符，从而避免字段值被恶意篡改。 枚举类最佳定义方式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public enum SampleEnum { /** 枚举实例 */ ONE(1, \u0026#34;one\u0026#34;), TWO(2, \u0026#34;two\u0026#34;), THREE(3, \u0026#34;three\u0026#34;); /** 属性 */ private final int value; private final String desc; SampleEnum(int value, String desc) { this.value = value; this.desc = desc; } /** 获取属性值 */ public int getValue() { return value; } public String getDesc() { return desc; } } 1.3. 定义集合常量 常见的集合常量定义如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public final class CodeSample { /** List 常量 */ public static final List\u0026lt;Integer\u0026gt; CONST_VALUE_LIST = Arrays.asList(1, 2, 3); /** Set 常量 */ public static final Set\u0026lt;Integer\u0026gt; CONST_VALUE_SET = new HashSet\u0026lt;\u0026gt;(Arrays.asList(1, 2, 3)); /** Map 常量 */ public static final Map\u0026lt;Integer, String\u0026gt; CONST_VALUE_MAP; static { CONST_VALUE_MAP = new HashMap\u0026lt;\u0026gt;(); CONST_VALUE_MAP.put(1, \u0026#34;value1\u0026#34;); CONST_VALUE_MAP.put(2, \u0026#34;value2\u0026#34;); CONST_VALUE_MAP.put(3, \u0026#34;value3\u0026#34;); } // ...省略 } 以上集合常量定义存在的问题：由于普通的集合对象（如 ArrayList、HashMap、HashSet 等）都是可变集合对象，即便是定义为静态常量，也可以通过操作方法进行修改。所以示例定义的都并不是真正意义上的集合常量。其中，Arrays.asList 方法生成的内部 ArrayList 不能执行 add/remove/clear 方法，但是可以执行方法，也属于可变集合对象。\n集合常量最佳定义方式：在 JDK 中，Collections 工具类中提供一套方法，用于把可变集合对象变为不可变（不可修改，修改时会抛出 UnsupportedOperationException 异常）集合对象。因此可以利用这套方法定义集合静态常量。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public final class CodeSample { /** List 常量 */ public static final List\u0026lt;Integer\u0026gt; CONST_VALUE_LIST = Collections.unmodifiableList(Arrays.asList(1, 2, 3)); /** Set 常量 */ public static final Set\u0026lt;Integer\u0026gt; CONST_VALUE_SET = Collections.unmodifiableSet(new HashSet\u0026lt;\u0026gt;(Arrays.asList(1, 2, 3))); /** Map 常量 */ public static final Map\u0026lt;Integer, String\u0026gt; CONST_VALUE_MAP; static { Map\u0026lt;Integer, String\u0026gt; tempMap = new HashMap\u0026lt;\u0026gt;(); tempMap.put(1, \u0026#34;value1\u0026#34;); tempMap.put(2, \u0026#34;value2\u0026#34;); tempMap.put(3, \u0026#34;value3\u0026#34;); CONST_VALUE_MAP = Collections.unmodifiableMap(tempMap); } // ...省略 } 1.4. 定义数组常量 常见的数组常量定义如下：\n1 2 3 4 5 public final class CodeSample { /** 常量值数组 */ public static final int[] CONST_VALUE_ARRAY = new int[]{1, 2, 3}; // ...省略 } 以上数组常量定义存在的问题：可以通过下标修改数组值，导致数组常量的值可变。因此并不是真正意义上的数组常量。\n数组常量最佳定义方式：使用“私有数组常量+公有克隆方法”的解决方案。先定义一个私有数组常量，保证不会被外部类使用；再定义一个获取数组常量方法，并返回一个数组常量的克隆值。\n1 2 3 4 5 6 7 8 9 public final class CodeSample { /** 常量值数组 */ private static final int[] CONST_VALUE_ARRAY = new int[]{1, 2, 3}; /** 定义获取常量值数组的方法 */ public static int[] getConstValueArray() { return CONST_VALUE_ARRAY.clone(); } // ...省略 } Notes: 由于每次返回的是一个克隆数组，即便修改了克隆数组的常量值，也不会导致原始数组常量值的修改。\n1.5. 定义多条件表达式 程序开发中，常见多个条件判断处理方式如下：\n利用运算符\u0026amp;\u0026amp;（或||）直接拼接，这种做法会导致条件判断过长，不利于后期维护。 利用运算符=和\u0026amp;\u0026amp;（或||）级联拼接。就把\u0026amp;\u0026amp;（或||）连接符拆开，利用运算符=和\u0026amp;\u0026amp;（或||）级联进行拼接。这种方式并不能减少方法的圈复杂度。 利用动态无参数 Lambda 表达式列表。即把每个条件表达式作为 BooleanSupplier 对象存在列表中，然后依次执行条件表达式得出最后结果。通过 SonarLint 插件扫描，没有提示任何问题。但是，每次都动态添加 Lambda 表达式，就会导致程序效率低下。 利用静态有参数 Lambda 表达式列表。要想固化 Lambda 表达式，就必须动态传入 AuditDataVO 对象。这里，采用 Predicate\u0026lt;AuditDataVO\u0026gt; 来接收 Lambda 表达式，在 Lambda 表达式中指定 AuditDataVO 对象 data。然后在 for 循环中，依次指定 AuditDataVO 对象 data，并计算表达式的值。 态有参数 Lambda 表达式列表这种方式的适用条件：\n适合于\u0026amp;\u0026amp;（或||）连接大量条件表达式的情况； 适合于每个条件表达式都需要传入相同参数的情况，如果每个条件表达式传入参数不同，只能使用动态无参数 Lambda 表达式列表方法； 如果需要传入两个参数，可以使用 BiPredicate 类型来接收 Lambda 表达式；如果需要传入多个参数，则需要自定义方法接口。 2. 代码精简最佳实践 2.1. 利用语法精简代码 利用三元表达式来取代一些 if-else 判断。Tips: 对于包装类型的算术计算，需要注意避免拆包时的空指针问题。 从 Java 5 起，提供了 for-each 循环来简化了数组和集合的循环遍历。For-each 循环允许无需保持传统 for 循环中的索引就可以遍历数组，或在使用迭代器时无需在 while 循环中调用 hasNext 方法和 next 方法就可以遍历集合。 所有实现 Closeable 接口的“资源”，均可采用 try-with-resource 的方式进行简化资源关闭的处理。 在方法中利用 return 关键字，可以提前函数返回，避免定义中间变量、或者做其他无用的逻辑处理。 利用 static 关键字，可以把字段变成静态字段，也可以把函数变为静态函数，调用时就无需初始化类对象。 Java 8 版本以后，利用 lambda 表达式替代匿名内部类的使用，在简化了代码的同时，更突出了原有匿名内部类中真正有用的那部分代码。 Java 8 版本以后，可以利用方法引用（::）简化 lambda 表达式，省略变量声明和函数调用。 当程序中大量使用同一静态常量和函数时，可以使用静态导入（import static），简化静态常量和函数的引用。静态引入容易造成代码阅读困难，所以在实际项目中应该警慎使用。 Java 的异常分为两类：Checked 异常和 Unchecked 异常。Unchecked 异常继承了 RuntimeException，特点是代码不需要处理它们也能通过编译，所以它们称作Unchecked 异常。利用 Unchecked 异常，可以避免不必要的 try-catch 和 throws 异常处理。 2.2. 利用注解精简代码 利用 Lombok 工具库，简化大量重复的样板代码。 利用 Validation 注解，简化方法参数的校验处理。 使用 Spring 的 @NonNull 注解，用于标注参数或返回值非空，适用于项目内部团队协作。只要实现方和调用方遵循规范，可以避免大胆地省略不必要的空值判断。 注解声明精简。 当注解属性值跟默认值一致时，可以删除该属性赋值； 当注解只有 value 属性时，可以去掉 value 进行简写； 当注解属性组合等于另一个特定注解时，直接采用该特定注解。 2.3. 利用常用类的 API 精简代码 利用构造方法，可以简化对象的初始化和设置属性操作。对于属性字段较少的类，可以自定义构造方法。注意：如果属性字段被替换时，存在构造函数初始化赋值问题。 利用 Set 集合的 add 方法的返回值，可以直接判断当时加入的值是否已经存在，从而避免调用 contains 方法判断存在。 利用 Map 的 computeIfAbsent 方法，可以保证获取到的对象非空，从而避免了不必要的空判断和重新设置值。 利用链式编程，也叫级联式编程，调用对象的函数时返回一个 this 对象指向对象本身，达到链式效果，可以级联调用。链式编程的优点是：编程性强、可读性强、代码简洁。 在 Java 8 版本，引入了一个 Optional 类，该类是一个可以为 null 的容器对象。利用此类可以简化判空的操作 利用 Java 8 版本中的流（Stream）简化集合相关的操作，它允许以声明式处理数据集合，可以看成为一个遍历数据集的高级迭代器。流主要有三部分构成：获取一个数据源→数据转换→执行操作获取想要的结果。每次转换原有 Stream 对象不改变，返回一个新的 Stream 对象，这就允许对其操作可以像链条一样排列，形成了一个管道。流（Stream）提供的功能非常有用，主要包括匹配、过滤、汇总、转化、分组、分组汇总等功能。 2.4. 利用常用的工具方法精简代码 使用一些工具类库提供的集合工具类来避免空值判断，例如 apache 的 CollectionUtils.isNotEmpty(Collection\u0026lt;?\u0026gt; coll) 方法 利用一些工具方法来避免条件判断，例如：Math.max(double a, double b) 取代使用 if-else 来获取最大值 使用 Spring 提供的 Assert 断言工具类来简化异常判断。注意：可能有些插件不认同这种判断，导致使用该对象时会有空指针警告。 把测试用例数据以 JSON 格式存入文件中，通过 JSON 的 parseObject 和 parseArray 方法解析成对象。虽然执行效率上有所下降，但可以减少大量的赋值语句，从而精简了测试代码。建议：JSON 文件名最好以被测试的方法命名，如果有多个版本可以用数字后缀表示。 自行封装一些工具类 2.5. 利用数据结构精简代码 对于固定上下限范围的 if-else 语句，可以用数组+循环来简化。 对于映射关系的 if-else 语句，可以用 Map 来简化。此外，此规则同样适用于简化映射关系的 switch 语句。 利用容器类简化。Java 不像 Python 和 Go，方法不支持返回多个对象。如果需要返回多个对象，就必须自定义类，或者利用容器类。常见的容器类有 Apache 的 Pair 类和 Triple 类，Pair 类支持返回 2 个对象，Triple 类支持返回 3 个对象。 ThreadLocal 提供了线程专有对象，可以在整个线程生命周期中随时取用。利用 ThreadLocal 保存线程上下文对象，可以避免不必要的参数传递。注意：ThreadLocal有一定的内存泄露的风险，尽量在业务代码结束前调用 remove 方法进行数据清除。 3. 代码编写最佳实践 3.1. 常量\u0026amp;变量 直接赋值常量值，只是创建了一个对象引用，而这个对象引用指向常量值。禁止声明新对象 1 2 3 4 // 反例 Long i = new Long(1L); // 正例 Long i = 1L; 当成员变量值无需改变时，尽量定义为静态常量。在类的每个对象实例中，每个成员变量都有一份副本，而成员静态常量只有一份实例。 尽量使用基本数据类型，避免自动装箱和拆箱。JVM 支持基本类型与对应包装类的自动转换，被称为自动装箱和拆箱。装箱和拆箱都是需要 CPU 和内存资源的，所以应尽量避免使用自动装箱和拆箱。 如果变量的初值会被覆盖，就没有必要给变量赋初值。 尽量使用函数内的基本类型临时变量。在函数内，基本类型的参数和临时变量都保存在栈（Stack）中，访问速度较快；对象类型的参数和临时变量的引用都保存在栈（Stack）中，内容都保存在堆（Heap）中，访问速度较慢。在类中，任何类型的成员变量都保存在堆（Heap）中，访问速度较慢。 在老版 JDK 中是建议“尽量不要在循环体外定义变量”，但是在新版的 JDK 中已经做了优化。通过对编译后的字节码分析，变量定义在循环体外和循环体内没有本质的区别，运行效率基本上是一样的。反而根据“局部变量作用域最小化”原则，变量定义在循环体内更科学更便于维护，避免了延长大对象生命周期导致延缓回收问题。 不可变的静态常量或者成员，尽量使用非线程安全类。为了提高性能。 3.2. 对象\u0026amp;类 禁止使用 JSON 转化对象。这种对象转化方式，虽然在功能上没有问题，但是在性能上却存在问题。 尽量不使用反射赋值对象。用反射赋值主要优点是节省了代码量，但缺点就是性能的下降。 采用 Lambda 表达式替换内部匿名类。实际 Lambda 表达式并非匿名内部类的语法糖。Lambda 表达式在大多数虚拟机中采用 invokeDynamic 指令实现，相对于匿名内部类在效率上会更高一些。 尽量避免定义不必要的子类。多一个类就需要多一份类加载。 尽量指定类的 final 修饰符。为类指定 final 修饰符，可以让该类不可以被继承。如果指定了一个类为 final，则该类所有的方法都是 final 的，Java 编译器会寻找机会内联所有的 final 方法。内联对于提升 Java 运行效率作用重大，能够使性能平均提高 50%。值得注意：使用 Spring 的 AOP 特性时，需要对 Bean 进行动态代理，如果 Bean 类添加了 final 修饰，会导致异常。 3.3. 方法 把跟类成员变量无关的方法声明成静态方法。静态方法的好处就是不用生成类的实例就可以直接调用。静态方法不再属于某个对象，而是属于它所在的类。只需要通过其类名就可以访问，不需要再消耗资源去反复创建对象。即便在类内部的私有方法，如果没有使用到类成员变量，也应该声明为静态方法。 尽量使用基本数据类型作为方法参数、方法返回值类型，避免不必要的装箱、拆箱和空指针判断。在 JDK 类库的方法中，很多方法返回值都采用了基本数据类型。比 如 ：Collection.isEmpty()和 Map.size() 协议方法参数值、返回值非空，避免不必要的空指针判断。协议编程，可以 @NonNull 和 @Nullable 标注参数 如果被调用方法已支持判空处理，调用方法无需再进行判空处理。 尽量避免不必要的函数封装。因为方法调用会引起入栈和出栈，导致消耗更多的 CPU 和内存。但为了使代码更简洁、更清晰、更易维护，增加一定的方法调用所带来的性能损耗是值得的。 尽量指定方法的 final 修饰符。注意：所有的 private 方法会隐式地被指定 final 修饰符，所以无须再为其指定 final 修饰符。 尽量使用方法传递代替值传递。 可以避免不必要的方法计算。比如 Optional 类的 orElse(T value) 方法和 orElseGet(Supplier supplier) 方法： orElse(T value) 方法无论前面 Optional 容器值是 null 还是 nonNull，都会提前执行 orElse 里的方法； orElseGet(Supplier supplier) 方法并不会，只会在 Optional 容器值为 null 时才调用 orElseGet 里的方法。 3.4. 表达式 尽量减少方法的重复调用。例如，使用普通 for 循环集合时，将 list.size() 方法提取在变量，避免每次循环时重新调用方法获取集合大小。 尽量避免不必要的方法调用。 尽量使用移位来代替正整数乘除。用移位操作可以极大地提高性能。对于乘除 2^n（n 为正整数）的正整数计算，可以用移位操作来代替。 1 2 3 4 5 6 // 反例 int num1 = a * 4; int num2 = a / 4; // 正例 int num1 = a \u0026lt;\u0026lt; 2; int num2 = a \u0026gt;\u0026gt; 2; 提取公共表达式，只计算一次值，然后重复利用值，避免重复计算。 尽量不在条件表达式中用 ! 取反。取反会多一次计算，如果没有必要则优化掉。 对于多常量选择分支，尽量使用 switch 语句而不是 if-else 语句。if-else 语句，每个 if 条件语句都要加装计算，直到 if 条件语句为 true 为止。switch 语句进行了跳转优化，Java 中采用 tableswitch 或 lookupswitch 指令实现，对于多常量选择分支处理效率更高。经过试验证明：在每个分支出现概率相同的情况下，低于 5 个分支时 if-else 语句效率更高，高于 5 个分支时 switch 语句效率更高。 3.5. 字符串 尽量不要使用正则表达式匹配。正则表达式匹配效率较低，尽量使用字符串匹配操作。 尽量使用字符替换字符串。字符串的长度不确定，而字符的长度固定为 1，查找和匹配的效率自然提高了。 尽量使用 StringBuilder 进行字符串拼接。String 是 final 类，内容不可修改，所以每次字符串拼接都会生成一个新对象。StringBuilder 在初始化时申请了一块内存，往后的字符串拼接都在这块内存中执行，不会申请新内存和生成新对象。 不要使用\u0026quot;\u0026quot; +转化字符串。此方式使用方便但是效率低，建议使用 String.valueOf 方法。 尽量预编译正则表达式。Pattern.compile 方法的性能开销很大。但这个方法隐藏在很多常用的方法里。比如：String.matches、String.replaceAll、String.split 等函数。对于多次调用这些方法，可以考虑预编译正则表达式以提高执行效率，并参考原有实现代码编写优化后的代码。 3.6. 数组 不要使用循环拷贝数组，尽量使用 System.arraycopy 或 Arrays.copyOf 拷贝数组。 集合转化为类型 T 数组时，尽量传入空数组 T[0]。将集合转换为数组有 2 种形式：toArray(new T[n]) 和 toArray(new T[0])。在旧的 Java 版本中，建议使用 toArray(new T[n])，因为创建数组时所需的反射调用非常慢。在 OpenJDK6 后，反射调用是内在的，使得性能得以提高，toArray(new T[0]) 比 toArray(new T[n]) 效率更高。此外，toArray(new T[n]) 比 toArray(new T[0]) 多获取一次列表大小，如果计算列表大小耗时过长，也会导致toArray(new T[n])效率降低。 集合转化为 Object 数组时，没有必要使用 toArray[new Object[0]]，尽量使用 toArray() 方法。避免了类型的判断，也避免了空数组的申请，所以效率会更高。 3.7. 集合 初始化集合时，尽量指定集合大小。Java 集合初始化时都会指定一个默认大小，当默认大小不再满足数据需求时就会扩容，每次扩容的时间复杂度有可能是 O(n)。所以尽量指定预知的集合大小，就能避免或减少集合的扩容次数。 不要使用循环拷贝集合，尽量使用 JDK 提供的方法拷贝集合。JDK 提供的方法可以一步指定集合的容量，避免多次扩容浪费时间和空间。同时这些方法的底层也是调用 System.arraycopy 方法实现，进行数据的批量拷贝效率更高。 尽量使用 Arrays.asList 转化数组为列表。（原因同上） 直接迭代需要使用的集合。 不要使用 size 方法检测空，必须使用 isEmpty 方法检测空。使用 isEmpty 方法使得代码更易读，并且可以获得更好的性能。任何 isEmpty 方法实现的时间复杂度都是 O(1)，但是某些 size 方法实现的时间复杂度有可能是 O(n)。 非随机访问的 List，尽量使用迭代代替随机访问。对于列表，可分为随机访问和非随机访问两类，可以用是否实现 RandomAccess 接口判断。随机访问列表，直接通过 get 方法获取数据不影响效率。而非随机访问列表，通过 get 获取数据效率极低。 尽量使用 HashSet 判断值存在。List 的 contains 方法普遍时间复杂度是 O(n)，而 HashSet 的时间复杂度为 O(1)。如果需要频繁调用 contains 方法查找数据，建议先将 List 转换成 HashSet。 避免先判断存在再进行获取。如果需要先判断存在再进行获取，可以直接获取并判断空，从而避免了二次查找操作。 3.8. 异常 直接捕获对应的异常，避免用 instanceof 判断，效率更高代码更简洁。 尽量避免在循环中捕获异常。当循环体抛出异常后并且无需循环继续执行时，就没有必要在循环体中捕获异常。因为过多的捕获异常会降低程序执行效率。 禁止使用异常控制业务流程。相对于条件表达式，异常的处理效率更低。 3.9. 缓冲区 初始化时尽量指定缓冲区大小，避免多次扩容浪费时间和空间。 尽量重复使用同一缓冲区。针对缓冲区，Java 虚拟机需要花时间生成对象，还要花时间进行垃圾回收处理。因此尽量重复利用缓冲区。 尽量设计使用同一缓冲区。去掉每个转化方法中的缓冲区申请，申请一个缓冲区给每个转化方法使用。从时间上来说，节约了大量缓冲区的申请释放时间；从空间上来说，节约了大量缓冲区的临时存储空间 尽量使用缓冲流减少 IO 操作。使用缓冲流 BufferedReader、 BufferedWriter、 BufferedInputStream、 BufferedOutputStream 等，可以大幅减少 IO 次数并提升 IO 速度。可以根据实际情况手动指定缓冲流的大小，把缓冲流的缓冲作用发挥到最大。 3.10. 线程 在单线程中，尽量使用非线程安全类，避免了不必要的同步开销。 在多线程中，尽量使用线程安全类，保证线程安全。 尽量减少同步代码块范围。在一个方法中，可能只有一小部分的逻辑是需要同步控制的，如果同步控制了整个方法会影响执行效率。所以尽量减少同步代码块的范围，只对需要进行同步的代码进行同步。 尽量合并为同一同步代码块。同步代码块是有性能开销的，如果确定可以合并为同一同步代码块，就应该尽量合并为同一同步代码块。 尽量使用线程池减少线程开销。多线程中两个必要的开销：线程的创建和上下文切换。采用线程池，可以尽量地避免这些开销。 4. 编码规则建议 使用通用工具函数 拆分超大函数。主要原因如下： 函数越短小精悍，功能就越单一，往往生命周期较长； 一个函数越长，就越不容易理解和维护，维护人员不敢轻易修改； 在过长函数中，往往含有难以发现的重复代码。 同一函数内的代码处理的内容尽量一致 多封装公共函数，减少代码行数，提高代码质量，可读性可维护性更强。 把获取参数值的处理逻辑封装为函数。把获取参数值从业务函数中独立，使业务逻辑更清晰，也可以在代码中重复使用。 减少函数代码层级（代码的缩进）。建议函数代码层级在 1-4 之间，过多的缩进会让函数难以阅读。 将复杂条件表达式封装为函数。 尽量避免不必要的空指针判断。这些不必要的空指针判断，基本属于永远不执行的 Death 代码，删除有助于代码维护。 内部函数尽量使用基础类型。避免了隐式封装类型的打包和拆包，也可避免空指针判断。 尽量避免返回的数组、列表、对象为 null，避免调用函数的空指针判断。 封装函数传入参数为对象。Java 规范不允许函数参数太多，不便于维护也不便于扩展。 尽量用函数替换匿名内部类的实现。在匿名内部类（包括 Lambda 表达式）中可以直接访问外部类的成员，包括类的成员变量、函数的内部变量。正因为可以随意访问外部变量，所以会导致代码边界不清晰。首先推荐用 Lambda 表达式简化匿名内部类，其次推荐用函数替换复杂的Lambda 表达式的实现。 提前 return 精简不必要的代码。 仅在需要时才定义变量。 ","permalink":"https://ktzxy.top/posts/8lzz4vfyny/","summary":"Java扩展 代码简洁之道","title":"Java扩展 代码简洁之道"},{"content":"在日常mysql运维中，经常要查询当前mysql下正在执行的sql语句及其他在跑的mysql相关线程，这就用到mysql processlist这个命令了。\n1 2 3 4 mysql\u0026gt; show processlist; //查询正在执行的sql语句 mysql\u0026gt; show full processlist; //查询正在执行的完整sql语句 mysql\u0026gt; **kill connection id** //停掉processlist查询出的某个线程，id是对应的id号 mysql\u0026gt; show processlist; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 +------+-------+---------------------+--------+-------------+------+-----------------------------------------------------------------------+------------------+ | Id | User | Host | db | Command | Time | State | Info | +------+-------+---------------------+--------+-------------+------+-----------------------------------------------------------------------+------------------+ | 1574 | root | localhost | huanpc | Sleep | 702 | | NULL | | 1955 | root | localhost | NULL | Query | 0 | init | show processlist | | 1958 | slave | 192.168.1.102:37399 | NULL | Binlog Dump | 10 | Master has sent all binlog to slave; waiting for binlog to be updated | NULL | +------+-------+---------------------+--------+-------------+------+-----------------------------------------------------------------------+------------------+ 3 rows in set (0.00 sec) mysql\u0026gt; kill connection 1574; Query OK, 0 rows affected (0.00 sec) mysql\u0026gt; show processlist; +------+-------+---------------------+------+-------------+------+-----------------------------------------------------------------------+------------------+ | Id | User | Host | db | Command | Time | State | Info | +------+-------+---------------------+------+-------------+------+-----------------------------------------------------------------------+------------------+ | 1955 | root | localhost | NULL | Query | 0 | init | show processlist | | 1958 | slave | 192.168.1.102:37399 | NULL | Binlog Dump | 18 | Master has sent all binlog to slave; waiting for binlog to be updated | NULL | +------+-------+---------------------+------+-------------+------+-----------------------------------------------------------------------+------------------+ 2 rows in set (0.00 sec) 除此之外，show processlist还能查看当前mysql连接数。 如果是root帐号，能看到所有用户的当前连接。 如果是其它普通帐号，只能看到自己占用的连接。 注意： show processlist;只列出前100条 如果想全列出要使用show full processlist;\n使用show status;可以比较全面地查看到mysql状态 mysql\u0026gt; show status;\n参数解释： Aborted_clients 由于客户没有正确关闭连接已经死掉，已经放弃的连接数量。\nAborted_connects 尝试已经失败的MySQL服务器的连接的次数。\nConnections 试图连接MySQL服务器的次数。\nCreated_tmp_tables 当执行语句时，已经被创造了的隐含临时表的数量。\nDelayed_insert_threads 正在使用的延迟插入处理器线程的数量。\nDelayed_writes 用INSERT DELAYED写入的行数。\nDelayed_errors 用INSERT DELAYED写入的发生某些错误(可能重复键值)的行数。\nFlush_commands 执行FLUSH命令的次数。\nHandler_delete 请求从一张表中删除行的次数。\nHandler_read_first 请求读入表中第一行的次数。\nHandler_read_key 请求数字基于键读行。\nHandler_read_next 请求读入基于一个键的一行的次数。\nHandler_read_rnd 请求读入基于一个固定位置的一行的次数。\nHandler_update 请求更新表中一行的次数。\nHandler_write 请求向表中插入一行的次数。\nKey_blocks_used 用于关键字缓存的块的数量。\nKey_read_requests 请求从缓存读入一个键值的次数。\nKey_reads 从磁盘物理读入一个键值的次数。\nKey_write_requests 请求将一个关键字块写入缓存次数。\nKey_writes 将一个键值块物理写入磁盘的次数。\nMax_used_connections 同时使用的连接的最大数目。\nNot_flushed_key_blocks 在键缓存中已经改变但是还没被清空到磁盘上的键块。\nNot_flushed_delayed_rows 在INSERT DELAY队列中等待写入的行的数量。\nOpen_tables 打开表的数量。\nOpen_files 打开文件的数量。\nOpen_streams 打开流的数量(主要用于日志记载）\nOpened_tables 已经打开的表的数量。\nQuestions 发往服务器的查询的数量。\nSlow_queries 要花超过long_query_time时间的查询数量。\nThreads_connected 当前打开的连接的数量。\nThreads_running 不在睡眠的线程数量。\nUptime 服务器工作了多少秒。\n","permalink":"https://ktzxy.top/posts/38avqr5o3k/","summary":"mysql状态说明","title":"mysql状态说明"},{"content":"Kibana介绍和使用 注意 Kibana和ElasticSearch配合使用的时候，需要确保两者的版本号一直，不然可能出现无法正常使用的问题\n下载 官网下载Kibana，下载完成后，解压缩文件，修改对应的配置\n找到 config/kibana.yml，然后修改下面的配置信息，因为kibana\n1 2 # Specifies locale to be used for all localizable strings, dates and number formats. i18n.locale: \u0026#34;zh-CN\u0026#34; 启动 修改配置完成后，找到 bin/kibana.bat，双击启动即可\n启动完成后，访问下面的URL进入到管理页面\n1 http://127.0.0.1:5601 ","permalink":"https://ktzxy.top/posts/q85bm997vk/","summary":"Kibana介绍和使用","title":"Kibana介绍和使用"},{"content":"docker-compose.yml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 version: \u0026#39;3.7\u0026#39; services: # docker监控工具 portainer: image: outlovecn/portainer-cn container_name: portainer ports: - \u0026#34;9000:9000\u0026#34; volumes: - /var/run/docker.sock:/var/run/docker.sock restart: always # 影音库 jellyfin: image: nyanmisaka/jellyfin container_name: jellyfin ports: - 8096:8096 volumes: - /data/media:/media #影音文件位置，冒号前自定义 restart: always # 影音库 plex: image: plexinc/pms-docker container_name: plex environment: - PLEX_CLAIM=claim-AmmJrgtS8xR3_f6zzLAD #替换为自己的CLAIM,访问 https://www.plex.tv/zh/claim 获取 - TZ=Asia/Shanghai volumes: - /data/media:/data #影音文件位置，冒号前自定义 restart: always network_mode: host # 影音库 emby: image: emby/embyserver container_name: emby volumes: - /data/emby/config:/config # Configuration directory - /data/media:/mnt/share # Media directory ports: - 18096:8096 # HTTP port devices: - /dev/dri:/dev/dri # VAAPI/NVDEC/NVENC render nodes restart: always # bt/pt下载工具 qbittorrent: image: linuxserver/qbittorrent:4.5.2 container_name: qbittorrent environment: - WEBUI_PORT=8081 #web-ui端口号，默认用户名:admin，默认密码：adminadmin - TZ=Asia/Shanghai volumes: - /data/qbittorrent/config:/config - /data/qbittorrent/downloads:/downloads #下载文件放置位置 restart: always network_mode: host # bt/pt下载工具 transmission: # image: chisbread/transmission # 快校版镜像 image: linuxserver/transmission # 官方版镜像，自行选择用哪种 container_name: transmission environment: - TZ=Asia/Shanghai - USER=liangge # 默认账号 - PASS=password # 默认密码 volumes: - /data/transmission/config:/config - /data/transmission/watch:/watch - /data/transmission/downloads:/downloads #下载文件放置位置 - /data/qbittorrent/downloads:/QBdownloads # qbittorrent的下载目录，用于转种 restart: always network_mode: host #默认web端口9091 # pt自动化 nas-tools: image: nastool/nas-tools #默认账号密码：admin/password container_name: nas-tools ports: - 13000:3000 # webui端口 volumes: - /data:/data #媒体库和下载目录的上级目录 environment: - NASTOOL_AUTO_UPDATE=false restart: always # pt自动化 IYUUPlus: image: iyuucn/iyuuplus container_name: IYUUPlus ports: - 8787:8787 volumes: - /data/IYUU/db:/IYUU/db - /data/transmission/config/torrents:/torrents #冒号左边是transmission的torrents文件夹在宿主机上的路径，如不使用tr，可删除本行 - /data/qbittorrent/config/qBittorrent/BT_backup:/BT_backup #冒号左边是qBittorrent的BT_backup文件夹在宿主机上的路径，如不使用qb，可删除本行 restart: always # 网盘管理 alist: image: \u0026#39;xhofe/alist\u0026#39; container_name: alist volumes: - \u0026#39;/data/alist:/opt/alist/data\u0026#39; ports: - \u0026#39;5244:5244\u0026#39; restart: always # 网盘下载器 Aria2-Pro: container_name: aria2-pro image: p3terx/aria2-pro environment: - TZ=Asia/Shanghai volumes: - /data/aria2:/downloads ports: - 6800:6800 - 6888:6888 - 6888:6888/udp restart: always logging: driver: json-file options: max-size: 1m # 网盘下载器的UI界面 AriaNg: container_name: ariang image: p3terx/ariang ports: - 6880:6880 restart: always logging: driver: json-file options: max-size: 1m # 个人网盘 nextcloud: container_name: nextcloud image: nextcloud ports: - 8060:80 environment: - MYSQL_DATABASE=nextcloud - MYSQL_USER=root - MYSQL_PASSWORD=123456 - MYSQL_HOST=mariadb volumes: - \u0026#34;/data/nextcloud:/var/www/html\u0026#34; restart: always # 音乐库 navidrome: container_name: navidrome image: deluan/navidrome ports: - \u0026#34;14533:4533\u0026#34; volumes: - \u0026#34;/data/navidrome:/data\u0026#34; - \u0026#34;/data/media/music:/music:ro\u0026#34; # 电子书 reader: image: hectorqin/reader container_name: reader ports: - 8082:8080 volumes: - /data/reader/logs:/logs - /data/reader/storage:/storage environment: - SPRING_PROFILES_ACTIVE=prod - READER_APP_CACHECHAPTERCONTENT=false #是否开启缓存章节内容 - READER_APP_REMOTEWEBVIEWAPI=http://remote-webview:8083 #开启远程webview restart: always remote-webview: image: hectorqin/remote-webview container_name: remote-webview restart: always ports: - 8083:8050 # 电子书 calibre-web: image: linuxserver/calibre-web container_name: calibre-web volumes: - /data/calibre-web/config:/config - /data/calibre-web/books:/books ports: - 8084:8083 restart: always # 用来生成calibre-web需要的metadata.db文件 calibre: image: linuxserver/calibre container_name: calibre volumes: - /data/calibre/config:/config ports: - 8087:8080 restart: always # 照片墙 photoprism: image: photoprism/photoprism container_name: photoprism # 设置重启策略 restart: always security_opt: - seccomp:unconfined - apparmor:unconfined ports: - 12342:12342 environment: PHOTOPRISM_ADMIN_PASSWORD: \u0026#34;photoprism\u0026#34; # 设置管理员密码，至少包含四个字符 PHOTOPRISM_HTTP_PORT: 12342 # 内部监听端口 PHOTOPRISM_DATABASE_DRIVER: \u0026#34;mariadb\u0026#34; # 使用 MariaDB (或 MySQL) 代替 SQLite 来提升性能 PHOTOPRISM_DATABASE_SERVER: \u0026#34;mariadb\u0026#34; # 设置 MariaDB 服务 (hostname:port) PHOTOPRISM_DATABASE_NAME: \u0026#34;photoprism\u0026#34; # 设置 MariaDB 库名 PHOTOPRISM_DATABASE_USER: \u0026#34;root\u0026#34; # 指定 MariaDB 的用户 PHOTOPRISM_DATABASE_PASSWORD: \u0026#34;123456\u0026#34; # 设置 MariaDB 用户密码 PHOTOPRISM_SITE_TITLE: \u0026#34;PhotoPrism\u0026#34; PHOTOPRISM_SITE_CAPTION: \u0026#34;Browse Your Life\u0026#34; PHOTOPRISM_SITE_DESCRIPTION: \u0026#34;照片墙\u0026#34; PHOTOPRISM_SITE_AUTHOR: \u0026#34;liangge\u0026#34; volumes: # 图片与视频的原生目录，文件上传后先存放在这里 - \u0026#34;/data/photoprism/photo:/photoprism/originals\u0026#34; # 导入目录，如果该目录中存在文件会自动导入 - \u0026#34;/data/media/photo:/photoprism/import\u0026#34; # 导航页 heimdall: image: linuxserver/heimdall container_name: heimdall environment: - TZ=Asia/Shanghai volumes: - /data/heimdall/config:/config ports: - 8088:80 restart: always # 监控面板 grafana: container_name: grafana image: grafana/grafana ports: - \u0026#34;3000:3000\u0026#34; #默认账号admin/admin，初次登陆需重置密码 restart: always prometheus: container_name: prometheus image: prom/prometheus ports: - \u0026#34;9090:9090\u0026#34; volumes: - /data/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml restart: always node-exporter: container_name: node-exporter image: prom/node-exporter ports: - \u0026#34;9100:9100\u0026#34; volumes: - /proc:/host/proc:ro - /sys:/host/sys:ro - /:/rootfs:ro restart: always qbittorrent-exporter: container_name: qbittorrent-exporter image: caseyscarborough/qbittorrent-exporter ports: - \u0026#34;9200:17871\u0026#34; environment: - TZ=Asia/Shanghai restart: always tinymediamanager: container_name: tinymediamanager image: dzhuang/tinymediamanager ports: - \u0026#34;15800:5800\u0026#34; volumes: - /data/media:/media:rw environment: - USER_ID=0 - GROUP_ID=0 - TZ=Asia/Shanghai extra_hosts: # 修改hosts文件,可能失效 - \u0026#34;api.themoviedb.org:52.84.18.87\u0026#34; - \u0026#34;image.tmdb.org:84.17.46.53\u0026#34; - \u0026#34;www.themoviedb.org:52.84.125.129\u0026#34; restart: always # 智能家居 homeassistant: container_name: homeassistant image: homeassistant/home-assistant environment: - TZ=Asia/Shanghai privileged: true network_mode: host #默认端口 restart: always # 数据库，给nextcloud和photoprism使用 mariadb: container_name: mariadb image: mariadb ports: - \u0026#34;3306:3306\u0026#34; restart: always environment: MARIADB_ROOT_PASSWORD: 123456 # 软路由 openwrt: image: piaoyizy/openwrt-x86 # 默认账号：root/password container_name: openwrt restart: always privileged: true networks: macnet: # 配置文件位置：/etc/config/network,一定要加上dns和网关 ipv4_address: 192.168.18.62 # 虚拟网卡 networks: macnet: external: true docker-install.sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #!/bin/bash # 安装ssh if command -v ssh \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;ssh is installed.\u0026#34; else echo \u0026#34;ssh未安装,开始安装ssh...\u0026#34; apt update apt install openssh-server fi # 安装curl if command -v curl \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;curl is installed.\u0026#34; else echo \u0026#34;curl未安装,开始安装curl...\u0026#34; apt update apt install curl fi # 安装docker if command -v docker \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;Docker is installed.\u0026#34; else echo \u0026#34;Docker未安装,开始安装Docker...\u0026#34; curl -fsSL https://get.docker.com | bash -s docker systemctl start docker systemctl enable docker docker version fi # 安装docker-compose if command -v docker-compose \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;docker-compose is installed.\u0026#34; else echo \u0026#34;docker-compose未安装,开始安装docker-compose...\u0026#34; apt update apt install docker-compose fi # 设置合盖不休眠 if cat /etc/systemd/logind.conf | grep \u0026#39;#HandleLidSwitch=suspend\u0026#39;; then sed -i \u0026#39;s/#HandleLidSwitch=suspend/HandleLidSwitch=lock/g\u0026#39; /etc/systemd/logind.conf systemctl restart systemd-logind else echo \u0026#39;已设置合盖不休眠\u0026#39; fi raid.sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 创建raid,默认两盘位，不是的话需修改此处 read -p \u0026#34;请输入第一块硬盘: \u0026#34; sd1 read -p \u0026#34;请输入第一块硬盘: \u0026#34; sd2 mdadm --create --verbose /dev/md0 --level=0 --raid-devices=2 /dev/$sd1 /dev/$sd2 # 挂载磁盘 mount /dev/md0 /data # 开机自动挂载磁盘 touch /etc/rc.local chmod 755 /etc/rc.local echo \u0026#39;\u0026#39;\u0026#39;#!/bin/bash\u0026#39;\u0026#39;\u0026#39; \u0026gt;\u0026gt; /etc/rc.local echo \u0026#39;\u0026#39;\u0026#39;mount /dev/md0 /data\u0026#39;\u0026#39;\u0026#39; \u0026gt;\u0026gt; /etc/rc.local systemctl start rc-local systemctl enable rc-local init 6 # 重启 router-install.sh 1 2 3 4 5 # 设置虚拟网卡 read -p \u0026#34;请输入你的网卡名称: \u0026#34; name ip link set $name promisc on read -p \u0026#34;请输入你的主路由ip: \u0026#34; ip docker network create -d macvlan --subnet=$ip/24 --gateway=$ip -o parent=$name macnet share-install.sh 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # 设置文件共享 mkdir -p /data/media/tv mkdir -p /data/media/movie mkdir -p /data/media/av if command -v samba \u0026gt;/dev/null 2\u0026gt;\u0026amp;1; then echo \u0026#34;smb is installed.\u0026#34; else echo \u0026#34;smb未安装,开始安装smb...\u0026#34; read -p \u0026#34;请输入你要共享的目录: \u0026#34; path mkdir -p $path # 共享目录，可修改。和smb.conf的path配置对应 apt update apt install samba systemctl start smb systemctl enable smb read -p \u0026#34;请输入你的共享用户名称: \u0026#34; user cat \u0026gt;\u0026gt; /etc/samba/smb.conf \u0026lt;\u0026lt;EOF [share] comment = share path = $path valid users = $user browseable = yes read only = yes EOF smbpasswd -a $user # 共享用户名 systemctl restart samba fi ","permalink":"https://ktzxy.top/posts/z5kn0wbanv/","summary":"nas 常用 docker 服务合集","title":"nas 常用 docker 服务合集"},{"content":"1. 什么是MySQL派生表？ 派生表（Derived Table），是复杂SQL在执行过程中的中间表，也可认为是临时表，存放SQL执行过程中必不可少的中间数据。通常由跟在from子句或者join子句之后的子查询产生，比如下面两个派生表例子，derived_t1和derived_t2都是派生表。\nselect * from (select * from t1) as derived_t1;\nselect t1.* from t1 join (select distinct class_id from t2) as derived_t2 where t1.id=derived_t2.class_id;\n2. MySQL优化器如何处理派生表？ MySQL优化器处理派生表有两种策略：\n将派生生合并(Merging)到外部的查询块 物化(Materialize)派生表到内部的临时表 2.1 合并(Merging ) 对于简单的派生表，比如没有使用group by，having，distinct，limit等，这类派生表可直接合并到外部的查询块中。如下面两个例子：\n例子1： SELECT * FROM (SELECT * FROM t1) AS derived_t1;\n合并后变成：\nSELECT * FROM t1;\n例子2：\nSELECT * FROM t1 JOIN (SELECT t2.f1 FROM t2) AS derived_t2 ON t1.f2=derived_t2.f1 WHERE t1.f1 \u0026gt; 0;\n合并后变成：\nSELECT t1.* FROM t1 JOIN t2 ON t1.f2=t2.f1 WHERE t1.f1 \u0026gt; 0;\n2.2 物化(Materialization) 所谓物化，可以理解为在内存中生成一张临时表，将派生表实例化，物化派生表成本较高，尤其当派生表很大，内存不够用时，物化的派生表还要转存到磁盘中，对性能影响进一步增加。\n2.3 优化器对派生表做的优化 对于简单的派生表，进行合并处理。派生表合并也有限制，当外层查询块涉及的表数量超过一定阈值(61)时，优化器将选择物化派生表，而不是合并派生表。 MySQL优化器在处理派生表时，能合并的先合并，不能合并的，考虑物化派生表，然而物化派生表也是有成本的，优化器也会尽量推迟派生表的物化，甚至不物化派生表。比如join的两个表，A为派生表，另外一个表为B，优化器推迟派生表A的物化，先处理表B，当表B中的数据根据条件过滤后，数据很少，或者完全没有数据时，此时物化派生表A成本就会低很多，甚至不用物化派生表。 查询条件从外层查询下推到派生表中，以便生成更小的派生表，越小的派生表，其性能越好。 优化器将会对物化的派生表增加索引，以加速访问。比如下面这个例子，优化器将会对derived_t2的f1字段添加索引，以提高查询性能。 SELECT * FROM t1 JOIN (SELECT DISTINCT f1 FROM t2) AS derived_t2 ON t1.f1=derived_t2.f1; 3. 派生表合并开关 派生表合并，通过参数optimizer_switch来控制是否打开，默认为打开。 optimizer_switch=\u0026lsquo;derived_merge=ON\u0026rsquo;;\n4. 派生表合并限制 不是所有的派生表都可以被merge，简单的派生表，可以被merge，复杂的带有GROUP，HAVING，DISTINCT，LIMIT，聚集函数等子句的派生表只能被物化，不能被合并。\n","permalink":"https://ktzxy.top/posts/qpveq05rn7/","summary":"MySQL派生表优化","title":"MySQL派生表优化"},{"content":"Python 教程 (1) 这个教程根据廖雪峰的Python3教程所写.主要是一些基础教程, 截止于模块的学习之前.\n[TOC]\nJupyter 使用 Jupyter Notebook是一个Web应用程序，允许您创建和共享包含实时代码，方程，可视化和说明文本的文档。 用途包括：数据清理和转换，数值模拟，统计建模，机器学习等等。\n一些常用的 Jupyter 的魔术命令.\n魔术命令 作用 %quickref 显示 IPython 快速参考 %debug 从最新的异常跟踪的底部进入交互式调试器 %run script.py 执行 script.py %load script.py 加载 script.py %cd direcrory 切换工作目录 更多内容参见 Jupyter 安装与使用. 比如运行一个py程序. 我们可以用 %load 来加载一个存在的 py文件.\n1 %load baidu.py Python 基础 一些注意事项 python 不用分号 # 表示注释, 多行注释用两行 \u0026rsquo;\u0026rsquo;\u0026rsquo; 包着. print 里可以用逗号分开不同的内容, 也可以用加号连接相同类型的内容, 比如都是 str. py3默认支持中文, 可以不加 #-*-coding: UTF-8 -*- 下面是一个简单的求绝对值的例子:\n1 2 3 4 5 6 x = input(\u0026#34;This is an absolute function, please enter an value:\u0026#34;) # 输入一个数字 a = float(x) if a \u0026gt; 0: print(\u0026#34;|\u0026#34;, a, \u0026#34;|=\u0026#34; ,a) else: print(\u0026#34;|\u0026#34;,a, \u0026#34;|=\u0026#34;,-a) This is an absolute function, please enter an value:1 | 1.0 |= 1.0 python 是动态语言, 可以覆盖取值和更换变量类型, 比如:\n1 2 3 4 a = 123 print(a) a = \u0026#34;wenchao\u0026#34; print(a) 123 wenchao python 里的 \u0026ldquo;/\u0026rdquo; 就是表示除法, 地板除法用 \u0026ldquo;//\u0026rdquo;, 相应的 \u0026ldquo;%\u0026rdquo; 表示余数.\n字符编码 ASCII、Unicode和UTF-8的关系 ASCII 是最早的编码, 使用一个字节能表示英文字母和一些符号, 但是不能表示中文等其他文字; GB2312 是中国制定的中文编码, 使用2个字节. Unicode 是统一所有语言的一套编码, 不会出现乱码问题. UTF-8 编码是一种可变长编码, 相当于是 Unicode 的压缩, 能节省空间, 也能兼容所有的编码. 在计算机内存中，统一使用Unicode编码，当需要保存到硬盘或者需要传输的时候，就转换为UTF-8编码。 浏览网页的时候，服务器会把动态生成的Unicode内容转换为UTF-8再传输到浏览器. Python 3 的字符串是以Unicode编码的. 对于单个字符的编码，Python提供了ord()函数获取单个字符的整数表示，chr()函数把编码转换为对应的字符：\n1 2 3 4 5 a= ord(\u0026#39;A\u0026#39;) b= ord(\u0026#39;超\u0026#39;) c=chr(100) d=chr(25991) print(a,b,c,d) 65 36229 d 文 同样我们可以直接使用十六进制来表示一个字符: 比如文的整数表示 25991, 超是 36229, 转化为十六进制为 6587 和 8d85.于是我们可以在前面添加 \\u\n1 \u0026#39;\\u6587\\u8d85\u0026#39; '文超' 格式化 最后一个常见的问题是如何输出格式化的字符串。我们经常会输出类似\u0026rsquo;亲爱的xxx你好！你xx月的话费是xx，余额是xx\u0026rsquo;之类的字符串，而xxx的内容都是根据变量变化的，所以，需要一种简便的格式化字符串的方式。在Python中，采用的格式化方式和C语言是一致的，用%实现. 常见的占位符有有:\n占位符 替换内容 %d 整数 %f 浮点数 %s 字符串 %x 十六进制整数 注意 % 前面没有逗号. 另外, 当使用%符号时, 用两个来转义.\n1 print(\u0026#39;Hello, %s\u0026#39; % \u0026#39;world\u0026#39;) Hello, world 1 print(\u0026#39;Hi, %s, you have %% $%d.\u0026#39; % (\u0026#39;Michael\u0026#39;, 1000000)) Hi, Michael, you have % $1000000. 1 2 3 4 s1=72 s2=85 r=(s2-s1)/s1*100 print(\u0026#39;小明成绩提升了百分之 %.2f%%.\u0026#39; %r) # 使用 `%.2f` 表示保留浮点后两位. 小明成绩提升了百分之 18.06%. 列表和元组 python里的列表 list 用方括号括起来, 可以用len()函数查看元素个数, 序列是从零开始, 使用负数表示倒数第几个:\n1 2 3 4 classmates = [\u0026#39;Michael\u0026#39;, \u0026#39;Noam\u0026#39;, \u0026#39;Wenchao\u0026#39;] print(len(classmates), classmates[0], classmates[-1]) 3 Michael Wenchao 下面我们介绍一下list的操作. 首先可以直接对某个值进行更改赋值, 比如 s[1]=2 这样. 我们还能增添和删除列表, 使用 append insert pop等.\n1 2 classmates.append(\u0026#39;Tamar\u0026#39;) # 增加元素, 每次运行都会增加一个重复 print(classmates) ['Michael', 'Noam', 'Wenchao', 'Tamar'] 1 2 classmates.insert(1, \u0026#39;Jack\u0026#39;) # 插入到序列1的位置 print(classmates) ['Michael', 'Jack', 'Noam', 'Wenchao', 'Tamar'] 1 2 classmates.pop() # 删除末尾的一个元素, (i) 表示删除序列号为i的元素 print(classmates) ['Michael', 'Jack', 'Noam', 'Wenchao'] 1 2 3 4 5 6 L = [ [\u0026#39;Apple\u0026#39;, \u0026#39;Google\u0026#39;, \u0026#39;Microsoft\u0026#39;], [\u0026#39;Java\u0026#39;, \u0026#39;Python\u0026#39;, \u0026#39;Ruby\u0026#39;, \u0026#39;PHP\u0026#39;], [\u0026#39;Adam\u0026#39;, \u0026#39;Bart\u0026#39;, \u0026#39;Lisa\u0026#39;] ] print(L[1][-1]) # 打印第二行最后一列的元素. PHP 进一步地, 我们可以使用tuple来表示不能改变的列表, 用小括号表示. 但要注意的是, 只有一个元素的时候, 要在后面加逗号, 比如 t=(1,) 否则回合数学括号混淆. tuple里面可以嵌套列表, 这样可以让它更灵活, 列表内的元素是可变的. tuple 是安全的列表.\n条件判断 if, elif, else 使用需要后面接冒号作为缩进的标志. 缩进用空格, jupyter 里可以用 tab代替. 如果我们需要用到 input, 注意input()返回的数据类型是str, 我们可能需要用 int, float 等转换类型.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 w=input(\u0026#34;请输入你的体重(kg):\u0026#34;) h=input(\u0026#34;请输入你的身高(cm):\u0026#34;) W=float(w) H=float(h)/100 bmi=W/H**2 if bmi\u0026lt;18.5: print(\u0026#34;你太瘦了, 要多补一补!\u0026#34;) elif 18.5\u0026lt;=bmi\u0026lt;25: print(\u0026#34;恭喜!你的BMI指数正常.\u0026#34;) elif 25\u0026lt;=bmi\u0026lt;28: print(\u0026#34;有点重了哦,注意啦!\u0026#34;) elif 28\u0026lt;=bmi\u0026lt;32: print(\u0026#34;你已经是个小胖子啦~\u0026#34;) else: print(\u0026#34;再不减肥你和猪没啥区别啦 XD\u0026#34;) print(\u0026#34;你的BMI指数是:%.2f.\u0026#34;% bmi) 请输入你的体重(kg):50 请输入你的身高(cm):173 你太瘦了, 要多补一补! 你的BMI指数是:16.71 循环语句 为了让计算机能计算成千上万次的重复运算，我们就需要循环语句。\nPython的循环有两种，一种是for...in循环; 第二种循环是while循环，只要条件满足，就不断循环，条件不满足时退出循环。\n注意使用循环的变量要先进行赋值.\n1 2 3 4 sum=0 for x in range(101): # range 函数的序列也要注意. sum=sum+x print(sum) # print 要删除缩进. 5050 1 2 3 4 5 6 sum=0 n=1 while n\u0026lt;101: sum=sum+n n=n+1 print(sum) 5050 1 2 3 4 5 6 L = [\u0026#39;Bart\u0026#39;, \u0026#39;Lisa\u0026#39;, \u0026#39;Adam\u0026#39;] n=0 for x in L: print(\u0026#34;Hellp, %s!\u0026#34; %L[n]) n=n+1 Hellp, Bart! Hellp, Lisa! Hellp, Adam! 使用dict和set Dict 是 key-value存储方式, 且一个key只能对应一个value.\n和list比较，dict有以下几个特点：\n查找和插入的速度极快，不会随着key的增加而变慢； 需要占用大量的内存，内存浪费多。 dict的key必须是不可变对象。但是value 是可变的, 本身也能用pop删除一个key:value元素组.\n下面是 dict 用法:\n1 2 3 d = {\u0026#39;Michael\u0026#39;: 95, \u0026#39;Bob\u0026#39;: 75, \u0026#39;Tracy\u0026#39;: 85} d[\u0026#39;Michael\u0026#39;] d.get(\u0026#39;Tracy\u0026#39;) 85 set和dict类似，也是一组key的集合，但不存储value。不可以放入可变对象. 由于key不能重复，所以，在set中，没有重复的key。(所以集合相同的元素会被合并) 一般的tuple放入set不会出错，而特殊的tuple(带有list)的出错，因为带有可变对象.\n1 2 3 4 5 6 s = set([1,1,2, 2, 3]) print(s) s.add(4) # 增加key print(s) s.remove(1) # 删除key print(s) {1, 2, 3} {1, 2, 3, 4} {2, 3, 4} 集合可以做运算,使用 \u0026amp; 表示交集, | 表示并集.\n对于其他不可变量, 比如 str 是不能直接操作的, 但是可以改变后存到重新存到新的变量:\n1 2 3 a = \u0026#39;abc\u0026#39; b = a.replace(\u0026#39;a\u0026#39;,\u0026#39;A\u0026#39;) print(b) Abc 函数 在Python中，定义一个函数要使用def语句，依次写出函数名、括号、括号中的参数和冒号:，然后，在缩进块中编写函数体，函数的返回值用return语句返回。\n1 2 3 4 5 6 7 # 这是个坐标变换公式 import math def move(x,y,t,a): nx=x+t*math.cos(a) ny=y-t*math.sin(a) return float(\u0026#34;%.2f\u0026#34; % nx),float(\u0026#34;%.2f\u0026#34;% ny) # 或者可以使用 round 函数来保留小数点后两位, 比如 round(nx,2). move(2,3,1,math.pi/3) # 返回值是一个tuple. (2.5, 2.13) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 这是一个二次方程求根公式 import math def quad(a,b,c): delta=b**2-4*a*c if a==0: return \u0026#34;This is not a quadratic equation\u0026#34; elif b**2-4*a*c\u0026gt;0: x1=(-b+math.sqrt(delta))/(2*a) x2=(-b-math.sqrt(delta))/(2*a) return \u0026#34;The solutions of the equation are %.2f and %.2f\u0026#34; %(x1,x2) elif b**2-4*a*c==0: x=(-b)/(2*a) return \u0026#34;This equation have two equal solutions %.2f\u0026#34; %(x) else: return \u0026#34;There is no real solutions for this quadratic euqation.\u0026#34; quad(2,6,2) 'The solutions of the equation are -0.38 and -2.62' 参数的使用 1 2 3 4 5 6 7 8 9 # 这是一个n次幂的公式, 默认情况下使用一个自变量表示平方 def power(x,n=2): s=1 while n\u0026gt;0: s=s*x n=n-1 return s print(power(5),power(2,3)) 25 8 从上面的例子可以看出，默认参数可以简化函数的调用。设置默认参数时，有几点要注意：\n必选参数在前，默认参数在后，否则Python的解释器会报错. 有时候默认参数设置为None可以防止多次调用结果不一样. *nums 可以把list元素 nums 变成可变参数传到函数内, 这种写法相当有用, 而且很常见. 1 2 3 4 5 6 7 8 9 # 关于3的例子 def pwsum(*args): sum=0 for n in args: sum =sum+n**2 return sum nums=[1,5,7,8,10] pwsum(*nums) 239 关键字参数 **kw , kw 接受的是一个 dict.\n可变参数既可以直接传入：func(1, 2, 3)，又可以先组装list或tuple，再通过*args传入：func(*(1, 2, 3))；\n关键字参数既可以直接传入：func(a=1, b=2)，又可以先组装dict，再通过**kw传入：func(**{'a': 1, 'b': 2})。\n使用*args和**kw是Python的习惯写法，当然也可以用其他参数名，但最好使用习惯用法。\n递归函数 在函数内部，可以调用其他函数。如果一个函数在内部调用自身本身，这个函数就是递归函数。\n递归函数关键就是把步骤分为：之前的n-1步和最后一步\n1 2 3 4 5 6 7 8 # 阶乘函数 def frac(n): if n==1: return 1 else: return n* frac(n-1) frac(10) 3628800 1 2 3 4 5 6 7 8 9 10 # 汉诺塔 def move(n, a, b, c): if n == 1: print(a, \u0026#39;--\u0026gt;\u0026#39;, c) else: move((n-1), a, c, b) # 最上面的 n-1个 从 a 通过 c 移到 b print(a, \u0026#39;--\u0026gt;\u0026#39;, c) # 最下面一个直接移到c move((n-1), b, a, c) #上面的 n-1 个从b通过 a 移动到c move(3,\u0026#34;A\u0026#34;,\u0026#34;B\u0026#34;,\u0026#34;C\u0026#34;) A --\u0026gt; C A --\u0026gt; B C --\u0026gt; B A --\u0026gt; C B --\u0026gt; A B --\u0026gt; C A --\u0026gt; C 高级特性 在Python中，代码不是越多越好，而是越少越好。代码不是越复杂越好，而是越简单越好。\n切片 取一个list或tuple的部分元素是非常常见的操作.我们可以用循环:\n1 2 3 4 5 6 L = [\u0026#39;Michael\u0026#39;, \u0026#39;Sarah\u0026#39;, \u0026#39;Tracy\u0026#39;, \u0026#39;Bob\u0026#39;, \u0026#39;Jack\u0026#39;] r = [] n = 3 for i in range(n): # range n: 0,1,2,3,...,n-1 r.append(L[i]) r ['Michael', 'Sarah', 'Tracy'] 在Python中提供了Slice操作符, 比如前三个元素可以使用下列代码:\n1 2 3 4 5 L = [\u0026#39;Michael\u0026#39;, \u0026#39;Sarah\u0026#39;, \u0026#39;Tracy\u0026#39;, \u0026#39;Bob\u0026#39;, \u0026#39;Jack\u0026#39;] L[0:3] # 从索引0开始取，直到索引3为止，但不包括索引3。 L[:3] # 第一个索引是0可以省略 L[-2:] # 从倒数第二个开始 L[-2:-1] # 倒数第二个到倒数第一个之前, 所以只有一个元素 可以直接使用range创建list.\n1 2 L=list(range(100)) L[10:22:2] # 最后的2是等差数列的差值 [10, 12, 14, 16, 18, 20] 1 2 3 4 5 6 7 8 9 10 11 12 13 # 删除字符串头尾的空格 def trim(s): if s==\u0026#34;\u0026#34;: return while s[0]==\u0026#34; \u0026#34;: s = s[1:] if s == \u0026#34;\u0026#34;: return s while s[-1]==\u0026#34; \u0026#34;: s= s[:-1] return s trim(\u0026#34; heko \u0026#34;) 'heko' 迭代 如果给定一个list或tuple，我们可以通过for循环来遍历这个list或tuple，这种遍历我们称为迭代（Iteration）。在Python中，迭代是通过for \u0026hellip; in来完成的.\n如何判断一个对象是可迭代对象呢？方法是通过collections模块的Iterable类型判断：\n1 2 3 4 5 6 7 \u0026gt;\u0026gt;\u0026gt; from collections import Iterable \u0026gt;\u0026gt;\u0026gt; isinstance(\u0026#39;abc\u0026#39;, Iterable) # str是否可迭代 True \u0026gt;\u0026gt;\u0026gt; isinstance([1,2,3], Iterable) # list是否可迭代 True \u0026gt;\u0026gt;\u0026gt; isinstance(123, Iterable) # 整数是否可迭代 False 1 2 3 4 5 6 7 8 9 10 11 12 13 def findMinAndMax(L): if L==[]: return (None, None) max = min = L[0] for x in L: if x \u0026gt; max: max =x if x \u0026lt; min: min =x return (min, max) findMinAndMax([2,3,7,1,24,67]) (1, 67) 列表生成式 使用 for 循环生成列表比较麻烦, 可以用简化的列表生成式:\n1 2 \u0026gt;\u0026gt;\u0026gt; [x * x for x in range(1, 11)] [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] 代替\n1 2 3 4 5 6 \u0026gt;\u0026gt;\u0026gt; L = [] \u0026gt;\u0026gt;\u0026gt; for x in range(1, 11): ... L.append(x * x) ... \u0026gt;\u0026gt;\u0026gt; L [1, 4, 9, 16, 25, 36, 49, 64, 81, 100] 运用列表生成式，可以写出非常简洁的代码。例如，列出当前目录下的所有文件和目录名，可以通过一行代码实现:\n1 2 import os [d for d in os.listdir(\u0026#39;.\u0026#39;)] ['.ipynb_checkpoints', 'google-picture', 'Python3.ipynb', 'replacetxt.ipynb', 'ssr-address.ipynb', 'tieba'] 1 2 3 L1=[\u0026#39;Hello\u0026#39;,\u0026#39;World\u0026#39;,18,\u0026#39;Apple\u0026#39;,None] L2=[l.lower() for l in L1 if isinstance(l,str)] print(L2) ['hello', 'world', 'apple'] 生成器 通过列表生成式，我们可以直接创建一个列表。但是，受到内存限制，列表容量肯定是有限的。而且，创建一个包含100万个元素的列表，不仅占用很大的存储空间，如果我们仅仅需要访问前面几个元素，那后面绝大多数元素占用的空间都白白浪费了。所以，如果列表元素可以按照某种算法推算出来，那我们是否可以在循环的过程中不断推算出后续的元素呢？这样就不必创建完整的list，从而节省大量的空间。在Python中，这种一边循环一边计算的机制，称为生成器：generator。\n要创建一个generator，有很多种方法。第一种方法很简单，只要把一个列表生成式的[]改成()，就创建了一个generator. 如果要一个一个打印出来，可以通过next()函数获得generator的下一个返回值. 因为generator也是可迭代对象, 所以我们可以使用for循环调用.\n斐波拉契数列用列表生成式写不出来，但是，用函数把它打印出来却很容易：\n1 2 3 4 5 6 7 8 def fib(max): n, a, b = 0, 0, 1 while n \u0026lt; max: print(b) a, b = b, a + b n = n + 1 return \u0026#39;done\u0026#39; fib(5) 1 1 2 3 5 'done' fib函数实际上是定义了斐波拉契数列的推算规则，可以从第一个元素开始，推算出后续任意的元素，这种逻辑其实非常类似generator。\n也就是说，上面的函数和generator仅一步之遥。要把fib函数变成generator，只需要把print(b)改为yield b就可以了\n1 2 3 4 5 6 7 8 9 10 11 def fib(max): n, a, b = 0, 0, 1 while n \u0026lt; max: yield b a, b = b, a + b n = n + 1 return \u0026#39;done\u0026#39; g=fib(5) print(g) for n in g: print(n) \u0026lt;generator object fib at 0x05659FB0\u0026gt; 1 1 2 3 5 generator和函数的执行流程不一样。函数是顺序执行，遇到return语句或者最后一行函数语句就返回。而变成generator的函数，在每次调用next()的时候执行，遇到yield语句返回，再次执行时从上次返回的yield语句处继续执行。\n下面是一个杨辉三角的生成器:\n1 2 3 4 5 6 7 8 9 10 11 def YHT(max): n=0 L=[1] while n\u0026lt;max: yield L L = [1]+[L[n]+L[n+1] for n in range(len(L)-1)]+[1] n=n+1 return None for n in YHT(10): print(n) [1] [1, 1] [1, 2, 1] [1, 3, 3, 1] [1, 4, 6, 4, 1] [1, 5, 10, 10, 5, 1] [1, 6, 15, 20, 15, 6, 1] [1, 7, 21, 35, 35, 21, 7, 1] [1, 8, 28, 56, 70, 56, 28, 8, 1] [1, 9, 36, 84, 126, 126, 84, 36, 9, 1] 迭代器 我们已经知道，可以直接作用于for循环的数据类型有以下几种：\n一类是集合数据类型，如list、tuple、dict、set、str等；\n一类是generator，包括生成器和带yield的generator function。\n这些可以直接作用于for循环的对象统称为可迭代对象：Iterable。可以使用isinstance()判断一个对象是否是Iterable对象：\n1 2 3 from collections import Iterable isinstance(\u0026#39;abc\u0026#39;, Iterable) isinstance((x for x in range(10)), Iterable) True 而生成器不但可以作用于for循环，还可以被next()函数不断调用并返回下一个值，直到最后抛出StopIteration错误表示无法继续返回下一个值了。\n可以被next()函数调用并不断返回下一个值的对象称为迭代器：Iterator。可以使用isinstance()判断一个对象是否是Iterator对象：\n1 2 from collections import Iterator isinstance((x for x in range(10)), Iterator) True 生成器都是Iterator对象，但list、dict、str虽然是Iterable，却不是Iterator。\n把list、dict、str等Iterable变成Iterator可以使用iter()函数：\n1 2 \u0026gt;\u0026gt;\u0026gt; isinstance(iter(\u0026#39;abc\u0026#39;), Iterator) True Iterator甚至可以表示一个无限大的数据流，例如全体自然数。而使用list是永远不可能存储全体自然数的。\n凡是可作用于for循环的对象都是Iterable类型；\n凡是可作用于next()函数的对象都是Iterator类型，它们表示一个惰性计算的序列；\n函数式编程 高级函数 把函数作为参数传入，这样的函数称为高阶函数，函数式编程就是指这种高度抽象的编程范式。\nmap 和 reduce 函数 map()函数接收两个参数，一个是函数，一个是Iterable，map将传入的函数依次作用到序列的每个元素，并把结果作为新的Iterator返回。\n1 2 3 4 5 6 def f(x): return x**2 r=map(f,[x for x in range(10)]) next(r) list(r) # Iterator是惰性序列, 用list 函数列出 [1, 4, 9, 16, 25, 36, 49, 64, 81] reduce把一个函数作用在一个序列[x1, x2, x3, ...]上，这个函数必须接收两个参数，reduce把结果继续和序列的下一个元素做累积计算，其效果就是：\n1 reduce(f, [x1, x2, x3, x4]) = f(f(f(x1, x2), x3), x4) 比方说对一个序列求和，就可以用reduce实现：(注意 reduce 函数需要从模块functools中调用)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from functools import reduce # 把数字拼在一起 def combine(x,y): return 10*x+y reduce(combine, [1,2,5,7,9]) DIGITS = {\u0026#39;0\u0026#39;: 0, \u0026#39;1\u0026#39;: 1, \u0026#39;2\u0026#39;: 2, \u0026#39;3\u0026#39;: 3, \u0026#39;4\u0026#39;: 4, \u0026#39;5\u0026#39;: 5, \u0026#39;6\u0026#39;: 6, \u0026#39;7\u0026#39;: 7, \u0026#39;8\u0026#39;: 8, \u0026#39;9\u0026#39;: 9} # char2digit def str2int(s): def fn(x, y): return x * 10 + y def char2num(s): return DIGITS[s] return reduce(fn, map(char2num, s)) str2int(\u0026#39;273243\u0026#39;) 273243 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 # 输入的不规范的英文名字，变为首字母大写，其他小写的规范名字 from functools import reduce def normalize(name): return name[0].upper() + name[1:].lower() # prod of list def prod(list): def pd(x,y): return x*y return reduce(pd, list) prod([x for x in range(2,5)]) # str2float函数，把字符串\u0026#39;123.456\u0026#39;转换成浮点数123.456 DIGITS = {\u0026#39;0\u0026#39;: 0, \u0026#39;1\u0026#39;: 1, \u0026#39;2\u0026#39;: 2, \u0026#39;3\u0026#39;: 3, \u0026#39;4\u0026#39;: 4, \u0026#39;5\u0026#39;: 5, \u0026#39;6\u0026#39;: 6, \u0026#39;7\u0026#39;: 7, \u0026#39;8\u0026#39;: 8, \u0026#39;9\u0026#39;: 9} def str2float(s): def char2num(s): return DIGITS[s] def fn(x, y): return x * 10 + y def fn2(x,y): return x * 0.1 + y s=s.split(\u0026#39;.\u0026#39;) # 将 string 拆成一个两半的列表 return reduce(fn, map(char2num, s[0]))+0.1*reduce(fn2, map(char2num, s[1][::-1])) #[::-1] 逆序 str2float(\u0026#39;23.13\u0026#39;) 23.13 filter 函数 Python内建的filter()函数用于过滤序列。filter()把传入的函数依次作用于每个元素，然后根据返回值是True还是False决定保留还是丢弃该元素。\n1 2 3 4 def not_empty(s): return s and s.strip() list(filter(not_empty, [\u0026#39;A\u0026#39;, \u0026#39;\u0026#39;, \u0026#39;B\u0026#39;, None, \u0026#39;C\u0026#39;, \u0026#39; \u0026#39;])) ['A', 'B', 'C'] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # generate prime def _odd_iter(): n = 1 while True: n = n + 2 yield n def not_divisible(n): return lambda x: x % n \u0026gt; 0 def primes(): yield 2 it = _odd_iter() # 初始序列 while True: n = next(it) # 返回序列的第一个数 yield n it = filter(not_divisible(n), it) # 构造新序列 # 打印100以内的素数: for n in primes(): if n \u0026lt; 5: print(n) else: break 1 2 3 4 5 # 回文数 def is_palindrome(n): return str(n) == str(n)[::-1] output=filter(is_palindrome, range(10, 100)) print(\u0026#39;10~100:\u0026#39;, list(output)) 10~100: [11, 22, 33, 44, 55, 66, 77, 88, 99] sorted 函数 排序也是在程序中经常用到的算法。无论使用冒泡排序还是快速排序，排序的核心是比较两个元素的大小。如果是数字，我们可以直接比较，但如果是字符串或者两个dict呢？直接比较数学上的大小是没有意义的，因此，比较的过程必须通过函数抽象出来。\nPython内置的sorted()函数就可以对list进行排序\n1 2 \u0026gt;\u0026gt;\u0026gt; sorted([36, 5, -12, 9, -21]) [-21, -12, 5, 9, 36] 此外，sorted()函数也是一个高阶函数，它还可以接收一个key函数来实现自定义的排序，例如按绝对值大小排序：\n1 2 \u0026gt;\u0026gt;\u0026gt; sorted([36, 5, -12, 9, -21], key=abs) [5, 9, -12, -21, 36] 要进行反向排序，不必改动key函数，可以传入第三个参数reverse=True：\n1 2 \u0026gt;\u0026gt;\u0026gt; sorted([\u0026#39;bob\u0026#39;, \u0026#39;about\u0026#39;, \u0026#39;Zoo\u0026#39;, \u0026#39;Credit\u0026#39;], key=str.lower, reverse=True) [\u0026#39;Zoo\u0026#39;, \u0026#39;Credit\u0026#39;, \u0026#39;bob\u0026#39;, \u0026#39;about\u0026#39;] 1 2 3 4 5 6 7 8 9 # 成绩或者姓名排序 L = [(\u0026#39;Bob\u0026#39;, 75), (\u0026#39;Adam\u0026#39;, 92), (\u0026#39;Bart\u0026#39;, 66), (\u0026#39;Lisa\u0026#39;, 88)] def by_name(t): return t[0] def by_credit(t): return t[1] L1 = sorted(L, key=by_name) L2 = sorted(L, key=by_credit, reverse=True) print(L1,\u0026#39;\\n\u0026#39;,L2) [('Adam', 92), ('Bart', 66), ('Bob', 75), ('Lisa', 88)] [('Adam', 92), ('Lisa', 88), ('Bob', 75), ('Bart', 66)] 返回函数 以后再深耕: 返回函数.\n匿名函数 关键字lambda表示匿名函数，冒号前面的x表示函数参数。匿名函数有个限制，就是只能有一个表达式，不用写return，返回值就是该表达式的结果。用匿名函数有个好处，因为函数没有名字，不必担心函数名冲突。此外，匿名函数也是一个函数对象，也可以把匿名函数赋值给一个变量，再利用变量来调用该函数：\n1 2 3 4 5 \u0026gt;\u0026gt;\u0026gt; f = lambda x: x * x \u0026gt;\u0026gt;\u0026gt; f \u0026lt;function \u0026lt;lambda\u0026gt; at 0x101c6ef28\u0026gt; \u0026gt;\u0026gt;\u0026gt; f(5) 25 1 2 L=list(filter(lambda n : n%2 == 1, range(1,20))) print(L) [1, 3, 5, 7, 9, 11, 13, 15, 17, 19] 装饰器 比如，在函数调用前后自动打印日志，但又不希望修改now()函数的定义，这种在代码运行期间动态增加功能的方式，称之为“装饰器”（Decorator）。\n偏函数 functools.partial就是帮助我们创建一个偏函数, 把一个函数的某些参数给固定住（也就是设置默认值），返回一个新的函数，调用这个新函数会更简单。\n1 int2 = functools.partial(int, base=2) 当函数的参数个数太多，需要简化时，使用functools.partial可以创建一个新的函数，这个新函数可以固定住原函数的部分参数，从而在调用时更简单。\n","permalink":"https://ktzxy.top/posts/05ecqoe2gb/","summary":"Python3Notes1","title":"Python3Notes1"},{"content":"1、服务器系统配置初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #/bin/bash # 安装系统性能分析工具及其他 yum install gcc make autoconf vim sysstat net-tools iostat iftop iotp wget lrzsz lsof unzip openssh-clients net-tool vim ntpdate -y # 设置时区并同步时间 ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime if ! crontab -l |grep ntpdate \u0026amp;\u0026gt;/dev/null ; then (echo \u0026#34;* 1 * * * ntpdate time.windows.com \u0026gt;/dev/null 2\u0026gt;\u0026amp;1\u0026#34;;crontab -l) |crontab fi # 禁用selinux sed -i \u0026#39;/SELINUX/{s/permissive/disabled/}\u0026#39; /etc/selinux/config # 关闭防火墙 if egrep \u0026#34;7.[0-9]\u0026#34; /etc/redhat-release \u0026amp;\u0026gt;/dev/null; then systemctl stop firewalld systemctl disable firewalld elif egrep \u0026#34;6.[0-9]\u0026#34; /etc/redhat-release \u0026amp;\u0026gt;/dev/null; then service iptables stop chkconfig iptables off fi # 历史命令显示操作时间 if ! grep HISTTIMEFORMAT /etc/bashrc; then echo \u0026#39;export HISTTIMEFORMAT=\u0026#34;%Y-%m-%d %H:%M:%S `whoami` \u0026#34;\u0026#39; \u0026gt;\u0026gt; /etc/bashrc fi # SSH超时时间 if ! grep \u0026#34;TMOUT=600\u0026#34; /etc/profile \u0026amp;\u0026gt;/dev/null; then echo \u0026#34;export TMOUT=600\u0026#34; \u0026gt;\u0026gt; /etc/profile fi # 禁止root远程登录 切记给系统添加普通用户，给su到root的权限 sed -i \u0026#39;s/#PermitRootLogin yes/PermitRootLogin no/\u0026#39; /etc/ssh/sshd_config # 禁止定时任务向发送邮件 sed -i \u0026#39;s/^MAILTO=root/MAILTO=\u0026#34;\u0026#34;/\u0026#39; /etc/crontab # 设置最大打开文件数 if ! grep \u0026#34;* soft nofile 65535\u0026#34; /etc/security/limits.conf \u0026amp;\u0026gt;/dev/null; then cat \u0026gt;\u0026gt; /etc/security/limits.conf \u0026lt;\u0026lt; EOF * soft nofile 65535 * hard nofile 65535 EOF fi # 系统内核优化 cat \u0026gt;\u0026gt; /etc/sysctl.conf \u0026lt;\u0026lt; EOF net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_max_tw_buckets = 20480 net.ipv4.tcp_max_syn_backlog = 20480 net.core.netdev_max_backlog = 262144 net.ipv4.tcp_fin_timeout = 20 EOF # 减少SWAP使用 echo \u0026#34;0\u0026#34; \u0026gt; /proc/sys/vm/swappiness 2、批量创建多个用户并设置密码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #!/bin/bash USER_LIST=$@ USER_FILE=./user.info for USER in $USER_LIST;do if ! id $USER \u0026amp;\u0026gt;/dev/null; then PASS=$(echo $RANDOM |md5sum |cut -c 1-8) useradd $USER echo $PASS | passwd --stdin $USER \u0026amp;\u0026gt;/dev/null echo \u0026#34;$USER $PASS\u0026#34; \u0026gt;\u0026gt; $USER_FILE echo \u0026#34;$USER User create successful.\u0026#34; else echo \u0026#34;$USER User already exists!\u0026#34; fi done 3、一键查看服务器利用率 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #!/bin/bash function cpu(){ util=$(vmstat | awk \u0026#39;{if(NR==3)print $13+$14}\u0026#39;) iowait=$(vmstat | awk \u0026#39;{if(NR==3)print $16}\u0026#39;) echo \u0026#34;CPU -使用率：${util}% ,等待磁盘IO相应使用率：${iowait}:${iowait}%\u0026#34; } function memory (){ total=`free -m |awk \u0026#39;{if(NR==2)printf \u0026#34;%.1f\u0026#34;,$2/1024}\u0026#39;` used=`free -m |awk \u0026#39;{if(NR==2) printf \u0026#34;%.1f\u0026#34;,($2-$NF)/1024}\u0026#39;` available=`free -m |awk \u0026#39;{if(NR==2) printf \u0026#34;%.1f\u0026#34;,$NF/1024}\u0026#39;` echo \u0026#34;内存 - 总大小: ${total}G , 使用: ${used}G , 剩余: ${available}G\u0026#34; } disk(){ fs=$(df -h |awk \u0026#39;/^\\/dev/{print $1}\u0026#39;) for p in $fs; do mounted=$(df -h |awk \u0026#39;$1==\u0026#34;\u0026#39;$p\u0026#39;\u0026#34;{print $NF}\u0026#39;) size=$(df -h |awk \u0026#39;$1==\u0026#34;\u0026#39;$p\u0026#39;\u0026#34;{print $2}\u0026#39;) used=$(df -h |awk \u0026#39;$1==\u0026#34;\u0026#39;$p\u0026#39;\u0026#34;{print $3}\u0026#39;) used_percent=$(df -h |awk \u0026#39;$1==\u0026#34;\u0026#39;$p\u0026#39;\u0026#34;{print $5}\u0026#39;) echo \u0026#34;硬盘 - 挂载点: $mounted , 总大小: $size , 使用: $used , 使用率: $used_percent\u0026#34; done } function tcp_status() { summary=$(ss -antp |awk \u0026#39;{status[$1]++}END{for(i in status) printf i\u0026#34;:\u0026#34;status[i]\u0026#34; \u0026#34;}\u0026#39;) echo \u0026#34;TCP连接状态 - $summary\u0026#34; } cpu memory disk tcp_status 4、找出占用CPU 内存过高的进程 1 2 3 4 5 #!/bin/bash echo \u0026#34;-------------------CUP占用前10排序--------------------------------\u0026#34; ps -eo user,pid,pcpu,pmem,args --sort=-pcpu |head -n 10 echo \u0026#34;-------------------内存占用前10排序--------------------------------\u0026#34; ps -eo user,pid,pcpu,pmem,args --sort=-pmem |head -n 10 5、查看网卡的实时流量 1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/bash eth0=$1 echo -e \u0026#34;流量进入--流量传出 \u0026#34; while true; do old_in=$(cat /proc/net/dev |grep $eth0 |awk \u0026#39;{print $2}\u0026#39;) old_out=$(cat /proc/net/dev |grep $eth0 |awk \u0026#39;{print $10}\u0026#39;) sleep 1 new_in=$(cat /proc/net/dev |grep $eth0 |awk \u0026#39;{print $2}\u0026#39;) new_out=$(cat /proc/net/dev |grep $eth0 |awk \u0026#39;{print $10}\u0026#39;) in=$(printf \u0026#34;%.1f%s\u0026#34; \u0026#34;$((($new_in-$old_in)/1024))\u0026#34; \u0026#34;KB/s\u0026#34;) out=$(printf \u0026#34;%.1f%s\u0026#34; \u0026#34;$((($new_out-$old_out)/1024))\u0026#34; \u0026#34;KB/s\u0026#34;) echo \u0026#34;$in $out\u0026#34; done 6、监控多台服务器磁盘利用率脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #!/bin/bash HOST_INFO=host.info for IP in $(awk \u0026#39;/^[^#]/{print $1}\u0026#39; $HOST_INFO); do #取出用户名和端口 USER=$(awk -v ip=$IP \u0026#39;ip==$1{print $2}\u0026#39; $HOST_INFO) PORT=$(awk -v ip=$IP \u0026#39;ip==$1{print $3}\u0026#39; $HOST_INFO) #创建临时文件，保存信息 TMP_FILE=/tmp/disk.tmp #通过公钥登录获取主机磁盘信息 ssh -p $PORT $USER@$IP \u0026#39;df -h\u0026#39; \u0026gt; $TMP_FILE #分析磁盘占用空间 USE_RATE_LIST=$(awk \u0026#39;BEGIN{OFS=\u0026#34;=\u0026#34;}/^\\/dev/{print $NF,int($5)}\u0026#39; $TMP_FILE) #循环磁盘列表，进行判断 for USE_RATE in $USE_RATE_LIST; do #取出等号（=）右边的值 挂载点名称 PART_NAME=${USE_RATE%=*} #取出等号（=）左边的值 磁盘利用率 USE_RATE=${USE_RATE#*=} #进行判断 if [ $USE_RATE -ge 80 ]; then echo \u0026#34;Warning: $PART_NAME Partition usage $USE_RATE%!\u0026#34; echo \u0026#34;服务器$IP的磁盘空间占用过高，请及时处理\u0026#34; | mail -s \u0026#34;空间不足警告\u0026#34; 你的qq@qq.com else echo \u0026#34;服务器$IP的$PART_NAME目录空间良好\u0026#34; fi done done 7、批量检测网站是否异常并邮件通知 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/bin/bash URL_LIST=\u0026#34;www.baidu.com www.ctnrs.com www.der-matech.net.cn www.der-matech.com.cn www.der-matech.cn www.der-matech.top www.der-matech.org\u0026#34; for URL in $URL_LIST; do FAIL_COUNT=0 for ((i=1;i\u0026lt;=3;i++)); do HTTP_CODE=$(curl -o /dev/null --connect-timeout 3 -s -w \u0026#34;%{http_code}\u0026#34; $URL) if [ $HTTP_CODE -eq 200 ]; then echo \u0026#34;$URL OK\u0026#34; break else echo \u0026#34;$URL retry $FAIL_COUNT\u0026#34; let FAIL_COUNT++ fi done if [ $FAIL_COUNT -eq 3 ]; then echo \u0026#34;Warning: $URL Access failure!\u0026#34; echo \u0026#34;网站$URL坏掉，请及时处理\u0026#34; | mail -s \u0026#34;$URL网站高危\u0026#34; 1794748404@qq.com fi done 8、批量主机远程执行命令脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #!/bin/bash COMMAND=$* HOST_INFO=host.info for IP in $(awk \u0026#39;/^[^#]/{print $1}\u0026#39; $HOST_INFO); do USER=$(awk -v ip=$IP \u0026#39;ip==$1{print $2}\u0026#39; $HOST_INFO) PORT=$(awk -v ip=$IP \u0026#39;ip==$1{print $3}\u0026#39; $HOST_INFO) PASS=$(awk -v ip=$IP \u0026#39;ip==$1{print $4}\u0026#39; $HOST_INFO) expect -c \u0026#34; spawn ssh -p $PORT $USER@$IP expect { \\\u0026#34;(yes/no)\\\u0026#34; {send \\\u0026#34;yes\\r\\\u0026#34;; exp_continue} \\\u0026#34;password:\\\u0026#34; {send \\\u0026#34;$PASS\\r\\\u0026#34;; exp_continue} \\\u0026#34;$USER@*\\\u0026#34; {send \\\u0026#34;$COMMAND\\r exit\\r\\\u0026#34;; exp_continue} } \u0026#34; echo \u0026#34;-------------------\u0026#34; done 9、一键部署LNMP网站平台脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #!/bin/bash NGINX_V=1.15.6 PHP_V=5.6.36 TMP_DIR=/tmp INSTALL_DIR=/usr/local PWD_C=$PWD echo echo -e \u0026#34;\\tMenu\\n\u0026#34; echo -e \u0026#34;1. Install Nginx\u0026#34; echo -e \u0026#34;2. Install PHP\u0026#34; echo -e \u0026#34;3. Install MySQL\u0026#34; echo -e \u0026#34;4. Deploy LNMP\u0026#34; echo -e \u0026#34;9. Quit\u0026#34; function command_status_check() { if [ $? -ne 0 ]; then echo $1 exit fi } function install_nginx() { cd $TMP_DIR yum install -y gcc gcc-c++ make openssl-devel pcre-devel wget wget http://nginx.org/download/nginx-${NGINX_V}.tar.gz tar zxf nginx-${NGINX_V}.tar.gz cd nginx-${NGINX_V} ./configure --prefix=$INSTALL_DIR/nginx \\ --with-http_ssl_module \\ --with-http_stub_status_module \\ --with-stream command_status_check \u0026#34;Nginx - 平台环境检查失败！\u0026#34; make -j 4 command_status_check \u0026#34;Nginx - 编译失败！\u0026#34; make install command_status_check \u0026#34;Nginx - 安装失败！\u0026#34; mkdir -p $INSTALL_DIR/nginx/conf/vhost alias cp=cp ; cp -rf $PWD_C/nginx.conf $INSTALL_DIR/nginx/conf rm -rf $INSTALL_DIR/nginx/html/* echo \u0026#34;ok\u0026#34; \u0026gt; $INSTALL_DIR/nginx/html/status.html echo \u0026#39;\u0026lt;?php echo \u0026#34;ok\u0026#34;?\u0026gt;\u0026#39; \u0026gt; $INSTALL_DIR/nginx/html/status.php $INSTALL_DIR/nginx/sbin/nginx command_status_check \u0026#34;Nginx - 启动失败！\u0026#34; } function install_php() { cd $TMP_DIR yum install -y gcc gcc-c++ make gd-devel libxml2-devel \\ libcurl-devel libjpeg-devel libpng-devel openssl-devel \\ libmcrypt-devel libxslt-devel libtidy-devel wget http://docs.php.net/distributions/php-${PHP_V}.tar.gz tar zxf php-${PHP_V}.tar.gz cd php-${PHP_V} ./configure --prefix=$INSTALL_DIR/php \\ --with-config-file-path=$INSTALL_DIR/php/etc \\ --enable-fpm --enable-opcache \\ --with-mysql --with-mysqli --with-pdo-mysql \\ --with-openssl --with-zlib --with-curl --with-gd \\ --with-jpeg-dir --with-png-dir --with-freetype-dir \\ --enable-mbstring --enable-hash command_status_check \u0026#34;PHP - 平台环境检查失败！\u0026#34; make -j 4 command_status_check \u0026#34;PHP - 编译失败！\u0026#34; make install command_status_check \u0026#34;PHP - 安装失败！\u0026#34; cp php.ini-production $INSTALL_DIR/php/etc/php.ini cp sapi/fpm/php-fpm.conf $INSTALL_DIR/php/etc/php-fpm.conf cp sapi/fpm/init.d.php-fpm /etc/init.d/php-fpm chmod +x /etc/init.d/php-fpm /etc/init.d/php-fpm start command_status_check \u0026#34;PHP - 启动失败！\u0026#34; } read -p \u0026#34;请输入编号：\u0026#34; number case $number in 1) install_nginx;; 2) install_php;; 3) install_mysql;; 4) install_nginx install_php ;; 9) exit;; esac 10、监控MySQL主从同步状态是否异常脚本 1 2 3 4 5 6 7 8 9 10 11 12 #!/bin/bash HOST=localhost USER=root PASSWD=123.com IO_SQL_STATUS=$(mysql -h$HOST -u$USER -p$PASSWD -e \u0026#39;show slave status\\G\u0026#39; 2\u0026gt;/dev/null |awk \u0026#39;/Slave_.*_Running:/{print $1$2}\u0026#39;) for i in $IO_SQL_STATUS; do THREAD_STATUS_NAME=${i%:*} THREAD_STATUS=${i#*:} if [ \u0026#34;$THREAD_STATUS\u0026#34; != \u0026#34;Yes\u0026#34; ]; then echo \u0026#34;Error: MySQL Master-Slave $THREAD_STATUS_NAME status is $THREAD_STATUS!\u0026#34; |mail -s \u0026#34;Master-Slave Staus\u0026#34; xxx@163.com fi done 11、MySql数据库备份脚本 分库备份 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 mysqldump -uroot -pxxx -B A \u0026gt; A.sql #!/bin/bash DATE=$(date +%F_%H-%M-%S) HOST=localhost USER=backup PASS=123.com BACKUP_DIR=/data/db_backup DB_LIST=$(mysql -h$HOST -u$USER -p$PASS -s -e \u0026#34;show databases;\u0026#34; 2\u0026gt;/dev/null |egrep -v \u0026#34;Database|information_schema|mysql|performance_schema|sys\u0026#34;) for DB in $DB_LIST; do BACKUP_NAME=$BACKUP_DIR/${DB}_${DATE}.sql if ! mysqldump -h$HOST -u$USER -p$PASS -B $DB \u0026gt; $BACKUP_NAME 2\u0026gt;/dev/null; then echo \u0026#34;$BACKUP_NAME 备份失败!\u0026#34; fi done 分表备份 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 mysqldump -uroot -pxxx -A t \u0026gt; t.sql #!/bin/bash DATE=$(date +%F_%H-%M-%S) HOST=localhost USER=backup PASS=123.com BACKUP_DIR=/data/db_backup DB_LIST=$(mysql -h$HOST -u$USER -p$PASS -s -e \u0026#34;show databases;\u0026#34; 2\u0026gt;/dev/null |egrep -v \u0026#34;Database|information_schema|mysql|performance_schema|sys\u0026#34;) for DB in $DB_LIST; do BACKUP_DB_DIR=$BACKUP_DIR/${DB}_${DATE} [ ! -d $BACKUP_DB_DIR ] \u0026amp;\u0026amp; mkdir -p $BACKUP_DB_DIR \u0026amp;\u0026gt;/dev/null TABLE_LIST=$(mysql -h$HOST -u$USER -p$PASS -s -e \u0026#34;use $DB;show tables;\u0026#34; 2\u0026gt;/dev/null) for TABLE in $TABLE_LIST; do BACKUP_NAME=$BACKUP_DB_DIR/${TABLE}.sql if ! mysqldump -h$HOST -u$USER -p$PASS $DB $TABLE \u0026gt; $BACKUP_NAME 2\u0026gt;/dev/null; then echo \u0026#34;$BACKUP_NAME 备份失败!\u0026#34; fi done done 12、Nginx访问日志分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #!/bin/bash # 日志格式: $remote_addr - $remote_user [$time_local] \u0026#34;$request\u0026#34; $status $body_bytes_sent \u0026#34;$http_referer\u0026#34; \u0026#34;$http_user_agent\u0026#34; \u0026#34;$http_x_forwarded_for\u0026#34; LOG_FILE=$1 echo \u0026#34;统计访问最多的10个IP\u0026#34; awk \u0026#39;{a[$1]++}END{print \u0026#34;UV:\u0026#34;,length(a);for(v in a)print v,a[v]}\u0026#39; $LOG_FILE |sort -k2 -nr |head -10 echo \u0026#34;----------------------\u0026#34; echo \u0026#34;统计时间段访问最多的IP\u0026#34; awk \u0026#39;$4\u0026gt;=\u0026#34;[01/Dec/2018:13:20:25\u0026#34; \u0026amp;\u0026amp; $4\u0026lt;=\u0026#34;[27/Nov/2018:16:20:49\u0026#34;{a[$1]++}END{for(v in a)print v,a[v]}\u0026#39; $LOG_FILE |sort -k2 -nr|head -10 echo \u0026#34;----------------------\u0026#34; echo \u0026#34;统计访问最多的10个页面\u0026#34; awk \u0026#39;{a[$7]++}END{print \u0026#34;PV:\u0026#34;,length(a);for(v in a){if(a[v]\u0026gt;10)print v,a[v]}}\u0026#39; $LOG_FILE |sort -k2 -nr echo \u0026#34;----------------------\u0026#34; echo \u0026#34;统计访问页面状态码数量\u0026#34; awk \u0026#39;{a[$7\u0026#34; \u0026#34;$9]++}END{for(v in a){if(a[v]\u0026gt;5)print v,a[v]}}\u0026#39; $LOG_FILE |sort -k3 -nr 13、Nginx访问日志自动按天（周、月）切割 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #!/bin/bash #nginx日志目录 LOG_DIR=/www/server/nginx/logs #获取到上一天的时间 YESTERDAY_TIME=$(date -d \u0026#34;yesterday\u0026#34; +%F) #归档日志取时间 LOG_MONTH_DIR=$LOG_DIR/$(date +\u0026#34;%Y-%m\u0026#34;) #归档日志的名称 LOG_FILE_LIST=\u0026#34;access.log\u0026#34; for LOG_FILE in $LOG_FILE_LIST; do [ ! -d $LOG_MONTH_DIR ] \u0026amp;\u0026amp; mkdir -p $LOG_MONTH_DIR mv $LOG_DIR/$LOG_FILE $LOG_MONTH_DIR/${LOG_FILE}_${YESTERDAY_TIME} done kill -USR1 $(cat $LOG_DIR/nginx.pid) 14、自动发布Java项目（Tomcat） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #!/bin/bash DATE=$(date +%F_%T) TOMCAT_NAME=$1 TOMCAT_DIR=/usr/local/$TOMCAT_NAME ROOT=$TOMCAT_DIR/webapps/ROOT BACKUP_DIR=/data/backup WORK_DIR=/tmp PROJECT_NAME=tomcat-java-demo # 拉取代码 cd $WORK_DIR if [ ! -d $PROJECT_NAME ]; then git clone https://github.com/lizhenliang/tomcat-java-demo cd $PROJECT_NAME else cd $PROJECT_NAME git pull fi # 构建 mvn clean package -Dmaven.test.skip=true if [ $? -ne 0 ]; then echo \u0026#34;maven build failure!\u0026#34; exit 1 fi # 部署 TOMCAT_PID=$(ps -ef |grep \u0026#34;$TOMCAT_NAME\u0026#34; |egrep -v \u0026#34;grep|$$\u0026#34; |awk \u0026#39;NR==1{print $2}\u0026#39;) [ -n \u0026#34;$TOMCAT_PID\u0026#34; ] \u0026amp;\u0026amp; kill -9 $TOMCAT_PID [ -d $ROOT ] \u0026amp;\u0026amp; mv $ROOT $BACKUP_DIR/${TOMCAT_NAME}_ROOT$DATE unzip $WORK_DIR/$PROJECT_NAME/target/*.war -d $ROOT $TOMCAT_DIR/bin/startup.sh 15、自动发布PHP项目 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #!/bin/bash DATE=$(date +%F_%T) WWWROOT=/usr/local/nginx/html/$1 BACKUP_DIR=/data/backup WORK_DIR=/tmp PROJECT_NAME=php-demo # 拉取代码 cd $WORK_DIR if [ ! -d $PROJECT_NAME ]; then git clone https://github.com/lizhenliang/php-demo cd $PROJECT_NAME else cd $PROJECT_NAME git pull fi # 部署 if [ ! -d $WWWROOT ]; then mkdir -p $WWWROOT rsync -avz --exclude=.git $WORK_DIR/$PROJECT_NAME/* $WWWROOT else rsync -avz --exclude=.git $WORK_DIR/$PROJECT_NAME/* $WWWROOT fi 16、DOS攻击防范（自动屏蔽攻击IP） 1 2 3 4 5 6 7 8 9 10 11 12 #!/bin/bash DATE=$(date +%d/%b/%Y:%H:%M) #nginx日志 LOG_FILE=/usr/local/nginx/logs/demo2.access.log #分析ip的访问情况 ABNORMAL_IP=$(tail -n5000 $LOG_FILE |grep $DATE |awk \u0026#39;{a[$1]++}END{for(i in a)if(a[i]\u0026gt;10)print i}\u0026#39;) for IP in $ABNORMAL_IP; do if [ $(iptables -vnL |grep -c \u0026#34;$IP\u0026#34;) -eq 0 ]; then iptables -I INPUT -s $IP -j DROP echo \u0026#34;$(date +\u0026#39;%F_%T\u0026#39;) $IP\u0026#34; \u0026gt;\u0026gt; /tmp/drop_ip.log fi done 17、目录入侵检测与告警 1 2 3 4 5 6 7 8 9 10 #!/bin/bash MON_DIR=/opt inotifywait -mqr --format %f -e create $MON_DIR |\\ while read files; do #同步文件 rsync -avz /opt /tmp/opt #检测文件是否被修改 #echo \u0026#34;$(date +\u0026#39;%F %T\u0026#39;) create $files\u0026#34; | mail -s \u0026#34;dir monitor\u0026#34; xxx@163.com done ","permalink":"https://ktzxy.top/posts/a119mbv6vt/","summary":"实用 shell 脚本","title":"实用 shell 脚本"},{"content":"Kettle精讲 一、kettle简介 1. kettle的发展史 Kettle最早是一个开源的ETL工具，全称为KDE Extraction, Transportation, Transformation and Loading Environment。KDE源于最开始的计划是在K Desktop Environment(www.kde.org)上开发这个软件，但这个计划被取消。在2006年，Pentaho公司收购了Kettle项目，原Kettle项目发起人Matt Casters加入了Pentaho团队，成为Pentaho套件数据集成架构师，从此，Kettle成为企业级数据集成及商业智能套件Pentaho的主要组成部分，Kettle亦重命名为Pentaho Data Integration（PDI）。Pentaho公司于2015年被Hitachi（日立） Data Systems收购，Hitachi Data Systems于2017年改名为Hitachi Vantara。\nPentaho Data Integration以Java开发，支持跨平台运行，可以在Window、Linux、Unix上运行，绿色无需安装，数据抽取高效稳定。Pentaho Data Integration分为商业版与开源版，开源版的截止2021年1月的累计下载量达836万，其中19%来自中国。在中国，一般人仍习惯把Pentaho Data Integration（PDI）的开源版称为Kettle。\n2. kettle 与ETL ETL（Extract-Transform-Load的缩写，即数据抽取、转换、装载的过程）\n在各个企业中，对数据的处理几乎成为其数字化发展的必要流程，而数据的处理，无外乎抽取、统计分析、转换、装载，因此，各个企业目前都需要ETL工程师来完成数据的处理工作。\nkettle是一个ETL工具，允许管理来自不同异构数据源的数据，并在基于图形化的工具中，完成对ETL的操作。\n3. kettle的架构 Transformation：转换\n在大部分场景下，可以直接称之为“数据流” 它可以完成对数据的【输入】\u0026ndash;\u0026gt;【处理】\u0026ndash;\u0026gt;【输出】 一旦启动一个转换任务，则其中的所有组件会同时启动，并根据配置，逐条处理数据 Job：作业\n作业可以称之为步骤流或者控制流 在作业中，可以挂载（调用）转换任务，也可以挂载（调用）job任务 作业中的各个组件，按照顺序执行，可以对执行的结果进行判断并处理分支 作业可以检测数据表、文件是否存在，执行Shell脚本，执行SQL脚本，获取数据，发送邮件等 核心组件：\n组件 描述 spoon 【勺子】是kettle的图形化工具，可以通过简单的拖拉拽方式完成kettle任务的设计、运行与调试，为kettle最常用的组件。 Pan 【煎锅】Transformation执行器(命令行方式)，Pan用于在终端执行Transformation，没有图形界面 Kitchen 【厨房】Job执行器(命令行方式)，Kitchen用于在终端执行Job，没有图形界面。 Carte 嵌入式Web服务，用于远程执行Job或Transformation，Kettle通过Carte建立集群 4. kettle的特点 免费开源：基于java的免费开源的软件，对商业用户也没有限制，可以在任何的公司中使用。 容易配置：可以在Window、Linux、Unix上运行，绿色无需安装，数据抽取高效稳定。 兼容各种数据源：ETL工具集，它允许你管理来自不同数据库的数据。 简单开发：通过图形界面设计与开发任务，无需写代码实现。 二、kettle的安装 安装要求：\n安装所在的服务器或者Windows中，需要jdk1.8 在获取到压缩包之后，将压缩包解压至无中文路径下即可，注意，是整体路径中，任何一级目录中都不包含中文\n解压后的目录结构如下：\n三、kettle的初体验 需求：将一个csv文件中的数据内容输出到Excel文件中\n1. 新增转换任务 新增一个转换任务的方式：\n2. 配置csv输入组件（step） ==注意：内容包含中文，可在文件编码选择utf-8；根据字段内容选择合适的类型==\n3. 配置Excel输出组件（step） 1）通过拖拽的方式将Excel输出放入编辑页面中\n2）将输入组件与输出组件连接到一起：\n3）双击Excel输出组件，对内容进行配置\n4. 创建作业（job） 1）双击【主对象树】中的作业或点击【文件】-【新建】-【作业】\n2）每个任务由一个start组件开始\n5. 在作业中挂载转换任务 配置转换任务与结束节点\n测试运行 保存任务并执行\n四、kettle名词解释 1. 转换 转换（transformation）是ETL解决方案中最主要的部分，它处理抽取、转换、加载各阶段各种对数据的操作。转换包含一个或多个“Step-步骤”，例如读取文件，过滤数据，数据加载等操作都是步骤。转换里的步骤通过“Hop-跳”来连接，跳定义了一个单向通道，允许数据从一个步骤向另一个步骤流动。此外，转换中的每个步骤还可以注释，目的主要是使转换文档化。\n每个“Transformation-转换”对应的保存文件名称为“xx.ktr”\n2. Step-步骤 Step是转换里的基本组成部分。\n“CSV文件输入”和“Excel输出”显示了两个Step步骤。\n每个Step都有唯一的一个名字，一个Step可以有多个输出跳，一个“步骤”的数据有多个输出跳时可以设置数据“分发”或者“复制”，“分发”是目标“步骤”轮流接收数据，“复制”是所有的记录被同时发送到所有的目标“步骤”。\n==注意：分发会导致一份文件中的内容被发送到不通的文件中，输出到两个文件中的内容不同。==\n3. Hop-跳 “Hop-跳”就是步骤之间带箭头的连线，跳定义了步骤之间的数据通路。在转换中“跳”不能循环，因为每个步骤都依赖前一个步骤获取字段值。所以转换任务是一个DAG（有向无环图）\n“Hop-跳”实际上是两个“Step-步骤”之间的记录行的缓存，缓存数据量可以在转换配置中设置（双击配置页面空白处），当缓存记录数满了，写数据的步骤停止写入，此时输出步骤不会停止，持续的读取缓存数据，直到缓存中有空间，写数据步骤继续运行。当缓存清空后，读取数据的步骤停止读取数据，直到缓存中又有数据。\n当单击“跳”时，连线变灰色，代表不使用，再次单击变蓝代表启用。\n4. 并行（转换） 当“Transformation-转换”启动后，所有“Step-步骤”都同时启动，这些“Step-步骤”都是并发方式运行，各自从对应的输入跳中读取数据，并把处理过的数据写到输出跳，直到输入跳里不再有数据，就终止步骤的运行，当所有的步骤都终止了，整个转换就停止了。\n“Transformation-转换”里的步骤几乎是同时启动的，所以不可能判断出哪个步骤是第一个启动的步骤。如果想要一个任务沿着指定的顺序执行，那么就要使用“Job-作业”。\n5. 数据类型 数据以数据行的形式沿着步骤移动。一个数据行是零到多个字段的集合，字段包括下面几种数据类型：\nString： 字符类型 Number： 双精度浮点数（3.14） Integer： 带符号长整型 BigNumber： 任意精度数值（3.141592653） Date： 带毫秒精度的日期时间值 Boolean： 取值为true和false的布尔值 Binary： 二进制字段可以包括图形、声音、视频及其他类型的二进制数据 6. Job-作业 大多数ETL项目都需要完成各种各样的操作，而且这些操作要按照一定顺序完成。因为转换以并行方式执行，就需要一个可以串行执行的作业来处理这些操作。\n作业是步骤流，转换是数据流，这是作业和转换的最大区别。\n作业的每个步骤必须等到前面的步骤都跑完了，后面的步骤才会执行\n而转换会一次性把所有的控件全部先启动（一个控件对应启动一个线程）然后数据流会从第一个控件开始，一条记录，一条记录的流向最后的控件。\n一个作业包括一个或多个作业项，这些作业项以某种顺序来执行。作业执行顺序由作业项之间的跳和每个作业项的执行结果来决定。 可以单机作业跳来改变作业跳的状态，有三种状态（锁-必须执行、对号-执行成功时、错号-执行失败时，这三种状态可以通过单击连线进行切换）。\n上图中的“Start”是“job-作业”的起点，一个作业只能定义一个“Start”。上图中每个“转换”就是作业的作业项，作业项是作业的基本构成部分。默认情况下作业中作业项都是以串行的方式制定，只是在特殊的情况下以并行方式执行。\n当作业中有多条路径时，会采用回溯算法来执行作业项，如下图：\n回溯算法就是：假设扫行到了图里的一条路径的某个节点时，要依次扫行这个节点的所有子路径，直到没有再可以执行的子路径，就返回该节点的上一节点，再反复这个过程。\n上图中的三个作业的执行顺序如下：\n首先“开始”作业项搜索所有下一个节点作业项，找到了“A”和“C”\n执行“A” 搜索“A”后面的作业项，发现了“B” 执行“B” 搜索“B”后面的作业项，没有找到任何作业项 回到“A”，也没有发现其他作业项（需要被执行的作业项） 回到Start，发现另一个要执行的作业项“C” 执行“C” 搜索“C”后面的作业项，没有找到任何作业项 回到Start，没有找到任何作业项 作业结束。 以上执行过程就是Start-\u0026gt;A-\u0026gt;B-\u0026gt;C,也有可能是Start-\u0026gt;C-\u0026gt;A-\u0026gt;B。\n作业除了以上串行执行外，还可以并行执行：\n每个“Job-作业”对应的保存文件名称为“xx.kjb”。\n五、转换核心对象 1. 输入 1) CSV文件输入 2) Excel输入 注意：kettle不支持读取使用Excel2007创建的Excel2003文件；03是xls，07以后是xlsx\n3) 文本文件输入 4）生成记录 补充：如何将任务放入到linux中执行\n首先将kettle的压缩包放入linux中，并通过unzip进行解压，如果没有指令可以通过yum install unzip 进行安装 解压指令：unzip 包名 -d /opt/installs 将配置好的任务传入linux中，建议在kettle的目录中新建一个job目录，将其放入 vim 配置文件.ktr 将其中的输出与输入目录改为linux中的具体目录 在kettle的主目录中，执行：./pan.sh -file=./job/generate_input.ktr 5) 表输入 Kettle支持抽取数据库表中的数据，以MySQL为例， 需要将mysql的驱动包放入Kettle解压目录“\u0026hellip;\\data-integration\\lib”下，这里放入之后，需要重新启动Kettle。\n在新建的一个【转换】配置了mysql数据库的连接，但在其他的转换任务中，无法直接使用，需要将此数据库连接共享才可以完成所有转换任务的共同使用。\n2. 输出 1) Excel输出/MicrosoftExcel输出 Excel输出”和“MicrosoftExcel输出”都是Excel输出，不同的是“Excel输出”写出支持“xls”格式，“MicrosoftExcel输出”支持“xls”和“xlsx”格式。\n2) SQL文件输出 在某些场景下，需要对数据库中的某个表进行备份或者迁移的动作，可以使用【SQL文件输出】步骤进行数据的处理。\n3) 文本文件输出 文本文件输出可以将各种数据源中的数据生成为文本文件，大多数的使用场景是将数据放入linux中，供hive等数据库数据分析使用。\n4) 表输出 kettle9.3连接mysql8.0x需要选项中加这三个参数，否则点击测试没有反应，mysql驱动用8.0.28，不能用localhost，要用127.0.0.1\n参数 (Parameter) 值 (Value) useSSL false allowPublicKeyRetrieval true serverTimezone Asia/Shanghai 1 注意：如果数据中存在中文，则需要在数据库配置处添加命名参数 命名参数：characterEncoding\n值：utf8\n3. 转换 1) Concat fields 在输出的时候，记得设定合并后的字段的长度不能太短\n2) 值映射 当遇到需要将某个字段中的值，根据值的内容来进行数据映射的时候，使用【值映射】步骤\n类似于 0 代表男 1 代表女，输出的时候不输出 0 或者1 ，而是输出 男或者女\n要求：在值映射的组件中进行字段值的配置时，“源值”与“目标值”的字段类型需要一致。\n3) 增加序列 相当于 row_number() over()\n4) 字段选择 当需要对抽取的数据只获取其中的某几列字段的时候，可以使用字段选择step\n【字段选择】step还可以对字段的名称进行更改，以及变更其数据类型\nKettle调优 1、调整JVM大小进行性能优化，修改Kettle根目录下的Spoon脚本。\n参数参考：\n-Xmx2048m：设置JVM最大可用内存为2048M。\n-Xms1024m：设置JVM促使内存为1024m。此值可以设置与-Xmx相同，以避免每次垃圾回收完成后JVM重新分配内存。\n-Xmn2g：设置年轻代大小为2G。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m，所以增大年轻代后，将会减小年老代大小。此值对系统性能影响较大，Sun官方推荐配置为整个堆的3/8。\n-Xss128k：设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M，以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下，减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的，不能无限生成，经验值在3000~5000左右。\n2、 调整提交（Commit）记录数大小进行优化，Kettle默认Commit数量为：1000，可以根据数据量大小来设置Commitsize：1000~50000\n3、尽量使用数据库连接池；\n4、尽量提高批处理的commit size；\n5、尽量使用缓存，缓存尽量大一些（主要是文本文件和数据流）；\n6、Kettle是Java做的，尽量用大一点的内存参数启动Kettle；\n7、可以使用sql来做的一些操作尽量用sql；\nGroup , merge , stream lookup,split field这些操作都是比较慢的，想办法避免他们.，能用sql就用sql；\n8、插入大量数据的时候尽量把索引删掉；\n9、尽量避免使用update , delete操作，尤其是update,如果可以把update变成先delete, 后insert；\n10、能使用truncate table的时候，就不要使用deleteall row这种类似sql合理的分区，如果删除操作是基于某一个分区的，就不要使用delete row这种方式（不管是deletesql还是delete步骤）,直接把分区drop掉，再重新创建；\n11、尽量缩小输入的数据集的大小（增量更新也是为了这个目的）；\n12、尽量使用数据库原生的方式装载文本文件(Oracle的sqlloader, mysql的bulk loader步骤)。\n","permalink":"https://ktzxy.top/posts/nk1fwk43b9/","summary":"Kettle精讲","title":"Kettle精讲"},{"content":"[TOC]\nPrometheus+Grafana+Alertmanager实现告警推送教程 一、alertmanager 1.1 alertmanager介绍 监控告警实现需要依赖 Alertmanager。\n1.2 源码方式安装启动 1. 文件准备 将下载好的Alertmanager文件解压\n输入\n1 tar -zxvf alertmanager-0.21.0.linux-386.tar.gz 然后移动到/opt/prometheus文件夹里面，没有该文件夹则创建\n2. alertmanager启动 root用户下启动\n输入:\n1 nohup ./alertmanager \u0026gt;/dev/null 2\u0026gt;\u0026amp;1 \u0026amp; 启动成功之后，在浏览器上输入 ip+9093可以查看相关信息\n示例图: 1.3 docker方式安装启动 1 docker run -d --name alertmanager -p 9093:9093 -v /home/prometheus/alertmanager.yml:/etc/alertmanager/alertmanager.yml prom/alertmanager:latest 1.4 alertmanager邮箱告警配置 1. 告警文件alertmanager.yml的配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 global: resolve_timeout: 5m # 所有报警信息进入后的根路由，用来设置报警的分发策略 smtp_from: \u0026#39;1090239782@qq.com\u0026#39; smtp_smarthost: \u0026#39;smtp.qq.com:465\u0026#39; smtp_auth_username: \u0026#39;1090239782@qq.com\u0026#39; smtp_auth_password: \u0026#39;dmsitabajsjbhbda\u0026#39; smtp_require_tls: false smtp_hello: \u0026#39;qq.com\u0026#39; route: # 这里的标签列表是接收到报警信息后的重新分组标签，例如，接收到的报警信息里面有许多具有 cluster=A 和 alertname=LatncyHigh 这样的标签的报警信息将会批量被聚合到一个分组里面 group_by: [\u0026#39;alertname\u0026#39;, \u0026#39;cluster\u0026#39;] #当一个新的报警分组被创建后，需要等待至少group_wait时间来初始化通知，这种方式可以确保您能有足够的时间为同一分组来获取多个警报，然后一起触发这个报警信息 group_wait: 30s # 当第一个报警发送后，等待\u0026#39;group_interval\u0026#39;时间来发送新的一组报警信息。 group_interval: 5m # 如果一个报警信息已经发送成功了，等待\u0026#39;repeat_interval\u0026#39;时间来重新发送他们 repeat_interval: 5m # 默认的receiver：如果一个报警没有被一个route匹配，则发送给默认的接收器 receiver: \u0026#39;email\u0026#39; # 优先使用default发送 receivers: - name: \u0026#39;email\u0026#39; email_configs: - to: \u0026#39;1090239782@qq.com\u0026#39; send_resolved: true inhibit_rules: - source_match: severity: \u0026#39;critical\u0026#39; target_match: severity: \u0026#39;warning\u0026#39; equal: [\u0026#39;alertname\u0026#39;, \u0026#39;dev\u0026#39;, \u0026#39;instance\u0026#39;] 注: smtp_from、smtp_auth_username、to的邮箱可以填写同一个，smtp_auth_password填写鉴权码，需要开启POS3。\n1.5 添加告警模板 在alertmanagers的文件夹下创建一个template文件夹，然后在该文件夹创建一个微信告警的模板wechat.tmpl，添加如下配置:\n1 2 3 4 5 6 7 8 9 10 11 12 {{ define \u0026#34;wechat.default.message\u0026#34; }} {{ range .Alerts }} ========start========= 告警程序: prometheus_alert 告警级别: {{ .Labels.severity}} 告警类型: {{ .Labels.alertname }} 故障主机: {{ .Labels.instance }} 告警主题: {{ .Annotations.summary }} 告警详情: {{ .Annotations.description }} =========end=========== {{ end }} {{ end }} 然后再到alertmanager.yml 添加如下配置:\n1 2 templates: - \u0026#39;/opt/prometheus/alertmanager-0.21.0.linux-386/template/wechat.tmpl\u0026#39; 效果图: 1.6 内存告警设置 1 2 3 4 5 6 7 8 9 10 11 - name: test-rule rules: - alert: \u0026#34;内存报警\u0026#34; expr: 100 - ((node_memory_MemAvailable_bytes * 100) / node_memory_MemTotal_bytes) \u0026gt; 30 for: 15s labels: severity: warning annotations: summary: \u0026#34;服务名:{{$labels.instance}}内存使用率超过30%了\u0026#34; description: \u0026#34;业务500报警: {{ $value }}\u0026#34; value: \u0026#34;{{ $value }}\u0026#34; 1.7 磁盘告警配置 1. 总量百分比设置: 1 (node_filesystem_size_bytes {mountpoint =\u0026#34;/\u0026#34;} - node_filesystem_free_bytes {mountpoint =\u0026#34;/\u0026#34;}) / node_filesystem_size_bytes {mountpoint =\u0026#34;/\u0026#34;} * 100 2. 查看某一目录的磁盘使用百分比 1 (node_filesystem_size_bytes{mountpoint=\u0026#34;/boot\u0026#34;}-node_filesystem_free_bytes{mountpoint=\u0026#34;/boot\u0026#34;})/node_filesystem_size_bytes{mountpoint=\u0026#34;/boot\u0026#34;} * 100 3. 正则表达式来匹配多个挂载点 (node_filesystem_size_bytes{mountpoint=\u0026quot;/|/run\u0026quot;}-node_filesystem_free_bytes{mountpoint=\u0026quot;/|/run\u0026quot;}) / node_filesystem_size_bytes{mountpoint=~\u0026quot;/|/run\u0026quot;} * 100\n4. 预计多长时间磁盘爆满 predict_linear(node_filesystem_free_bytes {mountpoint =\u0026quot;/\u0026quot;}[1h], 43600) \u0026lt; 0 predict_linear(node_filesystem_free_bytes {job=\u0026ldquo;node\u0026rdquo;}[1h], 43600) \u0026lt; 0\n1.8 CPU使用率 100 - (avg(irate(node_cpu_seconds_total{mode=\u0026ldquo;idle\u0026rdquo;}[5m])) by (instance) * 100)\n1.9 空闲内存剩余率 (node_memory_MemFree_bytes+node_memory_Cached_bytes+node_memory_Buffers_bytes) / node_memory_MemTotal_bytes * 100\n1.10 内存使用率 100 - (node_memory_MemFree_bytes+node_memory_Cached_bytes+node_memory_Buffers_bytes) / node_memory_MemTotal_bytes * 100\n1.11 磁盘使用率 100 - (node_filesystem_free_bytes{mountpoint=\u0026quot;/\u0026quot;,fstype=\u0026ldquo;ext4|xfs\u0026rdquo;} / node_filesystem_size_bytes{mountpoint=\u0026quot;/\u0026quot;,fstype=\u0026ldquo;ext4|xfs\u0026rdquo;} * 100)\n二、webhook 1 docker run -d -p 8060:8060 --name webhook timonwong/prometheus-webhook-dingtalk --ding.profile=\u0026#34;webhook1=https://oapi.dingtalk.com/robot/send?access_token=钉钉token\u0026#34; 三、prometheus prometheus-config.yml\n1 2 3 4 5 6 7 8 9 alerting: alertmanagers: - static_configs: - targets: - 192.168.0.50:9093 rule_files: - \u0026#34;/home/prometheus/rules/node_down.yml\u0026#34; # 实例存活报警规则文件 - \u0026#34;/home/prometheus/rules/memory_over.yml\u0026#34; # 内存报警规则文件 - \u0026#34;/home/prometheus/rules/cpu_over.yml\u0026#34; # cpu报警规则文件 ","permalink":"https://ktzxy.top/posts/c2r6qt27h2/","summary":"prometheus+alertmanager+webhook实现钉钉告警","title":"prometheus+alertmanager+webhook实现钉钉告警"},{"content":"select检查 UDF用户自定义函数\nSQL语句的select后面使用了自定义函数UDF，SQL返回多少行，那么UDF函数就会被调用多少次，这是非常影响性能的。\n1 2 #getOrderNo是用户自定义一个函数用户来根据order_sn来获取订单编号 select id, payment_id, order_sn, getOrderNo(order_sn) from payment_transaction where status = 1 and create_time between \u0026#39;2020-10-01 10:00:00\u0026#39; and \u0026#39;2020-10-02 10:00:00\u0026#39;; text类型检查\n如果select出现text类型的字段，就会消耗大量的网络和IO带宽，由于返回的内容过大超过max_allowed_packet设置会导致程序报错，需要评估谨慎使用。\n1 2 #表request_log的中content是text类型。 select user_id, content, status, url, type from request_log where user_id = 32121; group_concat谨慎使用\ngorup_concat是一个字符串聚合函数，会影响SQL的响应时间，如果返回的值过大超过了max_allowed_packet设置会导致程序报错。\n1 select batch_id, group_concat(name) from buffer_batch where status = 0 and create_time between \u0026#39;2020-10-01 10:00:00\u0026#39; and \u0026#39;2020-10-02 10:00:00\u0026#39;; 内联子查询\n在select后面有子查询的情况称为内联子查询，SQL返回多少行，子查询就需要执行过多少次，严重影响SQL性能。\n1 select id,(select rule_name from member_rule limit 1) as rule_name, member_id, member_type, member_name, status from member_info m where status = 1 and create_time between \u0026#39;2020-09-02 10:00:00\u0026#39; and \u0026#39;2020-10-01 10:00:00\u0026#39;; from检查 表的链接方式\n在MySQL中不建议使用Left Join，即使ON过滤条件列索引，一些情况也不会走索引，导致大量的数据行被扫描，SQL性能变得很差，同时要清楚ON和Where的区别。\n1 2 SELECT a.member_id,a.create_time,b.active_time FROM operation_log a LEFT JOIN member_info b ON a.member_id = b.member_id where b.`status` = 1 and a.create_time between \u0026#39;2020-10-01 00:00:00\u0026#39; and \u0026#39;2020-10-30 00:00:00\u0026#39; limit 100, 0; 子查询\n由于MySQL的基于成本的优化器CBO对子查询的处理能力比较弱，不建议使用子查询，可以改写成Inner Join。\n1 2 select b.member_id,b.member_type, a.create_time,a.device_model from member_operation_log a inner join (select member_id,member_type from member_base_info where `status` = 1 and create_time between \u0026#39;2020-10-01 00:00:00\u0026#39; and \u0026#39;2020-10-30 00:00:00\u0026#39;) as b on a.member_id = b.member_id; where检查 索引列被运算\n当一个字段被索引，同时出现where条件后面，是不能进行任何运算，会导致索引失效。\n1 2 3 4 #device_no列上有索引，由于使用了ltrim函数导致索引失效 select id, name , phone, address, device_no from users where ltrim(device_no) = \u0026#39;Hfs1212121\u0026#39;; #balance列有索引,由于做了运算导致索引失效 select account_no, balance from accounts where balance + 100 = 10000 and status = 1; 类型转换\n对于Int类型的字段，传varchar类型的值是可以走索引，MySQL内部自动做了隐式类型转换；相反对于varchar类型字段传入Int值是无法走索引的，应该做到对应的字段类型传对应的值总是对的。\n1 2 3 4 #user_id是bigint类型，传入varchar值发生了隐式类型转换，可以走索引。 select id, name , phone, address, device_no from users where user_id = \u0026#39;23126\u0026#39;; #card_no是varchar(20)，传入int值是无法走索引 select id, name , phone, address, device_no from users where card_no = 2312612121; 列字符集\n从MySQL 5.6开始建议所有对象字符集应该使用用utf8mb4，包括MySQL实例字符集，数据库字符集，表字符集，列字符集。避免在关联查询Join时字段字符集不匹配导致索引失效，同时目前只有utf8mb4支持emoji表情存储。\n1 2 3 4 character_set_server = utf8mb4 #数据库实例字符集 character_set_connection = utf8mb4 #连接字符集 character_set_database = utf8mb4 #数据库字符集 character_set_results = utf8mb4 #结果集字符集 group by检查 前缀索引\ngroup by后面的列有索引，索引可以消除排序带来的CPU开销，如果是前缀索引，是不能消除排序的。\n1 2 3 #device_no字段类型varchar(200)，创建了前缀索引。 mysql\u0026gt; alter table users add index idx_device_no(device_no(64)); mysql\u0026gt; select device_no, count(*) from users where create_time between \u0026#39;2020-10-01 00:00:00\u0026#39; and \u0026#39;2020-10-30 00:00:00\u0026#39; group by device_no; 函数运算\n假设需要统计某月每天的新增用户量，参考如下SQL语句，虽然可以走create_time的索引，但是不能消除排序，可以考虑冗余一个字段stats_date date类型来解决这种问题。\n1 select DATE_FORMAT(create_time, \u0026#39;%Y-%m-%d\u0026#39;), count(*) from users where create_time between \u0026#39;2020-09-01 00:00:00\u0026#39; and \u0026#39;2020-09-30 23:59:59\u0026#39; group by DATE_FORMAT(create_time, \u0026#39;%Y-%m-%d\u0026#39;); order by检查 前缀索引\norder by后面的列有索引，索引可以消除排序带来的CPU开销，如果是前缀索引，是不能消除排序的。\n字段顺序\n排序字段顺序，asc/desc升降要跟索引保持一致，充分利用索引的有序性来消除排序带来的CPU开销。\nlimit检查 limit m,n要慎重\n对于limit m, n分页查询，越往后面翻页即m越大的情况下SQL的耗时会越来越长，对于这种应该先取出主键id，然后通过主键id跟原表进行Join关联查询。\n表结构检查 表\u0026amp;列名关键字 在数据库设计建模阶段，对表名及字段名设置要合理，不能使用MySQL的关键字，如desc, order, status, group等。同时建议设置lower_case_table_names = 1表名不区分大小写。\n表存储引擎 对于OLTP业务系统，建议使用InnoDB引擎获取更好的性能，可以通过参数default_storage_engine控制。\nAUTO_INCREMENT属性 建表的时候主键id带有AUTO_INCREMENT属性，而且AUTO_INCREMENT=1，在InnoDB内部是通过一个系统全局变量dict_sys.row_id来计数，row_id是一个8字节的bigint unsigned，InnoDB在设计时只给row_id保留了6个字节的长度，这样row_id取值范围就是0到2^48 - 1，如果id的值达到了最大值，下一个值就从0开始继续循环递增，在代码中禁止指定主键id值插入。\n1 2 3 4 #新插入的id值会从10001开始，这是不对的，应该从1开始。 create table booking( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT \u0026#39;主键id\u0026#39;,......) engine = InnoDB auto_increment = 10000; 指定了id值插入，后续自增就会从该值开始+1，索引禁止指定id值插入。 insert into booking(id, book_sn) values(1234551121, \u0026#39;N12121\u0026#39;); NOT NULL属性 根据业务含义，尽量将字段都添加上NOT NULL DEFAULT VALUE属性，如果列值存储了大量的NULL，会影响索引的稳定性。\nDEFAULT属性 在创建表的时候，建议每个字段尽量都有默认值，禁止DEFAULT NULL，而是对字段类型填充响应的默认值。\nCOMMENT属性 字段的备注要能明确该字段的作用，尤其是某些表示状态的字段，要显式的写出该字段所有可能的状态数值以及该数值的含义。\nTEXT类型 不建议使用Text数据类型，一方面由于传输大量的数据包可能会超过max_allowed_packet设置导致程序报错，另一方面表上的DML操作都会变的很慢，建议采用es或者对象存储OSS来存储和检索。\n索引检查 索引属性 索引基数指的是被索引的列唯一值的个数，唯一值越多接近表的count(*)说明索引的选择率越高，通过索引扫描的行数就越少，性能就越高，例如主键id的选择率是100%，在MySQL中尽量所有的update都使用主键id去更新，因为id是聚集索引存储着整行数据，不需要回表，性能是最高的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 mysql\u0026gt; select count(*) from member_info; +----------+ | count(*) | +----------+ | 148416 | +----------+ 1 row in set (0.35 sec) mysql\u0026gt; show index from member_base_info; +------------------+------------+----------------------------+--------------+-------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | +------------------+------------+----------------------------+--------------+-------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ | member_info | 0 | PRIMARY | 1 | id | A | 131088 | NULL | NULL | | BTREE | | | | member_info | 0 | uk_member_id | 1 | member_id | A | 131824 | NULL | NULL | | BTREE | | | | member_info | 1 | idx_create_time | 1 | create_time | A | 6770 | NULL | NULL | | BTREE | | | +------------------+------------+----------------------------+--------------+-------------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ Table： 表名 Non_unique ：是否为unique index，0-是，1-否。 Key_name：索引名称 Seq_in_index：索引中的顺序号，单列索引-都是1；复合索引-根据索引列的顺序从1开始递增。 Column_name：索引的列名 Collation：排序顺序，如果没有指定asc/desc，默认都是升序ASC。 Cardinality：索引基数-索引列唯一值的个数。 sub_part：前缀索引的长度；例如index (member_name(10)，长度就是10。 Packed：索引的组织方式，默认是NULL。 Null：YES:索引列包含Null值；\u0026#39;\u0026#39;:索引不包含Null值。 Index_type：默认是BTREE，其他的值FULLTEXT，HASH，RTREE。 Comment：在索引列中没有被描述的信息，例如索引被禁用。 Index_comment：创建索引时的备注。 前缀索引 对于变长字符串类型varchar(m)，为了减少key_len，可以考虑创建前缀索引，但是前缀索引不能消除group by， order by带来排序开销。如果字段的实际最大值比m小很多，建议缩小字段长度。\n1 alter table member_info add index idx_member_name_part(member_name(10)); 复合索引顺序 有很多人喜欢在创建复合索引的时候，总以为前导列一定是唯一值多的列，例如索引index idx_create_time_status(create_time, status)，这个索引往往是无法命中，因为扫描的IO次数太多，总体的cost的比全表扫描还大，CBO最终的选择是走full table scan。\nMySQL遵循的是索引最左匹配原则，对于复合索引，从左到右依次扫描索引列，到遇到第一个范围查询（\u0026gt;=, \u0026gt;,\u0026lt;, \u0026lt;=, between ….. and ….）就停止扫描，索引正确的索引顺序应该是index idx_status_create_time(status, create_time)。\n1 select account_no, balance from accounts where status = 1 and create_time between \u0026#39;2020-09-01 00:00:00\u0026#39; and \u0026#39;2020-09-30 23:59:59\u0026#39;; 时间列索引 对于默认字段created_at(create_time)、updated_at(update_time)这种默认就应该创建索引，这一般来说是默认的规则。\nSQL优化案例 通过对慢查询的监控告警，经常发现一些SQL语句where过滤字段都有索引，但是由于SQL写法的问题导致索引失效，下面二个案例告诉大家如何通过SQL改写来查询。可以通过以下SQL来捞取最近5分钟的慢查询进行告警。\n1 select CONCAT( \u0026#39;# Time: \u0026#39;, DATE_FORMAT(start_time, \u0026#39;%y%m%d %H%i%s\u0026#39;), \u0026#39;\\n\u0026#39;, \u0026#39;# User@Host: \u0026#39;, user_host, \u0026#39;\\n\u0026#39;, \u0026#39;# Query_time: \u0026#39;, TIME_TO_SEC(query_time), \u0026#39; Lock_time: \u0026#39;, TIME_TO_SEC(lock_time), \u0026#39; Rows_sent: \u0026#39;, rows_sent, \u0026#39; Rows_examined: \u0026#39;, rows_examined, \u0026#39;\\n\u0026#39;, sql_text, \u0026#39;;\u0026#39; ) FROM mysql.slow_log where start_time between current_timestamp and date_add(CURRENT_TIMESTAMP,INTERVAL -5 MINUTE); 慢查询SQL 1 | 2020-10-02 19:17:23 | w_mini_user[w_mini_user] @ [10.200.20.11] | 00:00:02 | 00:00:00 | 9 | 443117 | mini_user | 0 | 0 | 168387936 | select id,club_id,reason,status,type,created_time,invite_id,falg_admin,file_id from t_user_msg where 1 and (team_id in (3212) and app_id is not null) or (invite_id=12395 or applicant_id=12395) order by created_time desc limit 0,10; | 1219921665 | 从慢查询slow_log可以看到，执行时间2s，扫描了443117行，只返回了9行，这是不合理的。\nSQL分析 1 2 3 4 5 6 7 8 9 #原始SQL，频繁访问的接口，目前执行时间2s。 select id,team_id,reason,status,type,created_time,invite_id,falg_admin,file_id from t_user_msg where 1 and (team_id in (3212) and app_id is not null) or (invite_id=12395 or app_id=12395) order by created_time desc limit 0,10; 执行计划 +----+-------------+--------------+-------+---------------------------------+------------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------------+-------+---------------------------------+------------+---------+------+------+-------------+ | 1 | SIMPLE | t_user_msg | index | invite_id,app_id,team_id | created_time | 5 | NULL | 10 | Using where | +----+-------------+--------------+-------+---------------------------------+------------+---------+------+------+-------------+ 1 row in set (0.00 sec) 从执行计划可以看到，表上有单列索引invite_id,app_id,team_id,created_time，走的是create_time的索引，而且type=index索引全扫描，因为create_time没有出现在where条件后，只出现在order by后面，只能是type=index，这也预示着表数据量越大该SQL越慢，我们期望是走三个单列索引invite_id，app_id，team_id，然后type=index_merge操作。\n按照常规思路，对于OR条件拆分两部分，分别进行分析。\n1 select id, ……. from t_user_msg where 1 and **(team_id in (3212) and app_id is not null)** order by created_time desc limit 0,10; 从执行计划看走的是team_id的索引，没有问题。\n1 2 3 | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------------+------+----------------------+---------+---------+-------+------+-----------------------------+ | 1 | SIMPLE | t_user_msg | ref | app_id,team_id | team_id | 8 | const | 30 | Using where; Using filesort | 再看另外一个sql语句：\n1 select id, ……. from t_user_msg where 1 and **(invite_id=12395 or app_id=12395)** order by created_time desc limit 0,10; 从执行计划上看，分别走的是invite_id,app_id的单列索引，同时做了index_merge合并操作，也没有问题。\n1 2 3 | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------------+-------------+-------------------------+-------------------------+---------+------+------+-------------------------------------------------------------------+ | 1 | SIMPLE | t_user_msg | index_merge | invite_id,app_id | invite_id,app_id | 9,9 | NULL | 2 | Using union(invite_id,app_id); Using where; Using filesort | 通过上面的分析，第一部分SQL走的执行计划走team_id索引没问题，第二部分SQL分别走invite_id,app_id索引并且index_merge也没问题，为什么两部分SQL进行OR关联之后走create_time的单列索引呢，不应该是三个单列索引的index_merge吗？\nindex_merge默认是在优化器选项是开启的，主要是将多个范围扫描的结果集合并成一个，可以通过变量查看。\n1 2 mysql \u0026gt;select @@optimizer_switch; | index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on, 其他三个字段都传入的是具体的值，而且都走了相应的索引，只能怀疑app_id is not null这个条件影响了CBO对最终执行计划的选择，去掉这个条件来看执行计划，竟然走了三个单列索引且type=index_merge，那下面只要搞定app_id is not null这个条件就OK了吧。\n1 2 3 | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------------+-------------+---------------------------------+---------------------------------+---------+------+------+---------------------------------------------------------------------------+ | 1 | SIMPLE | t_user_msg | index_merge | invite_id,app_id,teadm_id | team_id,invite_id,app_id | 8,9,9 | NULL | 32 | Using union(team_id,invite_id,app_id); Using where; Using filesort | SQL改写 通过上面分析得知，条件app_id is not null影响了CBO的选择，下面进行改造。\n改写优化1\n根据SQL开发规范改写，将OR改写成Union All方式即可，最终的SQL如下：\n1 2 3 4 5 select id, ……. from ( select id, ……. from t_user_msg where **1 and (club_id in (5821) and applicant_id is not null)** **union all** select id, ……. from t_user_msg where **1 and invitee_id=\u0026#39;146737\u0026#39;** **union all** select id, ……. from t_user_msg where **1 and app_id=\u0026#39;146737\u0026#39;** ) as a order by created_time desc limit 0,10; 一般情况下，Java代码和SQL是分开的，SQL是配置在xml文件中，根据业务需求，除了team_id是必填，其他两个都是可选的，所以这种改写虽然能提高SQL执行效率，但不适合这种业务场景。\n改写优化2\napp_id is not null 改写为IFNULL(app_id, 0) \u0026gt;0)，最终的SQL为：\n1 select id,team_id,reason,status,type,created_time,invite_id,falg_admin,file_id from t_user_msg where 1 and (team_id in (3212) and **IFNULL(app_id, 0) \u0026gt;0)**) or (invite_id=12395 or app_id=12395) order by created_time desc limit 0,10; 改写优化3\n将字段app_id bigint(20) DEFAULT NULL，变更为app_id bigint(20) NOT NULL DEFAULT 0，同时更新将app_id is null的时候全部更新成0，就可以将条件app_id is not null 转换为app_id \u0026gt; 0，最终的SQL为：\n1 select id,team_id,reason,status,type,created_at,invite_id,falg_admin,file_id from t_user_msg where 1 and (team_id in (3212) and **app_id \u0026gt; 0)**) or (invite_id=12395 or app_id=12395) order by created_time desc limit 0,10; 从执行计划看，两种改写优化方式都走三个单列索引，执行时间从2s降低至10ms，线上采用的是优化1的方式，如果一开始能遵循MySQL开发规范就就会避免问题的发生。\n总结 上面介绍了SQL规范性检查，表结构检查，索引检查以及通过SQL改写来优化查询，在编写代码的过程，如果能提前做这些规范性检查，评估出自己认为理想的执行计划，然后通过explain解析出MySQL CBO的执行计划，两者做对比分析差异，弄清楚自己的选择和CBO的不同，不但能够编写高质量的SQL，同时也能清楚CBO的工作原理。\n","permalink":"https://ktzxy.top/posts/6jl02cmmvr/","summary":"SQL调优","title":"SQL调优"},{"content":"MySQL备份工具之Xtrabackup xtrabackup是percona公司专门针对mysql 数据库开发的一款开源免费的物理备份(热备)工具,可以对innodb和xtradb等事务引擎数据库实现非阻塞(即不锁表)方式的备份,也可以针对myisam等非事务引擎锁表方式备份,是商业备份工具InnoDB Hotbackup的一个很好的替代品。\n1、介绍 1.1 主要特点 物理备份工具，拷贝数据文件 备份和恢复数据的速度非常快，安全可靠 在备份期间执行的事务不会间断，备份innodb数据不影响业务 备份期间不增加太多数据库的性能压力 支持对备份的数据自动校验 运行全量，增量，压缩备份及流备份 支持在线迁移表以及快速创建新的从库 运行几乎所有版本的mysql和maridb 1.2 相关词汇 文件扩展名\n文件扩展名 文件作用说明 .idb文件 以独立表空间存储的InnoDB引擎类型的数据文件扩展名 .ibdata文件 以共享表空间存储的InnoDB引擎类型的数据文件扩展名 .frm文件 存放于表相关的元数据(meta)信息及表结构的定义信息 .MYD文件 存放MyISAM引擎表的数据文件扩展名 .MYI文件 存放MyISAM引擎表的索引信息文件扩展名 名词\nredo日志redo日志，也称事务日志，是innodb引擎的重要组成部分，作用是记录innodb引擎中每一个数据发生的变化信息。主要用于保证innodb数据的完整性，以及丢数据后的恢复，同时可以有效提升数据库的io等性能。redo日志对应的配置参数为innodb_log_file_size和innodb_log_files_in_group Undo日志Undo是记录事务的逆向逻辑操作或者向物理操作对应的数据变化的内容，undo日志默认存放在共享表空间里面的ibdata*文件，和redo日志功能不同undo日志主要用于回滚数据库崩溃前未完整提交的事务数据,确保数据恢复前后一致。 LSNLSN，全拼log sequence number,中文是日志序列号，是一个64位的整型数字，LSN的作用是记录redo日志时，使用LSN唯一标识一条变化的数据。 checkpoint 用来标识数据库崩溃后,应恢复的redo log的起始点 1.3 XtraBackup备份原理 checkpoint，记录LSN号码 information schema.xxx备份 拷贝innoDB文件，过程中发生的新变化redo也会被保存，保存至备份路径 Binlog只读，FTWRL（global read lock） 拷贝Non InnoDB，拷贝完成解锁 生成备份相关的信息文件：binlog、LSN 刷新Last LSN 完成备份 备份时经历的阶段：\nInnoDB表：\n热备份：业务正常发生的时候，影响较小的备份方式 checkpoint：将已提交的数据页刷新到磁盘，记录一个LSN号码 拷贝InnoDB表相关的文件(ibdata1、frm、ibd\u0026hellip;) 备份期间产生的新的数据变化redo也会备份走 非InnoDB表：\n温备份：锁表备份 触发FTWRL全局锁表 拷贝非InnoDB表的数据 解锁 再次统计LSN号码，写入到专用文件xtrabackup checkpoint记录二进制日志位置 所有备份文件统一存放在一个目录下，备份完成\n1.4 XtraBackup恢复步骤 做恢复前准备 做数据合并，增量和全备份的数据合并 全备数据，先把全备的redo lo文件内容和全备数据合并，并且read only不进行回滚 把第一次增量的redo log变化加载到第一次增量数据再与全量数据做合并 把第二次增量的redo log变化加载到第二次增量数据备份，在与全量和第一次增量的合并再进行合并， 最后把脏数据进行提交或回滚 恢复binlog的文件内容 2、安装 2.1 安装依赖包 1 2 # wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo # yum -y install perl perl-devel libaio libaio-devel perl-Time-HiRes perl-DBD-MySQL libev 2.2 下载软件并安装 这里使用的是清华源，官方地址下载较慢。官方最新的是8.0版本，此版本只适用于mysql8.0版本的数据库，所以这里下载支持mysql5.6的版本\n1 2 # wget -c https://mirrors.tuna.tsinghua.edu.cn/percona/centos/7/os/x86_64/percona-xtrabackup-24-2.4.18-1.el7.x86_64.rpm # yum localinstall -y percona-xtrabackup-24-2.4.18-1.el7.x86_64.rpm 3、全量备份和恢复 3.1 前提 数据库处于运行状态 xtrabackup能连接上数据库：在mysql配置文件client下指定socket位置标签或者在使用时指定 1 2 [client] socket=/tmp/mysql.sock 读取配置文件mysqld下的datadir参数 1 2 [mysqld] datadir=/usr/local/mysql/data 开启了binlog 1 2 3 log-bin = /data/mysql/mysql-bin binlog_format=\u0026#34;ROW\u0026#34; expire_logs_days=3 xtrabackup是服务器端工具，不能远程备份 3.2 全备 1 # innobackupex --user=root --password=123456 /backup/xbk/ 在做全备时为了控制生成的目录名称，可以添加参数--no-timestamp并保留日期\n1 # innobackupex --user=root --password=123456 --no-timestamp /backup/xbk/full_`date +%F` 3.3 备份结果 在备份目录下查看备份的文件，除了mysql自身的数据文件外，还有这样几个文件\n1 2 3 4 5 6 7 8 # pwd /backup/xbk/2020-03-25_10-26-16 # ll ... -rw-r-----. 1 root root 27 Mar 25 10:53 xtrabackup_binlog_info -rw-r-----. 1 root root 147 Mar 25 10:53 xtrabackup_checkpoints -rw-r-----. 1 root root 480 Mar 25 10:53 xtrabackup_info -rw-r-----. 1 root root 31987200 Mar 25 10:53 xtrabackup_logfile xtrabackup_binlog_info 备份时刻的binlog位置 记录的是备份时刻，binlog的文件名字和当时的结束的position，可以用来作为截取binlog时的起点 1 2 # cat xtrabackup_binlog_info mysql-bin.000001 192790323 xtrabackup_checkpoints\n备份时刻，立即将已经commit过的，内存中的数据页刷新到磁盘CKPT开始备份数据，数据文件的LSN会停留在to_lsn位置 备份时刻有可能会有其他的数据写入，已备走的数据文件就不会再发生变化了 在备份过程中，备份软件会一直监控着redo的undo，如果一旦有变化会将日志也一并备走，并记录LSN到last_lsn，从to_lsn——\u0026gt;last_lsn就是，备份过程中产生的数据变化 1 2 3 4 5 6 7 8 # cat xtrabackup_checkpoints backup_type = full-backuped from_lsn = 0 # 上次所到达的LSN号(对于全备就是从0开始,对于增量有别的显示方法) to_lsn = 14194921406 # 备份开始时间(ckpt)点数据页的LSN last_lsn = 14200504300 # 备份结束后，redo日志最终的LSN compact = 0 recover_binlog_info = 0 flushed_lsn = 14177446392 xtrabackup_info 备份的全局信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # cat xtrabackup_info uuid = c04f3d33-6e43-11ea-9224-005056ac7d7c name = tool_name = innobackupex tool_command = --user=root --password=... /backup/xbk/ tool_version = 2.4.18 ibbackup_version = 2.4.18 server_version = 5.6.46-log start_time = 2020-03-25 10:26:16 end_time = 2020-03-25 10:53:05 lock_time = 0 binlog_pos = filename \u0026#39;mysql-bin.000001\u0026#39;, position \u0026#39;192790323\u0026#39; innodb_from_lsn = 0 innodb_to_lsn = 14194921406 partial = N incremental = N format = file compact = N compressed = N encrypted = N xtrabackup_logfile 备份过程中的redo，关联在备份期间对InnoDB表产生的新变化 3.4 全备份的恢复 恢复流程：\nxbk备份执行的瞬间,立即触发ckpt,已提交的数据脏页,从内存刷写到磁盘,并记录此时的LSN号 备份时，拷贝磁盘数据页，并且记录备份过程中产生的redo和undo一起拷贝走,也就是checkpoint LSN之后的日志 在恢复之前，模拟Innodb“自动故障恢复”的过程，将redo（前滚）与undo（回滚）进行应用 恢复过程是cp备份到原来数据目录下 模拟数据库宕机，删除数据\n1 2 # pkill mysqld # rm -rf datadir=/usr/local/mysql/data/* prepare预处理备份文件，将redo进行重做，已提交的写到数据文件，未提交的使用undo回滚掉。模拟了CSR的过程\n1 # innobackupex --apply-log /backup/xbk/2020-03-25_10-26-16 数据恢复并启动数据库\n1 2 3 # cp -a /backup/xbk/2020-03-25_10-26-16/* /usr/local/mysql/data/ # chown -R mysql.mysql /usr/local/mysql/data/ # /etc/init.d/mysqld start 4、增量备份和恢复 4.1 前提 增量必须依赖于全备 每次增量都是参照上次备份的LSN号码（xtrabackup checkpoints），在此基础上变化的数据页进行备份 会将备份过程中产生新的变化的redo一并备份走 恢复时增量备份无法单独恢复，必须基于全备进行恢复。必须将所有的增量备份，按顺序全部合并到全备中\n4.2 增量备份 全量备份 1 # innobackupex --user=root --password --no-timestamp /backup/full \u0026gt;\u0026amp;/tmp/xbk_full.log 第一次模拟新数据变化 1 2 3 4 5 db01 [(none)]\u0026gt;create database cs charset utf8; db01 [(none)]\u0026gt;use cs db01 [cs]\u0026gt;create table t1 (id int); db01 [cs]\u0026gt;insert into t1 values(1),(2),(3); db01 [cs]\u0026gt;commit; 第一次增量备份 1 # innobackupex --user=root --password=123 --no-timestamp --incremental --incremental-basedir=/backup/full /backup/inc1 \u0026amp;\u0026gt;/tmp/inc1.log 参数：\u0026ndash;incremental\t增量备份，后面跟要增量备份的路径 \u0026ndash;incremental-basedir=DIRECTORY\t基目录，增量备份使用，上一次（全备）增量备份所在目录\n第二次模拟新数据变化 1 2 3 db01 [cs]\u0026gt;create table t2 (id int); db01 [cs]\u0026gt;insert into t2 values(1),(2),(3); db01 [cs]\u0026gt;commit; 第二次增量备份 1 # innobackupex --user=root --password=123 --no-timestamp --incremental --incremental-basedir=/backup/inc1 /backup/inc2 \u0026amp;\u0026gt;/tmp/inc2.log 第三次模拟新数据变化 1 2 3 4 db01 [cs]\u0026gt;create table t3 (id int); db01 [cs]\u0026gt;insert into t3 values(1),(2),(3); db01 [cs]\u0026gt;commit; db01 [cs]\u0026gt;drop database cs; 4.3 备份恢复 恢复流程：\n挂出维护页，停止当天的自动备份脚本 检查备份：full+inc1+inc2+最新的完整二进制日志 进行备份整理（细节），截取关键的二进制日志（从备份——误删除之前） 测试库进行备份恢复及日志恢复 应用进行测试无误，开启业务 模拟数据库宕机，删除数据\n1 2 # pkill mysqld # rm -rf datadir=/usr/local/mysql/data/* 确认备份完整性，对比每个备份集中的checkpoints文件\n全备份的checkpoints文件内容如下，可以发现to_lsn和last_lsn中间相差9。这个数字差在5.7版本前为0，两者相等，在5.7版本后开启GTID后有了这个差值，作为内部使用。所以如果是满足这个条件，那么可以认为备份期间并没有新的数据修改。同样的，在增量备份的备份集下的文件也是如此，且增量备份from_lsn号与相邻的上一个备份的last_lsn减去9是一致的。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # cat full/xtrabackup_checkpoints backup_type = full-backuped from_lsn = 0 to_lsn = 337979814 last_lsn = 337979823 compact = 0 recover_binlog_info = 0 # cat inc1/xtrabackup_checkpoints backup_type = incremental from_lsn = 337979814 to_lsn = 337985758 last_lsn = 337985767 compact = 0 recover_binlog_info = 0 # cat inc2/xtrabackup_checkpoints backup_type = incremental from_lsn = 337985758 to_lsn = 337991702 last_lsn = 337991711 compact = 0 recover_binlog_info = 0 合并整理所有（apply-log）备份（full+inc1+inc2）到全备：\n基础全备整理 --redo-only参数表示只应用redo，不进行undo，防止LSN号发生变化，除最后一次的备份合并外都需要加此参数\n1 # innobackupex --apply-log --redo-only /data/backup/full 合并增量到全备中 合并完可以发现每个备份集中的check_points文件的last_lsn相同，说明合并成功 1 2 3 4 # 合并inc1到full中 # innobackupex --apply-log --redo-only --incremental-dir=/data/backup/inc1 /data/backup/full # 合并inc2到full中(最后一次增量) # innobackupex --apply-log --incremental-dir=/data/backup/inc2 /data/backup/full 最后一次整理全备 1 # innobackupex --apply-log /data/backup/full 数据恢复并启动数据库 1 2 3 # cp -a /backup/full/* /usr/local/mysql/data/ # chown -R mysql.mysql /usr/local/mysql/data/ # /etc/init.d/mysqld start 截取删除时刻 到drop之前的 binlog 查看最后一次增量备份中的文件内容\n1 2 3 4 5 # cat /data/backup/inc2/xtrabackup_binlog_info mysql-bin.000020 1629 9b8e7056-4d4c-11ea-a231-000c298e182d:1-19. df04d325-5946-11ea-000c298e182d:1-7 # mysqlbinlog --skip-gtids --start-position=1629 /data/binlog/mysql-bin.000020 \u0026gt;/data/backup/binlog.sql 或 # mysqlbinlog --skip-gtids --include-gtids=\u0026#39;9b8e7056-4d4c-11ea-a231-000c298e182d:1-19\u0026#39; /data/binlog/mysql-bin.000020 \u0026gt;/data/backup/binlog.sql 登录mysql，恢复最后的sql\n1 2 3 Master [(none)]\u0026gt;set sql_log_bin=0; Master [(none)]\u0026gt;source /data/backup/binlog.sql Master [(none)]\u0026gt;set sql_log_bin=1; 恢复完成。\n5、生产案例 5.1 生产场景 现有一个生产数据库，总数据量3TB，共10个业务，10个库500张表。周三上午10点，误DROP了taobao.1业务核心表20GB，导致taobao库业务无法正常运行。采用的备份策略是：周日full全备，周一到周五inc增量备份，binlog完整 针对此种场景，怎么快速恢复业务，还不影响其他业务？\n5.2 实现思路 迁移表空间\n1 2 3 create table t1; alter table taobao.t1 discard tablespace; alter table taobao.t1 import tablespace; 1、要想恢复单表，需要表结构和数据 首先合并备份到最新的备份 如何获取表结构？借助工具mysqlfrm\n1 yum install -y mysql-utilities 2、获取建表语句\n1 2 3 4 # mysqlfrm —diagnostic t2.frm create table `t2` ( `id` int(11) default null ) engine=InnoDB; 3、进入数据库中创建表\n1 2 3 create table `t2` ( `id` int(11) default null ) engine=InnoDB; 4、丢弃新建的表空间\n1 alter table t2 discard tablespace; 5、将表中的数据cp回数据库数据目录\n1 2 cp t2.ibd /data/23306/xbk/ chown mysql:mysql /data/3306/xbk/t2.ibd 6、导入表空间\n1 alter table t2 import tablespace; 7、切割二进制日志到删库前生成sql并导入\n6、备份脚本 6.1 备份用户创建 创建一个专用于备份的授权用户\n1 2 3 create user \u0026#39;back\u0026#39;@\u0026#39;localhost\u0026#39; identified by \u0026#39;123456\u0026#39;; grant reload,lock tables,replication client,create tablespace,process,super on *.* to \u0026#39;back\u0026#39;@\u0026#39;localhost\u0026#39; ; grant create,insert,select on percona_schema.* to \u0026#39;back\u0026#39;@\u0026#39;localhost\u0026#39;; 6.2 全量备份 mybak-all.sh\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/bin/bash #全量备份，只备份一次 #指定备份目录 backup_dir=\u0026#34;/bak/mysql-xback\u0026#34; #检查 [[ -d ${backup_dir} ]] || mkdir -p ${backup_dir} if [[ -d ${backup_dir}/all-backup ]];then echo \u0026#34;全备份已存在\u0026#34; exit 1 fi #命令，需要设置 innobackupex --defaults-file=/etc/my.cnf --user=back --password=\u0026#39;123456\u0026#39; --no-timestamp ${backup_dir}/all-backup \u0026amp;\u0026gt; /tmp/mysql-backup.log tail -n 1 /tmp/mysql-backup.log | grep \u0026#39;completed OK!\u0026#39; if [[ $? -eq 0 ]];then echo \u0026#34;all-backup\u0026#34; \u0026gt; /tmp/mysql-backup.txt else echo \u0026#34;备份失败\u0026#34; exit 1 fi 6.3 增量备份 mybak-section.sh\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #!/bin/bash #增量备份 #备份目录 backup_dir=\u0026#34;/bak/mysql-xback\u0026#34; #新旧备份 old_dir=`cat /tmp/mysql-backup.txt` new_dir=`date +%F-%H-%M-%S` #检查 if [[ ! -d ${backup_dir}/all-backup ]];then echo \u0026#34;还未全量备份\u0026#34; exit 1 fi #命令 /usr/bin/innobackupex --user=back --password=\u0026#39;123456\u0026#39; --no-timestamp --incremental --incremental-basedir=${backup_dir}/${old_dir} ${backup_dir}/${new_dir} \u0026amp;\u0026gt; /tmp/mysql-backup.log tail -n 1 /tmp/mysql-backup.log | grep \u0026#39;completed OK!\u0026#39; if [[ $? -eq 0 ]];then echo \u0026#34;${new_dir}\u0026#34; \u0026gt; /tmp/mysql-backup.txt else echo \u0026#34;备份失败\u0026#34; exit 1 fi 6.4 binlog备份 单点，备份binlog，要指定备份目录位置和其它变量\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 #!/bin/bash # # 注意：执行脚本前修改脚本中的变量 # 功能：cp方式增量备份 # # 适用：centos6+ # 语言：中文 # #使用：./xx.sh -uroot -p\u0026#39;123456\u0026#39;，将第一次增量备份后的binlog文件名写到/tmp/binlog-section中，若都没有，自动填写mysql-bin.000001 #过程：增量先刷新binlog日志，再查询/tmp/binlog-section中记录的上一次备份中最新的binlog日志的值 # cp中间的binlog日志，并进行压缩。再将备份中最新的binlog日志写入。 #恢复：先进行全量恢复，再根据全量备份附带的time-binlog.txt中的记录逐个恢复。当前最新的Binlog日志要去掉有问题的语句，例如drop等。 #[变量] #mysql这个命令所在绝对路径 my_sql=\u0026#34;/usr/local/mysql/bin/mysql\u0026#34; #mysqldump命令所在绝对路径 bak_sql=\u0026#34;/usr/local/mysql/bin/mysqldump\u0026#34; #binlog日志所在目录 binlog_dir=/usr/local/mysql/data #mysql-bin.index文件所在位置 binlog_index=${binlog_dir}/mysql-bin.index #备份到哪个目录 bak_dir=/bak/mysql-binback #这个脚本的日志输出到哪个文件 log_dir=/tmp/mybak-binlog.log #保存的天数，4周就是28天 save_day=10 #[自动变量] #当前年 date_nian=`date +%Y-` begin_time=`date +%F-%H-%M-%S` #所有天数的数组 save_day_zu=($(for i in `seq 1 ${save_day}`;do date -d -${i}days \u0026#34;+%F\u0026#34;;done)) #开始 /usr/bin/echo \u0026gt;\u0026gt; ${log_dir} /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) info:开始增量备份\u0026#34; \u0026gt;\u0026gt; ${log_dir} #检查 ${my_sql} $* -e \u0026#34;show databases;\u0026#34; \u0026amp;\u0026gt; /tmp/info_error.txt if [[ $? -ne 0 ]];then /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) info:登陆命令错误\u0026#34; \u0026gt;\u0026gt; ${log_dir} /usr/bin/cat /tmp/info_error.txt #如果错误则显示错误信息 exit 1 fi #移动到目录 cd ${bak_dir} bak_time=`date +%F-%H-%M` bak_timetwo=`date +%F` #刷新 ${my_sql} $* -e \u0026#34;flush logs\u0026#34; if [[ $? -ne 0 ]];then /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) error:刷新binlog失败\u0026#34; \u0026gt;\u0026gt; ${log_dir} exit 1 fi #获取开头和结尾binlog名字 last_bin=`cat /tmp/binlog-section` next_bin=`tail -n 1 ${binlog_dir}/mysql-bin.index` echo ${last_bin} |grep \u0026#39;mysql-bin\u0026#39; \u0026amp;\u0026gt; /dev/null if [[ $? -ne 0 ]];then echo \u0026#34;mysql-bin.000001\u0026#34; \u0026gt; /tmp/binlog-section #不存在则默认第一个 last_bin=`cat /tmp/binlog-section` fi #截取需要备份的binlog行数 a=`/usr/bin/sort ${binlog_dir}/mysql-bin.index | uniq | grep -n ${last_bin} | awk -F\u0026#39;:\u0026#39; \u0026#39;{print $1}\u0026#39;` b=`/usr/bin/sort ${binlog_dir}/mysql-bin.index | uniq | grep -n ${next_bin} | awk -F\u0026#39;:\u0026#39; \u0026#39;{print $1}\u0026#39;` let b-- #输出最新节点 /usr/bin/echo \u0026#34;${next_bin}\u0026#34; \u0026gt; /tmp/binlog-section #创建文件 rm -rf mybak-section-${bak_time} /usr/bin/mkdir mybak-section-${bak_time} for i in `sed -n \u0026#34;${a},${b}p\u0026#34; ${binlog_dir}/mysql-bin.index | awk -F\u0026#39;./\u0026#39; \u0026#39;{print $2}\u0026#39;` do if [[ ! -f ${binlog_dir}/${i} ]];then /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) error:binlog文件${i} 不存在\u0026#34; \u0026gt;\u0026gt; ${log_dir} exit 1 fi cp -rf ${binlog_dir}/${i} mybak-section-${bak_time}/ if [[ ! -f mybak-section-${bak_time}/${i} ]];then /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) error:binlog文件${i} 备份失败\u0026#34; \u0026gt;\u0026gt; ${log_dir} exit 1 fi done #压缩 if [[ -f mybak-section-${bak_time}.tar.gz ]];then /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) info:压缩包mybak-section-${bak_time}.tar.gz 已存在\u0026#34; \u0026gt;\u0026gt; ${log_dir} /usr/bin/rm -irf mybak-section-${bak_time}.tar.gz fi /usr/bin/tar -cf mybak-section-${bak_time}.tar.gz mybak-section-${bak_time} if [[ $? -ne 0 ]];then /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) error:压缩失败\u0026#34; \u0026gt;\u0026gt; ${log_dir} exit 1 fi #删除binlog文件夹 /usr/bin/rm -irf mybak-section-${bak_time} if [[ $? -ne 0 ]];then /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) info:删除sql文件失败\u0026#34; \u0026gt;\u0026gt; ${log_dir} exit 1 fi #整理压缩的日志文件 for i in `ls | grep \u0026#34;^mybak-section.*tar.gz$\u0026#34;` do echo $i | grep ${date_nian} \u0026amp;\u0026gt; /dev/null if [[ $? -eq 0 ]];then a=`echo ${i%%.tar.gz}` b=`echo ${a:(-16)}` #当前日志年月日 c=`echo ${b%-*}` d=`echo ${c%-*}` #看是否在数组中，不在其中，并且不是当前时间，则删除。 echo ${save_day_zu[*]} |grep -w $d \u0026amp;\u0026gt; /dev/null if [[ $? -ne 0 ]];then [[ \u0026#34;$d\u0026#34; != \u0026#34;$bak_timetwo\u0026#34; ]] \u0026amp;\u0026amp; rm -rf $i fi else #不是当月的，其他类型压缩包，跳过 continue fi done #结束 last_time=`date +%F-%H-%M-%S` /usr/bin/echo \u0026#34;begin_time:${begin_time} last_time:${last_time}\u0026#34; \u0026gt;\u0026gt; ${log_dir} /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) info:增量备份完成\u0026#34; \u0026gt;\u0026gt; ${log_dir} /usr/bin/echo \u0026gt;\u0026gt; ${log_dir} 主从，备份relay-bin，要指定备份目录位置和其它变量\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 #!/bin/bash # # 注意：执行脚本前修改脚本中的变量 # 功能：cp方式增量备份 # # 适用：centos6+ # 语言：中文 # #使用：./xx.sh -uroot -p\u0026#39;123456\u0026#39; #[变量] #mysql这个命令所在绝对路径 my_sql=\u0026#34;/usr/local/mysql/bin/mysql\u0026#34; #mysqldump命令所在绝对路径 bak_sql=\u0026#34;/usr/local/mysql/bin/mysqldump\u0026#34; #binlog日志所在目录 binlog_dir=/usr/local/mysql/data #mysql-bin.index文件所在位置 binlog_index=${binlog_dir}/mysql-bin.index #备份到哪个目录 bak_dir=/bak/mysql-binback #这个脚本的日志输出到哪个文件 log_dir=/tmp/mybak-binlog.log #保存的天数，4周就是28天 save_day=10 #[自动变量] #当前年 date_nian=`date +%Y-` begin_time=`date +%F-%H-%M-%S` #所有天数的数组 save_day_zu=($(for i in `seq 1 ${save_day}`;do date -d -${i}days \u0026#34;+%F\u0026#34;;done)) #开始 /usr/bin/echo \u0026gt;\u0026gt; ${log_dir} /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) info:开始增量备份\u0026#34; \u0026gt;\u0026gt; ${log_dir} #检查 ${my_sql} $* -e \u0026#34;show databases;\u0026#34; \u0026amp;\u0026gt; /tmp/info_error.txt if [[ $? -ne 0 ]];then /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) info:登陆命令错误\u0026#34; \u0026gt;\u0026gt; ${log_dir} /usr/bin/cat /tmp/info_error.txt #如果错误则显示错误信息 exit 1 fi #移动到目录 cd ${bak_dir} bak_time=`date +%F-%H-%M` bak_timetwo=`date +%F` #创建文件 rm -rf mybak-section-${bak_time} /usr/bin/mkdir mybak-section-${bak_time} for i in `ls ${binlog_dir}| grep relay-bin` do cp -rf ${binlog_dir}/${i} mybak-section-${bak_time}/ if [[ ! -f mybak-section-${bak_time}/${i} ]];then /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) error:binlog文件${i} 备份失败\u0026#34; \u0026gt;\u0026gt; ${log_dir} exit 1 fi done #压缩 if [[ -f mybak-section-${bak_time}.tar.gz ]];then /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) info:压缩包mybak-section-${bak_time}.tar.gz 已存在\u0026#34; \u0026gt;\u0026gt; ${log_dir} /usr/bin/rm -irf mybak-section-${bak_time}.tar.gz fi /usr/bin/tar -cf mybak-section-${bak_time}.tar.gz mybak-section-${bak_time} if [[ $? -ne 0 ]];then /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) error:压缩失败\u0026#34; \u0026gt;\u0026gt; ${log_dir} exit 1 fi #删除binlog文件夹 /usr/bin/rm -irf mybak-section-${bak_time} if [[ $? -ne 0 ]];then /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) info:删除sql文件失败\u0026#34; \u0026gt;\u0026gt; ${log_dir} exit 1 fi #整理压缩的日志文件 for i in `ls | grep \u0026#34;^mybak-section.*tar.gz$\u0026#34;` do echo $i | grep ${date_nian} \u0026amp;\u0026gt; /dev/null if [[ $? -eq 0 ]];then a=`echo ${i%%.tar.gz}` b=`echo ${a:(-16)}` #当前日志年月日 c=`echo ${b%-*}` d=`echo ${c%-*}` #看是否在数组中，不在其中，并且不是当前时间，则删除。 echo ${save_day_zu[*]} |grep -w $d \u0026amp;\u0026gt; /dev/null if [[ $? -ne 0 ]];then [[ \u0026#34;$d\u0026#34; != \u0026#34;$bak_timetwo\u0026#34; ]] \u0026amp;\u0026amp; rm -rf $i fi else #不是当月的，其他类型压缩包，跳过 continue fi done #结束 last_time=`date +%F-%H-%M-%S` /usr/bin/echo \u0026#34;begin_time:${begin_time} last_time:${last_time}\u0026#34; \u0026gt;\u0026gt; ${log_dir} /usr/bin/echo \u0026#34;time:$(date +%F-%H-%M-%S) info:增量备份完成\u0026#34; \u0026gt;\u0026gt; ${log_dir} /usr/bin/echo \u0026gt;\u0026gt; ${log_dir} ","permalink":"https://ktzxy.top/posts/ueqj0hq2yh/","summary":"MySQL备份工具之Xtrabackup","title":"MySQL备份工具之Xtrabackup "},{"content":"4.1、准备工作 ①创建Maven Module ②导入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 \u0026lt;packaging\u0026gt;war\u0026lt;/packaging\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;spring.version\u0026gt;5.3.1\u0026lt;/spring.version\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-context\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-beans\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--springmvc--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-web\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-webmvc\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-jdbc\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-aspects\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-test\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${spring.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Mybatis核心 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.7\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--mybatis和spring的整合包--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mybatis\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-spring\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.0.6\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 连接池 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;druid\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.9\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- junit测试 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;junit\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;junit\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;4.12\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;test\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- MySQL驱动 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;mysql\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mysql-connector-java\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;8.0.27\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- log4j日志 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;log4j\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;log4j\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.17\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.github.pagehelper\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;pagehelper\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.2.0\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 日志 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;ch.qos.logback\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;logback-classic\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- ServletAPI --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.servlet\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javax.servlet-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-databind\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.12.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;commons-fileupload\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;commons-fileupload\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spring5和Thymeleaf整合包 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.thymeleaf\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;thymeleaf-spring5\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.0.12.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; ③创建表 1 2 3 4 5 6 7 8 CREATE TABLE `t_emp` ( `emp_id` int(11) NOT NULL AUTO_INCREMENT, `emp_name` varchar(20) DEFAULT NULL, `age` int(11) DEFAULT NULL, `sex` char(1) DEFAULT NULL, `email` varchar(50) DEFAULT NULL, PRIMARY KEY (`emp_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 4.2、配置web.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;web-app xmlns=\u0026#34;http://xmlns.jcp.org/xml/ns/javaee\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xsi:schemaLocation=\u0026#34;http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd\u0026#34; version=\u0026#34;4.0\u0026#34;\u0026gt; \u0026lt;!-- 配置Spring的编码过滤器 --\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;filter-name\u0026gt;CharacterEncodingFilter\u0026lt;/filter-name\u0026gt; \u0026lt;filter-class\u0026gt;org.springframework.web.filter.CharacterEncodingFilter\u0026lt;/filter-class\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;encoding\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;utf-8\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;forceEncoding\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;true\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;filter-mapping\u0026gt; \u0026lt;filter-name\u0026gt;CharacterEncodingFilter\u0026lt;/filter-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/filter-mapping\u0026gt; \u0026lt;!-- 配置处理请求方式PUT和DELETE的过滤器 --\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;filter-name\u0026gt;HiddenHttpMethodFilter\u0026lt;/filter-name\u0026gt; \u0026lt;filter-class\u0026gt;org.springframework.web.filter.HiddenHttpMethodFilter\u0026lt;/filter-class\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;filter-mapping\u0026gt; \u0026lt;filter-name\u0026gt;HiddenHttpMethodFilter\u0026lt;/filter-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/filter-mapping\u0026gt; \u0026lt;!-- 配置SpringMVC的前端控制器 --\u0026gt; \u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;DispatcherServlet\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;org.springframework.web.servlet.DispatcherServlet\u0026lt;/servlet-class\u0026gt; \u0026lt;!-- 设置SpringMVC的配置文件的位置和名称 --\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;contextConfigLocation\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;classpath:SpringMVC.xml\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;!-- 将DispatcherServlet的初始化时间提前到服务器启动时 --\u0026gt; \u0026lt;load-on-startup\u0026gt;1\u0026lt;/load-on-startup\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;DispatcherServlet\u0026lt;/servlet-name\u0026gt; \u0026lt;url-pattern\u0026gt;/\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; \u0026lt;!-- 设置Spring的配置文件的位置和名称 --\u0026gt; \u0026lt;context-param\u0026gt; \u0026lt;param-name\u0026gt;contextConfigLocation\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;classpath:Spring.xml\u0026lt;/param-value\u0026gt; \u0026lt;/context-param\u0026gt; \u0026lt;!-- 配置Spring的监听器 --\u0026gt; \u0026lt;listener\u0026gt; \u0026lt;listener-class\u0026gt;org.springframework.web.context.ContextLoaderListener\u0026lt;/listener-class\u0026gt; \u0026lt;/listener\u0026gt; \u0026lt;/web-app\u0026gt; 4.3、创建SpringMVC的配置文件并配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:context=\u0026#34;http://www.springframework.org/schema/context\u0026#34; xmlns:mvc=\u0026#34;http://www.springframework.org/schema/mvc\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd\u0026#34;\u0026gt; \u0026lt;!--扫描组件--\u0026gt; \u0026lt;context:component-scan base-package=\u0026#34;com.hbnu.ssm.controller\u0026#34;\u0026gt;\u0026lt;/context:component-scan\u0026gt; \u0026lt;!--配置视图解析器--\u0026gt; \u0026lt;bean id=\u0026#34;viewResolver\u0026#34; class=\u0026#34;org.thymeleaf.spring5.view.ThymeleafViewResolver\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;order\u0026#34; value=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;characterEncoding\u0026#34; value=\u0026#34;UTF-8\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;templateEngine\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.thymeleaf.spring5.SpringTemplateEngine\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;templateResolver\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver\u0026#34;\u0026gt; \u0026lt;!-- 视图前缀 --\u0026gt; \u0026lt;property name=\u0026#34;prefix\u0026#34; value=\u0026#34;/WEB-INF/templates/\u0026#34;/\u0026gt; \u0026lt;!-- 视图后缀 --\u0026gt; \u0026lt;property name=\u0026#34;suffix\u0026#34; value=\u0026#34;.html\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;templateMode\u0026#34; value=\u0026#34;HTML5\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;characterEncoding\u0026#34; value=\u0026#34;UTF-8\u0026#34; /\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- 配置访问首页的视图控制 --\u0026gt; \u0026lt;mvc:view-controller path=\u0026#34;/\u0026#34; view-name=\u0026#34;index\u0026#34;\u0026gt;\u0026lt;/mvc:view-controller\u0026gt; \u0026lt;!-- 配置默认的servlet处理静态资源 --\u0026gt; \u0026lt;mvc:default-servlet-handler /\u0026gt; \u0026lt;!-- 开启MVC的注解驱动 --\u0026gt; \u0026lt;mvc:annotation-driven /\u0026gt; \u0026lt;!--配置文件上传解析器--\u0026gt; \u0026lt;bean id=\u0026#34;multipartResolver\u0026#34; class=\u0026#34;org.springframework.web.multipart.commons.CommonsMultipartResolver\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; \u0026lt;/beans\u0026gt; 4.4、搭建MyBatis环境 ①创建属性文件jdbc.properties 1 2 3 4 jdbc.user=root jdbc.password=123456 jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC jdbc.driver=com.mysql.cj.jdbc.Driver ②创建MyBatis的核心配置文件mybatis-config.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;utf-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE configuration PUBLIC \u0026#34;-//mybatis.org//DTD Config 3.0//EN\u0026#34; \u0026#34;https://mybatis.org/dtd/mybatis-3-config.dtd\u0026#34;\u0026gt; \u0026lt;configuration\u0026gt; \u0026lt;!-- MyBatis核心配置文件中的标签必须要按照指定的顺序配置： properties?,settings?,typeAliases?,typeHandlers?, objectFactory?,objectWrapperFactory?,reflectorFactory?, plugins?,environments?,databaseIdProvider?,mappers? --\u0026gt; \u0026lt;settings\u0026gt; \u0026lt;!--将下划线映射为驼峰--\u0026gt; \u0026lt;setting name=\u0026#34;mapUnderscoreToCamelCase\u0026#34; value=\u0026#34;true\u0026#34;/\u0026gt; \u0026lt;/settings\u0026gt; \u0026lt;plugins\u0026gt; \u0026lt;!--配置分页插件--\u0026gt; \u0026lt;plugin interceptor=\u0026#34;com.github.pagehelper.PageInterceptor\u0026#34;\u0026gt;\u0026lt;/plugin\u0026gt; \u0026lt;/plugins\u0026gt; \u0026lt;/configuration\u0026gt; ③创建Mapper接口和映射文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package com.hbnu.ssm.mapper; import com.hbnu.ssm.pojo.Employee; import java.util.List; /** * @Auther: 赵羽 * @Date: 2023/4/23 - 04 - 23 - 22:07 * @Description: com.hbnu.ssm.mapper * @version: 1.0 */ public interface EmployeeMapper { List\u0026lt;Employee\u0026gt; getAllEmployees(); } 1 2 3 4 5 6 7 8 9 10 11 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE mapper PUBLIC \u0026#34;-//mybatis.org//DTD Mapper 3.0//EN\u0026#34; \u0026#34;https://mybatis.org/dtd/mybatis-3-mapper.dtd\u0026#34;\u0026gt; \u0026lt;mapper namespace=\u0026#34;com.hbnu.ssm.mapper.EmployeeMapper\u0026#34;\u0026gt; \u0026lt;!--List\u0026lt;Employee\u0026gt; getAllEmployees();--\u0026gt; \u0026lt;select id=\u0026#34;getAllEmployees\u0026#34; resultType=\u0026#34;Employee\u0026#34;\u0026gt; select * from t_emp \u0026lt;/select\u0026gt; \u0026lt;/mapper\u0026gt; ④创建日志文件log4j.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34; ?\u0026gt; \u0026lt;!DOCTYPE log4j:configuration SYSTEM \u0026#34;log4j.dtd\u0026#34;\u0026gt; \u0026lt;log4j:configuration xmlns:log4j=\u0026#34;http://jakarta.apache.org/log4j/\u0026#34;\u0026gt; \u0026lt;appender name=\u0026#34;STDOUT\u0026#34; class=\u0026#34;org.apache.log4j.ConsoleAppender\u0026#34;\u0026gt; \u0026lt;param name=\u0026#34;Encoding\u0026#34; value=\u0026#34;UTF-8\u0026#34; /\u0026gt; \u0026lt;layout class=\u0026#34;org.apache.log4j.PatternLayout\u0026#34;\u0026gt; \u0026lt;param name=\u0026#34;ConversionPattern\u0026#34; value=\u0026#34;%-5p %d{MM-dd HH:mm:ss,SSS} %m (%F:%L) \\n\u0026#34; /\u0026gt; \u0026lt;/layout\u0026gt; \u0026lt;/appender\u0026gt; \u0026lt;logger name=\u0026#34;java.sql\u0026#34;\u0026gt; \u0026lt;level value=\u0026#34;debug\u0026#34; /\u0026gt; \u0026lt;/logger\u0026gt; \u0026lt;logger name=\u0026#34;org.apache.ibatis\u0026#34;\u0026gt; \u0026lt;level value=\u0026#34;info\u0026#34; /\u0026gt; \u0026lt;/logger\u0026gt; \u0026lt;root\u0026gt; \u0026lt;level value=\u0026#34;debug\u0026#34; /\u0026gt; \u0026lt;appender-ref ref=\u0026#34;STDOUT\u0026#34; /\u0026gt; \u0026lt;/root\u0026gt; \u0026lt;/log4j:configuration\u0026gt; ⑤框架结构 4.5、创建Spring的配置文件并配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 \u0026lt;?xml version=\u0026#34;1.0\u0026#34; encoding=\u0026#34;UTF-8\u0026#34;?\u0026gt; \u0026lt;beans xmlns=\u0026#34;http://www.springframework.org/schema/beans\u0026#34; xmlns:xsi=\u0026#34;http://www.w3.org/2001/XMLSchema-instance\u0026#34; xmlns:context=\u0026#34;http://www.springframework.org/schema/context\u0026#34; xmlns:tx=\u0026#34;http://www.springframework.org/schema/tx\u0026#34; xsi:schemaLocation=\u0026#34;http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd\u0026#34;\u0026gt; \u0026lt;!--扫描组件（除控制层）--\u0026gt; \u0026lt;context:component-scan base-package=\u0026#34;com.hbnu.ssm\u0026#34;\u0026gt; \u0026lt;context:exclude-filter type=\u0026#34;annotation\u0026#34; expression=\u0026#34;org.springframework.stereotype.Controller\u0026#34;/\u0026gt; \u0026lt;/context:component-scan\u0026gt; \u0026lt;!-- 引入jdbc.properties --\u0026gt; \u0026lt;context:property-placeholder location=\u0026#34;classpath:jdbc.properties\u0026#34;\u0026gt;\u0026lt;/context:property-placeholder\u0026gt; \u0026lt;!--配置数据源--\u0026gt; \u0026lt;bean id=\u0026#34;dataSource\u0026#34; class=\u0026#34;com.alibaba.druid.pool.DruidDataSource\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;driverClassName\u0026#34; value=\u0026#34;${jdbc.driver}\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;url\u0026#34; value=\u0026#34;${jdbc.url}\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;username\u0026#34; value=\u0026#34;${jdbc.username}\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;property name=\u0026#34;password\u0026#34; value=\u0026#34;${jdbc.password}\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!--配置事务管理器--\u0026gt; \u0026lt;bean id=\u0026#34;transactionManager\u0026#34; class=\u0026#34;org.springframework.jdbc.datasource.DataSourceTransactionManager\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- 开启事务注解驱动 将使用注解@Transactional 标识的方法或类中所有的方法进行事务管理 --\u0026gt; \u0026lt;tx:annotation-driven transaction-manager=\u0026#34;transactionManager\u0026#34;\u0026gt;\u0026lt;/tx:annotation-driven\u0026gt; \u0026lt;!-- 配置用于创建SqlSessionFactory的工厂bean,可以直接在Spring的IOC中获取SqlSessionFactory--\u0026gt; \u0026lt;bean class=\u0026#34;org.mybatis.spring.SqlSessionFactoryBean\u0026#34;\u0026gt; \u0026lt;!-- 设置MyBatis配置文件的路径（可以不设置） --\u0026gt; \u0026lt;property name=\u0026#34;configLocation\u0026#34; value=\u0026#34;classpath:mybatis-config.xml\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;!-- 设置数据源 --\u0026gt; \u0026lt;property name=\u0026#34;dataSource\u0026#34; ref=\u0026#34;dataSource\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;!-- 设置类型别名所对应的包 --\u0026gt; \u0026lt;property name=\u0026#34;typeAliasesPackage\u0026#34; value=\u0026#34;com.hbnu.ssm.pojo\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;!-- 设置映射文件的路径 若映射文件所在路径和mapper接口所在路径一致，则不需要设置 --\u0026gt; \u0026lt;!-- \u0026lt;property name=\u0026#34;mapperLocations\u0026#34; value=\u0026#34;classpath:mappers/*.xml\u0026#34;\u0026gt; \u0026lt;/property\u0026gt; --\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- 配置mapper接口的扫描配置 由mybatis-spring提供，可以将指定包下所有的mapper接口通过SqlSession创建动态代理实现类对象 并将这些对象交给IOC容器的bean管理 --\u0026gt; \u0026lt;bean class=\u0026#34;org.mybatis.spring.mapper.MapperScannerConfigurer\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;basePackage\u0026#34; value=\u0026#34;com.hbnu.ssm.mapper\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/beans\u0026gt; 4.6、测试功能 ①创建组件 实体类Employee\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 package com.hbnu.ssm.pojo; /** * @Auther: 赵羽 * @Date: 2023/4/23 - 04 - 23 - 22:14 * @Description: com.hbnu.ssm.pojo * @version: 1.0 */ public class Employee { private Integer empId; private String empName; private Integer age; private String gender; private String email; public Employee() { } public Employee(Integer empId, String empName, Integer age, String gender, String email) { this.empId = empId; this.empName = empName; this.age = age; this.gender = gender; this.email = email; } public Integer getEmpId() { return empId; } public void setEmpId(Integer empId) { this.empId = empId; } public String getEmpName() { return empName; } public void setEmpName(String empName) { this.empName = empName; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getGender() { return gender; } public void setGender(String gender) { this.gender = gender; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } @Override public String toString() { return \u0026#34;Employee{\u0026#34; + \u0026#34;empId=\u0026#34; + empId + \u0026#34;, empName=\u0026#39;\u0026#34; + empName + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, age=\u0026#34; + age + \u0026#34;, gender=\u0026#39;\u0026#34; + gender + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#34;, email=\u0026#39;\u0026#34; + email + \u0026#39;\\\u0026#39;\u0026#39; + \u0026#39;}\u0026#39;; } } 创建控制层组件EmployeeController\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 package com.hbnu.ssm.controller; import com.github.pagehelper.PageInfo; import com.hbnu.ssm.pojo.Employee; import com.hbnu.ssm.service.EmployeeService; import com.sun.org.glassfish.gmbal.ParameterNames; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import java.util.List; /** * @Auther: 赵羽 * @Date: 2023/4/23 - 04 - 23 - 21:29 * @Description: com.hbnu.ssm.controller * @version: 1.0 * * 查询所有的员工信息--\u0026gt;/employee--\u0026gt;get * 查询员工的分页信息--\u0026gt;/employee/page/1--\u0026gt;get * 根据id查询员工信息--\u0026gt;/employee/1--\u0026gt;get * 跳转到添加页面--\u0026gt;/to/add--\u0026gt;get * 添加员工信息--\u0026gt;/employee--\u0026gt;post * 修改员工信息--\u0026gt;/employee--\u0026gt;put * 删除员工信息--\u0026gt;/employee/1--\u0026gt;delete * */ @Controller public class EmployeeController { @Autowired private EmployeeService employeeService; @RequestMapping(value = \u0026#34;/employee/page/{pageNum}\u0026#34;,method = RequestMethod.GET) public String getEmployeePage(@PathVariable(\u0026#34;pageNum\u0026#34;) Integer pageNum,Model model) { //获取员工的分页信息 PageInfo\u0026lt;Employee\u0026gt; page = employeeService.getEmployeePage(pageNum); //将分页数据共享到请求域中 model.addAttribute(\u0026#34;page\u0026#34;, page); //跳转到employee_list.html return \u0026#34;employee_list.html\u0026#34;; } @RequestMapping(value = \u0026#34;/employee\u0026#34;,method = RequestMethod.GET) public String getAllEmployees(Model model) { //查询所有的员工信息 List\u0026lt;Employee\u0026gt; list = employeeService.getAllEmployees(); //将员工信息在请求域中共享 model.addAttribute(\u0026#34;list\u0026#34;,list); //跳转到employee_list.html return \u0026#34;employee_list.html\u0026#34;; } } 创建接口EmployeeService\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.hbnu.ssm.service; import com.github.pagehelper.PageInfo; import com.hbnu.ssm.pojo.Employee; import java.util.List; /** * @Auther: 赵羽 * @Date: 2023/4/23 - 04 - 23 - 21:45 * @Description: com.hbnu.ssm.service * @version: 1.0 */ public interface EmployeeService { /** * 查询所有的员工信息 * @return */ List\u0026lt;Employee\u0026gt; getAllEmployees(); /** * 获取员工的分页信息 * @param pageNum * @return */ PageInfo\u0026lt;Employee\u0026gt; getEmployeePage(Integer pageNum); } 创建实现类EmployeeServiceImpl\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.hbnu.ssm.service.impl; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.hbnu.ssm.mapper.EmployeeMapper; import com.hbnu.ssm.pojo.Employee; import com.hbnu.ssm.service.EmployeeService; import org.apache.ibatis.session.SqlSessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; /** * @Auther: 赵羽 * @Date: 2023/4/23 - 04 - 23 - 21:45 * @Description: com.hbnu.ssm.service.impl * @version: 1.0 */ @Service @Transactional public class EmployeeServiceImpl implements EmployeeService { @Autowired private EmployeeMapper employeeMapper; @Override public List\u0026lt;Employee\u0026gt; getAllEmployees() { return employeeMapper.getAllEmployees(); } @Override public PageInfo\u0026lt;Employee\u0026gt; getEmployeePage(Integer pageNum) { //开启分页功能 PageHelper.startPage(pageNum,4); //查询所有的员工信息 List\u0026lt;Employee\u0026gt; list = employeeMapper.getAllEmployees(); //获取分页相关数据 PageInfo\u0026lt;Employee\u0026gt; page = new PageInfo\u0026lt;Employee\u0026gt;(list,5); return page; } } ②创建页面 employee_list.html页面\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34; xmlns:th=\u0026#34;http://www.thymeleaf.org\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;员工列表\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; th:href=\u0026#34;@{/static/css/index_work.css}\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;table\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th colspan=\u0026#34;6\u0026#34;\u0026gt;员工列表\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;流水号\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;员工姓名\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;年龄\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;性别\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;邮箱\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;操作\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr th:each=\u0026#34;employee,status : ${page.list}\u0026#34; \u0026gt; \u0026lt;td th:text=\u0026#34;${status.count}\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${employee.empName}\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${employee.age}\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${employee.gender}\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${employee.email}\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;a href=\u0026#34;\u0026#34;\u0026gt;删除\u0026lt;/a\u0026gt; \u0026lt;a href=\u0026#34;\u0026#34;\u0026gt;修改\u0026lt;/a\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;div style=\u0026#34;text-align: center;\u0026#34;\u0026gt; \u0026lt;a th:if=\u0026#34;${page.hasPreviousPage}\u0026#34; th:href=\u0026#34;@{/employee/page/1}\u0026#34;\u0026gt;首页\u0026lt;/a\u0026gt; \u0026lt;a th:if=\u0026#34;${page.hasPreviousPage}\u0026#34; th:href=\u0026#34;@{\u0026#39;/employee/page/\u0026#39;+${page.prePage}}\u0026#34;\u0026gt;上一页\u0026lt;/a\u0026gt; \u0026lt;span th:each=\u0026#34;num : ${page.navigatepageNums}\u0026#34;\u0026gt; \u0026lt;a th:if=\u0026#34;${page.pageNum == num}\u0026#34; style=\u0026#34;color: red;\u0026#34; th:href=\u0026#34;@{\u0026#39;/employee/page/\u0026#39;+${num}}\u0026#34; th:text=\u0026#34;\u0026#39;[\u0026#39;+${num}+\u0026#39;]\u0026#39;\u0026#34;\u0026gt;\u0026lt;/a\u0026gt; \u0026lt;a th:if=\u0026#34;${page.pageNum != num}\u0026#34; th:href=\u0026#34;@{\u0026#39;/employee/page/\u0026#39;+${num}}\u0026#34; th:text=\u0026#34;${num}\u0026#34;\u0026gt;\u0026lt;/a\u0026gt; \u0026lt;/span\u0026gt; \u0026lt;a th:if=\u0026#34;${page.hasNextPage}\u0026#34; th:href=\u0026#34;@{\u0026#39;/employee/page/\u0026#39;+${page.nextPage}}\u0026#34;\u0026gt;下一页\u0026lt;/a\u0026gt; \u0026lt;a th:if=\u0026#34;${page.hasNextPage}\u0026#34; th:href=\u0026#34;@{\u0026#39;/employee/page/\u0026#39;+${page.pages}}\u0026#34;\u0026gt;末页\u0026lt;/a\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; index.html页面\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34; xmlns:th=\u0026#34;http://www.thymeleaf.org\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;首页\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;index.html\u0026lt;/h1\u0026gt; \u0026lt;a th:href=\u0026#34;@{employee/page/1}\u0026#34;\u0026gt;查询所有员工的分页信息\u0026lt;/a\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; ③访问测试分页功能 localhost:8080/employee/page/1\n","permalink":"https://ktzxy.top/posts/2vbb07ezlj/","summary":"ssm框架整合","title":"ssm整合"},{"content":" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #!/bin/bash # 切换到/opt目录 cd /opt # 安装Java Development Kit (JDK) 8 rpm -ivh jdk-8u371-linux-x64.rpm # 向环境变量配置文件中添加Java环境变量 echo \u0026#34;export JAVA_HOME=/usr/java/jdk1.8.0-x64 export CLASSPATH=.:$JAVA_HOME/lib/tools.jar:$JAVA_HOME/lib/dt.jar export PATH=$JAVA_HOME/bin:$PATH\u0026#34; \u0026gt;\u0026gt;/etc/profile.d/java.sh # 使环境变量配置立即生效 sourse /etc/profile.d/java.sh # 切换到/opt目录，解压Apache Tomcat 9 cd /opt tar zxvf apache-tomcat-9.0.78.tar.gz # 将解压的Tomcat移动到/usr/local/tomcat下 mv -f apache-tomcat-9.0.78 /usr/local/tomcat # 启动Tomcat /usr/local/tomcat/bin/startup.sh # 检查Tomcat启动是否成功 if [ $? -eq 0 ]; then echo \u0026#34;tomcat启动成功\u0026#34; else echo \u0026#34;tomcat启动失败\u0026#34; fi # 在Tomcat的webapps目录下创建kgc和benet应用目录 mkdir /usr/local/tomcat/webapps/kgc mkdir /usr/local/tomcat/webapps/benet # 向kgc和benet应用目录下的index.jsp文件写入内容 echo \u0026#34;This is kgc page\\!\u0026#34; \u0026gt;/usr/local/tomcat/webapps/kgc/index.jsp echo \u0026#34;This is benet page\\!\u0026#34; \u0026gt;/usr/local/tomcat/webapps/benet/index.jsp # 更新Tomcat的server.xml配置文件，以添加对kgc和benet域名的配置 # 创建临时文件来存储更新后的配置 temp_file=$(mktemp) # 使用sed命令将配置插入到临时文件中 sed \u0026#34;160a\\\\t\u0026lt;Host name=\u0026#34;www.kgc.com\u0026#34; appBase=\u0026#34;webapps\u0026#34; unpackWARs=\u0026#34;true\u0026#34; autoDeploy=\u0026#34;true\u0026#34; xmlValidation=\u0026#34;false\u0026#34; xmlNamespaceAware=\u0026#34;false\u0026#34;\u0026gt;\\n\\t\u0026lt;Context docBase=\u0026#34;/usr/local/tomcat/webapps/kgc\u0026#34; path=\u0026#34;\u0026#34; reloadable=\u0026#34;true\u0026#34;\\n\\t/\u0026gt;\\n\\t\u0026lt;/Host\u0026gt;\\n\\t\u0026lt;Host name=\u0026#34;www.benet.com\u0026#34; appBase=\u0026#34;webapps\u0026#34; unpackWARs=\u0026#34;true\u0026#34; autoDeploy=\u0026#34;true\u0026#34; xmlValidation=\u0026#34;false\u0026#34; xmlNamespaceAware=\u0026#34;false\u0026#34;\u0026gt;\\n\u0026lt;Context docBase=\u0026#34;/usr/local/tomcat/webapps/benet\u0026#34; path=\u0026#34;\u0026#34; reloadable=\u0026#34;true\u0026#34;\\n/\u0026gt;\\n\u0026lt;/Host\u0026gt;\u0026#34; /usr/local/tomcat/conf/server.xml \u0026gt;\u0026#34;$temp_file\u0026#34; \u0026amp;\u0026amp; # 检查是否成功写入临时文件 if [ $? -eq 0 ]; then # 在替换原始配置文件之前创建一个备份 cp /usr/local/tomcat/conf/server.xml /usr/local/tomcat/conf/server.xml.bak # 替换原始配置文件 mv -f \u0026#34;$temp_file\u0026#34; /usr/local/tomcat/conf/server.xml \u0026amp;\u0026amp; echo \u0026#34;Configuration updated successfully.\u0026#34; else # 如果写入临时文件失败，记录错误消息并清理临时文件 echo \u0026#34;Failed to update configuration.\u0026#34; rm \u0026#34;$temp_file\u0026#34; fi # 重写kgc和benet应用目录下的index.jsp文件内容，确保内容是最新的 echo \u0026#34;This is kgc page\\!\u0026#34; \u0026gt;/usr/local/tomcat/webapps/kgc/index.jsp echo \u0026#34;This is benet page\\!\u0026#34; \u0026gt;/usr/local/tomcat/webapps/benet/index.jsp # 获取本地eth0网卡的IP地址，并将其与kgc和benet域名一起添加到hosts文件中 local_ip=$(ip addr show eth0 | grep \u0026#34;inet \u0026#34; | awk \u0026#39;{print $2}\u0026#39; | cut -d \u0026#39;/\u0026#39; -f 1) echo \u0026#34;$local_ip www.kgc.com www.benet.com\u0026#34; \u0026gt;\u0026gt; /etc/hosts # 停止并重新启动Tomcat，以确保配置和内容更新生效 /usr/local/tomcat/bin/shutdown.sh /usr/local/tomcat/bin/startup.sh # 验证kgc和benet应用是否可以通过域名访问 curl www.kgc.com:8080/kgc/index.jsp curl www.benet.com:8080/benet/index.jsp ","permalink":"https://ktzxy.top/posts/frk61ebu2f/","summary":"tomcat部署和安装","title":"tomcat部署和安装"},{"content":"﻿# Day-11-网络编程\n引入 【1】网络编程: 把分布在不同地理区域的计算机与专门的外部设备用通信线路互连成一个规模大、功能强的网络系统，从而使众多的计算机可以方便地互相传递信息、共享硬件、软件、数据信息等资源。\n设备之间在网络中进行数据的传输，发送/接收数据。\n【2】通信的两个重要要素：IP+PORT\n【3】设备之间进行传输的时候，必须遵照一定的规则\u0026ndash;》通信协议\nlnetAddress 、lnetSocketAddress 【1】 lnetAddress \u0026mdash;》封装了IP\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.zy.test1; import java.net.InetAddress; import java.net.UnknownHostException; /** * @Auther: 赵羽 * @Description: com.zy.test1 * @version: 1.0 */ public class Test1 { public static void main(String[] args) throws UnknownHostException { //封装ip //InetAddress is = new InetAddress(); 不能直接创建对象，因为工netAddress()被default修饰了。 InetAddress ia2 = InetAddress.getByName(\u0026#34;localhost\u0026#34;); System.out.println(ia2); InetAddress ia3 = InetAddress.getByName(\u0026#34;www.baidu.com\u0026#34;); //封装域名 System.out.println(ia3); System.out.println(ia3.getHostName()); //获取域名 System.out.println(ia3.getHostAddress()); //获取ip地址 } } 【2】 lnetSocketAddress \u0026mdash;》封装了IP，端口号\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.zy.test1; import java.net.InetAddress; import java.net.InetSocketAddress; /** * @Auther: 赵羽 * @Description: com.zy.test1 * @version: 1.0 */ public class Test2 { public static void main(String[] args) { InetSocketAddress isa = new InetSocketAddress(\u0026#34;localhost\u0026#34;, 8080); System.out.println(isa); System.out.println(isa.getHostName()); System.out.println(isa.getPort()); InetAddress ia = isa.getAddress(); System.out.println(ia.getHostName()); System.out.println(ia.getHostAddress()); } } 套接字 基于TCP的网络编程 功能:模拟网站的登录，客户端录入账号密码，然后服务器端进行验证。\n单向通信：\n客户端：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.zy.test2; import java.io.DataOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.Socket; /** * @Auther: 赵羽 * @Description: com.zy.test2 * @version: 1.0 */ public class TestClient { public static void main(String[] args) throws IOException { //1。创建套接字:指定服务器的ip和端口号: Socket s = new Socket(\u0026#34;localhost\u0026#34;, 8585); //向外发送数据 利用传输流 OutputStream os = s.getOutputStream(); DataOutputStream dos = new DataOutputStream(os); //利用这个outputStream就可以向外发送数据了，但是没有直接发送string的方法 //所以我们又在outputStream外面套了一个处理流: DataoutputStream dos.writeUTF(\u0026#34;你好\u0026#34;); //3.关闭流 + 关闭网络资源 dos.close(); os.close(); s.close(); } } 服务端：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package com.zy.test2; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; /** * @Auther: 赵羽 * @Description: com.zy.test2 * @version: 1.0 */ public class TestServer { public static void main(String[] args) throws IOException { //1.创建套接字：指定服务器的端口号 ServerSocket ss = new ServerSocket(8585); //2.等着客户端发来的消息 Socket s = ss.accept();//阻塞方法:等待接收客户端的数据，什么时候接收到数据，什么时候程序继续向下执行。 //accept()返回值为一个Socket，这个Socket其实就是客户端的Socket //接到这个Socket以后，客户端和服务器才真正产生了连接，才真正可以通信了 //3.感受到的操作流： InputStream is = s.getInputStream(); DataInputStream dis = new DataInputStream(is); //4.读取客户端发来的数据： String str = dis.readUTF(); System.out.println(\u0026#34;客户端发来的数据为\u0026#34;+str); //5.关闭流+关闭网络资源： dis.close(); is.close(); s.close(); ss.close(); } } 测试: 先开服务器，再开启客户端\n双向通信：\n服务端：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package com.zy.test2; import java.io.*; import java.net.ServerSocket; import java.net.Socket; /** * @Auther: 赵羽 * @Description: com.zy.test2 * @version: 1.0 */ public class TestServer { public static void main(String[] args) throws IOException { //1.创建套接字：指定服务器的端口号 ServerSocket ss = new ServerSocket(8585); //2.等着客户端发来的消息 Socket s = ss.accept();//阻塞方法:等待接收客户端的数据，什么时候接收到数据，什么时候程序继续向下执行。 //accept()返回值为一个Socket，这个Socket其实就是客户端的Socket //接到这个Socket以后，客户端和服务器才真正产生了连接，才真正可以通信了 //3.感受到的操作流： InputStream is = s.getInputStream(); DataInputStream dis = new DataInputStream(is); //4.读取客户端发来的数据： String str = dis.readUTF(); System.out.println(\u0026#34;客户端发来的数据为\u0026#34;+str); //向客户端输出一句话 操作流---》输出流 OutputStream os = s.getOutputStream(); DataOutputStream dos = new DataOutputStream(os); dos.writeUTF(\u0026#34;你好，我是服务器端，我就收到你的消息了\u0026#34;); //5.关闭流+关闭网络资源： dos.close(); os.close(); dis.close(); is.close(); s.close(); ss.close(); } } 客户端：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.zy.test2; import java.io.*; import java.net.Socket; /** * @Auther: 赵羽 * @Description: com.zy.test2 * @version: 1.0 */ public class TestClient { public static void main(String[] args) throws IOException { //1。创建套接字:指定服务器的ip和端口号: Socket s = new Socket(\u0026#34;localhost\u0026#34;, 8585); //向外发送数据 利用传输流 OutputStream os = s.getOutputStream(); DataOutputStream dos = new DataOutputStream(os); //利用这个outputStream就可以向外发送数据了，但是没有直接发送string的方法 //所以我们又在outputStream外面套了一个处理流: DataoutputStream dos.writeUTF(\u0026#34;你好\u0026#34;); //接收服务器端的回话--》利用输入流: InputStream is = s.getInputStream(); DataInputStream dis = new DataInputStream(is); String str = dis.readUTF(); System.out.println(\u0026#34;服务器端对我说\u0026#34;+str); //3.关闭流 + 关闭网络资源 dis.close(); is.close(); dos.close(); os.close(); s.close(); } } 综合：\n封装的user类：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.zy.test3; import java.io.Serializable; /** * @Auther: 赵羽 * @Description: com.zy.test3 * @version: 1.0 */ public class User implements Serializable { private static final long serialVersionUID = 949071293973714766L; private String name; private String pwd; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPwd() { return pwd; } public User() { } public void setPwd(String pwd) { this.pwd = pwd; } public User(String name, String pwd) { this.name = name; this.pwd = pwd; } } 服务端：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.zy.test3; import java.io.*; import java.net.ServerSocket; import java.net.Socket; /** * @Auther: 赵羽 * @Description: com.zy.test2 * @version: 1.0 */ public class TestServer { public static void main(String[] args) throws IOException, ClassNotFoundException { //1.创建套接字：指定服务器的端口号 ServerSocket ss = new ServerSocket(8585); //2.等着客户端发来的消息 Socket s = ss.accept();//阻塞方法:等待接收客户端的数据，什么时候接收到数据，什么时候程序继续向下执行。 //accept()返回值为一个Socket，这个Socket其实就是客户端的Socket //接到这个Socket以后，客户端和服务器才真正产生了连接，才真正可以通信了 //3.感受到的操作流： InputStream is = s.getInputStream(); ObjectInputStream ois = new ObjectInputStream(is); //4.读取客户端发来的数据： User user = (User)(ois.readObject()); //对对象进行验证： boolean flag = false; if (user.getName().equals(\u0026#34;admin\u0026#34;)\u0026amp;\u0026amp;user.getPwd().equals(\u0026#34;123456\u0026#34;)){ flag = true; } //向客户端输出一句话 操作流---》输出流 OutputStream os = s.getOutputStream(); DataOutputStream dos = new DataOutputStream(os); dos.writeBoolean(flag); //5.关闭流+关闭网络资源： dos.close(); os.close(); ois.close(); is.close(); s.close(); ss.close(); } } 客户端：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.zy.test3; import java.io.*; import java.net.Socket; import java.util.Scanner; /** * @Auther: 赵羽 * @Description: com.zy.test2 * @version: 1.0 */ public class TestClient { public static void main(String[] args) throws IOException { //1。创建套接字:指定服务器的ip和端口号: Socket s = new Socket(\u0026#34;localhost\u0026#34;, 8585); //录入用户的账号和密码： Scanner sc = new Scanner(System.in); System.out.println(\u0026#34;请录入你的账号：\u0026#34;); String name = sc.next(); System.out.println(\u0026#34;请录入你的密码：\u0026#34;); String pwd = sc.next(); //将账号和密码封装为一个User的对象： User user = new User(name, pwd); //向外发送数据 利用传输流 OutputStream os = s.getOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(os); oos.writeObject(user); //接收服务器端的回话--》利用输入流: InputStream is = s.getInputStream(); DataInputStream dis = new DataInputStream(is); boolean b = dis.readBoolean(); if (b){ System.out.println(\u0026#34;恭喜，登录成功\u0026#34;); }else { System.out.println(\u0026#34;对不起，登录失败\u0026#34;); } //3.关闭流 + 关闭网络资源 dis.close(); is.close(); oos.close(); os.close(); s.close(); } } 异常处理：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 package com.zy.test3; import java.io.*; import java.net.ServerSocket; import java.net.Socket; /** * @Auther: 赵羽 * @Description: com.zy.test2 * @version: 1.0 */ public class TestServer { public static void main(String[] args) { //1.创建套接字：指定服务器的端口号 ServerSocket ss = null; DataOutputStream dos = null; OutputStream os = null; Socket s = null; InputStream is = null; ObjectInputStream ois = null; try { ss = new ServerSocket(8585); //2.等着客户端发来的消息 s = ss.accept();//阻塞方法:等待接收客户端的数据，什么时候接收到数据，什么时候程序继续向下执行。 //accept()返回值为一个Socket，这个Socket其实就是客户端的Socket //接到这个Socket以后，客户端和服务器才真正产生了连接，才真正可以通信了 //3.感受到的操作流： is = s.getInputStream(); ois = new ObjectInputStream(is); //4.读取客户端发来的数据： User user = null; try { user = (User)(ois.readObject()); } catch (ClassNotFoundException e) { e.printStackTrace(); } //对对象进行验证： boolean flag = false; if (user.getName().equals(\u0026#34;admin\u0026#34;)\u0026amp;\u0026amp;user.getPwd().equals(\u0026#34;123456\u0026#34;)){ flag = true; } //向客户端输出一句话 操作流---》输出流 os = s.getOutputStream(); dos= new DataOutputStream(os); dos.writeBoolean(flag); } catch (IOException e) { e.printStackTrace(); }finally { //5.关闭流+关闭网络资源： try { if (dos!=null){ dos.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (os!=null){ os.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (ois!=null){ ois.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (is!=null){ is.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (s!=null){ s.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (ss!=null){ ss.close(); } } catch (IOException e) { e.printStackTrace(); } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 package com.zy.test3; import java.io.*; import java.net.Socket; import java.util.Scanner; /** * @Auther: 赵羽 * @Description: com.zy.test2 * @version: 1.0 */ public class TestClient { public static void main(String[] args) { //1。创建套接字:指定服务器的ip和端口号: Socket s = null; OutputStream os =null; DataInputStream dis = null; InputStream is =null; ObjectOutputStream oos =null; try { s = new Socket(\u0026#34;localhost\u0026#34;, 8585); //录入用户的账号和密码： Scanner sc = new Scanner(System.in); System.out.println(\u0026#34;请录入你的账号：\u0026#34;); String name = sc.next(); System.out.println(\u0026#34;请录入你的密码：\u0026#34;); String pwd = sc.next(); //将账号和密码封装为一个User的对象： User user = new User(name, pwd); //向外发送数据 利用传输流 os = s.getOutputStream(); oos = new ObjectOutputStream(os); oos.writeObject(user); //接收服务器端的回话--》利用输入流: is = s.getInputStream(); dis = new DataInputStream(is); boolean b = dis.readBoolean(); if (b){ System.out.println(\u0026#34;恭喜，登录成功\u0026#34;); }else { System.out.println(\u0026#34;对不起，登录失败\u0026#34;); } } catch (IOException e) { e.printStackTrace(); }finally { //3.关闭流 + 关闭网络资源 try { if (dis!=null){ dis.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (is!=null){ is.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (oos!=null){ oos.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (os!=null){ os.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (s!=null){ s.close(); } } catch (IOException e) { e.printStackTrace(); } } } } 基于UDP的网络编程 TCP: 客户端: Socket 程序感受到的使用流∶输出流 服务器端:ServerSocket \u0026mdash;\u0026gt;Socket程序感受到的使用流︰输入流\n(客户端和服务器端地位不平等。)\nUDP: 发送方:DatagramSocket 发送:数据包 DatagramPacket\n接收方:DatagramSocket接收:数据包 DatagramPacket\n(发送方和接收方的地址是平等的。)\nUDP案例：完成网站的咨询聊天\n单向通信：\n发送方：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.zy.test4; import java.io.IOException; import java.net.*; /** * @Auther: 赵羽 * @Description: com.zy.test4 * @version: 1.0 */ public class TestSend { public static void main(String[] args) throws IOException { System.out.println(\u0026#34;学生上线了\u0026#34;); //1.准备套接字：指定发送方的端口号 DatagramSocket ds = new DatagramSocket(8585); //2.准备数据包 String str = \u0026#34;你好\u0026#34;; byte[] bytes = str.getBytes(); /* *需要四个参数: 1.指的是传送数据转为字节数组 2.字节数组的长度 3.封装接收方的ip 4.指定接收方的端口号 * */ DatagramPacket dp = new DatagramPacket(bytes, bytes.length, InetAddress.getByName(\u0026#34;localhost\u0026#34;), 8888); //发送 ds.send(dp); //关闭 ds.close(); } } 接收方：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.zy.test4; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.SocketException; /** * @Auther: 赵羽 * @Description: com.zy.test4 * @version: 1.0 */ public class TestReceive { public static void main(String[] args) throws IOException { System.out.println(\u0026#34;老师上线了\u0026#34;); //1.创建套接字：指定接收方的端口 DatagramSocket ds = new DatagramSocket(8888); //2.有一个空的数据包，打算用来接受 对方传过来的数据包： byte[] b = new byte[1024]; DatagramPacket dp = new DatagramPacket(b, b.length); //3.接受对方的数据包，然后放入我们的dp数据包中填充 ds.receive(dp); //4.取出数据 byte[] data = dp.getData(); String s = new String(data,0,dp.getLength()); //数据包中的有效长度 System.out.println(\u0026#34;学生对我说：\u0026#34;+s); //5.关闭 ds.close(); } } 双向通信：\n发送方：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.zy.test4; import java.io.IOException; import java.net.*; import java.util.Scanner; /** * @Auther: 赵羽 * @Description: com.zy.test4 * @version: 1.0 */ public class TestSend { public static void main(String[] args) throws IOException { System.out.println(\u0026#34;学生上线了\u0026#34;); //1.准备套接字：指定发送方的端口号 DatagramSocket ds = new DatagramSocket(8585); //2.准备数据包 Scanner sc = new Scanner(System.in); System.out.print(\u0026#34;学生：\u0026#34;); String str = sc.next(); byte[] bytes = str.getBytes(); /* *需要四个参数: 1.指的是传送数据转为字节数组 2.字节数组的长度 3.封装接收方的ip 4.指定接收方的端口号 * */ DatagramPacket dp = new DatagramPacket(bytes, bytes.length, InetAddress.getByName(\u0026#34;localhost\u0026#34;), 8888); //发送 ds.send(dp); //接受老师发送来的信息 byte[] b = new byte[1024]; DatagramPacket dp2 = new DatagramPacket(b, b.length); ds.receive(dp2); byte[] data = dp2.getData(); String s = new String(data,0,dp2.getLength()); //数据包中的有效长度 System.out.println(\u0026#34;老师对我说：\u0026#34;+s); //关闭 ds.close(); } } 接收方：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package com.zy.test4; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import java.util.Scanner; /** * @Auther: 赵羽 * @Description: com.zy.test4 * @version: 1.0 */ public class TestReceive { public static void main(String[] args) throws IOException { System.out.println(\u0026#34;老师上线了\u0026#34;); //1.创建套接字：指定接收方的端口 DatagramSocket ds = new DatagramSocket(8888); //2.有一个空的数据包，打算用来接受 对方传过来的数据包： byte[] b = new byte[1024]; DatagramPacket dp = new DatagramPacket(b, b.length); //3.接受对方的数据包，然后放入我们的dp数据包中填充 ds.receive(dp); //4.取出数据 byte[] data = dp.getData(); String s = new String(data,0,dp.getLength()); //数据包中的有效长度 System.out.println(\u0026#34;学生对我说：\u0026#34;+s); //老师进行回复 Scanner sc = new Scanner(System.in); System.out.println(\u0026#34;老师回复：\u0026#34;); String str =sc.next(); byte[] bytes = str.getBytes(); DatagramPacket dp2 = new DatagramPacket(bytes, bytes.length, InetAddress.getByName(\u0026#34;localhost\u0026#34;), 8585); ds.send(dp2); //5.关闭 ds.close(); } } 异常处理：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.zy.test4; import java.io.IOException; import java.net.*; import java.util.Scanner; /** * @Auther: 赵羽 * @Description: com.zy.test4 * @version: 1.0 */ public class TestSend { public static void main(String[] args) { System.out.println(\u0026#34;学生上线了\u0026#34;); //1.准备套接字：指定发送方的端口号 DatagramSocket ds = null; try { ds = new DatagramSocket(8585); //2.准备数据包 Scanner sc = new Scanner(System.in); System.out.print(\u0026#34;学生：\u0026#34;); String str = sc.next(); byte[] bytes = str.getBytes(); DatagramPacket dp = new DatagramPacket(bytes, bytes.length, InetAddress.getByName(\u0026#34;localhost\u0026#34;), 8888); //发送 ds.send(dp); //接受老师发送来的信息 byte[] b = new byte[1024]; DatagramPacket dp2 = new DatagramPacket(b, b.length); ds.receive(dp2); byte[] data = dp2.getData(); String s = new String(data,0,dp2.getLength()); //数据包中的有效长度 System.out.println(\u0026#34;老师对我说：\u0026#34;+s); } catch (SocketException e) { e.printStackTrace(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { //关闭 ds.close(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package com.zy.test4; import java.io.IOException; import java.net.*; import java.util.Scanner; /** * @Auther: 赵羽 * @Description: com.zy.test4 * @version: 1.0 */ public class TestReceive { public static void main(String[] args) { System.out.println(\u0026#34;老师上线了\u0026#34;); //1.创建套接字：指定接收方的端口 DatagramSocket ds = null; try { ds = new DatagramSocket(8888); //2.有一个空的数据包，打算用来接受 对方传过来的数据包： byte[] b = new byte[1024]; DatagramPacket dp = new DatagramPacket(b, b.length); //3.接受对方的数据包，然后放入我们的dp数据包中填充 ds.receive(dp); //4.取出数据 byte[] data = dp.getData(); String s = new String(data,0,dp.getLength()); //数据包中的有效长度 System.out.println(\u0026#34;学生对我说：\u0026#34;+s); //老师进行回复 Scanner sc = new Scanner(System.in); System.out.println(\u0026#34;老师回复：\u0026#34;); String str =sc.next(); byte[] bytes = str.getBytes(); DatagramPacket dp2 = new DatagramPacket(bytes, bytes.length, InetAddress.getByName(\u0026#34;localhost\u0026#34;), 8585); ds.send(dp2); } catch (SocketException e) { e.printStackTrace(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { //5.关闭 ds.close(); } } } 完整通信：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 package com.zy.test4; import java.io.IOException; import java.net.*; import java.util.Scanner; /** * @Auther: 赵羽 * @Description: com.zy.test4 * @version: 1.0 */ public class TestSend { public static void main(String[] args) { System.out.println(\u0026#34;学生上线了\u0026#34;); //1.准备套接字：指定发送方的端口号 DatagramSocket ds = null; try { ds = new DatagramSocket(8585); while (true){ //2.准备数据包 Scanner sc = new Scanner(System.in); System.out.print(\u0026#34;学生：\u0026#34;); String str = sc.next(); byte[] bytes = str.getBytes(); DatagramPacket dp = new DatagramPacket(bytes, bytes.length, InetAddress.getByName(\u0026#34;localhost\u0026#34;), 8888); //发送 ds.send(dp); if (str.equals(\u0026#34;byebye\u0026#34;)){ System.out.println(\u0026#34;学生下线了\u0026#34;); break; } //接受老师发送来的信息 byte[] b = new byte[1024]; DatagramPacket dp2 = new DatagramPacket(b, b.length); ds.receive(dp2); byte[] data = dp2.getData(); String s = new String(data,0,dp2.getLength()); //数据包中的有效长度 System.out.println(\u0026#34;老师对我说：\u0026#34;+s); } } catch (SocketException e) { e.printStackTrace(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { //关闭 ds.close(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package com.zy.test4; import java.io.IOException; import java.net.*; import java.util.Scanner; /** * @Auther: 赵羽 * @Description: com.zy.test4 * @version: 1.0 */ public class TestReceive { public static void main(String[] args) { System.out.println(\u0026#34;老师上线了\u0026#34;); //1.创建套接字：指定接收方的端口 DatagramSocket ds = null; try { ds = new DatagramSocket(8888); while (true){ //2.有一个空的数据包，打算用来接受 对方传过来的数据包： byte[] b = new byte[1024]; DatagramPacket dp = new DatagramPacket(b, b.length); //3.接受对方的数据包，然后放入我们的dp数据包中填充 ds.receive(dp); //4.取出数据 byte[] data = dp.getData(); String s = new String(data,0,dp.getLength()); //数据包中的有效长度 System.out.println(\u0026#34;学生对我说：\u0026#34;+s); if (s.equals(\u0026#34;byebye\u0026#34;)){ System.out.println(\u0026#34;学生已经下线了，老师也下线了\u0026#34;); break; } //老师进行回复 Scanner sc = new Scanner(System.in); System.out.println(\u0026#34;老师回复：\u0026#34;); String str =sc.next(); byte[] bytes = str.getBytes(); DatagramPacket dp2 = new DatagramPacket(bytes, bytes.length, InetAddress.getByName(\u0026#34;localhost\u0026#34;), 8585); ds.send(dp2); } } catch (SocketException e) { e.printStackTrace(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally { //5.关闭 ds.close(); } } } ","permalink":"https://ktzxy.top/posts/sdd7voc7m9/","summary":"Day 12 网络编程","title":"Day 12 网络编程"},{"content":" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #!/bin/bash # 设置 Tomcat 目录和相关命令路径 tomcat_home=\u0026#34;/data/tomcat/tomcat-8484\u0026#34; SHUTDOWN=\u0026#34;$tomcat_home/bin/shutdown.sh\u0026#34; STARTTOMCAT=\u0026#34;$tomcat_home/bin/startup.sh\u0026#34; # 添加错误处理 set -e # 参数校验 if [ $# -ne 1 ]; then echo \u0026#34;Usage: $0 {start|stop|restart|logs}\u0026#34; exit 1 fi # 根据传入参数执行不同操作 case $1 in start) echo \u0026#34;启动 $tomcat_home\u0026#34; # 检查 Tomcat 是否已经在运行 if ps -p $(pgrep -f \u0026#34;catalina\\.home=$tomcat_home\u0026#34;) \u0026amp;\u0026gt;/dev/null; then echo \u0026#34;Tomcat 已经在运行\u0026#34; exit 1 fi $STARTTOMCAT cd $tomcat_home/logs tail -f catalina.out ;; stop) echo \u0026#34;关闭 $tomcat_home\u0026#34; # 优化停止 Tomcat 的方式 if [ -f \u0026#34;$SHUTDOWN\u0026#34; ]; then $SHUTDOWN else netstat -anp | grep 8484 | grep -v grep | awk \u0026#39;{print $7}\u0026#39; | sed -e \u0026#39;s//java//g\u0026#39; | sed -e \u0026#39;s/^/kill -9 /g\u0026#39; | sh fi ;; restart) echo \u0026#34;重启 $tomcat_home\u0026#34; $0 stop sleep 5 $0 start ;; logs) echo \u0026#34;查看 Tomcat 日志：\u0026#34; cd $tomcat_home/logs tail -f catalina.out ;; *) echo \u0026#34;Invalid option. Usage: $0 {start|stop|restart|logs}\u0026#34; exit 1 ;; esac 1 2 sed -i \u0026#34;s/ //\u0026#34; tomcat-8484.sh #设置脚本文件为unix格式 chmod 777 ./tomcat-8484.sh 有这样一个场景，公司为了安全起见，需要对所有登录Linux服务器做安全限制，要求除了管理员其他要登录linux服务器的员工不能用最高权限账号登录，要创建新的用户，对目录及文件权限做出控制，只能对需要操作的目录允许读，写，执行权限，其他目录只有读的权限，并且所有tomcat不能直接在bin中用startup.sh,shutdown.sh进行启动和停止，要通过写shell脚本进行此操作，也就是说有两个步骤，创建用户并设置权限，写tomcat启动脚本\n1 2 3 4 5 6 groupadd tomcat #加组 useradd -g tomcat -s /usr/sbin/nologin tomcat #向组加用户 usermod -L tomcat #锁定密码，使密码无效 passwd tomcat # 设置密码 chown -R tomcat:tomcat /data #分配权限给用户 [root@localhost data]# ls -l total 0 drwxr-xr-x. 4 tomcat tomcat 79 May 20 08:03 tomcat [root@localhost data]# ","permalink":"https://ktzxy.top/posts/bhytpcx85p/","summary":"tomcat启动脚本","title":"tomcat启动脚本"},{"content":" 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #!/bin/bash # 定义软件包名称和版本 jdk_package=\u0026#34;jdk-8u371-linux-x64.rpm\u0026#34; tomcat_package=\u0026#34;apache-tomcat-9.0.78.tar.gz\u0026#34; tomcat_version=\u0026#34;9.0.78\u0026#34; tomcat_base_dir=\u0026#34;/usr/local/tomcat\u0026#34; # 检查是否以root身份运行 if [ \u0026#34;$(id -u)\u0026#34; != \u0026#34;0\u0026#34; ]; then echo \u0026#34;Please run as root.\u0026#34; exit 1 fi # 检查软件包是否存在 if [ ! -f \u0026#34;/opt/${jdk_package}\u0026#34; ] || [ ! -f \u0026#34;/opt/${tomcat_package}\u0026#34; ]; then echo \u0026#34;JDK or Tomcat package not found. Please make sure ${jdk_package} and ${tomcat_package} are in /opt.\u0026#34; exit 1 fi # 安装JDK rpm -ivh /opt/${jdk_package} # 解压Tomcat tar -xf /opt/${tomcat_package} -C /opt || { echo \u0026#34;Tomcat extraction failed.\u0026#34;; exit 1; } # 创建Tomcat目录并移动Tomcat mkdir -p ${tomcat_base_dir} \u0026amp;\u0026amp; mv -f /opt/apache-tomcat-${tomcat_version} ${tomcat_base_dir}/tomcat1 # 复制Tomcat实例 cp -a ${tomcat_base_dir}/tomcat1 ${tomcat_base_dir}/tomcat2 || { echo \u0026#34;Tomcat copy failed.\u0026#34;; exit 1; } # 编辑环境变量文件 echo \u0026#34;export CATALINA_HOME1=${tomcat_base_dir}/tomcat1 export CATALINA_BASE1=${tomcat_base_dir}/tomcat1 export TOMCAT_HOME1=${tomcat_base_dir}/tomcat1 #tomcat2 export CATALINA_HOME2=${tomcat_base_dir}/tomcat2 export CATALINA_BASE2=${tomcat_base_dir}/tomcat2 export TOMCAT_HOME2=${tomcat_base_dir}/tomcat2\u0026#34; | tee -a /etc/profile.d/tomcat.sh \u0026amp;\u0026amp; source /etc/profile.d/tomcat.sh || { echo \u0026#34;Environment variables configuration failed.\u0026#34;; exit 1; } # 修改Tomcat配置文件 sed -i.bak -e \u0026#39;123s/\u0026lt;!--//g\u0026#39;\\ -e \u0026#39;130s/\u0026lt;!--//g\u0026#39; $CATALINA_HOME1/conf/server.xml $CATALINA_HOME2/conf/server.xml sed -i.bak -e \u0026#39;22s/\u0026lt;Server port=\u0026#34;8005\u0026#34; shutdown=\u0026#34;SHUTDOWN\u0026#34;\u0026gt;/\u0026lt;Server port=\u0026#34;8006\u0026#34; shutdown=\u0026#34;SHUTDOWN\u0026#34;\u0026gt;/g\u0026#39; \\ -e \u0026#39;s/ \u0026lt;Connector port=\u0026#34;8080\u0026#34; protocol=\u0026#34;HTTP\\/1.1\u0026#34; / \u0026lt;Connector port=\u0026#34;8081\u0026#34; protocol=\u0026#34;HTTP\\/1.1\u0026#34;/g\u0026#39; \\ -e \u0026#39;s/ port=\u0026#34;8009\u0026#34;/ port=\u0026#34;8010\u0026#34; /g\u0026#39; \\ $CATALINA_HOME2/conf/server.xml || { echo \u0026#34;Tomcat configuration failed.\u0026#34;; exit 1; } # 更新start.sh和shutdown.sh文件 for tomcat_instance in 1 2; do cat \u0026lt;\u0026lt;EOF \u0026gt; ${tomcat_base_dir}/tomcat${tomcat_instance}/bin/start.sh export CATALINA_BASE=\\${CATALINA_BASE${tomcat_instance}} export CATALINA_HOME=\\${CATALINA_HOME${tomcat_instance}} export TOMCAT_HOME=\\${TOMCAT_HOME${tomcat_instance}} EOF cat \u0026lt;\u0026lt;EOF \u0026gt; ${tomcat_base_dir}/tomcat${tomcat_instance}/bin/shutdown.sh export CATALINA_BASE=\\${CATALINA_BASE${tomcat_instance}} export CATALINA_HOME=\\${CATALINA_HOME${tomcat_instance}} export TOMCAT_HOME=\\${TOMCAT_HOME${tomcat_instance}} EOF done mkdir -p $CATALINA_HOME1/webapps/kgc mkdir -p $CATALINA_HOME2/webapps/benet echo \u0026#34;This is kgc page\\!\u0026#34; \u0026gt;$CATALINA_HOME1/webapps/kgc/index.jsp echo \u0026#34;This is benet page\\!\u0026#34; \u0026gt;$CATALINA_HOME2/webapps/benet/index.jsp # 启动Tomcat实例 ${tomcat_base_dir}/tomcat1/bin/startup.sh || { echo \u0026#34;Tomcat 1 startup failed.\u0026#34;; exit 1; } ${tomcat_base_dir}/tomcat2/bin/startup.sh || { echo \u0026#34;Tomcat 2 startup failed.\u0026#34;; exit 1; } echo \u0026#34;Tomcat 1 and Tomcat 2 started successfully.\u0026#34; ","permalink":"https://ktzxy.top/posts/uk2hoa2cvi/","summary":"tomcat多实例部署","title":"tomcat多实例部署"},{"content":" 1 2 3 4 5 6 7 8 9 10 11 12 13 ●bin:存放启动和关闭Tomcat的脚本文件，比较常用的是 catalina.sh、startup.sh、shutdown.sh三个文件 ●conf：存放Tomcat 服务器的各种配置文件，比较常用的是 server.xml、context.xml、tomcat-users.xml、web.xml 四个文件。 ●server.xml: Tomcat的主配置文件，包含Service，Connector，Engine，Realm，Valve,Hosts主组件的相关配置信息; ●context.xml:所有host的默认配置信息; ●tomcat-user.xml:Realm认证时用到的相关角色、用户和密码等信息，Tomcat自带的manager默认情况下会用到此文件，在Tomcat中添加/删除用户，为用户指|定角色等将通过编辑此文件实现; ●web.xml:遵循Servlet规范标准的配置文件，用于配置servlet，并为所有的web应用程序提供包括MIME映射等默认配置信息; ●lib：存放Tomcat运行需要的库文件的jar 包，一般不作任何改动，除非连接第三方服务，比如 redis，那就需要添加相对应的jar 包 ●logs：存放 Tomcat 执行时的日志 ●temp：存放 Tomcat 运行时产生的文件 ●webapps：存放 Tomcat 默认的 Web 应用部署目录 ●work：Tomcat工作日录，存放jsp编译后产生的class文件，一般清除Tomcat缓存的时候会使用到 ●src:存放Tomcat 的源代码 ●doc:存放Tomcat文档 1 2 3 4 5 6 7 CLASSPATH：编译、运行Java程序时，JRE会去该变量指定的路径中搜索所需的类（.class）文件。 dt.jar：是关于运行环境的类库，主要是可视化的 swing 的包。 tools.jar：主要是一些jdk工具的类库，包括javac、java、javap（jdk自带的一个反编译工具）、javadoc等。 JDK ：java development kit （java开发工具） JRE ：java runtime environment （java运行时环境） JVM ：java virtual machine （java虚拟机），使java程序可以在多种平台上运行class文件。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #### 扩展和优化 Tomcat 的 catalina.sh 文件以调整 JVM 参数 CATALINA_OPTS=\u0026#34;-server \\ -Xms2048m -Xmx2048m \\ -Xmn512m \\ -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m \\ -XX:NewRatio=3 \\ -XX:SurvivorRatio=8 \\ -XX:+UseG1GC \\ -XX:MaxGCPauseMillis=200 \\ -XX:InitiatingHeapOccupancyPercent=70 \\ -XX:+PrintGCDetails \\ -XX:+PrintGCDateStamps \\ -XX:+PrintGCCause \\ -Xloggc:/var/log/tomcat/tomcat_gc.log \\ -Djava.awt.headless=true \\ -Dcom.sun.management.jmxremote.port=10086 \\ -Dcom.sun.management.jmxremote.ssl=false \\ -Dcom.sun.management.jmxremote.authenticate=false\u0026#34; -server: 启用服务器模式的 JVM。 -Xms 和 -Xmx: 分别设置堆的初始大小和最大大小，保持一致可以减少堆大小调整带来的开销。 -Xmn: 设置年轻代（Young Generation）大小。 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize: 设置元空间（MetaSpace，取代了 PermGen）的初始和最大大小。 -XX:NewRatio: 设置老年代与年轻代的大小比例。 -XX:SurvivorRatio: 设置 Eden 区与 Survivor 区的大小比例。 -XX:+UseG1GC: 使用 G1 垃圾收集器，根据实际需求可选用其他适合的 GC 策略。 -XX:MaxGCPauseMillis: 设置期望的最大 GC 停顿时间。 -XX:InitiatingHeapOccupancyPercent: 设置触发并发标记周期的堆占用百分比。 -XX:+PrintGCDetails 等：开启详细的 GC 日志记录，便于分析和调优。 # 禁用显式gc -XX:+DisableExplicitGC # 自动将System.gc() 调用转换成一个空操作，即应用中调用 System.gc()会变成一个空操作，避免程序员在代码里进行System.gc()这种危险操作。System.gc()除 非是到了万不得已的情况下使用，都应该交给JVM。 -Xloggc: 设置 GC 日志文件路径。 -Djava.awt.headless=true: 防止在无图形界面环境中出现相关异常。 -Dcom.sun.management.jmxremote.*: 开启 JMX 远程监控，便于通过 JConsole 或其他工具监控 JVM 状态。 ","permalink":"https://ktzxy.top/posts/oz2c80hkmf/","summary":"tomcat说明","title":"tomcat说明"},{"content":"Docker 常见疑难杂症解决方案\n1.Docker 迁移存储目录 默认情况系统会将 Docker 容器存放在/var/lib/docker 目录下\n问题起因:今天通过监控系统，发现公司其中一台服务器的磁盘快慢，随即上去看了下，发现 /var/lib/docker 这个目录特别大。由上述原因，我们都知道，在 /var/lib/docker 中存储的都是相关于容器的存储，所以也不能随便的将其删除掉。\n那就准备迁移 docker 的存储目录吧，或者对 /var 设备进行扩容来达到相同的目的。更多关于 dockerd 的详细参数，请点击查看 官方文档 地址。\n但是需要注意的一点就是，尽量不要用软链， 因为一些 docker 容器编排系统不支持这样做，比如我们所熟知的 k8s 就在内。\n1 2 3 4 5 # 发现容器启动不了了 ERROR：cannot create temporary directory! # 查看系统存储情况 $ du -h --max-depth=1 解决方法1：添加软链接\n1 2 3 4 5 6 7 8 9 10 11 # 1.停止docker服务 $ sudo systemctl stop docker # 2.开始迁移目录 $ sudo mv /var/lib/docker /data/ # 3.添加软链接 # sudo ln -s /data/docker /var/lib/docker # 4.启动docker服务 $ sudo systemctl start docker 解决方法2：改动 docker 配置文件\n1 2 3 4 5 6 7 8 9 10 # 3.改动docker启动配置文件 $ sudo vim /lib/systemd/system/docker.service ExecStart=/usr/bin/dockerd --graph=/data/docker/ # 4.改动docker启动配置文件 $ sudo vim /etc/docker/daemon.json { \u0026#34;live-restore\u0026#34;: true, \u0026#34;graph\u0026#34;: [ \u0026#34;/data/docker/\u0026#34; ] } 操作注意事项：在迁移 docker 目录的时候注意使用的命令，要么使用 mv 命令直接移动，要么使用 cp 命令复制文件，但是需要注意同时复制文件权限和对应属性，不然在使用的时候可能会存在权限问题。如果容器中，也是使用 root 用户，则不会存在该问题，但是也是需要按照正确的操作来迁移目录。\n1 2 3 4 5 # 使用mv命令 $ sudo mv /var/lib/docker /data/docker # 使用cp命令 $ sudo cp -arv /data/docker /data2/docker 下图中，就是因为启动的容器使用的是普通用户运行进程的，且在运行当中需要使用 /tmp 目录，结果提示没有权限。在我们导入容器镜像的时候，其实是会将容器启动时需要的各个目录的权限和属性都赋予了。如果我们直接是 cp 命令单纯复制文件内容的话，就会出现属性不一致的情况，同时还会有一定的安全问题。\n2.Docker 设备空间不足\nIncrease Docker container size from default 10GB on rhel7.\n问题起因一：容器在导入或者启动的时候，如果提示磁盘空间不足的，那么多半是真的因为物理磁盘空间真的有问题导致的。如下所示，我们可以看到 / 分区确实满了。\n1 2 3 4 5 6 # 查看物理磁盘空间 $ df -Th Filesystem Size Used Avail Use% Mounted on /dev/vda1 40G 40G 0G 100% / tmpfs 7.8G 0 7.8G 0% /dev/shm /dev/vdb1 493G 289G 179G 62% /mnt 如果发现真的是物理磁盘空间满了的话，就需要查看到底是什么占据了如此大的空间，导致因为容器没有空间无法启动。其中，docker 自带的命令就是一个很好的能够帮助我们发现问题的工具。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 查看基本信息 # 硬件驱动使用的是devicemapper，空间池为docker-252 # 磁盘可用容量仅剩16.78MB，可用供我们使用 $ docker info Containers: 1 Images: 28 Storage Driver: devicemapper Pool Name: docker-252:1-787932-pool Pool Blocksize: 65.54 kB Backing Filesystem: extfs Data file: /dev/loop0 Metadata file: /dev/loop1 Data Space Used: 1.225 GB Data Space Total: 107.4 GB Data Space Available: 16.78 MB Metadata Space Used: 2.073 MB Metadata Space Total: 2.147 GB 解决方法：通过查看信息，我们知道正是因为 docker 可用的磁盘空间不足，所以导致启动的时候没有足够的空间进行加载启动镜像。解决的方法也很简单，第一就是清理无效数据文件释放磁盘空间(清除日志)，第二就是修改 docker 数据的存放路径(大分区)。\n1 2 3 4 5 # 显示哪些容器目录具有最大的日志文件 $ du -d1 -h /var/lib/docker/containers | sort -h # 清除您选择的容器日志文件的内容 $ cat /dev/null \u0026gt; /var/lib/docker/containers/container_id/container_log_name 问题起因二：显然我遇到的不是上一种情况，而是在启动容器的时候，容器启动之后不久就显示是 unhealthy 的状态，通过如下日志发现，原来是复制配置文件启动的时候，提示磁盘空间不足。\n后面发现是因为 CentOS7 的系统使用的 docker 容器默认的创建大小就是 10G 而已，然而我们使用的容器却超过了这个限制，导致无法启动时提示空间不足。\n1 2 3 4 5 6 7 2019-08-16 11:11:15,816 INFO spawned: \u0026#39;app-demo\u0026#39; with pid 835 2019-08-16 11:11:16,268 INFO exited: app (exit status 1; not expected) 2019-08-16 11:11:17,270 INFO gave up: app entered FATAL state, too many start retries too quickly cp: cannot create regular file \u0026#39;/etc/supervisor/conf.d/grpc-app-demo.conf\u0026#39;: No space left on device cp: cannot create regular file \u0026#39;/etc/supervisor/conf.d/grpc-app-demo.conf\u0026#39;: No space left on device cp: cannot create regular file \u0026#39;/etc/supervisor/conf.d/grpc-app-demo.conf\u0026#39;: No space left on device cp: cannot create regular file \u0026#39;/etc/supervisor/conf.d/grpc-app-demo.conf\u0026#39;: No space left on device 解决方法1：改动 docker 启动配置文件\n1 2 3 4 5 # /etc/docker/daemon.json { \u0026#34;live-restore\u0026#34;: true, \u0026#34;storage-opt\u0026#34;: [ \u0026#34;dm.basesize=20G\u0026#34; ] } 解决方法2：改动 systemctl 的 docker 启动文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 # 1.stop the docker service $ sudo systemctl stop docker # 2.rm exised container $ sudo rm -rf /var/lib/docker # 2.edit your docker service file $ sudo vim /usr/lib/systemd/system/docker.service # 3.find the execution line ExecStart=/usr/bin/dockerd and change it to: ExecStart=/usr/bin/dockerd --storage-opt dm.basesize=20G # 4.start docker service again $ sudo systemctl start docker # 5.reload daemon $ sudo systemctl daemon-reload 问题起因三：还有一种情况也会让容器无法启动，并提示磁盘空间不足，但是使用命令查看发现并不是因为物理磁盘真的不足导致的。而是，因为对于分区的 inode 节点数满了导致的。\n1 2 # 报错信息 No space left on device 解决方法：因为 ext3 文件系统使用 inode table 存储 inode 信息，而 xfs 文件系统使用 B+ tree 来进行存储。考虑到性能问题，默认情况下这个 B+ tree 只会使用前 1TB 空间，当这 1TB 空间被写满后，就会导致无法写入 inode 信息，报磁盘空间不足的错误。我们可以在 mount 时，指定 inode64 即可将这个 B+ tree 使用的空间扩展到整个文件系统。\n1 2 3 4 5 # 查看系统的inode节点使用情况 $ sudo df -i # 尝试重新挂载 $ sudo mount -o remount -o noatime,nodiratime,inode64,nobarrier /dev/vda1 补充知识：文件储存在硬盘上，硬盘的最小存储单位叫做“扇区”(Sector)。每个扇区储存 512 字节(相当于0.5KB)。操作系统读取硬盘的时候，不会一个个扇区地读取，这样效率太低，而是一次性连续读取多个扇区，即一次性读取一个“块”(block)。这种由多个扇区组成的”块”，是文件存取的最小单位。”块”的大小，最常见的是4KB，即连续八个 sector 组成一个 block 块。文件数据都储存在”块”中，那么很显然，我们还必须找到一个地方储存文件的元信息，比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做“索引节点”(inode)。每一个文件都有对应的 inode，里面包含了除了文件名以外的所有文件信息。\ninode 也会消耗硬盘空间，所以硬盘格式化的时候，操作系统自动将硬盘分成两个区域。一个是数据区，存放文件数据；另一个是 inode 区(inode table)，存放 inode 所包含的信息。每个 inode 节点的大小，一般是 128 字节或 256 字节。inode 节点的总数，在格式化时就给定，一般是每1KB或每2KB就设置一个 inode 节点。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 每个节点信息的内容 $ stat check_port_live.sh File: check_port_live.sh Size: 225 Blocks: 8 IO Block: 4096 regular file Device: 822h/2082d Inode: 99621663 Links: 1 Access: (0755/-rwxr-xr-x) Uid: ( 1006/ escape) Gid: ( 1006/ escape) Access: 2019-07-29 14:59:59.498076903 +0800 Modify: 2019-07-29 14:59:59.498076903 +0800 Change: 2019-07-29 23:20:27.834866649 +0800 Birth: - # 磁盘的inode使用情况 $ df -i Filesystem Inodes IUsed IFree IUse% Mounted on udev 16478355 801 16477554 1% /dev tmpfs 16487639 2521 16485118 1% /run /dev/sdc2 244162560 4788436 239374124 2% / tmpfs 16487639 5 16487634 1% /dev/shm 3.Docker 缺共享链接库 Docker 命令需要对/tmp 目录下面有访问权限\n问题起因：给系统安装完 compose 之后，查看版本的时候，提示缺少一个名为 libz.so.1 的共享链接库。第一反应就是，是不是系统少安装那个软件包导致的。随即，搜索了一下，将相关的依赖包都给安装了，却还是提示同样的问题。\n1 2 3 # 提示错误信息 $ docker-compose --version error while loading shared libraries: libz.so.1: failed to map segment from shared object: Operation not permitted 解决方法：后来发现，是因为系统中 docker 没有对 /tmp 目录的访问权限导致，需要重新将其挂载一次，就可以解决了。\n1 2 # 重新挂载 $ sudo mount /tmp -o remount,exec 4.Docker 容器文件损坏 对 dockerd 的配置有可能会影响到系统稳定\n问题起因：容器文件损坏，经常会导致容器无法操作。正常的 docker 命令已经无法操控这台容器了，无法关闭、重启、删除。正巧，前天就需要这个的问题，主要的原因是因为重新对 docker 的默认容器进行了重新的分配限制导致的。\n1 2 # 操作容器遇到类似的错误 b\u0026#39;devicemapper: Error running deviceCreate (CreateSnapDeviceRaw) dm_task_run failed\u0026#39; 解决方法：可以通过以下操作将容器删除/重建。\n1 2 3 4 5 6 7 8 9 10 11 12 # 1.关闭docker $ sudo systemctl stop docker # 2.删除容器文件 $ sudo rm -rf /var/lib/docker/containers # 3.重新整理容器元数据 $ sudo thin_check /var/lib/docker/devicemapper/devicemapper/metadata $ sudo thin_check --clear-needs-check-flag /var/lib/docker/devicemapper/devicemapper/metadata # 4.重启docker $ sudo systemctl start docker 5.Docker 容器优雅重启 不停止服务器上面运行的容器，重启 dockerd 服务是多么好的一件事\n问题起因：默认情况下，当 Docker 守护程序终止时，它会关闭正在运行的容器。从 Docker-ce 1.12 开始，可以在配置文件中添加 live-restore 参数，以便在守护程序变得不可用时容器保持运行。需要注意的是 Windows 平台暂时还是不支持该参数的配置。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # Keep containers alive during daemon downtime $ sudo vim /etc/docker/daemon.yaml { \u0026#34;live-restore\u0026#34;: true } # 在守护进程停机期间保持容器存活 $ sudo dockerd --live-restore # 只能使用reload重载 # 相当于发送SIGHUP信号量给dockerd守护进程 $ sudo systemctl reload docker # 但是对应网络的设置需要restart才能生效 $ sudo systemctl restart docker 解决方法：可以通过以下操作将容器删除/重建。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # /etc/docker/daemon.yaml { \u0026#34;registry-mirrors\u0026#34;: [\u0026#34;https://vec0xydj.mirror.aliyuncs.com\u0026#34;], # 配置获取官方镜像的仓库地址 \u0026#34;experimental\u0026#34;: true, # 启用实验功能 \u0026#34;default-runtime\u0026#34;: \u0026#34;nvidia\u0026#34;, # 容器的默认OCI运行时(默认为runc) \u0026#34;live-restore\u0026#34;: true, # 重启dockerd服务的时候容易不终止 \u0026#34;runtimes\u0026#34;: { # 配置容器运行时 \u0026#34;nvidia\u0026#34;: { \u0026#34;path\u0026#34;: \u0026#34;/usr/bin/nvidia-container-runtime\u0026#34;, \u0026#34;runtimeArgs\u0026#34;: [] } }, \u0026#34;default-address-pools\u0026#34;: [ # 配置容器使用的子网地址池 { \u0026#34;scope\u0026#34;: \u0026#34;local\u0026#34;, \u0026#34;base\u0026#34;:\u0026#34;172.17.0.0/12\u0026#34;, \u0026#34;size\u0026#34;:24 } ] } 6.Docker 容器无法删除 找不到对应容器进程是最吓人的\n问题起因：今天遇到 docker 容器无法停止/终止/删除，以为这个容器可能又出现了 dockerd 守护进程托管的情况，但是通过ps -ef \u0026lt;container id\u0026gt;无法查到对应的运行进程。哎，后来开始开始查 supervisor 以及 Dockerfile 中的进程，都没有。这种情况的可能原因是容器启动之后，之后，主机因任何原因重新启动并且没有优雅地终止容器。剩下的文件现在阻止你重新生成旧名称的新容器，因为系统认为旧容器仍然存在。\n1 2 3 # 删除容器 $ sudo docker rm -f f8e8c3.. Error response from daemon: Conflict, cannot remove the default name of the container 解决方法：找到 /var/lib/docker/containers/ 下的对应容器的文件夹，将其删除，然后重启一下 dockerd 即可。我们会发现，之前无法删除的容器没有了。\n1 2 3 4 5 # 删除容器文件 $ sudo rm -rf /var/lib/docker/containers/f8e8c3...65720 # 重启服务 $ sudo systemctl restart docker.service 7.Docker 容器中文异常 容器存在问题话，记得优先在官网查询\n问题起因：今天登陆之前部署的 MySQL 数据库查询，发现使用 SQL 语句无法查询中文字段，即使直接输入中文都没有办法显示。\n1 2 3 4 5 # 查看容器支持的字符集 root@b18f56aa1e15:# locale -a C C.UTF-8 POSIX 解决方法：Docker 部署的 MySQL 系统使用的是 POSIX 字符集。然而 POSIX 字符集是不支持中文的，而 C.UTF-8 是支持中文的只要把系统中的环境 LANG 改为 \u0026ldquo;C.UTF-8\u0026rdquo; 格式即可解决问题。同理，在 K8S 进入 pod 不能输入中文也可用此方法解决。\n1 2 3 4 5 6 7 8 # 临时解决 docker exec -it some-mysql env LANG=C.UTF-8 /bin/bash # 永久解决 docker run --name some-mysql \\ -e MYSQL_ROOT_PASSWORD=my-secret-pw \\ -d mysql:tag --character-set-server=utf8mb4 \\ --collation-server=utf8mb4_unicode_ci 8.Docker 容器网络互通 了解 Docker 的四种网络模型\n问题起因：在本机部署 Nginx 容器想代理本机启动的 Python 后端服务程序，但是对代码服务如下的配置，结果访问的时候一直提示 502 错误。\n1 2 3 4 5 6 7 8 9 10 # 启动Nginx服务 $ docker run -d -p 80:80 $PWD:/etc/nginx nginx nginx server { ... location /api { proxy_pass http://localhost:8080 } ... } 解决方法：后面发现是因为 nginx.conf 配置文件中的 localhost 配置的有问题，由于 Nginx 是在容器中运行，所以 localhost 为容器中的 localhost，而非本机的 localhost，所以导致无法访问。\n可以将 nginx.conf 中的 localhost 改为宿主机的 IP 地址，就可以解决 502 的错误。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 查询宿主机IP地址 =\u0026gt; 172.17.0.1 $ ip addr show docker0 docker0: \u0026lt;BROADCAST,MULTICAST,UP,LOWER_UP\u0026gt; mtu 1500 qdisc noqueue state UP group default link/ether 02:42:d5:4c:f2:1e brd ff:ff:ff:ff:ff:ff inet 172.17.0.1/16 scope global docker0 valid_lft forever preferred_lft forever inet6 fe80::42:d5ff:fe4c:f21e/64 scope link valid_lft forever preferred_lft forever nginx server { ... location /api { proxy_pass http://172.17.0.1:8080 } ... } 当容器使用 host 网络时，容器与宿主共用网络，这样就能在容器中访问宿主机网络，那么容器的 localhost 就是宿主机的 localhost 了。\n1 2 3 # 服务的启动方式有所改变(没有映射出来端口) # 因为本身与宿主机共用了网络，宿主机暴露端口等同于容器中暴露端口 $ docker run -d -p 80:80 --network=host $PWD:/etc/nginx nginxx 9.Docker 容器总线错误 总线错误看到的时候还是挺吓人了\n问题起因：在 docker 容器中运行程序的时候，提示 bus error 错误。\n1 2 3 # 总线报错 $ inv app.user_op --name=zhangsan Bus error (core dumped) 解决方法：原因是在 docker 运行的时候，shm 分区设置太小导致 share memory 不够。不设置 –shm-size 参数时，docker 给容器默认分配的 shm 大小为 64M，导致程序启动时不足。\n1 2 # 启动docker的时候加上--shm-size参数(单位为b,k,m或g) $ docker run -it --rm --shm-size=200m pytorch/pytorch:latest 解决方法：还有一种情况就是容器内的磁盘空间不足，也会导致 bus error 的报错，所以清除多余文件或者目录，就可以解决了。\n1 2 3 4 5 # 磁盘空间不足 $ df -Th Filesystem Type Size Used Avail Use% Mounted on overlay overlay 1T 1T 0G 100% / shm tmpfs 64M 24K 64M 1% /dev/shm 10.Docker NFS 挂载报错 总线错误看到的时候还是挺吓人了\n问题起因：我们将服务部署到 openshift 集群中，启动服务调用资源文件的时候，报错信息如下所示。从报错信息中，得知是在 Python3 程序执行 read_file() 读取文件的内容，给文件加锁的时候报错了。但是奇怪的是，本地调试的时候发现服务都是可以正常运行的，文件加锁也是没问题的。后来发现，在 openshift 集群中使用的是 NFS 挂 载的共享磁盘。\n1 2 3 4 5 6 7 8 9 10 11 12 # 报错信息 Traceback (most recent call last): ...... File \u0026#34;xxx/utils/storage.py\u0026#34;, line 34, in xxx.utils.storage.LocalStorage.read_file OSError: [Errno 9] Bad file descriptor # 文件加锁代码 ... with open(self.mount(path), \u0026#39;rb\u0026#39;) as fileobj: fcntl.flock(fileobj, fcntl.LOCK_EX) data = fileobj.read() return data ... 解决方法：从下面的信息得知，要在 Linux 中使用 flock() 的话，就需要升级内核版本到 2.6.11+ 才行。后来才发现，这实际上是由 RedHat 內核中的一个错误引起的，并在 kernel-3.10.0-693.18.1.el7 版本中得到修复。所以对于 NFSv3 和 NFSv4 服务而已，就需要升级 Linux 内核版本才能够解决这个问题。\n1 2 3 4 # https://t.codebug.vip/questions-930901.htm $ In Linux kernels up to 2.6.11, flock() does not lock files over NFS (i.e., the scope of locks was limited to the local system). [...] Since Linux 2.6.12, NFS clients support flock() locks by emulating them as byte-range locks on the entire file. 11.Docker 默认使用网段 启动的容器网络无法相互通信，很是奇怪！\n问题起因：我们在使用 Docker 启动服务的时候，发现有时候服务之前可以相互连通，而有时间启动的多个服务之前却出现了无法访问的情况。究其原因，发现原来是因为使用的内部私有地址网段不一致导致的。有点服务启动到了 172.17 - 172.31 的网段，有的服务跑到了 192.169.0 - 192.168.224 的网段，这样导致服务启动之后出现无法访问的情况。\n解决方法：上述问题的处理方式，就是手动指定 Docker 服务的启动网段，就可以了。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 查看docker容器配置 $ cat /etc/docker/daemon.json { \u0026#34;registry-mirrors\u0026#34;: [\u0026#34;https://vec0xydj.mirror.aliyuncs.com\u0026#34;], \u0026#34;default-address-pools\u0026#34;:[{\u0026#34;base\u0026#34;:\u0026#34;172.17.0.0/12\u0026#34;,\u0026#34;size\u0026#34;:24}], \u0026#34;experimental\u0026#34;: true, \u0026#34;default-runtime\u0026#34;: \u0026#34;nvidia\u0026#34;, \u0026#34;live-restore\u0026#34;: true, \u0026#34;runtimes\u0026#34;: { \u0026#34;nvidia\u0026#34;: { \u0026#34;path\u0026#34;: \u0026#34;/usr/bin/nvidia-container-runtime\u0026#34;, \u0026#34;runtimeArgs\u0026#34;: [] } } } 12.Docker 服务启动串台 使用 docker-compose 命令各自启动两组服务，发现服务会串台！\n问题起因：在两个不同名称的目录目录下面，使用 docker-compose 来启动服务，发现当 A 组服务启动完毕之后，再启动 B 组服务的时候，发现 A 组当中对应的一部分服务又重新启动了一次，这就非常奇怪了！因为这个问题的存在会导致，A 组服务和 B 组服务无法同时启动。之前还以为是工具的 Bug，后来请教了“上峰”，才知道了原因，恍然大悟。\n1 2 3 # 服务目录结构如下所示 A: /data1/app/docker-compose.yml B: /data2/app/docker-compose.yml 解决方法：发现 A 和 B 两组服务会串台的原因，原来是 docker-compose 会给启动的容器加 label 标签，然后根据这些 label 标签来识别和判断对应的容器服务是由谁启动的、谁来管理的，等等。而这里，我们需要关注的 label 变量是 com.docker.compose.project，其对应的值是使用启动配置文件的目录的最底层子目录名称，即上面的 app 就是对应的值。我们可以发现， A 和 B 两组服务对应的值都是 app，所以启动的时候被认为是同一个，这就出现了上述的问题。如果需要深入了解的话，可以去看对应源代码。\n1 2 3 4 5 6 # 可以将目录结构调整为如下所示 A: /data/app1/docker-compose.yml B: /data/app2/docker-compose.yml A: /data1/app-old/docker-compose.yml B: /data2/app-new/docker-compose.yml 或者使用 docker-compose 命令提供的参数 -p 来规避该问题的发生。\n1 2 # 指定项目项目名称 $ docker-compose -f ./docker-compose.yml -p app1 up -d 13.Docker 命令调用报错 在编写脚本的时候常常会执行 docker 相关的命令，但是需要注意使用细节！\n问题起因：CI 更新环境执行了一个脚本，但是脚本执行过程中报错了，如下所示。通过对应的输出信息，可以看到提示说正在执行的设备不是一个 tty。\n随即，查看了脚本发现报错地方是执行了一个 exec 的 docker 命令，大致如下所示。很奇怪的是，手动执行或直接调脚本的时候，怎么都是没有问题的，但是等到 CI 调用的时候怎么都是有问题。后来好好看下下面这个命令，注意到 -it 这个参数了。\n1 2 3 4 5 6 7 # 脚本调用docker命令 docker exec -it \u0026lt;container_name\u0026gt; psql -Upostgres ...... 我们可以一起看下 exec 命令的这两个参数，自然就差不多理解了。 -i/-interactive #即使没有附加也保持 STDIN 打开；如果你需要执行命令则需要开启这个选项 -t/–tty #分配一个伪终端进行执行；一个连接用户的终端与容器 stdin 和 stdout 的桥梁 解决方法：docker exec 的参数 -t 是指 Allocate a pseudo-TTY 的意思，而 CI 在执行 job 的时候并不是在 TTY 终端中执行，所以 -t 这个参数会报错。\n14.Docker 定时任务异常 在 Crontab 定时任务中也存在 Docker 命令执行异常的情况！\n问题起因：今天发现了一个问题，就是在备份 Mysql 数据库的时候，使用 docker 容器进行备份，然后使用 Crontab 定时任务来触发备份。但是发现备份的 MySQL 数据库居然是空的，但是手动执行对应命令切是好的，很奇怪。\n1 2 3 4 # Crontab定时任务 0 */6 * * * \\ docker exec -it \u0026lt;container_name\u0026gt; sh -c \\ \u0026#39;exec mysqldump --all-databases -uroot -ppassword ......\u0026#39; 解决方法：后来发现是因为执行的 docker 命令多个 -i 导致的。因为 Crontab 命令执行的时候，并不是交互式的，所以需要把这个去掉才可以。总结就是，如果你需要回显的话则需要 -t 选项，如果需要交互式会话则需要 -i 选项。\n1 2 -i/-interactive #即使没有附加也保持 STDIN 打开；如果你需要执行命令则需要开启这个选项 -t/–tty #分配一个伪终端进行执行；一个连接用户的终端与容器 stdin 和 stdout 的桥梁 15.Docker 变量使用引号 compose 里边环境变量带不带引号的问题！\n问题起因：使用过 compose 的同学可能都遇到过，我们在编写启动配置文件的时候，添加环境变量的时候到底是使用单引号、双引号还是不使用引号。时间长了，可能我们总是三者是一样的，可以相互使用。但是，直到最后我们发现坑越来越多，越来越隐晦。\n反正我是遇到过很多是因为添加引号导致的服务启动问题，后来得出的结论就是一律不适用引号。裸奔，体验前所未有的爽快！直到现在看到了 Github 中对应的 issus 之后，才终于破案了。\n1 2 3 4 5 6 7 8 # TESTVAR=\u0026#34;test\u0026#34; 在Compose中进行引用TESTVAR变量，无法找到 # TESTVAR=test 在Compose中进行引用TESTVAR变量，可以找到 # docker run -it --rm -e TESTVAR=\u0026#34;test\u0026#34; test:latest 后来发现docker本身其实已经正确地处理了引号的使用 解决方法：得到的结论就是，因为 Compose 解析 yaml 配置文件，发现引号也进行了解释包装。这就导致原本的 TESTVAR=\u0026ldquo;test\u0026rdquo; 被解析成了 \u0026lsquo;TESTVAR=\u0026ldquo;test\u0026rdquo;\u0026rsquo;，所以我们在引用的时候就无法获取到对应的值。现在解决方法就是，不管是我们直接在配置文件添加环境变量或者使用 env_file 配置文件，能不使用引号就不适用引号。\n16. Docker 删除镜像报错 无法删除镜像，归根到底还是有地方用到了！\n问题起因：清理服器磁盘空间的时候，删除某个镜像的时候提示如下信息。提示需要强制删除，但是发现及时执行了强制删除依旧没有效果。\n1 2 3 4 5 6 7 # 删除镜像 $ docker rmi 3ccxxxx2e862 Error response from daemon: conflict: unable to delete 3ccxxxx2e862 (cannot be forced) - image has dependent child images # 强制删除 $ dcoker rmi -f 3ccxxxx2e862 Error response from daemon: conflict: unable to delete 3ccxxxx2e862 (cannot be forced) - image has dependent child images 解决方法：后来才发现，出现这个原因主要是因为 TAG，即存在其他镜像引用了这个镜像。这里我们可以使用如下命令查看对应镜像文件的依赖关系，然后根据对应 TAG 来删除镜像。\n1 2 3 4 5 6 7 8 # 查询依赖 - image_id表示镜像名称 $ docker image inspect --format=\u0026#39;{{.RepoTags}} {{.Id}} {{.Parent}}\u0026#39; $(docker image ls -q --filter since=\u0026lt;image_id\u0026gt;) # 根据TAG删除镜像 $ docker rmi -f c565xxxxc87f bash # 删除悬空镜像 $ docker rmi $(docker images --filter \u0026#34;dangling=true\u0026#34; -q --no-trunc) 17.Docker 普通用户切换 切换 Docker 启动用户的话，还是需要注意下权限问题的！\n问题起因：我们都知道在 Docker 容器里面使用 root 用户的话，是不安全的，很容易出现越权的安全问题，所以一般情况下，我们都会使用普通用户来代替 root 进行服务的启动和管理的。今天给一个服务切换用户的时候，发现 Nginx 服务一直无法启动，提示如下权限问题。因为对应的配置文件也没有配置 var 相关的目录，无奈 🤷‍♀ ！️\n1 2 3 # Nginx报错信息 nginx: [alert] could not open error log file: open() \u0026#34;/var/log/nginx/error.log\u0026#34; failed (13: Permission denied) 2020/11/12 15:25:47 [emerg] 23#23: mkdir() \u0026#34;/var/cache/nginx/client_temp\u0026#34; failed (13: Permission denied) 解决方法：后来发现还是 nginx.conf 配置文件，配置的有问题，需要将 Nginx 服务启动时候需要的文件都配置到一个无权限的目录，即可解决。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 nginx user www-data; worker_processes 1; error_log /data/logs/master_error.log warn; pid /dev/shm/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; gzip on; sendfile on; tcp_nopush on; keepalive_timeout 65; client_body_temp_path /tmp/client_body; fastcgi_temp_path /tmp/fastcgi_temp; proxy_temp_path /tmp/proxy_temp; scgi_temp_path /tmp/scgi_temp; uwsgi_temp_path /tmp/uwsgi_temp; include /etc/nginx/conf.d/*.conf; } 18. Docker 不稳定 通过实践，我发现 Docker 还是挺容易挂的，尤其是长时间跑高之后。为了保证 Docker 服务的持续运行，除了要让 Docker 开机自启动之外，还需要对 Docker 服务进行监控，一旦发现服务挂了就马上重启服务。\n可以通过一条简单的 crontab 定时任务解决：\n1 2 # 适用于 CentOS 7，如果 Docker 正在服务，不会产生负面影响 * * * * * systemctl start docker 19.定期清理 时间长了，宿主机会有很多不需要的镜像、停止的容器等，如果有需要，同样可以通过定时任务进行清理。\n1 2 3 4 # 每天凌晨 2 点清理容器和镜像 0 2 * * * docker container prune --force \u0026amp;\u0026amp; docker image prune --force # 更凶残地方式 0 2 * * * docker system prune --force 20.yum源报错 CentOS镜像-CentOS镜像下载安装-开源镜像站-阿里云\n问题提出：\ncannot find a valid baseurl for repo:base/7/x86_64的解决方案 报错说明：\n出现cannot find a valid baseurl for repo:base/7/x86_64错误通常是由于YUM仓库源无法找到或无法访问，导致YUM无法正常工作。这种情况常见于CentOS 7系统。解决这个问题需要检查几个方面，如网络连接、DNS设置和YUM仓库源配置。以下是详细的排查解决方法。\n检查网络连接 可以通过以下命令检查系统是否能访问外部网站：\n1 ping -c 4 google.com 如果不能ping通，可能是网络配置问题。你需要确保网络连接正常，可能需要重新启动网络服务：\n1 systemctl restart network 方法二：检查DNS设置 如果你的网络连接正常但依然不能访问仓库，可能是DNS问题。\n更新DNS配置 编辑/etc/resolv.conf文件，确保其中包含有效的DNS服务器，例如Google的公共DNS：\n1 vi /etc/resolv.conf 添加以下行：\n1 2 nameserver 8.8.8.8 nameserver 8.8.4.4 保存文件并退出。\n检查是否能解析域名 再次检查系统是否能解析域名：\n1 ping -c 4 google.com 方法三：检查YUM仓库配置 如果网络连接和DNS设置都正常，可能是YUM仓库配置有问题，需要检查并更新YUM仓库源。\n更新YUM仓库源 备份现有的YUM配置文件：\n1 cp -r /etc/yum.repos.d /etc/yum.repos.d.backup 编辑或替换仓库配置文件 检查/etc/yum.repos.d/CentOS-Base.repo文件。确保baseurl和gpgcheck配置正确。你可以手动编辑这个文件，或者更换为可靠的YUM仓库源。 使用阿里云或其他国内镜像源 如果你在国内，使用国内的镜像源通常可以提供更快和更稳定的访问速度。以下是如何配置阿里云镜像源：\n更新YUM仓库源为阿里云镜像源：\n1 vi /etc/yum.repos.d/CentOS-Base.repo 将内容替换为以下内容：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 [base] name=CentOS-$releasever - Base - mirrors.aliyun.com baseurl=http://mirrors.aliyun.com/centos/$releasever/os/$basearch/ gpgcheck=1 gpgkey=http://mirrors.aliyun.com/centos/RPM-GPG-KEY-CentOS-7 [updates] name=CentOS-$releasever - Updates - mirrors.aliyun.com baseurl=http://mirrors.aliyun.com/centos/$releasever/updates/$basearch/ gpgcheck=1 gpgkey=http://mirrors.aliyun.com/centos/RPM-GPG-KEY-CentOS-7 [extras] name=CentOS-$releasever - Extras - mirrors.aliyun.com baseurl=http://mirrors.aliyun.com/centos/$releasever/extras/$basearch/ gpgcheck=1 gpgkey=http://mirrors.aliyun.com/centos/RPM-GPG-KEY-CentOS-7 [centosplus] name=CentOS-$releasever - Plus - mirrors.aliyun.com baseurl=http://mirrors.aliyun.com/centos/$releasever/centosplus/$basearch/ gpgcheck=1 enabled=0 gpgkey=http://mirrors.aliyun.com/centos/RPM-GPG-KEY-CentOS-7 保存文件并退出。\n清理并重建缓存\n1 2 3 yum clean all yum makecache yum update ","permalink":"https://ktzxy.top/posts/7k38hq60mo/","summary":"Docker 常见疑难杂症解决方案","title":"Docker 常见疑难杂症解决方案"},{"content":"MySQL 数据库 SQL 示例 1. 练习示例涉及的数据库表 DDL 脚本 1.1. 普通的示例表 部门表 1 2 3 4 5 6 DROP TABLE IF EXISTS `dept`; create table dept( id int auto_increment comment \u0026#39;ID\u0026#39; primary key, name varchar(50) not null comment \u0026#39;部门名称\u0026#39; )comment \u0026#39;部门表\u0026#39;; INSERT INTO dept (id, name) VALUES (1, \u0026#39;研发部\u0026#39;), (2, \u0026#39;市场部\u0026#39;),(3, \u0026#39;财务部\u0026#39;), (4, \u0026#39;销售部\u0026#39;), (5, \u0026#39;总经办\u0026#39;), (6, \u0026#39;人事部\u0026#39;); 员工表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 DROP TABLE IF EXISTS `emp`; create table emp( id int auto_increment comment \u0026#39;ID\u0026#39; primary key, name varchar(50) not null comment \u0026#39;姓名\u0026#39;, age int comment \u0026#39;年龄\u0026#39;, job varchar(20) comment \u0026#39;职位\u0026#39;, salary int comment \u0026#39;薪资\u0026#39;, entrydate date comment \u0026#39;入职时间\u0026#39;, managerid int comment \u0026#39;直属领导ID\u0026#39;, dept_id int comment \u0026#39;部门ID\u0026#39; )comment \u0026#39;员工表\u0026#39;; INSERT INTO emp (id, name, age, job,salary, entrydate, managerid, dept_id) VALUES (1, \u0026#39;金庸\u0026#39;, 66, \u0026#39;总裁\u0026#39;,20000, \u0026#39;2000-01-01\u0026#39;, null,5), (2, \u0026#39;张无忌\u0026#39;, 20, \u0026#39;项目经理\u0026#39;,12500, \u0026#39;2005-12-05\u0026#39;, 1,1), (3, \u0026#39;杨逍\u0026#39;, 33, \u0026#39;开发\u0026#39;, 8400,\u0026#39;2000-11-03\u0026#39;, 2,1), (4, \u0026#39;韦一笑\u0026#39;, 48, \u0026#39;开发\u0026#39;,11000, \u0026#39;2002-02-05\u0026#39;, 2,1), (5, \u0026#39;常遇春\u0026#39;, 43, \u0026#39;开发\u0026#39;,10500, \u0026#39;2004-09-07\u0026#39;, 3,1), (6, \u0026#39;小昭\u0026#39;, 19, \u0026#39;程序员鼓励师\u0026#39;,6600, \u0026#39;2004-10-12\u0026#39;, 2,1), (7, \u0026#39;灭绝\u0026#39;, 60, \u0026#39;财务总监\u0026#39;,8500, \u0026#39;2002-09-12\u0026#39;, 1,3), (8, \u0026#39;周芷若\u0026#39;, 19, \u0026#39;会计\u0026#39;,48000, \u0026#39;2006-06-02\u0026#39;, 7,3), (9, \u0026#39;丁敏君\u0026#39;, 23, \u0026#39;出纳\u0026#39;,5250, \u0026#39;2009-05-13\u0026#39;, 7,3), (10, \u0026#39;赵敏\u0026#39;, 20, \u0026#39;市场部总监\u0026#39;,12500, \u0026#39;2004-10-12\u0026#39;, 1,2), (11, \u0026#39;鹿杖客\u0026#39;, 56, \u0026#39;职员\u0026#39;,3750, \u0026#39;2006-10-03\u0026#39;, 10,2), (12, \u0026#39;鹤笔翁\u0026#39;, 19, \u0026#39;职员\u0026#39;,3750, \u0026#39;2007-05-09\u0026#39;, 10,2), (13, \u0026#39;方东白\u0026#39;, 19, \u0026#39;职员\u0026#39;,5500, \u0026#39;2009-02-12\u0026#39;, 10,2), (14, \u0026#39;张三丰\u0026#39;, 88, \u0026#39;销售总监\u0026#39;,14000, \u0026#39;2004-10-12\u0026#39;, 1,4), (15, \u0026#39;俞莲舟\u0026#39;, 38, \u0026#39;销售\u0026#39;,4600, \u0026#39;2004-10-12\u0026#39;, 14,4), (16, \u0026#39;宋远桥\u0026#39;, 40, \u0026#39;销售\u0026#39;,4600, \u0026#39;2004-10-12\u0026#39;, 14,4), (17, \u0026#39;陈友谅\u0026#39;, 42, null,2000, \u0026#39;2011-10-12\u0026#39;, 1,null); 薪资等级表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 DROP TABLE IF EXISTS `salgrade`; create table salgrade( grade int, losal int, hisal int ) comment \u0026#39;薪资等级表\u0026#39;; insert into salgrade values (1,0,3000); insert into salgrade values (2,3001,5000); insert into salgrade values (3,5001,8000); insert into salgrade values (4,8001,10000); insert into salgrade values (5,10001,15000); insert into salgrade values (6,15001,20000); insert into salgrade values (7,20001,25000); insert into salgrade values (8,25001,30000); 用户表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 create table tb_user( id int primary key auto_increment comment \u0026#39;主键\u0026#39;, name varchar(50) not null comment \u0026#39;用户名\u0026#39;, phone varchar(11) not null comment \u0026#39;手机号\u0026#39;, email varchar(100) comment \u0026#39;邮箱\u0026#39;, profession varchar(11) comment \u0026#39;专业\u0026#39;, age tinyint unsigned comment \u0026#39;年龄\u0026#39;, gender char(1) comment \u0026#39;性别 , 1: 男, 2: 女\u0026#39;, status char(1) comment \u0026#39;状态\u0026#39;, createtime datetime comment \u0026#39;创建时间\u0026#39; ) comment \u0026#39;系统用户表\u0026#39;; INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;吕布\u0026#39;, \u0026#39;17799990000\u0026#39;, \u0026#39;lvbu666@163.com\u0026#39;, \u0026#39;软件工程\u0026#39;, 23, \u0026#39;1\u0026#39;, \u0026#39;6\u0026#39;, \u0026#39;2001-02-02 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;曹操\u0026#39;, \u0026#39;17799990001\u0026#39;, \u0026#39;caocao666@qq.com\u0026#39;, \u0026#39;通讯工程\u0026#39;, 33, \u0026#39;1\u0026#39;, \u0026#39;0\u0026#39;, \u0026#39;2001-03-05 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;赵云\u0026#39;, \u0026#39;17799990002\u0026#39;, \u0026#39;17799990@139.com\u0026#39;, \u0026#39;英语\u0026#39;, 34, \u0026#39;1\u0026#39;, \u0026#39;2\u0026#39;, \u0026#39;2002-03-02 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;孙悟空\u0026#39;, \u0026#39;17799990003\u0026#39;, \u0026#39;17799990@sina.com\u0026#39;, \u0026#39;工程造价\u0026#39;, 54, \u0026#39;1\u0026#39;, \u0026#39;0\u0026#39;, \u0026#39;2001-07-02 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;花木兰\u0026#39;, \u0026#39;17799990004\u0026#39;, \u0026#39;19980729@sina.com\u0026#39;, \u0026#39;软件工程\u0026#39;, 23, \u0026#39;2\u0026#39;, \u0026#39;1\u0026#39;, \u0026#39;2001-04-22 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;大乔\u0026#39;, \u0026#39;17799990005\u0026#39;, \u0026#39;daqiao666@sina.com\u0026#39;, \u0026#39;舞蹈\u0026#39;, 22, \u0026#39;2\u0026#39;, \u0026#39;0\u0026#39;, \u0026#39;2001-02-07 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;露娜\u0026#39;, \u0026#39;17799990006\u0026#39;, \u0026#39;luna_love@sina.com\u0026#39;, \u0026#39;应用数学\u0026#39;, 24, \u0026#39;2\u0026#39;, \u0026#39;0\u0026#39;, \u0026#39;2001-02-08 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;程咬金\u0026#39;, \u0026#39;17799990007\u0026#39;, \u0026#39;chengyaojin@163.com\u0026#39;, \u0026#39;化工\u0026#39;, 38, \u0026#39;1\u0026#39;, \u0026#39;5\u0026#39;, \u0026#39;2001-05-23 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;项羽\u0026#39;, \u0026#39;17799990008\u0026#39;, \u0026#39;xiaoyu666@qq.com\u0026#39;, \u0026#39;金属材料\u0026#39;, 43, \u0026#39;1\u0026#39;, \u0026#39;0\u0026#39;, \u0026#39;2001-09-18 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;白起\u0026#39;, \u0026#39;17799990009\u0026#39;, \u0026#39;baiqi666@sina.com\u0026#39;, \u0026#39;机械工程及其自动化\u0026#39;, 27, \u0026#39;1\u0026#39;, \u0026#39;2\u0026#39;, \u0026#39;2001-08-16 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;韩信\u0026#39;, \u0026#39;17799990010\u0026#39;, \u0026#39;hanxin520@163.com\u0026#39;, \u0026#39;无机非金属材料工程\u0026#39;, 27, \u0026#39;1\u0026#39;, \u0026#39;0\u0026#39;, \u0026#39;2001-06-12 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;荆轲\u0026#39;, \u0026#39;17799990011\u0026#39;, \u0026#39;jingke123@163.com\u0026#39;, \u0026#39;会计\u0026#39;, 29, \u0026#39;1\u0026#39;, \u0026#39;0\u0026#39;, \u0026#39;2001-05-11 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;兰陵王\u0026#39;, \u0026#39;17799990012\u0026#39;, \u0026#39;lanlinwang666@126.com\u0026#39;, \u0026#39;工程造价\u0026#39;, 44, \u0026#39;1\u0026#39;, \u0026#39;1\u0026#39;, \u0026#39;2001-04-09 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;狂铁\u0026#39;, \u0026#39;17799990013\u0026#39;, \u0026#39;kuangtie@sina.com\u0026#39;, \u0026#39;应用数学\u0026#39;, 43, \u0026#39;1\u0026#39;, \u0026#39;2\u0026#39;, \u0026#39;2001-04-10 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;貂蝉\u0026#39;, \u0026#39;17799990014\u0026#39;, \u0026#39;84958948374@qq.com\u0026#39;, \u0026#39;软件工程\u0026#39;, 40, \u0026#39;2\u0026#39;, \u0026#39;3\u0026#39;, \u0026#39;2001-02-12 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;妲己\u0026#39;, \u0026#39;17799990015\u0026#39;, \u0026#39;2783238293@qq.com\u0026#39;, \u0026#39;软件工程\u0026#39;, 31, \u0026#39;2\u0026#39;, \u0026#39;0\u0026#39;, \u0026#39;2001-01-30 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;芈月\u0026#39;, \u0026#39;17799990016\u0026#39;, \u0026#39;xiaomin2001@sina.com\u0026#39;, \u0026#39;工业经济\u0026#39;, 35, \u0026#39;2\u0026#39;, \u0026#39;0\u0026#39;, \u0026#39;2000-05-03 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;嬴政\u0026#39;, \u0026#39;17799990017\u0026#39;, \u0026#39;8839434342@qq.com\u0026#39;, \u0026#39;化工\u0026#39;, 38, \u0026#39;1\u0026#39;, \u0026#39;1\u0026#39;, \u0026#39;2001-08-08 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;狄仁杰\u0026#39;, \u0026#39;17799990018\u0026#39;, \u0026#39;jujiamlm8166@163.com\u0026#39;, \u0026#39;国际贸易\u0026#39;, 30, \u0026#39;1\u0026#39;, \u0026#39;0\u0026#39;, \u0026#39;2007-03-12 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;安琪拉\u0026#39;, \u0026#39;17799990019\u0026#39;, \u0026#39;jdodm1h@126.com\u0026#39;, \u0026#39;城市规划\u0026#39;, 51, \u0026#39;2\u0026#39;, \u0026#39;0\u0026#39;, \u0026#39;2001-08-15 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;典韦\u0026#39;, \u0026#39;17799990020\u0026#39;, \u0026#39;ycaunanjian@163.com\u0026#39;, \u0026#39;城市规划\u0026#39;, 52, \u0026#39;1\u0026#39;, \u0026#39;2\u0026#39;, \u0026#39;2000-04-12 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;廉颇\u0026#39;, \u0026#39;17799990021\u0026#39;, \u0026#39;lianpo321@126.com\u0026#39;, \u0026#39;土木工程\u0026#39;, 19, \u0026#39;1\u0026#39;, \u0026#39;3\u0026#39;, \u0026#39;2002-07-18 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;后羿\u0026#39;, \u0026#39;17799990022\u0026#39;, \u0026#39;altycj2000@139.com\u0026#39;, \u0026#39;城市园林\u0026#39;, 20, \u0026#39;1\u0026#39;, \u0026#39;0\u0026#39;, \u0026#39;2002-03-10 00:00:00\u0026#39;); INSERT INTO tempdb.tb_user (name, phone, email, profession, age, gender, status, createtime) VALUES (\u0026#39;姜子牙\u0026#39;, \u0026#39;17799990023\u0026#39;, \u0026#39;37483844@qq.com\u0026#39;, \u0026#39;工程造价\u0026#39;, 29, \u0026#39;1\u0026#39;, \u0026#39;4\u0026#39;, \u0026#39;2003-05-26 00:00:00\u0026#39;); 1.2. 多对多关系示例表 多对多关系(学生表、课程表、学生课程中间表) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 DROP TABLE IF EXISTS `student`; create table student( id int auto_increment primary key comment \u0026#39;主键ID\u0026#39;, name varchar(10) comment \u0026#39;姓名\u0026#39;, no varchar(10) comment \u0026#39;学号\u0026#39; ) comment \u0026#39;学生表\u0026#39;; insert into student values (null, \u0026#39;黛绮丝\u0026#39;, \u0026#39;2000100101\u0026#39;),(null, \u0026#39;谢逊\u0026#39;, \u0026#39;2000100102\u0026#39;),(null, \u0026#39;殷天正\u0026#39;, \u0026#39;2000100103\u0026#39;),(null, \u0026#39;韦一笑\u0026#39;, \u0026#39;2000100104\u0026#39;); DROP TABLE IF EXISTS `course`; create table course( id int auto_increment primary key comment \u0026#39;主键ID\u0026#39;, name varchar(10) comment \u0026#39;课程名称\u0026#39; ) comment \u0026#39;课程表\u0026#39;; insert into course values (null, \u0026#39;Java\u0026#39;), (null, \u0026#39;PHP\u0026#39;), (null , \u0026#39;MySQL\u0026#39;) , (null, \u0026#39;Hadoop\u0026#39;); DROP TABLE IF EXISTS `student_course`; create table student_course( id int auto_increment comment \u0026#39;主键\u0026#39; primary key, studentid int not null comment \u0026#39;学生ID\u0026#39;, courseid int not null comment \u0026#39;课程ID\u0026#39;, constraint fk_courseid foreign key (courseid) references course (id), constraint fk_studentid foreign key (studentid) references student (id) )comment \u0026#39;学生课程中间表\u0026#39;; insert into student_course values (null,1,1),(null,1,2),(null,1,3),(null,2,2),(null,2,3),(null,3,4); 1.3. 表数据约束示例表 用户表（约束） 1 2 3 4 5 6 7 8 DROP TABLE IF EXISTS `user`; create table user( id int primary key auto_increment comment \u0026#39;主键\u0026#39;, name varchar(10) not null unique comment \u0026#39;姓名\u0026#39;, age int check ( age \u0026gt; 0 \u0026amp;\u0026amp; age \u0026lt;= 120 ) comment \u0026#39;年龄\u0026#39;, status char(1) default \u0026#39;1\u0026#39; comment \u0026#39;状态\u0026#39;, gender char(1) comment \u0026#39;性别\u0026#39; ) comment \u0026#39;用户表\u0026#39;; 部门表、员工表（外键约束） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 create table dept( id int auto_increment comment \u0026#39;ID\u0026#39; primary key, name varchar(50) not null comment \u0026#39;部门名称\u0026#39; )comment \u0026#39;部门表\u0026#39;; INSERT INTO dept (id, name) VALUES (1, \u0026#39;研发部\u0026#39;), (2, \u0026#39;市场部\u0026#39;),(3, \u0026#39;财务部\u0026#39;), (4, \u0026#39;销售部\u0026#39;), (5, \u0026#39;总经办\u0026#39;); create table emp( id int auto_increment comment \u0026#39;ID\u0026#39; primary key, name varchar(50) not null comment \u0026#39;姓名\u0026#39;, age int comment \u0026#39;年龄\u0026#39;, job varchar(20) comment \u0026#39;职位\u0026#39;, salary int comment \u0026#39;薪资\u0026#39;, entrydate date comment \u0026#39;入职时间\u0026#39;, managerid int comment \u0026#39;直属领导ID\u0026#39;, dept_id int comment \u0026#39;部门ID\u0026#39; )comment \u0026#39;员工表\u0026#39;; INSERT INTO emp (id, name, age, job,salary, entrydate, managerid, dept_id) VALUES (1, \u0026#39;金庸\u0026#39;, 66, \u0026#39;总裁\u0026#39;,20000, \u0026#39;2000-01-01\u0026#39;, null,5),(2, \u0026#39;张无忌\u0026#39;, 20, \u0026#39;项目经理\u0026#39;,12500, \u0026#39;2005-12-05\u0026#39;, 1,1), (3, \u0026#39;杨逍\u0026#39;, 33, \u0026#39;开发\u0026#39;, 8400,\u0026#39;2000-11-03\u0026#39;, 2,1),(4, \u0026#39;韦一笑\u0026#39;, 48, \u0026#39;开发\u0026#39;,11000, \u0026#39;2002-02-05\u0026#39;, 2,1), (5, \u0026#39;常遇春\u0026#39;, 43, \u0026#39;开发\u0026#39;,10500, \u0026#39;2004-09-07\u0026#39;, 3,1),(6, \u0026#39;小昭\u0026#39;, 19, \u0026#39;程序员鼓励师\u0026#39;,6600, \u0026#39;2004-10-12\u0026#39;, 2,1); ","permalink":"https://ktzxy.top/posts/kbestqe73n/","summary":"MySQL SQL示例","title":"MySQL SQL示例"},{"content":"计算机网络分层结构 - 物理层 两种分层结构 OSI体系结构 应用层 表示层 会话层 运输层 网络层 数据链路层 物理层 TCP/IP体系结构 应用层 运输层（TCP、UDP） 网络层（IP） 数据链路层 物理层 物理层 基本概念 物理层解决如何在连接各种计算机的传输媒体上传输数据比特流，而不是指具体的传输媒体，尽可能的屏蔽掉传输媒体和通信手段的差异。\n物理层的主要任务描述为：确定传输媒体的接口的一些特性\n机械特性：接口形状，引线数目 电气特性：规定电压的范围（-5V 到 +5V） 功能特性：例：规定 -5V表示0，+5V表示1 过程特性：各个相关部件的工作步骤 数据通信模型 与通信相关的术语 数据：运送消息的实体 信号：数据电器或电磁的表现 模拟信号：代表消息的参数的取值是连续的 数字信息：代表消息的参数的取值是离散的 码元：在使用时间域的波形表示数字信号时，则代表不同离散数值的基本波形就成为了码元 有关信道的基本概念 信道一般表示向一个方向上传递信息的媒体，我们平时说的通信线路往往包含一条发送信息的信道和一个接受信息的信道\n单工通道：只有一个方向上的通信，而没有反方向的交互 半双工通信：不能同时发送，只能一个发送，一个接受（例如：对讲机） 全双工通信：通信双方可以同时发送和接受（例如：手机） 基带信号和带通信号 基带信号：来自信源的信号 带通信号：是通过将基带信号进行载波调制后，把信号的频率搬到较高的频段 对基带信号的几种调制方式：调幅、调频、调相\n曼切斯特编码 利用曼彻斯特编码，一个时钟周期只可以表示一个bit，并且必须通过两次采样才能得到一个bit，但它能携带时钟信号，且可表示没有数据传输\n差分曼彻斯特编码 bit中间有信号跳变，bit与bit之间也有信号跳变，表示下一个bit为 0 bit中间有信号跳变，bit与bit之间没有信号跳变，表示下一个bit为 1 差分曼切斯特编码和曼彻斯特编码相同，但是抗干扰性强于曼切斯特编码，例如\n信道极限容量 奈氏准则 任何信道中，码元的传输速率是有上限的，否则就会出现码间串扰的问题，使得接收端对码元的判决成为不可能。\n信噪比：香农定理 香农用信息论的理论，推导出带宽首先且有高斯白噪声干扰的信道的极限，无差错的信息传输速率\n信道的极限信息传输传输速度 $c$ 可以表示为 $$ c=w \\log _{2}(1 + s / N) \\quad b / s (有噪声干扰的传输速率) $$ 其中：\nw：信道的带宽，以HZ为单位 S：信道内所传信号的平均功率 N：信道内部的高斯噪声功率 结论：香农公式表明，信道的带宽或信道内 的信噪比越大，则信息的极限传输速率就越高。\n物理层下面的传输媒体 导向传输媒体：导向传输媒体中，电磁波沿着固体媒体传播 屏蔽双绞线：STP 非屏蔽双绞线：UTP 同轴电缆：有线电视 光缆 非导向传播媒体：无线传输 其中，我们常常说的网线，就是双绞线。\n10M 和 100M带宽的网络，只需要使用 1,2,3,6 四根线\n而 1000M带宽的网络，8根线都需要使用\n信道复用技术 信道复用指的就是多个用户共用同一信道进行传输\n频分复用 用户 分配到一定的频带后，在通信过程中自始至终都占用这个频带，频分复用的所有用户在同样的时间，占用不同的带宽资源\n时分复用 时分复用，指的是每个数据占用一个时间片，时分复用可能会导致线路资源的浪费\n波分复用 波分复用就是光的频分复用\n数字传输系统 主要用于广域网上的传输，脉码调制PCM体制最初是为了在电话局之间的中继线上传送多路的电话，由于历史的原因，PCM有两个互不兼容的国际标准：\n欧洲标准：（EI），我国目前采用广度 北美标准：（TI） 宽带接入技术 XDSL：用数字技术对现有的模拟电话用户线进行改造 电话信号频率：300~3400HZ 然后把更高频率用于用户上网，使用的就是频分复用技术 DMT技术：把更多的信道用于下行，少数信道用户上行 这是因为上传流量远远小于下载流量 ","permalink":"https://ktzxy.top/posts/m5e84siilf/","summary":"物理层","title":"物理层"},{"content":"1. MongoDb 概述 MongoDB 是一个跨平台的，面向文档的数据库，是当前 NoSQL 数据库产品中最热门的一种。它介于关系数据库和非关系数据库之间，是非关系数据库当中功能最丰富，最像关系数据库的产品。它支持的数据结构非常松散，是类似 JSON 的 BSON 格式，因此可以存储比较复杂的数据类型。\n1.1. MongoDB 特点 MongoDB 最大的特点是他支持的查询语言非常强大，其语法有点类似于面向对象的查询语言，几乎可以实现类似关系数据库单表查询的绝大部分功能，而且还支持对数据建立索引。它是一个面向集合的，模式自由的文档型数据库，具体特点总结如下：\n面向集合存储，易于存储对象类型的数据 模式自由 支持动态查询 支持完全索引，包含内部对象 支持复制和故障恢复 使用高效的二进制数据存储，包括大型对象（如视频等） 自动处理碎片，以支持云计算层次的扩展性 支持 Python，PHP，Ruby，Java，C，C#，Javascript，Perl 及 C++语言的驱动程序，社区中也提供了对 Erlang 及.NET 等平台的驱动程序 文件存储格式为 BSON（一种 JSON 的扩展） 1.2. 功能 面向集合的存储：适合存储对象及 JSON 形式的数据。 动态查询：Mongo 支持丰富的查询表达式。查询指令使用 JSON 形式的标记，可轻易查询文档中内嵌的对象及数组。 完整的索引支持：包括文档内嵌对象及数组。Mongo 的查询优化器会分析查询表达式，并生成一个高效的查询计划。 查询监视：Mongo 包含一个监视工具用于分析数据库操作的性能。 复制及自动故障转移：Mongo 数据库支持服务器之间的数据复制，支持主-从模式及服务器之间的相互复制。复制的主要目标是提供冗余及自动故障转移。 高效的传统存储方式：支持二进制数据及大型对象（如照片或图片） 自动分片以支持云级别的伸缩性：自动分片功能支持水平的数据库集群，可动态添加额外的机器 1.3. 适用场景 网站数据：Mongo 非常适合实时的插入，更新与查询，并具备网站实时数据存储所需的复制及高度伸缩性。 缓存：由于性能很高，Mongo 也适合作为信息基础设施的缓存层。在系统重启之后，由 Mongo 搭建的持久化缓存层可以避免下层的数据源过载。 大尺寸，低价值的数据：使用传统的关系型数据库存储一些数据时可能会比较昂贵，在此之前，很多时候程序员往往会选择传统的文件进行存储。 高伸缩性的场景：Mongo 非常适合由数十或数百台服务器组成的数据库。Mongo 的路线图中已经包含对 MapReduce 引擎的内置支持。 用于对象及 JSON 数据的存储：Mongo 的 BSON 数据格式非常适合文档化格式的存储及查询。 2. MongoDB 体系结构 2.1. 数据库的整体结构 MongoDB 的逻辑结构是一种层次结构。主要由：数据库(database)、集合(collection)、文档(document)这三部分组成的。逻辑结构是面向用户的，用户使用 MongoDB 开发应用程序使用的就是逻辑结构。\nMongoDB 的文档（document），相当于关系数据库中的一行记录。 多个文档组成一个集合（collection），相当于关系数据库的表。 多个集合（collection），逻辑上组织在一起，就是数据库（database）。 一个 MongoDB 实例支持多个数据库（database）。 数据库(database)、集合(collection)、文档(document)的层次结构如下图\n在 mongodb 中是通过数据库、集合、文档的方式来管理数据，下边是 mongodb 与关系数据库的一些概念对比：\nSQL 术语/概念 MongoDB 术语/概念 解释/说明 database database 数据库 table collection 数据库表/集合 row document 数据记录行/文档 column field 数据字段/域 index index 索引 table joins \\ 表连接（MongoDB 不支持） primary key primary key 主键。MongoDB 自动在每个集合中添加名称为\\_id的主键 一个 mongodb 实例可以创建多个数据库 一个数据库可以创建多个集合 一个集合可以包括多个文档。 Notes: MongoDB 的文件单个大小不超过 4M ，新版本后可提升到 16M\n2.2. 存储对象的结构 MongoDB 中存储的对象是 BSON，是一种类似 JSON 的二进制文件，它是由许多的键值对组成。例如：\n1 2 3 4 5 6 7 8 9 10 { \u0026#34;name\u0026#34; : \u0026#34;MooN\u0026#34;, \u0026#34;age\u0026#34; : 20, \u0026#34;sex\u0026#34; : \u0026#34;male\u0026#34; } { \u0026#34;name\u0026#34; : \u0026#34;jack\u0026#34;, \u0026#34;class\u0026#34; : 3, \u0026#34;grade\u0026#34; : 3 } 2.3. MongoDB 中的 key 命名规则 \\0 不能使用。 带有 . 号、_ 号和 $ 号前缀的 Key 被保留，不能使用。 大小写有区别，Age 与 age 是不同的 key。 同一个文档不能有相同的 Key。 除了上面几条规则外，其他所有 UTF-8 字符都可以使用。 3. 连接 mongodb mongodb 的使用方式是客户服务器模式，即使用一个客户端连接 mongodb 数据库（服务端）。\n3.1. 命令格式连接 1 mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]] 参数解释：\nmongodb:// 固定前缀 username：账号，可不填 password：密码，可不填 host：主机名或 ip 地址，只有 host 主机名为必填项。 port：端口，可不填，默认 27017 /database：连接某一个数据库 ?options：连接参数，key/value 对 例子：\n1 2 3 mongodb://localhost # 连接本地数据库27017端口 mongodb://root:123456@localhost # 使用用户名root密码为123456连接本地数据库27017端口 mongodb://localhost,localhost:27018,localhost:27019 # 连接三台主从服务器，端口为27017、27018、27019 3.2. 使用 mongodb 自带的 javascript shell（mongo.exe）连接 windows 版本的 mongodb 安装成功，在安装目录下的 bin 目录有 mongo.exe 客户端程序。进入 cmd 窗口执行 mongo.exe：\n此时就可以输入命令来操作 mongodb 数据库了，javascript shell 可以运行 javascript 程序。\n3.3. 使用 studio3T 连接 内容参考本笔记的《MongoDB 安装与使用》的《studio3t》章节\n3.4. 使用 java 程序连接 详细参数参考：http://mongodb.github.io/mongo-java-driver/3.4/driver/tutorials/connect-to-mongodb/\n添加依赖：\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.mongodb\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mongo‐java‐driver\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.4.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 测试程序：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void testConnection() { //创建mongodb 客户端 MongoClient mongoClient = new MongoClient(\u0026#34;localhost\u0026#34;, 27017); //或者采用连接字符串 //MongoClientURI connectionString = new MongoClientURI(\u0026#34;mongodb://root:root@localhost:27017\u0026#34;); //MongoClient mongoClient = new MongoClient(connectionString); //连接数据库 MongoDatabase database = mongoClient.getDatabase(\u0026#34;test\u0026#34;); // 连接collection MongoCollection\u0026lt;Document\u0026gt; collection = database.getCollection(\u0026#34;student\u0026#34;); // 查询第一个文档 Document myDoc = collection.find().first(); //得到文件内容 json串 String json = myDoc.toJson(); System.out.println(json); } 4. 数据库操作 4.1. 查询数据库 查询全部数据库 1 show dbs 显示当前数据库 1 db 4.2. 创建/切换数据库 命令格式： 1 use DATABASE_NAME 示例：有 test02 数据库则切换到此数据库，没有则创建。 1 use test02 注意：新创建的数据库不显示，需要至少包括一个集合。\n4.3. 删除数据库（慎用！！！） 命令格式：\n1 db.dropDatabase() 例子：\n1 2 3 4 5 # 删除test02数据库 # 先切换数据库 use test02 # 再执行删除 db.dropDatabase() 5. 集合操作 集合相当于关系数据库中的表，一个数据库可以创建多个集合，一个集合是将相同类型的文档管理起来。\n5.1. 创建集合 语法格式：\n1 db.createCollection(name, options) 参数说明：\nname: 新创建的集合名称 options: 创建参数 5.2. 删除集合 语法格式：\n1 db.集合名称.drop() 例子：删除 student 集合\n1 db.student.drop() 5.3. 显示当前数据库中集合 语法格式：\n1 show tables 5.4. 给集合创建索引 5.4.1. 命令语句创建 TODO: 待整理\n5.4.2. 使用 studio 3t 软件创建 右键点击需要增加索引的集合 选择索引相应的域 生成唯一索引 6. 文档操作 6.1. 插入文档 mongodb 中文档的格式是 json 格式，下边就是一个文档，包括两个 key：_id 主键和 name\n1 2 3 4 { \u0026#34;_id\u0026#34; : ObjectId(\u0026#34;5b2cc4bfa6a44812707739b5\u0026#34;), \u0026#34;name\u0026#34; : \u0026#34;moon\u0026#34; } 插入命令格式： 1 db.COLLECTION_NAME.insert(document) 注：每个文档默认以 _id 作为主键，主键默认类型为 ObjectId（对象类型），mongodb 会自动生成主键值。\n例子：往 student 集合中插入一条文档 1 db.student.insert({\u0026#34;name\u0026#34;:\u0026#34;moon\u0026#34;,\u0026#34;age\u0026#34;:10}) 注意：同一个集合中的文档的 key 名称与个数可以不相同！但是建议设置为相同的。\n6.2. 更新文档 命令格式： 1 db.COLLECTION_NAME.update(\u0026lt;query\u0026gt;, \u0026lt;update\u0026gt;, \u0026lt;options\u0026gt;) 参数说明：\nquery：查询条件，相当于 sql 语句的 where update：更新文档内容 options：选项 6.2.1. 替换文档 将符合条件 \u0026quot;name\u0026quot;:\u0026quot;moon\u0026quot; 的第一个文档替换为 {\u0026quot;name\u0026quot;:\u0026quot;斩月\u0026quot;,\u0026quot;age\u0026quot;:10}。\n1 db.student.update({\u0026#34;name\u0026#34;:\u0026#34;moon\u0026#34;},{\u0026#34;name\u0026#34;:\u0026#34;斩月\u0026#34;,\u0026#34;age\u0026#34;:10}) 6.2.2. $set 修改器 使用 $set 修改器指定要更新的 key，key 不存在则创建，存在则更新。 将符合条件 \u0026ldquo;name\u0026rdquo;:\u0026ldquo;斩月\u0026rdquo; 的所有文档更新 name 和 age 的值。 1 db.student.update({\u0026#34;name\u0026#34;:\u0026#34;斩月\u0026#34;},{$set:{\u0026#34;name\u0026#34;:\u0026#34;天锁斩月\u0026#34;,\u0026#34;age\u0026#34;:10}},{multi:true}) 注：参数 multi：false 表示更新第一个匹配的文档，true 表示更新所有匹配的文档\n6.3. 删除文档 6.3.1. 语法格式 命令格式：\n1 db.COLLECTION_NAME.remove(\u0026lt;query\u0026gt;) 参数说明：\nquery：删除条件，相当于 sql 语句中的 where 6.3.2. 示例 删除所有文档 1 db.COLLECTION_NAME.remove({}) 删除符合条件的文档 1 db.COLLECTION_NAME.remove({\u0026#34;name\u0026#34;:\u0026#34;moon\u0026#34;}) 6.4. 查询文档 6.4.1. 语法格式 命令格式：\n1 db.COLLECTION_NAME.find(query, projection) 参数说明：\nquery：查询条件，可不填 projection：投影查询 key，可不填 6.4.2. 示例 查询全部 1 db.COLLECTION_NAME.find() 查询符合条件的记录 1 2 # 查询name等为\u0026#34;斩月\u0026#34;的文档。 db.COLLECTION_NAME.find({\u0026#34;name\u0026#34;:\u0026#34;斩月\u0026#34;}) 投影查询 1 2 # 只显示name和age两个key，_id主键不显示。 db.COLLECTION_NAME.find({\u0026#34;name\u0026#34;:\u0026#34;斩月\u0026#34;}, {name:1, age:1, _id:0}) 注：1 代表显示，0 代表不显示\n7. 用户 7.1. 创建用户 语法格式：\n1 2 3 4 5 6 7 8 9 10 11 mongo\u0026gt;db.createUser( { user: \u0026#34;\u0026lt;name\u0026gt;\u0026#34;, pwd: \u0026#34;\u0026lt;cleartext password\u0026gt;\u0026#34;, customData: { \u0026lt;any information\u0026gt; }, roles: [ { role: \u0026#34;\u0026lt;role\u0026gt;\u0026#34;, db: \u0026#34;\u0026lt;database\u0026gt;\u0026#34; } | \u0026#34;\u0026lt;role\u0026gt;\u0026#34;, ... ] } ) 例子：创建 root 用户，角色为 root\n1 2 3 4 5 6 7 8 9 10 # 切换到admin数据库 use admin # 创建用户 db.createUser( { user:\u0026#34;root\u0026#34;, pwd:\u0026#34;123\u0026#34;, roles:[{role:\u0026#34;root\u0026#34;,db:\u0026#34;admin\u0026#34;}] } ) 内置角色如下：\n数据库用户角色：read、readWrite; 数据库管理角色：dbAdmin、dbOwner、userAdmin； 集群管理角色：clusterAdmin、clusterManager、clusterMonitor、hostManager； 备份恢复角色：backup、restore； 所有数据库角色：readAnyDatabase、readWriteAnyDatabase、userAdminAnyDatabase、dbAdminAnyDatabase 超级用户角色：root 7.2. 认证登录 为了安全需要，Mongodb 要打开认证开关，即用户连接 Mongodb 要进行认证，其中就可以通过账号密码方式进行认证。\n在 mongo.conf 中设置auth=true 1 2 # 开启认证登陆 auth=true 重启 Mongodb 使用账号和密码连接数据库 1）mongo.exe 连接\n1 .\\mongo.exe -u root -p 123 --authenticationDatabase admin 2）Studio 3T 连接\n7.3. 查询用户 查询当前库下的所有用户：\n1 show users 7.4. 删除用户 语法格式： 1 db.dropUser(\u0026#34;用户名\u0026#34;) 例子：删除 test1 用户 1 db.dropUser(\u0026#34;test1\u0026#34;) 7.5. 修改用户 语法格式： 1 2 3 4 5 6 7 8 9 10 11 db.updateUser( \u0026#34;\u0026lt;username\u0026gt;\u0026#34;, { customData : { \u0026lt;any information\u0026gt; }, roles : [ { role: \u0026#34;\u0026lt;role\u0026gt;\u0026#34;, db: \u0026#34;\u0026lt;database\u0026gt;\u0026#34; } | \u0026#34;\u0026lt;role\u0026gt;\u0026#34;, ... ], pwd: \u0026#34;\u0026lt;cleartext password\u0026gt;\u0026#34; }, writeConcern: { \u0026lt;write concern\u0026gt; }) 例子： 1 2 3 4 5 6 7 8 9 10 11 12 # 先创建test1用户： db.createUser( { user:\u0026#34;test1\u0026#34;, pwd:\u0026#34;test1\u0026#34;, roles:[{role:\u0026#34;root\u0026#34;,db:\u0026#34;admin\u0026#34;}] } ) # 修改test1用户的角色为readWriteAnyDatabase use admin db.updateUser(\u0026#34;test1\u0026#34;,{roles:[{role:\u0026#34;readWriteAnyDatabase\u0026#34;,db:\u0026#34;admin\u0026#34;}]}) 7.6. 修改密码 语法格式： 1 db.changeUserPassword(\u0026#34;username\u0026#34;,\u0026#34;newPasswd\u0026#34;) 例子：修改 test1 用户的密码为 123 1 2 use admin db.changeUserPassword(\u0026#34;test1\u0026#34;,\u0026#34;123\u0026#34;) ","permalink":"https://ktzxy.top/posts/fhrj0q4ypi/","summary":"MongoDB 基础","title":"MongoDB 基础"},{"content":"1、SpringMVC 1.1、SpringMVC简介 1.1.1、什么是MVC MVC是一种软件架构的思想，将软件按照模型、视图、控制器来划分\nM：Model，模型层，指工程中的JavaBean，作用是处理数据\nJavaBean分为两类：\n一类称为实体类Bean：专门存储业务数据的，如 Student、User 等 一类称为业务处理 Bean：指 Service 或 Dao 对象，专门用于处理业务逻辑和数据访问。 V：View，视图层，指工程中的html或jsp等页面，作用是与用户进行交互，展示数据\nC：Controller，控制层，指工程中的servlet，作用是接收请求和响应浏览器\nMVC的工作流程： 用户通过视图层发送请求到服务器，在服务器中请求被Controller接收，Controller\n调用相应的Model层处理请求，处理完毕将结果返回到Controller，Controller再根据请求处理的结果\n找到相应的View视图，渲染数据后最终响应给浏览器\n1.2、什么是SpringMVC SpringMVC是Spring的一个后续产品，是Spring的一个子项目\nSpringMVC 是 Spring 为表述层开发提供的一整套完备的解决方案。在表述层框架历经 Strust、\nWebWork、Strust2 等诸多产品的历代更迭之后，目前业界普遍选择了 SpringMVC 作为 Java EE 项目\n表述层开发的首选方案。\n注：三层架构分为表述层（或表示层）、业务逻辑层、数据访问层，表述层表示前台页面和后台\nservlet\n1.3、SpringMVC的特点 Spring 家族原生产品，与 IOC 容器等基础设施无缝对接 基于原生的Servlet，通过了功能强大的前端控制器DispatcherServlet，对请求和响应进行统一 处理\n表述层各细分领域需要解决的问题全方位覆盖，提供全面解决方案 代码清新简洁，大幅度提升开发效率 内部组件化程度高，可插拔式组件即插即用，想要什么功能配置相应组件即可 性能卓著，尤其适合现代大型、超大型互联网项目要求 2、入门案例 2.1、开发环境 IDE：idea 2019.2\n构建工具：maven3.6.1\n服务器：tomcat8.5\nSpring版本：5.3.1\n2.2、创建maven工程 ①添加web模块 ②打包方式：war ③引入依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 \u0026lt;dependencies\u0026gt; \u0026lt;!-- SpringMVC --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-webmvc\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 日志 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;ch.qos.logback\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;logback-classic\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- ServletAPI --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;javax.servlet\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;javax.servlet-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.1.0\u0026lt;/version\u0026gt; \u0026lt;scope\u0026gt;provided\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spring5和Thymeleaf整合包 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.thymeleaf\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;thymeleaf-spring5\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.0.12.RELEASE\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 注：由于 Maven 的传递性，我们不必将所有需要的包全部配置依赖，而是配置最顶端的依赖，其他靠\n传递性导入。\n注意：手动创建web，在创建web.xml时，将路径添加完整\n\u0026hellip;Modules名\\src\\main\\webapp\\WEB-INF\\web.xml\n2.3、配置web.xml 注册SpringMVC的前端控制器DispatcherServlet\n①默认配置方式 此配置作用下，SpringMVC的配置文件默认位于WEB-INF下，默认名称为-\nservlet.xml，例如，以下配置所对应SpringMVC的配置文件位于WEB-INF下，文件名为springMVC\u0002\nservlet.xml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 \u0026lt;!-- 配置SpringMVC的前端控制器，对浏览器发送的请求统一进行处理 --\u0026gt; \u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;springMVC\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;org.springframework.web.servlet.DispatcherServlet\u0026lt;/servlet\u0002class\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;springMVC\u0026lt;/servlet-name\u0026gt; \u0026lt;!-- 设置springMVC的核心控制器所能处理的请求的请求路径 /所匹配的请求可以是/login或.html或.js或.css方式的请求路径 但是/不能匹配.jsp请求路径的请求 --\u0026gt; \u0026lt;url-pattern\u0026gt;/\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; ②扩展配置方式 可通过init-param标签设置SpringMVC配置文件的位置和名称，通过load-on-startup标签设置\nSpringMVC前端控制器DispatcherServlet的初始化时间\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 \u0026lt;!-- 配置SpringMVC的前端控制器，对浏览器发送的请求统一进行处理 --\u0026gt; \u0026lt;servlet\u0026gt; \u0026lt;servlet-name\u0026gt;springMVC\u0026lt;/servlet-name\u0026gt; \u0026lt;servlet-class\u0026gt;org.springframework.web.servlet.DispatcherServlet\u0026lt;/servlet\u0002class\u0026gt; \u0026lt;!-- 通过初始化参数指定SpringMVC配置文件的位置和名称 --\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;!-- contextConfigLocation为固定值 --\u0026gt; \u0026lt;param-name\u0026gt;contextConfigLocation\u0026lt;/param-name\u0026gt; \u0026lt;!-- 使用classpath:表示从类路径查找配置文件，例如maven工程中的src/main/resources --\u0026gt; \u0026lt;param-value\u0026gt;classpath:springmvc.xml\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;!-- 作为框架的核心组件，在启动过程中有大量的初始化操作要做 而这些操作放在第一次请求时才执行会严重影响访问速度 因此需要通过此标签将启动控制DispatcherServlet的初始化时间提前到服务器启动时 --\u0026gt; \u0026lt;load-on-startup\u0026gt;1\u0026lt;/load-on-startup\u0026gt; \u0026lt;/servlet\u0026gt; \u0026lt;servlet-mapping\u0026gt; \u0026lt;servlet-name\u0026gt;springMVC\u0026lt;/servlet-name\u0026gt; \u0026lt;!-- 设置springMVC的核心控制器所能处理的请求的请求路径 /所匹配的请求可以是/login或.html或.js或.css方式的请求路径 但是/不能匹配.jsp请求路径的请求 --\u0026gt; \u0026lt;url-pattern\u0026gt;/\u0026lt;/url-pattern\u0026gt; \u0026lt;/servlet-mapping\u0026gt; 1 2 3 4 5 6 7 8 9 注： \u0026lt;url-pattern\u0026gt;标签中使用/和/*的区别： /所匹配的请求可以是/login或.html或.js或.css方式的请求路径，但是/不能匹配.jsp请求路径的请求 因此就可以避免在访问jsp页面时，该请求被DispatcherServlet处理，从而找不到相应的页面 /*则能够匹配所有请求，例如在使用过滤器时，若需要对所有请求进行过滤，就需要使用/*的写法 2.4、创建请求控制器 由于前端控制器对浏览器发送的请求进行了统一的处理，但是具体的请求有不同的处理过程，因此需要创建处理具体请求的类，即请求控制器\n请求控制器中每一个处理请求的方法成为控制器方法\n因为SpringMVC的控制器由一个POJO（普通的Java类）担任，因此需要通过@Controller注解将其标识为一个控制层组件，交给Spring的IoC容器管理，此时SpringMVC才能够识别控制器的存在\n1 2 3 @Controller public class HelloController { } 2.5、创建SpringMVC的配置文件 SpringMVC的配置文件默认的位置和名称：\n位置：WEB-INF下\n名称：-serverlet.xml, 当前配置下的配置文件名为SpringMVC-servlet.xml\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 \u0026lt;!-- 自动扫描包 --\u0026gt; \u0026lt;context:component-scan base-package=\u0026#34;com.atguigu.mvc.controller\u0026#34;/\u0026gt; \u0026lt;!-- 配置Thymeleaf视图解析器 --\u0026gt; \u0026lt;bean id=\u0026#34;viewResolver\u0026#34; class=\u0026#34;org.thymeleaf.spring5.view.ThymeleafViewResolver\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;order\u0026#34; value=\u0026#34;1\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;characterEncoding\u0026#34; value=\u0026#34;UTF-8\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;templateEngine\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.thymeleaf.spring5.SpringTemplateEngine\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;templateResolver\u0026#34;\u0026gt; \u0026lt;bean class=\u0026#34;org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver\u0026#34;\u0026gt; \u0026lt;!-- 视图前缀 --\u0026gt; \u0026lt;property name=\u0026#34;prefix\u0026#34; value=\u0026#34;/WEB-INF/templates/\u0026#34;/\u0026gt; \u0026lt;!-- 视图后缀 --\u0026gt; \u0026lt;property name=\u0026#34;suffix\u0026#34; value=\u0026#34;.html\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;templateMode\u0026#34; value=\u0026#34;HTML5\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;characterEncoding\u0026#34; value=\u0026#34;UTF-8\u0026#34; /\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;!-- 处理静态资源，例如html、js、css、jpg 若只设置该标签，则只能访问静态资源，其他请求则无法访问 此时必须设置\u0026lt;mvc:annotation-driven/\u0026gt;解决问题 --\u0026gt; \u0026lt;mvc:default-servlet-handler/\u0026gt; \u0026lt;!-- 开启mvc注解驱动 --\u0026gt; \u0026lt;mvc:annotation-driven\u0026gt; \u0026lt;mvc:message-converters\u0026gt; \u0026lt;!-- 处理响应中文内容乱码 --\u0026gt; \u0026lt;bean class=\u0026#34;org.springframework.http.converter.StringHttpMessageConverter\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;defaultCharset\u0026#34; value=\u0026#34;UTF-8\u0026#34; /\u0026gt; \u0026lt;property name=\u0026#34;supportedMediaTypes\u0026#34;\u0026gt; \u0026lt;list\u0026gt; \u0026lt;value\u0026gt;text/html\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;application/json\u0026lt;/value\u0026gt; \u0026lt;/list\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; \u0026lt;/mvc:message-converters\u0026gt; \u0026lt;/mvc:annotation-driven\u0026gt; 2.6、测试HelloWorld 配置tomcat\n①实现对首页的访问 在请求控制器中创建处理请求的方法\n1 2 3 4 5 6 7 8 // @RequestMapping注解：处理请求和控制器方法之间的映射关系 // @RequestMapping注解的value属性可以通过请求地址匹配请求，/表示的当前工程的上下文路径 // localhost:8080/springMVC/ @RequestMapping(\u0026#34;/\u0026#34;) public String protal() { //设置视图名称 return \u0026#34;index\u0026#34;; } ②通过超链接跳转到指定页面 在主页index.html中设置超链接\n1 2 3 4 5 6 7 8 9 10 11 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34; xmlns:th=\u0026#34;http://www.thymeleaf.org\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;首页\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;首页\u0026lt;/h1\u0026gt; \u0026lt;a th:href=\u0026#34;@{/hello}\u0026#34;\u0026gt;HelloWorld\u0026lt;/a\u0026gt;\u0026lt;br/\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 在请求控制器中创建处理请求的方法\n1 2 3 4 @RequestMapping(\u0026#34;/hello\u0026#34;) public String HelloWorld() { return \u0026#34;target\u0026#34;; } 2.7、总结 浏览器发送请求，若请求地址符合前端控制器的url-pattern，该请求就会被前端控制器DispatcherServlet处理。前端控制器会读取SpringMVC的核心配置文件，通过扫描组件找到控制器，将请求地址和控制器中@RequestMapping注解的value属性值进行匹配，若匹配成功，该注解所标识的控制器方法就是处理请求的方法。处理请求的方法需要返回一个字符串类型的视图名称，该视图名称会被视图解析器解析，加上前缀和后缀组成视图的路径，通过Thymeleaf对视图进行渲染，最终转发到视图所对应页面\n3、@RequestMapping注解 3.1、@RequestMapping注解的功能 从注解名称上我们可以看到，@RequestMapping注解的作用就是将请求和处理请求的控制器方法关联起来，建立映射关系。\nSpringMVC 接收到指定的请求，就会来找到在映射关系中对应的控制器方法来处理这个请求。\n3.2、@RequestMapping注解的位置 @RequestMapping标识一个类：设置映射请求的请求路径的初始信息\n@RequestMapping标识一个方法：设置映射请求请求路径的具体信息\n1 2 3 4 5 6 7 8 9 @Controller @RequestMapping(\u0026#34;/test\u0026#34;) public class RequestMappingController { //此时请求映射所映射的请求的请求路径为：/test/testRequestMapping @RequestMapping(\u0026#34;/testRequestMapping\u0026#34;) public String testRequestMapping(){ return \u0026#34;success\u0026#34;; } } 3.3、@RequestMapping注解的value属性\n@RequestMapping注解的value属性通过请求的请求地址匹配请求映射\n@RequestMapping注解的value属性是一个字符串类型的数组，表示该请求映射能够匹配多个请求地址所对应的请求\n@RequestMapping注解的value属性必须设置，至少通过请求地址匹配请求映射\n1 2 \u0026lt;a th:href=\u0026#34;@{/testRequestMapping}\u0026#34;\u0026gt;测试@RequestMapping的value属性--\u0026gt;/testRequestMapping\u0026lt;/a\u0026gt;\u0026lt;br\u0026gt; \u0026lt;a th:href=\u0026#34;@{/test}\u0026#34;\u0026gt;测试@RequestMapping的value属性--\u0026gt;/test\u0026lt;/a\u0026gt;\u0026lt;br\u0026gt; 1 2 3 4 5 6 @RequestMapping( value = {\u0026#34;/testRequestMapping\u0026#34;, \u0026#34;/test\u0026#34;} ) public String testRequestMapping(){ return \u0026#34;success\u0026#34;; } 3.4、@RequestMapping注解的method属性 @RequestMapping注解的method属性通过请求的请求方式（get或post）匹配请求映射\n@RequestMapping注解的method属性是一个RequestMethod类型的数组，表示该请求映射能够匹配多种请求方式的请求\n若当前请求的请求地址满足请求映射的value属性，但是请求方式不满足method属性，则浏览器报错\n405：Request method \u0026lsquo;POST\u0026rsquo; not supported\n1 2 3 4 \u0026lt;a th:href=\u0026#34;@{/test}\u0026#34;\u0026gt;测试@RequestMapping的value属性--\u0026gt;/test\u0026lt;/a\u0026gt;\u0026lt;br\u0026gt; \u0026lt;form th:action=\u0026#34;@{/test}\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; 1 2 3 4 5 6 7 @RequestMapping( value = {\u0026#34;/testRequestMapping\u0026#34;, \u0026#34;/test\u0026#34;}, method = {RequestMethod.GET, RequestMethod.POST} ) public String testRequestMapping(){ return \u0026#34;success\u0026#34;; } 注：\n1、对于处理指定请求方式的控制器方法，SpringMVC中提供了@RequestMapping的派生注解\n处理get请求的映射\u0026ndash;\u0026gt;@GetMapping\n处理post请求的映射\u0026ndash;\u0026gt;@PostMapping\n处理put请求的映射\u0026ndash;\u0026gt;@PutMapping\n处理delete请求的映射\u0026ndash;\u0026gt;@DeleteMapping\n2、常用的请求方式有get，post，put，delete\n但是目前浏览器只支持get和post，若在form表单提交时，为method设置了其他请求方式的字符串（put或delete），则按照默认的请求方式get处理\n若要发送put和delete请求，则需要通过spring提供的过滤器HiddenHttpMethodFilter，在RESTful部分会讲到\n3.5、@RequestMapping注解的params属性（了解） @RequestMapping注解的params属性通过请求的请求参数匹配请求映射\n@RequestMapping注解的params属性是一个字符串类型的数组，可以通过四种表达式设置请求参数\n和请求映射的匹配关系\n\u0026ldquo;param\u0026rdquo;：要求请求映射所匹配的请求必须携带param请求参数\n\u0026ldquo;!param\u0026rdquo;：要求请求映射所匹配的请求必须不能携带param请求参数\n\u0026ldquo;param=value\u0026rdquo;：要求请求映射所匹配的请求必须携带param请求参数且param=value\n\u0026ldquo;param!=value\u0026rdquo;：要求请求映射所匹配的请求必须携带param请求参数但是param!=value\n1 2 \u0026lt;a th:href=\u0026#34;@{/test(username=\u0026#39;admin\u0026#39;,password=123456)\u0026#34;\u0026gt;测试@RequestMapping的 params属性--\u0026gt;/test\u0026lt;/a\u0026gt;\u0026lt;br\u0026gt; 1 2 3 4 5 6 7 8 @RequestMapping( value = {\u0026#34;/testRequestMapping\u0026#34;, \u0026#34;/test\u0026#34;} ,method = {RequestMethod.GET, RequestMethod.POST} ,params = {\u0026#34;username\u0026#34;,\u0026#34;password!=123456\u0026#34;} ) public String testRequestMapping(){ return \u0026#34;success\u0026#34;; } 注：\n若当前请求满足@RequestMapping注解的value和method属性，但是不满足params属性，此时页面回报错400：Parameter conditions \u0026ldquo;username, password!=123456\u0026rdquo; not met for actual request parameters: username={admin}, password={123456}\n3.6、@RequestMapping注解的headers属性（了解） @RequestMapping注解的headers属性通过请求的请求头信息匹配请求映射\n@RequestMapping注解的headers属性是一个字符串类型的数组，可以通过四种表达式设置请求头信\n息和请求映射的匹配关系\n\u0026ldquo;header\u0026rdquo;：要求请求映射所匹配的请求必须携带header请求头信息\n\u0026ldquo;!header\u0026rdquo;：要求请求映射所匹配的请求必须不能携带header请求头信息\n\u0026ldquo;header=value\u0026rdquo;：要求请求映射所匹配的请求必须携带header请求头信息且header=value\n\u0026ldquo;header!=value\u0026rdquo;：要求请求映射所匹配的请求必须携带header请求头信息且header!=value\n若当前请求满足@RequestMapping注解的value和method属性，但是不满足headers属性，此时页面\n显示404错误，即资源未找到\n3.7、SpringMVC支持ant风格的路径 在@RequestMapping注解的value属性值中设置一些特殊字符\n？：表示任意的单个字符（不包括？）\n*：表示任意的0个或多个字符（不包括？和/）\n**：表示任意层数的任意目录\n（注意使用方式只能** 写在双斜线中，前后不能有任何的其他字符）\n3.8、SpringMVC支持路径中的占位符（重点） 原始方式：/deleteUser?id=1\nrest方式：/user/delete/1\nSpringMVC路径中的占位符常用于RESTful风格中，当请求路径中将某些数据通过路径的方式传输到服\n务器中，就可以在相应的@RequestMapping注解的value属性中通过占位符{xxx}表示传输的数据，在\n通过@PathVariable注解，将占位符所表示的数据赋值给控制器方法的形参\n1 \u0026lt;a th:href=\u0026#34;@{/testRest/1/admin}\u0026#34;\u0026gt;测试路径中的占位符--\u0026gt;/testRest\u0026lt;/a\u0026gt;\u0026lt;br\u0026gt; 1 2 3 4 5 6 7 @RequestMapping(\u0026#34;/testRest/{id}/{username}\u0026#34;) public String testRest(@PathVariable(\u0026#34;id\u0026#34;) String id, @PathVariable(\u0026#34;username\u0026#34;) String username){ System.out.println(\u0026#34;id:\u0026#34;+id+\u0026#34;,username:\u0026#34;+username); return \u0026#34;success\u0026#34;; } //最终输出的内容为--\u0026gt;id:1,username:admin 4、SpringMVC获取请求参数 4.1、通过ServletAPI获取 将HttpServletRequest作为控制器方法的形参，此时HttpServletRequest类型的参数表示封装了当前请求的请求报文的对象\n1 2 3 4 5 6 7 @RequestMapping(\u0026#34;/testParam\u0026#34;) public String testParam(HttpServletRequest request){ String username = request.getParameter(\u0026#34;username\u0026#34;); String password = request.getParameter(\u0026#34;password\u0026#34;); System.out.println(\u0026#34;username:\u0026#34;+username+\u0026#34;,password:\u0026#34;+password); return \u0026#34;success\u0026#34;; } 4.2、通过控制器方法的形参获取请求参数 在控制器方法的形参位置，设置和请求参数同名的形参，当浏览器发送请求，匹配到请求映射时，在\nDispatcherServlet中就会将请求参数赋值给相应的形参\n1 2 \u0026lt;a th:href=\u0026#34;@{/testParam(username=\u0026#39;admin\u0026#39;,password=123456)}\u0026#34;\u0026gt;测试获取请求参数-- \u0026gt;/testParam\u0026lt;/a\u0026gt;\u0026lt;br\u0026gt; 1 2 3 4 5 @RequestMapping(\u0026#34;/testParam\u0026#34;) public String testParam(String username, String password){ System.out.println(\u0026#34;username:\u0026#34;+username+\u0026#34;,password:\u0026#34;+password); return \u0026#34;success\u0026#34;; } 注：\n若请求所传输的请求参数中有多个同名的请求参数，此时可以在控制器方法的形参中设置字符串\n数组或者字符串类型的形参接收此请求参数\n若使用字符串数组类型的形参，此参数的数组中包含了每一个数据\n若使用字符串类型的形参，此参数的值为每个数据中间使用逗号拼接的结果\n4.3、@RequestParam @RequestParam是将请求参数和控制器方法的形参创建映射关系\n@RequestParam注解一共有三个属性：\nvalue：指定为形参赋值的请求参数的参数名\nrequired：设置是否必须传输此请求参数，默认值为true\n若设置为true时，则当前请求必须传输value所指定的请求参数，若没有传输该请求参数，且没有设置\ndefaultValue属性，则页面报错400：Required String parameter \u0026lsquo;xxx\u0026rsquo; is not present；若设置为\nfalse，则当前请求不是必须传输value所指定的请求参数，若没有传输，则注解所标识的形参的值为\nnull\ndefaultValue：不管required属性值为true或false，当value所指定的请求参数没有传输或传输的值\n为\u0026quot;\u0026ldquo;时，则使用默认值为形参赋值\n4.4、@RequestHeader @RequestHeader是将请求头信息和控制器方法的形参创建映射关系\n@RequestHeader注解一共有三个属性：value、required、defaultValue，用法同@RequestParam\n4.5、@CookieValue @CookieValue是将cookie数据和控制器方法的形参创建映射关系\n@CookieValue注解一共有三个属性：value、required、defaultValue，用法同@RequestParam\n4.6、通过POJO获取请求参数 可以在控制器方法的形参位置设置一个实体类类型的形参，此时若浏览器传输的请求参数的参数名和实体类中的属性名一致，那么请求参数就会为此属性赋值\n1 2 3 4 5 6 7 8 \u0026lt;form th:action=\u0026#34;@{/testpojo}\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; 用户名：\u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;username\u0026#34;\u0026gt;\u0026lt;br\u0026gt; 密码：\u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34;\u0026gt;\u0026lt;br\u0026gt; 性别：\u0026lt;input type=\u0026#34;radio\u0026#34; name=\u0026#34;sex\u0026#34; value=\u0026#34;男\u0026#34;\u0026gt;男\u0026lt;input type=\u0026#34;radio\u0026#34;name=\u0026#34;sex\u0026#34; value=\u0026#34;女\u0026#34;\u0026gt;女\u0026lt;br\u0026gt; 年龄：\u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;age\u0026#34;\u0026gt;\u0026lt;br\u0026gt; 邮箱：\u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;email\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; 1 2 3 4 5 6 @RequestMapping(\u0026#34;/testpojo\u0026#34;) public String testPOJO(User user){ System.out.println(user); return \u0026#34;success\u0026#34;; } //最终结果--\u0026gt;User{id=null, username=\u0026#39;张三\u0026#39;, password=\u0026#39;123\u0026#39;, age=23, sex=\u0026#39;男\u0026#39;,email=\u0026#39;123@qq.com\u0026#39;} 4.7、解决获取请求参数的乱码问题 tomcat7.x中配置server.xml后，改为get请求，则无乱码\ntomcat8.x以后，post还有乱码，则需要添加下列配置\n解决获取请求参数的乱码问题，可以使用SpringMVC提供的编码过滤器CharacterEncodingFilter，但是必须在web.xml中进行注册\n注意：一定到重启服务器，才能生效\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 \u0026lt;!--配置springMVC的编码过滤器--\u0026gt; \u0026lt;filter\u0026gt; \u0026lt;filter-name\u0026gt;CharacterEncodingFilter\u0026lt;/filter-name\u0026gt; \u0026lt;filter-class\u0026gt;org.springframework.web.filter.CharacterEncodingFilter\u0026lt;/filter-class\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;encoding\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;UTF-8\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;init-param\u0026gt; \u0026lt;param-name\u0026gt;forceEncoding\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;true\u0026lt;/param-value\u0026gt; \u0026lt;/init-param\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;filter-mapping\u0026gt; \u0026lt;filter-name\u0026gt;CharacterEncodingFilter\u0026lt;/filter-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/filter-mapping\u0026gt; 注：\nSpringMVC中处理编码的过滤器一定要配置到其他过滤器之前，否则无效\n常见配置乱码设置：\n1.Help-\u0026gt;Edit Custom VM Options\n添加-Dfile.encoding=UTF-8\n2.File Encodings设置\n3.tomcat内配置\n4.tomcat-\u0026gt;conf-\u0026gt;server.xml内配置\n5、域对象共享数据 5.1、使用ServletAPI向request域对象共享数据 1 2 3 4 5 @RequestMapping(\u0026#34;/testServletAPI\u0026#34;) public String testServletAPI(HttpServletRequest request){ request.setAttribute(\u0026#34;testScope\u0026#34;, \u0026#34;hello,servletAPI\u0026#34;); return \u0026#34;success\u0026#34;; } 5.2、使用ModelAndView向request域对象共享数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RequestMapping(\u0026#34;/test/mav\u0026#34;) public ModelAndView testModelAndView(){ /** * ModelAndView有Model和View的功能 * Model主要用于向请求域共享数据 * View主要用于设置视图，实现页面跳转 */ ModelAndView mav = new ModelAndView(); //向请求域共享数据 mav.addObject(\u0026#34;testScope\u0026#34;, \u0026#34;hello,ModelAndView\u0026#34;); //设置视图，实现页面跳转 mav.setViewName(\u0026#34;success\u0026#34;); return mav; } 1 \u0026lt;a th:href=\u0026#34;@{test/mav}\u0026#34;\u0026gt;使用ModelAndView向request域对象共享数据\u0026lt;/a\u0026gt; 1 \u0026lt;P th:text=\u0026#34;${testScope}\u0026#34;\u0026gt;\u0026lt;/P\u0026gt; 5.3、使用Model向request域对象共享数据 1 2 3 4 5 @RequestMapping(\u0026#34;/testModel\u0026#34;) public String testModel(Model model){ model.addAttribute(\u0026#34;testScope\u0026#34;, \u0026#34;hello,Model\u0026#34;); return \u0026#34;success\u0026#34;; } 5.4、使用map向request域对象共享数据 1 2 3 4 5 @RequestMapping(\u0026#34;/testMap\u0026#34;) public String testMap(Map\u0026lt;String, Object\u0026gt; map){ map.put(\u0026#34;testScope\u0026#34;, \u0026#34;hello,Map\u0026#34;); return \u0026#34;success\u0026#34;; } 5.5、使用ModelMap向request域对象共享数据\n1 2 3 4 5 @RequestMapping(\u0026#34;/testModelMap\u0026#34;) public String testModelMap(ModelMap modelMap){ modelMap.addAttribute(\u0026#34;testScope\u0026#34;, \u0026#34;hello,ModelMap\u0026#34;); return \u0026#34;success\u0026#34;; } 5.6、Model、ModelMap、Map的关系 Model、ModelMap、Map类型的参数其实本质上都是 BindingAwareModelMap 类型的\n1 2 3 4 public interface Model{} public class ModelMap extends LinkedHashMap\u0026lt;String, Object\u0026gt; {} public class ExtendedModelMap extends ModelMap implements Model {} public class BindingAwareModelMap extends ExtendedModelMap {} 5.7、向session域共享数据 1 2 3 4 5 @RequestMapping(\u0026#34;/testSession\u0026#34;) public String testSession(HttpSession session){ session.setAttribute(\u0026#34;testSessionScope\u0026#34;, \u0026#34;hello,session\u0026#34;); return \u0026#34;success\u0026#34;; } 5.8、向application域共享数据 1 2 3 4 5 6 @RequestMapping(\u0026#34;/testApplication\u0026#34;) public String testApplication(HttpSession session){ ServletContext application = session.getServletContext(); application.setAttribute(\u0026#34;testApplicationScope\u0026#34;, \u0026#34;hello,application\u0026#34;); return \u0026#34;success\u0026#34;; } 1 2 \u0026lt;P th:text=\u0026#34;${session.testSessionScope}\u0026#34;\u0026gt;\u0026lt;/P\u0026gt; \u0026lt;P th:text=\u0026#34;${application.testApplicationScope}\u0026#34;\u0026gt;\u0026lt;/P\u0026gt; 1 2 \u0026lt;a th:href=\u0026#34;@{test/session}\u0026#34;\u0026gt;使用向会话域共享数据\u0026lt;/a\u0026gt;\u0026lt;br\u0026gt; \u0026lt;a th:href=\u0026#34;@{test/application}\u0026#34;\u0026gt;测试向应用域共享数据\u0026lt;/a\u0026gt;\u0026lt;br\u0026gt; 6、SpringMVC的视图 SpringMVC中的视图是View接口，视图的作用渲染数据，将模型Model中的数据展示给用户\nSpringMVC视图的种类很多，默认有转发视图和重定向视图\n当工程引入jstl的依赖，转发视图会自动转换为JstlView\n若使用的视图技术为Thymeleaf，在SpringMVC的配置文件中配置了Thymeleaf的视图解析器，由此视图解析器解析之后所得到的是ThymeleafView\n6.1、ThymeleafView 当控制器方法中所设置的视图名称没有任何前缀时，此时的视图名称会被SpringMVC配置文件中所配置的视图解析器解析，视图名称拼接视图前缀和视图\n后缀所得到的最终路径，会通过转发的方式实现跳转\n1 2 3 4 @RequestMapping(\u0026#34;/testHello\u0026#34;) public String testHello(){ return \u0026#34;hello\u0026#34;; } 6.2、转发视图 SpringMVC中默认的转发视图是InternalResourceView\nSpringMVC中创建转发视图的情况：\n当控制器方法中所设置的视图名称以\u0026quot;forward:\u0026ldquo;为前缀时，创建InternalResourceView视图，此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析，而是会将前缀\u0026quot;forward:\u0026ldquo;去掉，剩余部分作为最终路径通过转发的方式实现跳转\n例如\u0026quot;forward:/\u0026quot;，\u0026ldquo;forward:/employee\u0026rdquo;\n1 2 3 4 @RequestMapping(\u0026#34;/testForward\u0026#34;) public String testForward(){ return \u0026#34;forward:/testHello\u0026#34;; } 6.3、重定向视图 SpringMVC中默认的重定向视图是RedirectView\n当控制器方法中所设置的视图名称以\u0026quot;redirect:\u0026ldquo;为前缀时，创建RedirectView视图，此时的视图名称不\n会被SpringMVC配置文件中所配置的视图解析器解析，而是会将前缀\u0026quot;redirect:\u0026ldquo;去掉，剩余部分作为最终路径通过重定向的方式实现跳转\n例如\u0026quot;redirect:/\u0026quot;，\u0026ldquo;redirect:/employee\u0026rdquo;\n1 2 3 4 @RequestMapping(\u0026#34;/testRedirect\u0026#34;) public String testRedirect(){ return \u0026#34;redirect:/testHello\u0026#34;; } 注：\n重定向视图在解析时，会先将redirect:前缀去掉，然后会判断剩余部分是否以/开头，若是则会自\n动拼接上下文路径\n6.4、视图控制器view-controller 当控制器方法中，仅仅用来实现页面跳转，即只需要设置视图名称时，可以将处理器方法使用view\u0002\ncontroller标签进行表示\n1 2 3 4 5 \u0026lt;!-- path：设置处理的请求地址 view-name：设置请求地址所对应的视图名称 --\u0026gt; \u0026lt;mvc:view-controller path=\u0026#34;/testView\u0026#34; view-name=\u0026#34;success\u0026#34;\u0026gt;\u0026lt;/mvc:view-controller\u0026gt; 注：\n当SpringMVC中设置任何一个view-controller时，其他控制器中的请求映射将全部失效，此时需要在SpringMVC的核心配置文件中设置开启mvc注解驱动的标签：\u0026lt;mvc:annotation-driven /\u0026gt;\n7、RESTful 7.1、RESTful简介 REST：Representational State Transfer，表现层资源状态转移。\n①资源 资源是一种看待服务器的方式，即，将服务器看作是由很多离散的资源组成。每个资源是服务器上一个可命名的抽象概念。因为资源是一个抽象的概念，所以它不仅仅能代表服务器文件系统中的一个文件、数据库中的一张表等等具体的东西，可以将资源设计的要多抽象有多抽象，只要想象力允许而且客户端应用开发者能够理解。与面向对象设计类似，资源是以名词为核心来组织的，首先关注的是名词。一个资源可以由一个或多个URI来标识。URI既是资源的名称，也是资源在Web上的地址。对某个资源感兴趣的客户端应用，可以通过资源的URI与其进行交互。\n②资源的表述 资源的表述是一段对于资源在某个特定时刻的状态的描述。可以在客户端-服务器端之间转移（交换）。资源的表述可以有多种格式，例如HTML/XML/JSON/纯文本/图片/视频/音频等等。资源的表述格式可以通过协商机制来确定。请求-响应方向的表述通常使用不同的格式。\n③状态转移 状态转移说的是：在客户端和服务器端之间转移（transfer）代表资源状态的表述。通过转移和操作资源的表述，来间接实现操作资源的目的。\n7.2、RESTful的实现 具体说，就是 HTTP 协议里面，四个表示操作方式的动词：GET、POST、PUT、DELETE。\n它们分别对应四种基本操作：GET 用来获取资源，POST 用来新建资源，PUT 用来更新资源，DELETE用来删除资源。\nREST 风格提倡 URL 地址使用统一的风格设计，从前到后各个单词使用斜杠分开，不使用问号键值对方式携带请求参数，而是将要发送给服务器的数据作为 URL 地址的一部分，以保证整体风格的一致性。\n操作 传统方式 REST****风格 查询操作 getUserById?id=1 user/1\u0026ndash;\u0026gt;get请求方式 保存操作 saveUser user\u0026ndash;\u0026gt;post请求方式 删除操作 deleteUser?id=1 user/1\u0026ndash;\u0026gt;delete请求方式 更新操作 updateUser user\u0026ndash;\u0026gt;put请求方式 7.3、HiddenHttpMethodFilter 由于浏览器只支持发送get和post方式的请求，那么该如何发送put和delete请求呢？\nSpringMVC 提供了 HiddenHttpMethodFilter 帮助我们将 POST 请求转换为 DELETE 或 PUT 请求\nHiddenHttpMethodFilter 处理put和delete请求的条件：\na\u0026gt;当前请求的请求方式必须为post\nb\u0026gt;当前请求必须传输请求参数_method\n满足以上条件，HiddenHttpMethodFilter 过滤器就会将当前请求的请求方式转换为请求参数\n_method的值，因此请求参数_method的值才是最终的请求方式\n在web.xml中注册HiddenHttpMethodFilter\n1 2 3 4 5 6 7 8 \u0026lt;filter\u0026gt; \u0026lt;filter-name\u0026gt;HiddenHttpMethodFilter\u0026lt;/filter-name\u0026gt; \u0026lt;filter-class\u0026gt;org.springframework.web.filter.HiddenHttpMethodFilter\u0026lt;/filter\u0002class\u0026gt; \u0026lt;/filter\u0026gt; \u0026lt;filter-mapping\u0026gt; \u0026lt;filter-name\u0026gt;HiddenHttpMethodFilter\u0026lt;/filter-name\u0026gt; \u0026lt;url-pattern\u0026gt;/*\u0026lt;/url-pattern\u0026gt; \u0026lt;/filter-mapping\u0026gt; 注：\n目前为止，SpringMVC中提供了两个过滤器：CharacterEncodingFilter和HiddenHttpMethodFilter\n在web.xml中注册时，必须先注册CharacterEncodingFilter，再注册HiddenHttpMethodFilter\n原因：\n在 CharacterEncodingFilter 中通过 request.setCharacterEncoding(encoding) 方法设置字符集的\nrequest.setCharacterEncoding(encoding) 方法要求前面不能有任何获取请求参数的操作\n而 HiddenHttpMethodFilter 恰恰有一个获取请求方式的操作：\n1 String paramValue = request.getParameter(this.methodParam); 8、RESTful案例 8.1、准备工作 和传统 CRUD 一样，实现对员工信息的增删改查。\n搭建环境 准备实体类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.hbnu.pojo; /** * @Auther: 赵羽 * @Date: 2023/4/30 - 04 - 30 - 19:47 * @Description: com.hbnu.pojo * @version: 1.0 */ public class Employee { private Integer id; private String lastName; private String email; //1 male, 0 female private Integer gender; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public Integer getGender() { return gender; } public void setGender(Integer gender) { this.gender = gender; } public Employee(Integer id, String lastName, String email, Integer gender) { super(); this.id = id; this.lastName = lastName; this.email = email; this.gender = gender; } public Employee() { } } 准备dao模拟数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.hbnu.dao; import com.hbnu.pojo.Employee; import org.springframework.stereotype.Repository; import java.util.Collection; import java.util.HashMap; import java.util.Map; /** * @Auther: 赵羽 * @Date: 2023/4/30 - 04 - 30 - 19:50 * @Description: com.hbnu.dao * @version: 1.0 */ @Repository public class EmployeeDao { private static Map\u0026lt;Integer, Employee\u0026gt; employees = null; static{ employees = new HashMap\u0026lt;Integer, Employee\u0026gt;(); employees.put(1001, new Employee(1001, \u0026#34;E-AA\u0026#34;, \u0026#34;aa@163.com\u0026#34;, 1)); employees.put(1002, new Employee(1002, \u0026#34;E-BB\u0026#34;, \u0026#34;bb@163.com\u0026#34;, 1)); employees.put(1003, new Employee(1003, \u0026#34;E-CC\u0026#34;, \u0026#34;cc@163.com\u0026#34;, 0)); employees.put(1004, new Employee(1004, \u0026#34;E-DD\u0026#34;, \u0026#34;dd@163.com\u0026#34;, 0)); employees.put(1005, new Employee(1005, \u0026#34;E-EE\u0026#34;, \u0026#34;ee@163.com\u0026#34;, 1)); } private static Integer initId = 1006; public void save(Employee employee){ if(employee.getId() == null){ employee.setId(initId++); } employees.put(employee.getId(), employee); } public Collection\u0026lt;Employee\u0026gt; getAll(){ return employees.values(); } public Employee get(Integer id){ return employees.get(id); } public void delete(Integer id){ employees.remove(id); } } 8.2、功能清单 功能 URL 地址 请求方式 访问首页√ / GET 查询全部数据√ /employee GET 删除√ /employee/2 DELETE 跳转到添加数据页面√ /toAdd GET 执行保存√ /employee POST 跳转到更新数据页面√ /employee/2 GET 执行更新√ /employee PUT 8.3、具体功能：访问首页 ①配置view-controller 1 \u0026lt;mvc:view-controller path=\u0026#34;/\u0026#34; view-name=\u0026#34;index\u0026#34;/\u0026gt; ②创建index.html页面 1 2 3 4 5 6 7 8 9 10 11 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34; xmlns:th=\u0026#34;http://www.thymeleaf.org\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34; \u0026gt; \u0026lt;title\u0026gt;Title\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;h1\u0026gt;首页\u0026lt;/h1\u0026gt; \u0026lt;a th:href=\u0026#34;@{/employee}\u0026#34;\u0026gt;访问员工信息\u0026lt;/a\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 8.4、具体功能：查询所有员工数据 ①控制器方法 1 2 3 4 5 6 7 8 @RequestMapping(value = \u0026#34;/employee\u0026#34;,method = RequestMethod.GET) public String getAllEmployees(Model model) { //获取所有的员工信息 Collection\u0026lt;Employee\u0026gt; employeeDaoAll = employeeDao.getAll(); //将所有员工的信息在请求域中共享 model.addAttribute(\u0026#34;allEmployee\u0026#34;, employeeDaoAll); return \u0026#34;employee_list\u0026#34;; } ②创建employee_list.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34; xmlns:th=\u0026#34;http://www.thymeleaf.org\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;Employee Info\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; th:href=\u0026#34;@{/static/css/index_work.css}\u0026#34;\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; th:src=\u0026#34;@{/static/js/vue.js}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;table border=\u0026#34;1\u0026#34; cellpadding=\u0026#34;0\u0026#34; cellspacing=\u0026#34;0\u0026#34; style=\u0026#34;text-align:center;\u0026#34; id=\u0026#34;dataTable\u0026#34;\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th colspan=\u0026#34;5\u0026#34;\u0026gt;Employee Info\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th\u0026gt;id\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;lastName\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;email\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;gender\u0026lt;/th\u0026gt; \u0026lt;th\u0026gt;options(\u0026lt;a th:href=\u0026#34;@{/to/add}\u0026#34;\u0026gt;add\u0026lt;/a\u0026gt;)\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr th:each=\u0026#34;employee : ${allEmployee}\u0026#34;\u0026gt; \u0026lt;td th:text=\u0026#34;${employee.id}\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${employee.lastName}\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${employee.email}\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;td th:text=\u0026#34;${employee.gender}\u0026#34;\u0026gt;\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;a class=\u0026#34;deleteA\u0026#34; @click=\u0026#34;deleteEmployee\u0026#34; th:href=\u0026#34;@{\u0026#39;/employee/\u0026#39;+${employee.id}}\u0026#34;\u0026gt;delete\u0026lt;/a\u0026gt; \u0026lt;a th:href=\u0026#34;@{\u0026#39;/employee/\u0026#39;+${employee.id}}\u0026#34;\u0026gt;update\u0026lt;/a\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;!-- 作用：通过超链接控制表单的提交，将post请求转换为delete请求 --\u0026gt; \u0026lt;form id=\u0026#34;delete_form\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;!-- HiddenHttpMethodFilter要求：必须传输_method请求参数，并且值为最终的请求方式 --\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;_method\u0026#34; value=\u0026#34;delete\u0026#34;/\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; var vue = new Vue({ el:\u0026#34;#dataTable\u0026#34;, methods:{ //event表示当前事件 deleteEmployee:function (event) { //通过id获取表单标签 var delete_form = document.getElementById(\u0026#34;delete_form\u0026#34;); //将触发事件的超链接的href属性为表单的action属性赋值 delete_form.action = event.target.href; //提交表单 delete_form.submit(); //阻止超链接的默认跳转行为 event.preventDefault(); } } }); \u0026lt;/script\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 8.5、具体功能：删除 ①创建处理delete请求方式的表单 1 2 3 4 5 \u0026lt;!-- 作用：通过超链接控制表单的提交，将post请求转换为delete请求 --\u0026gt; \u0026lt;form id=\u0026#34;delete_form\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;!-- HiddenHttpMethodFilter要求：必须传输_method请求参数，并且值为最终的请求方式 --\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;_method\u0026#34; value=\u0026#34;delete\u0026#34;/\u0026gt; \u0026lt;/form\u0026gt; 引入vue.js\n1 \u0026lt;script type=\u0026#34;text/javascript\u0026#34; th:src=\u0026#34;@{/static/js/vue.js}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; 删除超链接\n1 \u0026lt;a class=\u0026#34;deleteA\u0026#34; @click=\u0026#34;deleteEmployee\u0026#34;th:href=\u0026#34;@{\u0026#39;/employee/\u0026#39;+${employee.id}}\u0026#34;\u0026gt;delete\u0026lt;/a\u0026gt; 通过vue处理点击事件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; var vue = new Vue({ el:\u0026#34;#dataTable\u0026#34;, methods:{ //event表示当前事件 deleteEmployee:function (event) { //通过id获取表单标签 var delete_form = document.getElementById(\u0026#34;delete_form\u0026#34;); //将触发事件的超链接的href属性为表单的action属性赋值 delete_form.action = event.target.href; //提交表单 delete_form.submit(); //阻止超链接的默认跳转行为 event.preventDefault(); } } }); \u0026lt;/script\u0026gt; ③控制器方法 1 2 3 4 5 @RequestMapping(value = \u0026#34;/employee/{id}\u0026#34;, method = RequestMethod.DELETE) public String deleteEmployee(@PathVariable(\u0026#34;id\u0026#34;) Integer id){ employeeDao.delete(id); return \u0026#34;redirect:/employee\u0026#34;; } 8.6、具体功能：跳转到添加数据页面 ①配置view-controller 1 \u0026lt;mvc:view-controller path=\u0026#34;/to/add\u0026#34; view-name=\u0026#34;employee_add\u0026#34;\u0026gt;\u0026lt;/mvc:view-controller\u0026gt; ②创建employee_add.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34; xmlns:th=\u0026#34;http://www.thymeleaf.org\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;add employee\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; th:href=\u0026#34;@{/static/css/index_work.css}\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;form th:action=\u0026#34;@{/employee}\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;table\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th colspan=\u0026#34;2\u0026#34;\u0026gt;add employee\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;lastName\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;lastName\u0026#34;\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;email\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;email\u0026#34;\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;gender\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input type=\u0026#34;radio\u0026#34; name=\u0026#34;gender\u0026#34; value=\u0026#34;1\u0026#34;\u0026gt;male \u0026lt;input type=\u0026#34;radio\u0026#34; name=\u0026#34;gender\u0026#34; value=\u0026#34;0\u0026#34;\u0026gt;female \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td colspan=\u0026#34;2\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;add\u0026#34;\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 8.7、具体功能：执行保存 ①控制器方法 1 2 3 4 5 @RequestMapping(value = \u0026#34;/employee\u0026#34;, method = RequestMethod.POST) public String addEmployee(Employee employee){ employeeDao.save(employee); return \u0026#34;redirect:/employee\u0026#34;; } 8.8、具体功能：跳转到更新数据页面 ①修改超链接 1 \u0026lt;a th:href=\u0026#34;@{\u0026#39;/employee/\u0026#39;+${employee.id}}\u0026#34;\u0026gt;update\u0026lt;/a\u0026gt; ②控制器方法 1 2 3 4 5 6 @RequestMapping(value = \u0026#34;/employee/{id}\u0026#34;, method = RequestMethod.GET) public String getEmployeeById(@PathVariable(\u0026#34;id\u0026#34;) Integer id, Model model){ Employee employee = employeeDao.get(id); model.addAttribute(\u0026#34;employee\u0026#34;, employee); return \u0026#34;employee_update\u0026#34;; } ③创建employee_update.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 \u0026lt;!DOCTYPE html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34; xmlns:th=\u0026#34;http://www.thymeleaf.org\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;UTF-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;update employee\u0026lt;/title\u0026gt; \u0026lt;link rel=\u0026#34;stylesheet\u0026#34; th:href=\u0026#34;@{/static/css/index_work.css}\u0026#34;\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;form th:action=\u0026#34;@{/employee}\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;_method\u0026#34; value=\u0026#34;put\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;hidden\u0026#34; name=\u0026#34;id\u0026#34; th:value=\u0026#34;${employee.id}\u0026#34;\u0026gt; \u0026lt;table\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;th colspan=\u0026#34;2\u0026#34;\u0026gt;update employee\u0026lt;/th\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;lastName\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;lastName\u0026#34; th:value=\u0026#34;${employee.lastName}\u0026#34;\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;email\u0026lt;/td\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;email\u0026#34; th:value=\u0026#34;${employee.email}\u0026#34;\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td\u0026gt;gender\u0026lt;/td\u0026gt; \u0026lt;!-- th:field=\u0026#34;${employee.gender}\u0026#34;可用于单选框或复选框的回显 若单选框的value和employee.gender的值一致，则添加checked=\u0026#34;checked\u0026#34;属性 --\u0026gt; \u0026lt;td\u0026gt; \u0026lt;input type=\u0026#34;radio\u0026#34; name=\u0026#34;gender\u0026#34; value=\u0026#34;1\u0026#34; th:field=\u0026#34;${employee.gender}\u0026#34;\u0026gt;male \u0026lt;input type=\u0026#34;radio\u0026#34; name=\u0026#34;gender\u0026#34; value=\u0026#34;0\u0026#34; th:field=\u0026#34;${employee.gender}\u0026#34;\u0026gt;female \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;tr\u0026gt; \u0026lt;td colspan=\u0026#34;2\u0026#34;\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34; value=\u0026#34;update\u0026#34;\u0026gt; \u0026lt;/td\u0026gt; \u0026lt;/tr\u0026gt; \u0026lt;/table\u0026gt; \u0026lt;/form\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 8.9、具体功能：执行更新 ①控制器方法 1 2 3 4 5 @RequestMapping(value = \u0026#34;/employee\u0026#34;, method = RequestMethod.PUT) public String updateEmployee(Employee employee){ employeeDao.save(employee); return \u0026#34;redirect:/employee\u0026#34;; } 9、SpringMVC处理ajax请求 9.1、@RequestBody @RequestBody可以获取请求体信息，使用@RequestBody注解标识控制器方法的形参，当前请求的请求体就会为当前注解所标识的形参赋值\n1 2 3 4 5 6 \u0026lt;!--此时必须使用post请求方式，因为get请求没有请求体--\u0026gt; \u0026lt;form th:action=\u0026#34;@{/test/RequestBody}\u0026#34; method=\u0026#34;post\u0026#34;\u0026gt; 用户名：\u0026lt;input type=\u0026#34;text\u0026#34; name=\u0026#34;username\u0026#34;\u0026gt;\u0026lt;br\u0026gt; 密码：\u0026lt;input type=\u0026#34;password\u0026#34; name=\u0026#34;password\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;input type=\u0026#34;submit\u0026#34;\u0026gt; \u0026lt;/form\u0026gt; 1 2 3 4 5 @RequestMapping(\u0026#34;/test/RequestBody\u0026#34;) public String testRequestBody(@RequestBody String requestBody){ System.out.println(\u0026#34;requestBody:\u0026#34;+requestBody); return \u0026#34;success\u0026#34;; } 输出结果：\nrequestBody:username=admin\u0026amp;password=123456\n9.2、@RequestBody获取json格式的请求参数 在使用了axios发送ajax请求之后，浏览器发送到服务器的请求参数有两种格式：\n1、name=value\u0026amp;name=value\u0026hellip;，此时的请求参数可以通过request.getParameter()获取，对应SpringMVC中，可以直接通过控制器方法的形参获取此类请求参数\n2、{key:value,key:value,\u0026hellip;}，此时无法通过request.getParameter()获取，之前我们使用操作json的相关jar包gson或jackson处理此类请求参数，可以将其转换为指定的实体类对象或map集合。在SpringMVC中，直接使用@RequestBody注解标识控制器方法的形参即可将此类请求参数转换为java对象\n使用@RequestBody获取json格式的请求参数的条件：\n1、导入jackson的依赖\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-databind\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.12.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 2、SpringMVC的配置文件中设置开启mvc的注解驱动\n1 2 \u0026lt;!--开启mvc的注解驱动--\u0026gt; \u0026lt;mvc:annotation-driven /\u0026gt; 3、在控制器方法的形参位置，设置json格式的请求参数要转换成的java类型（实体类或map）的参\n数，并使用@RequestBody注解标识\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 \u0026lt;input type=\u0026#34;button\u0026#34; value=\u0026#34;测试@RequestBody获取json格式的请求参数\u0026#34;@click=\u0026#34;testRequestBody()\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; th:src=\u0026#34;@{/js/vue.js}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; th:src=\u0026#34;@{/js/axios.min.js}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; var vue = new Vue({ el:\u0026#34;#app\u0026#34;, methods:{ testRequestBody(){ axios.post( \u0026#34;/SpringMVC/test/RequestBody/json\u0026#34;, {username:\u0026#34;admin\u0026#34;,password:\u0026#34;123456\u0026#34;} ).then(response=\u0026gt;{ console.log(response.data); }); } } }); \u0026lt;/script\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 //将json格式的数据转换为map集合 @RequestMapping(\u0026#34;/test/RequestBody/json\u0026#34;) public void testRequestBody(@RequestBody Map\u0026lt;String, Object\u0026gt; map,HttpServletResponse response) throws IOException { System.out.println(map); //{username=admin, password=123456} response.getWriter().print(\u0026#34;hello,axios\u0026#34;); } //将json格式的数据转换为实体类对象 @RequestMapping(\u0026#34;/test/RequestBody/json\u0026#34;) public void testRequestBody(@RequestBody User user, HttpServletResponseresponse) throws IOException { System.out.println(user); //User{id=null, username=\u0026#39;admin\u0026#39;, password=\u0026#39;123456\u0026#39;, age=null,gender=\u0026#39;null\u0026#39;} response.getWriter().print(\u0026#34;hello,axios\u0026#34;); } 9.3、@ResponseBody @ResponseBody用于标识一个控制器方法，可以将该方法的返回值直接作为响应报文的响应体响应到浏览器\n1 2 3 4 5 6 7 8 9 10 11 @RequestMapping(\u0026#34;/testResponseBody\u0026#34;) public String testResponseBody(){ //此时会跳转到逻辑视图success所对应的页面 return \u0026#34;success\u0026#34;; } @RequestMapping(\u0026#34;/testResponseBody\u0026#34;) @ResponseBody public String testResponseBody(){ //此时响应浏览器数据success return \u0026#34;success\u0026#34;; } 9.4、@ResponseBody响应浏览器json数据 服务器处理ajax请求之后，大多数情况都需要向浏览器响应一个java对象，此时必须将java对象转换为\njson字符串才可以响应到浏览器，之前我们使用操作json数据的jar包gson或jackson将java对象转换为\njson字符串。在SpringMVC中，我们可以直接使用@ResponseBody注解实现此功能\n@ResponseBody响应浏览器json数据的条件：\n1、导入jackson的依赖\n1 2 3 4 5 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.core\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-databind\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.12.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 2、SpringMVC的配置文件中设置开启mvc的注解驱动\n1 2 \u0026lt;!--开启mvc的注解驱动--\u0026gt; \u0026lt;mvc:annotation-driven /\u0026gt; 3、使用@ResponseBody注解标识控制器方法，在方法中，将需要转换为json字符串并响应到浏览器\n的java对象作为控制器方法的返回值，此时SpringMVC就可以将此对象直接转换为json字符串并响应到浏览器\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 \u0026lt;input type=\u0026#34;button\u0026#34; value=\u0026#34;测试@ResponseBody响应浏览器json格式的数据\u0026#34;@click=\u0026#34;testResponseBody()\u0026#34;\u0026gt;\u0026lt;br\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; th:src=\u0026#34;@{/js/vue.js}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34; th:src=\u0026#34;@{/js/axios.min.js}\u0026#34;\u0026gt;\u0026lt;/script\u0026gt; \u0026lt;script type=\u0026#34;text/javascript\u0026#34;\u0026gt; var vue = new Vue({ el:\u0026#34;#app\u0026#34;, methods:{ testResponseBody(){ axios.post(\u0026#34;/SpringMVC/test/ResponseBody/json\u0026#34;).then(response=\u0026gt;{ console.log(response.data); }); } } }); \u0026lt;/script\u0026gt; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 //响应浏览器list集合 @RequestMapping(\u0026#34;/test/ResponseBody/json\u0026#34;) @ResponseBody public List\u0026lt;User\u0026gt; testResponseBody(){ User user1 = new User(1001,\u0026#34;admin1\u0026#34;,\u0026#34;123456\u0026#34;,23,\u0026#34;男\u0026#34;); User user2 = new User(1002,\u0026#34;admin2\u0026#34;,\u0026#34;123456\u0026#34;,23,\u0026#34;男\u0026#34;); User user3 = new User(1003,\u0026#34;admin3\u0026#34;,\u0026#34;123456\u0026#34;,23,\u0026#34;男\u0026#34;); List\u0026lt;User\u0026gt; list = Arrays.asList(user1, user2, user3); return list; } //响应浏览器map集合 @RequestMapping(\u0026#34;/test/ResponseBody/json\u0026#34;) @ResponseBody public Map\u0026lt;String, Object\u0026gt; testResponseBody(){ User user1 = new User(1001,\u0026#34;admin1\u0026#34;,\u0026#34;123456\u0026#34;,23,\u0026#34;男\u0026#34;); User user2 = new User(1002,\u0026#34;admin2\u0026#34;,\u0026#34;123456\u0026#34;,23,\u0026#34;男\u0026#34;); User user3 = new User(1003,\u0026#34;admin3\u0026#34;,\u0026#34;123456\u0026#34;,23,\u0026#34;男\u0026#34;); Map\u0026lt;String, Object\u0026gt; map = new HashMap\u0026lt;\u0026gt;(); map.put(\u0026#34;1001\u0026#34;, user1); map.put(\u0026#34;1002\u0026#34;, user2); map.put(\u0026#34;1003\u0026#34;, user3); return map; } //响应浏览器实体类对象 @RequestMapping(\u0026#34;/test/ResponseBody/json\u0026#34;) @ResponseBody public User testResponseBody(){ return user; } 9.5、@RestController注解 @RestController注解是springMVC提供的一个复合注解，标识在控制器的类上，就相当于为类添加了\n@Controller注解，并且为其中的每个方法添加了@ResponseBody注解\n10、文件上传和下载 10.1、文件下载 ResponseEntity用于控制器方法的返回值类型，该控制器方法的返回值就是响应到浏览器的响应报文\n使用ResponseEntity实现下载文件的功能\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @RequestMapping(\u0026#34;/testDown\u0026#34;) public ResponseEntity\u0026lt;byte[]\u0026gt; testResponseEntity(HttpSession session) throws IOException { //获取ServletContext对象 ServletContext servletContext = session.getServletContext(); //获取服务器中文件的真实路径 String realPath = servletContext.getRealPath(\u0026#34;/static/img/1.jpg\u0026#34;); //创建输入流 InputStream is = new FileInputStream(realPath); //创建字节数组 byte[] bytes = new byte[is.available()]; //将流读到字节数组中 is.read(bytes); //创建HttpHeaders对象设置响应头信息 MultiValueMap\u0026lt;String, String\u0026gt; headers = new HttpHeaders(); //设置要下载方式以及下载文件的名字 headers.add(\u0026#34;Content-Disposition\u0026#34;, \u0026#34;attachment;filename=1.jpg\u0026#34;); //设置响应状态码 HttpStatus statusCode = HttpStatus.OK; //创建ResponseEntity对象 ResponseEntity\u0026lt;byte[]\u0026gt; responseEntity = new ResponseEntity\u0026lt;\u0026gt;(bytes, headers,statusCode); //关闭输入流 is.close(); return responseEntity; } 10.2、文件上传 文件上传要求form表单的请求方式必须为post，并且添加属性enctype=\u0026ldquo;multipart/form-data\u0026rdquo;\nSpringMVC中将上传的文件封装到MultipartFile对象中，通过此对象可以获取文件相关信息\n上传步骤：\n①添加依赖： 1 2 3 4 5 6 \u0026lt;!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;commons-fileupload\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;commons-fileupload\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.3.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; ②在SpringMVC的配置文件中添加配置： 1 2 3 \u0026lt;!--必须通过文件解析器的解析才能将文件转换为MultipartFile对象--\u0026gt; \u0026lt;bean id=\u0026#34;multipartResolver\u0026#34; class=\u0026#34;org.springframework.web.multipart.commons.CommonsMultipartResolver\u0026#34;\u0026gt; \u0026lt;/bean\u0026gt; ③控制器方法： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RequestMapping(\u0026#34;/testUp\u0026#34;) public String testUp(MultipartFile photo, HttpSession session) throws IOException { //获取上传的文件的文件名 String fileName = photo.getOriginalFilename(); //处理文件重名问题 String hzName = fileName.substring(fileName.lastIndexOf(\u0026#34;.\u0026#34;)); fileName = UUID.randomUUID().toString() + hzName; //获取服务器中photo目录的路径 ServletContext servletContext = session.getServletContext(); String photoPath = servletContext.getRealPath(\u0026#34;photo\u0026#34;); File file = new File(photoPath); if(!file.exists()){ file.mkdir(); } String finalPath = photoPath + File.separator + fileName; //实现上传功能 photo.transferTo(new File(finalPath)); return \u0026#34;success\u0026#34;; } 11、拦截器 11.1、拦截器的配置 SpringMVC中的拦截器用于拦截控制器方法的执行\nSpringMVC中的拦截器需要实现HandlerInterceptor\nSpringMVC的拦截器必须在SpringMVC的配置文件中进行配置：\n1 2 3 4 5 6 7 8 9 10 11 12 \u0026lt;bean class=\u0026#34;com.hbnu.interceptor.FirstInterceptor\u0026#34;\u0026gt;\u0026lt;/bean\u0026gt; \u0026lt;ref bean=\u0026#34;firstInterceptor\u0026#34;\u0026gt;\u0026lt;/ref\u0026gt; \u0026lt;!-- 以上两种配置方式都是对DispatcherServlet所处理的所有的请求进行拦截 --\u0026gt; \u0026lt;mvc:interceptor\u0026gt; \u0026lt;mvc:mapping path=\u0026#34;/**\u0026#34;/\u0026gt; \u0026lt;mvc:exclude-mapping path=\u0026#34;/testRequestEntity\u0026#34;/\u0026gt; \u0026lt;ref bean=\u0026#34;firstInterceptor\u0026#34;\u0026gt;\u0026lt;/ref\u0026gt; \u0026lt;/mvc:interceptor\u0026gt; \u0026lt;!-- 以上配置方式可以通过ref或bean标签设置拦截器，通过mvc:mapping设置需要拦截的请求， 通过mvc:exclude-mapping设置需要排除的请求，即不需要拦截的请求 --\u0026gt; 11.2、拦截器的三个抽象方法 SpringMVC中的拦截器有三个抽象方法：\npreHandle：控制器方法执行之前执行preHandle()，其boolean类型的返回值表示是否拦截或放行，返回true为放行，即调用控制器方法；返回false表示拦截，即不调用控制器方法\npostHandle：控制器方法执行之后执行postHandle()\nafterCompletion：处理完视图和模型数据，渲染视图完毕之后执行afterCompletion()\n11.3、多个拦截器的执行顺序 ①若每个拦截器的preHandle()都返回true\n此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关：\npreHandle()会按照配置的顺序执行，而postHandle()和afterCompletion()会按照配置的反序执行\n②若某个拦截器的preHandle()返回了false\npreHandle()返回false和它之前的拦截器的preHandle()都会执行，postHandle()都不执行，返回false\n的拦截器之前的拦截器的afterCompletion()会执行\n12、异常处理器 12.1、基于配置的异常处理 SpringMVC提供了一个处理控制器方法执行过程中所出现的异常的接口：HandlerExceptionResolver\nHandlerExceptionResolver接口的实现类有：DefaultHandlerExceptionResolver和\nSimpleMappingExceptionResolver\nSpringMVC提供了自定义的异常处理器SimpleMappingExceptionResolver，使用方式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \u0026lt;bean class=\u0026#34;org.springframework.web.servlet.handler.SimpleMappingExceptionResolver\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;exceptionMappings\u0026#34;\u0026gt; \u0026lt;props\u0026gt; \u0026lt;!-- properties的键表示处理器方法执行过程中出现的异常 properties的值表示若出现指定异常时，设置一个新的视图名称，跳转到指定页面 --\u0026gt; \u0026lt;prop key=\u0026#34;java.lang.ArithmeticException\u0026#34;\u0026gt;error\u0026lt;/prop\u0026gt; \u0026lt;/props\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- exceptionAttribute属性设置一个属性名，将出现的异常信息在请求域中进行共享 --\u0026gt; \u0026lt;property name=\u0026#34;exceptionAttribute\u0026#34; value=\u0026#34;ex\u0026#34;\u0026gt;\u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 12.2、基于注解的异常处理 1 2 3 4 5 6 7 8 9 10 11 //@ControllerAdvice将当前类标识为异常处理的组件 @ControllerAdvice public class ExceptionController { //@ExceptionHandler用于设置所标识方法处理的异常 @ExceptionHandler(ArithmeticException.class) //ex表示当前请求处理中出现的异常对象 public String handleArithmeticException(Exception ex, Model model){ model.addAttribute(\u0026#34;ex\u0026#34;, ex); return \u0026#34;error\u0026#34;; } } 13、注解配置SpringMVC 使用配置类和注解代替web.xml和SpringMVC配置文件的功能\n13.1、创建初始化类，代替web.xml 在Servlet3.0环境中，容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类，\n如果找到的话就用它来配置Servlet容器。 Spring提供了这个接口的实现，名为\nSpringServletContainerInitializer，这个类反过来又会查找实现WebApplicationInitializer的类并将配\n置的任务交给它们来完成。Spring3.2引入了一个便利的WebApplicationInitializer基础实现，名为\nAbstractAnnotationConfigDispatcherServletInitializer，当我们的类扩展了\nAbstractAnnotationConfigDispatcherServletInitializer并将其部署到Servlet3.0容器的时候，容器会自动发现它，并用它来配置Servlet上下文。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer { /** * 指定spring的配置类 * @return */ @Override protected Class\u0026lt;?\u0026gt;[] getRootConfigClasses() { return new Class[]{SpringConfig.class}; } /** * 指定SpringMVC的配置类 * @return */ @Override protected Class\u0026lt;?\u0026gt;[] getServletConfigClasses() { return new Class[]{WebConfig.class}; } /** * 指定DispatcherServlet的映射规则，即url-pattern * @return */ @Override protected String[] getServletMappings() { return new String[]{\u0026#34;/\u0026#34;}; } /** * 添加过滤器 * @return */ @Override protected Filter[] getServletFilters() { CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter(); encodingFilter.setEncoding(\u0026#34;UTF-8\u0026#34;); encodingFilter.setForceRequestEncoding(true); HiddenHttpMethodFilter hiddenHttpMethodFilter = newHiddenHttpMethodFilter(); return new Filter[]{encodingFilter, hiddenHttpMethodFilter}; } } 13.2、创建SpringConfig配置类，代替spring的配置文件 1 2 3 4 @Configuration public class SpringConfig { //ssm整合之后，spring的配置信息写在此类中 } 13.3、创建WebConfig配置类，代替SpringMVC的配置文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 @Configuration //扫描组件 @ComponentScan(\u0026#34;com.hbnu.controller\u0026#34;) //开启MVC注解驱动 @EnableWebMvc public class WebConfig implements WebMvcConfigurer { //使用默认的servlet处理静态资源 @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } //配置文件上传解析器 @Bean public CommonsMultipartResolver multipartResolver(){ return new CommonsMultipartResolver(); } //配置拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { FirstInterceptor firstInterceptor = new FirstInterceptor(); registry.addInterceptor(firstInterceptor).addPathPatterns(\u0026#34;/**\u0026#34;); } //配置视图控制 /*@Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(\u0026#34;/\u0026#34;).setViewName(\u0026#34;index\u0026#34;); }*/ //配置异常映射 /*@Override public void configureHandlerExceptionResolvers(List\u0026lt;HandlerExceptionResolver\u0026gt; resolvers) { SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver(); Properties prop = new Properties(); prop.setProperty(\u0026#34;java.lang.ArithmeticException\u0026#34;, \u0026#34;error\u0026#34;); //设置异常映射 exceptionResolver.setExceptionMappings(prop); //设置共享异常信息的键 exceptionResolver.setExceptionAttribute(\u0026#34;ex\u0026#34;); resolvers.add(exceptionResolver); }*/ //配置生成模板解析器 @Bean public ITemplateResolver templateResolver() { WebApplicationContext webApplicationContext =ContextLoader.getCurrentWebApplicationContext(); // ServletContextTemplateResolver需要一个ServletContext作为构造参数，可通过WebApplicationContext 的方法获得 ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(webApplicationContext.getServletContext()); templateResolver.setPrefix(\u0026#34;/WEB-INF/templates/\u0026#34;); templateResolver.setSuffix(\u0026#34;.html\u0026#34;); templateResolver.setCharacterEncoding(\u0026#34;UTF-8\u0026#34;); templateResolver.setTemplateMode(TemplateMode.HTML); return templateResolver; } //生成模板引擎并为模板引擎注入模板解析器 @Bean public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) { SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(templateResolver); return templateEngine; } //生成视图解析器并未解析器注入模板引擎 @Bean public ViewResolver viewResolver(SpringTemplateEngine templateEngine) { ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); viewResolver.setCharacterEncoding(\u0026#34;UTF-8\u0026#34;); viewResolver.setTemplateEngine(templateEngine); return viewResolver; } } 13.4、测试功能 1 2 3 4 @RequestMapping(\u0026#34;/\u0026#34;) public String index(){ return \u0026#34;index\u0026#34;; } 14、SpringMVC执行流程 14.1、SpringMVC常用组件 DispatcherServlet：前端控制器，不需要工程师开发，由框架提供 作用：统一处理请求和响应，整个流程控制的中心，由它调用其它组件处理用户的请求\nHandlerMapping：处理器映射器，不需要工程师开发，由框架提供 作用：根据请求的url、method等信息查找Handler，即控制器方法\nHandler：处理器，需要工程师开发 作用：在DispatcherServlet的控制下Handler对具体的用户请求进行处理\nHandlerAdapter：处理器适配器，不需要工程师开发，由框架提供 作用：通过HandlerAdapter对处理器（控制器方法）进行执行\nViewResolver：视图解析器，不需要工程师开发，由框架提供 作用：进行视图解析，得到相应的视图，例如：ThymeleafView、InternalResourceView、\nRedirectView\nView：视图 作用：将模型数据通过页面展示给用户\n14.2、DispatcherServlet初始化过程 DispatcherServlet 本质上是一个 Servlet，所以天然的遵循 Servlet 的生命周期。所以宏观上是 Servlet生命周期来进行调度。\n①初始化WebApplicationContext 所在类：org.springframework.web.servlet.FrameworkServlet\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 protected WebApplicationContext initWebApplicationContext() { WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null; if (this.webApplicationContext != null) { // A context instance was injected at construction time -\u0026gt; use it wac = this.webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac =(ConfigurableWebApplicationContext) wac; if (!cwac.isActive()) { // The context has not yet been refreshed -\u0026gt; provide services such as // setting the parent context, setting the application context id, etc if (cwac.getParent() == null) { // The context instance was injected without an explicit parent -\u0026gt; set // the root application context (if any; may be null) as the parent cwac.setParent(rootContext); } configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null) { // No context instance was injected at construction time -\u0026gt; see if one // has been registered in the servlet context. If one exists, it is assumed // that the parent context (if any) has already been set and that the // user has performed any initialization such as setting the context id wac = findWebApplicationContext(); } if (wac == null) { // No context instance is defined for this servlet -\u0026gt; create a local one // 创建WebApplicationContext wac = createWebApplicationContext(rootContext); } if (!this.refreshEventReceived) { // Either the context is not a ConfigurableApplicationContext with refresh // support or the context injected at construction time had already been // refreshed -\u0026gt; trigger initial onRefresh manually here. synchronized (this.onRefreshMonitor) { // 刷新WebApplicationContext onRefresh(wac); } } if (this.publishContext) { // Publish the context as a servlet context attribute. // 将IOC容器在应用域共享 String attrName = getServletContextAttributeName(); getServletContext().setAttribute(attrName, wac); } return wac; } ②创建WebApplicationContext 所在类：org.springframework.web.servlet.FrameworkServlet\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) { Class\u0026lt;?\u0026gt; contextClass = getContextClass(); if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException(\u0026#34;Fatal initialization error in servlet with name \u0026#39;\u0026#34; +getServletName() + \u0026#34;\u0026#39;: custom WebApplicationContext class [\u0026#34; + contextClass.getName() + \u0026#34;] is not of type ConfigurableWebApplicationContext\u0026#34;); } // 通过反射创建 IOC 容器对象 ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass); wac.setEnvironment(getEnvironment()); // 设置父容器 wac.setParent(parent); String configLocation = getContextConfigLocation(); if (configLocation != null) { wac.setConfigLocation(configLocation); } configureAndRefreshWebApplicationContext(wac); return wac; } ③DispatcherServlet初始化策略 FrameworkServlet创建WebApplicationContext后，刷新容器，调用onRefresh(wac)，此方法在\nDispatcherServlet中进行了重写，调用了initStrategies(context)方法，初始化策略，即初始化\nDispatcherServlet的各个组件\n所在类：org.springframework.web.servlet.DispatcherServlet\n1 2 3 4 5 6 7 8 9 10 11 protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); } 14.3、DispatcherServlet调用组件处理请求 ①processRequest() FrameworkServlet重写HttpServlet中的service()和doXxx()，这些方法中调用了\nprocessRequest(request, response)\n所在类：org.springframework.web.servlet.FrameworkServlet\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 protected final void processRequest(HttpServletRequest request,HttpServletResponse response)throws ServletException, IOException { long startTime = System.currentTimeMillis(); Throwable failureCause = null; LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext(); LocaleContext localeContext = buildLocaleContext(request); RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes requestAttributes = buildRequestAttributes(request,response, previousAttributes); WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(),new RequestBindingInterceptor()); initContextHolders(request, localeContext, requestAttributes); try { // 执行服务，doService()是一个抽象方法，在DispatcherServlet中进行了重写 doService(request, response); } catch (ServletException | IOException ex) { failureCause = ex; throw ex; } catch (Throwable ex) { failureCause = ex; throw new NestedServletException(\u0026#34;Request processing failed\u0026#34;, ex); } finally { resetContextHolders(request, previousLocaleContext, previousAttributes); if (requestAttributes != null) { requestAttributes.requestCompleted(); } logResult(request, response, failureCause, asyncManager); publishRequestHandledEvent(request, response, startTime, failureCause); } } ②doService() 所在类：org.springframework.web.servlet.DispatcherServlet\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @Override protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { logRequest(request); // Keep a snapshot of the request attributes in case of an include, // to be able to restore the original attributes after the include. Map\u0026lt;String, Object\u0026gt; attributesSnapshot = null; if (WebUtils.isIncludeRequest(request)) { attributesSnapshot = new HashMap\u0026lt;\u0026gt;(); Enumeration\u0026lt;?\u0026gt; attrNames = request.getAttributeNames(); while (attrNames.hasMoreElements()) { String attrName = (String) attrNames.nextElement(); if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) { attributesSnapshot.put(attrName,request.getAttribute(attrName)); } } } // Make framework objects available to handlers and view objects. request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE,getWebApplicationContext()); request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver); request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver); request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource()); if (this.flashMapManager != null) { FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request,response); if (inputFlashMap != null) { request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE,Collections.unmodifiableMap(inputFlashMap)); } request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap()); request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager); } RequestPath requestPath = null; if (this.parseRequestPath \u0026amp;\u0026amp; !ServletRequestPathUtils.hasParsedRequestPath(request)) { requestPath = ServletRequestPathUtils.parseAndCache(request); } try { // 处理请求和响应 doDispatch(request, response); } finally { if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { // Restore the original attribute snapshot, in case of an include. if (attributesSnapshot != null) { restoreAttributesAfterInclude(request, attributesSnapshot); } } if (requestPath != null) { ServletRequestPathUtils.clearParsedRequestPath(request); } } } ③doDispatch() 所在类：org.springframework.web.servlet.DispatcherServlet\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. /* mappedHandler：调用链 包含handler、interceptorList、interceptorIndex handler：浏览器发送的请求所匹配的控制器方法 interceptorList：处理控制器方法的所有拦截器集合 interceptorIndex：拦截器索引，控制拦截器afterCompletion()的执行 */ mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. // 通过控制器方法创建相应的处理器适配器，调用所对应的控制器方法 HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler. String method = request.getMethod(); boolean isGet = \u0026#34;GET\u0026#34;.equals(method); if (isGet || \u0026#34;HEAD\u0026#34;.equals(method)) { long lastModified = ha.getLastModified(request,mappedHandler.getHandler()); if (new ServletWebRequest(request,response).checkNotModified(lastModified) \u0026amp;\u0026amp; isGet) { return; } } // 调用拦截器的preHandle() if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. // 由处理器适配器调用具体的控制器方法，最终获得ModelAndView对象 mv = ha.handle(processedRequest, response,mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); // 调用拦截器的postHandle() mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { // As of 4.3, we\u0026#39;re processing Errors thrown from handler methods as well, // making them available for @ExceptionHandler methods and otherscenarios. dispatchException = new NestedServletException(\u0026#34;Handler dispatchfailed\u0026#34;, err); } // 后续处理：处理模型数据和渲染视图 processDispatchResult(processedRequest, response, mappedHandler, mv,dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler,new NestedServletException(\u0026#34;Handler processingfailed\u0026#34;, err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { // Instead of postHandle and afterCompletion if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { // Clean up any resources used by a multipart request. if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } } ④processDispatchResult() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 private void processDispatchResult(HttpServletRequest request,HttpServletResponse response,@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,@Nullable Exception exception) throws Exception { boolean errorView = false; if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { logger.debug(\u0026#34;ModelAndViewDefiningException encountered\u0026#34;,exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler(): null); mv = processHandlerException(request, response, handler, exception); errorView = (mv != null); } } // Did the handler return a view to render? if (mv != null \u0026amp;\u0026amp; !mv.wasCleared()) { // 处理模型数据和渲染视图 render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else { if (logger.isTraceEnabled()) { logger.trace(\u0026#34;No view rendering, null ModelAndView returned.\u0026#34;); } } if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { // Concurrent handling started during a forward return; } if (mappedHandler != null) { // Exception (if any) is already handled.. // 调用拦截器的afterCompletion() mappedHandler.triggerAfterCompletion(request, response, null); } } 14.4、SpringMVC的执行流程 \\1) 用户向服务器发送请求，请求被SpringMVC 前端控制器 DispatcherServlet捕获。\n\\2) DispatcherServlet对请求URL进行解析，得到请求资源标识符（URI），判断请求URI对应的映射：\na) 不存在\ni. 再判断是否配置了mvc:default-servlet-handler\nii. 如果没配置，则控制台报映射查找不到，客户端展示404错误\niii. 如果有配置，则访问目标资源（一般为静态资源，如：JS,CSS,HTML），找不到客户端也会展示404\n错误\nb) 存在则执行下面的流程\n\\3) 根据该URI，调用HandlerMapping获得该Handler配置的所有相关的对象（包括Handler对象以及\nHandler对象对应的拦截器），最后以HandlerExecutionChain执行链对象的形式返回。\n\\4) DispatcherServlet 根据获得的Handler，选择一个合适的HandlerAdapter。\n\\5) 如果成功获得HandlerAdapter，此时将开始执行拦截器的preHandler(…)方法【正向】\n\\6) 提取Request中的模型数据，填充Handler入参，开始执行Handler（Controller)方法，处理请求。\n在填充Handler的入参过程中，根据你的配置，Spring将帮你做一些额外的工作：\na) HttpMessageConveter： 将请求消息（如Json、xml等数据）转换成一个对象，将对象转换为指定\n的响应信息\nb) 数据转换：对请求消息进行数据转换。如String转换成Integer、Double等\nc) 数据格式化：对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等\nd) 数据验证： 验证数据的有效性（长度、格式等），验证结果存储到BindingResult或Error中\n\\7) Handler执行完成后，向DispatcherServlet 返回一个ModelAndView对象。\n\\8) 此时将开始执行拦截器的postHandle(\u0026hellip;)方法【逆向】。\n\\9) 根据返回的ModelAndView（此时会判断是否存在异常：如果存在异常，则执行\nHandlerExceptionResolver进行异常处理）选择一个适合的ViewResolver进行视图解析，根据Model\n和View，来渲染视图。\n\\10) 渲染视图完毕执行拦截器的afterCompletion(…)方法【逆向】。\n\\11) 将渲染结果返回给客户端。\n补充：ContextLoaderListener Spring提供了监听器ContextLoaderListener，实现ServletContextListener接口，可监听\nServletContext的状态，在web服务器的启动，读取Spring的配置文件，创建Spring的IOC容器。web\n应用中必须在web.xml中配置\n1 2 3 4 5 6 7 8 9 10 11 12 13 \u0026lt;listener\u0026gt; \u0026lt;!-- 配置Spring的监听器，在服务器启动时加载Spring的配置文件 Spring配置文件默认位置和名称：/WEB-INF/applicationContext.xml 可通过上下文参数自定义Spring配置文件的位置和名称 --\u0026gt; \u0026lt;listener-class\u0026gt;org.springframework.web.context.ContextLoaderListener\u0026lt;/listener-class\u0026gt; \u0026lt;/listener\u0026gt; \u0026lt;!--自定义Spring配置文件的位置和名称--\u0026gt; \u0026lt;context-param\u0026gt; \u0026lt;param-name\u0026gt;contextConfigLocation\u0026lt;/param-name\u0026gt; \u0026lt;param-value\u0026gt;classpath:spring.xml\u0026lt;/param-value\u0026gt; \u0026lt;/context-param\u0026gt; ","permalink":"https://ktzxy.top/posts/ku9r5s6zi6/","summary":"SpringMVC","title":"SpringMVC"},{"content":"1. MySQL 的视图 1.1. 简述 视图（view）是一个虚拟表，非真实存在，其本质是根据 SQL 语句获取动态的数据集，并为其命名，用户使用时只需使用视图名称即可获取结果集，并可以将其当作表来使用。\n数据库中只存放了视图的定义，而并没有存放视图中的数据（即只保存了查询的 SQL 逻辑，不保存查询结果）。这些数据存放在原来的表中。\n使用视图查询数据时，数据库系统会从原来的表中取出对应的数据。因此，视图中的数据是依赖于原来的表中的数据的。一旦表中的数据发生改变，显示在视图中的数据也会发生改变。\n1.2. 视图的语法 1.2.1. 创建视图 1 2 3 4 create [or replace] [algorithm = {undefined | merge | temptable}] view view_name [(column_list)] as select_statement [with [cascaded | local] check option] 参数说明：\nalgorithm：可选项，表示视图选择的算法。 view_name：表示要创建的视图名称。 column_list：可选项，指定视图中各个属性的名词，默认情况下与 SELECT 语句中的查询的属性相同。 select_statement：表示一个完整的查询语句，将查询记录导入视图中。 [with [cascaded | local] check option]：可选项，表示更新视图时要保证在该视图的权限范围之内。 示例：\n1 2 3 4 5 -- 创建视图测试相关的表与数据 CREATE OR REPLACE VIEW view_student_1 AS SELECT\tid,`name` FROM student WHERE id \u0026lt;= 10; 1.2.2. 查询视图 查看创建视图语句 1 SHOW CREATE VIEW 视图名称; 查看视图数据。跟普通的查询语句一样，将视图的名称作为表名即可 1 SELECT * FROM 视图名称 ...; 通过以下命令，查看当前数据库所有表和视图，表中会区分开真实的表与视图 1 2 3 4 5 6 7 8 9 mysql\u0026gt; SHOW FULL TABLES; +---------------------+------------+ | Tables_in_tempdb | Table_type | +---------------------+------------+ | score | BASE TABLE | | student | BASE TABLE | | student_course | BASE TABLE | | view_student_1 | VIEW | +---------------------+------------+ 1.2.3. 修改视图 修改视图是指修改数据库中已存在的表的定义。当基本表的某些字段发生改变时，可以通过修改视图来保持视图和基本表之间一致。MySQL中可以通过以下两种方式来修改视图。\n方式一：通过 CREATE OR REPLACE VIEW 语句 1 2 3 CREATE OR REPLACE VIEW 视图名称[(列名列表)] AS SELECT 语句 [ WITH [ CASCADED | LOCAL ] CHECK OPTION ] 示例：\n1 2 3 4 CREATE OR REPLACE VIEW view_student_1 AS SELECT id,`name`,`no` FROM student WHERE id \u0026lt;= 10; 方式二：通过 ALTER VIEW 语句 1 2 3 4 alter view 视图名 as select语句; ALTER VIEW 视图名称[(列名列表)] AS SELECT 语句 [ WITH [ CASCADED | LOCAL ] CHECK OPTION ] 示例：\n1 2 3 ALTER VIEW view_student_1 AS SELECT id,`name` FROM student WHERE id \u0026lt;= 10; 1.2.4. 重命名视图 1 RENAME TABLE 视图名 TO 新视图名; 示例：\n1 rename table view1_emp to my_view1; 1.2.5. 删除视图 1 DROP VIEW [IF EXISTS] 视图名称 [,视图名称] ...; Notes: 删除视图时，只能删除视图的定义，不会删除原表中的数据。\n示例：\n1 drop view if exists view_student_1; 1.3. 更新视图 可以通过 UPDATE、DELETE 或 INSERT 等语句去操作某些视图，从而更新基表的内容。对于可更新的视图，在视图中的行和基表中的行之间必须具有一对一的关系。如果视图包含下述结构中的任何一种，那么它就是不可更新的：\n聚合函数（SUM(), MIN(), MAX(), COUNT()等） DISTINCT GROUP BY HAVING UNION 或 UNION ALL 位于选择列表中的子查询 JOIN FROM 子句中的不可更新视图 WHERE 子句中的子查询，引用 FROM 子句中的表。 仅引用文字值（在该情况下，没有要更新的基本表） Notes: 视图中虽然可以更新数据，但是有很多的限制。一般情况下，最好将视图作为查询数据的虚拟表，而不要通过视图更新数据。因为，使用视图更新数据时，如果没有全面考虑在视图中更新数据的限制，就可能会造成数据更新失败。\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 -- ---------更新视图------- create or replace view view1_emp as select ename,job from emp; update view1_emp set ename = \u0026#39;周瑜\u0026#39; where ename = \u0026#39;鲁肃\u0026#39;; -- 可以修改 insert into view1_emp values(\u0026#39;孙权\u0026#39;,\u0026#39;文员\u0026#39;); -- 不可以插入 -- ----------视图包含聚合函数不可更新-------------- create or replace view view2_emp as select count(*) cnt from emp; insert into view2_emp values(100); update view2_emp set cnt = 100; -- ----------视图包含distinct不可更新--------- create or replace view view3_emp as select distinct job from emp; insert into view3_emp values(\u0026#39;财务\u0026#39;); -- ----------视图包含goup by 、having不可更新------------------ create or replace view view4_emp as select deptno ,count(*) cnt from emp group by deptno having cnt \u0026gt; 2; insert into view4_emp values(30,100); -- ----------------视图包含union或者union all不可更新---------------- create or replace view view5_emp as select empno,ename from emp where empno \u0026lt;= 1005 union select empno,ename from emp where empno \u0026gt; 1005; insert into view5_emp values(1015,\u0026#39;韦小宝\u0026#39;); -- -------------------视图包含子查询不可更新-------------------- create or replace view view6_emp as select empno,ename,sal from emp where sal = (select max(sal) from emp); insert into view6_emp values(1015,\u0026#39;韦小宝\u0026#39;,30000); -- ----------------------视图包含join不可更新----------------- create or replace view view7_emp as select dname,ename,sal from emp a join dept b on a.deptno = b.deptno; insert into view7_emp(dname,ename,sal) values(\u0026#39;行政部\u0026#39;,\u0026#39;韦小宝\u0026#39;,30000); -- --------------------视图包含常量文字值不可更新------------------- create or replace view view8_emp as select \u0026#39;行政部\u0026#39; dname,\u0026#39;杨过\u0026#39; ename; insert into view8_emp values(\u0026#39;行政部\u0026#39;,\u0026#39;韦小宝\u0026#39;); 其实在定义视图时，可以通过视图的检查选项指定条件，然后在插入、修改、删除数据时，都必须满足条件才能操作。\n1.4. 检查选项 当使用 WITH CHECK OPTION 子句创建视图时，MySQL 会通过视图检查正在更改的每条行数据，例如：插入，更新，删除，以使其符合视图的定义。MySQL 允许基于另一个视图创建视图，它还会检查依赖视图中的规则以保持一致性。为了确定检查的范围，mysql 提供了两个选项：CASCADED 和 LOCAL，默认值为 CASCADED\n1.4.1. CASCADED 级联操作。比如，v2视图是基于v1视图的，如果在v2视图创建的时候指定了检查选项为 cascaded，但是v1视图创建时未指定检查选项。则在执行检查时，不仅会检查v2，还会级联检查v2的关联视图v1。\n1.4.2. LOCAL 本地操作：比如，v2视图是基于v1视图的，如果在v2视图创建的时候指定了检查选项为 local，但是v1视图创建时未指定检查选项。则在执行检查时，只会检查v2，不会检查v2的关联视图v1。\n1.5. 视图运用案例 为了保证数据库表的安全性，开发人员在操作 tb_user 表时，只能看到的用户的基本字段，屏蔽手机号和邮箱两个字段。 1 2 3 create view tb_user_view as select id,`name`,profession,age,gender,`status`,createtime from tb_user; -- 查询 select * from tb_user_view; 查询每个学生所选修的课程（三张表联查），这个功能在很多的业务中都有使用到，为了简化操作，定义一个视图。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CREATE VIEW tb_stu_course_view AS SELECT s.NAME student_name, s.NO student_no, c.NAME course_name FROM student s, student_course sc, course c WHERE s.id = sc.studentid AND sc.courseid = c.id; -- 查询 SELECT * FROM tb_stu_course_view; 1.6. 视图总结 1.6.1. 视图的优缺点 优点：\n简化代码：可以把重复使用的查询封装成视图重复使用，同时可以使复杂的查询易于理解和使用。不仅简化用户对数据的理解，也可以简化用户的操作。那些被经常使用的查询可以被定义为视图，从而使得用户不必为以后的操作每次指定全部的条件。 数据安全性：如果一张表中有很多数据，很多信息不希望让所有人看到，此时可以使用视图视，如：社会保险基金表，可以用视图只显示姓名，地址，而不显示社会保险号和工资数等，可以对不同的用户，设定不同的视图。 逻辑数据独立，兼容老的表结构：视图可帮助用户屏蔽真实表结构变化带来的影响 缺点：\n性能差。数据库必须把视图的查询转化成对基本表的查询，如果这个视图是由一个复杂的多表查询所定义，那么，即使是视图的一个简单查询，数据库也把它变成一个复杂的结合体，需要花费一定的时间。 修改限制。当用户试图修改视图的某些行时，数据库必须把它转化为对基本表的某些行的修改。事实上，当从视图中插入或者删除时，情况也是这样。对于简单视图来说，这是很方便的；但是，对于比较复杂的视图，可能是不可修改的，特征如下： 有 UNIQUE 等集合操作符的视图。 有 GROUP BY 子句的视图。 有诸如 AVG\\SUM\\MAX 等聚合函数的视图。 使用 DISTINCT 关键字的视图。 连接表的视图（其中有些例外） 1.6.2. 视图的特点 视图的列可以来自不同的表，是表的抽象和在逻辑意义上建立的新关系。 视图是由基本表(实表)产生的表(虚表)。 视图的建立和删除不影响基本表。 对视图内容的更新(添加，删除和修改)直接影响基本表。 当视图来自多个基本表时，不允许添加和删除数据。 1.6.3. 视图的使用场景 重用SQL语句； 简化复杂的SQL操作。在编写查询后，可以方便的重用它而不必知道它的基本查询细节； 使用表的组成部分而不是整个表； 保护数据。可以给用户授予表的特定部分的访问权限而不是整个表的访问权限； 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。 2. 存储过程 2.1. 简述 MySQL 5.0 版本开始支持存储过程。存储过程是经过预编译并存储在数据库中的一段 SQL 语句的集合，功能强大，可以实现一些比较复杂的逻辑功能，类似于 JAVA 语言中的方法；调用存储过程可以简化应用开发 人员的很多工作，减少数据在数据库和应用服务器之间的传输，对于提高数据处理的效率是有好处的。\n存储过程就是数据库 SQL 语言层面的代码封装与重用。\n特性：\n函数的普遍特性：模块化，封装，代码复用。可以把某一业务SQL封装在存储过程中，需要用到的时候直接调用即可 存储过程可以输入输出参数，可以声明变量，有 if/else, case, while 等控制语句，通过编写存储过程，可以实现复杂的逻辑功能 速度快，只有首次执行需经过编译和优化步骤，后续被调用可以直接执行，省去以上步骤，减少网络交互，效率提升。如果涉及到多条SQL，每执行一次都是一次网络传输。而如果封装在存储过程中，只需要网络交互一次可能就可以了 2.2. 存储过程基础语法 2.2.1. 创建存储过程 1 2 3 4 5 6 delimiter 自定义结束符号 CREATE PROCEDURE 储存名称([ in, out, inout ] 参数名 数据类型...) BEGIN sql语句 END 自定义的结束符合 delimiter ; 示例：\n1 2 3 4 5 6 7 8 9 delimiter $$ create procedure proc01() begin select empno,ename from emp; end $$ delimiter ; -- 调用 call proc01(); Notes:\n特别注意：在语法中，变量声明、游标声明、handler声明是必须按照先后顺序书写的，否则创建存储过程出错。 在命令行中，执行创建存储过程的 SQL 时，需要通过关键字 delimiter 指定 SQL 语句的结束符。 2.2.2. 调用存储过程 1 CALL 存储过程名称 ([ 参数 ]); 示例：\n1 2 -- 调用 call proc01(); 2.2.3. 查看存储过程 查询指定数据库的存储过程及状态信息 1 SELECT * FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = \u0026#39;数据库名称\u0026#39;; 查询某个存储过程的定义 1 SHOW CREATE PROCEDURE 存储过程名称; 2.2.4. 删除存储过程 1 DROP PROCEDURE [ IF EXISTS ] 存储过程名称; 2.3. 变量 在 MySQL 中变量分为三种类型：系统变量、用户定义变量、局部变量。\n2.3.1. 系统变量 系统变量是 MySQL 服务器提供，不是由用户定义的，属于服务器层面。分为以下两种变量：\n全局变量(GLOBAL)：全局变量针对于所有的会话 会话变量(SESSION)：会话变量针对于单个会话，在另外一个会话窗口就不生效了 查看系统变量\n1 2 3 4 5 6 -- 查看所有系统变量 SHOW [ SESSION | GLOBAL ] VARIABLES; -- 可以通过LIKE模糊匹配方式查找变量 SHOW [ SESSION | GLOBAL ] VARIABLES LIKE \u0026#39;......\u0026#39;; -- 查看指定变量的值 SELECT @@[SESSION | GLOBAL].系统变量名; 设置系统变量\n1 2 SET [ SESSION | GLOBAL ] 系统变量名 = 值; SET @@[SESSION | GLOBAL].系统变量名 = 值; Notes: 如果没有指定 SESSION/GLOBAL，默认是 SESSION，会话变量。mysql 服务重新启动之后，所设置的全局参数会失效，若永久配置则需要在 /etc/my.cnf 文件中配置\n2.3.2. 用户（会话）定义变量 用户定义变量是用户自定义的变量，其作用域为当前连接（会话）。用户变量不用提前声明，使用时直接通过 @变量名称 声明即可。类比java的成员变量。\n变量赋值方式1：通过 SET 关键字 1 2 SET @var_name = expr [, @var_name = expr] ... ; SET @var_name := expr [, @var_name := expr] ... ; Notes: 赋值时，可以使用=，也可以使用:=。为了区分sql的运算符=，推荐使用:=\n变量赋值方式2：通过 SELECT 关键字 1 SELECT @var_name := expr [, @var_name := expr] ... ; 变量赋值方式3：通过 INTO 关键字查询表数据后赋值 1 SELECT 字段名 INTO @var_name FROM 表名; 变量的使用语法： 1 SELECT @var_name [,@var_name] ...; Notes: 用户定义的变量时无需对其进行声明或初始化，此时获取变量的值为 NULL\n使用示例：\n1 2 3 4 5 6 7 8 delimiter $$ create procedure proc04() begin set @var_name01 = \u0026#39;ZS\u0026#39;; -- 设置用户自定义会话变量 end $$ delimiter ; call proc04(); -- 调用存储过程 select @var_name01; -- 可以看到结果 2.3.3. 局部变量 局部变量是根据需要定义的在局部生效的变量，访问之前，需要 DECLARE 声明。可用作存储过程内的局部变量和输入参数，局部变量的范围是在其内声明的 BEGIN ... END 块。\n2.3.3.1. 变量定义语法 自定义局部变量，在 begin/end 块内有效。声明变量语法：\n1 declare var_name type [default var_value]; 参数说明：\nvar_name：变量名称 type：变量类型，即数据库字段类型：INT、BIGINT、CHAR、VARCHAR、DATE、TIME 等。 var_value：指定默认值（非必须） 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 declare nickname varchar(32); -- 示例 delimiter $$ create procedure proc02() begin declare var_name01 varchar(20) default \u0026#39;aaa\u0026#39;; -- 定义局部变量 set var_name01 = \u0026#39;MooN\u0026#39;; select var_name01; end $$ delimiter ; -- 调用存储过程 call proc02(); 2.3.3.2. 变量赋值语法 变量赋值方式1：通过 SET 关键字 1 2 SET 变量名 = 值; SET 变量名 := 值; 变量赋值方式2：通过 SELECT..INTO 语句为变量赋值 1 2 3 4 5 select col_name [...] into var_name[,...] from table_name where condition; 参数说明：\ncol_name 参数表示查询的字段名称 var_name 参数是变量的名称 table_name 参数指表的名称 condition 参数指查询条件 Notes: 注意：当将查询结果赋值给变量时，该查询语句的返回结果只能是单行单列！\n示例：\n1 2 3 4 5 6 7 8 9 10 delimiter $$ create procedure proc03() begin declare my_ename varchar(20) ; select ename into my_ename from emp where empno=1001; select my_ename; end $$ delimiter ; -- 调用存储过程 call proc03(); 2.4. 存储过程的参数 存储过程参数的类型，主要分为以下三种：IN、OUT、INOUT。具体的含义如下：\n类型 说明 in 输入参数。该值传到存储过程的过程里面去，在存储过程中修改该参数的值不能被返回。不指定时默认 out 输出参数。该值可在存储过程内部被改变，并向外输出 inout 输入输出参数。既能作为输入的参数，也可以作为输出参数 用法：\n1 2 3 4 CREATE PROCEDURE 存储过程名称 ([ IN/OUT/INOUT 参数名 参数类型 ]) BEGIN -- SQL语句 END ; 2.4.1. in 类型参数 in 类型表示传入存储过程的参数，可以传入数值或者变量，即使传入变量，并不会更改变量的值，可以内部更改，仅仅作用在函数范围内。\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 -- 封装有参数的存储过程，传入员工编号，查找员工信息 delimiter $$ create procedure dec_param01(in param_empno varchar(20)) begin select * from emp where empno = param_empno; end $$ delimiter ; call dec_param01(\u0026#39;1001\u0026#39;); -- 封装有参数的存储过程，可以通过传入部门名和薪资，查询指定部门，并且薪资大于指定值的员工信息 delimiter $$ create procedure dec_param0x(in dname varchar(50), in sal decimal(7,2)) begin select * from dept a, emp b where b.sal \u0026gt; sal and a.dname = dname; end $$ delimiter ; call dec_param0x(\u0026#39;学工部\u0026#39;,20000); 2.4.2. out 类型参数 out 类型的参数表示从存储过程内部传值给调用者\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 -- ---------传出参数：out--------------------------------- use mysql7_procedure; -- 封装有参数的存储过程，传入员工编号，返回员工名字 delimiter $$ create procedure proc08(in empno int , out out_ename varchar(50)) begin select ename into out_ename from emp where emp.empno = empno; end $$ delimiter ; call proc08(1001, @o_ename); select @o_ename; -- 封装有参数的存储过程，传入员工编号，返回员工名字和薪资 delimiter $$ create procedure proc09(in empno int, out out_ename varchar(50), out out_sal decimal(7,2)) begin select ename,sal into out_ename,out_sal from emp where emp.empno = empno; end $$ delimiter ; call proc09(1001, @o_dname,@o_sal); select @o_dname; select @o_sal; 2.4.3. inout 类型参数 inout 类型表示从外部传入的参数经过修改后可以返回的变量，既可以使用传入变量的值也可以修改变量的值（即使函数执行完）\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 -- 传入员工名，拼接部门号，传入薪资，求出年薪 delimiter $$ create procedure proc10(inout inout_ename varchar(50), inout inout_sal int) begin select concat(deptno,\u0026#34;_\u0026#34;,inout_ename) into inout_ename from emp where ename = inout_ename; set inout_sal = inout_sal * 12; end $$ delimiter ; set @inout_ename = \u0026#39;关羽\u0026#39;; set @inout_sal = 3000; call proc10(@inout_ename, @inout_sal) ; select @inout_ename ; select @inout_sal ; 2.5. 流程控制 - if 条件判断 IF 语句包含多个条件判断，根据结果为 TRUE、FALSE 执行语句，与编程语言中的if、else if、else 语法类似，其语法格式如下：\n1 2 3 4 5 IF search_condition_1 THEN statement_list_1 [ELSEIF search_condition_2 THEN statement_list_2] ... [ELSE statement_list_n] END IF 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 -- 判断存储流程输入 delimiter $$ create procedure proc_12_if(in score int) begin if score \u0026lt; 60 then select \u0026#39;不及格\u0026#39;; elseif score \u0026lt; 80 then select \u0026#39;及格\u0026#39; ; elseif score \u0026gt;= 80 and score \u0026lt; 90 then select \u0026#39;良好\u0026#39;; elseif score \u0026gt;= 90 and score \u0026lt;= 100 then select \u0026#39;优秀\u0026#39;; else select \u0026#39;成绩错误\u0026#39;; end if; end $$ delimiter ; -- 调用 call proc_12_if(80); -- 查询数据 delimiter $$ create procedure proc12_if(in in_ename varchar(50)) begin declare result varchar(20); declare var_sal decimal(7,2); select sal into var_sal from emp where ename = in_ename; if var_sal \u0026lt; 10000 then set result = \u0026#39;试用薪资\u0026#39;; elseif var_sal \u0026lt; 30000 then set result = \u0026#39;转正薪资\u0026#39;; else set result = \u0026#39;元老薪资\u0026#39;; end if; select result; end$$ delimiter ; -- 调用 call proc12_if(\u0026#39;庞统\u0026#39;); 2.6. 流程控制 - case 条件判断 CASE 是另一个条件判断的语句，类似于编程语言中的switch语法。语法结构如下：\n语法1：当 case_value = when_value 时，执行相应的 statement_list 逻辑 1 2 3 4 5 6 case case_value when when_value then statement_list [when when_value then statement_list] ... [else statement_list] end case 语法2：当 search_condition 为 true 时，执行相应的 statement_list 逻辑 1 2 3 4 5 6 case when search_condition then statement_list [when search_condition then statement_list] ... [else statement_list] end case Tips: 如果判定条件有多个，多个条件之间，可以使用 and 或 or 进行连接。\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 -- 语法1 delimiter $$ create procedure proc14_case(in pay_type int) begin case pay_type when 1 then select \u0026#39;微信支付\u0026#39;; when 2 then select \u0026#39;支付宝支付\u0026#39;; when 3 then select \u0026#39;银行卡支付\u0026#39;; else select \u0026#39;其他方式支付\u0026#39;; end case ; end $$ delimiter ; -- 调用 call proc14_case(2); call proc14_case(4); -- 语法2 delimiter $$ create procedure proc_15_case(in score int) begin case when score \u0026lt; 60 then select \u0026#39;不及格\u0026#39;; when score \u0026lt; 80 then select \u0026#39;及格\u0026#39;; when score \u0026gt;= 80 and score \u0026lt; 90 then select \u0026#39;良好\u0026#39;; when score \u0026gt;= 90 and score \u0026lt;= 100 then select \u0026#39;优秀\u0026#39;; else select \u0026#39;成绩错误\u0026#39;; end case; end $$ delimiter ; -- 调用 call proc_15_case(88); 2.7. 流程控制 - 循环 2.7.1. 简述 循环是一段在程序中只出现一次，但可能会连续运行多次的代码。 循环中的代码会运行特定的次数，或者是运行到特定条件成立时结束循环 存储过程的循环分类：\nwhile repeat loop 循环控制（结束/跳过）：\nleave 类似于java语言的 break，跳出，结束当前所在的循环 iterate 类似于java语言的 continue，继续，结束本次循环，继续下一次 2.7.2. while 循环 while 循环是有条件的循环控制语句。满足条件后，再执行循环体中的SQL语句。语法格式：\n1 2 3 4 5 6 [标签名:] WHILE 循环条件 DO 循环体; END WHILE [标签名]; 注：以上标签名可以省略。标签名一般在leave结束与iterate跳过循环时使用\n示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 -- -------存储过程-while delimiter $$ create procedure proc16_while1(in insertcount int) begin declare i int default 1; label:while i\u0026lt;=insertcount do insert into user(uid,username,`password`) values(i,concat(\u0026#39;user-\u0026#39;,i),\u0026#39;123456\u0026#39;); set i=i+1; end while label; end $$ delimiter ; call proc16_while(10); -- -------存储过程-while + leave delimiter $$ create procedure proc16_while2(in insertcount int) begin declare i int default 1; label:while i\u0026lt;=insertcount do insert into user(uid,username,`password`) values(i,concat(\u0026#39;user-\u0026#39;,i),\u0026#39;123456\u0026#39;); if i=5 then leave label; end if; set i=i+1; end while label; end $$ delimiter ; call proc16_while2(10); -- -------存储过程-while+iterate delimiter $$ create procedure proc16_while3(in insertcount int) begin declare i int default 1; label:while i\u0026lt;=insertcount do set i=i+1; if i=5 then iterate label; end if; insert into user(uid,username,`password`) values(i,concat(\u0026#39;user-\u0026#39;,i),\u0026#39;123456\u0026#39;); end while label; end $$ delimiter ; call proc16_while3(10); 2.7.3. repeat 循环 repeat 是有条件的循环控制语句，当满足 until 声明的条件的时候，则退出循环。语法格式：\n1 2 3 4 5 [标签名:] REPEAT 循环体; UNTIL 条件表达式 END REPEAT [标签名]; 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -- -------存储过程-循环控制-repeat delimiter $$ create procedure proc18_repeat(in insertCount int) begin declare i int default 1; label:repeat insert into user(uid, username, password) values(i,concat(\u0026#39;user-\u0026#39;,i),\u0026#39;123456\u0026#39;); set i = i + 1; until i \u0026gt; insertCount end repeat label; select \u0026#39;循环结束\u0026#39;; end $$ delimiter ; call proc18_repeat(100); 2.7.4. loop 循环 LOOP 实现简单的循环，如果不在 SQL 逻辑中增加退出循环的条件，可以用其来实现简单的死循环。LOOP 可以配合以下两个语句使用：\nLEAVE ：配合循环使用，退出循环。 ITERATE：必须用在循环中，作用是跳过当前循环剩下的语句，直接进入下一次循环。 语法格式：\n1 2 3 4 5 6 7 8 9 [标签:] LOOP 循环体; IF 条件表达式 THEN LEAVE [标签]; END IF; END LOOP; 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 -- -------存储过程-循环控制-loop delimiter $$ create procedure proc19_loop(in insertCount int) begin declare i int default 1; label:loop insert into user(uid, username, password) values(i,concat(\u0026#39;user-\u0026#39;,i),\u0026#39;123456\u0026#39;); set i = i + 1; if i \u0026gt; 5 then leave label; end if; end loop label; select \u0026#39;循环结束\u0026#39;; end $$ delimiter ; call proc19_loop(10); 示例2：计算从1到n之间的偶数累加的值，n为传入的参数值。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 create procedure p10(in n int) begin declare total int default 0; sum:loop if n\u0026lt;=0 then leave sum; end if; if n%2 = 1 then set n := n - 1; iterate sum; end if; set total := total + n; set n := n - 1; end loop sum; select total; end; call p10(100); 2.8. 游标 游标(cursor)是用来存储查询结果集的数据类型，在存储过程和函数中可以使用游标对结果集进行循环的处理，相当于指针，指向一行一行数据。游标的使用包括游标的声明、OPEN、FETCH 和 CLOSE\n2.8.1. 基础语法 1 2 3 4 5 6 7 8 -- 声明语法 declare cursor_name cursor for select_statement -- 打开语法 open cursor_name -- 取值语法 fetch cursor_name into var_name [, var_name] ... -- 关闭语法 close cursor_name 参数说明：\ncursor_name：游标的名称 select_statement：查询数据表返回的结果集 var_name：游标循环结果集每一行数据时，赋值的变量 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 use mysql7_procedure; delimiter $$ create procedure proc20_cursor(in in_dname varchar(50)) begin -- 定义局部变量 declare var_empno varchar(50); declare var_ename varchar(50); declare var_sal decimal(7,2); -- 声明游标 declare my_cursor cursor for select empno , ename, sal from dept a ,emp b where a.deptno = b.deptno and a.dname = in_dname; -- 打开游标 open my_cursor; -- 通过游标获取每一行数据 label:loop fetch my_cursor into var_empno, var_ename, var_sal; select var_empno, var_ename, var_sal; end loop label; -- 关闭游标 close my_cursor; end -- 调用存储过程 call proc20_cursor(\u0026#39;销售部\u0026#39;); 示例2：游标的使用取每行记录(多字段)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 delimiter $ create PROCEDURE phoneDeal() BEGIN DECLARE id varchar(64); -- id DECLARE phone1 varchar(16); -- phone DECLARE password1 varchar(32); -- 密码 DECLARE name1 varchar(64); -- id -- 遍历数据结束标志 DECLARE done INT DEFAULT FALSE; -- 游标 DECLARE cur_account CURSOR FOR select phone,password,name from account_temp; -- 将结束标志绑定到游标 DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; -- 打开游标 OPEN cur_account; -- 遍历 read_loop: LOOP -- 取值 取多个字段 FETCH NEXT from cur_account INTO phone1,password1,name1; IF done THEN LEAVE read_loop; END IF; -- 你自己想做的操作 insert into account(id,phone,password,name) value(UUID(),phone1,password1,CONCAT(name1,\u0026#39;的家长\u0026#39;)); END LOOP; CLOSE cur_account; END $ 注意：delimiter关键字后面必须有空格，否则在某些环境或某些情况下使用shell脚本调用执行会出现问题\n2.9. 异常处理 - HANDLER 条件处理程序 条件处理程序（Handler）可以用来定义在流程控制结构执行过程中遇到问题时相应的处理步骤。具体语法为：\n2.9.1. 语法定义 MySql存储过程也提供了对异常处理的功能：通过定义 HANDLER 来完成异常声明的实现。\n语法格式：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 DECLARE handler_action HANDLER FOR condition_value [, condition_value] ... statement -- handler_action 取值 handler_action: { CONTINUE -- 继续执行剩余的代码 | EXIT -- 直接终止程序 | UNDO -- 不支持 } -- condition_value 取值 condition_value: { mysql_error_code | SQLSTATE [VALUE] sqlstate_value | condition_name | SQLWARNING | NOT FOUND | SQLEXCEPTION } 参数说明：\nhandler_action 条件处理程序的名称，取值： CONTINUE: 继续执行当前程序 EXIT: 终止执行当前程序 condition_value 条件的取值： SQLSTATE sqlstate_value: 状态码，如 02000 SQLWARNING: 所有以01开头的SQLSTATE代码的简写 NOT FOUND: 所有以02开头的SQLSTATE代码的简写 SQLEXCEPTION: 所有没有被SQLWARNING 或 NOT FOUND捕获的SQLSTATE代码的简写 具体的错误状态码，可以参考官方文档：\n5.7版本：https://dev.mysql.com/doc/refman/5.7/en/declare-handler.html https://dev.mysql.com/doc/refman/8.0/en/declare-handler.html https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html 2.9.2. 存储过程中使用 handler 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 delimiter $$ create procedure proc20_cursor(in in_dname varchar(50)) begin -- 定义局部变量 declare var_empno int; declare var_ename varchar(50); declare var_sal decimal(7,2); declare flag int default 1; -- --------------------- -- 声明游标 declare my_cursor cursor for select empno,ename,sal from dept a, emp b where a.deptno = b.deptno and a.dname = in_dname; -- 定义句柄，当数据未发现时将标记位设置为0 declare continue handler for NOT FOUND set flag = 0; -- 打开游标 open my_cursor; -- 通过游标获取值 label:loop fetch my_cursor into var_empno, var_ename,var_sal; -- 判断标志位 if flag = 1 then select var_empno, var_ename,var_sal; else leave label; end if; end loop label; -- 关闭游标 close my_cursor; end $$; delimiter ; call proc21_cursor_handler(\u0026#39;销售部\u0026#39;); 2.10. 获取当前登陆用户 user() 函数用于是取得当前登陆的用户。一般在存储过程中使用，获取值。\n1 select user() into 变量名; 2.11. 查询最后插入的数据的id last_insert_id() 函数可以获得刚插入的数据的id值，这个是session 级的，并发没有问题。\n1 2 3 insert xxxxx....; select last_insert_id() into 变量名; -- 上面语句可以将最近插入的数据id赋值给变量，后面可以进行对应的逻辑处理 2.12. 存储过程综合示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 /* 创建下个月的每天对应的表user_2021_11_01、user_2021_11_02、... 需求描述： 我们需要用某个表记录很多数据，比如记录某某用户的搜索、购买行为(注意，此处是假设用数据库保存)， 当每天记录较多时，如果把所有数据都记录到一张表中太庞大，需要分表，我们的要求是，每天一张表，存当天的统计数据， 就要求提前生产这些表——每月月底创建下一个月每天的表！ */ -- 思路：循环构建表名 user_2021_11_01 到 user_2020_11_30；并执行create语句。 drop procedure if exists proc22_demo; delimiter $$ create procedure proc22_demo() begin declare next_year int; declare next_month int; declare next_month_day int; declare next_month_str char(2); declare next_month_day_str char(2); -- 处理每天的表名 declare table_name_str char(10); declare t_index int default 1; -- declare create_table_sql varchar(200); -- 获取下个月的年份 set next_year = year(date_add(now(),INTERVAL 1 month)); -- 获取下个月是几月 set next_month = month(date_add(now(),INTERVAL 1 month)); -- 下个月最后一天是几号 set next_month_day = dayofmonth(LAST_DAY(date_add(now(),INTERVAL 1 month))); if next_month \u0026lt; 10 then set next_month_str = concat(\u0026#39;0\u0026#39;,next_month); else set next_month_str = concat(\u0026#39;\u0026#39;,next_month); end if; while t_index \u0026lt;= next_month_day do if (t_index \u0026lt; 10) then set next_month_day_str = concat(\u0026#39;0\u0026#39;,t_index); else set next_month_day_str = concat(\u0026#39;\u0026#39;,t_index); end if; -- 2021_11_01 set table_name_str = concat(next_year,\u0026#39;_\u0026#39;,next_month_str,\u0026#39;_\u0026#39;,next_month_day_str); -- 拼接create sql语句 set @create_table_sql = concat( \u0026#39;create table user_\u0026#39;, table_name_str, \u0026#39;(`uid` INT ,`ename` varchar(50) ,`information` varchar(50)) COLLATE=\\\u0026#39;utf8_general_ci\\\u0026#39; ENGINE=InnoDB\u0026#39;); -- FROM后面不能使用局部变量！ prepare create_table_stmt FROM @create_table_sql; execute create_table_stmt; DEALLOCATE prepare create_table_stmt; set t_index = t_index + 1; end while; end $$ delimiter ; call proc22_demo(); 2.13. 存储过程优化思路 尽量利用一些 sql 语句来替代一些小循环，例如聚合函数，求平均函数等。 中间结果存放于临时表，加索引。 少使用游标。sql 是个集合语言，对于集合运算具有较高性能。而 cursors 是过程运算。比如对一个 100 万行的数据进行查询。游标需要读表 100 万次，而不使用游标则只需要少量几次读取。 事务越短越好。sqlserver 支持并发操作。如果事务过多过长，或者隔离级别过高，都会造成并发操作的阻塞，死锁。导致查询极慢，cpu 占用率极地。 使用 try-catch 处理错误异常。 查找语句尽量不要放在循环内 3. 存储函数 3.1. 概述 MySQL存储函数（自定义函数），函数一般用于计算和返回一个值，可以将经常需要使用的计算或功能写成一个函数。存储函数和存储过程一样，都是在数据库中定义一些 SQL 语句的集合。\n3.2. 创建语法 在MySQL中，创建存储函数使用 create function 关键字，其基本形式如下：\n1 2 3 4 5 6 create function func_name ([param_name type[,...]]) returns type [characteristic ...] begin routine_body end; 参数说明：\nfunc_name：存储函数的名称。 param_name type：可选项，指定存储函数的参数。type参数用于指定存储函数的参数类型，该类型可以是MySQL数据库中所有支持的类型。 RETURNS type：指定返回值的类型。 characteristic：可选项，指定存储函数的特性。 DETERMINISTIC：相同的输入参数总是产生相同的结果 NO SQL：不包含 SQL 语句 READS SQL DATA：包含读取数据的语句，但不包含写入数据的语句 routine_body：SQL 代码内容。 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 -- 创建存储函数-没有输输入参数 drop function if exists myfunc1_emp; delimiter $$ create function myfunc1_emp() returns int begin declare cnt int default 0; select count(*) into cnt from emp; return cnt; end $$ delimiter ; -- 调用存储函数 select myfunc1_emp(); -- 创建存储过程-有输入参数 drop function if exists myfunc2_emp; delimiter $$ create function myfunc2_emp(in_empno int) returns varchar(50) begin declare out_name varchar(50); select ename into out_name from emp where empno = in_empno; return out_name; end $$ delimiter ; select myfunc2_emp(1008); 3.3. 注意问题 在 mysql8.0 版本中 binlog 默认是开启的，一旦开启了，mysql 就要求在定义存储过程时，需要指定 characteristic 特性，否则就会报如下错误： 如果创建时出现错误，执行以下命令 1 set global log_bin_trust_function_creators=TRUE; -- 信任子程序的创建者 3.4. 存储函数与存储过程的区别 存储函数有且只有一个返回值，而存储过程可以有多个返回值，也可以没有返回值。 存储函数只能有输入参数，而且不能带in，而存储过程可以有多个in、out、inout参数。 存储过程中的语句功能更强大，存储过程可以实现很复杂的业务逻辑，而函数有很多限制，如不能在函数中使用insert、update、delete、create等语句； 存储函数只完成查询的工作，可接受输入参数并返回一个结果，也就是函数实现的功能针对性比较强。 存储过程可以调用存储函数。但函数不能调用存储过程。 存储过程一般是作为一个独立的部分来执行(call调用)。而函数可以作为查询语句的一个部分来调用 4. 触发器 4.1. 概述 触发器，就是一种特殊的存储过程。触发器和存储过程一样是一个能够完成特定功能、存储在数据库服务器上的 SQL 片段，但是触发器无需调用，当对数据库表中的数据执行 DML 操作时自动触发这个 SQL 片段的执行，无需手动调用。\n在MySQL中，只有执行insert,delete,update操作时才能触发触发器的执行。\n触发器的这种特性可以协助应用在数据库端确保数据的完整性，日志记录，数据校验等操作。\n使用别名 OLD 和 NEW 来引用触发器中发生变化的记录内容，这与其他的数据库是相似的。现在触发器还只支持行级触发，不支持语句级触发\n4.2. 触发器的特性 什么条件会触发：I、D、U 什么时候触发：在增删改前或者后 触发频率：针对每一行执行 触发器定义在表上，附着在表上 4.3. 触发器语法定义 4.3.1. 创建语法 创建只有一个执行语句的触发器 1 2 3 CREATE TRIGGER 触发器名 BEFORE | AFTER 触发事件[ INSERT | UPDATE | DELETE ] ON 表名 FOR EACH ROW 执行语句; 创建有多个执行语句的触发器 1 2 3 4 5 CREATE TRIGGER 触发器名 BEFORE | AFTER 触发事件[ INSERT | UPDATE | DELETE ] ON 表名 FOR EACH ROW BEGIN 执行语句列表 END; 参数说明：\n触发事件：取值：insert | update | delete 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 -- 如果触发器存在，则先删除 drop trigger if exists trigger_test1; -- 创建触发器trigger_test1 create trigger trigger_test1 after insert on user -- 触发时机：当添加user表数据时触发 for each row insert into user_logs values(NULL,now(), \u0026#39;有新用户注册\u0026#39;); -- 添加数据，触发器自动执行并添加日志代码 insert into user values(1,\u0026#39;张三\u0026#39;,\u0026#39;123456\u0026#39;); -- 如果触发器trigger_test2存在，则先删除 drop trigger if exists trigger_test2; -- 创建触发器trigger_test2 delimiter $$ create trigger trigger_test2 after update on user -- 触发时机：当修改user表数据时触发 for each row -- 每一行 begin insert into user_logs values(NULL,now(), \u0026#39;用户修改发生了修改\u0026#39;); end $$ delimiter ; -- 添加数据，触发器自动执行并添加日志代码 update user set password = \u0026#39;888888\u0026#39; where uid = 1; 4.3.2. 操作关键字 (NEW|OLD) MySQL 中定义了 NEW 和 OLD，用来表示触发器的所在表中，触发了触发器的那一行数据，来引用触发器中发生变化的记录内容。\n触发器类型 触发器类型NEW 和 OLD 的使用 INSERT 型触发器 NEW 表示将要或者已经新增的数据 UPDATE 型触发器 OLD 表示修改之前的数据，NEW 表示将要或已经修改后的数据 DELETE 型触发器 OLD 表示将要或者已经删除的数据 使用方法：\nNEW.columnName：获取新增数据某一列的值，columnName为相应数据表某一列名 示例：\n1 2 3 4 5 6 create trigger trigger_test3 after insert on user for each row insert into user_logs values(NULL,now(),concat(\u0026#39;有新用户添加，信息为:\u0026#39;,NEW.uid,NEW.username,NEW.password)); -- 测试 insert into user values(4,\u0026#39;赵六\u0026#39;,\u0026#39;123456\u0026#39;); 4.3.3. 查看触发器 语法：\n1 show triggers; 4.3.4. 删除触发器 如果没有指定 schema_name，默认为当前数据库。语法：\n1 drop trigger [if exists] [schema_name.]trigger_name; 示例：\n1 drop trigger if exists trigger_test1; 4.4. 触发器注意事项 MYSQL 中触发器中不能对本表进行 insert, update, delete 操作，以免递归循环触发 尽量少使用触发器，假设触发器触发每次执行1s，insert table 500条数据，那么就需要触发500次触发器，光是触发器执行的时间就花费了500s，而insert 500条数据一共是1s，那么这个insert的效率就非常低了。 触发器是针对每一行的；对增删改非常频繁的表上切记不要使用触发器，因为它会非常消耗资源。 5. 分区表(了解) Tips: 此知识点只需要了解，实际项目的应用极少\n5.1. 简介 分区是指根据一定的规则，数据库把一个表分解成多个更小的、更容易管理的部分。就访问数据库的应用而言，逻辑上只有一个表或一个索引，但是实际上这个表可能由数 10 个物理分区对象组成，每个分区都是一个独立的对象，可以独自处理，可以作为表的一部分进行处理。\n分区表是一个独立的逻辑表，但是底层由多个物理子表组成。实现分区的代码实际上是对一组底层表的的封装。对分区表的请求，都会转化成对存储引擎的接口调用。分区对于 SQL 层来说是一个完全封装底层实现的黑盒子，对应用是透明的。从底层的文件系统可以看出，每一个分区表都有一个使用#分隔命名的表文件。\nMySQL 在创建表时使用PARTITION BY子句定义每个分区存放的数据。在执行查询的时候，优化器根据分区定义过滤那些数据不在的分区，这样查询就无须扫描所有分区。\n分区的一个主要目的是将数据按照一个较粗的粒度分在不同的表中。另外，也方便一次批量删除整个分区的数据。\n5.2. 分区表的优点与限制 分区表的优点：\n当表非常大以至于无法全部都放在内存中，或者只在表的最后部分有热点数据，其他均是历史数据。此时分区表与单个磁盘或文件系统分区相比，可以存储更多的数据。 对于那些已经失去保存意义的数据，通常可以通过删除与那些数据有关的分区，很容易地删除那些数据。相反地，在某些情况下，添加新数据的过程又可以通过为那些新数据专门增加一个新的分区，来很方便地实现。 一些查询可以得到极大的优化，这主要是借助于满足一个给定 WHERE 语句的数据可以只保存在一个或多个分区内，这样在查找时就不用查找其他剩余的分区。因为分区可以在创建了分区表后进行修改，所以在第一次配置分区方案时不曾这么做时，可以重新组织数据，来提高那些常用查询的效率。 涉及到例如 SUM() 和 COUNT() 这样聚合函数的查询，可以很容易地进行并行处理。这种查询的一个简单例子如 SELECT salesperson_id, COUNT (orders) as order_total FROM sales GROUP BY salesperson_id;。通过“并行”，这意味着该查询可以在每个分区上同时进行，最终结果只需通过总计所有分区得到的结果。 通过跨多个磁盘来分散数据查询，来获得更大的查询吞吐量。 分区表的数据更容易维护。例如，想批量删除大量数据可以使用清除整个分区的方式。另外，还可以对一个独立分区进行优化、检查、修复等操作。 分区表的数据可以分布在不同的物理设备上，从而高效地利用多个硬件设备。可以使用分区表来避免某些特殊的瓶颈，例如 InnoDB 的单个索引的互斥访问、ext3 文件系统的 inode 锁竞争等。 如果需要，还可以备份和恢复独立的分区，这在非常大的数据集的场景下效果非常好。 分区表的限制：\n一个表最多只能有 1024 个分区。 MySQL 5.1 中，分区表达式必须是整数，或者返回整数的表达式。在 MySQL 5.5 中提供了非整数表达式分区的支持。 如果分区字段中有主键或者唯一索引的列，那么所有主键列和唯一索引列都必须包含进来。即：分区字段要么不包含主键或者索引列，要么包含全部主键和索引列。 分区表中无法使用外键约束。 MySQL的分区适用于一个表的所有数据和索引，不能只对表数据分区而不对索引分区，也不能只对索引分区而不对表分区，也不能只对表的一部分数据分区。 5.3. 分区表的原理 分区表由多个相关的底层表实现，这些底层表也是由句柄对象（Handlerobject)表示，所以也可以直接访问各个分区。存储引擎管理分区的各个底层表和管理普通表一样（所有的底层表都必须使用相同的存储引擎)，分区表的索引只是在各个底层表上各自加上一个完全相同的索引。从存储引擎的角度来看，底层表和一个普通表没有任何不同，存储引擎也无须知道这是一个普通表还是一个分区表的一部分。分区表上的操作按照下面的操作逻辑进行:\n虽然每个操作都会“先打开并锁住所有的底层表”，但这并不是说分区表在处理过程中是锁住全表的。如果存储引擎能够自己实现行级锁，例如 InnoDB，则会在分区层释放对应表锁。这个加锁和解锁过程与普通 InnoDB 上的查询类似。\n5.4. 分区表的类型 5.4.1. MySQL 支持的分区表 RANGE 分区：基于属于一个给定连续区间的列值，把多行分配给分区。 LIST 分区：类似于按 RANGE 分区，区别在于 LIST 分区是基于列值匹配一个离散值集合中的某个值来进行选择。 HASH 分区：基于用户定义的表达式的返回值来进行选择的分区，该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含 MySQL 中有效的、产生非负整数值的任何表达式。 KEY 分区：类似于按 HASH 分区，区别在于 KEY 分区只支持计算一列或多列，且 MySQL 服务器提供其自身的哈希函数。必须有一列或多列包含整数值。 复合分区/子分区：目前只支持 RANGE 和 LIST 的子分区，且子分区的类型只能为 HASH 和 KEY。 5.4.2. 查询数据库表是否支持分区 在进行分区之前，可以使用如下语句检查数据库表是否支持分区：\n1 2 3 4 5 6 mysql\u0026gt; show variables like \u0026#39;%partition%\u0026#39;; +-------------------+-------+ | Variable_name | Value | +-------------------+-------+ | have_partitioning | YES | +-------------------+-------+ 5.4.3. 分区的基本语法 RANGE 分区 1 2 3 4 5 6 7 8 CREATE TABLE test ( order_date DATETIME NOT NULL, ）ENGINE=InnoDB PARTITION BY RANGE(YEAR(order_date))( PARTITION p_0 VALUES LESS THAN (2010) , PARTITION p_1 VALUES LESS THAN (2011), PARTITION p_2 VALUES LESS THAN (2012), PARTITION p_other VALUES LESS THAN MAXVALUE); LIST 分区(类似枚举) 1 2 3 4 5 6 CREATE TABLE h2 ( c1 INT, c2 INT PARTITION BY LIST(c1) ( PARTITION p0 VALUES IN (1, 4, 7), PARTITION p1 VALUES IN (2, 5, 8)); range 和 List 都是整数类型分区，其实 range 和 List 也支持非整数分区，但是要结合 COLUMN 分区，支持整形、日期、字符串 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 CREATE TABLE emp_date( id INT NOT NULL, ename VARCHAR (30), hired DATE NOT NULL DEFAULT \u0026#39;1970-01-01\u0026#39;, separated DATE NOT NULL DEFAULT \u0026#39;9999-12-31\u0026#39;, job VARCHAR(30) NOT NULL, store_id INT NOT NULL) PARTITION BY RANGE COLUMNS (separated)( PARTITION pO VALUES LESS THAN (\u0026#39;1996-01-01\u0026#39;), PARTITION p1 VALUES LESS THAN (\u0026#39;2001-01-01\u0026#39;), PARTITION p2 VALUES LESS THAN (\u0026#39;2006-01-01\u0026#39;)); CREATE TABLE expenses ( expense_date DATE NOT NULL, category VARCHAR(30), amount DECIMAL (10,3) ) PARTITION BY LIST COLUMNS (category)( PARTITION p0 VALUES IN (\u0026#39;a\u0026#39;,\u0026#39;b\u0026#39;) , PARTITION p1 VALUES IN(\u0026#39;c\u0026#39;,\u0026#39;d\u0026#39;), PARTITION p2 VALUES IN(\u0026#39;e\u0026#39;,\u0026#39;f\u0026#39;), PARTITION p3 VALUES IN(\u0026#39;g\u0026#39;), PARTITION p4 VALUES IN(\u0026#39;h\u0026#39;)); -- 在结合 COLUMN 分区时还支持多列 CREATE TABLE rc3( a INT, b INT) PARTITION BY RANGE COLUMNS(a,b)( PARTITION p01 VALUES LESS THAN (0,10), PARTITION p02 VALUES LESS THAN (10,10), PARTITION p03 VALUES LESS THAN (10,20), PARTITION p04 VALUES LESS THAN (10,35), PARTITION p05 VALUES LESS THAN (10,MAXVALUE), PARTITION p06 VALUES LESS THAN (MAXVALUE,MAXVALUE)); Hash 分区 1 2 3 4 5 6 7 8 9 CREATE TABLE emp ( id INT NOT NULL, ename VARCHAR(30), hired DATE NOT NULL DEFAULT \u0026#39;1970-01-01\u0026#39; separated DATENOT NULL DEFAULT \u0026#39;9999-12-31\u0026#39;, job VARCHAR(30) NOT NULL, store_id INT NOT NULL ) PARTITION BY HASH (store_id) PARTITIONS 4; 以上示例创建了一个基于 store_id 列 HASH 分区的表，表被分成了 4 个分区，如果我们插入的记录store_id=234，则 234 mod 4 = 2，这条记录就会保存到第二个分区。虽然在HASH()中直接使用的 store_id 列，但是 MySQL 是允许基于某列值返回一个整数值的表达式或者 MySQL 中有效的任何函数或者其他表达式都是可以的。\nkey分区 1 2 3 4 5 6 7 8 9 10 - 创建了一个基于 job 字段进行 Key 分区的表，表被分成了 4 个分区。KEY ()里只允许出现表中的字段。 CREATE TABLE emp ( id INT NOT NULL, ename VARCHAR(30), hired DATE NOT NULL DEFAULT \u0026#39;1970-01-01\u0026#39; separated DATENOT NULL DEFAULT \u0026#39;9999-12-31\u0026#39;, job VARCHAR(30) NOT NULL, store_id INT NOT NULL ) PARTITION BY KEY (job) PARTITIONS 4; 5.5. 不建议使用 mysql 分区表 在实际互联网项目中，MySQL 分区表用的极少，更多的是分库分表。\n分库分表除了支持 MySQL 分区表的水平切分以外，还支持垂直切分，把一个很大的库（表）的数据分到几个库（表）中，每个库（表）的结构都相同，但他们可能分布在不同的 mysql 实例，甚至不同的物理机器上，以达到降低单库（表）数据量，提高访问性能的目的。两者对比如下：\n分区表，分区键设计不太灵活，如果不走分区键，很容易出现全表锁 一旦数据量并发量上来，如果在分区表实施关联，就是一个灾难 分库分表，使用者来掌控业务场景与访问模式，可控。分区表，由 mysql 本身来实现，不太可控 分区表无论怎么分，都是在一台机器上，天然就有性能的上限 6. Mysql 8.0 新特性详解 Tips: 建议使用 8.0.17 及之后的版本\n6.1. 新增降序索引 MySQL 早期的版本中在语法上是支持降序索引，但实际上创建的仍然是升序索引。如下MySQL 5.7 所示，c2字段降序，但是从show create table看c2仍然是升序。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # ====MySQL 5.7演示==== mysql\u0026gt; create table t1(c1 int,c2 int,index idx_c1_c2(c1,c2 desc)); mysql\u0026gt; insert into t1 (c1,c2) values(1, 10),(2,50),(3,50),(4,100),(5,80); mysql\u0026gt; show create table t1\\G *************************** 1. row *************************** Table: t1 Create Table: CREATE TABLE `t1` ( `c1` int(11) DEFAULT NULL, `c2` int(11) DEFAULT NULL, KEY `idx_c1_c2` (`c1`,`c2`) -- 注意这里，c2字段是升序 ) ENGINE=InnoDB DEFAULT CHARSET=latin1 mysql\u0026gt; explain select * from t1 order by c1,c2 desc; --5.7也会使用索引，但是Extra字段里有filesort文件排序 +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------------+ | 1 | SIMPLE | t1 | NULL | index | NULL | idx_c1_c2 | 10 | NULL | 1 | 100.00 | Using index; Using filesort | +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------------+ 8.0 版本可见 c2 字段降序。注意：只有 Innodb 存储引擎支持降序索引。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 # ====MySQL 8.0演示==== mysql\u0026gt; create table t1(c1 int,c2 int,index idx_c1_c2(c1,c2 desc)); mysql\u0026gt; insert into t1 (c1,c2) values(1, 10),(2,50),(3,50),(4,100),(5,80); mysql\u0026gt; show create table t1\\G *************************** 1. row *************************** Table: t1 Create Table: CREATE TABLE `t1` ( `c1` int DEFAULT NULL, `c2` int DEFAULT NULL, KEY `idx_c1_c2` (`c1`,`c2` DESC) --注意这里的区别，降序索引生效了 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci mysql\u0026gt; explain select * from t1 order by c1,c2 desc; --Extra字段里没有filesort文件排序，充分利用了降序索引 +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ | 1 | SIMPLE | t1 | NULL | index | NULL | idx_c1_c2 | 10 | NULL | 1 | 100.00 | Using index | +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+ mysql\u0026gt; explain select * from t1 order by c1 desc,c2; --Extra字段里有Backward index scan，意思是反向扫描索引; +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+----------------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+----------------------------------+ | 1 | SIMPLE | t1 | NULL | index | NULL | idx_c1_c2 | 10 | NULL | 1 | 100.00 | Backward index scan; Using index | +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+----------------------------------+ mysql\u0026gt; explain select * from t1 order by c1 desc,c2 desc; --Extra字段里有filesort文件排序，排序必须按照每个字段定义的排序或按相反顺序才能充分利用索引 +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------------+ | 1 | SIMPLE | t1 | NULL | index | NULL | idx_c1_c2 | 10 | NULL | 1 | 100.00 | Using index; Using filesort | +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------------+ mysql\u0026gt; explain select * from t1 order by c1,c2; --Extra字段里有filesort文件排序，排序必须按照每个字段定义的排序或按相反顺序才能充分利用索引 +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------------+ | 1 | SIMPLE | t1 | NULL | index | NULL | idx_c1_c2 | 10 | NULL | 1 | 100.00 | Using index; Using filesort | +----+-------------+-------+------------+-------+---------------+-----------+---------+------+------+----------+-----------------------------+ 6.2. group by 不再隐式排序 5.7 版本使用group by进行排序，会隐式将对分组的字段进行排序\n1 2 3 4 5 6 7 8 9 10 # ====MySQL 5.7演示==== mysql\u0026gt; select count(*),c2 from t1 group by c2; +----------+------+ | count(*) | c2 | +----------+------+ | 1 | 10 | | 2 | 50 | | 1 | 80 | | 1 | 100 | +----------+------+ 8.0 版本对于 group by 字段不再隐式排序，如需要排序，必须显式加上 order by 子句。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # ====MySQL 8.0演示==== mysql\u0026gt; select count(*),c2 from t1 group by c2; --8.0版本group by不再默认排序 +----------+------+ | count(*) | c2 | +----------+------+ | 1 | 10 | | 2 | 50 | | 1 | 100 | | 1 | 80 | +----------+------+ mysql\u0026gt; select count(*),c2 from t1 group by c2 order by c2; --8.0版本group by不再默认排序，需要自己加order by +----------+------+ | count(*) | c2 | +----------+------+ | 1 | 10 | | 2 | 50 | | 1 | 80 | | 1 | 100 | +----------+------+ 6.3. 增加隐藏索引 在 8.0 版本中，可以使用 invisible 关键字在创建表或者进行表变更中设置索引为隐藏索引。索引隐藏只是不可见，但是数据库后台还是会维护隐藏索引的，但在查询时优化器不使用该索引，就算使用force index 关键字，优化器也不会使用该索引，同时优化器也不会报索引不存在的错误，因为索引仍然真实存在，必要时，也可以把隐藏索引快速恢复成可见。\nNotes: 主键不能设置为 invisible。\n软删除就可以使用隐藏索引，比如分析某个索引没用了，删除后发现这个索引在某些时候还是有用的，就要把该索引恢复回去，如果表数据量很大的话，这种操作耗费时间是很多的，成本很高，此时就可以将索引先设置为隐藏索引，等到真的确认索引没用了再删除。\n创建隐藏索引 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 # 创建t2表，里面的c2字段为隐藏索引 mysql\u0026gt; create table t2(c1 int, c2 int, index idx_c1(c1), index idx_c2(c2) invisible); mysql\u0026gt; show index from t2\\G *************************** 1. row *************************** Table: t2 Non_unique: 1 Key_name: idx_c1 Seq_in_index: 1 Column_name: c1 Collation: A Cardinality: 0 Sub_part: NULL Packed: NULL Null: YES Index_type: BTREE Comment: Index_comment: Visible: YES Expression: NULL *************************** 2. row *************************** Table: t2 Non_unique: 1 Key_name: idx_c2 Seq_in_index: 1 Column_name: c2 Collation: A Cardinality: 0 Sub_part: NULL Packed: NULL Null: YES Index_type: BTREE Comment: Index_comment: Visible: NO --隐藏索引不可见 Expression: NULL 测试隐藏索引c2是否被使用 1 2 3 4 5 6 7 8 9 10 11 12 13 mysql\u0026gt; explain select * from t2 where c1=1; +----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+ | 1 | SIMPLE | t2 | NULL | ref | idx_c1 | idx_c1 | 5 | const | 1 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+ mysql\u0026gt; explain select * from t2 where c2=1; --隐藏索引c2不会被使用 +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | t2 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ 设置会话查询优化器对隐藏索引可见 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 mysql\u0026gt; select @@optimizer_switch\\G -- 查看各种参数 *************************** 1. row *************************** @@optimizer_switch: index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on,use_invisible_indexes=off,skip_scan=on,hash_join=on mysql\u0026gt; set session optimizer_switch=\u0026#34;use_invisible_indexes=on\u0026#34;; -- 在会话级别设置查询优化器可以看到隐藏索引 mysql\u0026gt; select @@optimizer_switch\\G *************************** 1. row *************************** @@optimizer_switch: index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on,use_invisible_indexes=on,skip_scan=on,hash_join=on mysql\u0026gt; explain select * from t2 where c2=1; +----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+ | 1 | SIMPLE | t2 | NULL | ref | idx_c2 | idx_c2 | 5 | const | 1 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+--------+---------+-------+------+----------+-------+ 修改索引是否隐藏 1 2 3 4 5 6 7 mysql\u0026gt; alter table t2 alter index idx_c2 visible; Query OK, 0 rows affected (0.02 sec) Records: 0 Duplicates: 0 Warnings: 0 mysql\u0026gt; alter table t2 alter index idx_c2 invisible; Query OK, 0 rows affected (0.01 sec) Records: 0 Duplicates: 0 Warnings: 0 6.4. 新增函数索引 MySQL 早期的版本中，如果在查询中加入了函数，索引不生效。在 8.0 版本之后引入了函数索引，MySQL 8.0.13 开始支持在索引中使用函数(表达式)的值。\n函数索引基于虚拟列功能实现，在 MySQL 中相当于新增了一个列，这个列会根据查询条件中的函数来进行计算结果，然后使用函数索引的时候就会用这个计算后的列作为索引。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 mysql\u0026gt; create table t3(c1 varchar(10),c2 varchar(10)); -- 创建测试的表 mysql\u0026gt; create index idx_c1 on t3(c1); -- 创建普通索引 mysql\u0026gt; create index func_idx on t3((UPPER(c2))); -- 创建一个大写的函数索引 mysql\u0026gt; show index from t3\\G *************************** 1. row *************************** Table: t3 Non_unique: 1 Key_name: idx_c1 Seq_in_index: 1 Column_name: c1 Collation: A Cardinality: 0 Sub_part: NULL Packed: NULL Null: YES Index_type: BTREE Comment: Index_comment: Visible: YES Expression: NULL *************************** 2. row *************************** Table: t3 Non_unique: 1 Key_name: func_idx Seq_in_index: 1 Column_name: NULL Collation: A Cardinality: 0 Sub_part: NULL Packed: NULL Null: YES Index_type: BTREE Comment: Index_comment: Visible: YES Expression: upper(`c2`) -- 函数表达式 mysql\u0026gt; explain select * from t3 where upper(c1)=\u0026#39;moonzero\u0026#39;; +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ | 1 | SIMPLE | t3 | NULL | ALL | NULL | NULL | NULL | NULL | 1 | 100.00 | Using where | +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+ mysql\u0026gt; explain select * from t3 where upper(c2)=\u0026#39;moonzero\u0026#39;; --使用了函数索引 +----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------+ | 1 | SIMPLE | t3 | NULL | ref | func_idx | func_idx | 43 | const | 1 | 100.00 | NULL | +----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------+ 6.5. innodb 存储引擎可跳过锁等待 在 5.7 及之前的版本，执行select...for update语句时，如果获取不到锁则会一直等待，直到 innodb_lock_wait_timeout 超时。\n在 8.0 版本后，对于select …… for share(8.0新增加查询共享锁的语法)或select …… for update，在语句后面添加 NOWAIT、SKIP LOCKED 等关键字语法，可以跳过锁等待，或者跳过锁定，能够立即返回。假设查询的行已经加锁，两个关键字分别处理逻辑如下：\nnowait 关键字的语句会立即报错返回。 skip locked 关键字的语句也会立即返回，但只是返回的结果中不包含被锁定的行。 应用场景：比如查询余票记录，如果某些记录已经被锁定，用 skip locked 可以跳过被锁定的记录，只返回没有锁定的记录，提高系统性能。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 # 先打开一个session1: mysql\u0026gt; select * from t1; +------+------+ | c1 | c2 | +------+------+ | 1 | 10 | | 2 | 50 | | 3 | 50 | | 4 | 100 | | 5 | 80 | +------+------+ mysql\u0026gt; begin; -- 开启事务 mysql\u0026gt; update t1 set c2 = 60 where c1 = 2; -- 锁定第二条记录 # 打开另外一个session2: mysql\u0026gt; select * from t1 where c1 = 2 for update; -- 等待超时 ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction mysql\u0026gt; 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\u0026gt; select * from t1 for update skip locked; -- 查询立即返回，过滤掉了第二行记录 +------+------+ | c1 | c2 | +------+------+ | 1 | 10 | | 3 | 50 | | 4 | 100 | | 5 | 80 | +------+------+ 6.6. 新增 innodb_dedicated_server 自适应参数 8.0 版本新增 innodb_dedicated_server参数，能够让 InnoDB 根据服务器上检测到的内存大小自动配置 innodb_buffer_pool_size，innodb_log_file_size 等参数，会尽可能多的占用系统可占用资源提升性能。解决非专业人员安装数据库后默认初始化数据库参数默认值偏低的问题。但前提是服务器是专用来给 MySQL 数据库的，如果还有其他软件或者资源或者多实例 MySQL 使用，不建议开启该参数，不然会影响其它程序。\n6.7. 死锁检查控制 MySQL 8.0（MySQL 5.7.15）增加了一个新的动态变量 innodb_deadlock_detect，用于控制系统是否执行 InnoDB 死锁检查，默认是打开的。死锁检测会耗费数据库性能的，对于高并发的系统，可以关闭死锁检测功能，提高系统性能。但是要确保系统极少情况会发生死锁，同时要将锁等待超时参数调小一点，以防出现死锁等待过久的情况。\n1 2 3 4 5 6 mysql\u0026gt; show variables like \u0026#39;%innodb_deadlock_detect%\u0026#39;; -- 默认值是ON，打开 +------------------------+-------+ | Variable_name | Value | +------------------------+-------+ | innodb_deadlock_detect | ON | +------------------------+-------+ 6.8. undo 文件不再使用系统表空间 默认创建2个UNDO表空间，不再使用系统表空间。\n6.9. binlog 日志过期时间精确到秒 在 8.0 版本之前，binlog 日志过期时间是通过 expire_logs_days 参数来设置，时间单位是“天”；而在 8.0 版本中，默认日志过期时间使用 binlog_expire_logs_seconds 参数（名称发生变化）来配置，并且单位为“秒”。\n6.10. 默认字符集的变动 在 8.0 版本之前，默认字符集为latin1，utf8 默认指向的是utf8mb3；8.0 版本默认字符集为utf8mb4，utf8 默认指向的也是utf8mb4。\n6.11. 系统表的存储引擎的变动 8.0 版本将系统表(mysql)和数据字典表全部改为 InnoDB 存储引擎，默认的 MySQL 实例将不包含 MyISAM 表，除非手动创建 MyISAM 表。\n6.12. 元数据存储变动 MySQL 8.0删除了之前版本的元数据文件，例如表结构.frm等文件，全部集中放入mysql.ibd文件里。可以看见下图test库文件夹里已经没有了frm文件。\n6.13. 自增变量持久化 在 8.0 以前的版本，自增主键 AUTO_INCREMENT 的值如果大于max(primary key)+1，在 MySQL 重启后，会重置AUTO_INCREMENT=max(primary key)+1，这种现象在某些情况下会导致业务主键冲突或者其他难以发现的问题。\n官网对于自增主键重启重置问题的说明\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 # ====MySQL 5.7演示==== mysql\u0026gt; create table t(id int auto_increment primary key,c1 varchar(20)); mysql\u0026gt; insert into t(c1) values(\u0026#39;Laker1\u0026#39;),(\u0026#39;Laker2\u0026#39;),(\u0026#39;Laker3\u0026#39;); mysql\u0026gt; select * from t; +----+--------+ | id | c1 | +----+--------+ | 1 | Laker1 | | 2 | Laker2 | | 3 | Laker3 | +----+--------+ mysql\u0026gt; delete from t where id = 3; mysql\u0026gt; select * from t; +----+--------+ | id | c1 | +----+--------+ | 1 | Laker1 | | 2 | Laker2 | +----+--------+ mysql\u0026gt; exit; Bye # 重启MySQL服务，并重新连接MySQL，此时自增主键 AUTO_INCREMENT 为 3 mysql\u0026gt; insert into t(c1) values(\u0026#39;Laker4\u0026#39;); mysql\u0026gt; select * from t; +----+--------+ | id | c1 | +----+--------+ | 1 | Laker1 | | 2 | Laker2 | | 3 | Laker4 | +----+--------+ 3 rows in set (0.00 sec) mysql\u0026gt; update t set id = 5 where c1 = \u0026#39;Laker1\u0026#39;; mysql\u0026gt; select * from t; +----+--------+ | id | c1 | +----+--------+ | 2 | Laker2 | | 3 | Laker4 | | 5 | Laker1 | +----+--------+ mysql\u0026gt; insert into t(c1) values(\u0026#39;Laker5\u0026#39;); mysql\u0026gt; select * from t; +----+--------+ | id | c1 | +----+--------+ | 2 | Laker2 | | 3 | Laker4 | | 4 | Laker5 | | 5 | Laker1 | +----+--------+ mysql\u0026gt; insert into t(c1) values(\u0026#39;Laker6\u0026#39;); ERROR 1062 (23000): Duplicate entry \u0026#39;5\u0026#39; for key \u0026#39;PRIMARY\u0026#39; 在 8.0 版本将会对 AUTO_INCREMENT 值进行持久化，MySQL 重启后，该值将不会改变，从而解决以上问题。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 # ====MySQL 8.0演示==== mysql\u0026gt; create table t(id int auto_increment primary key,c1 varchar(20)); mysql\u0026gt; insert into t(c1) values(\u0026#39;Laker1\u0026#39;),(\u0026#39;Laker2\u0026#39;),(\u0026#39;Laker3\u0026#39;); mysql\u0026gt; select * from t; +----+--------+ | id | c1 | +----+--------+ | 1 | Laker1 | | 2 | Laker2 | | 3 | Laker3 | +----+--------+ 3 rows in set (0.00 sec) mysql\u0026gt; delete from t where id = 3; mysql\u0026gt; select * from t; +----+--------+ | id | c1 | +----+--------+ | 1 | Laker1 | | 2 | Laker2 | +----+--------+ 2 rows in set (0.00 sec) mysql\u0026gt; exit; Bye [root@localhost ~]# service mysqld restart Shutting down MySQL.... SUCCESS! Starting MySQL... SUCCESS! # 重新连接MySQL mysql\u0026gt; insert into t(c1) values(\u0026#39;Laker4\u0026#39;); mysql\u0026gt; select * from t; --生成的id为4，不是3 +----+--------+ | id | c1 | +----+--------+ | 1 | Laker1 | | 2 | Laker2 | | 4 | Laker4 | +----+--------+ 3 rows in set (0.00 sec) mysql\u0026gt; update t set id = 5 where c1 = \u0026#39;Laker1\u0026#39;; mysql\u0026gt; select * from t; +----+--------+ | id | c1 | +----+--------+ | 2 | Laker2 | | 4 | Laker4 | | 5 | Laker1 | +----+--------+ 3 rows in set (0.00 sec) mysql\u0026gt; insert into t(c1) values(\u0026#39;Laker5\u0026#39;); mysql\u0026gt; select * from t; +----+--------+ | id | c1 | +----+--------+ | 2 | Laker2 | | 4 | Laker4 | | 5 | Laker1 | | 6 | Laker5 | +----+--------+ 6.14. DDL 操作原子化 MySQL 8.0 开始支持原子 DDL 操作，保证事务完整性，要么成功要么回滚。其中与表相关的原子 DDL 只支持 InnoDB 存储引擎。\n一个原子 DDL 操作内容包括：更新数据字典，存储引擎层的操作，在 binlog 中记录 DDL 操作。 支持与表相关的 DDL：数据库、表空间、表、索引的 CREATE、ALTER、DROP 以及 TRUNCATE TABLE。 支持的其它 DDL ：存储程序、触发器、视图、UDF 的 CREATE、DROP 以及ALTER 语句。 支持账户管理相关的 DDL：用户和角色的 CREATE、ALTER、DROP 以及适用的 RENAME等等。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 # MySQL 5.7 mysql\u0026gt; show tables; +----------------+ | Tables_in_test | +----------------+ | account | | employee | | t1 | +----------------+ mysql\u0026gt; drop table t1,t2; -- 删除表报错不会回滚，t1表会被删除 ERROR 1051 (42S02): Unknown table \u0026#39;test.t2\u0026#39; mysql\u0026gt; show tables; +----------------+ | Tables_in_test | +----------------+ | account | | employee | +----------------+ # MySQL 8.0 mysql\u0026gt; show tables; +----------------+ | Tables_in_test | +----------------+ | account | | employee | | t1 | +----------------+ mysql\u0026gt; drop table t1,t2; -- 删除表报错会回滚，t1表依然还在 ERROR 1051 (42S02): Unknown table \u0026#39;test.t2\u0026#39; mysql\u0026gt; show tables; +----------------+ | Tables_in_test | +----------------+ | account | | employee | | t1 | +----------------+ 6.15. 参数修改持久化 MySQL 8.0 版本支持在线修改全局参数并持久化，通过加上 PERSIST 关键字，可以将修改的参数持久化到新的配置文件（mysqld-auto.cnf）中，重启 MySQL 时，可以从该配置文件获取到最新的配置参数。而通过set global命令设置的变量参数在 MySQL 重启后会失效。\n1 mysql\u0026gt; set persist innodb_lock_wait_timeout=25; 系统会在数据目录下生成一个 json 格式的 mysqld-auto.cnf 的文件，格式化后如下所示：\n1 2 3 4 5 6 7 8 9 10 11 12 13 { \u0026#34;Version\u0026#34;: 1, \u0026#34;mysql_server\u0026#34;: { \u0026#34;innodb_lock_wait_timeout\u0026#34;: { \u0026#34;Value\u0026#34;: \u0026#34;25\u0026#34;, \u0026#34;Metadata\u0026#34;: { \u0026#34;Timestamp\u0026#34;: 1675290252103863, \u0026#34;User\u0026#34;: \u0026#34;root\u0026#34;, \u0026#34;Host\u0026#34;: \u0026#34;localhost\u0026#34; } } } } 当 my.cnf 和 mysqld-auto.cnf 同时存在时，后者具有更高优先级。\n","permalink":"https://ktzxy.top/posts/ipdtlj9cot/","summary":"MySQL 进阶","title":"MySQL 进阶"},{"content":"1. Java 8 新特性概述 Java 8 (又称为 jdk 1.8) 是 Java 语言开发的一个主要版本。Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ，它支持函数式编程，新的 JavaScript 引擎，新的日期 API，新的Stream API 等。Java8 新增了非常多的特性，常用有以下几个：\nLambda 表达式 − Lambda 允许把函数作为一个方法的参数（函数作为参数传递到方法中）。 方法引用 − 方法引用提供了非常有用的语法，可以直接引用已有 Java 类或对象（实例）的方法或构造器。与lambda联合使用，方法引用可以使语言的构造更紧凑简洁，减少冗余代码。 默认方法 − 默认方法就是一个在接口里面有了一个实现的方法。 新的编译工具 - 如：Nashorn引擎 jjs、 类依赖分析器jdeps。 Stream API − 新添加的 Stream API（java.util.stream） 把真正的函数式编程风格引入到Java中。 Date Time API − 加强对日期与时间的处理。 Optional 类 − Optional 类已经成为 Java 8 类库的一部分，用来解决空指针异常。 Nashorn, JavaScript 引擎 − Java 8 提供了一个新的Nashorn javascript引擎，它允许我们在JVM上运行特定的javascript应用。 2. Lambda 表达式 2.1. Lambda 表达式定义 Lambda 表达式，也可称为闭包，它是推动 Java 8 发布的最重要新特性 Lambda 允许把函数作为一个方法的参数（函数作为参数传递进方法中） 使用 Lambda 表达式可以使代码变的更加简洁紧凑 在调用方法时，如果参数是函数式接口，就可以考虑使用Lambda表达式，Lambda表达式相当于是对接口中抽象方法的重写 示例：当需要启动一个线程去完成任务时，通常会通过 Runnable 接口来定义任务内容，并使用 Thread 类来启动该线程。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /* 示例：当需要启动一个线程去完成任务时，通常会通过 `Runnable` 接口来定义任务内容，并使用 `Thread` 类来启动该线程。 */ @Test public void quickstartTest() { /* * 传统写法，使用匿名内部类实现 * 对于 Runnable 的匿名内部类用法，可以分析出几点内容： * 1.Thread 类需要 Runnable 接口作为参数，其中的抽象 run 方法是用来指定线程任务内容的核心 * 2.为了指定 run 的方法体，不得不需要 Runnable 接口的实现类 * 3.为了省去定义一个 Runnable 实现类的麻烦，不得不使用匿名内部类 * 4.必须覆盖重写抽象 run 方法，所以方法名称、方法参数、方法返回值不得不再写一遍，且不能写错 * 5.实际上，似乎只有方法体才是关键所在。 */ new Thread(new Runnable() { @Override public void run() { System.out.println(\u0026#34;新线程任务执行！\u0026#34;); } }).start(); /* * 使用Lambda表达式实现，Lambda是一个匿名函数 * 简化匿名内部类的使用，语法更加简单 */ new Thread(() -\u0026gt; { System.out.println(\u0026#34;使用Lambda表达式创建的线程任务执行了！\u0026#34;); }).start(); } 2.2. Lambda 表达式语法 2.2.1. Lambda的标准语法格式 标准语法格式 1 2 3 (参数类型 参数名称) -\u0026gt; { 代码体; } 其他简化格式 1 2 3 4 5 // 方式1 (parameters) -\u0026gt; expression // 方式2 (parameters) -\u0026gt; { statements; } 2.2.2. lambda表达式的重要特征（可省略规则） 可选类型声明：不需要声明参数类型，编译器可以统一识别参数值。 可选的参数圆括号：一个参数无需定义圆括号，但多个参数需要定义圆括号。 可选的大括号：如果主体包含了一个语句，就不需要使用大括号。 可选的返回关键字：如果主体只有一个表达式返回值则编译器会自动返回值，大括号需要指定明表达式返回了一个数值。 2.2.3. 使用 Lambda 表达式前提条件 Lambda 表达式主要用来定义行内执行的方法类型接口，例如，一个简单方法接口。在上面例子中，使用各种类型的Lambda表达式来定义MathOperation接口的方法。然后我们定义了sayMessage的执行。Lambda 表达式免去了使用匿名方法的麻烦，并且给予Java简单但是强大的函数化的编程能力。 使用Lambda表达式的接口只能有一个方法，此种接口可以称为函数式接口 如果一个接口使用注解@FunctonalInterface修饰，则该接口称为函数式接口。如果该接口中有多个方法，但除了一个方法外的其它方法都有默认实现（使用default关键字修改的方法），则也是可以做为函数式接口 如果接口里面有Object类下的非默认方法，也是一个函数式接口，可以使用lambda表达式 2.3. Lambda 表达式示例 TODO: 后面需要深入使用时，参考阿里的《Java工程师必读手册.pdf》电子书的[最完美的 Lambda 表达式只有一行]章节\n2.3.1. 基础综合示例1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 1. 不需要参数,返回值为 5 () -\u0026gt; 5 // 2. 接收一个参数(数字类型),返回其2倍的值 x -\u0026gt; 2 * x // 3. 接受2个参数(数字),并返回他们的差值 (x, y) -\u0026gt; x – y // 4. 接收2个int型整数,返回他们的和 (int x, int y) -\u0026gt; x + y // 5. 接受一个 string 对象,并在控制台打印,不返回任何值(看起来像是返回void) (String s) -\u0026gt; System.out.print(s) 2.3.2. 综合示例2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class Java8Tester { public static void main(String args[]) { Java8Tester tester = new Java8Tester(); // 类型声明 MathOperation addition = (int a, int b) -\u0026gt; a + b; // 不用类型声明 MathOperation subtraction = (a, b) -\u0026gt; a - b; // 大括号中的返回语句 MathOperation multiplication = (int a, int b) -\u0026gt; { return a * b; }; // 没有大括号及返回语句 MathOperation division = (int a, int b) -\u0026gt; a / b; System.out.println(\u0026#34;10 + 5 = \u0026#34; + tester.operate(10, 5, addition)); System.out.println(\u0026#34;10 - 5 = \u0026#34; + tester.operate(10, 5, subtraction)); System.out.println(\u0026#34;10 x 5 = \u0026#34; + tester.operate(10, 5, multiplication)); System.out.println(\u0026#34;10 / 5 = \u0026#34; + tester.operate(10, 5, division)); // 不用括号 GreetingService greetService1 = message -\u0026gt; System.out.println(\u0026#34;Hello \u0026#34; + message); // 用括号 GreetingService greetService2 = (message) -\u0026gt; System.out.println(\u0026#34;Hello \u0026#34; + message); greetService1.sayMessage(\u0026#34;Runoob\u0026#34;); greetService2.sayMessage(\u0026#34;Google\u0026#34;); } interface MathOperation { int operation(int a, int b); } interface GreetingService { void sayMessage(String message); } private int operate(int a, int b, MathOperation mathOperation) { return mathOperation.operation(a, b); } } 程序输出结果为：\n1 2 3 4 5 6 7 8 $ javac Java8Tester.java $ java Java8Tester 10 + 5 = 15 10 - 5 = 5 10 x 5 = 50 10 / 5 = 2 Hello Runoob Hello Google 2.3.3. 无参数无返回值的Lambda 定义只有一个抽象方法的接口 1 2 3 public interface Sportable { void doSport(); } 使用Lambda表达式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /* 无参数无返回值的Lambda */ @Test public void lambdaNoParamsTest() { // 传统写法：匿名内部类方式实现 playBasketball(new Sportable() { @Override public void doSport() { System.out.println(\u0026#34;使用匿名内部类方式调用playBasketball(Sportable sportable)方法...\u0026#34;); } }); /* * Lambda表达式方式实现 * 相当于是对接口抽象方法的重写 */ playBasketball(() -\u0026gt; System.out.println(\u0026#34;使用Lambda表达式方式调用playBasketball(Sportable sportable)方法...\u0026#34;)); } // 定义方法，入参为Sportable接口，方法体中调用Sportable接口的doSport()方法 private void playBasketball(Sportable sportable) { sportable.doSport(); } 2.3.4. 有参数有返回值的Lambda 示例：调用 java.util.Comparator\u0026lt;T\u0026gt; 接口的使用场景代码，其中的抽象方法定义为：\n1 public abstract int compare(T o1, T o2); 当需要对一个对象集合进行排序时，Collections.sort 方法需要一个 Comparator 接口实例来指定排序的规则\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /* 02 有参数有返回值的Lambda */ @Test public void lambdaHasParamsTest() { ArrayList\u0026lt;Person\u0026gt; persons = new ArrayList\u0026lt;\u0026gt;(); persons.add(new Person(\u0026#34;石原里美\u0026#34;, 30, 156)); persons.add(new Person(\u0026#34;新垣结衣\u0026#34;, 28, 168)); persons.add(new Person(\u0026#34;天锁斩月\u0026#34;, 183, 180)); persons.add(new Person(\u0026#34;樱木花道\u0026#34;, 18, 189)); // 传统写法：匿名内部类方式实现 /*Collections.sort(persons, new Comparator\u0026lt;Person\u0026gt;() { @Override public int compare(Person o1, Person o2) { // 返回对象年龄属性的差值，可以实现按年龄排序 return o1.getAge() - o2.getAge(); } });*/ // Lambda表达式方式实现，标准格式 Collections.sort(persons, (Person o1, Person o2) -\u0026gt; { return o1.getAge() - o2.getAge(); }); // 输入结果 for (Person person : persons) { System.out.println(person); } } 2.3.5. 省略格式的Lambda 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* Lambda表达式省略格式写法示例 */ public static void main(String[] args) { ArrayList\u0026lt;Person\u0026gt; persons = new ArrayList\u0026lt;\u0026gt;(); persons.add(new Person(\u0026#34;石原里美\u0026#34;, 30, 156)); persons.add(new Person(\u0026#34;新垣结衣\u0026#34;, 28, 168)); persons.add(new Person(\u0026#34;天锁斩月\u0026#34;, 183, 180)); persons.add(new Person(\u0026#34;樱木花道\u0026#34;, 18, 189)); /* * Lambda表达式省略格式写法：多个参数，有返回值 * 1. 小括号内参数的类型可以省略 * 2. 小括号内参数是多个，则小括号不可以省略 * 3. 如果大括号内有且仅有一个语句，可以同时省略大括号、return关键字及语句分号 */ Collections.sort(persons, (o1, o2) -\u0026gt; o1.getAge() - o2.getAge()); /* * Lambda表达式省略格式写法：单个参数，无返回值 * 1. 小括号内参数的类型可以省略 * 2. 小括号内参数只有一个，则小括号可以省略 * 3. 如果大括号内有且仅有一个语句，可以同时省略大括号、return关键字及语句分号 */ persons.forEach(person -\u0026gt; System.out.println(person)); } 2.4. Lambda的实现原理（！待整理） 参考《2019.10.25-JavaJDK新特性详解-JDK8》笔记\n2.4.1. 类型推断与检查 Java 编译器会从上下文中推断出用什么函数式接口来配合 Lambda 表达式。Java 编译器类型推断步骤如下：\n首先，根据 Lambda 表达式对应的方法、参数和返回值，确定使用了哪个函数式接口； 然后，Java 编译器根据这个函数式接口，获取到唯一抽象方法的函数描述符（参数和返回值类型）； 最后，Java 编译器通过函数描述符推断出 Lambda 表达式的参数类型。 类型检查：利用 Java 编译器推断出来的函数描述符（参数和返回值类型），验证 Lambda 表达式参数是否合法。\n2.4.2. this 指向对象 Lambda 表达式可以用来取代唯一抽象方法的内部匿名类的。但是 this 指针指向对象，却是完全不一样的：\n对于 Java 中的匿名内部类，编译器会自动生成它的类名（外部类类名$数字）。而匿名内部类中的 this，将指向的是这个内部类对象本身。 对于 Java 中的Lambda 表达式中的 this，指向的是 Lambda 表达式所在类的对象。即 Lambda 表达式中的 this 与普通表达式中的 this 没有任何区别。 2.5. 变量作用域 2.5.1. 概述 Java 局部类和匿名类都存在变量捕获（Captured Variable）和变量隐藏（Shadow Variable），但 Lambda 表达式只存在变量捕获，不存在变量隐藏。即 Lambda 表达式的作用域：\nLambda 表达式不会从超类继承或引入新级别的作用域 Lambda 表达式中的声明变量和普通封闭程序块中的一样 Lambda 表达式可以无限制地捕获变量或常量，但是局部变量必须定义为 final 或准 final 型（不允许修改）。因为 Lambda 表达式只通过 this 指针捕获一次局部变量值，后续局部变量发生更改将无法得知。所以干脆禁止这些局部变量的更改，期望这些局部变量被定义为 final 或准 final 型，否则会出现编译错误。\n1 2 3 4 5 6 String str = \u0026#34;Hello world\u0026#34;; // 定义为准备 final 型 new Thread(() -\u0026gt; { System.out.println(str); // str = \u0026#34;inner\u0026#34;; // 报错，不允许修改 }).start(); // str = \u0026#34;outer\u0026#34;; // 报错，不允许修改 2.5.2. 使用示例 lambda 表达式只能引用标记了 final 的外层局部变量，这就是说不能在 lambda 内部修改定义在域外的局部变量，否则会编译错误。\n1 2 3 4 5 6 7 8 9 10 11 12 public class Java8Tester { final static String salutation = \u0026#34;Hello! \u0026#34;; public static void main(String args[]) { GreetingService greetService1 = message -\u0026gt; System.out.println(salutation + message); greetService1.sayMessage(\u0026#34;Runoob\u0026#34;); } interface GreetingService { void sayMessage(String message); } } 程序输出结果\n1 2 3 $ javac Java8Tester.java $ java Java8Tester Hello! Runoob 也可以直接在 lambda 表达式中访问外层的局部变量\n1 2 3 4 5 6 7 8 9 10 11 public class Java8Tester { public static void main(String args[]) { final int num = 1; Converter\u0026lt;Integer, String\u0026gt; s = (param) -\u0026gt; System.out.println(String.valueOf(param + num)); s.convert(2); // 输出结果为 3 } public interface Converter\u0026lt;T1, T2\u0026gt; { void convert(int i); } } lambda 表达式的局部变量可以不用声明为 final，但是必须不可被后面的代码修改（即隐性的具有 final 的语义）\n1 2 3 4 5 6 int num = 1; Converter\u0026lt;Integer, String\u0026gt; s = (param) -\u0026gt; System.out.println(String.valueOf(param + num)); s.convert(2); num = 5; // 报错信息：Local variable num defined in an enclosing scope must be final or effectively final // 把num=5；注释掉就不报错了 在 Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量\n1 2 3 4 5 6 7 8 9 10 11 public class Java8Tester { public static void main(String args[]) { String first = \u0026#34;\u0026#34;; // 把String first = \u0026#34;\u0026#34;;注掉就不报错了 Comparator\u0026lt;String\u0026gt; comparator = (first, second) -\u0026gt; System.out.println(Integer.compare(first.length(), second.length())); // 编译会出错 comparator.com(\u0026#34;aaaaa\u0026#34;, \u0026#34;bb\u0026#34;); } public interface Comparator\u0026lt;T\u0026gt; { void com(String a, String b); } } 2.6. Lambda 和匿名内部类对比总结 实际上 Lambda 表达式并非匿名内部类的语法糖。Lambda 表达式在大多数虚拟机中采用 invokeDynamic 指令实现，相对于匿名内部类在效率上会更高一些。\n所需的类型不一样 匿名内部类需要的类型可以是类，抽象类，接口 Lambda 表达式需要的类型必须是接口 抽象方法的数量不一样 匿名内部类所需的接口中抽象方法的数量随意 Lambda 表达式所需的接口只能有一个抽象方法 实现原理不同 匿名内部类是在编译后会生成一个名称为 外部类类名$数字 的 class 文件 Lambda 表达式是在程序运行的时候动态生成一个类（class 文件），在类中新增一个方法，这个方法的方法体就是 Lambda 表达式中的代码；还会生成一个匿名内部类，实现接口，重写抽象方法；在接口的重写方法中会调用前面新生成的方法（即 Lambda 表达式的代码） 3. 方法引用 方法引用是Lambda表达式的一个简化写法。所引用的方法其实是Lambda表达式的方法体的实现。如果正好有某个方法满足一个lambda表达式的形式，那就可以将这个lambda表达式用方法引用的方式表示，但是如果这个lambda表达式的比较复杂就不能用方法引用进行替换。实际上方法引用是lambda表达式的一种语法糖\n方法引用通过方法的名字来指向一个方法 方法引用可以使语言的构造更紧凑简洁，减少冗余代码 方法引用语法是使用一对冒号 :: 应用场景：如果 Lambda 所要实现的方案，已经有其他方法存在相同方案，那么则可以使用方法引用\n方法引用的注意事项\n方法引用只能\u0026quot;引用\u0026quot;已经存在的方法 Lambda 体中调用的方法的参数列表与返回值类型，要与函数式中接口的抽象方法的参数列表和返回值类型一样 3.1. 方法引用语法格式 方法引用语法符号：:: 方法引用符号说明：双冒号为方法引用运算符，而它所在的表达式被称为方法引用。 3.2. 常见引用方式 主要有5种语法格式\n3.2.1. 实例对象普通方法的引用 最常见的一种用法，如果一个类中已经存在了一个成员方法，并且当 Lambda 表达式参数与调用的对象实例方法参数一致时，可以采用实例方法引用语法。表达式语法如下：\n1 2 // 对象名::引用成员方法 instanceName::methodName 使用示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 /* * 对象::实例方法 - 方法引用示例 */ @Test public void methodReftest01() { Date now = new Date(); // Lambda表达式实现函数式接口 // Supplier\u0026lt;Long\u0026gt; supplier = () -\u0026gt; now.getTime(); // 使用方法引用对象实例方法，实现函数式接口 Supplier\u0026lt;Long\u0026gt; supplier = now::getTime; Long time = supplier.get(); System.out.println(\u0026#34;time: \u0026#34; + time); } 3.2.2. 类静态方法的引用 当 Lambda 表达式参数与调用的静态方法参数一致时，可以采用静态方法引用语法。表达式语法如下：\n1 2 // 类名::引用静态方法名 ClassName::staticMethodName 使用示例：\n1 2 3 4 5 6 7 8 9 @Test public void test02() { // Lambda表达式实现函数式接口 // Supplier\u0026lt;Long\u0026gt; supplier = () -\u0026gt; System.currentTimeMillis(); // 使用方法引用类静态方法，实现函数式接口 Supplier\u0026lt;Long\u0026gt; supplier = System::currentTimeMillis; Long time = supplier.get(); System.out.println(\u0026#34;time = \u0026#34; + time); } 3.2.3. 参数类方法的引用 Java面向对象中，类名只能调用静态方法。而在方法引用中，也可以使用类名引用普通方法。\n但类名引用实例方法是有前提的，当 Lambda 表达式只有一个参数且调用该参数的无参类方法时，可以使用类名实例方法引用，实际上是拿第一个参数作为方法的调用者。表达式语法如下：\n1 2 // 类名::实例方法名 ClassName::methodName 使用示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test03() { // Lambda表达式实现函数式接口(一个参数) // Function\u0026lt;String, Integer\u0026gt; f1 = str -\u0026gt; str.length(); // 使用方法引用类实例方法，实现函数式接口(注意:类名::实例方法实际上会将第一个参数作为方法的调用者) Function\u0026lt;String, Integer\u0026gt; f1 = String::length; int length = f1.apply(\u0026#34;hello\u0026#34;); System.out.println(\u0026#34;length = \u0026#34; + length); // Lambda表达式实现函数式接口(两个参数) // BiFunction\u0026lt;String, Integer, String\u0026gt; f2 = (String str, Integer index) -\u0026gt; str.substring(index); // 使用方法引用类实例方法，实现函数式接口 BiFunction\u0026lt;String, Integer, String\u0026gt; f2 = String::substring; String str2 = f2.apply(\u0026#34;helloworld\u0026#34;, 3); System.out.println(\u0026#34;str2 = \u0026#34; + str2); } 3.2.4. 构造方法的引用 当 Lambda 表达式参数与调用的构造方法参数一致时，可以采用构造方法引用语法。由于构造器的名称与类名完全一样，所以可以使用类名引用。表达式语法如下：\n1 2 // 类名::new ClassName::new 使用示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Person { private String name; private int age; private int height; public Person() { System.out.println(\u0026#34;执行Person类无参构造\u0026#34;); } public Person(String name, int age) { String temp = new StringJoiner(\u0026#34;, \u0026#34;, \u0026#34;执行Person类有参构造\u0026#34; + \u0026#34;[\u0026#34;, \u0026#34;]\u0026#34;) .add(\u0026#34;name=\u0026#39;\u0026#34; + name + \u0026#34;\u0026#39;\u0026#34;) .add(\u0026#34;age=\u0026#34; + age) .toString(); System.out.println(temp); this.name = name; this.age = age; } // 省略其他代码 } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test04() { // Lambda表达式实现函数式接口 // Supplier\u0026lt;Person\u0026gt; supplier1 = () -\u0026gt; new Person(); // 使用方法引用类构造器方法，实现函数式接口 Supplier\u0026lt;Person\u0026gt; supplier1 = Person::new; Person person = supplier1.get(); System.out.println(\u0026#34;person = \u0026#34; + person); // Lambda表达式实现函数式接口 // BiFunction\u0026lt;String, Integer, Person\u0026gt; bif = (String name, Integer age) -\u0026gt; new Person(name, age); // 使用方法引用类构造器方法（有参构造），实现函数式接口。方法引用时，会根据参数列表的个数，引用相应的构造方法 BiFunction\u0026lt;String, Integer, Person\u0026gt; bif = Person::new; Person person2 = bif.apply(\u0026#34;新垣结衣\u0026#34;, 18); System.out.println(\u0026#34;person2 = \u0026#34; + person2); } 3.2.5. 数组构造器的引用 数组也是 Object 的子类对象，所以同样具有构造器引用。表达式语法如下：\n1 2 // 数据类型[]::new TypeName[]::new 使用示例：\n1 2 3 4 5 6 7 8 9 @Test public void test05() { // Lambda表达式实现函数式接口 // Function\u0026lt;Integer, int[]\u0026gt; f = (Integer length) -\u0026gt; new int[length]; // 使用方法引用数组构造器方法 Function\u0026lt;Integer, int[]\u0026gt; f = int[]::new; int[] arr = f.apply(10); System.out.println(Arrays.toString(arr)); } 3.3. 方法引用用法综合示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class Demo02MethodRefComprehensive { public static void main(String[] args) { // 构造器引用：它的语法是Class::new，或者更一般的Class\u0026lt;T\u0026gt;::new实例如下： Car car = Car.create(Car::new); Car car1 = Car.create(Car::new); Car car2 = Car.create(Car::new); Car car3 = new Car(); List\u0026lt;Car\u0026gt; cars = Arrays.asList(car, car1, car2, car3); System.out.println(\u0026#34;===================构造器引用========================\u0026#34;); // 静态方法引用：它的语法是Class::static_method，实例如下： cars.forEach(Car::collide); System.out.println(\u0026#34;===================静态方法引用========================\u0026#34;); // 特定类的任意对象的方法引用：它的语法是Class::method实例如下： cars.forEach(Car::repair); System.out.println(\u0026#34;==============特定类的任意对象的方法引用================\u0026#34;); // 特定对象的方法引用：它的语法是instance::method实例如下： final Car police = Car.create(Car::new); cars.forEach(police::follow); System.out.println(\u0026#34;===================特定对象的方法引用===================\u0026#34;); } } class Car { // Supplier是jdk1.8的接口，这里和lamda一起使用了 public static Car create(final Supplier\u0026lt;Car\u0026gt; supplier) { return supplier.get(); } public static void collide(final Car car) { System.out.println(\u0026#34;Collided \u0026#34; + car.toString()); } public void follow(final Car another) { System.out.println(\u0026#34;Following the \u0026#34; + another.toString()); } public void repair() { System.out.println(\u0026#34;Repaired \u0026#34; + this.toString()); } } 程序输出程序\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ===================构造器引用======================== Collided com.moon.java.jdk8methodref.Car@3b9a45b3 Collided com.moon.java.jdk8methodref.Car@7699a589 Collided com.moon.java.jdk8methodref.Car@58372a00 Collided com.moon.java.jdk8methodref.Car@4dd8dc3 ===================静态方法引用======================== Repaired com.moon.java.jdk8methodref.Car@3b9a45b3 Repaired com.moon.java.jdk8methodref.Car@7699a589 Repaired com.moon.java.jdk8methodref.Car@58372a00 Repaired com.moon.java.jdk8methodref.Car@4dd8dc3 ==============特定类的任意对象的方法引用================ Following the com.moon.java.jdk8methodref.Car@3b9a45b3 Following the com.moon.java.jdk8methodref.Car@7699a589 Following the com.moon.java.jdk8methodref.Car@58372a00 Following the com.moon.java.jdk8methodref.Car@4dd8dc3 ===================特定对象的方法引用=================== 4. JDK8 接口的默认方法与静态方法 4.1. JDK 8接口增强介绍 JDK 8以前的接口：\n1 2 3 4 public interface 接口名 { 静态常量; 抽象方法; } JDK 8对接口的增强，接口还可以有默认方法和静态方法\n1 2 3 4 5 6 public interface 接口名 { 静态常量; 抽象方法; 默认方法; 静态方法; } 4.2. 默认方法 4.2.1. 接口引入默认方法的背景介绍 Java 8 新增了接口的默认方法。简单说，默认方法就是接口可以有实现方法，而且不需要实现类去实现其方法。 只需在方法名前面加个default关键字即可实现默认方法。 为什么要有这个特性？\n首先，之前的接口是个双刃剑，好处是面向抽象而不是面向具体编程，缺陷是，当需要修改接口时候，需要修改全部实现该接口的类，目前的java 8之前的集合框架没有foreach方法，通常能想到的解决办法是在JDK里给相关的接口添加新的方法及实现。然而，对于已经发布的版本，是没法在给接口添加新方法的同时不影响已有的实现。所以引进的默认方法的目的是为了解决接口的修改与现有的实现类不兼容的问题。\n4.2.2. 接口默认方法语法格式 语法格式：\n1 2 3 4 5 public interface 接口名 { 修饰符 default 返回值类型 方法名() { 方法体; } } 注：接口中的默认方法修饰符可省略，默认是public\n示例：\n1 2 3 4 5 public interface Java8DefaultMethod { default void defaultMethod() { // do something... } } 4.2.3. 接口默认方法的使用 方式一：实现类直接调用接口默认方法 方式二：实现类重写接口默认方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 /** * 定义动物接口 */ interface Animal { /** * 定义默认方法（方法修饰符可省略，默认是public） */ public default void eat() { System.out.println(\u0026#34;我是Animal接口的默认方法eat()...\u0026#34;); } } /** * 默认方法使用方式一: 实现类可以直接使用 */ class Cat implements Animal { } /** * 默认方法使用方式二: 实现类重写接口默认方法，对象进行调用 */ class Person implements Animal { @Override public void eat() { System.out.println(\u0026#34;我是Person实现类重写后的默认方法eat()...\u0026#34;); } } @Test public void defaultFunctionTest() { // 方式一：创建实现类，直接调用默认方法 Cat cat = new Cat(); cat.eat(); System.out.println(\u0026#34;--------------------------\u0026#34;); // 方式二：创建实现类，实现类重写默认方法，再调用 Person person = new Person(); person.eat(); } 4.2.4. 多个默认方法 一个接口有默认方法，考虑这样的情况，一个类实现了多个接口，且这些接口有相同的默认方法，有以下两种解决的方案\n第一个解决方案是创建自己的默认方法，来覆盖重写接口的默认方法 第二种解决方案可以使用 super 关键字来调用指定接口的默认方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 /** * 定义接口1与同名参数列表相同的方法 */ interface Java8Interface1 { default void defaultMethod() { System.out.println(\u0026#34;Java8Interface1.defaultMethod()方法执行了....\u0026#34;); } } /** * 定义接口2与同名参数列表相同的方法 */ interface Java8Interface2 { default void defaultMethod() { System.out.println(\u0026#34;Java8Interface2.defaultMethod()方法执行了....\u0026#34;); } } /** * 第一个解决方案是创建自己的默认方法，来覆盖重写接口的默认方法 */ class MultiDefaultMethodImpl1 implements Java8Interface1, Java8Interface2 { @Override public void defaultMethod() { System.out.println(\u0026#34;实现两个接口的MultiDefaultMethodImpl1.defaultMethod()方法执行了....\u0026#34;); } } /** * 第二种解决方案可以使用 super 关键字来调用指定接口的默认方法 */ class MultiDefaultMethodImpl2 implements Java8Interface1, Java8Interface2 { @Override public void defaultMethod() { // 调用接口1的方法 Java8Interface1.super.defaultMethod(); System.out.println(\u0026#34;实现两个接口的MultiDefaultMethodImpl2.defaultMethod()方法执行了....\u0026#34;); // 调用接口2的方法 Java8Interface2.super.defaultMethod(); } } // 测试 @Test public void multiDefaultFunctionTest() { // 方式一：实现类，重写两个接口的同名方法 MultiDefaultMethodImpl1 impl1 = new MultiDefaultMethodImpl1(); impl1.defaultMethod(); System.out.println(\u0026#34;--------------------------\u0026#34;); // 方式二：实现类，重写两个接口的同名方法，方法内部使用super关键字调用指定的接口的默认方法 MultiDefaultMethodImpl2 impl2 = new MultiDefaultMethodImpl2(); impl2.defaultMethod(); } 4.3. 静态默认方法 Java 8 的另一个特性是接口可以声明（并且可以提供实现）静态方法。\n4.3.1. 接口默认方法语法格式 语法格式：\n1 2 3 4 5 public interface 接口名 { 修饰符 static 返回值类型 方法名() { 方法体; } } 注：接口中的默认方法修饰符可省略，默认是public\n示例：\n1 2 3 4 5 6 7 8 9 10 public interface Java8DefaultMethod1 { default void defaultMethod() { // do something... } // 静态方法 static void staticMethod() { // do something... } } 4.3.2. 接口静态方法的使用 直接使用接口名调用即可，接口名.静态方法名();\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 /** * 定义有静态方法的接口 */ interface StaticMethodInterface { /** * 定义静态方法（方法修饰符可省略，默认是public） */ public static void staticMethod() { System.out.println(\u0026#34;我是StaticMethodInterface接口的静态方法staticMethod()...\u0026#34;); } } /** * 接口实现类，静态方法不能被继承与重写 */ class StaticMethodInterfaceImpl implements StaticMethodInterface { // 静态方法不被重写，也不被继承 /*@Override public void staticMethod() { }*/ } // 测试 @Test public void defaultFunctionTest() { // 创建接口实现类 StaticMethodInterfaceImpl impl = new StaticMethodInterfaceImpl(); // 报错，说明实现类无法继承接口的静态方法，对象也不能调用 // impl.staticMethod(); // 接口静态方法的调用：接口名.静态方法名(); StaticMethodInterface.staticMethod(); } 4.4. 接口默认方法和静态方法的区别 默认方法通过实例调用，静态方法通过接口名调用。 默认方法可以被继承，实现类可以直接使用接口默认方法，也可以重写接口默认方法。 静态方法不能被继承，实现类不能重写接口静态方法，只能使用接口名调用。 小结：如果接口某个方法需要被实现类继承或重写，则使用默认方法，如果接口中的方法不需要被继承就使用静态方法\n4.5. 默认方法与默认方法综合示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class Jdk8DefaultAndStaticMethodDemo { public static void main(String[] args) { Vehicle vehicle = new Car(); vehicle.print(); } } interface Vehicle { default void print() { System.out.println(\u0026#34;我是一辆车!\u0026#34;); } static void blowHorn() { System.out.println(\u0026#34;按喇叭!!!\u0026#34;); } } interface FourWheeler { default void print() { System.out.println(\u0026#34;我是一辆四轮车!\u0026#34;); } } class Car implements Vehicle, FourWheeler { // 重写两个接口同名默认方法 @Override public void print() { Vehicle.super.print(); FourWheeler.super.print(); // 调用接口的默认方法 Vehicle.blowHorn(); // 调用接口静态方法 System.out.println(\u0026#34;我是一辆汽车!\u0026#34;); } } 程序输出结果\n1 2 3 4 我是一辆车! 我是一辆四轮车! 按喇叭!!! 我是一辆汽车! 5. Java 8 函数式接口 5.1. 定义 函数式接口在Java中是指：有且仅有一个抽象方法的接口\n函数式接口(FunctionalInterface)就是一个有且仅有一个抽象方法的接口，但可以有多个默认方法，静态方法 接口默认继承 java.lang.Object，所以如果接口显示声明覆盖了 Object 中方法，那么也不算抽象方法。 函数式接口可以被隐式转换为 lambda 表达式 函数式接口可以现有的函数友好地支持 lambda 表达式 5.2. @FunctionalInterface 注解 与 @Override 注解的作用类似，Java 8中专门为函数式接口引入了一个新的注解：@FunctionalInterface。该注解可用于一个接口的定义上：\n1 2 3 4 @FunctionalInterface public interface 接口名 { 返回值类型 方法名(); } 使用该注解来定义接口，编译器将会强制检查该接口是否确实有且仅有一个抽象方法，否则将会报错。不过，即使不使用该注解，只要满足函数式接口的定义，这仍然是一个函数式接口，使用起来效果一样\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Demo01FunctionalInterface { @Test public void functionalInterfaceTest() { // 创建待求和数组 int[] arr = {1, 2, 3, 4}; // 使用lambda表达式方式，调用方法 sum(arr, a -\u0026gt; { int total = 0; for (int n : a) { total += n; } return total; }); } // 定义方法，方法形参为自定义的函数式接口作为方法参数 private void sum(int[] arr, Operator operator) { // 1. 调用函数式接口的求和抽象方法 int sum = operator.getSum(arr); // 2. 输入结果 System.out.println(\u0026#34;数组的计算结果是：\u0026#34; + sum); } } /** * 定义函数式接口（只有一个抽象方法，可以有多个默认方法与静态方法） */ @FunctionalInterface interface Operator { int getSum(int[] arr); } 5.3. 相关Java内置函数式接口接口 JDK 1.8之前已有的函数式接口: java.lang.Runnable java.util.concurrent.Callable java.security.PrivilegedAction java.util.Comparator java.io.FileFilter java.nio.file.PathMatcher java.lang.reflect.InvocationHandler java.beans.PropertyChangeListener java.awt.event.ActionListener javax.swing.event.ChangeListener JDK 1.8 新增加的函数接口： java.util.function 包下 java.util.function 它包含了很多类，用来支持 Java的函数式编程，\n5.4. 常用内置函数式接口 5.4.1. Supplier 接口 1 2 3 4 5 6 7 @FunctionalInterface public interface Supplier\u0026lt;T\u0026gt; { /** * Gets a result. */ T get(); } java.util.function.Supplier\u0026lt;T\u0026gt; 接口，它意味着\u0026quot;供给\u0026quot;，对应的Lambda表达式需要“对外提供”一个符合泛型类型的对象数据。供给型接口，通过Supplier接口中的get()方法可以得到一个值，无参有返回的接口。\n示例：使用 Supplier 接口作为方法参数类型，通过Lambda表达式求出int数组中的最大值。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Demo02Supplier { @Test public void supplierTest() { // 使用Lambda表达式返回数组元素最大值 printMax(() -\u0026gt; { System.out.println(\u0026#34;Supplier接口实现get()方法执行开始...\u0026#34;); int[] arr = {11, 99, 88, 77, 22}; // Arrays工具类的sort方法默认是升序排序 Arrays.sort(arr); return arr[arr.length - 1]; }); } private void printMax(Supplier\u0026lt;Integer\u0026gt; supplier) { System.out.println(\u0026#34;printMax()方法执行开始...\u0026#34;); // 调用“供给”接口Supplier，获取数组最大值 Integer max = supplier.get(); System.out.println(\u0026#34;max = \u0026#34; + max); System.out.println(\u0026#34;printMax()方法执行结束...\u0026#34;); } } 输出结果\n1 2 3 4 printMax()方法执行开始... Supplier接口实现get()方法执行开始... max = 99 printMax()方法执行结束... 5.4.2. Consumer 接口 5.4.2.1. 基础使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @FunctionalInterface public interface Consumer\u0026lt;T\u0026gt; { /** * Performs this operation on the given argument. * * @param t the input argument */ void accept(T t); default Consumer\u0026lt;T\u0026gt; andThen(Consumer\u0026lt;? super T\u0026gt; after) { Objects.requireNonNull(after); return (T t) -\u0026gt; { accept(t); after.accept(t); }; } } java.util.function.Consumer\u0026lt;T\u0026gt; 接口则正好与Supplier相反，它不是生产一个数据，而是消费一个数据，其数据类型由泛型参数决定。Consumer消费型接口，可以拿到accept(T t)方法参数传递过来的数据进行处理, 有参无返回的接口。\n示例：将一个字符串转成全大写的字符串\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class Demo03Consumer { @Test public void consumerTest() { System.out.println(\u0026#34;程序开始!\u0026#34;); // 使用Lambda表达式将一个字符串转成大写的字符串 printString(str -\u0026gt; System.out.println(str.toUpperCase())); System.out.println(\u0026#34;程序结束!!\u0026#34;); } private void printString(Consumer\u0026lt;String\u0026gt; consumer) { System.out.println(\u0026#34;printString()方法执行开始...\u0026#34;); // 调用“消费型”接口Consumer，处理传入的字符串 consumer.accept(\u0026#34;Hello Consumer\u0026#34;); System.out.println(\u0026#34;printString()方法执行结束...\u0026#34;); } } // 输出结果 程序开始! printString()方法执行开始... HELLO CONSUMER printString()方法执行结束... 程序结束!! 5.4.2.2. 默认方法：andThen() 如果一个方法的参数和返回值全都是 Consumer 类型，那么就可以实现效果：消费一个数据的时候，首先做一个操作，然后再做一个操作，实现组合。而这个方法就是 Consumer 接口中的default方法 andThen()\n1 2 3 4 default Consumer\u0026lt;T\u0026gt; andThen(Consumer\u0026lt;? super T\u0026gt; after) { Objects.requireNonNull(after); return (T t) -\u0026gt; { accept(t); after.accept(t); }; } 备注： java.util.Objects 的 requireNonNull 静态方法将会在参数为null时主动抛出 NullPointerException 异常。这省去了重复编写if语句和抛出空指针异常的麻烦\n示例：将一个字符串先转成全小写的字符串，再转成全大写的字符串\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Demo04ConsumerAndThen { @Test public void consumerTest() { System.out.println(\u0026#34;程序开始!\u0026#34;); /* 使用Lambda表达式先将一个字符串转成小写的字符串,再转成大写 */ printString(str -\u0026gt; System.out.println(str.toLowerCase()), str -\u0026gt; System.out.println(str.toUpperCase())); System.out.println(\u0026#34;程序结束!!\u0026#34;); } private void printString(Consumer\u0026lt;String\u0026gt; c1, Consumer\u0026lt;String\u0026gt; c2) { System.out.println(\u0026#34;printString()方法执行开始...\u0026#34;); // 待处理字符串 String str = \u0026#34;Hello Consumer\u0026#34;; // 实现方式一：先后调用两个“消费型”接口Consumer，处理不同的逻辑 // c1.accept(str); // c2.accept(str); // 实现方式二：使用Consumer接口的andThen方法，实现先后执行不同的Consumer接口实现 c1.andThen(c2).accept(str); System.out.println(\u0026#34;printString()方法执行结束...\u0026#34;); } } // 输出结果 程序开始! printString()方法执行开始... hello consumer HELLO CONSUMER printString()方法执行结束... 程序结束!! 5.4.3. Function 接口 5.4.3.1. 基础使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @FunctionalInterface public interface Function\u0026lt;T, R\u0026gt; { /** * Applies this function to the given argument. * * @param t the function argument * @return the function result */ R apply(T t); default \u0026lt;V\u0026gt; Function\u0026lt;V, R\u0026gt; compose(Function\u0026lt;? super V, ? extends T\u0026gt; before) { Objects.requireNonNull(before); return (V v) -\u0026gt; apply(before.apply(v)); } default \u0026lt;V\u0026gt; Function\u0026lt;T, V\u0026gt; andThen(Function\u0026lt;? super R, ? extends V\u0026gt; after) { Objects.requireNonNull(after); return (T t) -\u0026gt; after.apply(apply(t)); } static \u0026lt;T\u0026gt; Function\u0026lt;T, T\u0026gt; identity() { return t -\u0026gt; t; } } java.util.function.Function\u0026lt;T,R\u0026gt; 接口用来根据一个类型的数据得到另一个类型的数据，前者称为前置条件，后者称为后置条件。Function转换型接口，对apply方法传入的T类型数据进行处理，返回R类型的结果，有参有返回的接口。\n请注意，Function的前置条件泛型和后置条件泛型可以相同。\n示例：将 String 类型转换为 Integer 类型\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class Demo05Function { @Test public void functionTest() { System.out.println(\u0026#34;程序开始!\u0026#34;); // 使用Lambda表达式将字符串转成数字 stringToInteger(str -\u0026gt; Integer.parseInt(str)); System.out.println(\u0026#34;程序结束!!\u0026#34;); } private void stringToInteger(Function\u0026lt;String, Integer\u0026gt; function) { System.out.println(\u0026#34;stringToInteger()方法执行开始...\u0026#34;); // 调用“转换型”接口Function，处理传入的字符串转成数字类型 Integer num = function.apply(\u0026#34;8\u0026#34;); System.out.println(\u0026#34;字符串转数字类型结果：\u0026#34; + num); System.out.println(\u0026#34;stringToInteger()方法执行结束...\u0026#34;); } } // 输出结果 程序开始! stringToInteger()方法执行开始... 字符串转数字类型结果：8 stringToInteger()方法执行结束... 程序结束!! 5.4.3.2. 默认方法：andThen() 1 2 3 4 default \u0026lt;V\u0026gt; Function\u0026lt;T, V\u0026gt; andThen(Function\u0026lt;? super R, ? extends V\u0026gt; after) { Objects.requireNonNull(after); return (T t) -\u0026gt; after.apply(apply(t)); } Function 接口中有一个默认的 andThen 方法，用来进行组合操作。\n示例：先将字符串解析成为int数字，再操作数字乘以10\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class Demo06FunctionAndThen { @Test public void functionTest() { System.out.println(\u0026#34;程序开始!\u0026#34;); // 使用Lambda表达式先将字符串解析成为int数字，再操作数字乘以10 stringToInteger(str -\u0026gt; Integer.parseInt(str), i -\u0026gt; i * 10); System.out.println(\u0026#34;程序结束!!\u0026#34;); } private void stringToInteger(Function\u0026lt;String, Integer\u0026gt; f1, Function\u0026lt;Integer, Integer\u0026gt; f2) { System.out.println(\u0026#34;stringToInteger()方法执行开始...\u0026#34;); // 实现方式一：选择调用“转换型”接口Function，先后处理不同的转换逻辑 // Integer num = f1.apply(\u0026#34;8\u0026#34;); // Integer result = f2.apply(num); // 实现方式二：使用Function接口的andThen方法，实现先后执行不同的Function接口实现 Integer result = f1.andThen(f2).apply(\u0026#34;8\u0026#34;); System.out.println(\u0026#34;字符串转数字类型再乘10后结果：\u0026#34; + result); System.out.println(\u0026#34;stringToInteger()方法执行结束...\u0026#34;); } } // 输入结果 程序开始! stringToInteger()方法执行开始... 字符串转数字类型再乘10后结果：80 stringToInteger()方法执行结束... 程序结束!! 5.4.4. Predicate 接口 5.4.4.1. 基础使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @FunctionalInterface public interface Predicate\u0026lt;T\u0026gt; { /** * Evaluates this predicate on the given argument. * * @param t the input argument * @return {@code true} if the input argument matches the predicate, * otherwise {@code false} */ boolean test(T t); default Predicate\u0026lt;T\u0026gt; and(Predicate\u0026lt;? super T\u0026gt; other) { Objects.requireNonNull(other); return (t) -\u0026gt; test(t) \u0026amp;\u0026amp; other.test(t); } default Predicate\u0026lt;T\u0026gt; negate() { return (t) -\u0026gt; !test(t); } default Predicate\u0026lt;T\u0026gt; or(Predicate\u0026lt;? super T\u0026gt; other) { Objects.requireNonNull(other); return (t) -\u0026gt; test(t) || other.test(t); } static \u0026lt;T\u0026gt; Predicate\u0026lt;T\u0026gt; isEqual(Object targetRef) { return (null == targetRef) ? Objects::isNull : object -\u0026gt; targetRef.equals(object); } } java.util.function.Predicate\u0026lt;T\u0026gt; 接口是一个函数式接口，它接受一个输入参数T，返回一个布尔值结果 该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑（比如：与，或，非） 该接口用于测试对象是 true 或 false 示例1：判断一个人名如果超过3个字就认为是很长的名字\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class Demo07Predicate { @Test public void predicateTest() { System.out.println(\u0026#34;程序开始!\u0026#34;); // 使用Lambda判断一个人名如果超过3个字就认为是很长的名字 isLongName(\u0026#34;石原里美\u0026#34;, str -\u0026gt; str.length() \u0026gt; 3); System.out.println(\u0026#34;程序结束!!\u0026#34;); } private void isLongName(String name, Predicate\u0026lt;String\u0026gt; predicate) { System.out.println(\u0026#34;isLongName()方法执行开始...\u0026#34;); // 调用“判断型”接口Predicate，进行相应的逻辑处理 boolean isLong = predicate.test(name); System.out.println(\u0026#34;名字是否过长：\u0026#34; + isLong); System.out.println(\u0026#34;isLongName()方法执行结束...\u0026#34;); } } // 输出结果 程序开始! isLongName()方法执行开始... 名字是否过长：true isLongName()方法执行结束... 程序结束!! 示例2：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package com.moon.jav.test; import java.util.Arrays; import java.util.List; import java.util.function.Predicate; public class Java8Tester { public static void main(String args[]) { List\u0026lt;Integer\u0026gt; list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); /* * Predicate\u0026lt;Integer\u0026gt; predicate = n -\u0026gt; true * n 是一个参数传递到 Predicate 接口的 test 方法 * n 如果存在则 test 方法返回 true */ System.out.println(\u0026#34;输出所有数据:\u0026#34;); // 传递参数 n eval(list, n -\u0026gt; true); /* * Predicate\u0026lt;Integer\u0026gt; predicate1 = n -\u0026gt; n%2 == 0 * n 是一个参数传递到 Predicate 接口的 test 方法 * 如果 n%2 为 0 test 方法返回 true */ System.out.println(\u0026#34;\\n输出所有偶数:\u0026#34;); eval(list, n -\u0026gt; n % 2 == 0); /* * Predicate\u0026lt;Integer\u0026gt; predicate2 = n -\u0026gt; n \u0026gt; 3 * n 是一个参数传递到 Predicate 接口的 test 方法 * 如果 n 大于 3 test 方法返回 true */ System.out.println(\u0026#34;\\n输出大于 3 的所有数字:\u0026#34;); eval(list, n -\u0026gt; n \u0026gt; 3); } public static void eval(List\u0026lt;Integer\u0026gt; list, Predicate\u0026lt;Integer\u0026gt; predicate) { for (Integer n : list) { if (predicate.test(n)) { System.out.print(n + \u0026#34; \u0026#34;); } } } } // 输出结果 输出所有数据: 1 2 3 4 5 6 7 8 9 输出所有偶数: 2 4 6 8 输出大于3的所有数字: 4 5 6 7 8 9 5.4.4.2. 默认方法：and、or、negate 1 2 3 4 5 6 7 8 9 10 11 12 13 default Predicate\u0026lt;T\u0026gt; and(Predicate\u0026lt;? super T\u0026gt; other) { Objects.requireNonNull(other); return (t) -\u0026gt; test(t) \u0026amp;\u0026amp; other.test(t); } default Predicate\u0026lt;T\u0026gt; negate() { return (t) -\u0026gt; !test(t); } default Predicate\u0026lt;T\u0026gt; or(Predicate\u0026lt;? super T\u0026gt; other) { Objects.requireNonNull(other); return (t) -\u0026gt; test(t) || other.test(t); } 默认方法and()：将两个 Predicate 条件使用“\u0026amp;\u0026amp;”逻辑连接起来实现“并且”的效果 默认方法or()：将两个 Predicate 条件使用“||”逻辑连接起来实现“或者”的效果 默认方法negate()：将 Predicate 条件使用“!”逻辑实现“非”（“取反”）的效果 示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class Demo08PredicateAndOrNegate { @Test public void predicateTest() { System.out.println(\u0026#34;程序开始!\u0026#34;); test(\u0026#34;Hello World\u0026#34;, str -\u0026gt; str.contains(\u0026#34;W\u0026#34;), str -\u0026gt; str.contains(\u0026#34;H\u0026#34;)); System.out.println(\u0026#34;程序结束!!\u0026#34;); } private void test(String str, Predicate\u0026lt;String\u0026gt; p1, Predicate\u0026lt;String\u0026gt; p2) { System.out.println(\u0026#34;test()方法执行开始...\u0026#34;); // 使用Lambda表达式判断一个字符串中既包含W,也包含H // and方法，相当于 p1.test(str) \u0026amp;\u0026amp; p2.test(str) boolean b1 = p1.and(p2).test(str); if (b1) { System.out.println(\u0026#34;既包含W,也包含H\u0026#34;); } // 使用Lambda表达式判断一个字符串中包含W或者包含H // or方法，相当于 p1.test(str) || p2.test(str) boolean b2 = p1.or(p2).test(str); if (b2) { System.out.println(\u0026#34;包含W或者包含H\u0026#34;); } // 使用Lambda表达式判断一个字符串中不包含W // negate相当于取反 相当于 !p1.test(str) boolean b3 = p1.negate().test(str); if (b3) { System.out.println(\u0026#34;不包含W\u0026#34;); } System.out.println(\u0026#34;test()方法执行结束...\u0026#34;); } } 5.5. 函数式接口应用示例 5.5.1. 实现对象通用 Builder 链式设置属性值 创建测试实体类 1 2 3 4 5 6 7 public class User { private String userName; private int age; // ...更多其他的属性 // ...省略 setter/getter } 利用 Supplier 与 Consumer 函数式接口特性，实现通用对象 Builder 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class CommonBuilder\u0026lt;T\u0026gt; { private final Supplier\u0026lt;T\u0026gt; instantiator; private List\u0026lt;Consumer\u0026lt;T\u0026gt;\u0026gt; modifiers = new ArrayList\u0026lt;\u0026gt;(); public CommonBuilder(Supplier\u0026lt;T\u0026gt; instantiator) { this.instantiator = instantiator; } public static \u0026lt;T\u0026gt; CommonBuilder\u0026lt;T\u0026gt; of(Supplier\u0026lt;T\u0026gt; instantiator) { return new CommonBuilder\u0026lt;\u0026gt;(instantiator); } public \u0026lt;V\u0026gt; CommonBuilder\u0026lt;T\u0026gt; with(ValueConsumer\u0026lt;T, V\u0026gt; consumer, V v) { Consumer\u0026lt;T\u0026gt; c = instance -\u0026gt; consumer.accept(instance, v); modifiers.add(c); return this; } public T build() { // 获取创建的对象 T value = instantiator.get(); // 循环所有消费方法，设置对象属性值 modifiers.forEach(modifier -\u0026gt; modifier.accept(value)); modifiers.clear(); return value; } // 自定义消费函数式接口 @FunctionalInterface public interface ValueConsumer\u0026lt;T, V\u0026gt; { void accept(T t, V v); } } Tips: 可以参考示例自定义的函数式接口，按需求支持多个参数的设置属性方法。\n测试 1 2 3 4 5 6 7 8 9 10 @Test public void test() { CommonBuilder\u0026lt;User\u0026gt; builder = CommonBuilder.of(User::new); User user = builder .with(User::setUserName, \u0026#34;MooN\u0026#34;) .with(User::setAge, 28) .build(); System.out.println(user); } 6. Stream 流 Java 8 API 添加了一个新的抽象称为流Stream，可以让你以一种声明的方式处理数据。 Stream 使用一种类似用SQL语句从数据库查询数据的直观方式来提供一种对Java集合运算和表达的高阶抽象。 Stream API 可以极大提高Java程序员的生产力，让程序员写出高效率、干净、简洁的代码。 这种风格将要处理的元素集合看作一种流，流在管道中传输，并且可以在管道的节点上进行处理，比如筛选，排序，聚合等。 元素流在管道中经过中间操作（intermediate operation）的处理，最后由最终操作(terminal operation)得到前面处理的结果。 Stream流式思想类似于工厂车间的“生产流水线”，Stream流不是一种数据结构，不保存数据，而是对数据进行加工处理处理\n6.1. 什么是 Stream？ Stream（流）是一个来自数据源的元素队列并支持聚合操作 元素：是特定类型的对象，形成一个队列。Java中的Stream并不会存储元素，而是按需计算。 数据源：流的来源。可以是集合，数组，I/O channel，产生器generator等。 聚合操作：类似SQL语句一样的操作，比如filter, map, reduce, find, match, sorted等。 和以前的Collection操作不同，Stream操作还有两个基础的特征： Pipelining:：中间操作都会返回流对象本身。这样多个操作可以串联成一个管道，如同流式风格（fluent style）。这样做可以对操作进行优化，比如延迟执行(laziness)和短路(short-circuiting)。 内部迭代：以前对集合遍历都是通过Iterator或者For-Each的方式,显式的在集合外部进行迭代，这叫做外部迭代。Stream提供了内部迭代的方式，通过访问者模式(Visitor)实现。 6.2. 流的操作特性（重要） stream 不是数据结构，不存储数据 stream 不改变原来的数据源，它会将操作后的数据保存到另外一个对象中。 stream 不可重复使用。每次进行操作后都会产生新的流，原来的流就会关闭，所以可以进行链式编程，就不会出现“流已关闭”的错误 惰性求值，流在中间处理过程中，只是对操作进行了记录，并不会立即执行（即相当于预告声明，不会马上操作），需要等到执行终止操作的时候才会进行实际的计算。（即Stream不调用终结方法时，中间的操作不会执行） 6.3. 流的分类 6.3.1. 流的操作类型 stream 所有操作组合在一起即变成了管道，管道中有以下两个操作：\n中间操作（intermediate）：调用中间操作方法会返回一个新的流。通过连续执行多个操作倒便就组成了 Stream 中的执行管道（pipeline）。需要注意的是这些管道被添加后并不会真正执行，只有等到调用终值操作之后才会执行。 终值操作（terminal）：在调用该方法之后，将执行之前所有的中间操作，获得返回结果结束对流的使用 流的执行顺序说明：其每个元素挨着作为参数去调用中间操作及终值操作，而不是遍历一个方法，再遍历下一个方法\n6.3.2. 流的API方法对应分类 无状态：指元素的处理不受之前元素的影响；\n有状态：指该操作只有拿到所有元素之后才能继续下去。\n非短路操作：指必须处理所有元素才能得到最终结果；\n短路操作：指遇到某些符合条件的元素就可以得到最终结果，如 A || B，只要A为true，则无需判断B的结果。\nIntermediate：\nmap (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered Terminal：\nforEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator Short-circuiting：\nanyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit 6.4. 流的常用创建方法 6.4.1. Collection 下的 stream() 方法 在 Java 8 中，所有的 Collection 集合接口都有stream()为集合创建串行流。串行的流，就是在一个线程上执行\n1 2 3 4 5 public static void main(String[] args) { List\u0026lt;String\u0026gt; strings = Arrays.asList(\u0026#34;abc\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;bc\u0026#34;, \u0026#34;efg\u0026#34;, \u0026#34;abcd\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;jkl\u0026#34;); List\u0026lt;String\u0026gt; filtered = strings.stream().filter(string -\u0026gt; !string.isEmpty()).collect(Collectors.toList()); // 获取一个串行流 Stream\u0026lt;String\u0026gt; parallelStream = strings.parallelStream(); // 获取一个并行流 } 6.4.2. Stream 中的静态方法：of()、iterate()、generate() 由于数组对象不可能添加默认方法，所以 Stream 接口中提供了静态方法 of\n1 2 3 4 5 6 7 Stream\u0026lt;Integer\u0026gt; stream = Stream.of(1,2,3,4,5,6); Stream\u0026lt;Integer\u0026gt; stream2 = Stream.iterate(0, (x) -\u0026gt; x + 2).limit(6); stream2.forEach(System.out::println); // 0 2 4 6 8 10 Stream\u0026lt;Double\u0026gt; stream3 = Stream.generate(Math::random).limit(2); stream3.forEach(System.out::println); 备注： Stream.of() 方法的参数其实是一个可变参数，所以支持数组。\n6.4.3. Arrays.stream() Arrays 中的 stream() 静态方法，将数组转成流\n1 2 Integer[] nums = new Integer[10]; Stream\u0026lt;Integer\u0026gt; stream = Arrays.stream(nums); 6.4.4. BufferedReader.lines() BufferedReader.lines() 方法，将每行内容转成流\n1 2 3 BufferedReader reader = new BufferedReader(new FileReader(\u0026#34;F:\\\\test_stream.txt\u0026#34;)); Stream\u0026lt;String\u0026gt; lineStream = reader.lines(); lineStream.forEach(System.out::println); 6.4.5. Pattern.splitAsStream() Pattern.splitAsStream() 方法，将字符串分隔成流\n1 2 3 Pattern pattern = Pattern.compile(\u0026#34;,\u0026#34;); Stream\u0026lt;String\u0026gt; stringStream = pattern.splitAsStream(\u0026#34;a,b,c,d\u0026#34;); stringStream.forEach(System.out::println); 6.5. 流的中间操作 6.5.1. map 方法 1 \u0026lt;R\u0026gt; Stream\u0026lt;R\u0026gt; map(Function\u0026lt;? super T, ? extends R\u0026gt; mapper); Stream流的map方法流中的元素映射到另一个流中，该接口需要一个 Function 函数式接口参数，该函数会被应用到每个元素上，可以将当前流中的T类型数据转换为另一种R类型新元素的流。示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Test public void mapTest() { Stream\u0026lt;String\u0026gt; original = Stream.of(\u0026#34;11\u0026#34;, \u0026#34;22\u0026#34;, \u0026#34;33\u0026#34;); // 获取流，调用Stream流的map将一种类型的流转换成另一种类型的流 /* * 示例1：将Stream流中的字符串转成Integer * map 方法的参数通过方法引用，将字符串类型转换成为了int类型（并自动装箱为 Integer 类对象）。 */ /*Stream\u0026lt;Integer\u0026gt; stream = original.map((String s) -\u0026gt; { return Integer.parseInt(s); });*/ // 简化lambda表达式 // original.map(s -\u0026gt; Integer.parseInt(s)).forEach(System.out::println) // 使用方法引用 original.map(Integer::parseInt).forEach(System.out::println); // 示例2：获取数组元素的平方数 List\u0026lt;Integer\u0026gt; numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5); List\u0026lt;Integer\u0026gt; squaresList = numbers.stream() .map(i -\u0026gt; i * i) .distinct() .collect(Collectors.toList()); System.out.println(squaresList); // 输出：[9, 4, 49, 25] } 6.5.2. flatMap 方法 1 \u0026lt;R\u0026gt; Stream\u0026lt;R\u0026gt; flatMap(Function\u0026lt;? super T, ? extends Stream\u0026lt;? extends R\u0026gt;\u0026gt; mapper); Stream流的flatMap方法流中的元素映射到另一个流中，接收一个函数作为参数，将流中的每个值都换成另一个流，然后把所有流连接成一个流。。示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 List\u0026lt;String\u0026gt; list = Arrays.asList(\u0026#34;a,b,c\u0026#34;, \u0026#34;1,2,3\u0026#34;); // 将每个元素转成一个新的且不带逗号的元素 Stream\u0026lt;String\u0026gt; s1 = list.stream().map(s -\u0026gt; s.replaceAll(\u0026#34;,\u0026#34;, \u0026#34;\u0026#34;)); s1.forEach(System.out::println); // abc 123 Stream\u0026lt;String\u0026gt; s3 = list.stream().flatMap(s -\u0026gt; { // 将每个元素转换成一个stream String[] split = s.split(\u0026#34;,\u0026#34;); Stream\u0026lt;String\u0026gt; s2 = Arrays.stream(split); return s2; }); s3.forEach(System.out::println); // a b c 1 2 3 理解flapMap的行为：flapMap是用来将多个Stream对象合并成一个新的流Stream对象，而在方法体中的操作，就是解除不需要的嵌套关系，将包含嵌套关系的Stream流转换成持有目标类型的Stream流对象。\n1 2 3 4 5 6 Stream\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; inputStream = Stream.of( Arrays.asList(1), Arrays.asList(2, 3), Arrays.asList(4, 5, 6) ); Stream\u0026lt;Integer\u0026gt; outputStream = inputStream.flatMap((childList) -\u0026gt; childList.stream()); 原本的inputStream持有的元素中类型为List\u0026lt;Integer\u0026gt;，而在扁平化后的outputStream只想要持有Integer类型的元素，即去除List这层嵌套关系。因此在flapMap中，对每个List\u0026lt;Integer\u0026gt;类型的元素执行childList.stream()方法，转换成Stream\u0026lt;Integer\u0026gt;类型，然后由flatMap进行合并。\n6.5.3. filter 方法 1 Stream\u0026lt;T\u0026gt; filter(Predicate\u0026lt;? super T\u0026gt; predicate); Stream流的 filter 方法用于过滤数据，返回符合过滤条件的数据，保留返回true的元素，抛弃返回false的元素。可以通过 filter 方法将一个流转换成另一个子集流。\n该接口接收一个 Predicate 函数式接口参数（可以是一个Lambda或方法引用）作为筛选条件。示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void filterTest() { List\u0026lt;String\u0026gt; nameList = new ArrayList\u0026lt;\u0026gt;(); Collections.addAll(nameList, \u0026#34;天锁斩月\u0026#34;, \u0026#34;剑圣\u0026#34;, \u0026#34;石原里美\u0026#34;, \u0026#34;樱木花道\u0026#34;, \u0026#34;敌法师\u0026#34;, \u0026#34;新垣结衣\u0026#34;); // 示例1：获取流，调用Stream流的filter过滤名字长度为4个字的人 /*nameList.stream().filter((String s) -\u0026gt; { return s.length() == 4; }).forEach((String n) -\u0026gt; { System.out.println(n); });*/ // 简化lambda表达式与使用方法引用 nameList.stream().filter(s -\u0026gt; s.length() == 4).forEach(System.out::println); // 示例2：获取空字符串的数量 List\u0026lt;String\u0026gt; strings = Arrays.asList(\u0026#34;abc\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;bc\u0026#34;, \u0026#34;efg\u0026#34;, \u0026#34;abcd\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;jkl\u0026#34;); int count = (int) strings.stream().filter(String::isEmpty).count(); System.out.println(\u0026#34;空字符串的数量: \u0026#34; + count); // 空字符串的数量: 2 } 6.5.4. limit 方法 1 Stream\u0026lt;T\u0026gt; limit(long maxSize); Stream流的 limit 方法可以对流进行截取，只取用前的maxSize个数据。参数是一个long型，如果集合当前长度大于参数则进行截取。否则不进行操作。示例如下：\n1 2 3 4 5 6 7 8 9 10 11 @Test public void limitTest() { List\u0026lt;String\u0026gt; nameList = new ArrayList\u0026lt;\u0026gt;(); Collections.addAll(nameList, \u0026#34;天锁斩月\u0026#34;, \u0026#34;剑圣\u0026#34;, \u0026#34;石原里美\u0026#34;, \u0026#34;樱木花道\u0026#34;, \u0026#34;敌法师\u0026#34;, \u0026#34;新垣结衣\u0026#34;); // 示例1：获取流，调用Stream流的limit获取前3个名字 nameList.stream() .limit(3) .forEach(System.out::println); // 示例2：获取10个随机数 new Random().ints().limit(10).forEach(System.out::println); } 6.5.5. skip 方法 1 Stream\u0026lt;T\u0026gt; skip(long n); Stream流的 skip 方法可以跳过前几个元素，并获取一个截取之后的新流。如果流的当前长度大于n，则跳过前n个；否则将会得到一个长度为0的空流。示例如下：\n1 2 3 4 5 6 7 @Test public void skipTest() { List\u0026lt;String\u0026gt; nameList = new ArrayList\u0026lt;\u0026gt;(); Collections.addAll(nameList, \u0026#34;天锁斩月\u0026#34;, \u0026#34;剑圣\u0026#34;, \u0026#34;石原里美\u0026#34;, \u0026#34;樱木花道\u0026#34;, \u0026#34;敌法师\u0026#34;, \u0026#34;新垣结衣\u0026#34;); // 示例1：获取流，调用Stream流的skip跳过前面2个数据 nameList.stream().skip(2).forEach(System.out::println); } 注：Stream流的skip(n)方法配合limit(n)方法，可以实现分页的效果\n6.5.6. distinct 方法 1 Stream\u0026lt;T\u0026gt; distinct(); Stream流的 distinct 方法用于去除重复数据。通过流中元素的 hashCode() 和 equals() 去除重复元素，如果对引用对象运行去重，引用对象要实现hashCode和equal方法，否则去重无效。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void distinctTest() { // 示例1：基本类型集合去重 List\u0026lt;Integer\u0026gt; integerList = Stream.of(22, 33, 22, 11, 33).distinct().collect(Collectors.toList()); System.out.println(integerList); List\u0026lt;String\u0026gt; stringList = Stream.of(\u0026#34;aa\u0026#34;, \u0026#34;bb\u0026#34;, \u0026#34;aa\u0026#34;, \u0026#34;bb\u0026#34;, \u0026#34;cc\u0026#34;).distinct().collect(Collectors.toList()); System.out.println(stringList); // 示例2：对象集合去重。引用对象必须要重写hashCode和equal方法，否则去重无效。 List\u0026lt;Person\u0026gt; persons = Stream.of( new Person(\u0026#34;新垣结衣\u0026#34;, 18), new Person(\u0026#34;石原里美\u0026#34;, 30), new Person(\u0026#34;夜神月\u0026#34;, 16), new Person(\u0026#34;新垣结衣\u0026#34;, 18), new Person(\u0026#34;石原里美\u0026#34;, 30), new Person(\u0026#34;L\u0026#34;, 17) ).distinct().collect(Collectors.toList()); System.out.println(persons); } 6.5.7. sorted 方法 1 2 Stream\u0026lt;T\u0026gt; sorted(); Stream\u0026lt;T\u0026gt; sorted(Comparator\u0026lt;? super T\u0026gt; comparator); Stream流的sorted方法是用于排序，可以根据元素的自然顺序排序，也可以指定比较器排序。\nsorted()：自然排序，流中元素需实现Comparable接口 sorted(Comparator comparator)：定制排序，自定义Comparator排序器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Test public void sortedTest() { /* * 示例1： * sorted(): 根据元素的自然顺序排序 * sorted(Comparator\u0026lt;? super T\u0026gt; comparator): 根据比较器指定的规则排序 */ Stream\u0026lt;Integer\u0026gt; stream = Stream.of(33, 22, 11, 55); // 对元素自然顺序排序 // stream.sorted().forEach(System.out::println); // 使用比较器排序 /*stream.sorted((Integer i1, Integer i2) -\u0026gt; { return i2 - i1; }).forEach(System.out::println);*/ // 使用lambda表达与方法引用 stream.sorted((i1, i2) -\u0026gt; i2 - i1).forEach(System.out::println); // 示例2：使用 sorted 方法对输出的 10 个随机数进行排序 new Random().ints().limit(10).sorted().forEach(System.out::println); // 示例3：字符串排序。String 类自身已实现Compareable接口 List\u0026lt;String\u0026gt; strList = Arrays.asList(\u0026#34;dd\u0026#34;, \u0026#34;ff\u0026#34;, \u0026#34;aa\u0026#34;) .stream() .sorted() .collect(Collectors.toList()); System.out.println(strList); // 示例4：对象自定义排序：先按姓名升序，姓名相同则按年龄升序 Person p1 = new Person(\u0026#34;石原里美\u0026#34;, 31); Person p2 = new Person(\u0026#34;新垣结衣\u0026#34;, 28); Person p3 = new Person(\u0026#34;敌法师\u0026#34;, 180); Person p4 = new Person(\u0026#34;新月\u0026#34;, 18); List\u0026lt;Person\u0026gt; persons = Arrays.asList(p1, p2, p3, p4).stream().sorted((o1, o2) -\u0026gt; { if (o1.getName().startsWith(o2.getName().substring(0, 1))) { return o1.getAge() - o2.getAge(); } else { return o1.getName().compareTo(o2.getName()); } }).collect(Collectors.toList()); System.out.println(persons); } 6.5.8. peek 方法 1 Stream\u0026lt;T\u0026gt; peek(Consumer\u0026lt;? super T\u0026gt; action); Stream流的peek方法如同于map，能得到流中的每一个元素。但map接收的是一个Function表达式，有返回值；而peek接收的是Consumer表达式，没有返回值\n1 2 3 4 5 6 7 8 9 10 11 Student s1 = new Student(\u0026#34;aa\u0026#34;, 10); Student s2 = new Student(\u0026#34;bb\u0026#34;, 20); List\u0026lt;Student\u0026gt; studentList = Arrays.asList(s1, s2); studentList.stream() .peek(o -\u0026gt; o.setAge(100)) .forEach(System.out::println); //结果： Student{name=\u0026#39;aa\u0026#39;, age=100} Student{name=\u0026#39;bb\u0026#39;, age=100} 6.6. 流的终止操作 6.6.1. forEach 方法 1 void forEach(Consumer\u0026lt;? super T\u0026gt; action); Stream 提供的方法 forEach 来迭代流中的每个数据。该方法接收一个 Consumer 接口函数，会将每一个流元素交给该函数进行处理。示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void forEachTest() { List\u0026lt;String\u0026gt; nameList = new ArrayList\u0026lt;\u0026gt;(); Collections.addAll(nameList, \u0026#34;天锁斩月\u0026#34;, \u0026#34;剑圣\u0026#34;, \u0026#34;石原里美\u0026#34;, \u0026#34;樱木花道\u0026#34;, \u0026#34;敌法师\u0026#34;, \u0026#34;新垣结衣\u0026#34;); // 示例1：遍历名称字符串集合 // 获取流，调用Stream流的forEach方法遍历集合 nameList.stream().forEach((String str) -\u0026gt; { System.out.println(str); }); // 简化Lambda表达式 nameList.stream().forEach(str -\u0026gt; System.out.println(str)); // 使用方法引用替换Lambda表达式 nameList.stream().forEach(System.out::println); // 示例2：输出了10个随机数 Random random = new Random(); // Random类的ints()方法获取IntStream流对象，可以使用 random.ints().limit(10).forEach(System.out::println); } 6.6.2. count 方法 1 long count(); Stream流提供 count 方法来统计其中的元素个数。该方法返回一个long值代表元素个数。示例如下：\n1 2 3 4 5 6 7 8 @Test public void countTest() { List\u0026lt;String\u0026gt; nameList = new ArrayList\u0026lt;\u0026gt;(); Collections.addAll(nameList, \u0026#34;天锁斩月\u0026#34;, \u0026#34;剑圣\u0026#34;, \u0026#34;石原里美\u0026#34;, \u0026#34;樱木花道\u0026#34;, \u0026#34;敌法师\u0026#34;, \u0026#34;新垣结衣\u0026#34;); // 获取流，调用Stream流的count获取集合的个数 long count = nameList.stream().count(); System.out.println(\u0026#34;count: \u0026#34; + count); } 6.6.3. match 相关方法 1 2 3 boolean anyMatch(Predicate\u0026lt;? super T\u0026gt; predicate); boolean allMatch(Predicate\u0026lt;? super T\u0026gt; predicate); boolean noneMatch(Predicate\u0026lt;? super T\u0026gt; predicate); Stream流的 anyMatch、allMatch、noneMatch 方法用于判断数据是否匹配指定的条件\nanyMatch：接收一个 Predicate 函数，只要流中有一个元素满足该断言则返回true，否则返回false allMatch：接收一个 Predicate 函数，当流中每个元素都符合该断言时才返回true，否则返回false noneMatch：接收一个 Predicate 函数，当流中每个元素都不符合该断言时才返回true，否则返回false 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void matchTest() { // 定义集合 List\u0026lt;Integer\u0026gt; list = Arrays.asList(5, 3, 6, 1); // allMatch: 匹配所有元素，所有元素都需要满足条件 boolean allMatch = list.stream().allMatch(i -\u0026gt; i \u0026gt; 2); System.out.println(allMatch); // anyMatch: 匹配某个元素，只要有其中一个元素满足条件即可 boolean anyMatch = list.stream().anyMatch(i -\u0026gt; i \u0026gt; 5); System.out.println(anyMatch); // noneMatch: 匹配所有元素，所有元素都不满足条件 boolean noneMatch = list.stream().noneMatch(i -\u0026gt; i \u0026lt; 0); System.out.println(noneMatch); } 6.6.4. find 相关方法 1 2 Optional\u0026lt;T\u0026gt; findFirst(); Optional\u0026lt;T\u0026gt; findAny(); Stream流的 findFirst、findAny 方法用于查找数据，都是返回流中的第一元素\n6.6.5. max 和 min 方法 1 2 Optional\u0026lt;T\u0026gt; min(Comparator\u0026lt;? super T\u0026gt; comparator); Optional\u0026lt;T\u0026gt; max(Comparator\u0026lt;? super T\u0026gt; comparator); Stream流的 max 和 min 方法是用于获取最大值和最小值\nmax：返回流中元素最大值 min：返回流中元素最小值 1 2 3 4 5 6 7 8 9 10 11 @Test public void maxAndMinTest() { // 定义集合 List\u0026lt;Integer\u0026gt; list = Arrays.asList(5, 3, 6, 1); // 获取最大值 Optional\u0026lt;Integer\u0026gt; max = list.stream().max((o1, o2) -\u0026gt; o1 - o2); System.out.println(\u0026#34;最大值: \u0026#34; + max.get()); // 获取最小值 Optional\u0026lt;Integer\u0026gt; min = list.stream().min(Integer::compareTo); System.out.println(\u0026#34;最小值: \u0026#34; + min.get()); } 6.6.6. reduce 方法 6.6.6.1. 功能介绍 这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值（种子），然后依照运算规则（BinaryOperator），和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说，字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce\n例如：Stream 的 sum 就相当于 Integer sum = integers.reduce(0, (a, b) -\u0026gt; a+b); 或 Integer sum = integers.reduce(0, Integer::sum);；也有没有起始值的情况，这时会把 Stream 的前面两个元素组合起来，返回的是 Optional。\n6.6.6.2. 源码相关API 1 Optional\u0026lt;T\u0026gt; reduce(BinaryOperator\u0026lt;T\u0026gt; accumulator); 第一次执行时，accumulator函数的第一个参数为流中的第一个元素，第二个参数为流中元素的第二个元素；第二次执行时，第一个参数为第一次函数执行的结果，第二个参数为流中的第三个元素；依次类推。 1 T reduce(T identity, BinaryOperator\u0026lt;T\u0026gt; accumulator); 流程跟上面一样，只是第一次执行时，accumulator函数的第一个参数为identity，而第二个参数为流中的第一个元素。 1 \u0026lt;U\u0026gt; U reduce(U identity, BiFunction\u0026lt;U, ? super T, U\u0026gt; accumulator, BinaryOperator\u0026lt;U\u0026gt; combiner); 在串行流(stream)中，该方法跟第二个方法一样，即第三个参数combiner不会起作用。在并行流(parallelStream)中，我们知道流被fork join出多个线程进行执行，此时每个线程的执行流程就跟第二个方法reduce(identity,accumulator)一样，而第三个参数combiner函数，则是将每个线程的执行结果当成一个新的流，然后使用第一个方法reduce(accumulator)流程进行规约 6.6.6.3. 基础使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 @Test public void reduceTest() { // 定义集合 List\u0026lt;Integer\u0026gt; list = Arrays.asList(4, 5, 3, 9); /* * T reduce(T identity, BinaryOperator\u0026lt;T\u0026gt; accumulator); * 第1个参数identity：方法执行时初始值，首次执行的时候，会赋值给accumulator参数函数的第一个入参 * 第2个参数accumulator：流每次处理数据的逻辑 * * 示例中reduce方法的执行流程： * 第一次, 将默认值赋值给x, 取出集合第一元素赋值给y * 第二次, 将上一次返回的结果赋值x, 取出集合第二元素赋值给y * 第三次, 将上一次返回的结果赋值x, 取出集合第三元素赋值给y * 第四次, 将上一次返回的结果赋值x, 取出集合第四元素赋值给y */ // 使用reduce方法求和 int result = list.stream().reduce(0, (x, y) -\u0026gt; { System.out.println(\u0026#34;x = \u0026#34; + x + \u0026#34;, y = \u0026#34; + y); return x + y; }); System.out.println(\u0026#34;result = \u0026#34; + result); // 21 // 使用reduce方法获取最大值 Integer max = list.stream().reduce(0, (x, y) -\u0026gt; x \u0026gt; y ? x : y); System.out.println(\u0026#34;max = \u0026#34; + max); // 字符串连接，concat = \u0026#34;ABCD\u0026#34; String concatString = Stream.of(\u0026#34;A\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;C\u0026#34;, \u0026#34;D\u0026#34;).reduce(\u0026#34;\u0026#34;, String::concat); System.out.println(\u0026#34;concatString = \u0026#34; + concatString); // 求最小值，minValue = -3.0 double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min); System.out.println(\u0026#34;minValue = \u0026#34; + minValue); // 求和，sumValue = 11, 有起始值 int sumValue = Stream.of(1, 2, 3, 4).reduce(1, Integer::sum); System.out.println(\u0026#34;sumValue = \u0026#34; + sumValue); // 求和，sumValue = 10, 无起始值 sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get(); System.out.println(\u0026#34;sumValue = \u0026#34; + sumValue); // 过滤，字符串连接，concatString = \u0026#34;ace\u0026#34; concatString = Stream.of(\u0026#34;a\u0026#34;, \u0026#34;B\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;D\u0026#34;, \u0026#34;e\u0026#34;, \u0026#34;F\u0026#34;) .filter(x -\u0026gt; x.compareTo(\u0026#34;Z\u0026#34;) \u0026gt; 0) .reduce(\u0026#34;\u0026#34;, String::concat); System.out.println(\u0026#34;concatString = \u0026#34; + concatString); //经过测试，当元素个数小于24时，并行时线程数等于元素个数，当大于等于24时，并行时线程数为16 List\u0026lt;Integer\u0026gt; testList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24); Integer v = testList.stream().reduce((x1, x2) -\u0026gt; x1 + x2).get(); System.out.println(v); // 300 // 可以使用方法引用简化lambda表达式。Integer类的static int sum(int a, int b)静态方法等价于(x1, x2) -\u0026gt; x1 + x2 Integer v1 = testList.stream().reduce(10, Integer::sum); System.out.println(v1); // 310 Integer v2 = testList.stream().reduce(0, (x1, x2) -\u0026gt; { System.out.println(\u0026#34;stream accumulator: x1:\u0026#34; + x1 + \u0026#34; x2:\u0026#34; + x2); return x1 - x2; }, (x1, x2) -\u0026gt; { System.out.println(\u0026#34;stream combiner: x1:\u0026#34; + x1 + \u0026#34; x2:\u0026#34; + x2); return x1 * x2; }); System.out.println(v2); // -300 Integer v3 = testList.parallelStream().reduce(0, (x1, x2) -\u0026gt; { System.out.println(\u0026#34;parallelStream accumulator: x1:\u0026#34; + x1 + \u0026#34; x2:\u0026#34; + x2); return x1 - x2; }, (x1, x2) -\u0026gt; { System.out.println(\u0026#34;parallelStream combiner: x1:\u0026#34; + x1 + \u0026#34; x2:\u0026#34; + x2); return x1 * x2; }); System.out.println(v3); // -775946240 } 6.6.6.4. 配合map方法使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Test public void mapAndReduceTest() { // 准备测试的集合数据 List\u0026lt;Person\u0026gt; persons = Arrays.asList( new Person(\u0026#34;新垣结衣\u0026#34;, 18), new Person(\u0026#34;夜神月\u0026#34;, 16), new Person(\u0026#34;石原里美\u0026#34;, 30), new Person(\u0026#34;L\u0026#34;, 17) ); /* * 示例1：求出所有年龄的总和 * 1.得到所有的年龄 * 2.让年龄相加 */ Integer totalAge = persons.stream() .map(p -\u0026gt; p.getAge()) .reduce(0, Integer::sum); // Integer类有sum方法，相当于(x, y) -\u0026gt; x + y System.out.println(\u0026#34;totalAge = \u0026#34; + totalAge); /* * 示例2：求出最大年龄 * 1.得到所有的年龄 * 2.获取最大的年龄 */ Integer maxAge = persons.stream() .map(Person::getAge) // 使用方法引用简化lambda表达式，类名::引用成员方法 相当于 p -\u0026gt; p.getAge() .reduce(0, Math::max); // Math类有max方法，相当于(a, b) -\u0026gt; (a \u0026gt;= b) ? a : b System.out.println(\u0026#34;maxAge = \u0026#34; + maxAge); // 示例3：统计a出现的次数。实现思路：将要统计的元素转成数值1，其他元素转成0，然后将所有1相加即为元素出现的次数 // 1 0 0 1 0 1 Integer count = Stream.of(\u0026#34;a\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;a\u0026#34;) .map(s -\u0026gt; { if (\u0026#34;a\u0026#34;.equalsIgnoreCase(s)) { return 1; } else { return 0; } }) .reduce(0, Integer::sum); System.out.println(\u0026#34;count = \u0026#34; + count); } 6.6.7. mapToInt、mapToLong、mapToDouble 方法 1 2 3 IntStream mapToInt(ToIntFunction\u0026lt;? super T\u0026gt; mapper); LongStream mapToLong(ToLongFunction\u0026lt;? super T\u0026gt; mapper); DoubleStream mapToDouble(ToDoubleFunction\u0026lt;? super T\u0026gt; mapper); Stream流转换后的数据都默认生成对象类型或者基本包装类型，如果结果只需要一些基本类型，此时就就会出现问题。因为包装类型占用的内存比基本类型多，在Stream流操作中还会自动装箱和拆箱，这样的操作会影响程序的执行效率。\n如果需要将Stream中的Integer类型数据转成int类型，可以使用 mapToInt 方法。同理于mapToLong、mapToDouble方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void mapToIntTest() { List\u0026lt;Integer\u0026gt; list = Arrays.asList(1, 2, 3, 4, 5); // 示例：把大于3的打印出来 // Integer占用的内存比int多,在Stream流操作中会自动装箱和拆箱 list.stream().filter(n -\u0026gt; n \u0026gt; 3).forEach(System.out::println); /* * IntStream mapToInt(ToIntFunction\u0026lt;? super T\u0026gt; mapper); * IntStream：内部操作的是int类型的数据，可以节省内存，减少自动装箱和拆箱的操作 */ // IntStream intStream = list.stream().mapToInt((Integer n) -\u0026gt; n.intValue()); IntStream intStream = list.stream().mapToInt(Integer::intValue); // 使用方法引用简化代码 intStream.filter(n -\u0026gt; n \u0026gt; 3).forEach(System.out::println); } 6.6.8. concat 方法 1 public static \u0026lt;T\u0026gt; Stream\u0026lt;T\u0026gt; concat(Stream\u0026lt;? extends T\u0026gt; a, Stream\u0026lt;? extends T\u0026gt; b) Stream流的 concat 方法用于将两个流合并成一个流\n特别注意：\n这是一个静态方法，与 java.lang.String 当中的 concat 方法是不同的 如果使用concat方法将两个流合并后，此前的两个流都已关闭，如果再次操作会出现异常 1 2 3 4 5 6 7 8 9 10 11 12 @Test public void concatTest() { Stream\u0026lt;String\u0026gt; streamA = Stream.of(\u0026#34;夜神月\u0026#34;, \u0026#34;L\u0026#34;, \u0026#34;斩月\u0026#34;); Stream\u0026lt;String\u0026gt; streamB = Stream.of(\u0026#34;石原里美\u0026#34;, \u0026#34;新垣结衣\u0026#34;); // 合并成一个流 Stream\u0026lt;String\u0026gt; newStream = Stream.concat(streamA, streamB); // 注意：合并流之后，之前的流都已关闭，如果再次操作会出现异常 // streamA.forEach(System.out::println); newStream.forEach(System.out::println); } 6.7. Stream 流数据的收集操作 6.7.1. collect 方法 Stream流提供 collect 方法，用于将流中元素收集成另外一个数据结构，其参数需要一个 java.util.stream.Collector\u0026lt;T,A, R\u0026gt; 接口对象来指定收集到哪种集合中。java.util.stream.Collectors 类提供一些方法，可以作为Collector接口的实例\n1 2 3 4 5 \u0026lt;R, A\u0026gt; R collect(Collector\u0026lt;? super T, A, R\u0026gt; collector); \u0026lt;R\u0026gt; R collect(Supplier\u0026lt;R\u0026gt; supplier, BiConsumer\u0026lt;R, ? super T\u0026gt; accumulator, BiConsumer\u0026lt;R, R\u0026gt; combiner); 6.7.2. Collector 接口 Collector\u0026lt;T, A, R\u0026gt; 是一个接口，有以下5个抽象方法\n1 Supplier\u0026lt;A\u0026gt; supplier() 创建一个结果容器A 1 BiConsumer\u0026lt;A, T\u0026gt; accumulator() 消费型接口，第一个参数为容器A，第二个参数为流中元素T。 1 BinaryOperator\u0026lt;A\u0026gt; combiner() 函数接口，该参数的作用跟上一个方法(reduce)中的combiner参数一样，将并行流中各个子进程的运行结果(accumulator函数操作后的容器A)进行合并。 1 Function\u0026lt;A, R\u0026gt; finisher() 函数式接口，参数为：容器A，返回类型为：collect方法最终想要的结果R。 1 Set\u0026lt;Characteristics\u0026gt; characteristics() 返回一个不可变的Set集合，用来表明该Collector的特征。有以下三个特征： CONCURRENT：表示此收集器支持并发。（官方文档还有其他描述，暂时没去探索，故不作过多翻译） UNORDERED：表示该收集操作不会保留流中元素原有的顺序。 IDENTITY_FINISH：表示finisher参数只是标识而已，可忽略。 6.7.3. Collector 工具库：Collectors Collectors 工具类实现了很多归约操作，例如将流转换成集合和聚合元素。Collectors可用于返回列表或字符串。\n具体常用的API参考以下各个操作的说明与示例\n6.7.4. 收集 Stream 流中的结果到集合中 1 2 3 4 5 public final class Collectors { public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, List\u0026lt;T\u0026gt;\u0026gt; toList() public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, Set\u0026lt;T\u0026gt;\u0026gt; toSet() public static \u0026lt;T, C extends Collection\u0026lt;T\u0026gt;\u0026gt; Collector\u0026lt;T, ?, C\u0026gt; toCollection(Supplier\u0026lt;C\u0026gt; collectionFactory) } 使用Collectors工具类中的转成Collection相关类型的方法。使用示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void streamToCollectionTest() { List\u0026lt;String\u0026gt; LIST = new ArrayList\u0026lt;\u0026gt;(); Collections.addAll(LIST, \u0026#34;天锁斩月\u0026#34;, \u0026#34;L\u0026#34;, \u0026#34;夜神月\u0026#34;, \u0026#34;樱木花道\u0026#34;, \u0026#34;宇智波鼬\u0026#34;, \u0026#34;金田一一\u0026#34;, \u0026#34;乌尔奇奥拉·西法\u0026#34;, \u0026#34;L\u0026#34;); // 将流中的数据收集到List集合 List\u0026lt;String\u0026gt; list = LIST.stream().collect(Collectors.toList()); System.out.println(\u0026#34;list: \u0026#34; + list); // 将流中的数据收集到Set集合 Set\u0026lt;String\u0026gt; set = LIST.stream().collect(Collectors.toSet()); System.out.println(\u0026#34;set: \u0026#34; + set); // 将流中的数据收集到指定的ArrayList类型中 ArrayList\u0026lt;String\u0026gt; arrayList = LIST.stream() .collect(Collectors.toCollection(ArrayList::new)); System.out.println(\u0026#34;arrayList: \u0026#34; + arrayList); // 将流中的数据收集到指定的ArrayList类型中 HashSet\u0026lt;String\u0026gt; hashSet = LIST.stream().collect(Collectors.toCollection(HashSet::new)); System.out.println(\u0026#34;hashSet: \u0026#34; + hashSet); } 6.7.5. 收集 Stream 流中的结果到数组中 1 2 3 4 public interface Stream\u0026lt;T\u0026gt; extends BaseStream\u0026lt;T, Stream\u0026lt;T\u0026gt;\u0026gt; { public Object[] toArray() public \u0026lt;T\u0026gt; T[] toArray(T[] a) } Stream接口提供 toArray 方法来将结果放到一个数组中，返回值类型是Object[]的数组。还能指定返回数组的类型T[]，示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void streamToArrayTest() { List\u0026lt;String\u0026gt; LIST = new ArrayList\u0026lt;\u0026gt;(); Collections.addAll(LIST, \u0026#34;天锁斩月\u0026#34;, \u0026#34;L\u0026#34;, \u0026#34;夜神月\u0026#34;, \u0026#34;樱木花道\u0026#34;, \u0026#34;宇智波鼬\u0026#34;, \u0026#34;金田一一\u0026#34;, \u0026#34;乌尔奇奥拉·西法\u0026#34;, \u0026#34;L\u0026#34;); // 将流中的数据收集到数组中，默认收到到Object类型的数组中 Object[] objects = LIST.stream().toArray(); for (Object object : objects) { System.out.println(\u0026#34;objectArray element: \u0026#34; + object); } // 将流中的数据收集到指定类型的数组中 String[] strArray = LIST.stream().toArray(String[]::new); for (String str : strArray) { System.out.println(\u0026#34;strArray element: \u0026#34; + str + \u0026#34;, string length: \u0026#34; + str.length()); } } 6.7.6. 对流中数据进行聚合、统计计算 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public final class Collectors { // 获取最大值 public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, Optional\u0026lt;T\u0026gt;\u0026gt; maxBy(Comparator\u0026lt;? super T\u0026gt; comparator) // 获取最小值 public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, Optional\u0026lt;T\u0026gt;\u0026gt; minBy(Comparator\u0026lt;? super T\u0026gt; comparator) // 求和 public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, Integer\u0026gt; summingInt(ToIntFunction\u0026lt;? super T\u0026gt; mapper) public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, Long\u0026gt; summingLong(ToLongFunction\u0026lt;? super T\u0026gt; mapper) public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, Double\u0026gt; summingDouble(ToDoubleFunction\u0026lt;? super T\u0026gt; mapper) // 获取平均值 public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, Double\u0026gt; averagingInt(ToIntFunction\u0026lt;? super T\u0026gt; mapper) public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, Double\u0026gt; averagingLong(ToLongFunction\u0026lt;? super T\u0026gt; mapper) public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, Double\u0026gt; averagingDouble(ToDoubleFunction\u0026lt;? super T\u0026gt; mapper) // 统计数量 public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, Long\u0026gt; counting() // 聚合统计操作，可以实现以上所有操作。它们主要用于int、double、long等基本类型上 public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, IntSummaryStatistics\u0026gt; summarizingInt(ToIntFunction\u0026lt;? super T\u0026gt; mapper) public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, LongSummaryStatistics\u0026gt; summarizingLong(ToLongFunction\u0026lt;? super T\u0026gt; mapper) public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, DoubleSummaryStatistics\u0026gt; summarizingDouble(ToDoubleFunction\u0026lt;? super T\u0026gt; mapper) } Collectors工具类提供相关用于Stream.collect()时的聚合处理的方法，当使用Stream流处理数据后，可以像数据库的聚合函数一样对某个字段进行操作。比如获取最大值，获取最小值，求总和，平均值，统计数量。示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 @Test public void streamToPolymerizationTest() { List\u0026lt;Student\u0026gt; STUDENTS = new ArrayList\u0026lt;\u0026gt;(); Collections.addAll(STUDENTS, new Student(\u0026#34;石原里美\u0026#34;, 23, 95), new Student(\u0026#34;樱庭奈奈美\u0026#34;, 18, 34), new Student(\u0026#34;长泽雅美\u0026#34;, 23, 45), new Student(\u0026#34;新垣结衣\u0026#34;, 18, 88) ); // 使用Collectors.maxBy()方法在收集流数据时获取最大值，获取学生分数的最大值 Optional\u0026lt;Student\u0026gt; max = STUDENTS.stream() .collect(Collectors.maxBy((o1, o2) -\u0026gt; o1.getSocre() - o2.getSocre())); System.out.println(\u0026#34;分数Socre最大值: \u0026#34; + max.orElseThrow(NullPointerException::new)); // 使用Collectors.minBy()方法在收集流数据时获取最小值，获取学生分数最小值 Optional\u0026lt;Student\u0026gt; min = STUDENTS.stream() .collect(Collectors.minBy((o1, o2) -\u0026gt; o1.getSocre() - o2.getSocre())); System.out.println(\u0026#34;分数Socre最小值: \u0026#34; + min.orElseThrow(NullPointerException::new)); // 使用Collectors.summingInt()方法在收集流数据时获取总和，获取学生年龄的总和 Integer sum = STUDENTS.stream() // .collect(Collectors.summingInt(o -\u0026gt; o.getAge())); .collect(Collectors.summingInt(Student::getAge)); // 使用方法引用优化 System.out.println(\u0026#34;年龄总和: \u0026#34; + sum); // 使用Collectors.averagingInt()方法在收集流数据时获取平均值，获取学生分数的平均值 Double avg = STUDENTS.stream() .collect(Collectors.averagingInt(Student::getSocre)); System.out.println(\u0026#34;分数平均值: \u0026#34; + avg); // 使用Collectors.counting()方法在收集流数据时获取数量，获取学生人数统计数量 Long count = STUDENTS.stream().collect(Collectors.counting()); System.out.println(\u0026#34;统计学生数量: \u0026#34; + count); // Collectors.summarizingInt()聚合操作汇总的方法，可以实现以上所有操作 IntSummaryStatistics statistics = STUDENTS.stream() .collect(Collectors.summarizingInt(Student::getSocre)); System.out.println( String.format(\u0026#34;max: %d, min: %d, sum: %d, avg: %s, count: %d\u0026#34;, statistics.getMax(), statistics.getMin(), statistics.getSum(), statistics.getAverage(), statistics.getCount() ) ); } 6.7.7. 对流中数据进行分组 1 2 3 4 5 6 7 8 9 10 public final class Collectors { public static \u0026lt;T, K\u0026gt; Collector\u0026lt;T, ?, Map\u0026lt;K, List\u0026lt;T\u0026gt;\u0026gt;\u0026gt; groupingBy(Function\u0026lt;? super T, ? extends K\u0026gt; classifier) public static \u0026lt;T, K, A, D\u0026gt; Collector\u0026lt;T, ?, Map\u0026lt;K, D\u0026gt;\u0026gt; groupingBy(Function\u0026lt;? super T, ? extends K\u0026gt; classifier, Collector\u0026lt;? super T, A, D\u0026gt; downstream) public static \u0026lt;T, K, D, A, M extends Map\u0026lt;K, D\u0026gt;\u0026gt; Collector\u0026lt;T, ?, M\u0026gt; groupingBy(Function\u0026lt;? super T, ? extends K\u0026gt; classifier, Supplier\u0026lt;M\u0026gt; mapFactory, Collector\u0026lt;? super T, A, D\u0026gt; downstream) } 6.7.7.1. 单个分组 Collectors工具类提供groupingBy()方法，用于在数据收集操作时根据某个属性将数据分组的方法，示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Test public void streamToGroupingByTest() { List\u0026lt;Student\u0026gt; STUDENTS = new ArrayList\u0026lt;\u0026gt;(); Collections.addAll(STUDENTS, new Student(\u0026#34;石原里美\u0026#34;, 23, 95), new Student(\u0026#34;樱庭奈奈美\u0026#34;, 18, 34), new Student(\u0026#34;长泽雅美\u0026#34;, 23, 45), new Student(\u0026#34;新垣结衣\u0026#34;, 18, 88) ); // 示例1：根据学生的年龄分组 Map\u0026lt;Integer, List\u0026lt;Student\u0026gt;\u0026gt; ageGroupMap = STUDENTS.stream() .collect(Collectors.groupingBy(Student::getAge)); /* * 在JDK8，Map提供一个新的API，用于Map数据的遍历 * default void forEach(BiConsumer\u0026lt;? super K, ? super V\u0026gt; action) */ ageGroupMap.forEach((k, v) -\u0026gt; System.out.println(k + \u0026#34;::\u0026#34; + v)); // 示例2：根据分数分组。将分数大于60的分为一组。小于60分成另一组 Map\u0026lt;String, List\u0026lt;Student\u0026gt;\u0026gt; socreGroupMap = STUDENTS.stream().collect(Collectors.groupingBy(s -\u0026gt; { if (s.getSocre() \u0026gt; 60) { return \u0026#34;及格\u0026#34;; } else { return \u0026#34;不及格\u0026#34;; } })); socreGroupMap.forEach((k, v) -\u0026gt; System.out.println(k + \u0026#34;::\u0026#34; + v)); } 6.7.7.2. 多级分组 Collectors工具类提供groupingBy()方法，还可以在收集数据时进行多级的分组，示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Test public void streamToMultiGroupingByTest() { List\u0026lt;Student\u0026gt; STUDENTS = new ArrayList\u0026lt;\u0026gt;(); Collections.addAll(STUDENTS, new Student(\u0026#34;石原里美\u0026#34;, 23, 95), new Student(\u0026#34;樱庭奈奈美\u0026#34;, 18, 34), new Student(\u0026#34;长泽雅美\u0026#34;, 23, 45), new Student(\u0026#34;新垣结衣\u0026#34;, 18, 88) ); /* * 示例1：根据学生的年龄分组后，再根据分数分组 * 使用到Collectors工具类的groupingBy重载方法 * public static \u0026lt;T, K, A, D\u0026gt; Collector\u0026lt;T, ?, Map\u0026lt;K, D\u0026gt;\u0026gt; groupingBy(Function\u0026lt;? super T, ? extends K\u0026gt; classifier, * Collector\u0026lt;? super T, A, D\u0026gt; downstream) */ Map\u0026lt;Integer, Map\u0026lt;String, List\u0026lt;Student\u0026gt;\u0026gt;\u0026gt; map = STUDENTS.stream() .collect(Collectors.groupingBy(Student::getAge, Collectors.groupingBy(s -\u0026gt; { if (s.getSocre() \u0026gt; 60) { return \u0026#34;及格\u0026#34;; } else { return \u0026#34;不及格\u0026#34;; } }))); // 遍历多级map map.forEach((k, v) -\u0026gt; { // 输出第一次分组的key System.out.println(k); // 遍历第二层map v.forEach((k2, v2) -\u0026gt; System.out.println(\u0026#34;\\t\u0026#34; + k2 + \u0026#34; == \u0026#34; + v2)); }); } 输出结果\n6.7.8. 对流中数据进行分区 1 2 3 4 5 public final class Collectors { public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, Map\u0026lt;Boolean, List\u0026lt;T\u0026gt;\u0026gt;\u0026gt; partitioningBy(Predicate\u0026lt;? super T\u0026gt; predicate) public static \u0026lt;T, D, A\u0026gt; Collector\u0026lt;T, ?, Map\u0026lt;Boolean, D\u0026gt;\u0026gt; partitioningBy(Predicate\u0026lt;? super T\u0026gt; predicate, Collector\u0026lt;? super T, A, D\u0026gt; downstream) } Collectors工具类提供partitioningBy()方法，用于在数据收集操作时根据指定的处理逻辑，返回值是否为true，把集合分割为两个列表，一个true列表，一个false列表。示例如下：\n注：分区与分组实质差不多，区别在于分组可以根据某个属性进去分组，结果可以将数据分成多个不同的组别；但分区只能分成两个区域，并且区域的key为true与false\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void partitioningByTest() { List\u0026lt;Student\u0026gt; STUDENTS = new ArrayList\u0026lt;\u0026gt;(); Collections.addAll(STUDENTS, new Student(\u0026#34;石原里美\u0026#34;, 23, 95), new Student(\u0026#34;樱庭奈奈美\u0026#34;, 18, 34), new Student(\u0026#34;长泽雅美\u0026#34;, 23, 45), new Student(\u0026#34;新垣结衣\u0026#34;, 18, 88) ); // 示例1：根据学生的分数进行分区 // partitioningBy会根据值是否为true，把集合分割为两个列表，一个true列表，一个false列表。 Map\u0026lt;Boolean, List\u0026lt;Student\u0026gt;\u0026gt; map = STUDENTS.stream() .collect(Collectors.partitioningBy(s -\u0026gt; s.getSocre() \u0026gt; 60)); // 遍历map map.forEach((k, v) -\u0026gt; System.out.println(k + \u0026#34; :: \u0026#34; + v)); } 6.7.9. 对流中数据进行拼接 1 2 3 4 5 6 7 8 9 10 public final class Collectors { // 直接将字符串元素拼接 public static Collector\u0026lt;CharSequence, ?, String\u0026gt; joining() // 指定连接符delimiter，将每个字符串元素与连接符拼接 public static Collector\u0026lt;CharSequence, ?, String\u0026gt; joining(CharSequence delimiter) // 指定连接符delimiter、前缀prefix、后缀suffix，将每个字符串元素与连接符拼接并加上前后缀 public static Collector\u0026lt;CharSequence, ?, String\u0026gt; joining(CharSequence delimiter, CharSequence prefix, CharSequence suffix) } Collectors工具类提供joining()方法，用于将所有元素拼接成一个字符串。可以指定拼接时的连接符与前、后缀。示例如下：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Test public void joiningTest() { // 示例1：将学生的姓名进去拼接 // 1. 直接将所有元素进行拼接 String names1 = STUDENTS.stream().map(Student::getName) .collect(Collectors.joining()); System.out.println(\u0026#34;字符直接拼接：\u0026#34; + names1); // 2. 指定连接符，将每个元素之间连接符拼接 String names2 = STUDENTS.stream().map(Student::getName) .collect(Collectors.joining(\u0026#34;_\u0026#34;)); System.out.println(\u0026#34;字符与连接符拼接：\u0026#34; + names2); // 3. 指定连接符、前缀、后缀，将每个元素之间连接符拼接再加上前后缀 String names3 = STUDENTS.stream().map(Student::getName) .collect(Collectors.joining(\u0026#34;_\u0026#34;, \u0026#34;[\u0026#34;, \u0026#34;]\u0026#34;)); System.out.println(\u0026#34;字符与连接符、前后缀拼接：\u0026#34; + names3); } 6.7.10. Collectors.toList() 源码解析（了解） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 // Collectors.toList() 源码 public static \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, List\u0026lt;T\u0026gt;\u0026gt; toList() { return new CollectorImpl\u0026lt;\u0026gt;((Supplier\u0026lt;List\u0026lt;T\u0026gt;\u0026gt;) ArrayList::new, List::add, (left, right) -\u0026gt; { left.addAll(right); return left; }, CH_ID); } // 转化一下源码中的lambda表达式，方便理解 public \u0026lt;T\u0026gt; Collector\u0026lt;T, ?, List\u0026lt;T\u0026gt;\u0026gt; toList() { Supplier\u0026lt;List\u0026lt;T\u0026gt;\u0026gt; supplier = () -\u0026gt; new ArrayList(); BiConsumer\u0026lt;List\u0026lt;T\u0026gt;, T\u0026gt; accumulator = (list, t) -\u0026gt; list.add(t); BinaryOperator\u0026lt;List\u0026lt;T\u0026gt;\u0026gt; combiner = (list1, list2) -\u0026gt; { list1.addAll(list2); return list1; }; Function\u0026lt;List\u0026lt;T\u0026gt;, List\u0026lt;T\u0026gt;\u0026gt; finisher = (list) -\u0026gt; list; Set\u0026lt;Collector.Characteristics\u0026gt; characteristics = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH)); return new Collector\u0026lt;T, List\u0026lt;T\u0026gt;, List\u0026lt;T\u0026gt;\u0026gt;() { @Override public Supplier supplier() { return supplier; } @Override public BiConsumer accumulator() { return accumulator; } @Override public BinaryOperator combiner() { return combiner; } @Override public Function finisher() { return finisher; } @Override public Set\u0026lt;Characteristics\u0026gt; characteristics() { return characteristics; } }; } 6.8. 并行的 Stream 流 6.8.1. 创建方式 直接获取并行的流。在 Java 8 中，所有的 Collection 集合，都可以使用接口的parallelStream()方法为集合创建并行流 将串行流转成并行流。创建串行的Stream流后，调用Stream接口的parallel()方法，将流转成并行流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void parallelStreamCreateTest() { // 定义集合 List\u0026lt;Integer\u0026gt; list = Arrays.asList(4, 5, 3, 9, 1, 2, 6); // 方式一：直接使用Collection接口的 parallelStream() 方法创建并行流 Stream\u0026lt;Integer\u0026gt; parallelStream1 = list.parallelStream(); // 调用Stream接口的isParallel()来判断当前是否为并行流 System.out.println(\u0026#34;parallelStream1是否为并行流: \u0026#34; + parallelStream1.isParallel()); // 方式二：将串行流转成并行流，调用Stream接口的 parallel() 方法，转成并行流 Stream\u0026lt;Integer\u0026gt; parallelStream2 = list.stream().parallel(); System.out.println(\u0026#34;parallelStream2是否为并行流: \u0026#34; + parallelStream2.isParallel()); } 6.8.2. 并行 Stream 流定义 以上学习使用的 Stream 流是串行的，就是在一个线程上执行。\n而parallelStream其实就是一个并行执行的流。它通过默认的ForkJoinPool，可能提高多线程任务的速度\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void serialAndParallelStreamTest() { // 创建串行流，输出执行时线程的名称 LIST.stream().filter(s -\u0026gt; { System.out.println(Thread.currentThread() + \u0026#34;::\u0026#34; + s); return s \u0026gt; 3; }).count(); System.out.println(\u0026#34;------------------------\u0026#34;); // 创建串行流，输出执行时线程的名称 LIST.parallelStream().filter(s -\u0026gt; { System.out.println(Thread.currentThread() + \u0026#34;::\u0026#34; + s); return s \u0026gt; 3; }).count(); } 6.8.3. 串行与并行流的效率对比 需求：使用for循环，串行Stream流，并行Stream流来对5亿个数字求和。看消耗的时间。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 // 定义循环的次数 private static final int times = 500000000; // 定义开始时间 private long startTime; @Before public void init() { startTime = System.currentTimeMillis(); } @After public void destory() { long endTime = System.currentTimeMillis(); System.out.println(\u0026#34;消耗时间: \u0026#34; + (endTime - startTime)); } /* for循环对5亿个数字求和的执行效率测试 消耗时间：166 */ @Test public void forEachEfficiencyTest() { int sum = 0; for (int i = 0; i \u0026lt; times; i++) { sum += i; } } /* 串行Stream流对5亿个数字求和的执行效率测试 消耗时间：390 */ @Test public void serialStreamTest() { // LongStream类的rangeClosed静态方法用于创建一个指定执行次数的流 LongStream.rangeClosed(0, times).reduce(0, Long::sum); } /* 并行Stream流对5亿个数字求和的执行效率测试 消耗时间：186 */ @Test public void parallelStreamTest() { // LongStream类的rangeClosed静态方法用于创建一个指定执行次数的流 LongStream.rangeClosed(0, times) .parallel() // 转成并行流 .reduce(0, Long::sum); } 示例正常来说，应该是parallelStream的执行效率是最高的（不知道为什么，本机测试居然比for循环还要慢）\nStream并行处理的过程会分而治之，也就是将一个大任务切分成多个小任务，这表示每个任务都是一个操作。\n6.8.4. parallelStream 线程安全问题 并行流parallelStream是多线程操作，所以就会出现线程安全的问题。线程安全问题示例：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 @Test public void threadSafetyTest() { // 定义待测试的集合 List\u0026lt;Integer\u0026gt; list = new ArrayList\u0026lt;\u0026gt;(); /* * 创建并行流，往集合插入数据，会出现线程安全问题 * 实现插入数据数量比指定的数次少 */ IntStream.rangeClosed(1, 1000) .parallel() // .forEach(i -\u0026gt; list.add(i)); .forEach(list::add); // 使用方法引用简化lambda表达式 System.out.println(\u0026#34;list = \u0026#34; + list.size()); /* 解决parallelStream线程安全问题方案一: 使用同步代码块 */ list.clear(); // 演示线程安全，重复使用集合，需要先清空 Object o = new Object(); // 定义锁对象 IntStream.rangeClosed(1, 1000) .parallel() .forEach(i -\u0026gt; { synchronized (o) { list.add(i); } }); System.out.println(\u0026#34;list = \u0026#34; + list.size()); /* 解决parallelStream线程安全问题方案二: 使用线程安全的集合 */ // 线程安全的集合1：Vector Vector\u0026lt;Integer\u0026gt; vector = new Vector(); IntStream.rangeClosed(1, 1000) .parallel() .forEach(vector::add); System.out.println(\u0026#34;vector = \u0026#34; + vector.size()); // 线程安全的集合2：Collections工具类提供的synchronizedList方法，将非线程安全的list转成线程安全 list.clear(); // 演示线程安全，重复使用集合，需要先清空 List\u0026lt;Integer\u0026gt; synchronizedList = Collections.synchronizedList(list); IntStream.rangeClosed(1, 1000) .parallel() .forEach(synchronizedList::add); System.out.println(\u0026#34;synchronizedList = \u0026#34; + synchronizedList.size()); /* 解决parallelStream线程安全问题方案三: 调用Stream流的collect/toArray */ List\u0026lt;Integer\u0026gt; collectList = IntStream.rangeClosed(1, 1000) .parallel() .boxed() // 转成stream流 .collect(Collectors.toList()); System.out.println(\u0026#34;collectList = \u0026#34; + collectList.size()); } // 输出结果 list = 985 list = 1000 vector = 1000 synchronizedList = 1000 collectList = 1000 总结parallelStream线程安全的解决方案有：\n在操作出现线程安全部分的代码时，使用同步代码块 使用线程安全的集合，如：Vector、Collections工具类提供的synchronizedList方法将集合转换成线程安全的 调用Stream流的collect、toArray去操作集合 6.8.5. parallelStream 底层实现原理 注：parallelStream底层实现是使用Fork/Join框架，此框架的详细学习笔记详见《并发编程》中的相关笔记\n6.8.5.1. Fork/Join 框架介绍 parallelStream使用的是Fork/Join框架。Fork/Join框架自JDK 7引入。Fork/Join框架可以将一个大任务拆分为很多小任务来异步执行。 Fork/Join框架主要包含三个模块：\n线程池：ForkJoinPool 任务对象：ForkJoinTask 执行任务的线程：ForkJoinWorkerThread 6.8.5.2. Fork/Join 原理-分治法 ForkJoinPool主要用来使用分治法(Divide-and-Conquer Algorithm)来解决问题。ForkJoinPool需要使用相对少的线程来处理大量的任务。比如要对1000万个数据进行排序，那么会将这个任务分割成两个500万的排序任务和一个针对这两组500万数据的合并任务。以此类推，对于500万的数据也会做出同样的分割处理，到最后会设置一个阈值来规定当数据规模到多少时，停止这样的分割处理。\n6.8.5.3. Fork/Join 原理-工作窃取算法 Fork/Join最核心的地方就是如何利用多核的处理器。工作窃取（work-stealing）算法就是整个Fork/Join框架的核心理念。Fork/Join工作窃取（work-stealing）算法是指某个线程从其他队列里窃取任务来执行\n假如我们需要做一个比较大的任务，我们可以把这个任务分割为若干互不依赖的子任务，为了减少线程间的竞争，于是把这些子任务分别放到不同的队列里，并为每个队列创建一个单独的线程来执行队列里的任务，线程和队列一一对应，比如A线程负责处理A队列里的任务。但是有的线程会先把自己队列里的任务干完，而其他线程对应的队列里还有任务等待处理。干完活的线程与其等着，不如去帮其他线程干活，于是它就去其他线程的队列里窃取一个任务来执行。而在这时它们会访问同一个队列，所以为了减少窃取任务线程和被窃取任务线程之间的竞争，通常会使用双端队列，被窃取任务线程永远从双端队列的头部拿任务执行，而窃取任务的线程永远从双端队列的尾部拿任务执行。\n工作窃取算法的优点是充分利用线程进行并行计算，并减少了线程间的竞争，其缺点是在某些情况下还是存在竞争。\nJava 8 引入了自动并行化的概念。它能够让一部分Java代码自动地以并行的方式执行，也就是使用了 ForkJoinPool的ParallelStream。\n对于 ForkJoinPool 通用线程池的线程数量，通常使用默认值就可以了，即运行时计算机的处理器数量。可以通过设置系统属性：java.util.concurrent.ForkJoinPool.common.parallelism=N （N为线程数量），来调整 ForkJoinPool 的线程数量，可以尝试调整成不同的参数来观察每次的输出结果。\n6.8.5.4. Fork/Join 案例 需求：使用Fork/Join计算1-10000的和，当一个任务的计算数量大于3000时拆分任务，数量小于3000时计算。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public class Demo07ForkJoin { @Test public void forkJoinTest() { long start = System.currentTimeMillis(); ForkJoinPool pool = new ForkJoinPool(); SumRecursiveTask task = new SumRecursiveTask(1, 99999999999L); Long result = pool.invoke(task); System.out.println(\u0026#34;result = \u0026#34; + result); long end = System.currentTimeMillis(); System.out.println(\u0026#34;消耗时间: \u0026#34; + (end - start)); } } /** 创建一个求和的任务 */ class SumRecursiveTask extends RecursiveTask\u0026lt;Long\u0026gt; { // 定义是否要拆分的临界值 private static final long THRESHOLD = 3000L; // 起始值 private final long start; // 结束值 private final long end; // 定义构造器，指定起始与结束值 public SumRecursiveTask(long start, long end) { this.start = start; this.end = end; } // 处理计算逻辑 @Override protected Long compute() { long length = end - start; if (length \u0026gt; THRESHOLD) { // 大于临界值，进行拆分 long middle = (start + end) / 2; SumRecursiveTask left = new SumRecursiveTask(start, middle); left.fork(); SumRecursiveTask right = new SumRecursiveTask(middle + 1, end); right.fork(); return left.join() + right.join(); } else { // 小于临界值，执行计算 long sum = 0; for (long i = start; i \u0026lt;= end; i++) { sum += i; } return sum; } } } 6.8.6. 总结 parallelStream 是线程不安全的 parallelStream 适用的场景 是CPU 密集型的，只是做到别浪费 CPU，假如本身电脑 CPU 的负载很大，那还到处用并行流，那并不能起到作用 磁盘 I/O、网络 I/O 都属于密集型 I/O 操作，这部分操作是较少消耗 CPU 资源，一般并行流中不适用于 I/O 密集型的操作，就比如使用并流行进行大批量的消息推送，涉及到了大量 I/O，使用并行流反而慢了很多 在使用并行流的时候是无法保证元素的顺序的，也就是即使你用了同步集合也只能保证元素都正确但无法保证其中的顺序 6.9. Stream 完整实例 6.9.1. 综合示例1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 package com.moon.test; import java.util.ArrayList; import java.util.Arrays; import java.util.IntSummaryStatistics; import java.util.List; import java.util.Random; import java.util.stream.Collectors; public class Java8Tester { public static void main(String args[]) { System.out.println(\u0026#34;使用 Java 7: \u0026#34;); // 计算空字符串 List\u0026lt;String\u0026gt; strings = Arrays.asList(\u0026#34;abc\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;bc\u0026#34;, \u0026#34;efg\u0026#34;, \u0026#34;abcd\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;jkl\u0026#34;); System.out.println(\u0026#34;列表: \u0026#34; + strings); long count = getCountEmptyStringUsingJava7(strings); System.out.println(\u0026#34;空字符数量为: \u0026#34; + count); count = getCountLength3UsingJava7(strings); System.out.println(\u0026#34;字符串长度为 3 的数量为: \u0026#34; + count); // 删除空字符串 List\u0026lt;String\u0026gt; filtered = deleteEmptyStringsUsingJava7(strings); System.out.println(\u0026#34;筛选后的列表: \u0026#34; + filtered); // 删除空字符串，并使用逗号把它们合并起来 String mergedString = getMergedStringUsingJava7(strings, \u0026#34;, \u0026#34;); System.out.println(\u0026#34;合并字符串: \u0026#34; + mergedString); List\u0026lt;Integer\u0026gt; numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5); // 获取列表元素平方数 List\u0026lt;Integer\u0026gt; squaresList = getSquares(numbers); System.out.println(\u0026#34;平方数列表: \u0026#34; + squaresList); List\u0026lt;Integer\u0026gt; integers = Arrays.asList(1, 2, 13, 4, 15, 6, 17, 8, 19); System.out.println(\u0026#34;列表: \u0026#34; + integers); System.out.println(\u0026#34;列表中最大的数 : \u0026#34; + getMax(integers)); System.out.println(\u0026#34;列表中最小的数 : \u0026#34; + getMin(integers)); System.out.println(\u0026#34;所有数之和 : \u0026#34; + getSum(integers)); System.out.println(\u0026#34;平均数 : \u0026#34; + getAverage(integers)); System.out.println(\u0026#34;随机数: \u0026#34;); // 输出10个随机数 Random random = new Random(); for (int i = 0; i \u0026lt; 10; i++) { System.out.println(random.nextInt()); } System.out.println(\u0026#34;使用 Java 8: \u0026#34;); System.out.println(\u0026#34;列表: \u0026#34; + strings); count = strings.stream().filter(string -\u0026gt; string.isEmpty()).count(); System.out.println(\u0026#34;空字符串数量为: \u0026#34; + count); count = strings.stream().filter(string -\u0026gt; string.length() == 3).count(); System.out.println(\u0026#34;字符串长度为 3 的数量为: \u0026#34; + count); filtered = strings.stream().filter(string -\u0026gt; !string.isEmpty()).collect(Collectors.toList()); System.out.println(\u0026#34;筛选后的列表: \u0026#34; + filtered); mergedString = strings.stream().filter(string -\u0026gt; !string.isEmpty()).collect(Collectors.joining(\u0026#34;, \u0026#34;)); System.out.println(\u0026#34;合并字符串: \u0026#34; + mergedString); squaresList = numbers.stream().map(i -\u0026gt; i * i).distinct().collect(Collectors.toList()); System.out.println(\u0026#34;Squares List: \u0026#34; + squaresList); System.out.println(\u0026#34;列表: \u0026#34; + integers); IntSummaryStatistics stats = integers.stream().mapToInt((x) -\u0026gt; x).summaryStatistics(); System.out.println(\u0026#34;列表中最大的数 : \u0026#34; + stats.getMax()); System.out.println(\u0026#34;列表中最小的数 : \u0026#34; + stats.getMin()); System.out.println(\u0026#34;所有数之和 : \u0026#34; + stats.getSum()); System.out.println(\u0026#34;平均数 : \u0026#34; + stats.getAverage()); System.out.println(\u0026#34;随机数: \u0026#34;); random.ints().limit(10).sorted().forEach(System.out::println); // 并行处理 count = strings.parallelStream().filter(string -\u0026gt; string.isEmpty()).count(); System.out.println(\u0026#34;空字符串的数量为: \u0026#34; + count); } private static int getCountEmptyStringUsingJava7(List\u0026lt;String\u0026gt; strings) { int count = 0; for (String string : strings) { if (string.isEmpty()) { count++; } } return count; } private static int getCountLength3UsingJava7(List\u0026lt;String\u0026gt; strings) { int count = 0; for (String string : strings) { if (string.length() == 3) { count++; } } return count; } private static List\u0026lt;String\u0026gt; deleteEmptyStringsUsingJava7(List\u0026lt;String\u0026gt; strings) { List\u0026lt;String\u0026gt; filteredList = new ArrayList\u0026lt;String\u0026gt;(); for (String string : strings) { if (!string.isEmpty()) { filteredList.add(string); } } return filteredList; } private static String getMergedStringUsingJava7(List\u0026lt;String\u0026gt; strings, String separator) { StringBuilder stringBuilder = new StringBuilder(); for (String string : strings) { if (!string.isEmpty()) { stringBuilder.append(string); stringBuilder.append(separator); } } String mergedString = stringBuilder.toString(); return mergedString.substring(0, mergedString.length() - 2); } private static List\u0026lt;Integer\u0026gt; getSquares(List\u0026lt;Integer\u0026gt; numbers) { List\u0026lt;Integer\u0026gt; squaresList = new ArrayList\u0026lt;Integer\u0026gt;(); for (Integer number : numbers) { Integer square = new Integer(number.intValue() * number.intValue()); if (!squaresList.contains(square)) { squaresList.add(square); } } return squaresList; } private static int getMax(List\u0026lt;Integer\u0026gt; numbers) { int max = numbers.get(0); for (int i = 1; i \u0026lt; numbers.size(); i++) { Integer number = numbers.get(i); if (number.intValue() \u0026gt; max) { max = number.intValue(); } } return max; } private static int getMin(List\u0026lt;Integer\u0026gt; numbers) { int min = numbers.get(0); for (int i = 1; i \u0026lt; numbers.size(); i++) { Integer number = numbers.get(i); if (number.intValue() \u0026lt; min) { min = number.intValue(); } } return min; } private static int getSum(List numbers) { int sum = (int) (numbers.get(0)); for (int i = 1; i \u0026lt; numbers.size(); i++) { sum += (int) numbers.get(i); } return sum; } private static int getAverage(List\u0026lt;Integer\u0026gt; numbers) { return getSum(numbers) / numbers.size(); } } 程序输出结果：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 使用 Java 7: 列表: [abc, , bc, efg, abcd, , jkl] 空字符数量为: 2 字符串长度为 3 的数量为: 3 筛选后的列表: [abc, bc, efg, abcd, jkl] 合并字符串: abc, bc, efg, abcd, jkl 平方数列表: [9, 4, 49, 25] 列表: [1, 2, 13, 4, 15, 6, 17, 8, 19] 列表中最大的数 : 19 列表中最小的数 : 1 所有数之和 : 85 平均数 : 9 随机数: 1940609383 1273448576 -1208033961 -880625237 -655596583 -65568491 -510907885 384715598 1145477696 447794939 使用 Java 8: 列表: [abc, , bc, efg, abcd, , jkl] 空字符串数量为: 2 字符串长度为 3 的数量为: 3 筛选后的列表: [abc, bc, efg, abcd, jkl] 合并字符串: abc, bc, efg, abcd, jkl Squares List: [9, 4, 49, 25] 列表: [1, 2, 13, 4, 15, 6, 17, 8, 19] 列表中最大的数 : 19 列表中最小的数 : 1 所有数之和 : 85 平均数 : 9.444444444444445 随机数: -1861468930 -1661597688 -154610776 133616016 776120450 896964313 916562908 1237476550 2074868773 2091065305 空字符串的数量为: 2 6.9.2. 综合示例2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Test public void comprehensiveStreamTest02() { // 第一个队伍 List\u0026lt;String\u0026gt; teamA = Arrays.asList(\u0026#34;天锁斩月\u0026#34;, \u0026#34;L\u0026#34;, \u0026#34;夜神月\u0026#34;, \u0026#34;樱木花道\u0026#34;, \u0026#34;宇智波鼬\u0026#34;, \u0026#34;金田一一\u0026#34;, \u0026#34;乌尔奇奥拉·西法\u0026#34;); // 第二个队伍 List\u0026lt;String\u0026gt; teamB = Arrays.asList(\u0026#34;樱庭奈奈美\u0026#34;, \u0026#34;长泽雅美\u0026#34;, \u0026#34;新垣结衣\u0026#34;, \u0026#34;石原里美\u0026#34;, \u0026#34;郭靖\u0026#34;, \u0026#34;杨过\u0026#34;, \u0026#34;张无忌\u0026#34;); // 1.第一个队伍只要名字为4个字的成员姓名; // 2.第一个队伍筛选之后只要前3个人; Stream\u0026lt;String\u0026gt; streamA = teamA.stream().filter(s -\u0026gt; s.length() == 4).limit(3); // 3.第二个队伍只要包含“美”字的成员姓名; // 4.第二个队伍筛选之后不要前2个人; Stream\u0026lt;String\u0026gt; streamB = teamB.stream().filter(s -\u0026gt; s.contains(\u0026#34;美\u0026#34;)).skip(2); // 5.将两个队伍合并为一个队伍; Stream\u0026lt;String\u0026gt; concatStream = Stream.concat(streamA, streamB); // 6.根据姓名创建Person象; Stream\u0026lt;Person\u0026gt; personStream = concatStream.map(Person::new); // 7.打印整个队伍的Person对象信息。 personStream.forEach(System.out::println); } 6.10. Stream 流最佳使用技巧 使用专属类型流以获得更好的性能。例如在使用 int、long 和 double 等基本类型数据时，使用 IntStream、LongStream 和 DoubleStream 等基本流，而不是 Integer、Long 和 Double 等装箱类型流。原始流可以通过避免装箱和拆箱的成本来提供更好的性能。 1 2 3 4 5 6 int[] array = new int[]{1, 2, 3, 4, 5}; // not good int sum = Arrays.stream(array).sum(); // best IntStream intStream = IntStream.of(array); int sum1 = intStream.sum(); 避免嵌套流。因为它可能导致代码难以阅读和理解。相反可以尝试将问题分解为更小的部分，并使用中间集合或局部变量来存储中间结果。 1 2 3 4 5 List\u0026lt;String\u0026gt; list1 = Arrays.asList(\u0026#34;apple\u0026#34;, \u0026#34;banana\u0026#34;, \u0026#34;cherry\u0026#34;); List\u0026lt;String\u0026gt; list2 = Arrays.asList(\u0026#34;orange\u0026#34;, \u0026#34;pineapple\u0026#34;, \u0026#34;mango\u0026#34;); List\u0026lt;String\u0026gt; collect = Stream.concat(list1.stream(), list2.stream()) .filter(s -\u0026gt; s.length() \u0026gt; 5) .collect(Collectors.toList()); 虽然并行流可以在处理大量数据时提供更好的性能，但它们也会引入开销和竞争条件。谨慎使用并行流，并考虑数据大小、操作复杂性和可用处理器数量等因素。 1 2 List\u0026lt;Integer\u0026gt; list = Arrays.asList(1, 2, 3, 4, 5); Integer sum = list.parallelStream().reduce(0, Integer::sum); Stream API 支持延迟计算，这意味着在调用终端操作之前不会执行中间操作。因此可以尝试使用惰性计算来通过减少不必要的计算来提高性能。 1 2 List\u0026lt;Integer\u0026gt; list = Arrays.asList(1, 2, 3, 4, 5); Optional\u0026lt;Integer\u0026gt; result = list.stream().filter(n -\u0026gt; n \u0026gt; 3).findFirst(); Stream API 旨在对数据执行功能操作，需要避免引入副作用。例如修改流外部的变量或执行 I/O 操作，因为这可能会导致不可预测的行为并降低代码可读性。 1 2 3 List\u0026lt;String\u0026gt; list = Arrays.asList(\u0026#34;apple\u0026#34;, \u0026#34;banana\u0026#34;, \u0026#34;cherry\u0026#34;); int count = 0; list.stream().filter(s -\u0026gt; s.startsWith(\u0026#34;a\u0026#34;)).forEach(s -\u0026gt; count++); 将流与不可变对象一起使用。Stream API 最适合使用不可变对象，可确保流的状态在处理过程中不会被修改，这可以带来更可预测的行为和更好的代码可读性。 1 2 3 4 List\u0026lt;String\u0026gt; list = Arrays.asList(\u0026#34;apple\u0026#34;, \u0026#34;banana\u0026#34;, \u0026#34;cherry\u0026#34;); List\u0026lt;String\u0026gt; result = list.stream() .map(String::toUpperCase) .collect(Collectors.toList()); 如果流可能包含大量不符合条件的元素，在 map() 之前使用 filter() 以避免不必要的处理，可以提高代码的性能。 1 2 3 4 5 List\u0026lt;Integer\u0026gt; list = Arrays.asList(1, 2, 3, 4, 5); List\u0026lt;Integer\u0026gt; filteredList = list.stream() .filter(i -\u0026gt; i % 2 == 0) .map(i -\u0026gt; i * 2) .collect(Collectors.toList()); 与使用 lambda 表达式相比，方法引用可以使代码更加简洁和可读。在合适的情况下，优先使用方法引用代替 lambda 表达式。 1 2 List\u0026lt;Integer\u0026gt; list = Arrays.asList(1, 2, 3, 4, 5); Integer sum = list.stream().reduce(0, Integer::sum); 如果流可能包含重复元素，优先使用 distinct() 操作来删除它们 1 2 List\u0026lt;Integer\u0026gt; list = Arrays.asList(1, 2, 3, 3, 4, 5, 5); List\u0026lt;Integer\u0026gt; distinctList = list.stream().distinct().collect(Collectors.toList()); 流的 sorted() 操作成本可能会很昂贵，尤其是对于大型流。仅在必要时谨慎使用 sorted()。如果已确定输入的数据已经排序，则可以跳过此操作。 1 2 List\u0026lt;Integer\u0026gt; list = Arrays.asList(3, 2, 1); List\u0026lt;Integer\u0026gt; SortedList = list.stream().sorted().collect(Collectors.toList()); 7. StringJoiner 类（字符拼接） 7.1. 简介 StringJoiner 是 java.util 包中的一个类，用于构造一个由分隔符分隔的字符序列（可选），并且可以从提供的前缀开始并以提供的后缀结尾\nStringJoiner 类共有2个构造函数，5个公有方法。其中最常用的方法就是 add 方法和 toString 方法，类似于 StringBuilder 中的 append 方法和 toString 方法\n7.2. 构造方法 1 public StringJoiner(CharSequence delimiter) 构造一个由分隔符分隔的字符序列，注意：delimiter其实是分隔符，并不是可变字符串的初始值 1 public StringJoiner(CharSequence delimiter, CharSequence prefix, CharSequence suffix) 构造一个由分隔符分隔的字符序列，prefix参数设置字符拼接的前缀，参数suffix设置后缀 7.3. 基础用法 7.3.1. StringJoiner 对象 手动创建 StringJoiner 对象，实现字符串拼接\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class StringJoinerTest { public static void main(String[] args) { StringJoiner sj = new StringJoiner(\u0026#34;Moon\u0026#34;); sj.add(\u0026#34;Zero\u0026#34;); sj.add(\u0026#34;kirA\u0026#34;); /* 输出结果 */ System.out.println(sj.toString()); // ZeroMoonkirA StringJoiner sj1 = new StringJoiner(\u0026#34;,\u0026#34;, \u0026#34;[\u0026#34;, \u0026#34;]\u0026#34;); sj1.add(\u0026#34;Moon\u0026#34;).add(\u0026#34;Zero\u0026#34;).add(\u0026#34;kirA\u0026#34;); /* 输出结果 */ System.out.println(sj1.toString()); // [Moon,Zero,kirA] } } 7.3.2. Stream 流的 joining 方法 在 Java8 的流操作中的 Collector.joining，底层也使用了 StringJoiner 进行字符串拼接了，基础用法如下：\n1 list.stream().collect(Collectors.joining(\u0026#34;:\u0026#34;)) 7.3.3. String 类的 join 静态方法 JDK 1.8 后，String 类新增两个 join 静态方法，底层也使用了 StringJoiner 进行字符串拼接了\n1 2 3 public static String join(CharSequence delimiter, CharSequence... elements) public static String join(CharSequence delimiter, Iterable\u0026lt;? extends CharSequence\u0026gt; elements) 基础用法：\n1 2 List\u0026lt;String\u0026gt; list = Arrays.asList(\u0026#34;a\u0026#34;, \u0026#34;b\u0026#34;, \u0026#34;c\u0026#34;, \u0026#34;d\u0026#34;); System.out.println(String.join(\u0026#34;,\u0026#34;, list)); // a,b,c,d 7.4. 实现原理 7.4.1. StringJoiner 通过查询源码，StringJoiner 其实是通过 StringBuilder 实现\n1 2 3 4 5 6 7 8 9 10 11 12 13 public StringJoiner add(CharSequence newElement) { prepareBuilder().append(newElement); return this; } private StringBuilder prepareBuilder() { if (value != null) { value.append(delimiter); } else { value = new StringBuilder().append(prefix); } return value; } 7.4.2. Collector.joining 还有 Java8 的流操作中的 Collector.joining 的实现原理也是使用了 StringJoiner，Collector.joining 的源代码如下：\n1 2 3 4 5 6 public static Collector\u0026lt;CharSequence, ?, String\u0026gt; joining(CharSequence delimiter,CharSequence prefix,CharSequence suffix) { return new CollectorImpl\u0026lt;\u0026gt;( () -\u0026gt; new StringJoiner(delimiter, prefix, suffix), StringJoiner::add, StringJoiner::merge, StringJoiner::toString, CH_NOID); } 7.4.3. String.join String.join 静态方法，底层也是手动创建 StringJoiner，通过 add 添加拼接的元素，最终调用 toString 方法完成字符串拼接\n1 2 3 4 5 6 7 8 9 10 public static String join(CharSequence delimiter, CharSequence... elements) { Objects.requireNonNull(delimiter); Objects.requireNonNull(elements); // Number of elements not likely worth Arrays.stream overhead. StringJoiner joiner = new StringJoiner(delimiter); for (CharSequence cs: elements) { joiner.add(cs); } return joiner.toString(); } 7.5. 使用场景总结 StringJoiner 其实是通过 StringBuilder 实现的，所以两者性能差不多，都是非线程安全的\n如果只是简单的字符串拼接，考虑直接使用\u0026quot;+\u0026quot;即可。 如果是在 for 循环中进行字符串拼接，考虑使用 StringBuilder 和 StringBuffer。 如果是通过一个集合（如List）进行字符串拼接，则考虑使用 StringJoiner。 如果是对一组数据进行拼接，则可以考虑将其转换成 Stream，并使用 StringJoiner 处理。 8. Optional 类 Optional 类是一个可以为null的容器对象。如果值存在则isPresent()方法会返回true，调用get()方法会返回该对象。 Optional 是个容器：它可以保存类型T的值，或者仅仅保存null。Optional提供很多有用的方法，这样我们就不用显式进行空值检测。 Optional 类的引入很好的解决空指针异常。 拓展：google的Guava Optional与Optional有同样的功能。不过需要注意的是，Guava Optional API 与 JDK 存在差异\n8.1. 类声明 java.util.Optional\u0026lt;T\u0026gt;类的声明：public final class Optional\u0026lt;T\u0026gt; extends Object\n8.2. Optional 类型说明 这也是一个模仿 Scala 语言中的概念，作为一个容器，它可能含有某值，或者不包含。使用它的目的是尽可能避免 NullPointerException Optional里面只持有一个元素，而Stream可持有多个元素 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 String strA = \u0026#34; abcd \u0026#34;, strB = null; print(strA); print(\u0026#34;\u0026#34;); print(strB); getLength(strA); getLength(\u0026#34;\u0026#34;); getLength(strB); public static void print(String text) { // Java 8 Optional.ofNullable(text).ifPresent(System.out::println); // Pre-Java 8 if (text != null) { System.out.println(text); } } public static int getLength(String text) { // Java 8 return Optional.ofNullable(text).map(String::length).orElse(-1); // Pre-Java 8 // return if (text != null) ? text.length() : -1; }; // 还有ifPresentOrElse()方法等 在更复杂的 if (xx != null) 的情况中，使用 Optional 代码的可读性更好，而且它提供的是编译时检查，能极大的降低 NPE 这种 Runtime Exception 对程序的影响，或者迫使程序员更早的在编码阶段处理空值问题，而不是留到运行时再发现和调试。\nStream 中的 findAny、max/min、reduce 等方法等返回 Optional 值。还有例如 IntStream.average() 返回 OptionalDouble 等等\n8.3. Optional 创建方式 Optional是一个没有子类的工具类。Optional本质是一个容器，需要将对象实例传入该容器中。Optional 的构造方法为 private，无法直接使用 new 构建对象，只能使用 Optional 提供的静态方法创建。Optional 三个创建方法如下：\n1 public static \u0026lt;T\u0026gt; Optional\u0026lt;T\u0026gt; of(T value) Optional.of(obj)方法，创建一个有值的Optional实例。如果方法传入的对象为null，将会抛出 NPE 异常。 1 public static \u0026lt;T\u0026gt; Optional\u0026lt;T\u0026gt; ofNullable(T value) Optional.ofNullable(obj)方法，创建一个可以为空的Optional实例。如果对象为null，将会创建不包含值的 empty Optional 对象实例。 1 public static\u0026lt;T\u0026gt; Optional\u0026lt;T\u0026gt; empty() Optional.empty()方法，创建一个空的Optional实例。等同于 Optional.ofNullable(null) 注：只有在确定对象不会为 null 的情况使用 Optional.of()，否则建议使用 Optional.ofNullable() 方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Test public void optionalCreateTest() { /* 由于Optional的构造方法为private修饰，无法直接使用new构建对象，只能使用Optional提供的静态方法创建 */ /* * 方式一：public static \u0026lt;T\u0026gt; Optional\u0026lt;T\u0026gt; of(T value) * 该方法创建一个有值的Optional实例。如果方法传入的对象为null，将会抛出 NPE 异常 */ Optional\u0026lt;String\u0026gt; op1 = Optional.of(\u0026#34;天锁斩月\u0026#34;); // Optional\u0026lt;String\u0026gt; op1 = Optional.of(null); // 异常 System.out.println(\u0026#34;op1: \u0026#34; + op1.get()); /* * 方式二：public static \u0026lt;T\u0026gt; Optional\u0026lt;T\u0026gt; ofNullable(T value) * 该方法创建一个可以为空的Optional实例。如果对象为null，将会创建不包含值的empty Optional对象实例 */ Optional\u0026lt;String\u0026gt; op2 = Optional.ofNullable(\u0026#34;石原里美\u0026#34;); // Optional\u0026lt;String\u0026gt; op2 = Optional.ofNullable(null); // 创建Optional实例时不会报错 // 如果是空的Optional对象实例，直接调用get()方法会抛出 NoSuchElementException 异常 System.out.println(\u0026#34;op2: \u0026#34; + op2.get()); /* * 方式三：public static\u0026lt;T\u0026gt; Optional\u0026lt;T\u0026gt; empty() * 该方法创创建一个空的Optional实例。等同于 Optional.ofNullable(null) */ Optional\u0026lt;String\u0026gt; op3 = Optional.empty(); // 如果是空的Optional对象实例，直接调用get()方法会抛出 NoSuchElementException 异常 // System.out.println(\u0026#34;op3: \u0026#34; + op3.get()); System.out.println(\u0026#34;op3是否有值: \u0026#34; + op3.isPresent()); } 8.4. 类的常用方法 8.4.1. get() 与 isPresent() 1 2 3 4 5 6 public T get() { if (value == null) { throw new NoSuchElementException(\u0026#34;No value present\u0026#34;); } return value; } 获取Optional容器的值。如果在这个Optional中包含这个值，返回值，否则抛出异常：NoSuchElementException 1 2 3 public boolean isPresent() { return value != null; } 判断是否包含值。如果值存在则方法会返回true，否则返回false。 通常使用方式：对象实例存入 Optional 容器中之后，最后需要从中取出。Optional.get() 方法用于取出内部对象实例，不过需要注意的是，如果是 empty Optional 实例，由于容器内没有任何对象实例，使用 get() 方法将会抛出 NoSuchElementException 异常。\n为了防止异常抛出，可以使用 Optional.isPresent()。这个方法将会判断内部是否存在对象实例，若存在则返回 true\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void getAndIsPresentTest() { // 创建测试的Optional容器 Optional\u0026lt;String\u0026gt; op = Optional.ofNullable(\u0026#34;石原里美\u0026#34;); // Optional\u0026lt;String\u0026gt; op = Optional.empty(); // isPresent()方法：用于判断Optional容器中是否有值，有值返回true，没有值返回false if (op.isPresent()) { // get()方法：用于获取Optional容器中的值，如果有值则返回具体值，没有值就报错 System.out.println(\u0026#34;op的值: \u0026#34; + op.get()); } else { System.out.println(\u0026#34;op没有值\u0026#34;); } } 8.4.2. orElse()、orElseGet() 与 orElseThrow() 1 2 3 public T orElse(T other) { return value != null ? value : other; } orElse方法：如果容器存在该值，返回值，否则返回形参传入的other值 1 2 3 public T orElseGet(Supplier\u0026lt;? extends T\u0026gt; other) { return value != null ? value : other.get(); } orElseGet方法：如果容器存在该值，返回值，否则返回形参的函数式接口Supplier返回的值 1 2 3 4 5 6 7 public \u0026lt;X extends Throwable\u0026gt; T orElseThrow(Supplier\u0026lt;? extends X\u0026gt; exceptionSupplier) throws X { if (value != null) { return value; } else { throw exceptionSupplier.get(); } } orElseThrow方法：如果容器存在该值，返回包含的值，否则抛出由 Supplier 继承的异常 示例：当一个对象为 null 时，业务上通常可以设置一个默认值，从而使流程继续下去。或者抛出一个内部异常，记录失败原因，快速失败\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 private final Optional\u0026lt;String\u0026gt; op = Optional.ofNullable(\u0026#34;石原里美\u0026#34;); // private final Optional\u0026lt;String\u0026gt; op = Optional.empty(); /* Optional类的orElse()方法 */ @Test public void orElseTest() { // orElse()方法：如果Optional容器中有值，就返回该值；如果没有值就返回参数指定的值 String name = op.orElse(\u0026#34;新垣结衣?\u0026#34;); System.out.println(\u0026#34;name = \u0026#34; + name); } /* Optional类的orElseGet()方法 */ @Test public void orElseGetTest() { // orElseGet()方法：如果Optional容器中有值，就返回该值；如果没有值就返回参数的Supplier接口提供的值 String name = op.orElseGet(() -\u0026gt; \u0026#34;长泽雅美\u0026#34;); System.out.println(\u0026#34;name = \u0026#34; + name); } /* Optional类的orElseThrow()方法 */ @Test public void orElseThrowTest() { // orElseThrow()方法：如果Optional容器中有值，就返回该值；如果没有值就抛出由Supplier继承的异常 String name = op.orElseThrow(NullPointerException::new); System.out.println(\u0026#34;name = \u0026#34; + name); } 8.4.3. ifPresent() 1 2 3 4 public void ifPresent(Consumer\u0026lt;? super T\u0026gt; consumer) { if (value != null) consumer.accept(value); } ifPresent方法：如果值存在则使用该值调用 Consumer 接口进行消费处理，否则不做任何事情。即使用 ifPresent 方法，不用再进行非空检查 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // private final Optional\u0026lt;String\u0026gt; op = Optional.ofNullable(\u0026#34;石原里美\u0026#34;); private final Optional\u0026lt;String\u0026gt; op = Optional.empty(); @Test public void ifPresentTest() { // ifPresent()方法：如果Optional容器中有值，使用该值调用Consumer接口进行消费处理，否则不做任何事情 op.ifPresent(s -\u0026gt; System.out.println(\u0026#34;name = \u0026#34; + s)); /* * 番外：JDK9 对Optional类做了增强，增加了ifPresentOrElse方法 * 该方法可以定义容器分别是否为空时的相应处理逻辑 * 参数1：当前容器有值时，执行此消费方法逻辑 * 参数2：当前容器为空时，执行此方法逻辑 */ /*op.ifPresentOrElse(s -\u0026gt; { System.out.println(\u0026#34;有值: \u0026#34; + s); }, () -\u0026gt; { System.out.println(\u0026#34;没有值\u0026#34;); });*/ } 8.4.4. map() 与 flatMap() 1 2 3 4 5 6 7 8 public\u0026lt;U\u0026gt; Optional\u0026lt;U\u0026gt; map(Function\u0026lt;? super T, ? extends U\u0026gt; mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Optional.ofNullable(mapper.apply(value)); } } map方法：如果存在该值，执行参数提供的映射方法Function，如果映射方法返回非null值，则map最终会返回一个包装了返回值的Optional实例。如果映射方法处理返回null时，则返回空的Optional实例 1 2 3 4 5 6 7 8 public\u0026lt;U\u0026gt; Optional\u0026lt;U\u0026gt; flatMap(Function\u0026lt;? super T, Optional\u0026lt;U\u0026gt;\u0026gt; mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Objects.requireNonNull(mapper.apply(value)); } } flatMap方法：如果值存在，返回基于Optional包含的映射方法的值，否则返回一个空的Optional 以上两个方法与 Java8 Stream 的相似，Stream.map()方法可以将当前对象转化为另外一个对象， Optional.map() 方法也与之类似\n示例：map 方法可以将原先 Optional\u0026lt;User\u0026gt; 转变成 Optional\u0026lt;String\u0026gt; ，此时 Optional 内部对象变成 String 类型。如果转化之前 Optional 对象为空，则什么也不会发生\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void mapTest() { // 示例：将用户对象的用户名转成大写并返回 // User user = null; // User user = new User(null, 18); User user = new User(\u0026#34;mooN\u0026#34;, 18); // map()方法：如果Optional容器中有值，则执行参数的Function接口实现逻辑；如没有值则返回空的Optional对象实例 /*String userName = Optional.ofNullable(user) .map(u -\u0026gt; u.getUserName()) .map(s -\u0026gt; s.toUpperCase()) .orElse(\u0026#34;null\u0026#34;);*/ /* 使用方法引用简化上面的代码 */ String userName = Optional.ofNullable(user) // 创建User对象的Optional容器 .map(User::getUserName) // 如果User实例不为空，则调用gteUserName方法获取用户名称，否则返回空Optional实例 .map(String::toUpperCase) // 如果userName不为空，则调用toUpperCase方法转成大写，否则返回空Optional实例 .orElse(\u0026#34;null\u0026#34;); // 如果上述其中一步返回空的Optional实例，则会执行 orElse 返回默认的值 System.out.println(\u0026#34;用户名转成大写后值为：\u0026#34; + userName); } 8.4.5. filter() 1 2 3 4 5 6 7 public Optional\u0026lt;T\u0026gt; filter(Predicate\u0026lt;? super T\u0026gt; predicate) { Objects.requireNonNull(predicate); if (!isPresent()) return this; else return predicate.test(value) ? this : empty(); } filter方法：如果值存在，并且这个值匹配给定的 predicate，返回一个包含此值的Optional实例，否则返回一个空的Optional实例 示例：当某些属性满足一定条件，才进行下一步动作\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void filterTest() { // 示例：如果用户对象的用户名称不为空且长度大于3，则输出结果 // User user = null; // User user = new User(null, 18); User user = new User(\u0026#34;mooN\u0026#34;, 18); // filter()方法：如果Optional容器中有值，并且这个值匹配参数的Predicate接口实现逻辑；返回一个包含此值的Optional实例，否则返回一个空的Optional实例 Optional.ofNullable(user) // 创建User对象的Optional容器 .filter(u -\u0026gt; { String userName = u.getUserName(); return userName != null \u0026amp;\u0026amp; userName.length() \u0026gt; 3; }) // 如果user对象不为空，则将user对象去匹配参数的Predicate接口实现逻辑，如果条件成功则返回user对象的Optional实例，否则返回空Optional实例 .ifPresent(u -\u0026gt; System.out.println(\u0026#34;用户名: \u0026#34; + u.getUserName())); // 如果上述其中一步返回空的Optional实例，则会执行则 } filter 方法将会判断对象是否符合条件。如果不符合条件，将会返回一个空的 Optional\n8.4.6. 其他常用方法 1 boolean equals(Object obj) 判断其他对象是否等于 Optional。 1 int hashCode() 返回存在值的哈希码，如果值不存在返回 0。 1 String toString() 返回一个Optional的非空字符串，用来调试 8.5. Optional 使用实例 8.5.1. 示例1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package com.moon.jav.test; import java.util.Optional; public class Java8Tester { public static void main(String args[]) { Java8Tester java8Tester = new Java8Tester(); Integer value1 = null; Integer value2 = new Integer(10); // Optional.ofNullable - 允许传递为 null 参数 Optional\u0026lt;Integer\u0026gt; a = Optional.ofNullable(value1); // Optional.of - 如果传递的参数是 null，抛出异常 NullPointerException Optional\u0026lt;Integer\u0026gt; b = Optional.of(value2); System.out.println(java8Tester.sum(a, b)); } public Integer sum(Optional\u0026lt;Integer\u0026gt; a, Optional\u0026lt;Integer\u0026gt; b) { // Optional.isPresent - 判断值是否存在 System.out.println(\u0026#34;第一个参数值存在: \u0026#34; + a.isPresent()); System.out.println(\u0026#34;第二个参数值存在: \u0026#34; + b.isPresent()); // Optional.orElse - 如果值存在，返回它，否则返回默认值 Integer value1 = a.orElse(new Integer(0)); //Optional.get - 获取值，值需要存在 Integer value2 = b.get(); return value1 + value2; } } 程序输出结果：\n1 2 3 第一个参数值存在: false 第二个参数值存在: true 10 8.5.2. 示例2 未使用Optional之前代码 1 2 3 4 5 6 7 8 9 10 if (staff != null) { Department department = staff.getDepartment(); if (department != null) { Company company = department.getCompany(); if (company != null) { return company.getName(); } } } return \u0026#34;Unknown\u0026#34;; 使用Optional重构，将 Staff，Department 修改 getter 方法返回结果类型改成 Optional对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class Staff { private Department department; public Optional\u0026lt;Department\u0026gt; getDepartment() { return Optional.ofNullable(department); } ... } public class Department { private Company company; public Optional\u0026lt;Company\u0026gt; getCompany() { return Optional.ofNullable(company); } ... } public class Company { private String name; public String getName() { return name; } ... } 利用 Optional 的 Fluent Interface，以及 lambda 表达式重构代码 1 2 3 4 5 Optional\u0026lt;Staff\u0026gt; staffOpt = ...; staffOpt.flatMap(Staff::getDepartment) .flatMap(Department::getCompany) .map(Company::getName) .orElse(\u0026#34;Unknown\u0026#34;); 9. JDK8 新的日期和时间 API 9.1. 旧版日期时间 API 存在的问题 设计很差： 在java.util和java.sql的包中都有日期类，java.util.Date同时包含日期和时间，而java.sql.Date仅包含日期。此外用于格式化和解析的类在java.text包中定义 非线程安全：java.util.Date 是非线程安全的，所有的日期类都是可变的，这是Java日期类最大的问题之一 时区处理麻烦：日期类并不提供国际化，没有时区支持，因此Java引入了java.util.Calendar和java.util.TimeZone类，但他们同样存在上述所有的问题 9.2. 新的日期时间 API JDK 8中增加了一套全新的日期时间API，这套API设计合理，是线程安全的。新的java.time包涵盖了所有处理日期，时间，日期/时间，时区，时刻（instants），过程（during）与时钟（clock）的操作。常用有以下的关键类：\nLocalDate：表示日期，包含年月日，格式为 2019-10-16 LocalTime：表示时间，包含时分秒，格式为 16:38:54.158549300 LocalDateTime：表示日期时间，包含年月日，时分秒，格式为 2018-09-06T15:33:56.750 DateTimeFormatter：日期时间格式化类 Instant：时间戳，表示一个特定的时间瞬间 Duration：用于计算2个时间(LocalTime，时分秒)的距离 Period：用于计算2个日期(LocalDate，年月日)的距离 ZonedDateTime：包含时区的时间 Java中使用的历法是ISO 8601日历系统，它是世界民用历法，也就是通常所说的公历。平年有365天，闰年是366天。此外Java 8还提供了4套其他历法，分别是：\nThaiBuddhistDate：泰国佛教历 MinguoDate：中华民国历 JapaneseDate：日本历 HijrahDate：伊斯兰历 9.2.1. JDK 8的日期和时间类 LocalDate、LocalTime、LocalDateTime类的实例是不可变的对象，分别表示使用 ISO-8601 日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间，并不包含当前的时间信息，也不包含与时区相关的信息\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 /* LocalDate类: 表示日期，有年月日信息 */ @Test public void localDateTest() { // 工厂方法LocalDate.of()可以创建任意日期，该方法需要传入年、月、日做参数，返回对应的LocalDate实例。 LocalDate date = LocalDate.of(2018, 8, 8); System.out.println(\u0026#34;指定日期: \u0026#34; + date); // 通过静态工厂方法LocalDate.now()获取当天日期 LocalDate now = LocalDate.now(); System.out.println(\u0026#34;今天的日期：\u0026#34; + now); // 获取年信息 System.out.println(\u0026#34;year: \u0026#34; + now.getYear()); // 获取月信息信息（值为Month的枚举类） System.out.println(\u0026#34;Month枚举: \u0026#34; + now.getMonth()); // 获取月信息（值为1~12），注：与Date类不一样，Date获取的月份是从0开始 System.out.println(\u0026#34;month: \u0026#34; + now.getMonthValue()); // 获取日信息 System.out.println(\u0026#34;day: \u0026#34; + now.getDayOfMonth()); } /* LocalTime: 表示时间，有时分秒的信息 */ @Test public void localTimeTest() { // 通过静态工厂方法LocalTime.of()获取指定时间对象 LocalTime time = LocalTime.of(13, 26, 39); System.out.println(\u0026#34;指定时间time: \u0026#34; + time); // 通过静态工厂方法LocalTime.now()获取当前时间 LocalTime now = LocalTime.now(); System.out.println(\u0026#34;当前的时间,不含有日期: \u0026#34; + now); System.out.println(\u0026#34;hour: \u0026#34; + now.getHour()); // 获取时 System.out.println(\u0026#34;minute: \u0026#34; + now.getMinute()); // 获取分 System.out.println(\u0026#34;second: \u0026#34; + now.getSecond()); // 获取秒 System.out.println(\u0026#34;nano: \u0026#34; + now.getNano()); // 获取毫秒 } /* LocalDateTime: 相当于 LocalDate + LocalTime 具有年月日 时分秒的信息 */ @Test public void localDateTimeTest() { // 通过静态工厂方法LocalDateTime.of()获取指定日期时间对象 LocalDateTime dateTime = LocalDateTime.of(2018, 7, 12, 13, 28, 59); System.out.println(\u0026#34;指定的日期时间: \u0026#34; + dateTime); // 通过静态工厂方法LocalDateTime.now()获取当前日期时间 LocalDateTime now = LocalDateTime.now(); System.out.println(\u0026#34;当前的日期时间: \u0026#34; + now); System.out.println(\u0026#34;year: \u0026#34; + now.getYear()); // 获取年 System.out.println(\u0026#34;month: \u0026#34; + now.getMonthValue()); // 获取月 System.out.println(\u0026#34;day: \u0026#34; + now.getDayOfMonth()); // 获取日 System.out.println(\u0026#34;hour: \u0026#34; + now.getHour()); // 获取时 System.out.println(\u0026#34;minute: \u0026#34; + now.getMinute()); // 获取分 System.out.println(\u0026#34;second: \u0026#34; + now.getSecond()); // 获取秒 } 对日期时间的修改，对已存在的LocalDate、LocalTime、LocalDateTime对象，使用withAttribute方法会创建对象的一个副本，并按照需要修改它的属性。以下所有的方法都返回了一个修改属性的对象，不会影响原来的对象。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 /* LocalDate、LocalTime、LocalDateTime 日期时间修改、计算测试 */ @Test public void computeLocalDateTimeTest() { // LocalDate、LocalTime、LocalDateTime都同样的修改方式，调用withXxxx方法修改相应的属性 LocalDateTime now = LocalDateTime.now(); // 修改当前时间的年属性，修改返回新的日期时间对象，不会影响原日期时间对象 LocalDateTime dateTime = now.withYear(2222); System.out.println(\u0026#34;dateTime = \u0026#34; + dateTime); System.out.println(\u0026#34;now == dateTime: \u0026#34; + (now == dateTime)); // false /* * 增加或减去日期、时间 * plusXxxx: 增加指定的时间 * minusXxxx: 减去指定的时间 * Temporal plus(long amountToAdd, TemporalUnit unit)：通过指定时间单位，增加时间 * default Temporal minus(long amountToSubtract, TemporalUnit unit)：通过指定时间单位，减少时间 * * 注1：Java 8除了不变类型和线程安全的好处之外，还提供如更好的plusHours()方法替换add()，并且是兼容的。 * 注2：这些方法返回一个全新的LocalTime实例，由于其不可变性，返回后一定要用变量赋值。 * 注3：LocalDate、LocalTime、LocalDateTime 均相同的api来操作日期时间 */ // 经过测试，增加指定时间后，如果日期出现跨天的话，日期也会增加 System.out.println(\u0026#34;当前时间：\u0026#34; + now + \u0026#34; ，加上2年：\u0026#34; + now.plusYears(2)); System.out.println(\u0026#34;当前时间：\u0026#34; + now + \u0026#34; ，加上5月：\u0026#34; + now.plusMonths(5)); System.out.println(\u0026#34;当前时间：\u0026#34; + now + \u0026#34; ，加上20天：\u0026#34; + now.plusDays(20)); System.out.println(\u0026#34;当前时间：\u0026#34; + now + \u0026#34; ，加上10小时：\u0026#34; + now.plusHours(10)); System.out.println(\u0026#34;当前时间：\u0026#34; + now + \u0026#34; ，加上20分钟：\u0026#34; + now.plusMinutes(20)); System.out.println(\u0026#34;当前时间：\u0026#34; + now + \u0026#34; ，加上30秒：\u0026#34; + now.plusSeconds(30)); // 经过测试，减去指定时间后，如果日期出现跨天的话，日期也会减少 System.out.println(\u0026#34;当前时间：\u0026#34; + now + \u0026#34; ，减去2年：\u0026#34; + now.minusYears(2)); System.out.println(\u0026#34;当前时间：\u0026#34; + now + \u0026#34; ，减去5月：\u0026#34; + now.minusMonths(5)); System.out.println(\u0026#34;当前时间：\u0026#34; + now + \u0026#34; ，减去29天：\u0026#34; + now.minusDays(29)); System.out.println(\u0026#34;当前时间：\u0026#34; + now + \u0026#34; ，减去17小时：\u0026#34; + now.minusHours(17)); System.out.println(\u0026#34;当前时间：\u0026#34; + now + \u0026#34; ，减去20分钟：\u0026#34; + now.minusMinutes(20)); System.out.println(\u0026#34;当前时间：\u0026#34; + now + \u0026#34; ，减去30秒：\u0026#34; + now.minusSeconds(30)); // 创建LocalTime对象，只包含时间信息，没有日期。操作增加/减少小时、分、秒来计算的时间 LocalTime time = LocalTime.now(); System.out.println(\u0026#34;获取当前的时间:\u0026#34; + time); // 23:30:22.677 LocalTime plusTime = time.plusHours(3); System.out.println(\u0026#34;3个小时后的时间为:\u0026#34; + plusTime); // 02:30:22.677 LocalTime minusTime = time.minusHours(2); System.out.println(\u0026#34;2个小时前的时间为:\u0026#34; + minusTime); // 21:30:22.677 // 创建LocalDate对象，只包含日期不包含时间信息。操作增加/减少小时、分、秒来计算的时间 LocalDate today = LocalDate.now(); System.out.println(\u0026#34;今天的日期为:\u0026#34; + today); // 2020-07-12 // 通过使用LocalDate的plus()方法来增加天、周、月，ChronoUnit类声明了这些时间单位。 // 由于LocalDate也是不变类型，返回后一定要用变量赋值。 LocalDate plusDate = today.plus(1, ChronoUnit.WEEKS); System.out.println(\u0026#34;一周后的日期为:\u0026#34; + plusDate); // 2020-07-19 LocalDate minusDate = today.minus(3, ChronoUnit.DAYS); System.out.println(\u0026#34;3天前的日期为:\u0026#34; + minusDate); // 2020-07-09 LocalDate previousYear = today.minus(1, ChronoUnit.YEARS); System.out.println(\u0026#34;一年前的日期 : \u0026#34; + previousYear); // 2019-07-12 LocalDate nextYear = today.plus(1, ChronoUnit.YEARS); System.out.println(\u0026#34;一年后的日期:\u0026#34; + nextYear); // 2021-07-12 /* 注：可以用同样的方法增加（或减少）1个月、1年、1小时、1分钟甚至一个世纪 */ } LocalDate、LocalTime、LocalDateTime类可以通过isBefore()、isAfter()、equals()方法来进行日期时间的比较\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 /* LocalDate、LocalTime、LocalDateTime 日期时间比较测试 */ @Test public void compareLocalDateTimeTest() { LocalDateTime dateTime = LocalDateTime.of(2018, 7, 12, 13, 28, 59); LocalDateTime now = LocalDateTime.now(); System.out.println(\u0026#34;当前日期时间:\u0026#34; + now); // 日期对象a,b 调用 a.isAfter(b)，用于判断a日期是否在b日期之后 System.out.println(dateTime + \u0026#34;是否在当前日期之后: \u0026#34; + now.isAfter(dateTime)); // true // 日期对象a,b 调用 a.isBefore(b)，用于判断a日期是否在b日期之前 System.out.println(dateTime + \u0026#34;是否在当前日期之前: \u0026#34; + now.isBefore(dateTime)); // false // 日期对象a,b 调用 a.isEqual(b)，用于判断a日期是否在b日期相等 System.out.println(dateTime + \u0026#34;是否与当前日期相等: \u0026#34; + now.isEqual(dateTime)); // false // 直接调用LocalDateTime对象的equals()方法也可以判断两个日期是否相等（LocalDate也有此API） System.out.println(\u0026#34;equals()方法，\u0026#34; + dateTime + \u0026#34;是否与当前日期相等: \u0026#34; + now.equals(dateTime)); // false LocalDate today = LocalDate.now(); LocalDate yesterday = today.minus(1, ChronoUnit.DAYS); System.out.println(\u0026#34;当前日期:\u0026#34; + today); System.out.println(yesterday + \u0026#34;是否在当前日期之后: \u0026#34; + yesterday.isBefore(today)); // true System.out.println(yesterday + \u0026#34;是否在当前日期之前: \u0026#34; + yesterday.isAfter(today)); // false System.out.println(yesterday + \u0026#34;是否与当前日期相等: \u0026#34; + yesterday.isEqual(today)); // false } 9.2.2. JDK 8的时间格式化与解析 通过 java.time.format.DateTimeFormatter 类可以进行日期时间解析与格式化。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @Test public void parseAndFormatLocalDateTimeTest() { // 日期字符串 String dayString = \u0026#34;20200714\u0026#34;; // 日期时间字符串 String dateTimeString = \u0026#34;2020年09月20日 15时16分16秒\u0026#34;; // 创建一个日期时间对象 LocalDateTime now = LocalDateTime.now(); /* ==================== 日期对象转字符串测试 ==================== */ // 创建日期格式化对象，使用JDK自带的时间格式 （JDK8 日期的格式化对象是DateTimeFormatter） DateTimeFormatter isoFormatter = DateTimeFormatter.BASIC_ISO_DATE; // 使用DateTimeFormatter类的静态ofPattern()方法，创建日期格式化对象，指定自定义格式 DateTimeFormatter customFormatter = DateTimeFormatter.ofPattern(\u0026#34;yyyy年MM月dd日 HH时mm分ss秒\u0026#34;); /* * 调用日期时间对象的format方法，按指定的格式将日期时间对象转成字符串 * public String format(DateTimeFormatter formatter) */ System.out.printf(\u0026#34;LocalDateTime对象 \u0026#39;%s\u0026#39; 转成JDK自带BASIC_ISO_DATE格式字符串：\u0026#39;%s\u0026#39;%n\u0026#34;, now, now.format(isoFormatter)); System.out.printf(\u0026#34;LocalDateTime对象 \u0026#39;%s\u0026#39; 转成自定义格式字符串：\u0026#39;%s\u0026#39;%n\u0026#34;, now, now.format(customFormatter)); /* ==================== 字符串解析成日期对象测试 ==================== */ /* * 日期时间解析：LocalDateTime类的parse静态方法，将日期时间字符串转成对象 * parse(CharSequence text, DateTimeFormatter formatter) * 作用：将字符串转成LocalDate日期对象 * text参数：待转换的日期时间字符串 * formatter参数：日期格式化对象DateTimeFormatter，该格式器有一些静态属性为指定解析时日期的格式 */ // LocalDate dateFormatted = LocalDate.parse(dayString, isoFormatter); System.out.printf(\u0026#34;字符串 \u0026#39;%s\u0026#39; 格式化后的日期LocalDate类型为：%s%n\u0026#34;, dayString, dateFormatted); LocalDateTime parseDateTime = LocalDateTime.parse(dateTimeString, customFormatter); System.out.printf(\u0026#34;字符串 \u0026#39;%s\u0026#39; 格式化后的日期LocalDateTime类型为：%s%n\u0026#34;, dateTimeString, parseDateTime); /* 测试多线程下，解析日期是否正常 */ for (int i = 0; i \u0026lt; 50; i++) { new Thread(() -\u0026gt; System.out.println(\u0026#34;多线程解析日期 = \u0026#34; + LocalDateTime.parse(dateTimeString, customFormatter)) ).start(); } } 9.2.3. JDK 8的 Instant 类 Instant 类是jdk8新提供的时间戳（时间线），内部保存了从1970年1月1日 00:00:00以来的秒和纳秒。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Test public void instantTest() { // Instant内部保存了秒和纳秒(一般不是给用户使用的，而是方便程序做一些统计的) // Instant类有一个静态工厂方法now()会返回当前的时间戳 Instant timestamp = Instant.now(); System.out.println(\u0026#34;What is value of this instant : \u0026#34; + timestamp); // 2020-09-28T09:22:33.732Z // 增加秒 Instant plus = timestamp.plusSeconds(20); System.out.println(\u0026#34;plus = \u0026#34; + plus); // 2020-09-28T09:22:53.732Z // 减去秒 Instant minus = timestamp.minusSeconds(20); System.out.println(\u0026#34;minus = \u0026#34; + minus); // 2020-09-28T09:22:13.732Z // 调用Instant对象的getEpochSecond()方法，获取秒值 System.out.println(\u0026#34;What is value of this instant.getEpochSecond() : \u0026#34; + timestamp.getEpochSecond()); // 调用Instant对象的getNano()方法，获取纳秒值 System.out.println(\u0026#34;What is value of this instant.getNano() : \u0026#34; + timestamp.getNano()); // 调用Instant对象的toEpochMilli()方法，获取毫秒值 System.out.println(\u0026#34;What is value of this instant.toEpochMilli() : \u0026#34; + timestamp.toEpochMilli()); // 1594646847755 /* * Instant类时间戳信息里同时包含了日期和时间，这和java.util.Date很像。 * 实际上Instant类确实等同于 Java 8之前的Date类，可以使用Date类和Instant类各自的转换方法互相转换 * 例如：Date.from(Instant) 将Instant转换成java.util.Date，Date.toInstant()则是将Date类转换成Instant类。 */ Date dateFromInstant = Date.from(timestamp); System.out.println(\u0026#34;Instant转成Date：\u0026#34; + dateFromInstant); // Mon Sep 28 17:22:33 CST 2020 Instant dateToInstant = new Date().toInstant(); System.out.println(\u0026#34;Date转成Instant：\u0026#34; + dateToInstant); // 2020-09-28T09:22:33.811Z } 9.2.4. JDK 8的计算日期时间差类 JDK8 提供了 Duration与Period类，用于计算日期时间差\nDuration：用于计算2个时间(LocalTime，时分秒)的距离 Period：用于计算2个日期(LocalDate，年月日)的距离 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 /* * 计算两个日期之间的天数、周数或月数 * 在Java 8中可以用java.time.Period类来做计算。 * 计算两个时间之间的天数、小时、分钟、秒、毫秒 * 在Java 8中可以用java.time.Duration类来做计算。 * 注：都调用以上两个的between()方法实现，都是后面参数减前端的参数 */ @Test public void computeTimeDifferenceTest() { LocalDate today = LocalDate.now(); System.out.println(\u0026#34;Today is : \u0026#34; + today); // 2020-07-13 LocalDate dateToCompute = LocalDate.of(2021, 12, 14); // 计算两个日期的差值 Period periodBetweenTwoDate = Period.between(today, dateToCompute); // getYears()计算的差值直接为年份数相减 System.out.println(\u0026#34;Years left between today and dateToCompute : \u0026#34; + periodBetweenTwoDate.getYears()); // getMonths()计算的差值直接为月份数相减，年不在计算范围内 System.out.println(\u0026#34;Months left between today and dateToCompute : \u0026#34; + periodBetweenTwoDate.getMonths()); // getDays()计算的差值直接为天数相减，月与年不在计算范围内 System.out.println(\u0026#34;Days left between today and dateToCompute : \u0026#34; + periodBetweenTwoDate.getDays()); LocalTime nowTime = LocalTime.now(); System.out.println(\u0026#34;Now is : \u0026#34; + nowTime); // 2020-07-13 LocalTime timeToCompute = LocalTime.of(20, 12, 14); // 计算两个时间的差值 Duration durationBetweenTwoTime = Duration.between(nowTime, timeToCompute); // toDays()计算两个时间相差的天数 System.out.println(\u0026#34;Days left between nowTime and timeToCompute : \u0026#34; + durationBetweenTwoTime.toDays()); // toHours()计算两个时间相差的小时数 System.out.println(\u0026#34;Hours left between nowTime and timeToCompute : \u0026#34; + durationBetweenTwoTime.toHours()); // toMinutes()计算两个时间相差的分钟数 System.out.println(\u0026#34;Minutes left between nowTime and timeToCompute : \u0026#34; + durationBetweenTwoTime.toMinutes()); // toMillis()计算两个时间相差的秒数 System.out.println(\u0026#34;Seconds left between nowTime and timeToCompute : \u0026#34; + durationBetweenTwoTime.toMillis()); // toNanos()计算两个时间相差的毫秒数 System.out.println(\u0026#34;Milliseconds left between nowTime and timeToCompute : \u0026#34; + durationBetweenTwoTime.toNanos()); } 9.2.5. JDK 8的时间校正器 时间校正器：用于自定义调整时间操作，将日期调整到指定某个时间点\nTemporalAdjuster：时间校正器 TemporalAdjusters：该类通过静态方法提供了大量的常用TemporalAdjuster的实现。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void temporalAdjusterTest() { // 获取当前日期时间 LocalDateTime now = LocalDateTime.now(); // TemporalAdjuster是函数式接口，使用lambda表达式创建TemporalAdjuster的实现 TemporalAdjuster firstDayOfNextMonth = temporal -\u0026gt; { // LocalDateTime实现了 TemporalAdjuster 接口 LocalDateTime dateTime = (LocalDateTime) temporal; // 返回时间校正的规则。示例：下一个月的第一天 return dateTime.plusMonths(1).withDayOfMonth(1); }; // 调用LocalDateTime对象的with方法，传入自定义时间校正器TemporalAdjuster的lambda实现 LocalDateTime newDateTime1 = now.with(firstDayOfNextMonth); System.out.println(\u0026#34;将当前日期时间调整到下一个月的第一天: \u0026#34; + newDateTime1); // 除了自定义时间校正器，JDK中TemporalAdjusters类提供了很多时间调整器 LocalDateTime newDateTime2 = now.with(TemporalAdjusters.firstDayOfNextYear()); System.out.println(\u0026#34;将当前日期时间调整到下一个年的第一天: \u0026#34; + newDateTime2); } 9.2.6. JDK 8设置日期时间的时区 Java8 中加入了对时区的支持，LocalDate、LocalTime、LocalDateTime是不带时区的，带时区的日期时间类分别为：ZonedDate、ZonedTime、ZonedDateTime\n其中每个时区都对应着ID，ID的格式为“区域/城市”。例如：Asia/Shanghai等。而所有的时区信息都定义在ZoneId类中\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 /* * Java 8中处理时区 * Java 8不仅分离了日期和时间，也把时区分离出来了。 * 现在有一系列单独的类如ZoneId来处理特定时区，ZoneDateTime类来表示某时区下的时间。 * 这在Java 8以前都是 GregorianCalendar类来做的。 */ @Test public void zoneDateTimeTest() { // 通过ZoneId类的getAvailableZoneIds静态方法，获取所有的时区ID // ZoneId.getAvailableZoneIds().forEach(System.out::println); // 获取计算机的当前时间。LocalDate、LocalTime、LocalDateTime是不带时区的 LocalDateTime now = LocalDateTime.now(); // 中国使用的东八区的时区.比标准时间早8个小时 System.out.println(\u0026#34;不带时区的LocalDateTime: \u0026#34; + now); // 2020-09-28T23:37:11.298 /* * 操作带时区的类：ZonedDateTime * now(Clock.systemUTC()): 创建世界标准时间 */ ZonedDateTime zonedDateTime1 = ZonedDateTime.now(Clock.systemUTC()); System.out.println(\u0026#34;世界标准时间ZonedDateTime: \u0026#34; + zonedDateTime1); // 2020-09-28T15:37:11.299Z // ZonedDateTime.now(): 使用计算机的默认的时区,创建日期时间 ZonedDateTime zonedDateTime2 = ZonedDateTime.now(); System.out.println(\u0026#34;带时区的当前时间zonedDateTime2: \u0026#34; + zonedDateTime2); // 2020-09-28T23:37:11.299+08:00[Asia/Shanghai] // ZonedDateTime.now(ZoneId zone) 使用指定的时区创建日期时间 ZonedDateTime zonedDateTime3 = ZonedDateTime.now(ZoneId.of(\u0026#34;America/Vancouver\u0026#34;)); System.out.println(\u0026#34;使用指定的时区创建日期时间: \u0026#34; + zonedDateTime3); // 2020-09-28T08:37:11.300-07:00[America/Vancouver] /* * 通过ZonedDateTime对象的withZoneSameInstant方法，修改时区 * 注：withZoneSameInstant: 即更改时区，也更改时间 */ ZonedDateTime withZoneSameInstant = zonedDateTime3.withZoneSameInstant(ZoneId.of(\u0026#34;Asia/Shanghai\u0026#34;)); System.out.println(\u0026#34;withZoneSameInstant = \u0026#34; + withZoneSameInstant); // 2020-09-28T23:37:11.300+08:00[Asia/Shanghai] /* * 通过ZonedDateTime对象的withZoneSameLocal方法，修改时区 * 注：withZoneSameLocal: 只更改时区,不更改时间 */ ZonedDateTime withZoneSameLocal = zonedDateTime3.withZoneSameLocal(ZoneId.of(\u0026#34;Asia/Shanghai\u0026#34;)); System.out.println(\u0026#34;withZoneSameLocal = \u0026#34; + withZoneSameLocal); // 2020-09-28T08:37:11.300+08:00[Asia/Shanghai] // Date and time with timezone in Java 8（Java 8中带时区的日期和时间） ZoneId america = ZoneId.of(\u0026#34;America/New_York\u0026#34;); // 指定美国时区 LocalDateTime localtDateAndTime = LocalDateTime.now(); // 创建时间对象 // 获取带有指定时区的时间对象（ZonedDateTime） ZonedDateTime dateTimeInNewYork = ZonedDateTime.of(localtDateAndTime, america); System.out.println(\u0026#34;Current date and time in a particular timezone : \u0026#34; + dateTimeInNewYork); // 2020-09-28T23:37:11.313-04:00[America/New_York] } 9.2.7. JDK8 其他的API使用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 /* * Java 8中检查是否周期性日期事件 * MonthDay对象：用于每年重复周期性事件，即月+日，如生日、节日等 * YearMonth对象：用于，还可以用这个类得到当月共有多少天。 */ @Test public void monthDayAndYearMonthTest() { /* MonthDay对象测试部分 */ LocalDate now = LocalDate.now(); LocalDate date = LocalDate.of(2020, 7, 12); // 通过静态工厂方法MonthDay.of(Month month, int dayOfMonth)，获取指定月与日的MonthDay对象 MonthDay birthday = MonthDay.of(date.getMonth(), date.getDayOfMonth()); // 通过静态方法MonthDay.from(TemporalAccessor temporal)，获取指定某年的的MonthDay对象 MonthDay currentMonthDay = MonthDay.from(now); // 比较两个MonthDay对象 if (currentMonthDay.equals(birthday)) { System.out.println(\u0026#34;是你的生日\u0026#34;); } else { System.out.println(\u0026#34;你的生日还没有到\u0026#34;); } /* YearMonth对象测试部分 */ YearMonth currentYearMonth = YearMonth.now(); System.out.printf(\u0026#34;Days in month year %s: %d%n\u0026#34;, currentYearMonth, currentYearMonth.lengthOfMonth()); // Days in month year 2020-07: 31 YearMonth creditCardExpiry = YearMonth.of(2020, Month.JULY); System.out.printf(\u0026#34;Your credit card expires on %s %n\u0026#34;, creditCardExpiry); // Your credit card expires on 2020-07 // YearMonth实例的lengthOfMonth()方法可以返回当月的天数，在判断2月有28天还是29天时非常有用 YearMonth yearMonth = YearMonth.of(2020, Month.FEBRUARY); if (yearMonth.lengthOfMonth() == 29) { System.out.println(yearMonth.getYear() + \u0026#34;是闰年\u0026#34;); } else { System.out.println(yearMonth.getYear() + \u0026#34;非闰年\u0026#34;); } } /* Java 8中检查是否闰年 */ @Test public void isLeapYearTest() { // 除了通过YearMonth实例的lengthOfMonth()返回的天数判断是否闰年，还可以使用LocalDate的isLeapYear()方法直接判断是否为闰年 LocalDate today = LocalDate.now(); if (today.isLeapYear()) { System.out.printf(\u0026#34;%d is Leap year\u0026#34;, today.getYear()); } else { System.out.printf(\u0026#34;%d is not a Leap year\u0026#34;, today.getYear()); } } /* * Java 8的Clock时钟类 * Java 8增加了一个 Clock 时钟类用于获取当时的时间戳，或当前时区下的日期时间信息。 * JDK8以后，可以用 Clock 对象相应的方法替换 System.currentTimeInMillis() 和 TimeZone.getDefault() 。 */ @Test public void clockTest() { // Returns the current time based on your system clock and set to UTC.（根据您的系统时钟返回当前时间，并将其设置为UTC。） Clock clock = Clock.systemUTC(); // 获取当前时间的毫秒值, 相关于JDK8以前的System.currentTimeInMillis()方法 System.out.println(\u0026#34;Clock的millis()方法获取的毫秒值: \u0026#34; + clock.millis()); // 1594568628639 System.out.println(\u0026#34;System.currentTimeInMillis()的毫秒值: \u0026#34; + clock.millis()); // 1594568628639 // Returns time based on system clock zone（根据系统时钟区域返回时间） Clock defaultZoneClock = Clock.systemDefaultZone(); System.out.println(\u0026#34;系统所在时钟区域的Clock的毫秒值: \u0026#34; + defaultZoneClock.millis()); // 1594568628711 } 9.3. 使用示例 9.3.1. 本地化日期时间 API LocalDate/LocalTime 和 LocalDateTime 类可以在处理时区不是必须的情况。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.moon.test; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; import java.time.Month; public class Java8Tester { public static void main(String args[]) { Java8Tester java8tester = new Java8Tester(); java8tester.testLocalDateTime(); } public void testLocalDateTime() { // 获取当前的日期时间 LocalDateTime currentTime = LocalDateTime.now(); System.out.println(\u0026#34;当前时间: \u0026#34; + currentTime); LocalDate date1 = currentTime.toLocalDate(); System.out.println(\u0026#34;date1: \u0026#34; + date1); Month month = currentTime.getMonth(); int day = currentTime.getDayOfMonth(); int seconds = currentTime.getSecond(); System.out.println(\u0026#34;月: \u0026#34; + month + \u0026#34;, 日: \u0026#34; + day + \u0026#34;, 秒: \u0026#34; + seconds); LocalDateTime date2 = currentTime.withDayOfMonth(10).withYear(2012); System.out.println(\u0026#34;date2: \u0026#34; + date2); // 12 december 2014 LocalDate date3 = LocalDate.of(2014, Month.DECEMBER, 12); System.out.println(\u0026#34;date3: \u0026#34; + date3); // 22 小时 15 分钟 LocalTime date4 = LocalTime.of(22, 15); System.out.println(\u0026#34;date4: \u0026#34; + date4); // 解析字符串 LocalTime date5 = LocalTime.parse(\u0026#34;20:15:30\u0026#34;); System.out.println(\u0026#34;date5: \u0026#34; + date5); } } 程序输出结果：\n1 2 3 4 5 6 7 当前时间: 2019-07-22T10:57:38.601 date1: 2019-07-22 月: JULY, 日: 22, 秒: 38 date2: 2012-07-10T10:57:38.601 date3: 2014-12-12 date4: 22:15 date5: 20:15:30 9.3.2. 使用时区的日期时间API 需要考虑到时区，就可以使用时区的日期时间API\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package com.moon.test; import java.time.ZoneId; import java.time.ZonedDateTime; public class Java8Tester { public static void main(String args[]) { Java8Tester java8tester = new Java8Tester(); java8tester.testZonedDateTime(); } public void testZonedDateTime() { // 获取当前时间日期 ZonedDateTime date1 = ZonedDateTime.parse(\u0026#34;2015-12-03T10:15:30+05:30[Asia/Shanghai]\u0026#34;); System.out.println(\u0026#34;date1: \u0026#34; + date1); ZoneId id = ZoneId.of(\u0026#34;Europe/Paris\u0026#34;); System.out.println(\u0026#34;ZoneId: \u0026#34; + id); ZoneId currentZone = ZoneId.systemDefault(); System.out.println(\u0026#34;当期时区: \u0026#34; + currentZone); } } 程序输出结果：\n1 2 3 date1: 2015-12-03T10:15:30+08:00[Asia/Shanghai] ZoneId: Europe/Paris 当期时区: Asia/Shanghai 10. Base64 在 Java 8 中，Base64 编码已经成为Java类库的标准。内置了 Base64 编码的编码器和解码器。 Base64 工具类提供了一套静态方法获取下面三种BASE64编解码器： 基本：输出被映射到一组字符A-Za-z0-9+/，编码不添加任何行标，输出的解码仅支持A-Za-z0-9+/ URL：输出映射到一组字符A-Za-z0-9+_，输出是URL和文件 MIME：输出隐射到MIME友好格式。输出每行不超过76字符，并且使用\\r并跟随\\n作为分割。编码输出最后没有行分割 10.1. 内部类 static class Base64.Decoder 该类实现一个解码器用于，使用 Base64 编码来解码字节数据 static class Base64.Encoder 该类实现一个编码器，使用 Base64 编码来编码字节数据 10.2. 相关方法 static Base64.Decoder getDecoder() 返回一个 Base64.Decoder ，解码使用基本型 base64 编码方案 static Base64.Encoder getEncoder() 返回一个 Base64.Encoder ，编码使用基本型 base64 编码方案 static Base64.Decoder getMimeDecoder() 返回一个 Base64.Decoder ，解码使用 MIME 型 base64 编码方案 static Base64.Encoder getMimeEncoder() 返回一个 Base64.Encoder ，编码使用 MIME 型 base64 编码方案 static Base64.Encoder getMimeEncoder(int lineLength, byte[] lineSeparator) 返回一个 Base64.Encoder ，编码使用 MIME 型 base64 编码方案，可以通过参数指定每行的长度及行的分隔符 static Base64.Decoder getUrlDecoder() 返回一个 Base64.Decoder ，解码使用 URL 和文件名安全型 base64 编码方案 static Base64.Encoder getUrlEncoder() 返回一个 Base64.Encoder ，编码使用 URL 和文件名安全型 base64 编码方案。 注意：Base64 类的很多方法从 java.lang.Object 类继承\n10.3. Base64 实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package com.moon.test; import java.io.UnsupportedEncodingException; import java.util.Base64; import java.util.UUID; public class Java8Tester { public static void main(String args[]) { try { // 使用基本编码 String base64encodedString = Base64.getEncoder().encodeToString(\u0026#34;runoob?java8\u0026#34;.getBytes(\u0026#34;utf-8\u0026#34;)); System.out.println(\u0026#34;Base64 编码字符串 (基本) :\u0026#34; + base64encodedString); // 解码 byte[] base64decodedBytes = Base64.getDecoder().decode(base64encodedString); System.out.println(\u0026#34;原始字符串: \u0026#34; + new String(base64decodedBytes, \u0026#34;utf-8\u0026#34;)); base64encodedString = Base64.getUrlEncoder().encodeToString(\u0026#34;TutorialsPoint?java8\u0026#34;.getBytes(\u0026#34;utf-8\u0026#34;)); System.out.println(\u0026#34;Base64 编码字符串 (URL) :\u0026#34; + base64encodedString); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i \u0026lt; 10; ++i) { stringBuilder.append(UUID.randomUUID().toString()); } byte[] mimeBytes = stringBuilder.toString().getBytes(\u0026#34;utf-8\u0026#34;); String mimeEncodedString = Base64.getMimeEncoder().encodeToString(mimeBytes); System.out.println(\u0026#34;Base64 编码字符串 (MIME) :\u0026#34; + mimeEncodedString); } catch (UnsupportedEncodingException e) { System.out.println(\u0026#34;Error :\u0026#34; + e.getMessage()); } } } 程序输出结果：\n1 2 3 4 5 6 7 8 9 10 Base64 编码字符串 (基本) :cnVub29iP2phdmE4 原始字符串: runoob?java8 Base64 编码字符串 (URL) :VHV0b3JpYWxzUG9pbnQ_amF2YTg= Base64 编码字符串 (MIME) :MmIxYmQ2NTQtZWU4OS00ZWQyLWFkYzctOWJlNDRlZDg2MDc5ZWQxM2FmNmYtYTExNi00OGEyLTlh NmYtZWE3NzQyZjc0MzA4ODIyN2Q1ODYtODIyYy00MGRkLTgyNWItYTNkN2NmYmZiMTdmYzcxN2U5 ODgtYWMwNi00MDZjLWIyOWUtOTBmZjcyNTVkNDllNzYzYmMzODItYjkwNi00MTVhLTgwYWItYmEz NDI5YTVjMTFhZjE1MmE2M2UtZjdmNS00MWJlLTkxYTMtMjlhMmFkZGZhYjUwY2E0MTI1MzktMzlk Zi00NTYyLTllYzMtMWExMzlkODE1Njc3YTk0YzFiNWUtOGVhOC00Yjg1LTg4NTYtYjE0YzA3Mzc5 ZDc3Mjc2MDdjZjUtYjA1ZC00N2U5LTlkODItZDk5YTliMDA1MmRkN2FkZTdjM2EtZTFhOS00Zjgz LTlkZjgtNWZjYWIyNzhjYjlk 11. JDK 8 重复注解与类型注解 11.1. 重复注解 11.1.1. 重复注解的介绍 自从 Java 5 中引入注解以来，在各个框架和项目中被广泛使用。不过注解有一个很大的限制是：在同一个地方不能多次使用同一个注解。JDK 8 引入了重复注解的概念，允许在同一个地方多次使用同一个注解\n在 JDK 8 中使用@Repeatable注解定义重复注解。\n11.1.2. 重复注解的使用步骤 定义一个可以重复的注解 1 2 3 4 5 @Retention(RetentionPolicy.RUNTIME) @Repeatable(MyRepeatableContainer.class) @interface MyRepeatableAnnotation { String value(); } 定义重复的注解容器注解 1 2 3 4 5 @Retention(RetentionPolicy.RUNTIME) @interface MyRepeatableContainer { // 定义重复注解容器的属性 MyRepeatableAnnotation[] value(); } 在测试类中配置多个重复的注解 1 2 3 4 5 6 7 8 9 10 11 12 /* 3. 在类中标识重复注解 */ @MyRepeatableAnnotation(\u0026#34;AA\u0026#34;) @MyRepeatableAnnotation(\u0026#34;BB\u0026#34;) @MyRepeatableAnnotation(\u0026#34;CC\u0026#34;) public class Demo01RepeatableAnnotation { /* 3. 在方法中标识重复注解 */ @MyRepeatableAnnotation(\u0026#34;XX\u0026#34;) @MyRepeatableAnnotation(\u0026#34;YY\u0026#34;) public void foo() { } } 解析得到指定注解 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 /* Class与Method对象的 getAnnotationsByType 方法是新增的API，用于获取重复的注解 */ @Test public void repeatableAnnotationTest() throws NoSuchMethodException { // 获取类上的重复注解 MyRepeatableAnnotation[] repeatableAnnos = RepeatableAnnotationDemo.class .getAnnotationsByType(MyRepeatableAnnotation.class); // 循环输出重复注解值 for (MyRepeatableAnnotation repeatableAnno : repeatableAnnos) { System.out.println(repeatableAnno + \u0026#34; 重复注解（标识在类上）的value值为: \u0026#34; + repeatableAnno.value()); } System.out.println(\u0026#34;------------------------------\u0026#34;); // 获取方法上的重复注解 MyRepeatableAnnotation[] methodRepeatableAnnos = RepeatableAnnotationDemo.class .getMethod(\u0026#34;foo\u0026#34;).getAnnotationsByType(MyRepeatableAnnotation.class); for (MyRepeatableAnnotation repeatableAnno : methodRepeatableAnnos) { System.out.println(repeatableAnno + \u0026#34; 重复注解（标识在方法上）的value值为: \u0026#34; + repeatableAnno.value()); } } 11.2. 类型注解 JDK 8为@Target元注解新增了两种类型：TYPE_PARAMETER，TYPE_USE\nTYPE_PARAMETER：表示该注解能写在类型参数的声明语句中。类型参数声明如：\u0026lt;T\u0026gt; TYPE_USE：表示注解可以再任何用到类型的地方使用 11.2.1. TYPE_PARAMETER 类型的使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 标识在类上的泛型前 public class TypeParameterDemo\u0026lt;@MyTypeParameter T\u0026gt; { // 标识在方法上的泛型前 public \u0026lt;@MyTypeParameter E extends Integer\u0026gt; void foo() { } } /** * 定义TYPE_PARAMETER类型的注解 * 表示该注解能写在类型参数的声明语句中。类型参数声明如：\u0026lt;T\u0026gt; */ @Target(ElementType.TYPE_PARAMETER) @interface MyTypeParameter { } 11.2.2. TYPE_USE 类型的使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class TypeUseDemo { // 在类属性类型前使用 private @MyTypeUse int a = 10; // 在方法形参的类型前使用 public void test(@MyTypeUse String str, @MyTypeUse int a) { // 在方法内的变量类型前使用 @MyTypeUse double d = 10.1; } } /** * 定义TYPE_USE类型的注解 * 表示注解可以再任何用到类型的地方使用 */ @Target(ElementType.TYPE_USE) @interface MyTypeUse { } ","permalink":"https://ktzxy.top/posts/9x1w9m6qae/","summary":"Java基础 Java8新特性","title":"Java基础 Java8新特性"},{"content":"目录介绍 1.异常Exception 2.异常Error 1.异常Exception 算术条件异常（譬如：整数除零等）：java.lang.ArithmeticException 数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出：java.lang.ArrayIndexOutOfBoundsException 数组存储异常。当向数组中存放非数组声明类型对象时抛出：java.lang.ArrayStoreException 强制类型转换异常。假设有类A和B（A不是B的父类或子类），O是A的实例，那么当强制将O构造为类B的- 实例时抛出该异常。该异常经常被称为强制类型转换异常：java.lang.ClassCastException 找不到类异常。当应用试图根据字符串形式的类名构造类，而在遍历CLASSPAH之后找不到对应名称的class文件时，抛出该异常：java.lang.ClassNotFoundException 不支持克隆异常。当没有实现Cloneable接口或者不支持克隆方法时,调用其clone()方法则抛出该异常：java.lang.CloneNotSupportedException 枚举常量不存在异常。当应用试图通过名称和枚举类型访问一个枚举对象，但该枚举对象并不包含常量时，抛出该异常：java.lang.EnumConstantNotPresentException 根异常。用以描述应用程序希望捕获的情况：java.lang.Exception 违法的访问异常。当应用试图通过反射方式创建某个类的实例、访问该类属性、调用该类方法，而当时又无法访问类的、属性的、方法的或构造方法的定义时抛出该异常：java.lang.IllegalAccessException 违法的监控状态异常。当某个线程试图等待一个自己并不拥有的对象（O）的监控器或者通知其他线程等待该对象（O）的监控器时，抛出该异常：java.lang.IllegalMonitorStateException 违法的状态异常。当在Java环境和应用尚未处于某个方法的合法调用状态，而调用了该方法时，抛出该异常：java.lang.IllegalStateException 违法的线程状态异常。当线程尚未处于某个方法的合法调用状态，而调用了该方法时，抛出异常：java.lang.IllegalThreadStateException 索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时，抛出该异常：java.lang.IndexOutOfBoundsException 实例化异常。当试图通过newInstance()方法创建某个类的实例，而该类是一个抽象类或接口时，抛出该异常：java.lang.InstantiationException 被中止异常。当某个线程处于长时间的等待、休眠或其他暂停状态，而此时其他的线程通过Thread的interrupt方法终止该线程时抛出该异常：java.lang.InterruptedException 数组大小为负值异常。当使用负数大小值创建数组时抛出该异常：java.lang.NegativeArraySizeException 属性不存在异常。当访问某个类的不存在的属性时抛出该异常：java.lang.NoSuchFieldException 方法不存在异常。当访问某个类的不存在的方法时抛出该异常：java.lang.NoSuchMethodException 空指针异常。当应用试图在要求使用对象的地方使用了null时，抛出该异常。譬如：调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等：java.lang.NullPointerException 数字格式异常。当试图将一个String转换为指定的数字类型，而该字符串确不满足数字类型要求的格式时，抛出该异常：java.lang.NumberFormatException 运行时异常。是所有Java虚拟机正常操作期间可以被抛出的异常的父类：java.lang.RuntimeException 安全异常。由安全管理器抛出，用于指示违反安全情况的异常：java.lang.SecurityException 字符串索引越界异常。当使用索引值访问某个字符串中的字符，而该索引值小于0或大于等于序列大小时，抛出该异常：java.lang.StringIndexOutOfBoundsException 类型不存在异常。当应用试图以某个类型名称的字符串表达方式访问该类型，但是根据给定的名称又找不到该类型是抛出该异常。该异常与ClassNotFoundException的区别在于该异常是unchecked（不被检查）异常，而ClassNotFoundException是checked（被检查）异常：java.lang.TypeNotPresentException 不支持的方法异常。指明请求的方法不被支持情况的异常：java.lang.UnsupportedOperationException 2.异常Error 抽象方法错误，当应用试图调用抽象方法时抛出：java.lang.AbstractMethodError 断言错误，用来指示一个断言失败的情况：java.lang.AssertionError 类循环依赖错误。在初始化一个类时，若检测到类之间循环依赖则抛出该异常：java.lang.ClassCircularityError 类格式错误。当Java虚拟机试图从一个文件中读取Java类，而检测到该文件的内容不符合类的有效格式时抛出：java.lang.ClassFormatError 错误。是所有错误的基类，用于标识严重的程序运行问题。这些问题通常描述一些不应被应用程序捕获的反常情况：java.lang.Error 初始化程序错误。当执行一个类的静态初始化程序的过程中，发生了异常时抛出。静态初始化程序是指直接包含于类中的static语句段：java.lang.ExceptionInInitializerError 违法访问错误。当一个应用试图访问、修改某个类的域（Field）或者调用其方法，但是又违反域或方法的可见性声明，则抛出该异常：java.lang.IllegalAccessError 不兼容的类变化错误。当正在执行的方法所依赖的类定义发生了不兼容的改变时，抛出该异常。一般在修改了应用中的某些类的声明定义而没有对整个应用重新编译而直接运行的情况下，容易引发该错误：java.lang.IncompatibleClassChangeError 实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常：java.lang.InstantiationError 内部错误。用于指示Java虚拟机发生了内部错误：java.lang.InternalError 链接错误。该错误及其所有子类指示某个类依赖于另外一些类，在该类编译之后，被依赖的类改变了其类定义而没有重新编译所有的类，进而引发错误的情况：java.lang.LinkageError 未找到类定义错误。当Java虚拟机或者类装载器试图实例化某个类，而找不到该类的定义时抛出该错误：java.lang.NoClassDefFoundError 域不存在错误。当应用试图访问或者修改某类的某个域，而该类的定义中没有该域的定义时抛出该错误：java.lang.NoSuchFieldError 方法不存在错误。当应用试图调用某类的某个方法，而该类的定义中没有该方法的定义时抛出该错误：java.lang.NoSuchMethodError 内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误：java.lang.OutOfMemoryError 堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出时抛出该错误：java.lang.StackOverflowError 线程结束。当调用Thread类的stop方法时抛出该错误，用于指示线程结束：java.lang.ThreadDeath 未知错误。用于指示Java虚拟机发生了未知严重错误的情况：java.lang.UnknownError 未满足的链接错误。当Java虚拟机未找到某个类的声明为native方法的本机语言定义时抛出：java.lang.UnsatisfiedLinkError 不支持的类版本错误。当Java虚拟机试图从读取某个类文件，但是发现该文件的主、次版本号不被当前Java虚拟机支持的时候，抛出该错误：java.lang.UnsupportedClassVersionError 验证错误。当验证器检测到某个类文件中存在内部不兼容或者安全问题时抛出该错误：java.lang.VerifyError 虚拟机错误。用于指示虚拟机被破坏或者继续执行操作所需的资源不足的情况：java.lang.VirtualMachineError ","permalink":"https://ktzxy.top/posts/70efb8dn5m/","summary":"常见的异常","title":"常见的异常"},{"content":"1、检测两台服务器指定目录下的文件一致性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #!/bin/bash ###################################### 检测两台服务器指定目录下的文件一致性 ##################################### #通过对比两台服务器上文件的md5值，达到检测一致性的目的 dir=/data/web b_ip=192.168.88.10 #将指定目录下的文件全部遍历出来并作为md5sum命令的参数，进而得到所有文件的md5值，并写入到指定文件中 find $dir -type f|xargs md5sum \u0026gt; /tmp/md5_a.txt ssh $b_ip \u0026#34;find $dir -type f|xargs md5sum \u0026gt; /tmp/md5_b.txt\u0026#34; scp $b_ip:/tmp/md5_b.txt /tmp #将文件名作为遍历对象进行一一比对 for f in `awk \u0026#39;{print 2} /tmp/md5_a.txt\u0026#39;`do #以a机器为标准，当b机器不存在遍历对象中的文件时直接输出不存在的结果 if grep -qw \u0026#34;$f\u0026#34; /tmp/md5_b.txt then md5_a=`grep -w \u0026#34;$f\u0026#34; /tmp/md5_a.txt|awk \u0026#39;{print $1}\u0026#39;` md5_b=`grep -w \u0026#34;$f\u0026#34; /tmp/md5_b.txt|awk \u0026#39;{print $1}\u0026#39;` #当文件存在时，如果md5值不一致则输出文件改变的结果 if [ $md5_a != $md5_b ]then echo \u0026#34;$f changed.\u0026#34; fi else echo \u0026#34;$f deleted.\u0026#34; fi done 2、定时清空文件内容，定时记录文件大小 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/bin/bash ################################################################# 每小时执行一次脚本（任务计划），当时间为0点或12点时，将目标目录下的所有文件内#容清空，但不删除文件，其他时间则只统计各个文件的大小，一个文件一行，输出到以时#间和日期命名的文件中，需要考虑目标目录下二级、三级等子目录的文件 ################################################################ logfile=/tmp/`date +%H-%F`.log n=`date +%H` if [ $n -eq 00 ] || [ $n -eq 12 ] then #通过for循环，以find命令作为遍历条件，将目标目录下的所有文件进行遍历并做相应操作 for i in `find /data/log/ -type f` do true \u0026gt; $i done else for i in `find /data/log/ -type f` do du -sh $i \u0026gt;\u0026gt; $logfile done fi 3、检测网卡流量，并按规定格式记录在日志中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #!/bin/bash ####################################################### #检测网卡流量，并按规定格式记录在日志中#规定一分钟记录一次 #日志格式如下所示: #2019-08-12 20:40 #ens33 input: 1234bps #ens33 output: 1235bps ######################################################3 while : do #设置语言为英文，保障输出结果是英文，否则会出现bug LANG=en logfile=/tmp/`date +%d`.log #将下面执行的命令结果输出重定向到logfile日志中 exec \u0026gt;\u0026gt; $logfile date +\u0026#34;%F %H:%M\u0026#34; #sar命令统计的流量单位为kb/s，日志格式为bps，因此要*1000*8 sar -n DEV 1 59|grep Average|grep ens33|awk \u0026#39;{print $2,\u0026#34;\\t\u0026#34;,\u0026#34;input:\u0026#34;,\u0026#34;\\t\u0026#34;,$5*1000*8,\u0026#34;bps\u0026#34;,\u0026#34;\\n\u0026#34;,$2,\u0026#34;\\t\u0026#34;,\u0026#34;output:\u0026#34;,\u0026#34;\\t\u0026#34;,$6*1000*8,\u0026#34;bps\u0026#34;}\u0026#39; echo \u0026#34;####################\u0026#34; #因为执行sar命令需要59秒，因此不需要sleep done 4、计算文档每行出现的数字个数，并计算整个文档的数字总数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/bin/bash ######################################################### #计算文档每行出现的数字个数，并计算整个文档的数字总数 ######################################################## #使用awk只输出文档行数（截取第一段） n=`wc -l a.txt|awk \u0026#39;{print $1}\u0026#39;` sum=0 #文档中每一行可能存在空格，因此不能直接用文档内容进行遍历 for i in `seq 1 $n`do #输出的行用变量表示时，需要用双引号 line=`sed -n \u0026#34;$i\u0026#34;p a.txt`#wc -L选项，统计最长行的长度 n_n=`echo $line|sed s\u0026#39;/[^0-9]//\u0026#39;g|wc -L` echo $n_nsum=$[$sum+$n_n] done echo \u0026#34;sum:$sum\u0026#34; 杀死所有脚本（Shell编程实战案例分享（PDF版））\n1 2 3 4 5 6 #!/bin/bash ################################################################ #有一些脚本加入到了cron之中，存在脚本尚未运行完毕又有新任务需要执行的情况， #导致系统负载升高，因此可通过编写脚本，筛选出影响负载的进程一次性全部杀死。 ################################################################ ps aux|grep 指定进程名|grep -v grep|awk \u0026#39;{print $2}\u0026#39;|xargs kill -9 5、从 FTP 服务器下载文件 1 2 3 4 5 6 7 8 9 10 11 12 13 #!/bin/bash if [ $# -ne 1 ]; then echo \u0026#34;Usage: $0 filename\u0026#34; fi dir=$(dirname $1) file=$(basename $1) ftp -n -v \u0026lt;\u0026lt; EOF # -n 自动登录 open 192.168.1.10 # ftp服务器 user admin password binary # 设置ftp传输模式为二进制，避免MD5值不同或.tar.gz压缩包格式错误 cd $dir get \u0026#34;$file\u0026#34; EOF 6、连续输入5个100以内的数字，统计和、最小和最大 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #!/bin/bash COUNT=1 SUM=0 MIN=0 MAX=100 while [ $COUNT -le 5 ]; do read -p \u0026#34;请输入1-10个整数：\u0026#34; INT if [[ ! $INT =~ ^[0-9]+$ ]]; then echo \u0026#34;输入必须是整数！\u0026#34; exit 1 elif [[ $INT -gt 100 ]]; then echo \u0026#34;输入必须是100以内！\u0026#34; exit 1 fi SUM=$(($SUM+$INT)) [ $MIN -lt $INT ] \u0026amp;\u0026amp; MIN=$INT [ $MAX -gt $INT ] \u0026amp;\u0026amp; MAX=$INT let COUNT++ done echo \u0026#34;SUM: $SUM\u0026#34; echo \u0026#34;MIN: $MIN\u0026#34; echo \u0026#34;MAX: $MAX 用户猜数字\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #!/bin/bash # 脚本生成一个 100 以内的随机数,提示用户猜数字,根据用户的输入,提示用户猜对了, # 猜小了或猜大了,直至用户猜对脚本结束。 # RANDOM 为系统自带的系统变量,值为 0‐32767的随机数 # 使用取余算法将随机数变为 1‐100 的随机数num=$[RANDOM%100+1]echo \u0026#34;$num\u0026#34; # 使用 read 提示用户猜数字 # 使用 if 判断用户猜数字的大小关系:‐eq(等于),‐ne(不等于),‐gt(大于),‐ge(大于等于), # ‐lt(小于),‐le(小于等于) while : do read -p \u0026#34;计算机生成了一个 1‐100 的随机数,你猜: \u0026#34; cai if [ $cai -eq $num ] then echo \u0026#34;恭喜,猜对了\u0026#34; exit elif [ $cai -gt $num ] then echo \u0026#34;Oops,猜大了\u0026#34; else echo \u0026#34;Oops,猜小了\u0026#34; fi done 7、监测 Nginx 访问日志 502 情况，并做相应动作bash 假设服务器环境为 lnmp，近期访问经常出现 502 现象，且 502 错误在重启 php-fpm 服务后消失，因此需要编写监控脚本，一旦出现 502，则自动重启 php-fpm 服务。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #场景： #1.访问日志文件的路径：/data/log/access.log #2.脚本死循环，每10秒检测一次，10秒的日志条数为300条，出现502的比例不低于10%（30条）则需要重启php-fpm服务 #3.重启命令为：/etc/init.d/php-fpm restart #!/bin/bash ########################################################### #监测Nginx访问日志502情况，并做相应动作 ########################################################### log=/data/log/access.log N=30 #设定阈值 while :do #查看访问日志的最新300条，并统计502的次数 err=`tail -n 300 $log |grep -c \u0026#39;502\u0026#34; \u0026#39;` if [ $err -ge $N ] then /etc/init.d/php-fpm restart 2\u0026gt; /dev/null #设定60s延迟防止脚本bug导致无限重启php-fpm服务 sleep 60 fi sleep 10 done 8、将结果分别赋值给变量 应用场景：希望将执行结果或者位置参数赋值给变量，以便后续使用。\n方法1： 1 2 3 for i in $(echo \u0026#34;4 5 6\u0026#34;); do eval a$i=$idone echo $a4 $a5 $a6 方法2：将位置参数192.168.1.1{1,2}拆分为到每个变量 1 2 3 4 5 6 7 8 num=0 for i in $(eval echo $*);do #eval将{1,2}分解为1 2 let num+=1 eval node${num}=\u0026#34;$i\u0026#34; done echo $node1 $node2 $node3 # bash a.sh 192.168.1.1{1,2} 192.168.1.11 192.168.1.12 方法3： 1 2 3 4 arr=(4 5 6) INDEX1=$(echo ${arr[0]}) INDEX2=$(echo ${arr[1]}) INDEX3=$(echo ${arr[2]}) 9、批量修改文件名 示例：\n1 2 3 # touch article_{1..3}.html # lsarticle_1.html article_2.html article_3.html 目的：把article改为bbs 方法1： 1 2 3 4 for file in $(ls *html); do mv $file bbs_${file#*_} # mv $file $(echo $file |sed -r \u0026#39;s/.*(_.*)/bbs\\1/\u0026#39;) # mv $file $(echo $file |echo bbs_$(cut -d_ -f2) 方法2： 1 2 for file in $(find . -maxdepth 1 -name \u0026#34;*html\u0026#34;); do mv $file bbs_${file#*_}done 方法3： 1 2 # rename article bbs *.html 把一个文档前五行中包含字母的行删掉，同时删除6到10行包含的所有字母 1）准备测试文件，文件名为2.txt\n1 2 3 4 5 6 7 8 9 10 11 12 13 第1行1234567不包含字母 第2行56789BBBBBB 第3行67890CCCCCCCC 第4行78asdfDDDDDDDDD 第5行123456EEEEEEEE 第6行1234567ASDF 第7行56789ASDF 第8行67890ASDF 第9行78asdfADSF 第10行123456AAAA 第11行67890ASDF 第12行78asdfADSF 第13行123456AAAA 2）脚本如下：\n1 2 3 4 5 6 7 8 #!/bin/bash ############################################################### 把一个文档前五行中包含字母的行删掉，同时删除6到10行包含的所有字母 ############################################################## sed -n \u0026#39;1,5\u0026#39;p 2.txt |sed \u0026#39;/[a-zA-Z]/\u0026#39;d sed -n \u0026#39;6,10\u0026#39;p 2.txt |sed s\u0026#39;/[a-zA-Z]//\u0026#39;g sed -n \u0026#39;11,$\u0026#39;p 2.txt #最终结果只是在屏幕上打印结果，如果想直接更改文件，可将输出结果写入临时文件中，再替换2.txt或者使用-i选项 10、统计当前目录中以.html结尾的文件总大 方法1： 1 # find . -name \u0026#34;*.html\u0026#34; -exec du -k {} \\; |awk \u0026#39;{sum+=$1}END{print sum}\u0026#39; 方法2： 1 2 3 4 for size in $(ls -l *.html |awk \u0026#39;{print $5}\u0026#39;); do sum=$(($sum+$size)) done echo $sum 11、扫描主机端口状态 1 2 3 4 5 6 7 8 9 10 #!/bin/bash HOST=$1 PORT=\u0026#34;22 25 80 8080\u0026#34; for PORT in $PORT; do if echo \u0026amp;\u0026gt;/dev/null \u0026gt; /dev/tcp/$HOST/$PORT; then echo \u0026#34;$PORT open\u0026#34; else echo \u0026#34;$PORT close\u0026#34; fi done 用 shell 打印示例语句中字母数小于6的单词\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #示例语句： #Bash also interprets a number of multi-character options. #!/bin/bash ############################################################## #shell打印示例语句中字母数小于6的单词 ############################################################## for s in Bash also interprets a number of multi-character options. do n=`echo $s|wc -c` if [ $n -lt 6 ] then echo $s fi done 12、输入数字运行相应命令 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #!/bin/bash ############################################################## #输入数字运行相应命令 ############################################################## echo \u0026#34;*cmd menu* 1-date 2-ls 3-who 4-pwd 0-exit \u0026#34; while : do #捕获用户键入值 read -p \u0026#34;please input number :\u0026#34; n n1=`echo $n|sed s\u0026#39;/[0-9]//\u0026#39;g` #空输入检测 if [ -z \u0026#34;$n\u0026#34; ] then continue fi #非数字输入检测 if [ -n \u0026#34;$n1\u0026#34; ] then exit 0 fi break done case $n in 1) date ;; 2) ls ;; 3) who ;; 4) pwd ;; 0) break ;; #输入数字非1-4的提示 *) echo \u0026#34;please input number is [1-4]\u0026#34; esac 13、Expect 实现 SSH 免交互执行命令 Expect是一个自动交互式应用程序的工具，如telnet，ftp，passwd等。需先安装expect软件包。\n方法1：EOF标准输出作为expect标准输入 1 2 3 4 5 6 7 8 9 10 #!/bin/bash USER=root PASS=123.com IP=192.168.1.120 expect \u0026lt;\u0026lt; EOFset timeout 30spawn ssh $USER@$IP expect { \u0026#34;(yes/no)\u0026#34; {send \u0026#34;yes\\r\u0026#34;; exp_continue} \u0026#34;password:\u0026#34; {send \u0026#34;$PASS\\r\u0026#34;} } expect \u0026#34;$USER@*\u0026#34; {send \u0026#34;$1\\r\u0026#34;} expect \u0026#34;$USER@*\u0026#34; {send \u0026#34;exit\\r\u0026#34;} expect eof EOF 方法2： 1 2 3 4 5 6 7 8 9 10 11 #!/bin/bash USER=root PASS=123.com IP=192.168.1.120 expect -c \u0026#34; spawn ssh $USER@$IP expect { \\\u0026#34;(yes/no)\\\u0026#34; {send \\\u0026#34;yes\\r\\\u0026#34;; exp_continue} \\\u0026#34;password:\\\u0026#34; {send \\\u0026#34;$PASS\\r\\\u0026#34;; exp_continue} \\\u0026#34;$USER@*\\\u0026#34; {send \\\u0026#34;df -h\\r exit\\r\\\u0026#34;; exp_continue} }\u0026#34; 方法3：将expect脚本独立出来 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 登录脚本： # cat login.exp #!/usr/bin/expect set ip [lindex $argv 0] set user [lindex $argv 1] set passwd [lindex $argv 2] set cmd [lindex $argv 3] if { $argc != 4 } { puts \u0026#34;Usage: expect login.exp ip user passwd\u0026#34; exit 1 } set timeout 30 spawn ssh $user@$ip expect { \u0026#34;(yes/no)\u0026#34; {send \u0026#34;yes\\r\u0026#34;; exp_continue} \u0026#34;password:\u0026#34; {send \u0026#34;$passwd\\r\u0026#34;} } expect \u0026#34;$user@*\u0026#34; {send \u0026#34;$cmd\\r\u0026#34;} expect \u0026#34;$user@*\u0026#34; {send \u0026#34;exit\\r\u0026#34;} expect eof 执行命令脚本：写个循环可以批量操作多台服务器\n1 2 3 4 5 6 7 8 #!/bin/bash HOST_INFO=user_info.txt for ip in $(awk \u0026#39;{print $1}\u0026#39; $HOST_INFO) do user=$(awk -v I=\u0026#34;$ip\u0026#34; \u0026#39;I==$1{print $2}\u0026#39; $HOST_INFO) pass=$(awk -v I=\u0026#34;$ip\u0026#34; \u0026#39;I==$1{print $3}\u0026#39; $HOST_INFO) expect login.exp $ip $user $pass $1 done Linux主机SSH连接信息：\n1 2 # cat user_info.txt 192.168.1.120 root 123456 创建10个用户，并分别设置密码，密码要求10位且包含大小写字母以及数字，最后需要把每个用户的密码存在指定文件中\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #!/bin/bash ############################################################## #创建10个用户，并分别设置密码，密码要求10位且包含大小写字母以及数字 #最后需要把每个用户的密码存在指定文件中#前提条件：安装mkpasswd命令 ############################################################## #生成10个用户的序列（00-09） for u in `seq -w 0 09`do #创建用户 useradd user_$u #生成密码 p=`mkpasswd -s 0 -l 10` #从标准输入中读取密码进行修改（不安全） echo $p|passwd --stdin user_$u #常规修改密码 echo -e \u0026#34;$p\\n$p\u0026#34;|passwd user_$u #将创建的用户及对应的密码记录到日志文件中 echo \u0026#34;user_$u $p\u0026#34; \u0026gt;\u0026gt; /tmp/userpassworddone 14、监控 httpd 的进程数，根据监控情况做相应处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #!/bin/bash ############################################################################################################################### #需求： #1.每隔10s监控httpd的进程数，若进程数大于等于500，则自动重启Apache服务，并检测服务是否重启成功 #2.若未成功则需要再次启动，若重启5次依旧没有成功，则向管理员发送告警邮件，并退出检测 #3.如果启动成功，则等待1分钟后再次检测httpd进程数，若进程数正常，则恢复正常检测（10s一次），否则放弃重启并向管理员发送告警邮件，并退出检测 ############################################################################################################################### #计数器函数 check_service() { j=0 for i in `seq 1 5` do #重启Apache的命令 /usr/local/apache2/bin/apachectl restart 2\u0026gt; /var/log/httpderr.log #判断服务是否重启成功 if [ $? -eq 0 ] then break else j=$[$j+1] fi #判断服务是否已尝试重启5次 if [ $j -eq 5 ] then mail.py exit fi done }while :do n=`pgrep -l httpd|wc -l` #判断httpd服务进程数是否超过500 if [ $n -gt 500 ] then /usr/local/apache2/bin/apachectl restart if [ $? -ne 0 ] then check_service else sleep 60 n2=`pgrep -l httpd|wc -l` #判断重启后是否依旧超过500 if [ $n2 -gt 500 ] then mail.py exit fi fi fi #每隔10s检测一次 sleep 10done 15、批量修改服务器用户密码 Linux主机SSH连接信息：旧密码\n1 2 3 4 # cat old_pass.txt 192.168.18.217 root 123456 22 192.168.18.218 root 123456 22 内容格式：IP User Password Port SSH远程修改密码脚本：新密码随机生成\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #!/bin/bash OLD_INFO=old_pass.txt NEW_INFO=new_pass.txt for IP in $(awk \u0026#39;/^[^#]/{print $1}\u0026#39; $OLD_INFO); do USER=$(awk -v I=$IP \u0026#39;I==$1{print $2}\u0026#39; $OLD_INFO) PASS=$(awk -v I=$IP \u0026#39;I==$1{print $3}\u0026#39; $OLD_INFO) PORT=$(awk -v I=$IP \u0026#39;I==$1{print $4}\u0026#39; $OLD_INFO) NEW_PASS=$(mkpasswd -l 8) # 随机密码 echo \u0026#34;$IP $USER $NEW_PASS $PORT\u0026#34; \u0026gt;\u0026gt; $NEW_INFO expect -c \u0026#34; spawn ssh -p$PORT $USER@$IP set timeout 2 expect { \\\u0026#34;(yes/no)\\\u0026#34; {send \\\u0026#34;yes\\r\\\u0026#34;;exp_continue} \\\u0026#34;password:\\\u0026#34; {send \\\u0026#34;$PASS\\r\\\u0026#34;;exp_continue} \\\u0026#34;$USER@*\\\u0026#34; {send \\\u0026#34;echo \\\u0026#39;$NEW_PASS\\\u0026#39; |passwd --stdin $USER\\r exit\\r\\\u0026#34;;exp_continue} }\u0026#34; done 生成新密码文件：\n1 2 3 # cat new_pass.txt 192.168.18.217 root n8wX3mU% 22 192.168.18.218 root c87;ZnnL 22 16、iptables 自动屏蔽访问网站频繁的IP 场景：恶意访问,安全防范\n1）屏蔽每分钟访问超过200的IP 方法1：根据访问日志（Nginx为例） 1 2 3 4 5 6 7 #!/bin/bash DATE=$(date +%d/%b/%Y:%H:%M) ABNORMAL_IP=$(tail -n5000 access.log |grep $DATE |awk \u0026#39;{a[$1]++}END{for(i in a)if(a[i]\u0026gt;100)print i}\u0026#39;) #先tail防止文件过大，读取慢，数字可调整每分钟最大的访问量。awk不能直接过滤日志，因为包含特殊字符。 for IP in $ABNORMAL_IP; do if [ $(iptables -vnL |grep -c \u0026#34;$IP\u0026#34;) -eq 0 ]; then iptables -I INPUT -s $IP -j DROP fidone 方法2：通过TCP建立的连接 1 2 3 4 5 6 7 8 #!/bin/bash ABNORMAL_IP=$(netstat -an |awk \u0026#39;$4~/:80$/ \u0026amp;\u0026amp; $6~/ESTABLISHED/{gsub(/:[0-9]+/,\u0026#34;\u0026#34;,$5);{a[$5]++}}END{for(i in a)if(a[i]\u0026gt;100)print i}\u0026#39;) #gsub是将第五列（客户端IP）的冒号和端口去掉 for IP in $ABNORMAL_IP; do if [ $(iptables -vnL |grep -c \u0026#34;$IP\u0026#34;) -eq 0 ]; then iptables -I INPUT -s $IP -j DROP fi done 2）屏蔽每分钟SSH尝试登录超过10次的IP 方法1：通过lastb获取登录状态: 1 2 3 4 5 #!/bin/bash DATE=$(date +\u0026#34;%a %b %e %H:%M\u0026#34;) #星期月天时分 %e单数字时显示7，而%d显示07 ABNORMAL_IP=$(lastb |grep \u0026#34;$DATE\u0026#34; |awk \u0026#39;{a[$3]++}END{for(i in a)if(a[i]\u0026gt;10)print i}\u0026#39;)for IP in $ABNORMAL_IP; do if [ $(iptables -vnL |grep -c \u0026#34;$IP\u0026#34;) -eq 0 ]; then iptables -I INPUT -s $IP -j DROP fidone 方法2：通过日志获取登录状态 1 2 3 4 5 6 7 8 9 #!/bin/bash DATE=$(date +\u0026#34;%b %d %H\u0026#34;) ABNORMAL_IP=\u0026#34;$(tail -n10000 /var/log/auth.log |grep \u0026#34;$DATE\u0026#34; |awk \u0026#39;/Failed/{a[$(NF-3)]++}END{for(i in a)if(a[i]\u0026gt;5)print i}\u0026#39;)\u0026#34; for IP in $ABNORMAL_IP; do if [ $(iptables -vnL |grep -c \u0026#34;$IP\u0026#34;) -eq 0 ]; then iptables -A INPUT -s $IP -j DROP echo \u0026#34;$(date +\u0026#34;%F %T\u0026#34;) - iptables -A INPUT -s $IP -j DROP\u0026#34; \u0026gt;\u0026gt;~/ssh-login-limit.log fi done 17、根据web访问日志，封禁请求量异常的IP，如IP在半小时后恢复正常，则解除封禁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 #!/bin/bash #################################################################################### #根据web访问日志，封禁请求量异常的IP，如IP在半小时后恢复正常，则解除封禁 #################################################################################### logfile=/data/log/access.log #显示一分钟前的小时和分钟 d1=`date -d \u0026#34;-1 minute\u0026#34; +%H%M` d2=`date +%M` ipt=/sbin/iptables ips=/tmp/ips.txt block() { #将一分钟前的日志全部过滤出来并提取IP以及统计访问次数 grep \u0026#39;$d1:\u0026#39; $logfile|awk \u0026#39;{print $1}\u0026#39;|sort -n|uniq -c|sort -n \u0026gt; $ips #利用for循环将次数超过100的IP依次遍历出来并予以封禁 for i in `awk \u0026#39;$1\u0026gt;100 {print $2}\u0026#39; $ips` do $ipt -I INPUT -p tcp --dport 80 -s $i -j REJECT echo \u0026#34;`date +%F-%T` $i\u0026#34; \u0026gt;\u0026gt; /tmp/badip.log done } unblock() { #将封禁后所产生的pkts数量小于10的IP依次遍历予以解封 for a in `$ipt -nvL INPUT --line-numbers |grep \u0026#39;0.0.0.0/0\u0026#39;|awk \u0026#39;$2\u0026lt;10 {print $1}\u0026#39;|sort -nr` do $ipt -D INPUT $a done $ipt -Z } #当时间在00分以及30分时执行解封函数 if [ $d2 -eq \u0026#34;00\u0026#34; ] || [ $d2 -eq \u0026#34;30\u0026#34; ] then #要先解再封，因为刚刚封禁时产生的pkts数量很少 unblock block else block fi 18、判断用户输入的是否为IP地址 方法1: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #!/bin/bash function check_ip(){ IP=$1 VALID_CHECK=$(echo $IP|awk -F. \u0026#39;$1\u0026lt; =255\u0026amp;\u0026amp;$2\u0026lt;=255\u0026amp;\u0026amp;$3\u0026lt;=255\u0026amp;\u0026amp;$4\u0026lt;=255{print \u0026#34;yes\u0026#34;}\u0026#39;) if echo $IP|grep -E \u0026#34;^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$\u0026#34;\u0026gt;/dev/null; then if [ $VALID_CHECK == \u0026#34;yes\u0026#34; ]; then echo \u0026#34;$IP available.\u0026#34; else echo \u0026#34;$IP not available!\u0026#34; fi else echo \u0026#34;Format error!\u0026#34; fi } check_ip 192.168.1.1 check_ip 256.1.1.1 方法2： 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/bin/bash function check_ip(){ IP=$1 if [[ $IP =~ ^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$ ]]; then FIELD1=$(echo $IP|cut -d. -f1) FIELD2=$(echo $IP|cut -d. -f2) FIELD3=$(echo $IP|cut -d. -f3) FIELD4=$(echo $IP|cut -d. -f4) if [ $FIELD1 -le 255 -a $FIELD2 -le 255 -a $FIELD3 -le 255 -a $FIELD4 -le 255 ]; then echo \u0026#34;$IP available.\u0026#34; else echo \u0026#34;$IP not available!\u0026#34; fi else echo \u0026#34;Format error!\u0026#34; fi } check_ip 192.168.1.1 check_ip 256.1.1.1 增加版： 加个死循环，如果IP可用就退出，不可用提示继续输入，并使用awk判断。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #!/bin/bash function check_ip(){ local IP=$1 VALID_CHECK=$(echo $IP|awk -F. \u0026#39;$1\u0026lt; =255\u0026amp;\u0026amp;$2\u0026lt;=255\u0026amp;\u0026amp;$3\u0026lt;=255\u0026amp;\u0026amp;$4\u0026lt;=255{print \u0026#34;yes\u0026#34;}\u0026#39;) if echo $IP|grep -E \u0026#34;^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$\u0026#34; \u0026gt;/dev/null; then if [ $VALID_CHECK == \u0026#34;yes\u0026#34; ]; then return 0 else echo \u0026#34;$IP not available!\u0026#34; return 1 fi else echo \u0026#34;Format error! Please input again.\u0026#34; return 1 fi } while true; do read -p \u0026#34;Please enter IP: \u0026#34; IP check_ip $IP [ $? -eq 0 ] \u0026amp;\u0026amp; break || continue done 19、判断用户输入的是否为数字 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 方法1： #!/bin/bash if [[ $1 =~ ^[0-9]+$ ]]; then echo \u0026#34;Is Number.\u0026#34; else echo \u0026#34;No Number.\u0026#34; fi 方法2： #!/bin/bash if [ $1 -gt 0 ] 2\u0026gt;/dev/null; then echo \u0026#34;Is Number.\u0026#34; else echo \u0026#34;No Number.\u0026#34; fi 方法3： #!/bin/bash echo $1 |awk \u0026#39;{print $0~/^[0-9]+$/?\u0026#34;Is Number.\u0026#34;:\u0026#34;No Number.\u0026#34;}\u0026#39; #三目运算符 12.14 找出包含关键字的文件 DIR=$1 KEY=$2 for FILE in $(find $DIR -type f); do if grep $KEY $FILE \u0026amp;\u0026gt;/dev/null; then echo \u0026#34;--\u0026gt; $FILE\u0026#34; fi done 20、监控目录，将新创建的文件名追加到日志中 场景：记录目录下文件操作。需先安装inotify-tools软件包。\n1 2 3 4 5 6 #!/bin/bash MON_DIR=/opt inotifywait -mq --format %f -e create $MON_DIR |\\ while read files; do echo $files \u0026gt;\u0026gt; test.log done 21、给用户提供多个网卡选择 场景：服务器多个网卡时，获取指定网卡，例如网卡流量\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #!/bin/bash function local_nic() { local NUM ARRAY_LENGTH NUM=0 for NIC_NAME in $(ls /sys/class/net|grep -vE \u0026#34;lo|docker0\u0026#34;); do NIC_IP=$(ifconfig $NIC_NAME |awk -F\u0026#39;[: ]+\u0026#39; \u0026#39;/inet addr/{print $4}\u0026#39;) if [ -n \u0026#34;$NIC_IP\u0026#34; ]; then NIC_IP_ARRAY[$NUM]=\u0026#34;$NIC_NAME:$NIC_IP\u0026#34; #将网卡名和对应IP放到数组 let NUM++ fi done ARRAY_LENGTH=${#NIC_IP_ARRAY[*]} if [ $ARRAY_LENGTH -eq 1 ]; then #如果数组里面只有一条记录说明就一个网卡 NIC=${NIC_IP_ARRAY[0]%:*} return 0 elif [ $ARRAY_LENGTH -eq 0 ]; then #如果没有记录说明没有网卡 echo \u0026#34;No available network card!\u0026#34; exit 1 else #如果有多条记录则提醒输入选择 for NIC in ${NIC_IP_ARRAY[*]}; do echo $NIC done while true; do read -p \u0026#34;Please enter local use to network card name: \u0026#34; INPUT_NIC_NAME COUNT=0 for NIC in ${NIC_IP_ARRAY[*]}; do NIC_NAME=${NIC%:*} if [ $NIC_NAME == \u0026#34;$INPUT_NIC_NAME\u0026#34; ]; then NIC=${NIC_IP_ARRAY[$COUNT]%:*} return 0 else COUNT+=1 fi done echo \u0026#34;Not match! Please input again.\u0026#34; done fi } local_nic 22、MySQL数据库备份 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #!/bin/bash DATE=$(date +%F_%H-%M-%S) HOST=192.168.1.120 DB=test USER=bak PASS=123456 MAIL=\u0026#34;zhangsan@example.com lisi@example.com\u0026#34; BACKUP_DIR=/data/db_backup SQL_FILE=${DB}_full_$DATE.sql BAK_FILE=${DB}_full_$DATE.zip cd $BACKUP_DIR if mysqldump -h$HOST -u$USER -p$PASS --single-transaction --routines --triggers -B $DB \u0026gt; $SQL_FILE; then zip $BAK_FILE $SQL_FILE \u0026amp;\u0026amp; rm -f $SQL_FILE if [ ! -s $BAK_FILE ]; then echo \u0026#34;$DATE 内容\u0026#34; | mail -s \u0026#34;主题\u0026#34; $MAIL fi else echo \u0026#34;$DATE 内容\u0026#34; | mail -s \u0026#34;主题\u0026#34; $MAIL fi find $BACKUP_DIR -name \u0026#39;*.zip\u0026#39; -ctime +14 -exec rm {} \\; 23、系统安全检测（centos） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 #!/bin/sh\u0026#34; echo \u0026#34;#######################################「OS系统信息」##########################################\u0026#34; OS_TYPE=`uname` OS_Number=`dmidecode -t system |grep \u0026#39;Serial Number\u0026#39;|awk \u0026#39;{print $3}\u0026#39;|awk -F, \u0026#39;{print $1}\u0026#39;` OS_VERSION=`cat /etc/redhat-release` OS_IPADDR=`ifconfig ens160|grep \u0026#34;inet\u0026#34; |awk \u0026#39;{print $2}\u0026#39; | sed -n \u0026#39;1p\u0026#39;` OS_KERNER=`uname -a|awk \u0026#39;{print $3}\u0026#39;` OS_NOWTIME=`date +%F_%T` OS_RUN_TIME=`uptime |awk \u0026#39;{print $3,$4}\u0026#39;|awk -F, \u0026#39;{print $1}\u0026#39;` OS_LASTREBOOT_TIME=`who -b|awk \u0026#39;{print $2,$3}\u0026#39;` OS_HOSTNAME=`hostname` echo \u0026#34; 主机类型: $OS_TYPE\u0026#34; echo \u0026#34; 主机序列号: $OS_Number\u0026#34; echo \u0026#34; 系统版本: $OS_VERSION\u0026#34; echo \u0026#34; 系统IP地址: $OS_IPADDR\u0026#34; echo \u0026#34; 内核版本: $OS_KERNER\u0026#34; echo \u0026#34; 系统时间: $OS_NOWTIME\u0026#34; echo \u0026#34; 运行时间: $OS_RUN_TIME\u0026#34; echo \u0026#34; 最后重启时间: $OS_LASTREBOOT_TIME\u0026#34; echo \u0026#34; 主机名称: $OS_HOSTNAME\u0026#34; echo \u0026#34; SELinux：` /usr/sbin/sestatus | grep \u0026#39;SELinux status:\u0026#39; | awk \u0026#39;{print $3}\u0026#39;`\u0026#34; echo \u0026#34; 语言环境：`echo $LANG`\u0026#34; echo \u0026#34;#######################################「OS资源信息」##########################################\u0026#34; OS_CPU_PRO=`cat /proc/cpuinfo |grep \u0026#34;processor\u0026#34; | wc -l` OS_CPU_COR=`cat /proc/cpuinfo| grep \u0026#34;cpu cores\u0026#34;| uniq |awk {\u0026#39;print $4\u0026#39;}` OS_CPU_TYPE=`grep \u0026#34;model name\u0026#34; /proc/cpuinfo | awk -F \u0026#39;: \u0026#39; \u0026#39;{print $2}\u0026#39; | sort | uniq` echo \u0026#34; CPU总个数: $OS_CPU_PRO\u0026#34; echo \u0026#34; CPU总核数: $OS_CPU_COR\u0026#34; echo \u0026#34; CPU型 号： $OS_CPU_TYPE\u0026#34; OS_SWAP_S=`free|grep Swap|awk {\u0026#39;print $2\u0026#39;}` OS_PARTS=(`df -T|sed 1d|egrep -v \u0026#34;tmpfs|sr0\u0026#34;|awk {\u0026#39;print $3\u0026#39;}`) OS_MEM_TAL=`free -m|grep Mem|awk \u0026#39;{print $2}\u0026#39;` OS_MEM_FREE=`free -m|grep Mem|awk \u0026#39;{print $7}\u0026#39;` echo \u0026#34; 内存总量: ${OS_MEM_TAL}MB\u0026#34; echo \u0026#34; 内存余量: ${OS_MEM_FREE}MB\u0026#34; OS_DISKS=0 OS_SWAP=`free|grep Swap|awk {\u0026#39;print $2\u0026#39;}` OS_PARTS=(`df -T|sed 1d|egrep -v \u0026#34;tmpfs|sr0\u0026#34;|awk {\u0026#39;print $3\u0026#39;}`) for ((i=0;i\u0026lt;`echo ${#OS_PARTS[*]}`;i++)) do OS_DISKS=`expr $OS_DISKS + ${OS_PARTS[$i]}` done ((OS_DISKS=\\($OS_DISKS+$OS_SWAP\\)/1024/1024)) echo \u0026#34; 磁盘总量: ${OS_DISKS}GB\u0026#34; OS_DISKS=0 OS_SWAP=`free|grep Swap|awk \u0026#39;{print $4}\u0026#39;` OS_PARTS=(`df -T|sed 1d|egrep -v \u0026#34;tmpfs|sr0\u0026#34;|awk \u0026#39;{print $5}\u0026#39;`) for ((i=0;i\u0026lt;`echo ${#OS_PARTS[*]}`;i++)) do OS_DISKS=`expr $OS_DISKS + ${OS_PARTS[$i]}` done ((freetotal=\\($OS_DISKS+$OS_SWAP\\)/1024/1024)) echo \u0026#34; 磁盘余量： ${freetotal}GB\u0026#34; echo \u0026#34;#######################################「OS网络监测」##########################################\u0026#34; echo `ip a | grep eno | awk \u0026#34;NR==2\u0026#34; | awk \u0026#39;{print $NF,\u0026#34;:\u0026#34;,$2}\u0026#39;` echo \u0026#34;网关：`ip route | awk \u0026#39;NR==1\u0026#39;| awk \u0026#39;{print $3}\u0026#39;`\u0026#34; echo \u0026#34;DNS: `cat /etc/resolv.conf | grep \u0026#34;nameserver\u0026#34; | awk \u0026#39;{print $2}\u0026#39;`\u0026#34; ping -c 4 www.baidu.com \u0026gt; /dev/null if [ $? -eq 0 ];then echo \u0026#34;网络连接状态：正常\u0026#34; else echo \u0026#34;网络连接状态：失败\u0026#34; fi echo echo \u0026#34;#######################################「OS安全检查」##########################################\u0026#34; echo \u0026#34;用户登陆信息：`last | grep \u0026#34;still logged in\u0026#34; | awk \u0026#39;{print $1}\u0026#39;| sort | uniq`\u0026#34; md5sum -c --quiet /etc/passwd \u0026gt; /dev/null 2\u0026amp;\u0026gt;1 if [ $? -eq 0 ];then echo \u0026#34;文件未被篡改\u0026#34; else echo \u0026#34;文件被篡改\u0026#34; fi Shell 脚本参数传递时有 \\r 换行符问题\n1 sed -i \u0026#34;s/\\r//\u0026#34; allSyncTask.sh 24、打开进程，并判断进程数量 1 2 3 4 5 6 7 #!/bin/bash ffmpegPid = $( ps -ef | grep -E \u0026#39;ffmpeg.*$1$2$3\u0026#39; | grep -v \u0026#39;grep\u0026#39; | awk \u0026#39;{print $2}\u0026#39;) if [ -z \u0026#34;$ffmpegPid\u0026#34; ] then nohup ffmpeg -re -rtsp_transport tcp -i \u0026#34;rtsp://ip:port/dss/monitor/params?cameraid=$1%24$2\u0026amp;substream=$3\u0026#34; -vcodec libx264 -vprofile baseline -acodec aac -ar 44100 -strict -2 -ac 1 -f flv -s 1280x720 -q 10 $4 \u0026gt; /root/ffmpeg$1$2$3.log 2\u0026gt;\u0026amp;1 \u0026amp; ps -ef | grep -E \u0026#39;ffmpeg.*$4*\u0026#39; | grep -v \u0026#39;grep\u0026#39; | awk \u0026#39;{print $2}\u0026#39; fi 关闭进程 1 2 3 #关闭进程 #!/bin/bash ps -ef | grep -E \u0026#39;ffmpeg.*$1*\u0026#39; | grep -v \u0026#39;grep\u0026#39; | awk \u0026#39;{print $2}\u0026#39; | xargs kill 25、java jar包启动-剔除Pom中依赖 1 2 3 4 5 6 7 8 9 10 #!/bin/bash pid=$(ps -ef | grep java | grep -E \u0026#39;*rtsptortmp.*\u0026#39; | awk \u0026#39;{print $2}\u0026#39;) echo \u0026#34;pid = $pid\u0026#34; if [ $pid ];then kill -9 $pid echo \u0026#34;kill the process rtsptortmp pid = $pid\u0026#34; fi nohup java -Dloader.path=/root/rtsptortmplib -jar rtsptortmp-1.0-SNAPSHOT.jar --spring.profiles.active=prod \u0026gt; /root/logs/rtsptortmp.log 2\u0026gt;\u0026amp;1 \u0026amp; tail -f /root/logs/rtsptortmp.log 26、Java jar包通用启动脚本 eg： ./start.sh java.jar\n1 2 3 4 5 6 7 8 9 10 #!/bin/bash pid=$(ps -ef | grep java | grep -E \u0026#39;*$1*\u0026#39; | awk \u0026#39;{print $2}\u0026#39;) echo \u0026#34;pid = $pid\u0026#34; if [ $pid ];then kill -9 $pid echo \u0026#34;kill the process pid = $pid\u0026#34; fi nohup java -jar -Xms256m -Xmx256m $1 --spring.profiles.active=prod \u0026gt; /root/logs/$1.log 2\u0026gt;\u0026amp;1 \u0026amp; tail -f /root/logs/$1.log 27、Jenkins项目打包发布脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #!/bin/bash //传入的war包名称 name=$1 //war包所在目录\tpath=$2 //上传的war包位置\tpath_w=$3\t//如果项目正在运行就杀死进程 if [ -f \u0026#34;$path/$name\u0026#34; ];then echo \u0026#34;delete the file $name\u0026#34; rm -f $path/$name else echo \u0026#34;the file $name is not exist\u0026#34; fi //把jenkins上传的war包拷贝到我们所在目录 cp $path_w/$name $path/ echo \u0026#34;copy the file $name from $path_w to $path\u0026#34; //获取该项目正在运行的pid\tpid=$(ps -ef | grep java | grep $name | awk \u0026#39;{print $2}\u0026#39;) echo \u0026#34;pid = $pid\u0026#34; //如果项目正在运行就杀死进程 if [ $pid ];then kill -9 $pid echo \u0026#34;kill the process $name pid = $pid\u0026#34; else echo \u0026#34;process is not exist\u0026#34; fi //要切换到项目目录下才能在项目目录下生成日志 cd $path //防止被jenkins杀掉进程 BUILD_ID=dontKillMe BUILD_ID=dontKillMe //启动项目 nohup java -server -Xms256m -Xmx512m -jar -Dserver.port=20000 $name \u0026gt;\u0026gt; nohup.out 2\u0026gt;\u0026amp;1 \u0026amp; //判断项目是否启动成功 pid_new=$(ps -ef | grep java | grep $name | awk \u0026#39;{print $2}\u0026#39;) if [ $? -eq 0 ];then echo \u0026#34;this application $name is starting pid_new = $pid_new\u0026#34; else echo \u0026#34;this application $name startup failure\u0026#34; fi echo $! \u0026gt; /var/run/myClass.pid echo \u0026#34;over\u0026#34; 28、检测两台服务器指定目录下的文件一致性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #!/bin/bash ###################################### 检测两台服务器指定目录下的文件一致性 ##################################### #通过对比两台服务器上文件的md5值，达到检测一致性的目的 dir=/data/web b_ip=192.168.88.10 #将指定目录下的文件全部遍历出来并作为md5sum命令的参数，进而得到所有文件的md5值，并写入到指定文件中 find $dir -type f|xargs md5sum \u0026gt; /tmp/md5_a.txt ssh $b_ip \u0026#34;find $dir -type f|xargs md5sum \u0026gt; /tmp/md5_b.txt\u0026#34; scp $b_ip:/tmp/md5_b.txt /tmp #将文件名作为遍历对象进行一一比对 for f in `awk \u0026#39;{print 2} /tmp/md5_a.txt\u0026#39;`do #以a机器为标准，当b机器不存在遍历对象中的文件时直接输出不存在的结果 if grep -qw \u0026#34;$f\u0026#34; /tmp/md5_b.txt then md5_a=`grep -w \u0026#34;$f\u0026#34; /tmp/md5_a.txt|awk \u0026#39;{print 1}\u0026#39;` md5_b=`grep -w \u0026#34;$f\u0026#34; /tmp/md5_b.txt|awk \u0026#39;{print 1}\u0026#39;` #当文件存在时，如果md5值不一致则输出文件改变的结果 if [ $md5_a != $md5_b ]then echo \u0026#34;$f changed.\u0026#34; fi else echo \u0026#34;$f deleted.\u0026#34; fi done 29、定时清空文件内容，定时记录文件大小 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/bin/bash ################################################################# 每小时执行一次脚本（任务计划），当时间为0点或12点时，将目标目录下的所有文件内#容清空，但不删除文件，其他时间则只统计各个文件的大小，一个文件一行，输出到以时#间和日期命名的文件中，需要考虑目标目录下二级、三级等子目录的文件 ################################################################ logfile=/tmp/`date +%H-%F`.log n=`date +%H` if [ $n -eq 00 ] || [ $n -eq 12 ] then #通过for循环，以find命令作为遍历条件，将目标目录下的所有文件进行遍历并做相应操作 for i in `find /data/log/ -type f` do true \u0026gt; $i done else for i in `find /data/log/ -type f` do du -sh $i \u0026gt;\u0026gt; $logfile done fi 30、检测网卡流量，并按规定格式记录在日志中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #!/bin/bash ####################################################### #检测网卡流量，并按规定格式记录在日志中#规定一分钟记录一次 #日志格式如下所示: #2019-08-12 20:40 #ens33 input: 1234bps #ens33 output: 1235bps ######################################################3 while : do #设置语言为英文，保障输出结果是英文，否则会出现bug LANG=en logfile=/tmp/`date +%d`.log #将下面执行的命令结果输出重定向到logfile日志中 exec \u0026gt;\u0026gt; $logfile date +\u0026#34;%F %H:%M\u0026#34; #sar命令统计的流量单位为kb/s，日志格式为bps，因此要*1000*8 sar -n DEV 1 59|grep Average|grep ens33|awk \u0026#39;{print $2,\u0026#34;\\t\u0026#34;,\u0026#34;input:\u0026#34;,\u0026#34;\\t\u0026#34;,$5*1000*8,\u0026#34;bps\u0026#34;,\u0026#34;\\n\u0026#34;,$2,\u0026#34;\\t\u0026#34;,\u0026#34;output:\u0026#34;,\u0026#34;\\t\u0026#34;,$6*1000*8,\u0026#34;bps\u0026#34;}\u0026#39; echo \u0026#34;####################\u0026#34; #因为执行sar命令需要59秒，因此不需要sleep done 31、计算文档每行出现的数字个数，并计算整个文档的数字总数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #!/bin/bash ######################################################### #计算文档每行出现的数字个数，并计算整个文档的数字总数 ######################################################## #使用awk只输出文档行数（截取第一段） n=`wc -l a.txt|awk \u0026#39;{print $1}\u0026#39;` sum=0 #文档中每一行可能存在空格，因此不能直接用文档内容进行遍历 for i in `seq 1 $n`do #输出的行用变量表示时，需要用双引号 line=`sed -n \u0026#34;$i\u0026#34;p a.txt`#wc -L选项，统计最长行的长度 n_n=`echo $line|sed s\u0026#39;/[^0-9]//\u0026#39;g|wc -L` echo $n_nsum=$[$sum+$n_n] done echo \u0026#34;sum:$sum\u0026#34; 32、杀死所有脚本 1 2 3 4 5 6 #!/bin/bash ################################################################ #有一些脚本加入到了cron之中，存在脚本尚未运行完毕又有新任务需要执行的情况， #导致系统负载升高，因此可通过编写脚本，筛选出影响负载的进程一次性全部杀死。 ################################################################ ps aux|grep 指定进程名|grep -v grep|awk \u0026#39;{print $2}\u0026#39;|xargs kill -9 33、批量修改文件名 示例：\n# touch article_{1..3}.html # lsarticle_1.html article_2.html article_3.html 目的：把article改为bbs\n方法1：\n1 2 3 4 for file in $(ls *html); do mv $file bbs_${file#*_} # mv $file $(echo $file |sed -r \u0026#39;s/.*(_.*)/bbs\\1/\u0026#39;) # mv $file $(echo $file |echo bbs_$(cut -d_ -f2) 方法2：\n1 2 for file in $(find . -maxdepth 1 -name \u0026#34;*html\u0026#34;); do mv $file bbs_${file#*_}done 方法3：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # rename article bbs *.html 把一个文档前五行中包含字母的行删掉，同时删除6到10行包含的所有字母 1）准备测试文件，文件名为2.txt 第1行1234567不包含字母 第2行56789BBBBBB 第3行67890CCCCCCCC 第4行78asdfDDDDDDDDD 第5行123456EEEEEEEE 第6行1234567ASDF 第7行56789ASDF 第8行67890ASDF 第9行78asdfADSF 第10行123456AAAA 第11行67890ASDF 第12行78asdfADSF 第13行123456AAAA ","permalink":"https://ktzxy.top/posts/kdsnmmg230/","summary":"Shell 脚本","title":"Shell 脚本"},{"content":"三次握手和四次挥手 三次握手 概念 为什么需要握手：握手的作用就是为了同步一些信息，比如最大滑动窗口\nTCP：是一个可靠的连接，也就是客户端和服务器双方必须感知对方的存在，也就是需要经历一个建立连接的过程\n用三次握手建立TCP连接，连接有三个阶段\n建立连接 数据传输 连接释放 连接的管理就是使连接的建立和释放都能正常地进行，连接阶段过程中要解决以下三个问题\n要使每一方都能确知对方的存在 要允许双方协商一些参数 能够对运输实体分配资源 TCP连接建立过程 TCP建立连接的过程：被称为握手\n① 握手过程其实是发送的TCP报文，在这里面有两个字段，SYN 和 seq\nSYN = 1：表示该报文不能携带数据，但是需要消耗一个SEQ（序号），可以想象成我们对消息编号 seq：TCP的每个字节发送的时候，都有一个序号，主要是为了保证可靠性，比如当我服务器通过TCP报文得到了有N个字节需要接受，但是最后只接受到了N-1个，我们通过序号就知道哪个没有被接收到。 客户端进入SYN_SENT状态，即同步已发送 ② 当服务器接受到我们的握手请求时，会回复一个确认报文\nSYN：表示不携带数据，同时消耗一个SEQ = y（这里的y是任意数字，可以是1,2,3,4） ACK：=1 表示这是一条确定报文 ack：x+1，其中x是刚刚客户端发送过来的 服务器进入SYN_RECVD状态，即同步已收到 ③ 当客户端收到确认报文的时候，客户端需要对这个确认报文进行回复\nACK：=1，表示这是一条确认报文 seq：= x +1， ack：= y+1 经过了这三次握手，两者就进入了连接状态\n通俗的理解 客户端：服务器，我们可以建立连接么？ -\u0026gt; SYN= 1 ， seq = x 服务器：可以啊，我们建立连接吧 ！ -\u0026gt; ACK =1, SYN = 1, seq = y, ack = x+1 客户端：收到，建立连接吧！ -\u0026gt; ACK = 1, SYN = 1，seq = x + 1， ack = y + 1 然后建立TCP连接\n中国机长版三次握手\n为什么是三次握手 四次握手 四次连接有点多余，第三次的时候，我们已经互相进行了连接确认\n但是因为我们无法保证百分百的可靠性\n两次握手： 客户端知道服务器有接收 和 发送的能力，服务器不知道客户端有没有接收数据的能力，因为通过第一次握手，已经知道了客户端能够发送数据，但是能不能接收数据，还是不清楚，因此这个TCP连接是不可靠的。\n为什么不能两次握手就建立连接\n因为超时重传机制的存在\n但客户端发送第一次握手的时候，可能会经历网络拥塞，然后客户端会以为这个连接已经丢失，然后会重新发送一个请求连接的信息到服务器，这次发送的消息很快被服务器接受，然后服务器建立连接就开始建立连接。但是当第一次发送的请求经过一段时间的阻塞后，成功到达服务器，然后服务器又连接连接，而此时客户端是不会理会这次请求的建立，所以服务器一直在等待客户端数据的发送。\n四次挥手 所谓的四次挥手，就是关闭TCP连接的过程，指的是断开一个TPC连接，需要客户端和服务端总共发送4个包，以确定双方连接的断开。\n主要目的：保证TCP连接的全双工连接\n四次挥手示意图 由于TCP连接是全双工的，因此每个方向都必须单独关闭，这个原则是当以防完成它的数据发送任务后，就能发送一个FIN包来终止这个方向的连接。\n收到一个FIN包只意味着这一方向上没有数据流动，一个TCP连接在收到一个FIN后，仍然能发送数据，首先进行关闭的一方将执行主动关闭，而另一方执行被动关闭。\n四次挥手过程 第一次挥手：客户端发送一个FIN包（FIN=1，seq=U）给服务器，用来关闭客户端到服务器端的数据传输，客户端进入FIN_WAIT_1状态（终止等待） 第二次挥手：服务器端收到FIN包后，发送一个ACK包（ACK=1，ack=u+1，在随机产生一个值v 给seq）给客户端，服务器进入了CLOSE_WAIT状态（关闭等待） 第三次挥手：服务器端发送一个FIN包（FIN=1，ACK=1，ack=u+1，在随机产生 一个w值给seq）给客户端，用来关闭服务器到客户端的数据传输，服务端进入了LAST_ACK（最后确定）状态 第四次挥手：客户端接收FIN包，然后进入TIME_WAIT状态，接着发送一个ACK包（ACK=1，seq=u+1, ack = w+1） 给服务端，服务端确定序号，进入CLOSe状态，完成了四次挥手。 挥手中的状态 CLOSED：表示初始状态 ESTABLISHED：表示连接已经连接 FIN_WAIT：状态FIN_WAIT_1和FIN_WAIT_2都表示等待对方的FIN报文，这两个状态的区别是，当主动发送方给对方发送了断开请求时，就进入了FIN_WAIT_1状态，而到被动方在回应后，主动发送方就进入了FIN_WAIT_2。 FIN_WAIT_2：上面已经详细解释了这种状态，实际上FIN_WAIT_2状态下的SOCKET，表示半连接，也即有一方要求close连接，但另外还告诉对方，我暂时还有点数据需要传送给你，稍后再关闭连接 CLOSE_WAIT：这个状态的含义是 表示在等待关闭 LAST_ACK：在被动关闭放发送FIN报文后，最后等待对方的ACK报文，当收到了ACK报文后，就进入了CLOSE状态。 为什么TIME_WAIT状态还需要等待2MSL后才能返回CLOSE 这是因为虽然双方都同意了关闭连接，而且握手的4个报文也都协调和发送完毕，按道理可以直接回到CLOSE状态\n但是因为我们需要假设网络是不可靠的，你无法保证你最后发送的ACK报文是会一定被对方收到，因此处于LAST_ACK状态下的socket可能会因为超时未收到ACK报文，而重发FIN报文，所以这个TIME_WAIT状态的作用就是用来重发可能丢失的报文。\n中国机长版四次挥手 当客户端与服务器在规定的时间内没有得到应答\n会发送报文进行探测，假设没有应答，那么就会关闭连接\n下面是四次挥手的过程\n","permalink":"https://ktzxy.top/posts/e29nar2prh/","summary":"三次握手和四次挥手","title":"三次握手和四次挥手"},{"content":"说明：根据王佩丰Excel24讲学习所作（部分）\n一、认识 1.快速到达数据底部或者顶部 ​\t鼠标点击任意单元格，箭头放在上边框或者下边框，点击两次\n2.冻结窗口 ​\t滚动工作表其余部分时，保持首行/首列不变\n3.填充柄 ​\t选择任意一个或多个填充过的单元格，鼠标点击右下角，（鼠标点击不放）进行下拉；鼠标右击下拉可进行以年/月/工作日填充（日期为例）\n​\t自定义填充\n二、设置单元格格式 1.使用单元格格式工具美化表格 鼠标右击——设置单元格格式\n或者点击\n弹出\n2.单元格数字格式 类型 原格式 转变后的格式 数值 -25636 -25,636 货币 10000 ¥10,000.00 会计专用 1555 $ 1,555.00 日期 39814 2009年1月1日 时间 0.6980556 下午4时45分12秒 百分比 0.11 11.00% 分数 0.1 0 科学计数 1.2E+14 1.2E+14 文本 2422 2422 特殊 25368 二万五千三百六十八 数字格式-自定义格式\n数据分列\n【数据】-【分列】\n三、查找替换定位 替换 替换颜色\n匹配字体替换\n输入要查找的要素，结合通配符“*和?”，可以实现高级模糊查找。在Excel的查找和替换中使用星号“*”可查找任意字符串，例如 查找“IT* ”可找到“IT主站”和“IT论坛”等。使用问号可查找任意单个字符。例如查找“?23” 可找到“023”和“423”等\n注意：结合单元格匹配使用\n当替换项含有*号时，可在前加入~使其不生效\n定位 通过名称框定位单元格及区域位置\n定义名称\n修改批注图形形状\n空白区域插入形状，并在菜单形状右击【添加到快速访问工具栏】 选中批注图形，在左上角【快速访问工具栏】中更改形状 通过定位自动填充单元格\n选中所有区域，用于定位条件筛选 按键= 和 ⬆ 按住Alt，然后回车 四、排序与选择 多条件排序 按颜色排序\n自定义排序\n利用排序插入行\n筛选 一般筛选\n选中首行，点击筛选\n==注意：==若要复制筛选后的数据，还需选择定位条件当中的可见单元格\n条件筛选\n从众多科目中筛选不重复的科目\n筛选部门是一车间且科目是邮寄费的发生额？\n筛选部门是一车间或科目是邮寄费的发生额？\n筛选出一车间或大于3000的二车间或发生额大于10000的数据？\n五、分类汇总与数据有效性 分类汇总工具 使用分类汇总前先排序\n分地区与产品分类统计数量、金额、成本的总计\n先排序（所属区域基础上再给产品类别排序），然后选中所属区域分类汇总\n再选中产品类别分类汇总\n使用分类汇总批量合并内容相同的单元格\n将所属区域进行排序\n将所属区域进行分类汇总，汇总方式为计数\n选中该列单元格，定位到空值，并合并后居中\n取消分类汇总\n选择格式刷\n数据有效性 【数据】【数据验证】\n选中该列，点击数据验证，设置相应条件\n六、数据透视表 【插入】【数据透视表】\n数据透视表选项——\u0026gt;显示——\u0026gt;勾选经典数据透视表布局\n数据透视表中的结合\n批量创建工作表\n选择任意字段，插入数据透视表 将该字段放到报表筛选字段处 再将该字段拖至值字段处 点击数据透视表选项，显示报表筛选页 然后利用Shift选中所有新创的工作表将原有内容替换掉空白 七、认识函数与公式 1、运算符 算术运算符+ - * / % \u0026amp; ^ 比较运算符= \u0026gt; \u0026lt; \u0026gt;= \u0026lt;= \u0026lt;\u0026gt; 2、公式中的比较判断 比较运算符的结果：TRUE FALSE 3、运算符优先级 － 负号 % 百分比 ^ 求幂 * / 乘和除 + - 加和减 \u0026amp; 文本连接 =,\u0026lt;,\u0026gt;,\u0026lt;=,\u0026gt;=,\u0026lt;\u0026gt; 比较 4、单元格引用 相对引用：A1 绝对引用：$A$1 混合引用：$A1 A$1 函数求和，可结合定位工具实现跳跃自动选区求和\n使用公式时，不方便拖拽时，可使用定位，再Ctr + 回车 批量填充\n八、IF函数逻辑判断 IF函数的基本用法\n函数语法：IF(logical_test,[value_if_true],[value_if_false])\nIF函数 VLOOKUP函数 ISERROR函数 AND 和 OR 九、COUNTIF函数 Countif函数超过15位字符时的错误，则在后面加上*\n背景填充：【条件格式】【新建规则】\n十、SUMIF函数 Sumif函数超过15位字符时的错误\n关于第三参数简写时的注意事项\nsumifs\n替代vlookup\n数据有效性\n十一、Vlookup函数 Vlookup函数语法 VLOOKUP(lookup_value,table_array,col_index_num,[range_lookup])\n基本应用 通配符查找\n近似匹配\n数字格式问题 十二、Match和Index函数 MATCH(lookup_value,lookup_array,[match_type])\nINDEX(array,row_num,[column_num])\n单元格引用原理 返回多列结果 十三、邮件合并 在工作当中，需要生成多个文档或者电子邮件之类的，大体内容保持不变，关键信息变动，使用邮件合并更加方便达到要求。\n批量生成多个文档 【邮件】【开始邮件合并】【邮件合并分步向导】\n在完成合并步骤前可选择完成完成合并并编辑单个文档\n利用word发送邮件 同理\n每页显示多条记录 第一步，选择目录\n效果如图\n邮件合并后的数字格式处理 数字格式\t\\# \u0026quot;#,##0\u0026quot;\n日期格式\t\\@ \u0026quot;M/d/yyyy\u0026quot;\t（m需要大写）\n按下Alt + F9键\n修改后\n十四、日期函数 十五、简单文本函数 截取字符串 获取文本中的信息 身份证 十六、数学函数 round、roundup、rounddown、int、mod（求余数）、row、column\n十七、vlookup函数与数组 回顾sumif、sumifs函数\n十八、indirect函数 跨表引用\n顺序不同如何处理\n混合引用\n根据省份确定城市\n十九、动态图表 先在空白处写出公式\n=OFFSET($B$1,COUNTA($B:$B)-10,0,10,1)\n然后添加到名称\n最后插入图表\n案例2\n开发工具 插入滚动条\n设置最小值和单元格链接\n写出公式=OFFSET($B$1,$F$2,0,$F$4,1)\n公式下定义名称\n插入图表，选择数据，添加\n系类名称：成交量\n系列值：=Sheet1!成交量\n滑动滚动条\n二十、实用技巧 1.数字自动占位补全，【设置单元格格式】自定义 类型填写000\n2.选定区域内容后面批量加下划线，【设置单元格格式】自定义 类型填写@*_\n3.该列前面输入多个内容，alt+下键 可显示前面输入过的内容，可直接选择\n4.批量创建文件夹，=\u0026ldquo;md \u0026ldquo;\u0026amp;文件夹名字 创建文本文档，将函数结果复制到文本中，另存为bat文件，ANSI编码\n5.防看错行列的聚光灯效果 创建公式 =OR(cell(\u0026ldquo;row\u0026rdquo;)=ROW(),cell(\u0026ldquo;col\u0026rdquo;)=COLUMN())\n6.复制自定义格式单元格 =text(A1,\u0026ldquo;000\u0026rdquo;)\n7.批量插入图片 选中所有图片，插入excel，格式 调整大小 光标定位到任意单元格 拖动最后一张照片 到合适位置 ctrl + g 定位条件 【对象】 对齐对象 纵向分布 左对齐\n8.单元格带单位求和 先去掉元求和 再【设置单元格格式】自定义 类型 原来基础上加元字\n9.批量对文件重命名 先在excel中对原名处理 =\u0026ldquo;前缀\u0026rdquo;\u0026amp;A1 然后=\u0026ldquo;ren 原名列 新名列\u0026rdquo;，将函数结果复制到文本中，另存为bat文件，ANSI编码\n10.通过地址插入图片 =\u0026quot;\u0026lt;img src\u0026rdquo;\u0026ldquo;\u0026ldquo;地址\u0026rdquo;\u0026ldquo;\u0026ldquo;width=\u0026ldquo;\u0026ldquo;101\u0026quot;\u0026ldquo;height=\u0026ldquo;\u0026ldquo;122\u0026gt;\u0026rdquo; 右键选择性粘贴 unicode文本\n11.一键批量提取工作表名称 新建名称 =getworkbook(1) 然后 = index(名字,row(A1))\n12.批量套打小数位很长 Alt + F9 放在结果后面大括号前面 ==\\#\u0026ldquo;0.00\u0026rdquo;== Alt + F9再切换回来\n13.筛选数据自动重新编号 =subtotal(3,$C$6:C6)*1\n文件夹中提取文件名 bat脚本 dir * . * /b\u0026gt;提取文件名.txt 设置数字以万元显示 设置单元格格式 自定义 0!.0,万 多表透视表 Alt + D + P 三个分开按 excel证件照换背景 【格式】删除背景，更改填充色 ","permalink":"https://ktzxy.top/posts/1o1lhzmfgj/","summary":"Excel学习","title":"Excel学习"},{"content":"1. MySQL 锁机制概述 锁是计算机协调多个进程或线程并发访问某一资源的机制（避免争抢）。在数据库中，除传统的计算资源（CPU、RAM、I/O）的争用以外，数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题，锁冲突也是影响数据库并发访问性能的一个重要因素。\n加锁是实现数据库并发控制的一个非常重要的技术。当事务在对某个数据对象进行操作前，先向系统发出请求，对其加锁。加锁后事务就对该数据对象有了一定的控制，在该事务释放锁之前，其他的事务不能对此数据对象进行更新操作。\n1.1. 锁的分类 从性能的角度，可以分为以下两类：\n乐观锁(用版本对比或CAS机制)：适合读操作较多的场景 悲观锁：适合写操作较多的场景 Tips: 如果在写操作较多的场景使用乐观锁会导致比对次数过多，影响性能\n从数据操作的粒度，分为以下四类：\n全局锁 表级锁 页级锁 行级锁 从对数据库（表）操作的类型，分为以下三类：\n读锁（共享锁、S 锁(Shared)） 写锁（独占锁、排他锁、X 锁(eXclusive)） 意向锁（Intention Lock） Notes: 各类型的锁说明详见后面章节\n1.2. InnoDB 锁分类 按照 MySQL 官方的说法，InnoDB 中锁可以分为：\nShared and Exclusive Locks Intention Locks Record Locks Gap Locks Next-Key Locks Insert Intention Locks AUTO-INC Locks Predicate Locks for Spatial Indexes 官网说明：https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html\nInnoDB 存储引擎既支持表锁，也支持行锁。表锁实现简单，占用资源较少，粒度很粗，性能较差；行锁粒度更细，可以实现更精准的并发控制。\n2. 解决并发事务问题 在数据库中，除传统的计算资源（如 CPU、RAM、I/O 等）的争用以外，数据也是一种供许多用户共享的资源。事务并发执行时可能带来的各种问题，最大的一个难点是：一方面要最大程度地利用数据库的并发访问，另外一方面还要确保每个用户能以一致的方式读取和修改数据，尤其是一个事务进行读取操作，另一个同时进行改动操作的情况下。\n各个数据库厂商对 SQL 标准的支持都可能不一样，与 SQL 标准不同的一点就是，MySQL 在 REPEATABLE READ 隔离级别实际上就基本解决了幻读问题。解决脏读、不可重复读、幻读这些问题有两种可选的解决方案：\n2.1. 方案一：读操作 MVCC，写操作进行加锁 MVCC 就是通过生成一个 ReadView，然后通过 ReadView 找到符合条件的记录版本（历史版本是由 undo 日志构建的），其实就像是在生成 ReadView 的那个时刻做了一个快照，查询语句只能读到在生成 ReadView 之前已提交事务所做的更改，在生成 ReadView 之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作肯定针对的是最新版本的记录，读记录的历史版本和改动记录的最新版本本身并不冲突，也就是采用MVCC 时，读-写操作并不冲突。\n普通的 SELECT 语句在 READ COMMITTED 和 REPEATABLE READ 隔离级别下会使用到 MVCC 读取记录。\n在 READ COMMITTED 隔离级别下，一个事务在执行过程中每次执行 SELECT 操作时都会生成一个 ReadView，ReadView 的存在本身就保证了事务不可以读取到未提交的事务所做的更改，也就是避免了脏读现象； 在 REPEATABLE READ 隔离级别下，一个事务在执行过程中只有第一次执行 SELECT 操作才会生成一个 ReadView，之后的 SELECT 操作都复用这个 ReadView，这样也就避免了不可重复读和很大程度上避免了幻读的问题。 2.2. 一致性读（Consistent Reads）/快照读 事务利用 MVCC 进行的读取操作称之为一致性读（一致性无锁读，也称之为快照读）。所有普通的 SELECT 语句（plain SELECT，指不加锁的 select 语句在非串行化事务隔离级别下）在 READ COMMITTED、REPEATABLE READ 隔离级别下都算是一致性读。\n一致性读并不会对表中的任何记录做加锁操作，其他事务可以自由的对表中的记录做改动。采用 MVCC 方式的话，读-写操作彼此并不冲突，性能更高，采用加锁方式的话，读-写操作彼此需要排队执行，影响性能。\n2.3. 方案二：读、写操作都采用加锁的方式 脏读的产生是因为当前事务读取了另一个未提交事务写的一条记录，如果另一个事务在写记录的时候就给这条记录加锁，那么当前事务就无法继续读取该记录了，所以也就不会有脏读问题的产生了。 不可重复读的产生是因为当前事务先读取一条记录，另外一个事务对该记录做了改动之后并提交之后，当前事务再次读取时会获得不同的值，如果在当前事务读取记录时就给该记录加锁，那么另一个事务就无法修改该记录，也不会发生不可重复读的情况 幻读问题的产生是因为当前事务读取了一个范围的记录，然后另外的事务向该范围内插入了新记录，当前事务再次读取该范围的记录时发现了新插入的新记录，把新插入的那些记录称之为幻影记录。采用加锁的方式解决幻读问题就有不太容易了，因为当前事务在第一次读取记录时那些幻影记录并不存在，所以读取的时候加锁就有点麻烦 3. 全局锁 3.1. 概述 全局锁就是对整个数据库实例（数据库中的所有表）加锁，加锁后整个实例就处于只读状态，后续的 DML 的写语句，DDL 语句，已经更新操作的事务提交语句都将被阻塞。其典型的使用场景是做全库的逻辑备份，对所有的表进行锁定，从而获取一致性视图，保证数据的完整性。\n3.2. 语法 加全局锁 1 flush tables with read lock; 数据备份(示例)。注：mysqldump 是 mysql 提供的备份工具，需要在系统的命令行执行，而非 mysql 客户端中执行。 1 mysqldump -uroot –p123456 temp_db \u0026gt; temp_db.sql 释放锁 1 unlock tables; 3.3. 特点 数据库中加全局锁，是一个比较重的操作，存在以下问题：\n如果在主库上备份，那么在备份期间都不能执行更新，业务基本上就得停摆。 如果在从库上备份，那么在备份期间从库不能执行主库同步过来的二进制日志（binlog），会导致主从延迟。 在 InnoDB 引擎中，可以在备份时加上参数 --single-transaction 参数来完成不加锁的一致性数据备份。\n1 mysqldump --single-transaction -uroot –p123456 temp_db \u0026gt; temp_db.sql 4. 锁定读（Locking Reads）/LBCC 4.1. 『当前读』与『快照读』 快照读：读取的是快照版本。普通的 SELECT 就是快照读。通过 mvcc 机制来进行并发控制的，不用加锁。 当前读：即锁定读（Locking Reads），读取的是最新版本，并且对读取的记录加锁，阻塞其他事务同时改动相同记录，避免出现安全问题。以下的情况是当前读： SQL 行锁类型 说明 SELECT \u0026hellip; LOCK IN SHARE MODE 共享锁 需要手动在 SELECT 之后加 LOCK IN SHARE MODE SELECT \u0026hellip; FOR UPDATE 排他锁 需要手动在 SELECT 之后加 FOR UPDATE INSERT \u0026hellip; 排他锁 自动加锁 UPDATE \u0026hellip; 排他锁 自动加锁 DELETE \u0026hellip; 排他锁 自动加锁 串行化事务隔离级别 串行 当前读这种实现方式，也可以称之为 LBCC（基于锁的并发控制，Lock-Based Concurrency Control）\n4.2. 读锁（共享锁）和写锁（独占锁） 使用加锁时，既要允许读-读情况不受影响，又要使写-写、读-写或写-读情况中的操作相互阻塞，MySQL 中的锁可以分成：\n读锁（共享锁），英文名：Shared Locks，简称 S 锁。在事务中要读取一条记录时，需要先获取该记录的 S 锁。针对同一份数据，多个读操作可以同时进行而不会互相影响。 写锁（独占锁、排他锁），英文名：Exclusive Locks，简称 X 锁。在事务要改动一条记录时，需要先获取该记录的 X 锁。在当前写操作没有提交事务前，它会阻断其他写锁和读锁。数据修改操作都会加写锁，查询也可以通过 for update 加写锁（仅适用于 InnoDB） 两种锁的兼容性：\n假如事务 E1 首先获取了一条记录的 S 锁之后，事务 E2 接着也要访问这条记录：\n如果事务 E2 想要再获取一个记录的 S 锁，那么事务 E2 也会获得该锁，也就意味着事务 E1 和 E2 在该记录上同时持有 S 锁。 如果事务 E2 想要再获取一个记录的 X 锁，那么此操作会被阻塞，直到事务 E1 提交之后将 S 锁释放掉。 如果事务 E1 首先获取了一条记录的 X 锁之后，那么不管事务 E2 接着想获取该记录的 S 锁还是 X 锁都会被阻塞，直到事务 E1 提交。 两种行锁的兼容情况如下:\n总结：S 锁和 S 锁是兼容的，S 锁和 X 锁是不兼容的，X 锁和 X 锁也是不兼容的\n4.3. 锁定读的 SELECT 语句 可以通过以下语句显示给记录集加共享锁或排他锁。\n4.3.1. SELECT 语句的共享锁 对读取的记录加 S 锁的语法格式： 1 SELECT... LOCK IN SHARE MODE; 就是在普通的 SELECT 语句后边加 LOCK IN SHARE MODE，如果当前事务执行了该语句，那么它会为读取到的记录加 S 锁，这样允许别的事务继续获取这些记录的 S 锁（比如别的事务也使用SELECT ... LOCK IN SHARE MODE语句来读取这些记录），但是不能获取这些记录的 X 锁（比如使用SELECT ... FOR UPDATE语句来读取这些记录，或者直接修改这些记录）。如果别的事务想要获取这些记录的 X 锁，那么它们会阻塞，直到当前事务提交之后将这些记录上的 S 锁释放掉。\nLOCK IN SHARE MODE 使用注意事项：多个事务同时更新同一个表单时很容易造成死锁。\n4.3.2. SELECT 语句的排他锁 对读取的记录加 X 锁的语法格式： 1 SELECT ... FOR UPDATE [OF column_list][WAIT n|NOWAIT][SKIP LOCKED]; 普通的 SELECT 语句后边加FOR UPDATE，如果当前事务执行了该语句，那么它会为读取到的记录加 X 锁，这样既不允许别的事务获取这些记录的 S 锁（比方说别的事务使用SELECT ... LOCK IN SHARE MODE语句来读取这些记录），也不允许获取这些记录的 X 锁（比如说使用SELECT... FOR UPDATE语句来读取这些记录，或者直接修改这些记录）。如果别的事务想要获取这些记录的 S 锁或者 X 锁，那么它们会阻塞，直到当前事务提交之后将这些记录上的 X 锁释放掉。\n一些使用示例：\n1 2 3 4 5 6 7 8 -- 会等待行锁释放之后，返回查询结果。 select * from t for update; -- 不等待行锁释放，提示锁冲突，不返回结果 select * from t for update nowait; -- 等待5秒，若行锁仍未释放，则提示锁冲突，不返回结果 select * from t for update wait 5; -- 查询返回查询结果，但忽略有行锁的记录 select * from t for update skip locked; SELECT... FOR UPDATE 使用注意事项：\nfor update 仅适用于 innodb，且必须在事务范围内才能生效。 根据主键进行查询，查询条件为 like 或者不等于，主键字段产生表锁。 根据非索引字段进行查询，会产生表锁。 4.4. 写操作的锁 4.4.1. DELETE 操作 对一条记录做 DELETE 操作的过程，先在 B+树中定位到这条记录的位置，然后获取一下这条记录的 X 锁，然后再执行 delete mark 操作。可以把这个定位待删除记录在 B+树中位置的过程看成是一个获取X锁的锁定读。\n4.4.2. INSERT 操作 一般情况下，新插入一条记录的操作并不加锁，InnoDB 通过一种称之为隐式锁来保护这条新插入的记录在本事务提交前不被别的事务访问。也有特殊情况下 INSERT 操作也是会获取锁\n4.4.3. UPDATE 操作 对一条记录做 UPDATE 操作时分为三种情况：\n如果未修改该记录的键值并且被更新的列占用的存储空间在修改前后未发生变化，则先在 B+树中定位到这条记录的位置，然后再获取一下记录的 X 锁，最后在原记录的位置进行修改操作。可以把这个定位待修改记录在 B+树中位置的过程看成是一个获取 X 锁的锁定读。 如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化，则先在 B+树中定位到这条记录的位置，然后获取一下记录的 X 锁，将该记录彻底删除掉（就是把记录彻底移入垃圾链表），最后再插入一条新记录。这个定位待修改记录在 B+树中位置的过程看成是一个获取 X 锁的锁定读，新插入的记录由 INSERT 操作提供的隐式锁进行保护。 如果修改了该记录的键值，则相当于在原记录上做 DELETE 操作之后再来一次 INSERT 操作，加锁操作就需要按照 DELETE 和 INSERT 的规则进行了。 4.5. MySQL 是如何避免幻读 在快照读情况下，MySQL 通过 MVCC 机制来避免幻读。 在当前读情况下，MySQL 通过 next-key 临键锁来避免幻读（加行锁和间隙锁来实现的）。next-key包括两部分：行锁和间隙锁。行锁是加在索引上的锁，间隙锁是加在索引之间的。 Serializable 隔离级别也可以避免幻读，会锁住整张表，并发性极低，一般不会使用。\n5. 表级锁 5.1. 概述 表级锁（表锁），每次操作锁住整张表。开销小，加锁快；不会出现死锁；锁定粒度大，发生锁冲突的概率最高，并发度最低，一般用在整表数据迁移的场景。应用在 MyISAM、InnoDB、BDB 等存储引擎中。对于表级锁，主要分为以下三类：\n表锁 元数据锁（meta data lock，MDL） 意向锁 5.2. 表锁的分类 表共享读锁（read lock，S 锁） 表独占写锁（write lock，X 锁） 在对某个表执行 SELECT、INSERT、DELETE、UPDATE 语句时，InnoDB 存储引擎是不会为这个表添加表级别的 S 锁或者 X 锁的。\nInnoDB 存储引擎提供的表级 S 锁或者 X 锁是相当鸡肋，只会在一些特殊情况下，比如：崩溃恢复过程中用到。但也是可以手动获取相应的锁。但注意的是，尽量避免在使用 InnoDB 存储引擎的表上使用 LOCK TABLES 这样的手动锁表语句，它们并不会提供什么额外的保护，只是会降低并发能力而已。\n5.2.1. 表锁的语法 当在系统变量 autocommit=0、innodb_table_locks = 1 时，手动获取 InnoDB 存储引擎提供的表的 S 锁或者 X 锁的语法格式如下：\n1 2 3 4 -- InnoDB 存储引擎会对表加表级别的 S 锁。 LOCK TABLES 表名 READ; -- InnoDB 存储引擎会对表加表级别的 X 锁。 LOCK TABLES 表名 WRITE; 释放锁语法格式：\n1 2 UNLOCK TABLES; -- 或者客户端断开连接 查看表的加锁状态语法格式：\n1 2 --查看表上加过的锁 show open tables; 5.2.2. 表锁的读写锁测试 读锁测试\n左侧为客户端一，对指定表加了读锁，不会影响右侧客户端二的读，但是会阻塞右侧客户端的写。\n写锁测试\n左侧为客户端一，对指定表加了写锁，会阻塞右侧客户端的读和写。\n结论：读锁不会阻塞其他客户端的读，但是会阻塞写；写锁既会阻塞其他客户端的读，又会阻塞其他客户端的写。\n5.3. 表级别的 AUTO-INC 锁 在使用 MySQL 过程中，可以为表的某个列添加 AUTO_INCREMENT 属性，之后在插入记录时，可以不指定该列的值，系统会自动为它赋上递增的值系统实现这种自动给 AUTO_INCREMENT 修饰的列递增赋值，其实现原理主要是两个：\n采用 AUTO-INC 锁，也就是在执行插入语句时就在表级别加一个 AUTO-INC 锁，然后为每条待插入记录的 AUTO_INCREMENT 修饰的列分配递增的值，在该语句执行结束后，再把 AUTO-INC 锁释放掉。这样一个事务在持有 AUTO-INC 锁的过程中，其他事务的插入语句都要被阻塞，可以保证一个语句中分配的递增值是连续的。 如果插入语句在执行前不可以确定具体要插入多少条记录（无法预计即将插入记录的数量），比方说使用INSERT ... SELECT、REPLACE ... SELECT或者LOAD DATA这种插入语句，一般是使用 AUTO-INC 锁为 AUTO_INCREMENT 修饰的列生成对应的值。\n采用一个轻量级的锁，在为插入语句生成 AUTO_INCREMENT 修饰的列的值时获取一下这个轻量级锁，然后生成本次插入语句需要用到的 AUTO_INCREMENT 列的值之后，就把该轻量级锁释放掉，并不需要等到整个插入语句执行完才释放锁。 如果插入语句在执行前就可以确定具体要插入多少条记录，那么一般采用轻量级锁的方式对 AUTO_INCREMENT 修饰的列进行赋值。这种方式可避免锁定表，可以提升插入性能。\nInnoDB 提供了一个称之为 innodb_autoinc_lock_mode 的系统变量来控制到底使用上述两种方式中的哪种来为 AUTO_INCREMENT 修饰的列进行赋值\n当 innodb_autoinc_lock_mode 值为 0 时，一律采用 AUTO-INC 锁 当 innodb_autoinc_lock_mode 值为 2 时，一律采用轻量级锁 当 innodb_autoinc_lock_mode 值为 1 时，两种方式混着来（也就是在插入记录数量确定时采用轻量级锁，不确定时使用 AUTO-INC 锁） 需要注意：当 innodb_autoinc_lock_mode 值为 2 时，可能会造成不同事务中的插入语句为 AUTO_INCREMENT 修饰的列生成的值是交叉的，在有主从复制的场景中是不安全的。\nMySQL 5.7.X 版本中缺省值为 1\n5.4. 元数据锁 元数据锁（meta data lock），简写 MDL。在对某个表执行一些诸如 ALTER TABLE、DROP TABLE 这类的 DDL 语句时，其他事务对这个表并发执行诸如 SELECT、INSERT、DELETE、UPDATE 的语句会发生阻塞。同样，某个事务中对某个表执行 SELECT、INSERT、DELETE、UPDATE 语句时，在其他会话中对这个表执行 DDL 语句也会发生阻塞。\n以上过程是通过在 server 层使用称之为元数据锁来实现的，一般情况下也不会使用 InnoDB 存储引擎自己提供的表级别的 S 锁和 X 锁。\nMDL 加锁过程是系统自动控制，无需显式使用。在 MySQL 5.5 中引入了 MDL，当对一张表进行增删改查的时候，加 MDL 读锁(共享)；当对表结构进行变更操作的时候，加 MDL 写锁(排他)。\n常见的 SQL 操作时，所添加的元数据锁：\n5.4.1. 元数据锁效果测试 当执行 SELECT、INSERT、UPDATE、DELETE 等语句时，添加的是元数据共享锁（SHARED_READ / SHARED_WRITE），之间是兼容的。 当执行SELECT语句时，添加的是元数据共享锁（SHARED_READ），会阻塞元数据排他锁（EXCLUSIVE），之间是互斥的。 5.4.2. 查看元数据锁 通过操作的过程中，可以通过以下 SQL 可以查看数据库中的元数据锁的情况：\n1 SELECT object_type,\tobject_schema, object_name, lock_type, lock_duration FROM PERFORMANCE_SCHEMA.metadata_locks; 5.5. 意向锁 5.5.1. 存在问题简述 在对表上锁的时候，如果需要获取是否有行被上锁，那就需要依次扫描整个表。假如客户端A对表加了行锁后，客户端B如何给表加表锁呢，来通过示意图简单分析一下：\n首先客户端A，开启一个事务，然后执行 DML 操作，在执行 DML 语句时，会对涉及到的行加行锁。\n当客户端B，想对这张表加表锁时，会检查当前表是否有对应的行锁，如果没有，则添加表锁，此时就会从第一行数据，检查到最后一行数据，效率较低。\n以上示例的处理方式效率太低。因此 InnoDB 提出了一种意向锁（英文名：Intention Locks），就是用于解决该问题。引入意向锁之后，客户端A，在执行 DML 操作时，会对涉及的行加行锁，同时也会对该表加上意向锁。\n而其他客户端，在对这张表加表锁的时候，会根据该表上所加的意向锁来判定是否可以成功加表锁，而不用逐行判断行锁情况了。\n5.5.2. 意向锁的概念 意向锁（Intention Lock）：又称I锁，是表级锁。它的提出仅仅为了后续在加表级别的 S 锁和 X 锁时可以快速判断表中是否有记录被上锁，以避免用遍历的方式来查看表中有没有上锁的记录。用户并不能手动添加意向锁，只能由 InnoDB 存储引擎自行添加。\n当有事务给表的数据行加了共享锁或排他锁，同时会给表设置一个标识，代表已经有行锁了，其他事务要想对表加表锁时，就不必逐行判断有没有行锁可能跟表锁冲突了，直接读这个标识就可以确定自己该不该加表锁。特别是表中的记录很多时，逐行判断加表锁的方式效率很低。而这个标识就是意向锁。\n5.5.3. 意向锁的分类 意向共享锁，英文名：Intention Shared Lock，简称 IS 锁。当事务准备在某条记录上加 S 锁时，需要先在表级别加一个 IS 锁。由语句 select ... lock in share mode 添加。与表锁共享锁(read)兼容，与表锁排他锁(write)互斥。当有其他事务对整个表加共享锁之前，需要先获取到意向共享锁。 意向独占锁，英文名：Intention Exclusive Lock，简称 IX 锁。当事务准备在某条记录上加 X 锁时，需要先在表级别加一个 IX 锁。由 insert、update、delete、select...for update 添加 。与表锁共享锁(read)及排他锁(write)都互斥，意向锁之间不会互斥。当其他事务对整个表加排他锁之前，需要先获取到意向排他锁。 Tips: 一旦事务提交了，意向共享锁、意向排他锁，都会自动释放。\n可以通过以下 SQL，查看意向锁及行锁的加锁情况：\n1 SELECT object_schema,object_name,index_name,lock_type,lock_mode,lock_data FROM performance_schema.data_locks; 5.6. 表级别的各种锁的兼容性 其实 IS 锁和 IX 锁是兼容的，IX 锁和 IX 锁是兼容的。表级别的各种锁的兼容性如下：\nX IX S IS X 不兼容 不兼容 不兼容 不兼容 IX 不兼容 不兼容 S 不兼容 不兼容 IS 不兼容 锁的组合性：\nX IX S IS 表锁 √ √ √ √ 行锁 √ √ 5.7. 表的S锁与X锁 如果一个事务给表加了 S 锁\n别的事务可以继续获得该表的 S 锁 别的事务可以继续获得该表中的某些记录的 S 锁 别的事务不可以继续获得该表的 X 锁 别的事务不可以继续获得该表中的某些记录的 X 锁 如果一个事务给表加了 X 锁（意味着该事务要独占这个表）\n别的事务不可以继续获得该表的 S 锁 别的事务不可以继续获得该表中的某些记录的 S 锁 别的事务不可以继续获得该表的 X 锁 别的事务不可以继续获得该表中的某些记录的 X 锁 6. 页锁 只有 BDB 存储引擎支持页锁，页锁就是在页的粒度上进行锁定，锁定的数据资源比行锁要多，因为一个页中可以有多个行记录。当使用页锁的时候，会出现数据浪费的现象，但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间，会出现死锁。锁定粒度介于表锁和行锁之间，并发度一般。\nNotes: 了解即可，基本不用。\n7. 行级锁 7.1. InnoDB 的行级锁 行级锁（行锁），也称为记录锁，即在操作时锁住某一行数据。开销大，加锁慢；会出现死锁；锁定粒度最小，发生锁冲突的概率最低，并发度最高。应用在InnoDB存储引擎中。\n值得注意的是，InnoDB 的行锁实际是通过给索引上的索引项加锁(在索引对应的索引项上做标记)，不是针对整个行记录加的锁。InnoDB 这种行锁实现特点意味着：只有通过索引条件检索数据，InnoDB 才使用行级锁，否则，InnoDB 将使用表锁。如果索引失效，也会从行锁升级为表锁(RR级别会升级为表锁，RC级别不会升级为表锁)。\n只有执行计划真正使用了索引(不论是使用主键索引、唯一索引或普通索引)，InnoDB 才能使用行锁来对数据加锁：即便在条件中使用了索引字段，但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的，如果 MySQL 认为全表扫描效率更高，比如对一些很小的表，它就不会使用索引，这种情况下 InnoDB 将使用表锁，而不是行锁。同时当使用范围条件而不是相等条件检索数据，并请求锁时，InnoDB 会给符合条件的已有数据记录的索引项加锁。\nNotes: 关于RR级别行锁升级为表锁的原因分析\n因为在 RR 隔离级别下，需要解决不可重复读和幻读问题，所以在遍历扫描聚集索引记录时，为了防止扫描过的索引被其它事务修改(不可重复读问题)或间隙被其它事务插入记录(幻读问题)，从而导致数据不一致，所以 MySQL 的解决方案就是把所有扫描过的索引记录和间隙都锁上。\n7.2. 行锁的分类 InnoDB 中，行锁也是分成了各种类型。即使对同一条记录加行锁，如果类型不同，起到的功效也是不同。主要分为以下三类：\n记录锁（Record Lock）：锁定单个行记录的锁，防止其他事务对此行进行 update 和 delete。在 RC、RR 隔离级别下都支持 间隙锁（Gap Lock）：锁定索引记录间隙（不含该记录），确保索引记录间隙不变，防止其他事务在这个间隙进行 insert，产生幻读。在 RR 隔离级别下都支持。 临键锁（Next-Key Lock）：行锁和间隙锁组合，同时锁住数据，并锁住数据前面的间隙 Gap。在 RR 隔离级别下支持。 7.3. Record Locks（记录锁） 记录锁，就是仅仅把一条记录锁上，官方的类型名称为：LOCK_REC_NOT_GAP。例如把id值为9的记录加一个记录锁，示意图如下：\n记录锁是有 S 锁和 X 锁之分\n当一个事务获取了一条记录的 S 型记录锁后，其他事务也可以继续获取该记录的 S 型记录锁，但不可以继续获取 X 型记录锁； 当一个事务获取了一条记录的 X 型记录锁后，其他事务既不可以继续获取该记录的 S 型记录锁，也不可以继续获取 X 型记录锁； 7.4. Gap Locks（间隙锁） MySQL 在 REPEATABLE READ 隔离级别下是可以解决幻读问题的，解决方案有两种，可以使用 MVCC 方案解决，也可以采用加锁方案解决。但是在使用加锁方案解决时有问题，就是事务在第一次执行读取操作时，那些幻影记录尚不存在，无法给这些幻影记录加上记录锁。\nInnoDB 提出了一种名为 Gap Locks 的锁，官方的类型名称为：LOCK_GAP，也可以简称为 gap 锁。间隙锁实质上是对索引前后的间隙上锁，不对索引本身上锁。间隙锁是在可重复读（REPEATABLE READ）隔离级别下才会生效。。\n例如：会话1开启一个事务，执行\n1 2 begin; update `user` set age = 27 where id = 9; 此时会在id为9的记录前后加了 gap 锁。意味着不允许别的事务在这条记录前后间隙插入新记录。\n会话2开启一个事务，执行\n1 2 begin; insert into `user` values (12, \u0026#39;傷月\u0026#39;, 22); 此时会报错：Lock wait timeout exceeded; try restarting transaction。因为主键（或者索引）正好在9~13之间，此区间存在间隙锁，所以不能插入。\n如果将id改成15，此时新增的记录不在被锁的区间内，所以可以成功插入。\n1 insert into `user` values (15, \u0026#39;傷月\u0026#39;, 22); Tips: 间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存，一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁。\n7.5. Next-Key Locks（临键锁） 有些情况，既想锁住某条记录，又想阻止其他事务在该记录前边的间隙插入新记录，所以 InnoDB 就提出了一种名为 Next-Key Locks 的锁，官方的类型名称为：LOCK_ORDINARY，也可以简称为 next-key 锁。next-key 锁的本质就是一个记录锁和一个 gap 锁的组合，即包含记录本身。\n默认情况下，InnoDB 以 REPEATABLE READ 隔离级别运行。在这种情况下，InnoDB 使用 Next-Key Locks 锁进行搜索和索引扫描，这可以防止幻读的发生。\n索引上的等值查询(唯一索引)，给不存在的记录加锁时，优化为间隙锁 索引上的等值查询(非唯一普通索引)，向右遍历时最后一个值不满足查询需求时，next-key lock 退化为间隙锁 索引上的范围查询(唯一索引)，会访问到不满足条件的第一个值为止 7.6. Insert Intention Locks 一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了所谓的 gap 锁（next-key 锁也包含 gap 锁），如果有的话，插入操作需要等待，直到拥有 gap 锁的那个事务提交。\nInnoDB 规定事务在等待的时候也需要在内存中生成一个锁结构，表明有事务想在某个间隙中插入新记录，但是现在处于等待状态。这种类型的锁命名为 Insert Intention Locks，官方的类型名称为：LOCK_INSERT_INTENTION，也可以称为插入意向锁。\n可以理解为插入意向锁是一种锁的的等待队列，让等锁的事务在内存中进行排队等待，当持有锁的事务完成后，处于等待状态的事务就可以获得锁继续事务了。\n7.7. 隐式锁 锁的的维护是需要成本的，为了节约资源，MySQL 在设计提出了一个隐式锁的概念。一般情况下 INSERT 操作是不加锁的，当然真的有并发冲突的情况下，还是会导致问题的。\n在 MySQL 中，一个事务对新插入的记录可以不显式的加锁，但是别的事务在对这条记录加 S 锁或者 X 锁时，会去检查索引记录中的 trx_id 隐藏列，然后进行各种判断，会先帮助当前事务生成一个锁结构，然后自己再生成一个锁结构后进入等待状态。但是由于事务 id 的存在，相当于加了一个隐式锁。此时隐式锁就起到了延迟生成锁的用处。这个过程，用户无法干预，是由引擎自动处理的，对用户是完全透明的，只需了解即可。\n8. MySQL 其他存储引擎中的锁 MySQL 支持多种存储引擎，不同存储引擎对锁的支持也是不一样\n存储引擎 表级锁 行级锁 MyISAM ✅ ❌ InnoDB ✅ ✅ MEMORY ✅ ❌ BDB ✅ ❌ 对于 MyISAM、MEMORY、MERGE 这些存储引擎来说，它们只支持表级锁，而且这些引擎并不支持事务，所以使用这些存储引擎的锁一般都是针对当前会话而言。\n因为使用 MyISAM、MEMORY、MERGE 这些存储引擎的表在同一时刻只允许一个会话对表进行写操作，所以这些存储引擎实际上最好用在只读，或者大部分都是读操作，或者单用户的情景下。另外，在 MyISAM 存储引擎中有一个称之为 Concurrent Inserts 的特性，支持在对 MyISAM 表读取时同时插入记录，这样可以提升一些插入速度。\n其他存储引擎的锁具体的细节参考官方文档\n8.1. 锁的内存结构 锁其实是一个内存中的结构，在事务执行前本来是没有锁的，也就是说一开始是没有锁结构和记录进行关联的，当一个事务想对这条记录做改动时，首先会看看内存中有没有与这条记录关联的锁结构，如没有的话，此时就会在内存中生成一个锁结构与之关联。锁结构里至少要有两个比较重要的属性：\ntrx信息：代表这个锁结构是哪个事务生成的。 is_waiting：代表当前事务是否在等待。 当事务T1改动了某条记录后，就生成了一个锁结构与该记录关联，因为之前没有别的事务为这条记录加锁，所以is_waiting属性就是false，此场景称为获取锁成功，或者加锁成功，然后就可以继续执行操作了。\n在事务T1提交之前，另一个事务T2也想对该记录做改动，那么先去看看有没有锁结构与该记录关联，如发现有一个锁结构与之关联后，然后也生成了一个锁结构与这条记录关联，不过锁结构的is_waiting属性值为true，表示当前事务需要等待，把这个场景就称之为获取锁失败，或者加锁失败，或者没有成功的获取到锁\n在事务T 提交之后，就会把该事务生成的锁结构释放掉，然后看看还有没有别的事务在等待获取锁，发现了事务T2还在等待获取锁，所以把事务T2对应的锁结构的is_waiting属性设置为false，然后把该事务对应的线程唤醒，让它继续执行，此时事务T2就算获取到锁了。\n锁的实现方式与并发编程里的CLH队列非常相似\n对一条记录加锁的本质就是在内存中创建一个锁结构与之关联。但是实际上，当一个事务对多条记录加锁时，并不是一个记录一个锁结构，如果每条记录都生成一个锁结构，不管是执行效率还是空间效率来说都是很不划算的。\n锁结构实际是很复杂的，可以大概了解下里面包含的元素：\n锁所在的事务信息：无论是表级锁还是行级锁，一个锁属于一个事务，这里记载着该锁对应的事务信息。 索引信息：对于行级锁来说，需要记录一下加锁的记录属于哪个索引。 表锁/行锁信息：表级锁结构和行级锁结构在这个位置的内容是不同的。 表级锁记载着这是对哪个表加的锁，还有其他的一些信息 行级锁记载了记录所在的表空间、记录所在的页号、区分到底是为哪一条记录加了锁的数据结构 锁模式：锁是 IS，IX，S，X 中的哪一种。 锁类型：表锁还是行锁，行锁的具体类型。 其他一些和锁管理相关的数据结构，比如哈希表和链表等。 总结：同一个事务里，同一个数据页面，同一个加锁类型的锁会保存在一起。\n8.2. 锁表的原因分析 锁表发生在insert、update、delete 中 锁表的原理是 数据库使用独占式封锁机制，当执行上面的语句时，对表进行锁住，直到发生commit 或者 回滚 或者退出数据库用户 锁表的原因： 第一、 A程序执行了对 tableA 的 insert ，并还未 commit时，B程序也对tableA 进行insert 则此时会发生资源正忙的异常 就是锁表 第二、锁表常发生于并发而不是并行（并行时，一个线程操作数据库时，另一个线程是不能操作数据库的，cpu 和i/o 分配原则） 减少锁表的概率： 减少insert 、update 、delete 语句执行 到 commit 之间的时间。具体点批量执行改为单个执行、优化sql自身的非执行速度 如果异常对事物进行回滚 8.3. 如何判断数据库表已经锁表 查询语法：\n1 select * from v$locked_object; 可以获得被锁的对象的object_id及产生锁的会话sid。\n9. 锁等待分析 9.1. 查询数据库参数 通过检查 InnoDB_row_lock 状态变量来分析系统上的行锁的争夺情况\n1 2 3 4 5 6 7 8 9 10 mysql\u0026gt; show status like \u0026#39;innodb_row_lock%\u0026#39;; +-------------------------------+-------+ | 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 | +-------------------------------+-------+ 对各个状态参数的说明如下：\nInnodb_row_lock_current_waits：当前正在等待锁定的数量 Innodb_row_lock_time：从系统启动到现在锁定总时间长度（重点关注） Innodb_row_lock_time_avg：每次等待所花平均时间（重点关注） Innodb_row_lock_time_max：从系统启动到现在等待最长的一次所花时间 Innodb_row_lock_waits：系统启动后到现在总共等待的次数（重点关注） Tips: 如果出现等待次数很高，而且每次等待时长也不小的时候，就需要分析系统中为什么会有如此多的等待，然后根据分析结果着手制定优化计划。\n9.2. 查看 INFORMATION_SCHEMA 系统库锁相关数据表 通过查询 INFORMATION_SCHEMA 系统库可以看到当时事务与锁的\nMySQL 5.7 版本及以前：\n1 2 3 4 5 6 -- 查看事务 select * from INFORMATION_SCHEMA.INNODB_TRX; -- 查看锁（5.7以前的版本） select * from INFORMATION_SCHEMA.INNODB_LOCKS; -- 查看锁等待（5.7以前的版本） select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS; MySQL 8.0 版本之后，需要将换成 INNODB_LOCKS 表换成 data_locks；INNODB_LOCK_WAITS 表换成 data_lock_waits\n1 2 3 4 5 6 -- 查看事务 select * from INFORMATION_SCHEMA.INNODB_TRX; -- 查看锁（8.0 以后的版本） select * from INFORMATION_SCHEMA.DATA_LOCKS; -- 查看锁等待（8.0 以后的版本） select * from INFORMATION_SCHEMA.DATA_LOCK_WAITS; TODO: 本地安装的8.0版本数据库以上的INNODB_LOCKS、INNODB_LOCK_WAITS、DATA_LOCKS、DATA_LOCK_WAITS 表均不存在，待确认什么问题\n9.3. 查看事务加锁的情况 1 show engine innodb status; 查看事务加锁的情况，不过一般情况下，看不到哪个事务对哪些记录加了那些锁，需要修改系统变量 innodb_status_output_locks（MySQL5.6.16 引入），缺省值是OFF。\n1 2 3 4 5 6 mysql\u0026gt; show variables like \u0026#39;innodb_status_output_locks\u0026#39;; +----------------------------+-------+ | Variable_name | Value | +----------------------------+-------+ | innodb_status_output_locks | OFF | +----------------------------+-------+ 修改全局变量 innodb_status_output_locks 为 ON，开启。\n1 2 3 4 5 6 7 8 9 10 -- 修改为开启 mysql\u0026gt; set global innodb_status_output_locks = ON; -- 查询 mysql\u0026gt; show variables like \u0026#39;innodb_status_output_locks\u0026#39;; +----------------------------+-------+ | Variable_name | Value | +----------------------------+-------+ | innodb_status_output_locks | ON | +----------------------------+-------+ 此时开启一个事务来测试效果，再次执行show engine innodb status\\G\n分析结果如下：\n1 TABLE LOCK table `mysqladv`.`teacher` trx id 12851 lock mode IX 表示事务 ID 为 12851 对 mysqladv 下的 teacher 表加了表级意向独占锁。 1 RECORD LOCKS space id 33 page no 4 n bits 72 index idx_name of table `mysqladv`.`teacher` trx id 12852 lock_mode X locks gap before rec 表示一个内存中的锁结构 space id 33：表空间 id 为 33 page no 3：页编号为 4 ndex PRIMARY：对应的索引是 idx_name； lock_mode X locks gap before rec：存放的是一个 X 型的 gap 锁 表示的加锁记录的详细信息\n1 RECORD LOCKS space id 33 page no 4 n bits 72 index idx_name of table `mysqladv`.`teacher` trx id 12852 lock_mode X 表示一个内存中的锁结构 space id 33：表空间 id 为 33 page no 3：页编号为 4 index PRIMARY：对应的索引是 idx_name lock_mode X：存放的是一个 X 型的 next-key 锁 如果是记录锁，则会显示lock_mode X locks rec but not gap 10. 死锁和空间锁 一般来说，只要有并发和加锁这两种情况的共同加持下，都会有死锁的可能。\n10.1. 死锁的概念 指两个或两个以上的进程在执行过程中，由于竞争资源或者由于彼此通信而造成的一种阻塞的现象，若无外力作用，它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。\n10.2. 死锁的学术化的定义 死锁的发生必须具备以下四个必要条件。\n互斥条件：指进程对所分配到的资源进行排它性使用，即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源，则请求者只能等待，直至占有资源的进程用毕释放。 请求和保持条件：指进程已经保持至少一个资源，但又提出了新的资源请求，而该资源已被其它进程占有，此时请求进程阻塞，但又对自己已获得的其它资源保持不放。 不剥夺条件：指进程已获得的资源，在未使用完之前，不能被剥夺，只能在使用完时由自己释放。 环路等待条件：指在发生死锁时，必然存在一个进程-资源的环形链，即进程集合{P0，P1，P2，...，Pn}中的 P0 正在等待一个 P1 占用的资源；P1正在等待 P2 占用的资源，\u0026hellip;，Pn 正在等待已被 P0 占用的资源。 理解了死锁的原因，尤其是产生死锁的四个必要条件，就可以最大可能地避免、预防和解除死锁。只要打破四个必要条件之一就能有效预防死锁的发生。\n打破互斥条件：改造独占性资源为虚拟资源，大部分资源已无法改造。 打破不可抢占条件：当一进程占有一独占性资源后又申请一独占性资源而无法满足，则退出原占有的资源。 打破占有且申请条件：采用资源预先分配策略，即进程运行前申请全部资源，满足则运行，不然就等待，这样就不会占有且申请。 打破循环等待条件：实现资源有序分配策略，对所有设备实现分类编号，所有进程只能采用按序号递增的形式申请资源。 避免死锁常见的算法有有序资源分配法、银行家算法。\n10.3. MySQL 中的死锁 MySQL 中的死锁的成因是一样的。如以下示例\n会话1： 1 2 begin; select * from `user` where id = 1 for update; 会话2： 1 2 begin; select * from `user` where id = 3 for update; 会话1执行以下语句，可以看到这个语句的执行将会被阻塞 1 select * from `user` where id = 3 for update; 此时会话2执行以下语句，MySQL 检测到了死锁，并结束了会话 2 中事务的执行 1 2 mysql\u0026gt; select * from `user` where id = 1 for update; ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction 此时，切回会话 1，发现原本阻塞的 SQL 语句执行完成了。\n同时通过以下语句，可以看见死锁的详细情况：\n1 show engine innodb status\\G 大多数情况 mysql 可以自动检测死锁并回滚产生死锁的那个事务，但是有些情况 mysql 没法自动检测死锁，这种情况可以通过日志分析找到对应事务线程id，可以通过kill杀掉。\n1 kill 事务id; 10.4. Predicate Locks for Spatial Indexes MySQL5.7 开始 MySQL 整合了 boost.geometry 库以更好的支持空间数据类型，并支持在在 Spatial 数据类型的列上构建索引，在 InnoDB 内，这个索引和普通的索引有所不同，基于 R-TREE 的结构，目前支持对 2D 数据的描述，暂不支持 3D\nR-TREE 和 BTREE 不同，它能够描述多维空间，而多维数据并没有明确的数据顺序，因此无法在 RR 隔离级别下构建 NEXT-KEY 锁以避免幻读，因此 InnoDB 使用称为 Predicate Lock 的锁模式来加锁，会锁住一块查询用到的被称为MBR(minimum boundingrectangle/box)的数据区域。 因此这个锁不是锁到某个具体的记录之上的，可以理解为一种 Page 级别的锁。\nPredicate Lock 和普通的记录锁或者表锁（如上所述）存储在不同的 lock hash中，其相互之间不会产生冲突。\n11. 数据库锁的总结 11.1. 表锁与行锁的比较 锁定粒度：表锁 \u0026gt; 行锁 加锁效率：表锁 \u0026gt; 行锁 冲突概率：表锁 \u0026gt; 行锁 并发性能：表锁 \u0026lt; 行锁 11.2. 锁优化实践 尽可能让所有数据检索都通过索引来完成，避免无索引行锁升级为表锁 合理设计索引，尽量缩小锁的范围 尽可能减少检索条件范围，避免间隙锁 尽量控制事务大小，减少锁定资源量和时间长度，涉及事务加锁的 sql 尽量放在事务最后执行 尽可能用低的事务隔离级别 使用读写锁分离 分段加锁 多个线程尽量以相同的顺序去获取资源，而不要将锁的粒度过于细化，不然可能会出现线程的加锁和释放次数过多，反而效率不如一次加一把粒度大的锁。 ","permalink":"https://ktzxy.top/posts/4f9oasi3l7/","summary":"MySQL 锁","title":"MySQL 锁"},{"content":"[TOC]\n1、cpu_over.yml 1 2 3 4 5 6 7 8 9 10 11 groups: - name: CPU报警规则 rules: - alert: CPU使用率告警 expr: 100 - (avg by (instance)(irate(node_cpu_seconds_total{mode=\u0026#34;idle\u0026#34;}[1m]) )) * 100 \u0026gt; 90 for: 1m labels: user: prometheus severity: warning annotations: description: \u0026#34;服务器: CPU使用超过90%！(当前值: {{ $value }}%)\u0026#34; 2、memory_over.yml 1 2 3 4 5 6 7 8 9 10 11 groups: - name: 内存报警规则 rules: - alert: 内存使用率告警 expr: (node_memory_MemTotal_bytes - (node_memory_MemFree_bytes+node_memory_Buffers_bytes+node_memory_Cached_bytes )) / node_memory_MemTotal_bytes * 100 \u0026gt; 80 for: 1m labels: user: prometheus severity: warning annotations: description: \u0026#34;服务器: 内存使用超过80%！(当前值: {{ $value }}%)\u0026#34; 3、node_down.yml 1 2 3 4 5 6 7 8 9 10 11 groups: - name: 实例存活告警规则 rules: - alert: 实例存活告警 expr: up == 0 for: 1m labels: user: prometheus severity: warning annotations: description: \u0026#34;{{ $labels.instance }} of job {{ $labels.job }} has been down for more than 1 minutes.\u0026#34; ","permalink":"https://ktzxy.top/posts/fvltsbm6id/","summary":"rules.yml","title":"rules.yml"},{"content":"node.js 笔记 1. Node.js 简介 Node.js 是一个开源和跨平台的 JavaScript 运行时环境。它几乎是任何类型项目的流行工具！Node.js 在浏览器之外运行 V8 JavaScript 引擎（Google Chrome 的内核），性能非常好。\n2. npm 包管理器 2.1. npm 简介 npm 是 Node.js 标准的软件包管理器。它起初是作为下载和管理 Node.js 包依赖的方式，但其现在也已成为前端 JavaScript 中使用的工具。\n2.2. 查看 npm 版本 node.js 已经集成了 npm 工具，在命令提示符输入 npm -v 可查看当前 npm 版本\n2.3. 设置 npm 的包路径 包路径就是npm从远程下载的js包所存放的路径\n使用 npm config ls 查询NPM管理包路径（NPM下载的依赖包所存放的路径）\nNPM 默认的管理包路径在C:/用户/[用户名]/AppData/Roaming/npm/node_meodules，为了方便对依赖包管理，可以将管理包的路径设置在指定的位置，建议将安装目录设置在node.js的目录下，在node.js的安装目录下创建npm_modules和npm_cache，执行下边的命令\n1 2 3 # 例如：安装node.js在D:\\development\\nodejs\\下，打开cmd命令符窗口，执行命令如下： npm config set prefix \u0026#34;D:\\development\\nodejs\\npm_modules\u0026#34; npm config set cache \u0026#34;D:\\development\\nodejs\\npm_cache\u0026#34; 此时再使用 npm config ls 查询NPM管理包路径发现路径已更改\n注：配置以后，一些全局安装的工具会安装到npm_modules此文件夹中，需要在环境变量中配置npm_modules此文件夹，才可以全局的使用这些安装的工具\n2.4. 下载与安装包 2.4.1. 安装所有依赖 如果项目具有 package.json 文件，则通过运行：\n1 npm install 运行命令后，会在 node_modules 文件夹（如果尚不存在则会创建）中安装项目所需的所有东西。\n2.4.2. 安装单个软件包 通过运行以下命令安装特定的软件包：\n1 npm install \u0026lt;package-name\u0026gt; 此命令通过会带一些参数\n--save 安装并添加条目到 package.json 文件的 dependencies。 --save-dev 安装并添加条目到 package.json 文件的 devDependencies。 以上参数的区别主要是，devDependencies 通常是开发的工具（例如测试的库），而 dependencies 则是与生产环境中的应用程序相关。\n2.5. npm 全局安装与本地安装 当使用 npm 安装软件包时，可以执行两种安装类型：\n本地安装 全局安装 本地和全局的软件包之间的主要区别是：\n本地的软件包：安装在运行 npm install \u0026lt;package-name\u0026gt; 的目录中，并且放置在此目录下的 node_modules 文件夹中。 全局的软件包：放在系统中的单独位置（确切的位置取决于设置），无论在何处运行 npm install -g \u0026lt;package-name\u0026gt;。 2.5.1. 本地的软件包 1 npm install \u0026lt;package-name\u0026gt; 默认情况下，软件包会被安装到当前文件树中的 node_modules 子文件夹下。\n在这种情况下，npm 还会在当前文件夹中存在的 package.json 文件的 dependencies 属性中添加相应软件包条目。\n2.5.2. 全局的软件包 使用 -g 参数可以执行全局安装：\n1 npm install -g \u0026lt;package-name\u0026gt; 在这种情况下，npm 不会将软件包安装到本地文件夹下，而是使用全局的位置。\n2.5.2.1. 查看全局安装的位置 1 npm root -g 默认情况下，在 macOS 或 Linux 上，此位置可能是 /usr/local/lib/node_modules；在 Windows 上，可能是 C:\\Users\\YOU\\AppData\\Roaming\\npm\\node_modules；如果使用 nvm 管理 Node.js 版本，则软件包的位置可能为 /Users/joe/.nvm/versions/node/v8.9.0/lib/node_modules\n2.5.2.2. 查看系统已安装的全局软件包 通过在命令行上运行以下命令查看：\n1 npm list -g --depth 0 2.6. 更新软件包 通过运行以下命令，npm 会检查所有软件包是否有满足版本限制的更新版本。\n1 npm update 指定单个软件包进行更新：\n1 npm update \u0026lt;package-name\u0026gt; 2.7. 卸载软件包 卸载（删除）软件包，也分为全局卸载（删除）和本地卸载（删除）\n2.7.1. 卸载本地软件 若要卸载之前在本地安装（在 node_modules 文件夹使用 npm install \u0026lt;package-name\u0026gt;）的软件包，则从项目的根文件夹（包含 node_modules 文件夹的文件夹）中运行：\n1 npm uninstall \u0026lt;package-name\u0026gt; 如果使用 -S 或 --save 参数，则此操作还会移除 package.json 文件中的引用。如果程序包是开发依赖项（列出在 package.json 文件的 devDependencies 中），则必须使用 -D 或 --save-dev 标志从文件中移除：\n1 2 npm uninstall -S \u0026lt;package-name\u0026gt; npm uninstall -D \u0026lt;package-name\u0026gt; 2.7.2. 卸载全局软件 如果该软件包是全局安装的，则需要添加 -g 或 --global 参数。可以在系统上的任何位置运行此命令，因为当前所在的文件夹无关紧要。\n1 npm uninstall -g \u0026lt;package-name\u0026gt; 例如：\n1 npm uninstall -g webpack 2.8. 版本控制 除了简单的下载外，npm 还可以管理版本控制，因此可以指定软件包的任何特定版本，或者要求版本高于或低于所需版本。\n很多时候，一个库仅与另一个库的主版本兼容。或者，一个库的最新版本中有一个缺陷（仍未修复）引起了问题。\n指定库的显式版本还有助于使每个人都使用相同的软件包版本，以便整个团队运行相同的版本，直至 package.json 文件被更新。\n在所有这些情况中，版本控制都有很大的帮助，npm 遵循语义版本控制标准。\n2.8.1. npm 的语义版本控制 npm 的语义版本控制是指，所有的版本都有 3 个数字：x.y.z。\n第一个数字是主版本 第二个数字是次版本 第三个数字是补丁版本 发布新的版本时，要遵循以下规则：\n当进行不兼容的 API 更改时，则升级主版本。 当以向后兼容的方式添加功能时，则升级次版本。 当进行向后兼容的缺陷修复时，则升级补丁版本。 2.8.2. 版本规则符号 npm 设置了一些规则，可用于在 package.json 文件中选择要将软件包更新到的版本（当运行 npm update 时）。\n符号 规则说明 ^ 只会执行不更改最左边非零数字的更新。如果写入的是^0.13.0，则当运行npm update时，可以更新到0.13.1、0.13.2等，但不能更新到0.14.0或更高版本。如果写入的是^1.13.0，则当运行npm update时，可以更新到1.13.1、1.14.0等，但不能更新到2.0.0或更高版本 ~ 如果写入的是〜0.13.0，则当运行npm update时，会更新到补丁版本：即0.13.1可以，但0.14.0不可以 \u0026gt; 接受高于指定版本的任何版本 \u0026gt;= 接受等于或高于指定版本的任何版本 \u0026lt;= 接受等于或低于指定版本的任何版本 \u0026lt; 接受低于指定版本的任何版本 = 接受确切的版本 - 接受一定范围的版本。例如：2.1.0 - 2.6.2 ` 还有其他的规则：\n无符号：仅接受指定的特定版本（例如 1.2.1）。 latest：使用可用的最新版本。 2.9. 运行任务 package.json 文件支持一种用于指定命令行任务（可通过使用以下方式运行）的格式：\n1 npm run \u0026lt;task-name\u0026gt; 修改配置文件定义代码（任务）片段，示例如下：\n1 2 3 4 5 6 { \u0026#34;scripts\u0026#34;: { \u0026#34;start-dev\u0026#34;: \u0026#34;node lib/server-development\u0026#34;, \u0026#34;start\u0026#34;: \u0026#34;node lib/server-production\u0026#34; }, } 使用此特性运行 Webpack 一些命令\n1 2 3 4 5 6 7 { \u0026#34;scripts\u0026#34;: { \u0026#34;watch\u0026#34;: \u0026#34;webpack --watch --progress --colors --config webpack.conf.js\u0026#34;, \u0026#34;dev\u0026#34;: \u0026#34;webpack --progress --colors --config webpack.conf.js\u0026#34;, \u0026#34;prod\u0026#34;: \u0026#34;NODE_ENV=production webpack -p --config webpack.conf.js\u0026#34;, }, } 运行时只需要输入定义好的代码片段名称即可，效果相当于运行相应的长命令\n1 2 3 $ npm run watch $ npm run dev $ npm run prod 2.10. package.json 指南（待整理） 3. 常用命令 3.1. 初始化包管理配置文件 在项目目录路径下，通过命令行工具输入以下命令，初始化包管理配置文件 package.json\n1 npm init –y 3.2. 清理缓存 1 npm cache clean --force 4. 其他 4.1. 在 node.js 中体验 ES6 模块化 node.js 中默认仅支持 CommonJS 模块化规范，若想基于 node.js 体验与学习 ES6 的模块化语法，可以按照如下两个步骤进行配置：\n确保安装了 v14.15.1 或更高版本的 node.js 在 package.json 的根节点中添加 \u0026quot;type\u0026quot;: \u0026quot;module\u0026quot; 节点 ","permalink":"https://ktzxy.top/posts/q0wneoc6s3/","summary":"node","title":"node"},{"content":"﻿### 在数据库supermarket上完成以下实验内容。\n1、完成教材的例3-77~例3-88的操作。 建立咖啡类商品的视图 1 2 3 4 5 create view Coffee as select GoodsNO,GoodsName,InPrice,SalePrice,ProductTime from Goods G join Category C on G.CategoryNO = C.CategoryNO where CategoryName = \u0026#39;咖啡\u0026#39; 建立MIS专业学生的视图，并要求通过试图完成修改与插入操作时视图仍只有MIS专业学生 1 2 3 4 5 create view MIS_student as select * from Student where Major = \u0026#39;MIS\u0026#39; with check option 建立购买了咖啡类商品的学生视图 1 2 3 4 5 6 create view Buy_coffee as select * from Student where exists( select * from SaleBill SA join Coffee C on SA.GoodsNO=C.GoodsNO where SA.SNO=Student.SNO) 建立保存商品编号与销售额的视图 1 2 3 4 5 6 create view SumSale(GoodsNO,SumSale) as select G.GoodsNO,SUM(SalePrice * S.Number) SumSale from SaleBill S join Goods G on S.GoodsNO = G.GoodsNO group by G.GoodsNO 建立销售额前5的商品视图 1 2 3 4 5 6 7 create view Top5SumSale(GoodsNO,SumSale) as select top 5 G.GoodsNO,SUM(SalePrice * S.Number) SumSale from SaleBill S join Goods G on S.GoodsNO = G.GoodsNO group by G.GoodsNO order by SumSale desc 删除视图Coffee 1 drop view Coffee 查询MIS专业购买了咖啡类商品的学生信息 1 2 3 4 5 select * from Student where Major = \u0026#39;MIS\u0026#39; and exists( select * from SaleBill S join Coffee C on C.GoodsNO = S.GoodsNO where S.SNO = Student.SNO) 查询销售额前5商品的供应商编号 1 2 select top 5 SupplierNO from SumSale T join Goods G on T.GoodsNO = G.GoodsNO 查询销售额大于100的供应商编号 1 2 3 4 5 select G.GoodsNO,SUM(SalePrice * S.Number) SumSale from SaleBill S join Goods G on S.GoodsNO = G.GoodsNO group by G.GoodsNO having SUM(SalePrice * S.Number)\u0026gt;100 在Buy_coffee视图中插入一个新的学生信息，其中学号为S09，姓名为程伟，出生年份为1993，其余为空 1 insert into Buy_coffee(SNO,SName,BirthYear) values(\u0026#39;S09\u0026#39;,\u0026#39;程伟\u0026#39;,1993) 将视图MIS_student中姓名为“李明”的学生微信更改为“LiMing” 1 update MIS_student set WeiXin = \u0026#39;LiMing\u0026#39; where SName = \u0026#39;李明\u0026#39; 将视图MIS_student中姓名为“闵红”的学生元组删除 1 delete from MIS_student where SName = \u0026#39;闵红\u0026#39; 2、写出创建满足下还要求的视图的SQL语句。 (1)统计每个学生的消费金额。\n1 2 3 4 5 6 7 8 9 10 11 create view ExpenseOfStudent as ( select S.SNO,SName,SUM(SalePrice * SA.Number) 消费 from Student S join SaleBill SA on S.SNO = SA.SNO join Goods G on SA.GoodsNO = G.GoodsNO group by S.SNO,SName ); go select * from ExpenseOfStudent (2)统计每个供货商提供的商品种类(一个商品编号代表一种)。\n1 2 3 4 5 6 7 8 9 create view Goods_type as ( select S.SupplierNO,SupplierName,count(S.SupplierNO) 商品种类数量 from Supplier S join Goods G on S.SupplierNO = G.SupplierNO group by S.SupplierNO,SupplierName ); go select * from Goods_type (3)统计各商品种类的销售数量及平均售价。\n1 2 3 4 5 6 7 8 9 10 create view Goods_Sale as ( select C.CategoryNO,C.CategoryName,SUM(G.Number) 销售数量,AVG(G.SalePrice) 平均售价 from Goods G join Category C on G.CategoryNO = C.CategoryNO group by C.CategoryNO,C.CategoryName ); go select * from Goods_Sale (4)建立Sup001供货商的商品信息视图，并要求通过视图完成修改与插入操作时视图仍只有Sup001供货商的商品。\n1 2 3 4 5 6 7 8 9 10 11 create view Sup001_Supplier as select S.SupplierNO,S.SupplierName,G.GoodsNO,G.GoodsName,G.InPrice,G.SalePrice,G.ProductTime,SA.Number ,SA.SNO from Goods G join Supplier S on G.SupplierNO = S.SupplierNO join SaleBill SA on G.GoodsNO = SA.GoodsNO where S.SupplierNO = \u0026#39;Sup001\u0026#39; with check option go select * from Sup001_Supplier 3、利用上述视图，完成如下任务。 (1)统计每个MIS专业学生的消费金额。\n1 2 3 4 select S.SName,S.Major,SUM(ep.消费) 消费总额 from ExpenseOfStudent ep join Student S on ep.SNO = S.SNO where S.Major = \u0026#39;MIS\u0026#39; group by S.SName,S.Major (2)查询售价低于该商品种类售价平均价的商品名和售价。\n1 2 3 select G.GoodsName,G.SalePrice from Goods G join Goods_Sale GS on G.CategoryNO = GS.CategoryNO where G.SalePrice \u0026lt; GS.平均售价 (3)利用第4题(4)中的视图插入供货商Sup002的商品信息，结果如何?为什么?\n1 2 插入失败，原因是因为前面创建视图的时候规定了该视图在修改与插入的操作时， 视图仍只有Sup001供货商的商品 (4)利用第4题(4)中的视图删除GN0004的商品信息，结果如何?为什么?\n1 2 删除成功，因为前面创建视图的时候规定了该视图在修改与插入的操作时，视图仍只有Sup001供货商的商品， 但是对删除的权限没有规定，而且视图内有GN0004的商品信息 (5)查询供货种类大于等于2的供货商的名称及数量。\n1 select SupplierName,GT.商品种类数量 from Goods_type GT where GT.商品种类数量 \u0026gt;= 2 ","permalink":"https://ktzxy.top/posts/31py7ns887/","summary":"实验6 视图的创建与使用","title":"实验6 视图的创建与使用"},{"content":"Kubernetes配置默认存储类 前言 今天在配置Kubesphere的时候，出现了下面的错误\n经过排查，发现是这个原因\n我通过下面命令，查看Kubernetes集群中的默认存储类\n1 kubectl get storageclass 发现空空如也，所以问题应该就出现在这里了~，下面我们给k8s集群安装上默认的存储类\n安装nfs 我们使用的是nfs来作为k8s的存储类\n首先找一台新的服务器，作为nfs服务端，然后进行 nfs的安装 【服务器：192.168.177.141】\n然后使用命令安装nfs\n1 yum install -y nfs-utils 首先创建存放数据的目录\n1 mkdir -p /data/k8s 设置挂载路径\n1 2 3 4 # 打开文件 vim /etc/exports # 添加如下内容 /data/k8s *(rw,no_root_squash) node节点上安装 然后需要在k8s集群node节点上安装nfs，这里需要在 node1 和 node2节点上安装\n1 yum install -y nfs-utils 执行完成后，会自动帮我们挂载上\n启动nfs 在node节点上配置完成后，我们就接着到刚刚nfs服务器，启动我们的nfs\n1 systemctl start nfs 配置StorageClass 要使用StorageClass，我们就得安装对应的自动配置程序，比如上面我们使用的是nfs，那么我们就需要使用到一个 nfs-client 的自动配置程序，我们也叫它 Provisioner，这个程序使用我们已经配置的nfs服务器，来自动创建持久卷，也就是自动帮我们创建PV\n1 2 自动创建的 PV 以${namespace}-${pvcName}-${pvName}这样的命名格式创建在 NFS 服务器上的共享数据目录中 而当这个 PV 被回收后会以archieved-${namespace}-${pvcName}-${pvName}这样的命名格式存在 NFS 服务器上。 当然在部署nfs-client之前，我们需要先成功安装上 nfs 服务器，上面已经安装好了，服务地址是192.168.177.141，共享数据目录是/data/k8s/，然后接下来我们部署 nfs-client 即可，我们也可以直接参考 nfs-client 文档，进行安装即可。\n配置Deployment 首先配置 Deployment，将里面的对应的参数替换成我们自己的 nfs 配置（nfs-client.yaml）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 kind: Deployment apiVersion: apps/v1 metadata: name: nfs-client-provisioner spec: replicas: 1 selector: matchLabels: app: nfs-client-provisioner strategy: type: Recreate template: metadata: labels: app: nfs-client-provisioner spec: serviceAccountName: nfs-client-provisioner containers: - name: nfs-client-provisioner image: quay.io/external_storage/nfs-client-provisioner:latest volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: fuseim.pri/ifs - name: NFS_SERVER value: 192.168.177.141 - name: NFS_PATH value: /data/k8s volumes: - name: nfs-client-root nfs: server: 192.168.177.141 path: /data/k8s 替换配置 将环境变量 NFS_SERVER 和 NFS_PATH 替换，当然也包括下面的 nfs 配置，我们可以看到我们这里使用了一个名为 nfs-client-provisioner 的serviceAccount，所以我们也需要创建一个 sa，然后绑定上对应的权限：（nfs-client-sa.yaml）\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 apiVersion: v1 kind: ServiceAccount metadata: name: nfs-client-provisioner --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: nfs-client-provisioner-runner rules: - apiGroups: [\u0026#34;\u0026#34;] resources: [\u0026#34;persistentvolumes\u0026#34;] verbs: [\u0026#34;get\u0026#34;, \u0026#34;list\u0026#34;, \u0026#34;watch\u0026#34;, \u0026#34;create\u0026#34;, \u0026#34;delete\u0026#34;] - apiGroups: [\u0026#34;\u0026#34;] resources: [\u0026#34;persistentvolumeclaims\u0026#34;] verbs: [\u0026#34;get\u0026#34;, \u0026#34;list\u0026#34;, \u0026#34;watch\u0026#34;, \u0026#34;update\u0026#34;] - apiGroups: [\u0026#34;storage.k8s.io\u0026#34;] resources: [\u0026#34;storageclasses\u0026#34;] verbs: [\u0026#34;get\u0026#34;, \u0026#34;list\u0026#34;, \u0026#34;watch\u0026#34;] - apiGroups: [\u0026#34;\u0026#34;] resources: [\u0026#34;events\u0026#34;] verbs: [\u0026#34;list\u0026#34;, \u0026#34;watch\u0026#34;, \u0026#34;create\u0026#34;, \u0026#34;update\u0026#34;, \u0026#34;patch\u0026#34;] - apiGroups: [\u0026#34;\u0026#34;] resources: [\u0026#34;endpoints\u0026#34;] verbs: [\u0026#34;create\u0026#34;, \u0026#34;delete\u0026#34;, \u0026#34;get\u0026#34;, \u0026#34;list\u0026#34;, \u0026#34;watch\u0026#34;, \u0026#34;patch\u0026#34;, \u0026#34;update\u0026#34;] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: run-nfs-client-provisioner subjects: - kind: ServiceAccount name: nfs-client-provisioner namespace: default roleRef: kind: ClusterRole name: nfs-client-provisioner-runner apiGroup: rbac.authorization.k8s.io 我们这里新建的一个名为 nfs-client-provisioner 的ServiceAccount，然后绑定了一个名为 nfs-client-provisioner-runner 的ClusterRole，而该ClusterRole声明了一些权限，其中就包括对persistentvolumes的增、删、改、查等权限，所以我们可以利用该ServiceAccount来自动创建 PV。\n创建StorageClass对象 nfs-client 的 Deployment 声明完成后，我们就可以来创建一个StorageClass对象了：（nfs-client-class.yaml）\n1 2 3 4 5 apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: course-nfs-storage provisioner: fuseim.pri/ifs # or choose another name, must match deployment\u0026#39;s env PROVISIONER_NAME\u0026#39; 我们声明了一个名为 course-nfs-storage 的StorageClass对象，注意下面的provisioner对应的值一定要和上面的Deployment下面的 PROVISIONER_NAME 这个环境变量的值一样\n1 2 3 4 5 apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: course-nfs-storage provisioner: fuseim.pri/ifs # or choose another name, must match deployment\u0026#39;s env PROVISIONER_NAME\u0026#39; 创建资源对象 在我们准备好上述的配置文件后，我们就可以开始创建我们的资源对象了\n1 2 3 kubectl create -f nfs-client.yaml kubectl create -f nfs-client-sa.yaml kubectl create -f nfs-client-class.yaml 创建完成后，使用下面命令来查看资源状态\n1 2 3 kubectl get pods # 查看存储类 kubectl get storageclass 我们可以设置这个 course-nfs-storage 的 StorageClass 为 Kubernetes 的默认存储后端，我们可以用 kubectl patch 命令来更新\n1 kubectl patch storageclass course-nfs-storage -p \u0026#39;{\u0026#34;metadata\u0026#34;: {\u0026#34;annotations\u0026#34;:{\u0026#34;storageclass.kubernetes.io/is-default-class\u0026#34;:\u0026#34;true\u0026#34;}}}\u0026#39; 执行完命令后，我们默认存储类就配置成功了~\n","permalink":"https://ktzxy.top/posts/ygfgxu9fey/","summary":"32 Kubernetes配置默认存储类","title":"32 Kubernetes配置默认存储类"},{"content":"1. Redis 基础入门 1.1. Redis 介绍 redis 是一种基于键值对（key-value）数据库，其中 value 可以为 string、hash、list、set、zset 等多种数据结构，可以满足很多应用场景。还提供了键过期，发布订阅，事务，流水线等附加功能\n流水线：Redis 的流水线功能允许客户端一次将多个命令请求发送给服务器，并将被执行的多个命令请求的结果在一个命令回复中全部返回给客户端，使用这个功能可以有效地减少客户端在执行多个命令时需要与服务器进行通信的次数\n1.2. Redis 优缺点 优点：\n基于内存操作，速度快（官方给出的读写性能 10 万/S，与机器性能也有关）： 数据放内存中是速度快的主要原因 C 语言实现，与操作系统距离近 使用了单线程架构，预防多线程可能产生的竞争问题 键值对的数据结构服务器，支持多种数据类型：包括 String、Hash、List、Set、ZSet 等，并且每种数据类型底层都做了优化，让读取数据的速度更快。 丰富的功能：键过期，发布订阅，事务，流水线等等 Reids 是单线程：简单稳定，避免线程切换开销及多线程的竞争问题。单线程是指网络请求使用一个线程来处理，即一个线程处理所有网络请求，但 Redis 运行时不止有一个线程，比如数据持久化的过程会另起线程。Redis 6.0 后开始支持多线程。 支持持久化：有 RDB 和 AOF 两种持久化机制，将数据持久化到硬盘，以有效地避免发生断电或机器故障，数据可能会丢失的问题。 支持主从复制：实现多个相同数据的 redis 副本 支持高可用和分布式：哨兵机制实现高可用，保证 redis 节点故障发现和自动转移 支持事务：所有操作都是原子性的，同时还支持对几个操作合并后的原子性执行。 I/O 多路复用模型：Redis 采用 I/O 多路复用技术。使用单线程来轮询描述符，将数据库的操作都转换成了事件，不在网络I/O上浪费过多的时间。 客户端语言多：java php python c c++ nodejs 等 缺点：\n对结构化查询的支持比较差。 数据库容量受到物理内存的限制，不适合用作海量数据的高性能读写，因此 Redis 适合的场景主要局限在较小数据量的操作。 Redis 较难支持在线扩容，在集群容量达到上限时在线扩容会变得很复杂。 1.3. redis 应用场景 缓存热点数据（最多使用）：关系型数据库作为存储层，其吞吐能力有限，由于 Redis 具有支撑高并发的特性，所以使用 Redis 保存一些热点数据进行缓存，如数据查询、短连接、新闻内容、商品内容等等。请求数据时合理使用缓存，通常能起到加速数据的读写速度和降低后端数据库压力的作用。 计数：利用 Redis 原子性的自增操作，可以实现快速计数、查询缓存的功能，同时数据可以异步落地到其他数据源。如：视频网站播放数，网站浏览数统计、统计用户点赞数、用户访问数等。 分布式集群架构中共享/存储 Session 或 token：一个分布式 Web 服务将用户的 Session 信息（例如用户登录信息）保存在各自服务器中，当使用负载均衡时，分布式服务会将用户的访问均衡到不同服务器上，用户刷新一次访问可能会请求到没有保存登陆 session 信息的服务，此时会出现需要重新登录的情况，这种操作对于用户十分不友好。为了解决多个服务器共享 Session 的问题，可以使用 Redis 将用户的 Session 进行集中管理，在这种模式下只要保证 Redis 是高可用和扩展性的，每次用户更新或者查询登录信息都直接从 Redis 中集中获取。 限速器：一般用于安全的考虑或者资源的控制，会在每次进行登录时，让用户输入手机验证码，从而确定是否是用户本人。但是为了短信接口不被频繁访问，会限制用户每分钟获取验证码的频率，例如一分钟不能超过 5 次。一些网站限制一个 IP 地址不能在一秒钟之内方问超过 n 次也可以采用类似限速的思路。Redis 可用于限制某个用户访问某个接口的频率，比如秒杀场景用于防止用户快速点击带来不必要的压力。 应用排行榜：按照热度排名，按照发布时间排行，主要用到列表和有序集合。 社交网络好友关系：利用集合的一些命令，如交集、并集、差集等，实现共同好友、共同爱好、赞、踩、粉丝、下拉刷新、聊天室的在线好友列表等功能。 简单的消息队列：可以使用 Redis 自身的发布/订阅模式或者 List 数据结构来实现简单的消息队列，实现异步操作。如：秒杀、抢购、12306等等 数据过期处理，可以精确到毫秒。 1.4. 重大版本 版本号第二位为奇数，为非稳定版本（2.7、2.9、3.1） 第二为偶数，为稳定版本（2.6、2.8、3.0） 当前奇数版本是下一个稳定版本的开发版本，如 2.9 是 3.0 的开发版本 1.5. Redis 与 Memcached 的区别 Redis 是单线程的，只使用单核；而 Memcached 是支持多线程，可以使用多核。 Redis 支持多种数据类型，提供 string，list，set，zset，hash 等数据结构的存储；而 MemCached 数据结构单一，只支持简单数据类型，仅用来缓存数据，需要客户端自己处理复杂对象。 Redis 支持数据持久化，宕机重启后，将自动加载宕机时刻的数据到缓存中，具有更好的灾备机制；MemCached 不支持数据持久化，重启后数据会消失。 Redis 提供主从同步机制和 cluster 集群部署能力，能够提供高可用服务；Memcached 没有提供原生的集群模式，需要依靠客户端实现往集群中分片写入数据（使用 Magent 在客户端进行一致性 hash 做分布式）。 Redis 的速度比 Memcached 快很多。 Redis 使用单线程的多路 IO 复用模型；Memcached 使用多线程的非阻塞 IO 模型。 Redis 的 Key 长度支持到 512k；Memcached 最大键的长度为 250 个字符，可以接受的储存数据不能超过 1MB（可修改配置文件变大）。 内存管理区别： Memcached 内存管理：使用 Slab Allocation。原理相当简单，预先分配一系列大小固定的组，然后根据数据大小选择最合适的块存储。避免了内存碎片。（缺点：不能变长，浪费了一定空间）memcached 默认情况下下一个 slab 的最大值为前一个的 1.25 倍。 Redis 内存管理：通过定义一个数组来记录所有的内存分配情况，Redis 采用的是包装的 malloc/free，相较于 Memcached 简单很多。由于 malloc 首先以链表的方式搜索已管理的内存中可用的空间分配，导致内存碎片比较多。 Redis 使用的是单线程模型，保证了数据按顺序提交；Memcache 需要使用 CAS 保证数据一致性(乐观锁)。 1.6. Redis 相关资料 Redis 官网 Redis 官方文档 Redis 官网国内中文翻译版 Redis 命令参考文档 Redisson 官网 Redis 在线测试 - Redis 官方命令在线测试工具 Redis 命令参考 - Redis Command Reference 和 Redis Documentation 的中文翻译版 2. Redis 数据结构介绍 Redis 是一种高级的 Key-Value 的存储系统，其中 Value 支持多种类型的数据结构：\n字符串（String）：二进制安全的字符串， 散列（Hash）：由field和关联的value组成的map数据结构。field和value都是字符串的。 列表（List）：按插入顺序排序的字符串元素的集合。基本上就是链表（linked lists）。 集合（Set）：不重复且无序的字符串元素的集合。 有序集合（SortedSet）：类似Set，但是每个字符串元素都关联到一个叫score浮动数值（floating number value）。里面的元素总是通过score进行着排序，所以不同的是，它是可以检索的一系列元素。 Bit arrays (或者说 simply bitmaps)：通过特殊的命令，可以将 String 值当作一系列 bits 处理：可以设置和清除单独的 bits，数出所有设为 1 的 bits 的数量，找到最前的被设为 1 或 0 的 bit，等等。 HyperLogLogs：这是被用于估计一个 set 中元素数量的概率性的数据结构。 2.1. Redis keys（键） Redis key值是二进制安全的，这意味着可以用任何二进制序列作为key值，如foo的简单字符、一个JPEG文件的内容、空字符串都是有效key值。关于key的定义，需要注意的几点：\nkey不要太长，最好不要操作1024个字节，这不仅会消耗内存还会降低查找效率 key不要太短，如果太短会降低key的可读性 在项目中，key最好有一个统一的命名规范，如：项目名_模块名_存储内容=\u0026quot;\u0026quot;、业务名:表名:id 2.2. String（字符串类型） 这是最简单 Redis 类型。值可以是任何种类的字符串（包括二进制数据），例如可以在一个键下保存一副 jpeg 图片。但值的长度不能超过 512MB。\n2.3. Hash（哈希） Redis 的 Hashes（哈希）类型，类似的Java中的哈希类型，数据结构，但是要注意，哈希类型中的映射关系叫作 field-value，注意这里的 value 是指 field 对应的值，不是键对应的值。\n2.4. Set（无序去重集合类型） Redis 的 Set 类型，无序去重的集合。Set 提供了交集、并集等方法。对于实现共同好友、共同关注等功能特别方便。\n2.5. List（有序可重复集合类型） Redis 的 List 类型，有序可重复的集合，底层是依赖双向链表实现的。\n2.6. SortedSet（有序去重集合类型） Redis 的 SortedSet 类型，相当于可排序的 Set 集合，内部维护了一个 score 的参数来实现排序。适用于排行榜和带权重的消息队列等场景。\n2.6.1. 有序集合底层实现数据结构 有序集合是由 ziplist (压缩列表) 或 skiplist (跳跃表) 组成的。\n压缩列表 ziplist 本质上就是一个字节数组，是 Redis 为了节约内存而设计的一种线性数据结构，可以包含多个元素，每个元素可以是一个字节数组或一个整数。 跳跃表 skiplist 是一种有序数据结构，它通过在每个节点中维持多个指向其他节点的指针，从而达到快速访问节点的目的。跳跃表支持平均 O(logN)、最坏 O(N) 复杂度的节点查找，还可以通过顺序性操作来批量处理节点。 当数据比较少时，有序集合是以压缩列表 ziplist 存储的（反之则以跳跃表 skiplist 存储），使用压缩列表存储必满足以下两个条件：\n有序集合保存的元素个数要小于 128 个； 有序集合保存的所有元素成员的长度都必须小于 64 字节。 如果不能满足以上两个条件中的任意一个，有序集合将会使用跳跃表 skiplist 结构进行存储。\n2.6.2. 跳表插入数据的过程 2.6.2.1. 随机层数 在理解跳跃表的添加流程前，需要了解一个概念：节点的随机层数。\n所谓的随机层数指的是每次添加节点之前，会先生成当前节点的随机层数，根据生成的随机层数来决定将当前节点存在几层链表中。\n2.6.2.2. 为什么这样设计 此设计的目的是为了保证 Redis 的执行效率。哪么为什么要生成随机层数，而不是制定一个固定的规则，比如上层节点是下层跨越两个节点的链表组成，如下图所示：\n如果制定了规则，那么就需要在添加或删除时，为了满足其规则，做额外的处理，比如添加了一个新节点，如下图所示：\n这样就不满足制定的上层节点跨越下层两个节点的规则了，就需要额外的调整上层中的所有节点，这样程序的效率就降低了，所以使用随机层数，不强制制定规则，这样就不需要进行额外的操作，从而也就不会占用服务执行的时间了。\n2.6.2.3. 添加元素的流程 Redis 中跳跃表的添加流程如下图所示：\n第一个元素添加到最底层的有序链表中（最底层存储了所有元素数据）。 第二个元素生成的随机层数是 2，所以再增加 1 层，并将此元素存储在第 1 层和最低层。 第三个元素生成的随机层数是 4，所以再增加 2 层，整个跳跃表变成了 4 层，将此元素保存到所有层中。 第四个元素生成的随机层数是 1，所以把它按顺序保存到最后一层中即可。 其他新增节点以此类推。\n2.7. Redis 集合类型存储数据的特点 2.7.1. 共同点 对于集合类型(List/Set/SortSet)，有如下共同点：\n如果元素都没有，那么这个 key 自动从 Redis 中删除 如果强行删除 key，那么原来的所有 value 也会被删除 2.7.2. SortedSet 和 List 异同点 相同点：\n都是有序的 都可以获得某个范围内的元素 不同点：\n列表基于链表实现，获取两端元素速度快，访问中间元素速度慢；有序集合基于散列表和跳跃表实现，访问中间元素时间复杂度是 OlogN 列表不能简单的调整某个元素的位置；有序列表可以调整，只需要更改元素的分数 有序集合更耗内存 2.8. Redis 特殊的数据类型 Bitmap：位图，可以认为是一个以位为单位数组，数组中的每个单元只能存0或者1，数组的下标在 Bitmap 中叫做偏移量。Bitmap 的长度与集合中元素个数无关，而是与基数的上限有关。 Hyperloglog：是用来做基数统计的算法，其优点是，在输入元素的数量或者体积非常非常大时，计算基数所需的空间总是固定的、并且是很小的。典型的使用场景是统计独立访客。 Geospatial：主要用于存储地理位置信息，并对存储的信息进行操作，适用场景如定位、附近的人等。 3. Jedis（Java 操作 Redis） 3.1. Jedis 概述 Jedis 就是 Java 语法操作 Redis 的技术，类似于JDBC\n3.2. Java 连接 Redis 导入jar包 commons-pool2-2.3.jar jedis-2.7.0.jar 3.3. Jedis 类相关方法 3.3.1. 构造方法 1 2 3 4 Jedis(String host, String port); // 获取Jedis对象 // host：Redis服务器ip地址 // port：Redis服务器端口 3.3.2. 常用方法 1 2 3 4 5 String set(String key, String value); // 设置键值，成功返回“ok” String get(String key, String value); // 根据键获取值 3.4. Jedis 连接池配置对象 构造方法 1 JedisPoolConfig config = new JedisPoolConfig(); 常用设置初始参数方法 1 2 3 4 5 void setMaxTotal(int maxTotal); // 设置连接池最大连接数，参数为int类型 void setMaxWaitMillis(long maxWaitMillis); // 设置最大等待时间，参数为long类型毫秒值 3.5. JedisPool 连接池对象 构造方法 1 2 3 4 JedisPool(JedisPoolConfig poolConfig, String host, int port); // poolConfig：连接池配置对象，需要设置相关初始化参数 // host：Redis数据库ip地址 // port：Redis数据库端口 JedisPool 常用方法 1 2 Jedis getResource(); // 获取Jedis对象 3.6. 单实例与 Jedis 连接池连接 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package lessonDemo; import org.junit.Test; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class TestJedis { // 单例连接 @Test public void testJedis() { // 设置ip地址和端口，获取jedis对象 Jedis jedis = new Jedis(\u0026#34;192.168.34.128\u0026#34;, 6379); // 设置数据 String n = jedis.set(\u0026#34;gender\u0026#34;, \u0026#34;man\u0026#34;); System.out.println(n); // 获取值 String value = jedis.get(\u0026#34;gender\u0026#34;); System.out.println(value); // 释放资源 jedis.close(); } // 连接池连接 @Test public void testJedisPool() { // 获取连接池配置对象，设置配置项 JedisPoolConfig config = new JedisPoolConfig(); // 最大连接数 config.setMaxTotal(30); // 最大空闲连接数 config.setMaxIdle(10); // 获取连接池 JedisPool jedisPool = new JedisPool(config, \u0026#34;192.168.34.128\u0026#34;, 6379); // 获取jedis对象 Jedis jedis = jedisPool.getResource(); // 设置数据 jedis.set(\u0026#34;java\u0026#34;, \u0026#34;kaka2\u0026#34;); System.out.println(jedis.get(\u0026#34;java\u0026#34;)); // 释放资源 jedis.close(); jedisPool.close(); } } 3.7. Jedis 连接池工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package com.moonzero.utils; import java.util.ResourceBundle; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; /** * Jedis连接工具类 */ public class JedisUtil { // 设置静态连接池成员变量 // 最大连接数 private static String maxTotal; // 最大等待时间 private static String maxWaitMillis; // Redis数据库ip地址 private static String host; // Redis数据库端口号 private static String port; // 定义连接池对象 private static JedisPool pool; // 静态代码块 static { // 创建ResourceBundle对象，读取redis.properites配置文件 ResourceBundle rb = ResourceBundle.getBundle(\u0026#34;jedis\u0026#34;); maxTotal = rb.getString(\u0026#34;maxTotal\u0026#34;); maxWaitMillis = rb.getString(\u0026#34;maxWaitMillis\u0026#34;); host = rb.getString(\u0026#34;host\u0026#34;); port = rb.getString(\u0026#34;port\u0026#34;); // Jedis配置对象，设置初始参数 JedisPoolConfig config = new JedisPoolConfig(); config.setMaxTotal(Integer.parseInt(maxTotal)); config.setMaxWaitMillis(Long.parseLong(maxWaitMillis)); // 获取Jedis连接池对象 pool = new JedisPool(config, host, Integer.parseInt(port)); } // 获取Jedis连接对象方法 public static Jedis getJedis() { return pool.getResource(); } } 配置文件jedis.properties\n1 2 3 4 maxTotal=10 maxWaitMillis=2000 host=192.168.34.128 port=6379 4. 持久化机制 4.1. 概述 Redis 的高性能是由于其将所有数据都存储在了内存中，为了使 Redis 在重启之后仍能保证数据不丢失，需要将数据从内存中同步到硬盘中，这一过程就是持久化。\nRedis 支持两种方式的持久化机制，RDB方式与AOF方式。可以单独使用其中一种或将二者结合使用。\nRDB 持久化（默认支持，无需配置）：该机制是指在指定的时间间隔内将内存中的数据集快照写入磁盘。 AOF 持久化：该机制将以日志的形式记录服务器所处理的每一个写操作，在 Redis 服务器启动之初会读取该文件来重新构建数据库，以保证启动后数据库中的数据是完整的。 无持久化：可以通过配置的方式禁用 Redis 服务器的持久化功能，即将 Redis 视为一个功能加强版的 memcached。 4.2. RDB 4.2.1. RDB 概述 RDB 持久化（Redis DataBase 缩写快照），在指定的时间间隔内把当前进程数据生成快照（.rdb）文件保存到硬盘的过程。Redis 启动时会读取 RDB 快照文件，将数据从硬盘载入内存。通过 RDB 方式的持久化，一旦 Redis 异常退出，就会丢失最近一次持久化以后更改的数据。类似于内存快照。\n4.2.2. RDB 触发方式 RDB 持久化有手动触发和自动触发的两种方式。\n4.2.2.1. 手动触发 手动触发有 save 和 bgsave 两命令\nsave 命令：阻塞当前 Redis 主线程，直到 RDB 持久化过程完成为止，若内存实例比较大会造成长时间阻塞，线上生产环境不建议使用。 bgsave 命令：redis 进程执行 fork 操作创建子线程，由子线程完成持久化，阻塞时间很短（微秒级），是 save 的优化，在执行 redis-cli shutdown 关闭 redis 服务时，如果没有开启 AOF 持久化，会自动执行 bgsave。显然 bgsave 是对 save 的优化。 4.2.2.2. 自动触发 根据配置规则进行自动快照，如 SAVE 100 10，100秒内至少有10个键被修改则进行快照。如果从节点执行全量复制操作，主节点会自动执行 BGSAVE 生成 RDB 文件并发送给从节点。默认情况下执行 shutdown 命令关闭 Reids 时，如果没有开启 AOF 持久化功能则自动执行 BGSAVE 命令。\n具体配置操作如下：\n修改配置文件 redis.conf，配置快照参数 1 2 3 save 900 1 # 每900秒(15分钟)至少有1个key发生变化，则dump内存快照。 save 300 10 # 每300秒(5分钟)至少有10个key发生变化，则dump内存快照 save 60 10000 # 每60秒(1分钟)至少有10000个key发生变化，则dump内存快照 设置保存位置设置 4.2.3. bgsave 持久化流程 bgsave 是主流的触发 RDB 持久化的方式，执行过程如下：\n执行 BGSAVE 命令。 Redis 父进程判断当前是否存在正在执行的子进程，如果存在，BGSAVE 命令直接返回。 父进程执行 fork 操作创建子进程，fork 操作过程中父进程会阻塞。 父进程 fork 完成后，父进程继续接收并处理客户端的请求，而子进程开始将内存中的数据写进硬盘的临时文件。 当子进程写完所有数据后会用该临时文件替换旧的 RDB 文件。 4.2.4. bgsave 如何实现快照的时候允许数据修改 主要是利用 bgsave 的子线程实现的，具体操作如下：\n如果主线程执行读操作，则主线程和 bgsave 子进程互相不影响； 如果主线程执行写操作，则被修改的数据会复制一份副本，然后 bgsave 子进程会把该副本数据写入 RDB 文件，在这个过程中，主线程仍然可以直接修改原来的数据。 4.2.5. RDB 文件备份与恢复操作 备份操作：\n1 2 3 4 127.0.0.1:6379\u0026gt; config set dir /usr/local ok 127.0.0.1:6379\u0026gt; bgsave 上述命令的含义是，先设置 rdb 文件保存路径，然后执行持久化后，将 dump.rdb 保存到 usr/local 下\n恢复操作：将 dump.rdb 放到 redis 安装目录与 redis.conf 同级目录，重启 redis 即可。\n4.2.6. RDB 优劣分析 优势：\n一旦采用该方式，那么整个 Redis 数据库将只包含一个文件（dump.rdb），这对于文件备份而言是非常完美的。比如可能打算每个小时归档一次最近24小时的数据，同时还要每天归档一次最近30天的数据。通过这样的备份策略，一旦系统出现灾难性故障，可以非常容易的进行恢复。 对于备份、全量复制、灾难恢复而言，RDB 是非常不错的选择。因为可以非常轻松的将一个单独的二进制文件压缩后再转移到其它存储介质上。 性能最大化。对于 Redis 的服务进程而言，在开始持久化时，它唯一需要做的只是fork（分叉）出子进程，之后再由子进程完成这些持久化的工作，这样就可以极大的避免服务主进程执行IO操作了。 恢复数据效率高。相比于 AOF 机制，如果数据集很大，加载 RDB 恢复数据效率远快于 AOF 方式。 劣势：\n无法做到实时持久化，因为系统一旦在定时持久化之前出现宕机现象，此前没有来得及写入磁盘的数据都将丢失。如果想保证数据的高可用性，即最大限度的避免数据丢失，那么 RDB 将不是一个很好的选择。 由于 RDB 是通过 fork 子进程来协助完成数据持久化工作的，每次都要创建子进程，频繁操作成本过高。因此，如果当数据集较大时，可能会导致整个服务器停止服务几百毫秒，甚至是1秒钟 RDB 文件使用特定二进制格式保存，Redis 版本升级过程中有多个格式的 RDB 版本，会存在老版本 Redis 无法兼容新版 RDB 格式的问题。 4.3. AOF 4.3.1. AOF 概述 针对 RDB 不适合实时持久化，redis 提供了 AOF 持久化（Append Only File）方式。以独立日志的方式记录每次写命令，Redis 重启时会重新执行 AOF 文件中的命令达到恢复数据的目的。\nAOF 的主要作用是解决了数据持久化的实时性，AOF 是 Redis 持久化的主流方式。开启 AOF 方式持久化后每执行一条写命令，Redis 就会将该命令写进 aof_buf 缓冲区，AOF 缓冲区根据对应的策略向硬盘做同步操作。类似于追加日志文件 binlog。\n4.3.2. AOF 配置详解 4.3.2.1. 开启 AOF 默认情况下 Redis 没有开启 AOF 方式的持久化，通过 appendonly 参数启用。\n修改 redis.conf 设置文件，修改appendonly yes (旧的版本默认 AOF 处于关闭，为 no；在 Redis 6.0 之后已经默认是开启) 1 appendonly yes # 启用 aof 持久化方式 4.3.2.2. AOF 的同步策略选择 通过 appendfsync 参数设置同步策略(时机)，可选值如下：\nalways：主线程调用 write 执行写操作后，后台线程（ aof_fsync 线程）立即会调用 fsync 函数同步 AOF 文件（刷盘），fsync 完成后线程返回，这样会严重降低 Redis 的性能（write + fsync）。 everysec：默认策略，线程调用 write 执行写操作后立即返回，由后台线程（ aof_fsync 线程）每秒钟调用 fsync 函数（系统调用）同步一次 AOF 文件（write+fsync，fsync间隔为 1 秒） no：主线程调用 write 执行写操作后立即返回，让操作系统决定何时进行同步，Linux 下一般为 30 秒一次（write 但不 fsync，fsync 的时机由操作系统决定）。 默认情况下系统每30秒会执行一次同步操作。为了防止缓冲区数据丢失，可以在 Redis 写入 AOF 文件后主动要求系统将缓冲区数据同步到硬盘上。\n修改 redis.conf 设置文件，设置 appendfsync 参数\n1 2 3 # appendfsync always # 每收到写命令就立即强制写入磁盘，最慢的，但是保证完全的持久化，不推荐使用 appendfsync everysec # 每秒强制写入磁盘一次，性能和持久化方面做了折中，推荐 # appendfsync no # 完全依赖 os，性能最好,持久化没保证（操作系统自身的同步） 4.3.2.3. aof 文件名称配置 默认文件名：appendfilename \u0026quot;appendonly.aof\u0026quot;。可以修改为指定文件名称\n4.3.3. AOF 重写 当 AOF 变得太大时，Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件，这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样，但体积更小。\nAOF 重写（rewrite） 是一个有歧义的名字，该功能是通过读取数据库中的键值对来实现的，程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。\n由于 AOF 文件记录每次操作命令，因此会比 RDB 文件大的多。AOF 会记录对同一个 key 的多次写操作，但只有最后一次写操作才有意义。Redis 提供了 AOF 文件重写功能，用最少的命令达到相同效果。由于 AOF 重写会进行大量的写入操作，为了避免对 Redis 正常处理命令请求造成影响，Redis 将 AOF 重写程序放到子进程里执行。\nAOF 文件重写期间，Redis 还会维护一个 AOF 重写缓冲区，该缓冲区会在子进程创建新 AOF 文件期间，记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后，服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾，使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。最后，服务器用新的 AOF 文件替换旧的 AOF 文件，以此来完成 AOF 文件重写操作。\nTips: Redis 7.0 版本之前，如果在重写期间有写入命令，AOF 可能会使用大量内存，重写期间到达的所有写入命令都会写入磁盘两次。7.0 版本之后，AOF 重写机制得到了优化改进。\n可以通过命令方式与配置方式，开启 AOF 重写功能\n命令方式手动开启： 1 BGREWRITEAOF 修改 redis.conf 文件以下配置项，让程序自动决定触发时机，Redis 也会在触发阈值时自动去重写 AOF 文件。 auto-aof-rewrite-min-size：如果 AOF 文件大小小于该值，则不会触发 AOF 重写。默认值为 64 MB。 auto-aof-rewrite-percentage：执行 AOF 重写时，当前 AOF 大小（aof_current_size）和上一次重写时 AOF 大小（aof_base_size）的比值。如果当前 AOF 文件大小增加了这个百分比值，将触发 AOF 重写。将此值设置为 0 将禁用自动 AOF 重写。默认值为 100。 redis.conf 配置示例：\n1 2 3 no-appendfsync-on-rewrite yes # 正在导出 rdb 快照的过程中，要不要停止同步 aof auto-aof-rewrite-percentage 100 # aof 文件大小比起上次重写时的大小，增长率100%时，触发重写 auto-aof-rewrite-min-size 64mb # aof 文件大小至少超过 64M 时，触发重写 4.3.4. AOF 流程说明 AOF 持久化执行流程：命令写入(append)、文件写入(write)、文件同步(sync)、文件重写(rewrite)、重启加载(load)\n命令追加（append）：所有的写入命令(如：set、hset 等)会 append（追加）到 aof_buf（缓冲区）中 文件写入（write）：将 aof_buf（缓冲区）的数据写入到 AOF 文件中。这一步需要调用 write 函数（系统调用），将数据写入到了系统内核缓冲区之后直接返回了（延迟写）。注意！！！此时并没有同步到磁盘。 文件同步（fsync）：AOF 缓冲区根据对应的策略（fsync 策略），向硬盘做 sync（同步）操作。这一步需要调用 fsync 函数（系统调用），fsync 针对单个文件操作，对其进行强制硬盘同步，fsync 将阻塞直到写入磁盘完成后返回，保证了数据持久化。 文件重写（rewrite）：随着 AOF 文件越来越大，需要定期对 AOF 文件进行 rewrite（重写），达到压缩文件体积的目的。AOF 文件重写是把 Redis 进程内的数据转化为写命令同步到新 AOF 文件的过程。 重启加载（load）：当 Redis 服务器重启时，可以 load（加载）AOF 文件进行数据恢复。 Tips: Linux 系统直接提供了一些函数用于对文件和设备进行访问和控制，这些函数被称为系统调用（syscall）。\n4.3.5. AOF 恢复操作 设置 appendonly yes 将 appendonly.aof 文件放到 dir 参数指定的目录 启动 Redis，Redis 会自动加载 appendonly.aof 文件 4.3.6. AOF 优劣分析 优势：\n该机制可以带来更高的数据安全性，即数据持久性。Redis中提供了3中同步策略，即每秒同步、每修改同步和不同步。 每秒同步是异步完成的，其效率也是非常高的，区别只是一旦系统出现宕机现象，那么这一秒钟之内修改的数据将会丢失。 每修改同步，可以将其视为同步持久化，即每次发生的数据变化都会被立即记录到磁盘中。可以预见，这种方式在效率上是最低的。 无同步，完全依赖操作系统自身的同步，因此持久化没保证，但性能最好。 由于该机制对日志文件的写入操作采用的是 append-only 模式，所以没有磁盘寻址的开销，写入性能非常高。即使在写入过程中即使出现宕机现象，也不会破坏日志文件中已经存在的内容。然而如果本次操作只是写入了一半数据就出现了系统崩溃问题，也可以在 Redis 下一次启动之前，可以通过 redis-check-aof 工具来帮助解决数据一致性的问题。 如果日志过大，Redis 可以自动启用 rewrite 机制。即 Redis 以 append 模式不断的将修改数据写入到老的磁盘文件中，同时 Redis 还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行 rewrite 切换时可以更好的保证数据安全性。AOF 文件没被 rewrite 之前（文件过大时会对命令进行合并重写），可以删除其中的某些命令（比如误操作的 flushall ） AOF 包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上也可以通过该文件完成数据的重建。 劣势：\n对于相同数量的数据集而言，AOF 文件通常要大于 RDB 文件，且恢复速度慢。 根据同步策略的不同，AOF 在运行启动效率上往往会慢于 RDB。总之，每秒同步策略的效率是比较高的，同步禁用策略的效率和 RDB 一样高效。 4.3.7. Multi Part AOF 机制 从 Redis 7.0.0 开始，Redis 使用了 Multi Part AOF 机制。即将原来的单个 AOF 文件拆分成多个 AOF 文件。在 Multi Part AOF 中，AOF 文件被分为三种类型：\nBASE：表示基础 AOF 文件，它一般由子进程通过重写产生，该文件最多只有一个。 INCR：表示增量 AOF 文件，它一般会在 AOFRW 开始执行时被创建，该文件可能存在多个。 HISTORY：表示历史 AOF 文件，它由 BASE 和 INCR AOF 变化而来，每次 AOFRW 成功完成时，本次 AOFRW 之前对应的 BASE 和 INCR AOF 都将变为 HISTORY，HISTORY 类型的 AOF 会被 Redis 自动删除。 4.3.8. AOF 校验机制 AOF 校验机制是 Redis 在启动时对 AOF 文件进行检查，以判断文件是否完整，是否有损坏或者丢失的数据。这个机制的原理其实非常简单，就是通过使用一种叫做校验和（checksum） 的数字来验证 AOF 文件。这个校验和是通过对整个 AOF 文件内容进行 CRC64 算法计算得出的数字。如果文件内容发生了变化，那么校验和也会随之改变。因此，Redis 在启动时会比较计算出的校验和与文件末尾保存的校验和（计算的时候会把最后一行保存校验和的内容给忽略点），从而判断 AOF 文件是否完整。如果发现文件有问题，Redis 就会拒绝启动并提供相应的错误信息。AOF 校验机制十分简单有效，可以提高 Redis 数据的可靠性。\nTips: 类似地，RDB 文件也有类似的校验机制来保证 RDB 文件的正确性。\n4.3.9. AOF 为什么是在执行完命令之后记录日志？ 关系型数据库（如 MySQL）通常都是执行命令之前记录日志（方便故障恢复），而 Redis AOF 持久化机制是在执行完命令之后再记录日志。\n先执行完命令再记录日志的原因：\n避免额外的检查开销，AOF 记录日志不会对命令进行语法检查。 在命令执行完之后再记录，不会阻塞当前的命令执行。 先执行完命令再记录日志的风险：\n数据可能会丢失：如果 Redis 刚执行完命令，此时发生故障宕机，会导致这条命令存在丢失的风险。 可能阻塞其他操作：AOF 记录日志也是在主线程中执行，所以当 Redis 把日志文件写入磁盘的时候，可能会阻塞后续的其他命令操作无法执行。 4.4. RDB 与 AOF 比较 RDB 和 AOF 各有自己的优缺点，如果对数据安全性要求较高，在实际开发中往往会结合两者来使用。\nRDB 比 AOF 优秀的地方：\nRDB 文件存储的内容是经过压缩的二进制数据， 保存着某个时间点的数据集，文件很小，适合做数据的备份，灾难恢复。AOF 文件存储的是每一次写命令，类似于 MySQL 的 binlog 日志，通常会比 RDB 文件大很多。当 AOF 变得太大时，Redis 能够在后台自动重写 AOF。新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样，但体积更小。不过，Redis 7.0 版本之前，如果在重写期间有写入命令，AOF 可能会使用大量内存，重写期间到达的所有写入命令都会写入磁盘两次。 使用 RDB 文件恢复数据，直接解析还原数据即可，不需要一条一条地执行命令，速度非常快。而 AOF 则需要依次执行每个写命令，速度非常慢。也就是说，与 AOF 相比，恢复大数据集的时候，RDB 速度更快。 AOF 比 RDB 优秀的地方：\nRDB 的数据安全性不如 AOF，没有办法实时或者秒级持久化数据。生成 RDB 文件的过程是比较繁重的， 虽然 BGSAVE 子进程写入 RDB 文件的工作不会阻塞主线程，但会对机器的 CPU 资源和内存资源产生影响，严重的情况下甚至会直接把 Redis 服务干宕机。AOF 支持秒级数据丢失（取决 fsync 策略，如果是 everysec，最多丢失 1 秒的数据），仅仅是追加命令到 AOF 文件，操作轻量。 RDB 文件是以特定的二进制格式保存的，并且在 Redis 版本演进中有多个版本的 RDB，所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题。 AOF 以一种易于理解和解析的格式包含所有操作的日志。你可以轻松地导出 AOF 文件进行分析，你也可以直接操作 AOF 文件来解决一些问题。比如，如果执行 FLUSHALL 命令意外地刷新了所有内容后，只要 AOF 文件没有被重写，删除最新命令并重启即可恢复之前的状态。 4.4.1. 重启时恢复加载顺序及流程比较 当 AOF 和 RDB 文件同时存在时，优先加载 若关闭了 AOF，加载 RDB 文件 加载 AOF/RDB 成功，redis 重启成功 AOF/RDB 存在错误，redis 启动失败并打印错误信息 4.4.2. RDB 和 AOF 如何选择 通常来说，建议同时开启 RDB 和 AOF 持久化或者开启 RDB 和 AOF 混合持久化，以保证数据安全。\n如果数据不敏感，且可以从其他地方重新生成，可以关闭持久化。 如果数据比较重要，且能够承受几分钟的数据丢失，比如缓存等，只需要使用 RDB 即可。 如果是用做内存数据，要使用 Redis 的持久化，建议是 RDB 和 AOF 都开启。 如果只用 AOF，优先使用 everysec 的配置选择，因为它在可靠性和性能之间取了一个平衡。 当 RDB 与 AOF 两种方式都开启时，Redis 会优先使用 AOF 恢复数据，因为 AOF 保存的文件比 RDB 文件更完整。\n4.5. Redis 4.0 持久化机制的优化 由于 RDB 和 AOF 各有优势，于是，Redis 4.0 开始支持 RDB 和 AOF 的混合持久化（默认关闭，可以通过配置项 aof-use-rdb-preamble 开启）。此方式的优缺点如下：\n优点：如果把混合持久化打开，AOF 重写的时候就直接把 RDB 格式的内容写到 AOF 文件开头。这样可以结合 RDB 和 AOF 的优点，使得 Redis 可以快速加载同时避免丢失过多数据的风险。\n缺点：\n实现复杂度高：混合持久化需要同时维护 RDB 文件和 AOF 文件，因此实现复杂度相对于单独使用 RDB 或 AOF 持久化方式要高。 可读性差：AOF 文件中添加了 RDB 格式的内容，其压缩格式不再是 AOF 格式，使得 AOF 文件的可读性变得很差。 兼容性差：如果开启混合持久化，那么此混合持久化 AOF 文件，就不能用在 Redis 4.0 之前版本。 小结：Redis 混合持久化方式适合用于需要兼顾启动速度和减低数据丢失的场景。但需要注意的是，混合持久化的实现复杂度较高、可读性差，只能用于 Redis 4.0 以上版本，因此在选择时需要根据实际情况进行权衡。\n可参考官方文档地址：https://redis.io/topics/persistence\n5. Redis 事务 5.1. 概述 因为 Redis 是单线程的，所以 Redis 的单条命令是原子性执行的，不可再分，要么执行成功，要么执行失败。提供的所有 API 都是原子操作。\nRedis 支持分布式环境下的事务操作，其事务可以一次执行多个命令，其特征是：在事务中的所有命令都会串行化的顺序执行，并且在执行过程中，不会被其他客户端发送来的命令请求打断。服务器在执行完事务中的所有命令之后，才会继续处理其他客户端的其他命令。\nTODO: 『如果在一个事务中的命令出现错误，那么所有的命令都不会执行』？？这个待确认！\n5.2. Redis 事务的特性 原子性：事务不保证原子性，并且没有回滚。即事务中如果有某一条命令执行失败，其后的命令仍然会被继续执行不会被影响。 隔离性：Redis 是单进程程序，并且它保证在执行事务时，不会对事务进行中断，事务可以运行直到执行完所有事务队列中的命令为止。因此 Redis 的事务是总是带有隔离性的。 5.3. 事务的执行流程 Redis 的事务操作分为开启事务、命令入队列、执行事务三个阶段。执行流程如下图：\n事务的生命周期如下：\n事务开启：客户端执行 multi 命令开启事务。 提交请求：客户端提交任意多条命令到事务。 任务入队列：Redis 将客户端所有请求都放入事务队列中等待执行。 入队状态反馈：服务器返回 QURUD，表示命令已被放入事务队列。 执行命令：客户端通过 exec 命令来执行事务块内所有命令。按命令执行的先后顺序排列，返回事务块内所有命令的返回值。当操作被打断时，返回空值 nil。 事务执行错误：在 Redis 事务中如果某条命令执行错误，则其他命令会继续执行，不会回滚。可以通过 watch 监控事务执行的状态并处理命令执行错误的异常情况。 执行结果反馈：服务器向客户端返回事务执行的结果。 Notes: 通过调用 DISCARD 命令，客户端可以清空事务队列，并放弃执行事务，并且客户端会从事务状态中退出。\n5.4. 事务相关命令 Notes: 此部分内容详见《Redis 操作命令》笔记\n5.5. 基于 Spring Boot 的实现事务 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 private final RedisTemplate redisTemplate; public void transactionSet(Map\u0026lt;String, Objects\u0026gt; commandList) { // 1. 开启事务权限 redisTemplate.setEnableTransactionSupport(true); try { // 2. 开启事务 redisTemplate.multi(); // 3. 执行事务命令 for (Map.Entry\u0026lt;String, Objects\u0026gt; entry : commandList.entrySet()) { String key = entry.getKey(); Objects value = entry.getValue(); redisTemplate.opsForValue().set(key, value); } // 4. 成功提交 redisTemplate.exec(); } catch (Exception e) { // 5. 失败则回滚 redisTemplate.discard(); } } 以上示例方法接收事务命令 commandList 并以事务命令列表在一个事务中执行。具体步骤为：开启事务权限、开启事务、执行事务命令、提供事务和回滚事务。\n6. Redis 消息订阅与发布 6.1. 概述 Redis 发布、订阅是一种消息通信模式：发送者（Pub）向频道（Channel）发送消息；订阅者（Sub）接收频道上的消息。Redis 客户端可以订阅任意数量的频道，发送者也可以向任意频道发送数据。\n上图是，1个发送者（pub1）、1个频道（channe0）和 3个订阅者（sub1、sub2、sub3）的关系。由于 3 个订阅者 sub1、sub2、sub3 都订阅了频道 channel0，在发送者 pub1 向频道 channel0 发送一条消息后，这条消息就会被发送给订阅它的三个客户端。\n6.2. 订阅与发布相关命令 Notes: 此部分内容详见《Redis 操作命令》笔记\n7. Redis 的内存管理机制 7.1. 获取当前最大内存与动态设置最大内存值 获取最大内存：\n1 config get maxmemory 使用命令方式，设置最大内存：\n1 config set maxmemory 1GB 7.2. 过期键的删除策略 Redis 是 key-value 数据库，可以设置 Redis 中缓存的 key 的过期时间。Redis 的过期策略就是指当 Redis 中缓存的 key 过期了，过期策略通常有以下三种：\n定时过期：每个设置过期时间的 key 都需要创建一个定时器，到过期时间就会立即清除。该策略可以立即清除过期的数据，对内存很友好；但是会占用大量的CPU资源去处理过期的数据，从而影响缓存的响应时间和吞吐量。 惰性过期：只有当访问一个 key 时，才会判断该 key 是否已过期，过期则清除。该策略可以最大化地节省 CPU 资源，却对内存非常不友好。极端情况可能出现大量的过期 key 没有再次被访问，从而不会被清除，占用大量内存。 定期过期：每隔一定的时间，会扫描一定数量的数据库的 expires 字典中一定数量的 key，并清除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时，可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。至于要删除多少过期键，以及要检查多少个数据库，则由内部算法决定。 内存不足时过期：Redis 通过 maxmemory 参数设置最大内存的限制，当使用的内存超过了设置的最大内存，就要进行内存释放。在进行内存释放的时候，会按照配置的淘汰策略清理内存。 Redis 中同时使用了惰性过期和定期过期两种过期策略。值得注意的是，定期过期每次间隔并不是将所有的key检查一次，而是随机抽取进行检查。主要考虑到全量Key检查会影响性能，因此需要配合惰性过期，在获取某个key的时候再检查一次是否过期，从而确保消除定期随机检查时没有检查出来的过期 key。\nNotes: expires 字典会保存所有设置了过期时间的 key 的过期时间数据，其中，key 是指向键空间中的某个键的指针，value 是该键的毫秒精度的 UNIX 时间戳表示的过期时间。键空间是指该 Redis 集群中保存的所有键\n7.3. 内存淘汰策略 如果 Redis 使用的内存达到设置的上限，默认 Redis 的写命令会返回错误信息，但是读命令还可以正常返回。此时 Redis 会触发内存淘汰策略，删除一些不常用的旧数据。\nRedis 的内存淘汰策略是指在 Redis 的用于缓存的内存不足时，怎么处理需要新写入且需要申请额外空间的数据。内存淘汰策略可通过配置文件中配置项 maxmemory-policy 来修改，默认配置是 no-eviction。\n7.3.1. Redis 的数据淘汰策略 Redis 提供 6 种数据淘汰策略：\n全局的键空间选择性移除\nno-eviction：禁止删除数据，当内存不足以容纳新写入数据时，新写入操作会报错。 allkeys-random：当内存不足以容纳新写入数据时，在键空间中，随机移除某个 key。 allkeys-lru：当内存不足以容纳新写入数据时，在键空间中，移除最近最少使用的 key。（这个是最常用的） 设置过期时间的键空间选择性移除\nvolatile-random：当内存不足以容纳新写入数据时，在设置了过期时间的键空间中，随机移除某个 key。 volatile-lru：当内存不足以容纳新写入数据时，在设置了过期时间的键空间中，利用 LRU 算法移除设置了过期时间的 key。 volatile-ttl：当内存不足以容纳新写入数据时，在设置了过期时间的键空间中，有更早过期时间的 key 优先移除。 Redis v4.0 版本后新增了 2 种淘汰机制：\nvolatile-lfu：最少使用，从已设置过期时间的数据集中挑选最不经常使用的数据淘汰。 allkeys-lfu：当内存不足以容纳新写入数据时，从数据集中移除最不经常使用的 key。 Tips:\nvolatile 前缀的策略是对已设置过期时间的数据集淘汰数据；allkeys 前缀的策略是对全部数据集淘汰数据；后缀的 lru、ttl、random 则是三种不同的淘汰策略；还有一种特殊 no-enviction 永不回收的策略。 LRU（Least Recently Used）：最近使用次数最少 LFU（Least Frequently Used）：最不常用 7.3.2. 淘汰策略使用建议 如果数据呈现幂律分布，也就是一部分数据访问频率高，一部分数据访问频率低，即业务有明显的冷热数据区分，则建议优先使用 allkeys-lru 淘汰策略。充分利用 LRU 算法的优势，把最近最常访问的数据留在缓存中。 如果数据呈现平等分布，也就是所有的数据访问频率都相同。即业务中数据访问频率差别不大，没有明显冷热数据区分。则使用 allkeys-random 随机淘汰策略。 如果业务中有置顶的需求，可以使用 volatile-lru 策略，同时置顶数据不设置过期时间，这些数据就一直不被删除，会淘汰其他设置过期时间的数据。 如果业务中有短时高频访问的数据，可以使用 allkeys-lfu 或 volatile-lfu 策略。 7.3.3. 注意事项 Redis 的内存淘汰策略的选取并不会影响过期的 key 的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据，而过期策略用于处理过期的缓存数据。 如果没有设置 expire 的 key，而 volatile 相关的策略是从到了过期时间的键中进行筛选，因此 volatile-lru, volatile-random 和 volatile-ttl 策略的行为和 noeviction(不删除) 基本上一致。 8. Redis 的线程模型 Redis 基于 Reactor 模式开发了网络事件处理器，这个处理器被称为文件事件处理器。它的组成结构为4部分：\n多个套接字 IO多路复用程序 文件事件分派器 事件处理器 因为文件事件分派器队列的消费是单线程的，所以 Redis 才叫单线程模型。\n文件事件处理器使用I/O多路复用（multiplexing）程序来同时监听多个套接字，并根据套接字目前执行的任务来为套接字关联不同的事件处理器。 当被监听的套接字准备好执行连接 accept、read、write、close 等操作时，与操作相对应的文件事件就会产生，这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。 虽然文件事件处理器以单线程方式运行，但通过使用 I/O 多路复用程序来监听多个套接字，文件事件处理器既实现了高性能的网络通信模型，又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接，这保持了 Redis 内部单线程设计的简单性。\n8.1. 为何选择单线程模型？ 避免过多的上下文切换开销：程序始终运行在进程中单个线程内，没有多线程切换的场景。 避免同步机制的开销：如果 Redis 选择多线程模型，需要考虑数据同步的问题，则必然会引入某些同步机制，会导致在操作数据过程中带来更多的开销，增加程序复杂度的同时还会降低性能。 实现简单，方便维护：如果 Redis 使用多线程模式，那么所有的底层数据结构的设计都必须考虑线程安全问题，那么 Redis 的实现将会变得更加复杂。 8.2. Redis 6.0 版本后为何引入多线程 Redis 支持多线程主要有两个原因：\n可以充分利用服务器 CPU 资源，旧版本中单线程模型的主线程只能利用一个 cpu。 多线程任务可以分摊 Redis 同步 IO 读写的负荷。 9. Redis 分区（待了解） 9.1. 为什么要做 Redis 分区 分区可以让 Redis 使用所有机器的内存，管理更大的内存。如果没有分区，最多只能使用一台机器的内存。分区使 Redis 的计算能力通过简单地增加计算机而得到成倍提升，Redis 的网络带宽也会随着计算机和网卡的增加而成倍增长。\n9.2. Redis 分区实现方案 客户端分区，就是在客户端就已经决定数据会被存储到哪个 redis 节点或者从哪个 redis 节点读取。大多数客户端已经实现了客户端分区。 代理分区，意味着客户端将请求发送给代理，然后代理根据分区规则决定请求哪些 Redis 实例，决定到哪个节点写数据或者读数据，然后根据 Redis 的响应结果返回给客户端。redis 和 memcached 的一种代理实现就是 Twemproxy。 查询路由(Query routing)，是客户端随机地请求任意一个 redis 实例，然后由 Redis 将请求转发给正确的节点。Redis Cluster 实现了一种混合形式的查询路由，但并不是直接将请求从一个 redis 节点转发到另一个 redis 节点，而是在客户端的帮助下直接 redirected 到正确的 redis 节点。 9.3. Redis 分区的缺点 通常不支持涉及多个 key 的操作。例如不能对两个集合求交集，因为他们可能被存储到不同的 Redis 实例（实际上这种情况也有办法，但是不能直接使用交集指令）。 同时操作多个 key，则不能使用 Redis 事务。 分区使用的粒度是 key，不能使用一个非常长的排序 key 存储一个数据集（The partitioning granularity is the key, so it is not possible to shard a dataset with a single huge key like a very big sorted set）。 当使用分区的时候，数据处理会非常复杂，例如为了备份，必须从不同的 Redis 实例和主机同时收集 RDB/AOF 文件。 分区时动态扩容或缩容可能非常复杂。Redis 集群在运行时增加或者删除 Redis 节点，能做到最大程度对用户透明地数据再平衡，但其他一些客户端分区或者代理分区方法则不支持这种特性。然而有一种预分片的技术也可以较好的解决这个问题。 10. Redis 最佳实践（整理中） 10.1. 键名的生产实践 Redis 没有命令空间，而且也没有对键名有强制要求。设计合理的键名，有利于防止键冲突和项目的可维护性。\n推荐使用键命名方式具有可读性和可管理性，建议以业务名(或数据库名)为前缀(防止key冲突)，用冒号分隔。例如：业务名:对象名:id:[属性] 推荐保持键的简洁性。在保证语义的前提下，控制key的长度，当key较多时，内存占用也不容忽视。例如：user:{uid}:friends:messages:{mid}可简化为u:{uid}:fr:m:{mid}，从而减少由于键过长的内存浪费 不能包含特殊字符。反例：包含空格、换行、单双引号以及其他转义字符 10.2. Redis 如何与数据库保持双写一致性 保证缓存和数据库的双写一致性，有以下几种同步策略：\n先更新缓存再更新数据库 与 先更新数据库再更新缓存（均不推荐） 更新数据库的同时也手动更新缓存，无论更新缓存是前还是后，其优点是每次数据变化时都能及时地更新缓存。但这种操作的消耗很大，如果数据需要经过复杂的计算再写入缓存的话，频繁的更新缓存会影响到服务器的性能。如果是写入数据比较频繁的场景，可能会导致频繁的更新缓存却没有业务来读取该数据。所以这两种方式均不推荐\n先删除缓存再更新数据库： 先删除缓存的优点是操作简单，无论更新的操作复杂与否，都是直接删除缓存中的数据。更新数据库后，等后续新的读请求获取数据库的最新值，再写入缓存中。\n存在问题：删除缓存数据之后，更新数据库完成之前，这个时间段内如果有别的读请求，就会从数据库读取旧数据后并重新写到缓存中，并且后续读取都是旧数据，再次造成数据不一致。\n先更新数据库再删除缓存： 先更新数据库成功后，再删除缓存，后续读取请求时再将新数据回写缓存。\n存在问题：更新数据库和删除缓存这段时间内，别的读请求还是缓存的旧数据，但等数据库更新完成并删除缓存后，就会恢复数据一致，这种影响相对比较小。还一种情况就是，如果出现操作数据库但删除缓存失败的话，也会造成数据不一致，此时一般会采用异步的重试机制来删除旧的缓存。\n异步更新缓存： 数据库的更新操作完成后不直接操作缓存，而是把这个操作命令封装成消息发送到消息队列中，然后由 Redis 去消费更新数据，消息队列可以保证数据操作顺序一致性，确保缓存系统的数据正常。\n存在问题：这种方式的代码开发与部署成本都变高，因为需要引入消息中间件并且要编写服务生产与消费相关逻辑代码。\n延迟双删： 先删除缓存，再修改数据库，最后再延迟将最新值更新到缓存。这种方案可以防止在某个线程在删除缓存后修改数据库前，有其他线程查询缓存发现没有数据，去查询数据库旧的数据并更新到缓存。这里第二次删除缓存时采取延迟的我的做法，是因为数据库可能涉及主从架构，存在数据同步的延迟。\n综上分析后结论是：『先更新数据库再删除缓存』是影响更小的方案。如果第二步出现失败的情况，则可以采用重试机制解决问题。\n10.3. Redis 常见性能问题和解决方案（使用中再总结迭代） Master 库最好不要做任何持久化工作，如 RDB 内存快照和 AOF 日志文件。如果 Master 写内存快照，save 命令调度 rdbSave 函数，会阻塞主线程的工作，当快照比较大时会间断性暂停服务，影响性能。 如果数据比较重要，让某个 Slave 开启 AOF 备份数据，策略设置为每秒同步一次。 为了主从复制的速度和连接的稳定性， Master 和 Slave 最好在同一个局域网内。 尽量避免在压力很大的主库上增加从库 主从复制不要用图状结构，用单向链表结构更为稳定，即： Master \u0026lt;- Slave1 \u0026lt;- Slave2 \u0026lt;- Slave3...。这种结构方便解决单点故障问题，实现 Slave 对 Master 的替换。如果 Master 挂了，可以立刻启用 Slave1 做 Master，其他不变。 10.4. Redis 持久化数据和缓存如何扩容 如果 Redis 被当做缓存使用，使用一致性哈希实现动态扩容缩容。 如果 Redis 被当做一个持久化存储使用，必须使用固定的 keys-to-nodes 映射关系，节点的数量一旦确定不能变化。否则的话（即 Redis 节点需要动态变化的情况），必须使用可以在运行时进行数据再平衡的一套系统，而当前只有 Redis 集群可以做到这样。 11. Redis 扩展知识 11.1. Redis 网络模型 Redis 通过『IO多路复用+事件派发』来提高网络性能，并且支持各种不同的多路复用实现，并且将这些实现进行封装，提供了统一的高性能事件库。\nI/O 多路复用是指利用单个线程来同时监听多个 Socket，并在某个 Socket 可读、可写时得到通知，从而避免无效的等待，充分利用 CPU 资源。目前的 I/O 多路复用都是采用的 epoll 模式实现，它会在通知用户进程 Socket 就绪的同时，把已就绪的 Socket 写入用户空间，不需要挨个遍历 Socket 来判断是否就绪，提升了性能。\n其中 Redis 的网络模型就是使用 I/O 多路复用结合事件的处理器来应对多个 Socket 请求。比如，提供了连接应答处理器、命令回复处理器，命令请求处理器；\n在 Redis 6.0 之后，为了提升更好的性能，在命令回复处理器使用了多线程来处理回复事件，在命令请求处理器中，将命令的转换使用了多线程，增加命令转换速度，在命令执行的时候，依然是单线程。\n11.2. Redis 中的 Copy On Write 技术 单线程的 Redis 是通过 Copy On Write 技术来实现一边响应主线程的任务，一边持久化数据。具体是依赖系统的 fork 函数的 Copy On Write 实现，实现过程如下：\n在执行 RDB 持久化时，Redis 进程会 fork 一个子进程来执行持久化，该过程是阻塞的。 当 fork 过程完成后，父进程会继续接收客户端的命令。 此时子进程与 Redis 主进程共享内存中的数据，但是子进程并不会修改内存中的数据，而是不断的遍历读取并写入数据到磁盘，也就是持久化数据的过程。 然而 Redis 主进程则不一样，它需要响应客户端的命令，如果收到写入数据的操作请求，主进程就会使用 COW 机制将数据先复制再修改。 而此时，子进程使用的数据页并不会发生任何改变，依然是 fork 时的数据，继续进行持久化。 ","permalink":"https://ktzxy.top/posts/n5szsni24z/","summary":"Redis 基础","title":"Redis 基础"},{"content":"﻿# Day-12-多线程\n简介 线程：在多任务操作系统中，每个运行的程序都是一个进程，用来执行不同的任务，而在一个进程中还可以有多个执行单元同时运行，来同时完成一个或多个程序任务，这些执行单元可以看做程序执行的一条条线索，被称为线程。\n注意︰操作系统中的每一个进程中都至少存在一个线程，当一个Java程序启动时，就会产生一个进程，该进程中会默认创建一个线程，在这个线程上会运行main()方法中的代码。\n多线程可以充分利用CPU资源,进一步提升程序执行效率。\n总结：\n线程就是独立的执行路径;\n在程序运行时，即使没有自己创建线程，后台也会有多个线程，如主线程，gc线程;main()称之为主线程，为系统的入口，用于执行整个程序;\n在一个进程中，如果开辟了多个线程，线程的运行由调度器安排调度，调度器是与操作系统紧密相关的，先后顺序是不能认为的干预的。\n对同一份资源操作时，会存在资源抢夺的问题，需要加入并发控制;线程会带来额外的开销，如CPU调度时间，并发控制开销。\n每个线程在自己的工作内存交互，内存控制不当会造成数据不一致\n创建 方式一：继承Thread类，重写run()方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.zy.thread_num; /** * @Auther: 赵羽 * @Description: com.zy.thread_num * @version: 1.0 */ //创建线程方式一:继承Thread类，重写run()方法，调用start开启线程 //总结:注意,线程开启不一定立即执行,由CPU调度执行 public class TestThread01 extends Thread { @Override public void run() { //run方法线程体 for (int i = 0; i \u0026lt; 20; i++) { System.out.println(\u0026#34;我喜欢学java\u0026#34;); } } //main线程，主线程 public static void main(String[] args) { //创建一个线程对象 TestThread01 testThread01 = new TestThread01(); //调用start()开启线程 testThread01.start(); for (int i = 0; i \u0026lt; 100; i++) { System.out.println(\u0026#34;你好\u0026#34;); } } } 练习Thread，实现多线程同步下载图片\n准备：commons-io-2.8.0.jar包\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package com.zy.thread_num; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.URL; /** * @Auther: 赵羽 * @Description: com.zy.thread_num * @version: 1.0 */ //练习Thread，实现多线程同步下载图片 public class TestThread02 extends Thread{ private String url; //网络图片地址 private String name; //保存得文件名 public TestThread02(String url,String name) { //构造器 this.url = url; this.name = name; } //下载图片线程得执行体 @Override public void run() { WebDownloader webDownloader = new WebDownloader(); webDownloader.downloader(url,name); System.out.println(\u0026#34;下载了文件名为：\u0026#34;+name);; } public static void main(String[] args) { TestThread02 t1 = new TestThread02(\u0026#34;https://img.ivsky.com/img/tupian/t/202102/26/sunyunzhu_baotunqun-004.jpg\u0026#34;,\u0026#34;2.jpg\u0026#34;); TestThread02 t2 = new TestThread02(\u0026#34;https://img.ivsky.com/img/tupian/t/202102/26/sunyunzhu_baotunqun-005.jpg\u0026#34;,\u0026#34;3.jpg\u0026#34;); TestThread02 t3 = new TestThread02(\u0026#34;https://img.ivsky.com/img/tupian/t/202102/26/sunyunzhu_baotunqun-006.jpg\u0026#34;,\u0026#34;4.jpg\u0026#34;); TestThread02 t4 = new TestThread02(\u0026#34;https://img.ivsky.com/img/tupian/t/202102/26/sunyunzhu_baotunqun-007.jpg\u0026#34;,\u0026#34;5.jpg\u0026#34;); t1.start(); t2.start(); t3.start(); t4.start(); } } //下载器 class WebDownloader{ //下载方法 public void downloader(String url,String name){ try { FileUtils.copyURLToFile(new URL(url),new File(name)); } catch (IOException e) { e.printStackTrace(); System.out.println(\u0026#34;io异常，downloader方法出现问题\u0026#34;); } } } 方式二：实现Runnable接口，重写run()方法\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package com.zy.thread_num; /** * @Auther: 赵羽 * @Description: com.zy.thread_num * @version: 1.0 */ //创建线程方式二﹔实现runnable接口,重写run方法，执行线程需要丢入runnable接口实现类，调用start方法。 public class TestThread03 implements Runnable{ @Override public void run() { //run方法线程体 for (int i = 0; i \u0026lt; 20; i++) { System.out.println(\u0026#34;我喜欢学java\u0026#34;); } } public static void main(String[] args) { //创建runnable接口的实现类对象 TestThread03 testThread03 = new TestThread03(); //创建线程对象，通过线程对象来开启线程 new Thread(testThread03).start(); for (int i = 0; i \u0026lt; 100; i++) { System.out.println(\u0026#34;你好\u0026#34;); } } } 乌龟赛跑\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package com.zy.thread_num; /** * @Auther: 赵羽 * @Description: com.zy.thread_num * @version: 1.0 */ //模拟乌龟赛跑 public class TestThread05 implements Runnable{ //胜利者 private static String winner; @Override public void run() { for (int i = 0; i \u0026lt;=100; i++) { //模拟兔子休息 if (Thread.currentThread().getName().equals(\u0026#34;兔子\u0026#34;)\u0026amp;\u0026amp;i%10==0){ try { Thread.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } } //判断比赛是否结束 boolean flag = gameover(i); //如果比赛结束了，就停止程序 if (flag){ break; } System.out.println(Thread.currentThread().getName()+\u0026#34;---\u0026gt;跑了\u0026#34;+i+\u0026#34;步\u0026#34;); } } //判断是否完成比赛 private boolean gameover(int steps){ //判断是否有胜利者 if (winner!=null){ //已经存在胜利者 return true; }{ if (steps\u0026gt;=100){ winner = Thread.currentThread().getName(); System.out.println(\u0026#34;胜利者是：\u0026#34;+winner); return true; } } return false; } public static void main(String[] args) { TestThread05 race = new TestThread05(); new Thread(race,\u0026#34;兔子\u0026#34;).start(); new Thread(race,\u0026#34;乌龟\u0026#34;).start(); } } 方式三：实现Callable接口，重写call)方法，并使用Future来获取call()方法的返回结果\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 package com.zy.thread_num; import org.apache.commons.io.FileUtils; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.concurrent.*; /** * @Auther: 赵羽 * @Date: 2021/3/6 - 03 - 06 - 16:56 * @Description: com.zy.thread_num * @version: 1.0 */ //创建线程方式三：实现Callable接口 //从JDK 5开始,Java提供了一个新的Callable接口，来满足这种既能创建多线程又可以有返回值的需求。 public class TestThread06 implements Callable\u0026lt;Boolean\u0026gt; { private String url; //网络图片地址 private String name; //保存得文件名 public TestThread06(String url,String name) { //构造器 this.url = url; this.name = name; } //下载图片线程得执行体 @Override public Boolean call() { WebDownloader webDownloader = new WebDownloader(); webDownloader.downloader(url,name); System.out.println(\u0026#34;下载了文件名为：\u0026#34;+name); return true; } public static void main(String[] args) throws ExecutionException, InterruptedException { TestThread06 t1 = new TestThread06(\u0026#34;https://img.ivsky.com/img/tupian/t/202102/26/sunyunzhu_baotunqun-004.jpg\u0026#34;,\u0026#34;2.jpg\u0026#34;); TestThread06 t2 = new TestThread06(\u0026#34;https://img.ivsky.com/img/tupian/t/202102/26/sunyunzhu_baotunqun-005.jpg\u0026#34;,\u0026#34;3.jpg\u0026#34;); TestThread06 t3 = new TestThread06(\u0026#34;https://img.ivsky.com/img/tupian/t/202102/26/sunyunzhu_baotunqun-006.jpg\u0026#34;,\u0026#34;4.jpg\u0026#34;); TestThread06 t4 = new TestThread06(\u0026#34;https://img.ivsky.com/img/tupian/t/202102/26/sunyunzhu_baotunqun-007.jpg\u0026#34;,\u0026#34;5.jpg\u0026#34;); //创建执行服务： ExecutorService ser = Executors.newFixedThreadPool(4); //提交执行 Future\u0026lt;Boolean\u0026gt; submit1 = ser.submit(t1); Future\u0026lt;Boolean\u0026gt; submit2 = ser.submit(t2); Future\u0026lt;Boolean\u0026gt; submit3 = ser.submit(t3); Future\u0026lt;Boolean\u0026gt; submit4 = ser.submit(t4); //获取结果 Boolean r1 = submit1.get(); Boolean r2 = submit2.get(); Boolean r3 = submit3.get(); Boolean r4 = submit4.get(); //关闭服务 ser.shutdownNow(); } } //下载器 class WebDownloaders{ //下载方法 public void downloader(String url,String name){ try { FileUtils.copyURLToFile(new URL(url),new File(name)); } catch (IOException e) { e.printStackTrace(); System.out.println(\u0026#34;io异常，downloader方法出现问题\u0026#34;); } } } 对比 通过实现Runnable接口(或者Callable接口）相对于继承Thread类实现多线程来说，有如下显著的好处︰\n适合多个线程去处理同一个共享资源的情况。 可以避免Java单继承带来的局限性。 静态代理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 package com.zy.thread_num; /** * @Auther: 赵羽 * @Description: com.zy.thread_num * @version: 1.0 */ /* * 静态代理模式总结： * 真实对象和代理对象都要实现同一个接口 * 代理对象要代理真实角色 * 好处： * 代理对象可以做很多真实对象做不了的事请 * 真实对象专注做自己的事请 * */ public class StaticProxy { public static void main(String[] args) { You you = new You(); new Thread(()-\u0026gt;System.out.println(\u0026#34;我爱你\u0026#34;)).start(); new WeddingCompany(new You()).HappyMarry(); /*WeddingCompany weddingCompany = new WeddingCompany(you); weddingCompany.HappyMarry();*/ } } interface Marry{ void HappyMarry(); } //真实角色 结婚本人 class You implements Marry{ @Override public void HappyMarry() { System.out.println(\u0026#34;我要结婚了，很开心\u0026#34;); } } //代理角色， 婚庆公司 帮助本人结婚 class WeddingCompany implements Marry{ private Marry target; public WeddingCompany(Marry target) { this.target = target; } @Override public void HappyMarry() { before(); this.target.HappyMarry(); after(); } private void after() { System.out.println(\u0026#34;结婚之后，收尾款\u0026#34;); } private void before() { System.out.println(\u0026#34;结婚之前，布置现场\u0026#34;); } } Lamda表达式 入希腊字母表中排序第十一位的字母，英语名称为Lambda\n避免匿名内部类定义过多\n其实质属于函数式编程的概念\n理解Functional lnterface(函数式接口）是学习Java8 lambda表达式的关键所在。\n函数式接口的定义:\n​\t任何接口，如果只包含唯一一个抽象方法，那么它就是一个函数式接口。\n​\t对于函数式接口，我们可以通过lambda表达式来创建该接口的对象。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package com.zy.thread_num2; /** * @Auther: 赵羽 * @Description: com.zy.thread_num2 * @version: 1.0 */ /* 推导lambda表达式 */ public class Demo1 { //3.静态内部类 static class Like2 implements ILike{ @Override public void lambda() { System.out.println(\u0026#34;I like lambda2\u0026#34;); } } public static void main(String[] args) { Like like = new Like(); like.lambda(); Like2 like2 = new Like2(); like2.lambda(); //4.局部内部类 class Like3 implements ILike{ @Override public void lambda() { System.out.println(\u0026#34;I like lambda3\u0026#34;); } } Like3 like3 = new Like3(); like3.lambda(); //5.匿名内部类，没有类的名称，必须借助接口或者父类 ILike ilike = new ILike() { @Override public void lambda() { System.out.println(\u0026#34;I like lambda4\u0026#34;); } }; ilike.lambda(); //6.用lambda简化 ILike like1 = ()-\u0026gt; { System.out.println(\u0026#34;I like lambda5\u0026#34;); }; like1.lambda(); } } //1.定义一个函数式接口 interface ILike{ void lambda(); } //2.实现类 class Like implements ILike{ @Override public void lambda() { System.out.println(\u0026#34;I like lambda\u0026#34;); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package com.zy.thread_num2; /** * @Auther: 赵羽 * @Description: com.zy.thread_num2 * @version: 1.0 */ /* * 总结: lambda表达式只能有一行代码的情况下才能简化成为一行，如果有多行，那么就用代码块包裹。 * 前提是接口为函数式接 * 多个参数也可以去掉参数类型，要去掉就都去掉，必须加上括号 * */ public class Demo2 { public static void main(String[] args) { //1.lambda表达示简化 ILove love =(int a)-\u0026gt; { System.out.println(\u0026#34;i love you--\u0026gt;\u0026#34;+a); }; //简化1.参数类型 love = (a)-\u0026gt; { System.out.println(\u0026#34;i love you--\u0026gt;\u0026#34;+a); }; //简化2.简化括号 love = a-\u0026gt; { System.out.println(\u0026#34;i love you--\u0026gt;\u0026#34;+a); }; //简化3.去掉花括号 love = a-\u0026gt;System.out.println(\u0026#34;i love you--\u0026gt;\u0026#34;+a); love.love(520); } } interface ILove{ void love(int a); } 线程状态 线程状态。线程可以处于下列状态之一：\nNEW 至今尚未启动的线程处于这种状态。 RUNNABLE 正在 Java 虚拟机中执行的线程处于这种状态。 BLOCKED 受阻塞并等待某个监视器锁的线程处于这种状态。 WAITING 无限期地等待另一个线程来执行某一特定操作的线程处于这种状态。 TIMED_WAITING 等待另一个线程来执行取决于指定等待时间的操作的线程处于这种状态。 TERMINATED 已退出的线程处于这种状态。 在给定时间点上，一个线程只能处于一种状态。这些状态是虚拟机状态，它们并没有反映所有操作系统线程状态。\n线程停止 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.zy.thread_num3; /** * @Auther: 赵羽 * @Description: com.zy.thread_num3 * @version: 1.0 */ /* 测试stop 1.建议线程正常停止---\u0026gt;利用次数,不建议死循环。 2.建议使用标志位---\u0026gt;设置一个标志位 3.不要使用stop或者destroy 等过时或者JDK不建议使用的方法 */ public class Demo1 implements Runnable{ //1.设置一个标识位 private boolean flag = true; @Override public void run() { int i = 0; while (flag){ System.out.println(\u0026#34;线程运行中\u0026#34;+i++); } } //2.设置一个二公开的方法停止线程，转换标志位 public void stop(){ this.flag = false; } public static void main(String[] args) { Demo1 demo1 = new Demo1(); new Thread(demo1).start(); for (int i = 0; i \u0026lt; 1000; i++) { System.out.println(\u0026#34;******\u0026#34;+i); if (i==900){ //调用stop方法切换标志位，让线程停止 demo1.stop(); System.out.println(\u0026#34;线程该停止了\u0026#34;); } } } } 线程休眠 sleep(时间)指定当前线程阻塞的毫秒数;\nsleep存在异常InterruptedException;\nsleep时间达到后线程进入就绪状态;\nsleep可以模拟网络延时，倒计时等。\n每一个对象都有一个锁，sleep不会释放锁;\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package com.zy.thread_num3; /** * @Auther: 赵羽 * @Description: com.zy.thread_num3 * @version: 1.0 */ //模拟十秒倒计时 public class Demo2 { public static void main(String[] args) { try { tenDown(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void tenDown() throws InterruptedException { int num =10; while (true){ Thread.sleep(1000); System.out.println(num--); if (num\u0026lt;=0){ break; } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.zy.thread_num3; import java.text.SimpleDateFormat; import java.util.Date; /** * @Auther: 赵羽 * @Description: com.zy.thread_num3 * @version: 1.0 */ public class Demo2 { public static void main(String[] args) { //打印当前系统时间 Date startTime = new Date(System.currentTimeMillis()); //获取系统当前时间 while (true){ try { Thread.sleep(1000); System.out.println(new SimpleDateFormat(\u0026#34;HH:mm:ss\u0026#34;).format(startTime)); startTime = new Date(System.currentTimeMillis()); //更新当前时间 } catch (InterruptedException e) { e.printStackTrace(); } } } public static void tenDown() throws InterruptedException { int num =10; while (true){ Thread.sleep(1000); System.out.println(num--); if (num\u0026lt;=0){ break; } } } } 线程礼让 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package com.zy.thread_num3; /** * @Auther: 赵羽 * @Description: com.zy.thread_num3 * @version: 1.0 */ //测试礼让线程 //礼让线程不一定成功 public class Demo3 { public static void main(String[] args) { TestYield testYield = new TestYield(); new Thread(testYield,\u0026#34;a\u0026#34;).start(); new Thread(testYield,\u0026#34;b\u0026#34;).start(); } } class TestYield implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName() + \u0026#34;线程开始运行\u0026#34;); Thread.yield(); //礼让 System.out.println(Thread.currentThread().getName() + \u0026#34;线程停止运行\u0026#34;); } } 线程强制执行_join Join合并线程，待此线程执行完成后，再执行其他线程，其他线程阻塞\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.zy.thread_num3; /** * @Auther: 赵羽 * @Description: com.zy.thread_num3 * @version: 1.0 */ public class TestJoin implements Runnable{ @Override public void run() { for (int i = 0; i \u0026lt; 1000; i++) { System.out.println(\u0026#34;插队的来了\u0026#34;+i); } } public static void main(String[] args) { //启动线程 TestJoin testJoin = new TestJoin(); Thread thread = new Thread(testJoin); thread.start(); //主线程 for (int i = 0; i \u0026lt; 500; i++) { if (i==200){ try { thread.join(); //插队 } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(\u0026#34;正常站队---------\u0026gt;\u0026#34;+i); } } } 线程检测 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package com.zy.thread_num3; /** * @Auther: 赵羽 * @Description: com.zy.thread_num3 * @version: 1.0 */ //观测线程状态 public class TestState { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(()-\u0026gt;{ for (int i = 0; i \u0026lt; 5; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(\u0026#34;***********\u0026#34;); }); //观察状态 Thread.State state = thread.getState(); System.out.println(state); //观察启动后 thread.start(); state = thread.getState(); System.out.println(state); while (state != Thread.State.TERMINATED){ //只要线程不终止，就一直输出状态 Thread.sleep(100); state = thread.getState(); //更新 System.out.println(state); } } } 线程的优先级 在应用程序中，要对线程进行调度，最直接的方式就是设置线程的优先级。==优 先级越高的线程获得CPu执行的机会越大，而优先级越低的线程获得cPu执行 的机会越小。== 线程的优先级用1~10之间的整数来表示，==数字越大优先级越高==。 除了可以直接使用数字表示线程的优先级，还可以使用Thread类中提供的三个 静态常量表示线程的优先级。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package com.zy.thread_num3; /** * @Auther: 赵羽 * @Description: com.zy.thread_num3 * @version: 1.0 */ public class TestPriority { public static void main(String[] args) { //主线称默认优先级 System.out.println(Thread.currentThread().getName()+\u0026#34;----\u0026gt;\u0026#34;+Thread.currentThread().getPriority()); //5 MyPriority myPriority = new MyPriority(); Thread t1 = new Thread(myPriority); Thread t2 = new Thread(myPriority); Thread t3 = new Thread(myPriority); Thread t4 = new Thread(myPriority); Thread t5 = new Thread(myPriority); Thread t6 = new Thread(myPriority); //先设置优先级，再启动 t1.start(); //5 t2.setPriority(1); t2.start(); //1 t3.setPriority(5); t3.start(); //5 t4.setPriority(Thread.MAX_PRIORITY); t4.start(); //10 /*t5.setPriority(-1); t5.start(); //报错 t6.setPriority(11); t6.start(); //报错 */ } } class MyPriority implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()+\u0026#34;----\u0026gt;\u0026#34;+Thread.currentThread().getPriority()); } } 守护(daemon)线程 线程分为用户线程和守护线程\n虚拟机必须确保用户线程执行完毕\n虚拟机不用等待守护线程执行完毕\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.zy.thread_num3; /** * @Auther: 赵羽 * @Description: com.zy.thread_num3 * @version: 1.0 */ public class TestDaemon { public static void main(String[] args) { God god = new God(); You you = new You(); Thread thread = new Thread(god); thread.setDaemon(true); //默认为false表示是用户线程，正常的线程都是用户线程 thread.start(); //上帝守护线程启动 new Thread(you).start(); //用户线程启动 } } class God implements Runnable{ @Override public void run() { while (true){ System.out.println(\u0026#34;上帝保佑着你\u0026#34;); } } } class You implements Runnable{ @Override public void run() { for (int i = 0; i \u0026lt; 100; i++) { System.out.println(\u0026#34;一生都开心的活着\u0026#34;); } System.out.println(\u0026#34;good bye this world\u0026#34;); } } 线程同步 ​\t线程安全问题其实就是由多个线程同时处理共享资源所导致的。要想解决线程安全问题，必须得保证处理共享资源的代码在任意时刻只能有一个线程访问。为此, Java中提供了==线程同步机制==。\n线程不安全案例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package com.zy.thread_num4; /** * @Auther: 赵羽 * @Description: com.zy.thread_num4 * @version: 1.0 */ //不安全的买票 public class Demo1 { public static void main(String[] args) { Buyticket station = new Buyticket(); new Thread(station,\u0026#34;张三\u0026#34;).start(); new Thread(station,\u0026#34;李四\u0026#34;).start(); new Thread(station,\u0026#34;王五\u0026#34;).start(); } } class Buyticket implements Runnable{ //票 private int ticketNum =10; boolean flag =true; //外部停止方式 @Override public void run() { //买票 while (flag){ try { buy(); } catch (InterruptedException e) { e.printStackTrace(); } } } private void buy() throws InterruptedException { if (ticketNum\u0026lt;=0){ flag =false; return; } //模拟延迟 Thread.sleep(100); System.out.println(Thread.currentThread().getName()+\u0026#34;买到\u0026#34;+ticketNum--+\u0026#34;张票\u0026#34;); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 package com.zy.thread_num4; /** * @Auther: 赵羽 * @Description: com.zy.thread_num4 * @version: 1.0 */ //不安全的取钱 public class Demo2 { public static void main(String[] args) { //账户 Account account = new Account(100,\u0026#34;购房基金\u0026#34;); Drawing you = new Drawing(account, 50, \u0026#34;你\u0026#34;); Drawing girlfriend = new Drawing(account, 100, \u0026#34;女朋友\u0026#34;); you.start(); girlfriend.start(); } } //账户 class Account{ int money; String name; public Account(int money, String name) { this.money = money; this.name = name; } public Account(int i) { } } //银行：模拟取款 class Drawing extends Thread{ Account account; //账户 int drawingMoney; //去了多少钱 int nowMoney; //现在手里有多少钱 public Drawing(Account account, int drawingMoney, String name) { super(name); this.account = account; this.drawingMoney = drawingMoney; } @Override public void run() { //判断有没有钱 if (account.money-drawingMoney\u0026lt;0){ System.out.println(Thread.currentThread().getName() + \u0026#34;钱不够，取不了\u0026#34;); return; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //卡内余额 = 余额 - 你取得钱 account.money = account.money -drawingMoney; //你手里钱 nowMoney = nowMoney + drawingMoney; System.out.println(account.name+\u0026#34;余额里的钱为：\u0026#34;+account.money); //Thread.currentThread().getName() \u0026lt;==\u0026gt;this.getName() System.out.println(this.getName()+\u0026#34;手里的钱：\u0026#34;+nowMoney); } } 同步方法 由于我们可以通过private关键字来保证数据对象只能被方法访问，所以我们只 需要针对方法提出一套机制,这套机制就是synchronized关键字，它包括两种用法:synchronized方法和synchronized块．\n1 [修饰符]synchronized 返回值类型 方法名([参数1，.....]){} 在方法前面也可以使用synchronized关键字来修饰，被修饰的方法为同步方法,它能实现和同步代码块同样的功能。 被synchronized修饰的方法在某一时刻只允许一个线程访问，访问该方法的其他线程都会发生阻塞，直到当前线程访问完毕后，其他线程才有机会执行。\n注意：\n同步方法也有锁，它的锁就是当前调用该方法的对象，就是this指向的对象。\nJava中静态方法的锁是该方法所在类的class对象，该对象可以直接类名.class的方式获取。\n同步代码块和同步方法解决多线程问题有好处也有弊端。同步==解决了多个线程同时访问共享数据时的线程安全问题==，只要加上同一个锁，在同一时间内只能有一条线程执行，但是线程在执行同步代码时每次都会判断锁的状态，非常==消耗资源，效率较低==。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 //安全的买票 public class Demo1 { public static void main(String[] args) { Buyticket station = new Buyticket(); new Thread(station,\u0026#34;张三\u0026#34;).start(); new Thread(station,\u0026#34;李四\u0026#34;).start(); new Thread(station,\u0026#34;王五\u0026#34;).start(); } } class Buyticket implements Runnable{ //票 private int ticketNum =10; boolean flag =true; //外部停止方式 @Override public void run() { //买票 while (flag){ try { buy(); } catch (InterruptedException e) { e.printStackTrace(); } } } //synchronized 同步方法，锁的是this private synchronized void buy() throws InterruptedException { if (ticketNum\u0026lt;=0){ flag =false; return; } //模拟延迟 Thread.sleep(100); System.out.println(Thread.currentThread().getName()+\u0026#34;买到\u0026#34;+ticketNum--+\u0026#34;张票\u0026#34;); } } 同步块 1 2 3 4 5 synchronized(lock){ #lock是一个任意类型的锁对象 //操作共享资源代码块 ... } #synchronized关键字后大括号{}内包含的就是需要同步操作的共享资源代码块 注意： lock锁对象可以是任意类型的对象，但多个线程共享的锁对象必须是相同的。锁对象的创建代码不能放到run()方法中，否则每个线程运行到run()方法都会创建一个新对象，这样每个线程都会有一个不同的锁。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 //安全的取钱 public class Demo2 { public static void main(String[] args) { //账户 Account account = new Account(100,\u0026#34;购房基金\u0026#34;); Drawing you = new Drawing(account, 50, \u0026#34;你\u0026#34;); Drawing girlfriend = new Drawing(account, 100, \u0026#34;女朋友\u0026#34;); you.start(); girlfriend.start(); } } //账户 class Account{ int money; String name; public Account(int money, String name) { this.money = money; this.name = name; } public Account(int i) { } } //银行：模拟取款 class Drawing extends Thread{ Account account; //账户 int drawingMoney; //去了多少钱 int nowMoney; //现在手里有多少钱 public Drawing(Account account, int drawingMoney, String name) { super(name); this.account = account; this.drawingMoney = drawingMoney; } //synchronized 默认锁的是this @Override public void run() { //所的对象就是变化的量，需要增删改的对象 synchronized (account){ //判断有没有钱 if (account.money-drawingMoney\u0026lt;0){ System.out.println(Thread.currentThread().getName() + \u0026#34;钱不够，取不了\u0026#34;); return; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //卡内余额 = 余额 - 你取得钱 account.money = account.money -drawingMoney; //你手里钱 nowMoney = nowMoney + drawingMoney; System.out.println(account.name+\u0026#34;余额里的钱为：\u0026#34;+account.money); //Thread.currentThread().getName() \u0026lt;==\u0026gt;this.getName() System.out.println(this.getName()+\u0026#34;手里的钱：\u0026#34;+nowMoney); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package com.zy.thread_num4; import java.util.concurrent.CopyOnWriteArrayList; /** * @Auther: 赵羽 * @Description: com.zy.thread_num4 * @version: 1.0 */ //测试JUC安全类型的集合 public class Demo3 { public static void main(String[] args) { CopyOnWriteArrayList\u0026lt;String\u0026gt; list = new CopyOnWriteArrayList\u0026lt;String\u0026gt;(); for (int i = 0; i \u0026lt; 10000; i++) { new Thread(()-\u0026gt;{ list.add(Thread.currentThread().getName()); }).start(); } try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(list.size()); } } 同步锁 从JDK5开始，Java增加了一个功能更强大的Lock锁。其最大的优势在于Lock锁可以让某个线程在持续获取同步锁失败后返回，不再继续等待，另外Lock锁在使用是也更加灵活。\n注意：\n​\tReentrantLock类是Lock锁接口的实现类，也是常用的同步锁，在该同步锁中除了lock()方法和unlock()方法外，还提供了一些其他同步锁操作的方法，例如tryLock()方法可以判断某个线程锁是否可用。\n​\t在使用Lock同步锁时，可以根据需要在不同代码位置灵活的上锁和解锁，为了保证所有情况下都能正常解锁以确保其他线程可以执行，通常情况下会在finallyi代码块中调用unlock()方法来解锁。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package com.zy.thread_num4; import java.util.concurrent.locks.ReentrantLock; /** * @Auther: 赵羽 * @Description: com.zy.thread_num4 * @version: 1.0 */ //测试Lock锁 public class TestLock { public static void main(String[] args) { TestLock2 testLock2 = new TestLock2(); new Thread(testLock2).start(); new Thread(testLock2).start(); new Thread(testLock2).start(); } } class TestLock2 implements Runnable{ int ticketNum = 10; //定义lock锁 private final ReentrantLock lock =new ReentrantLock(); @Override public void run() { while (true){ try { lock.lock(); //加锁 if (ticketNum\u0026gt;0){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(ticketNum--); }else { break; } }finally { lock.unlock(); //解锁 } } } } 对比 synchronized 与Lock的对比\nLock是显式锁（手动开启和关闭锁，别忘记关闭锁) synchronized是隐式锁，出了 作用域自动释放\nLock只有代码块锁，synchronized有代码块锁和方法锁\n使用Lock锁，JVM将花费较少的时间来调度线程，性能更好。并且具有更好的扩展性(提供更多的子类)\n优先使用顺序: Lock \u0026gt;同步代码块（已经进入了方法体，分配了相应资源)\u0026gt;同步方法（在方法体之外)\n死锁 多个线程各自占有一些共享资源﹐并且互相等待其他线程占有的资源才能运行﹐而导致两个或者多个线程都在等待对方释放资源﹐都停止执行的情形.某一个同步块同时拥有“两个以上对象的锁”时﹐就可能会发生“死锁”的问题.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 //死锁：多个线程互相抱着对方需要的资源，然后形成僵持。 public class DeadLock { public static void main(String[] args) { Makeup g1 = new Makeup(0, \u0026#34;灰姑娘\u0026#34;); Makeup g2 = new Makeup(1, \u0026#34;白雪公主\u0026#34;); g1.start(); g2.start(); } } //口红 class Lipstick{ } //镜子 class Mirror{ } //化妆 class Makeup extends Thread{ //需要的资源只有一份 static Lipstick lipstick = new Lipstick(); static Mirror mirror = new Mirror(); int choice; //选择 String girlName; //使用化妆品的人 Makeup(int choice,String girlName){ this.choice = choice; this.girlName = girlName; } @Override public void run() { //化妆 try { makeup(); } catch (InterruptedException e) { e.printStackTrace(); } } //化妆，互相持有对方的锁，就是需要拿到对方的资源 private void makeup() throws InterruptedException { if (choice==0){ synchronized (lipstick){ //获得口红的锁 System.out.println(this.girlName+\u0026#34;获得口红的锁\u0026#34;); Thread.sleep(1000); synchronized (mirror){ //一秒钟后获得镜子的锁 System.out.println(this.girlName+\u0026#34;获得镜子的锁\u0026#34;); } } }else { synchronized (mirror){ //获得镜子的锁 System.out.println(this.girlName+\u0026#34;获得镜子的锁\u0026#34;); Thread.sleep(2000); synchronized (lipstick){ //二秒钟后获得口红的锁 System.out.println(this.girlName+\u0026#34;获得口红的锁\u0026#34;); } } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 package com.zy.thread_num4; /** * @Auther: 赵羽 * @Description: com.zy.thread_num4 * @version: 1.0 */ /* * 产生死锁的四个必要条件: 1．互斥条件:一个资源每次只能被一个进程使用。 2．请求与保持条件:一个进程因请求资源而阻塞时，对已获得的资源保持不放。 3. 不剥夺条件︰进程已获得的资源，在末使用完之前，不能强行剥夺。 4．循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。 * 解决： 只要想办法破掉其中的任意一个或多个条件就可以避免死锁发生。 *避免死锁的发生： 加锁顺序(线程按照一定的顺序加锁) 加锁时限(线程尝试获取锁的时候加上一定的时限，超过时限则放弃对该锁的请求，并释放自己占有的锁) 死锁检测 * */ //死锁：多个线程互相抱着对方需要的资源，然后形成僵持。 //解决：将锁放在外面 public class DeadLock { public static void main(String[] args) { Makeup g1 = new Makeup(0, \u0026#34;灰姑娘\u0026#34;); Makeup g2 = new Makeup(1, \u0026#34;白雪公主\u0026#34;); g1.start(); g2.start(); } } //口红 class Lipstick{ } //镜子 class Mirror{ } //化妆 class Makeup extends Thread{ //需要的资源只有一份 static Lipstick lipstick = new Lipstick(); static Mirror mirror = new Mirror(); int choice; //选择 String girlName; //使用化妆品的人 Makeup(int choice,String girlName){ this.choice = choice; this.girlName = girlName; } @Override public void run() { //化妆 try { makeup(); } catch (InterruptedException e) { e.printStackTrace(); } } //化妆，互相持有对方的锁，就是需要拿到对方的资源 private void makeup() throws InterruptedException { if (choice==0){ synchronized (lipstick){ //获得口红的锁 System.out.println(this.girlName+\u0026#34;获得口红的锁\u0026#34;); Thread.sleep(1000); } synchronized (mirror){ //一秒钟后获得镜子的锁 System.out.println(this.girlName+\u0026#34;获得镜子的锁\u0026#34;); } }else { synchronized (mirror){ //获得镜子的锁 System.out.println(this.girlName+\u0026#34;获得镜子的锁\u0026#34;); Thread.sleep(2000); } synchronized (lipstick){ //二秒钟后获得口红的锁 System.out.println(this.girlName+\u0026#34;获得口红的锁\u0026#34;); } } } } 线程通信 Java在Object类中提供了wait()、notify()、notifyAll()等方法用于解决线程间的通信问题，因为Java中所有类都是Object类的子类或间接子类，因此任何类的实例对象都可以直接使用这些方法。\n说明︰其中wait()方法用于使当前线程进入等待状态,notify()和notifyAll()方法用于唤醒当前处于等待状态的线程。\n注意:wait()、notify()和notifyAll()方法的调用者都应该是同步锁对象，如果这三个方法的调用者不是同步锁对象,Java虚拟机就会抛出IllegalMonitorStateException异常。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 package com.zy.thread_num4; /** * @Auther: 赵羽 * @Description: com.zy.thread_num4 * @version: 1.0 */ //测试：生产者消费者模型---\u0026gt;利用缓冲区解决：管程法 //生产者，消费者，产品，缓冲区 public class TestPc { public static void main(String[] args) { SynContainer container =new SynContainer(); new Productor(container).start(); new Consumer(container).start(); } } //生产者 class Productor extends Thread{ SynContainer container; public Productor(SynContainer synContainer) { this.container = synContainer; } //生产 @Override public void run() { for (int i = 0; i \u0026lt; 100; i++) { System.out.println(\u0026#34;生产了\u0026#34;+i+\u0026#34;只鸡\u0026#34;); container.push(new Chicken(i)); } } } //消费者 class Consumer extends Thread{ SynContainer container; public Consumer(SynContainer synContainer) { this.container = synContainer; } //消费 @Override public void run() { for (int i = 0; i \u0026lt; 100; i++) { System.out.println(\u0026#34;消费了---\u0026gt;\u0026#34;+container.pop().id+\u0026#34;只鸡\u0026#34;); } } } //产品 class Chicken{ int id; //产品编号 public Chicken(int id) { this.id = id; } } //缓冲区 class SynContainer{ //需要一个容器大小 Chicken[] chickens = new Chicken[10]; //容器计数器 int count =0; //生产者放入产品 public synchronized void push(Chicken chicken){ //如果容器满了，就需要等待消费者消费 if (count==chickens.length){ //通知消费者消费，生产者等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果没有满，我们就需要丢入产品 chickens[count] = chicken; count++; //可以通知消费者消费了 this.notifyAll(); } //消费者消费产品 public synchronized Chicken pop(){ //判断能否消费 if (count==0){ //等待生产者生产，消费者等待 try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } //如果可以消费 count--; Chicken chicken = chickens[count]; //吃完了，通知生产者生产 this.notifyAll(); return chicken; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 package com.zy.thread_num4; /** * @Auther: 赵羽 * @Description: com.zy.thread_num4 * @version: 1.0 */ //测试生产者和消费者问题2：信号灯法 利用标志位 public class TestPc2 { public static void main(String[] args) { TV tv = new TV(); new Player(tv).start(); new Watcher(tv).start(); } } //生产者---\u0026gt;演员 class Player extends Thread{ TV tv; public Player(TV tv){ this.tv = tv; } @Override public void run() { for (int i = 0; i \u0026lt; 20; i++) { if (i%2==0){ this.tv.play(\u0026#34;快了大本营播放中。。。\u0026#34;); }else { this.tv.play(\u0026#34;抖音：记录美好生活\u0026#34;); } } } } //消费者---\u0026gt;观众 class Watcher extends Thread{ TV tv; public Watcher(TV tv){ this.tv = tv; } @Override public void run() { for (int i = 0; i \u0026lt; 20; i++) { tv.watch(); } } } //产品----\u0026gt;节目 class TV{ //演员表演，观众等待 T //观众观看，演员等待 F String voice; //表演的节目 boolean flag = true; //表演 public synchronized void play(String voice){ if (!flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(\u0026#34;演员表演了：\u0026#34;+voice); //通知观众观看 this.notifyAll(); //通知唤醒 this.voice = voice; this.flag =!this.flag; } //观看 public synchronized void watch(){ if (flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(\u0026#34;观看了：\u0026#34;+voice); //观看演员表演 this.notifyAll(); this.flag = !this.flag; } //观看 } 线程池 在大规模应用程序中，创建、分配和释放多线程对象会产生大量内存管理开销。为此，可以考虑使用Java提供的线程池来创建多线程,进一步优化线程管理。\nExecutor接口实现线程池管理\n步骤︰\n1.创建一个实现Runnable接口或者Callable接口的实现类，同时重写run()或者call()方法﹔ 2.创建Runnable接口或者Callable接口的实现类对象﹔ 3.使用Executors线程执行器类创建线程池; 4.使用ExecutorService执行器服务类的submit()方法将Runnable接口或者Callable接口的实现类对象提交到线程池进行管理; 5.线程任务执行完成后，可以使用shutdown()方法关闭线程池。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package com.zy.thread_num4; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @Auther: 赵羽 * @Description: com.zy.thread_num4 * @version: 1.0 */ public class TestPool { public static void main(String[] args) { //1.创建服务，创建线程池 //newFixedThreadPool 参数为：线程池大小 ExecutorService service = Executors.newFixedThreadPool(10); //执行 service.execute(new MyThread()); service.execute(new MyThread()); service.execute(new MyThread()); service.execute(new MyThread()); //2.关闭连接 service.shutdown(); } } class MyThread implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()); } } ce(); } } System.out.println(\u0026#34;演员表演了：\u0026#34;+voice); //通知观众观看 this.notifyAll(); //通知唤醒 this.voice = voice; this.flag =!this.flag; } //观看 public synchronized void watch(){ if (flag){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println(\u0026#34;观看了：\u0026#34;+voice); //观看演员表演 this.notifyAll(); this.flag = !this.flag; } //观看 } 线程池 在大规模应用程序中，创建、分配和释放多线程对象会产生大量内存管理开销。为此，可以考虑使用Java提供的线程池来创建多线程,进一步优化线程管理。\nExecutor接口实现线程池管理\n步骤︰\n1.创建一个实现Runnable接口或者Callable接口的实现类，同时重写run()或者call()方法﹔ 2.创建Runnable接口或者Callable接口的实现类对象﹔ 3.使用Executors线程执行器类创建线程池; 4.使用ExecutorService执行器服务类的submit()方法将Runnable接口或者Callable接口的实现类对象提交到线程池进行管理; 5.线程任务执行完成后，可以使用shutdown()方法关闭线程池。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package com.zy.thread_num4; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @Auther: 赵羽 * @Description: com.zy.thread_num4 * @version: 1.0 */ public class TestPool { public static void main(String[] args) { //1.创建服务，创建线程池 //newFixedThreadPool 参数为：线程池大小 ExecutorService service = Executors.newFixedThreadPool(10); //执行 service.execute(new MyThread()); service.execute(new MyThread()); service.execute(new MyThread()); service.execute(new MyThread()); //2.关闭连接 service.shutdown(); } } class MyThread implements Runnable{ @Override public void run() { System.out.println(Thread.currentThread().getName()); } } ","permalink":"https://ktzxy.top/posts/pjh3g4qobi/","summary":"Day 13 多线程","title":"Day 13 多线程"},{"content":"1. 注解(Annatation)概述 注解（Annotation）是 JDK1.5 之后的新特性，是 Java 提供用于设置程序中元素的关联信息和元数据（MetaData）的方法。它本质是一个接口，程序可以通过反射来获取指定程序中元素的 Annotation 注解对象，然后通过该 Annotation 对象来获取注解中的元数据信息。\n注解可以标记在类、接口、方法、成员变量，构造方法，局部变量等等元素上。\nTips: 可以理解为，注解是给编译器或 JVM 查看的，然后可以根据其标记执行对应的功能。\n2. 注解作用 2.1. 注解使用例子 编译检查，如@Override。 生成帮忙文档 做为框架的配置方案（重点） XML配置 注解配置 2.2. 扩展：框架的两种配置方案优缺点 注：框架 = 代码 + 配置（个性化）。框架（struts2,hibernate,spring)都提供了两种配置方案\nXML 配置： 优点：配置信息和类分离，降低程序的耦合性（扩展性更好） 缺点：每一个类需要对应一个XML文件，如果类很多，而XML文件也会很多。XML 维护成本高（可读性差） 注解配置： 优点：将配置信息和类写在一起，可读性高，开发效率相对较高。 缺点：程序耦合性高 3. Java常用内置注解的使用 3.1. @Override 注解 该注解只能用于修饰方法声明，表示该方法是限定重写父类方法。该注解只能用于方法\n3.2. @Deprecated 注解 用于表示某个程序中的元素(类，方法等)已经过时。不建议继续使用，还是可以使用。\n3.3. @SuppressWarnings 注解 @SuppressWarnings 注解的作用是抑制编译器警告。常用警告名称：\ndeprecation 忽略过时 rawtypes 忽略类型安全 unused 忽略不使用 unchecked 忽略安全检查 null 忽略空指针 all 忽略所有编译器警告 注：如果多个警告就使用{}将多个警告包括起来，封装成字符串数组\n4. 自定义注解 属性的作用：可以给每个注解加上多个不同的属性，用户使用注解的时候，可以传递参数给属性，让注解的功能更加强大\n4.1. 自定义注解格式 1 2 3 修饰符 @interface 注解名 { } 4.2. 注解的属性 4.2.1. 属性定义格式 第1种定义方式：数据类型 属性名(); 第2种定义方式：数据类型 属性名() default 默认值; 注意事项： 如果注解有定义了属性，且属性没有默认值，则在使用注解的时候，就需要给属性赋值 如果属性有默认值，则使用注解的时候，这个属性就可以不赋值。也可以重新赋值，覆盖原有的默认值 4.2.2. 注解支持的数据类型 8种数据类型都支持 String Enum Class Annotation 以及上面类型的数组形式 4.3. 特殊属性名 value 如果注解中只有一个属性且属性名为 value 时，在使用注解时可以直接给出属性值而不需要给属性名。(省略 value= 部分) 无论这个 value 是单个元素还是数组，都可以省略。 如果注解中除了 value 属性还有其他属性，且其他属性中至少有一个属性没有默认值时，则 value 属性名不能省略。 1 2 3 4 5 public @interface T_T { String value(); // 书名 double price() default 100; // 价格 String[] authors(); // 作者 } 5. 元注解 5.1. 元注解的概念 Java 默认提供的注解，用于标识在注解上的注解，用来约束注解的功能，称为元注解。Java 所有的内置注解定义都使用了元注解。元注解有以下几个：\n@Target：修饰的对象范围 @Retention：定义被保留的时间长短 @Inherited：阐述了某个被标注的类型是被继承的 @Documented：描述-javadoc 5.2. @Target 元注解 @Target：标识注解使用范围(写在自定义注解类上)。Annotation可被用于 packages、types（类、接口、枚举、Annotation 类型）、类型成员（方法、构造方法、成员变量、枚举值）、方法参数和本地变量（如循环变量、catch 参数），如果不写默认是任何地方都可以使用。使用格式如下：\n1 2 @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) public @interface MyAnnotation {} @Target 可选取值来自 ElementType 枚举类：\nElementType.TYPE: 用在类、接口（包括注解类型）或者枚举(enum)上 ElementType.FIELD：用在成员变量上 ElementType.METHOD: 用在成员方法上 ElementType.PARAMETER：用在方法参数（形式参数）上 ElementType.CONSTRUCTOR：用在构造方法上 ElementType.LOCAL_VARIABLE：用在局部变量上 ElementType.ANNOTATION_TYPE：用过声明一个注解 ElementType.PACKAGE：用于描述包 ElementType.TYPE_PARAMETER：JDK1.8 以后加入，对普通变量的声明 ElementType.TYPE_USE：JDK1.8 以后加入，能标注任何类型的名称 5.3. @Retention 元注解 @Retention：用来标识注解的生命周期（有效作用范围），表示需要在什么级别保存注解信息。使用格式如下：\n1 2 @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation {} @Retention 可选取值来自 RetentionPolicy 枚举类：\nRetentionPolicy.SOURCE：注解只存在于 Java 源代码中，编译生成字节码文件和程序运行时就不存在了。（即源文件保留） RetentionPolicy.CLASS：默认值，注解存在于 Java 源代码、编译以后的字节码文件中，运行的时候内存就不存在。（即 class 保留） RetentionPolicy.RUNTIME：注解存在于 Java 源代码中、编译以后的字节码文件中、运行时的内存中，程序可以通过反射获取该注解。（即运行时保留） 5.4. @Inherited 元注解 @Inherited 作用：表示该注解可以被子类继承。如果一个使用了 @Inherited 修饰的 annotation 类型被用于一个 class，则这个 annotation 将被用于该 class 的子类。使用格式如下：\n1 2 @Inherited public @interface MyAnnotation {} 5.5. @Documented 元注解 @Documented 作用：表示该注解会出现在帮忙文档（javadoc）中。描述其它类型的 annotation 应该被作为被标注的程序成员的公共 API，因此可以被例如 javadoc 此类的工具文档化。使用格式如下：\n1 2 @Documented public @interface MyAnnotation {} 6. 注解的原理 6.1. Annotation 接口 所有注解类型的公共接口，所有注解都是 java.lang.annotation.Annotation 的子类(类似所有类都 Object 的子类)\n6.2. AnnotatedElement 接口 该接口中定义了一系列与注解解析相关的方法。注：当前对象是指方法调用者\n1 boolean isAnnotationPresent(Class\u0026lt;T\u0026gt; annotationClass) 判断当前对象是否使用了指定annotationClass的注解。如果使用了，则返回true，否则返回false 1 T getAnnotation(Class\u0026lt;T\u0026gt; annotationClass) 根据注解的Class类型获得当前对象上指定的注解对象（注解类的对象)。需要向下转型成注解的类型，然后才能调用注解里的属性。 1 Annotation[] getAnnotations() 获得当前对象及其从父类上继承的所有的注解对象数组 1 Annotation[] getDeclaredAnnotations() 获得类中所有声明的注解，不包括父类的 6.3. 注解原理简述 注解本质是一个继承了 Annotation 的特殊接口，其具体实现类是 Java 运行时生成的动态代理类。通过反射获取注解时，返回的是 Java 运行时生成的动态代理对象。通过代理对象调用自定义注解的方法，最终会调用 AnnotationInvocationHandler 的 invoke 方法。该方法会从 memberValues 这个 Map 中索引出对应的值。而 memberValues 的来源是 Java 常量池。\n7. 注解解析 7.1. 解析原则 注解作用在哪个成员上，就通过该成员对应的对象获得注解对象。\n比如，注解作用在成员方法上，则通过成员方法对应的Method对象获得；作用在类上的，则通过Class对象获得\n1 2 3 4 // 得到方法对象 Method method = clazz.getDeclaredMethod(\u0026#34;方法名\u0026#34;); // 得到方法上的注解 注解类 xx = (注解类) method.getAnnotation(注解类名.class); 7.2. 注解解析案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package level02.test02; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.Arrays; /* * 关卡2训练案例2 * 定义一个注解：Book * 包含属性：String value() 书名 * 包含属性：double price() 价格，默认值为 100 * 包含属性：String[] authors() 多位作者 * 1、定义类在成员方法上使用 Book 注解 * 2、解析获得该成员方法上使用注解的属性值。 */ public class Day20Test02_02 { @SuppressWarnings({ \u0026#34;unchecked\u0026#34;, \u0026#34;rawtypes\u0026#34; }) public static void main(String[] args) throws Exception { // 使用反射获取Method对象 Class clazz = Class.forName(\u0026#34;level02.test02.Day20Test02_02\u0026#34;); Method m = clazz.getDeclaredMethod(\u0026#34;method\u0026#34;); // 获取到的注解的对象，需要向下转型成注解的类型 Book book = (Book) m.getAnnotation(Book.class); // 获取注解的值 System.out.println(book.value()); // 使用包装类的toString方法转到字符串再输出（直接输出也可以） System.out.println(Double.toString(book.price())); System.out.println(book.price()); // 使用Arrays工具类的toString方法将数组输出 System.out.println(Arrays.toString(book.authors())); } @Book(value = \u0026#34;傻的吗\u0026#34;, authors = { \u0026#34;真的傻\u0026#34;, \u0026#34;还是真的是傻的\u0026#34; }) public static void method() { System.out.println(\u0026#34;试试调用我呀\u0026#34;); } } @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Book { String value(); double price() default 100; String[] authors(); } ","permalink":"https://ktzxy.top/posts/lenxao922d/","summary":"Java基础 注解","title":"Java基础 注解"},{"content":"方案概述 方案一：优化现有mysql数据库。优点：不影响现有业务，源程序不需要修改代码，成本最低。缺点：有优化瓶颈，数据量过亿就玩完了。\n方案二：升级数据库类型，换一种100%兼容mysql的数据库。优点：不影响现有业务，源程序不需要修改代码，你几乎不需要做任何操作就能提升数据库性能，缺点：多花钱\n方案三：一步到位，大数据解决方案，更换newsql/nosql数据库。优点：扩展性强，成本低，没有数据容量瓶颈，缺点：需要修改源程序代码\n以上三种方案，按顺序使用即可，数据量在亿级别一下的没必要换nosql，开发成本太高。\n方案一详细说明：优化现有mysql数据库 1.数据库设计和表创建时就要考虑性能 2.sql的编写需要注意优化 3.分区 4.分表 5.分库 1.数据库设计和表创建时就要考虑性能\nmysql数据库本身高度灵活，造成性能不足，严重依赖开发人员能力。也就是说开发人员能力高，则mysql性能高。这也是很多关系型数据库的通病，所以公司的dba通常工资巨高。\n设计表时要注意：\n1.表字段避免null值出现，null值很难查询优化且占用额外的索引空间，推荐默认数字0代替null。 2.尽量使用INT而非BIGINT，如果非负则加上UNSIGNED（这样数值容量会扩大一倍），当然能使用TINYINT、SMALLINT、MEDIUM_INT更好。 3.使用枚举或整数代替字符串类型 4.尽量使用TIMESTAMP而非DATETIME 5.单表不要有太多字段，建议在20以内 6.用整型来存IP 索引\n1.索引并不是越多越好，要根据查询有针对性的创建，考虑在WHERE和ORDER BY命令上涉及的列建立索引，可根据EXPLAIN来查看是否用了索引还是全表扫描 2.应尽量避免在WHERE子句中对字段进行NULL值判断，否则将导致引擎放弃使用索引而进行全表扫描 3.值分布很稀少的字段不适合建索引，例如\u0026quot;性别\u0026quot;这种只有两三个值的字段 4.字符字段只建前缀索引 5.字符字段最好不要做主键 6.不用外键，由程序保证约束 7.尽量不用UNIQUE，由程序保证约束 8.使用多列索引时主意顺序和查询条件保持一致，同时删除不必要的单列索引 简言之就是使用合适的数据类型，选择合适的索引\n选择合适的数据类型 （1）使用可存下数据的最小的数据类型，整型 \u0026lt; date,time \u0026lt; char,varchar \u0026lt; blob （2）使用简单的数据类型，整型比字符处理开销更小，因为字符串的比较更复杂。如，int类型存储时间类型，bigint类型转ip函数 （3）使用合理的字段属性长度，固定长度的表会更快。使用enum、char而不是varchar （4）尽可能使用not null定义字段 （5）尽量少用text，非用不可最好分表 # 选择合适的索引列 （1）查询频繁的列，在where，group by，order by，on从句中出现的列 （2）where条件中\u0026lt;，\u0026lt;=，=，\u0026gt;，\u0026gt;=，between，in，以及like 字符串+通配符（%）出现的列 （3）长度小的列，索引字段越小越好，因为数据库的存储单位是页，一页中能存下的数据越多越好 （4）离散度大（不同的值多）的列，放在联合索引前面。查看离散度，通过统计不同的列值来实现，count越大，离散程度越高：\n原开发人员已经跑路，该表早已建立，我无法修改，故：该措辞无法执行，放弃！\n2.sql的编写需要注意优化\n1.使用limit对查询结果的记录进行限定\n2.避免select *，将需要查找的字段列出来\n3.使用连接（join）来代替子查询\n4.拆分大的delete或insert语句\n5.可通过开启慢查询日志来找出较慢的SQL\n6.不做列运算：SELECT id WHERE age + 1 = 10，任何对列的操作都将导致表扫描，它包括数据库教程函数、计算表达式等等，查询时要尽可能将操作移至等号右边\n7.sql语句尽可能简单：一条sql只能在一个cpu运算；大语句拆小语句，减少锁时间；一条大sql可以堵死整个库\n8.OR改写成IN：OR的效率是n级别，IN的效率是log(n)级别，in的个数建议控制在200以内\n9.不用函数和触发器，在应用程序实现\n10.避免%xxx式查询\n11.少用JOIN\n12.使用同类型进行比较，比如用'123\u0026rsquo;和'123\u0026rsquo;比，123和123比\n13.尽量避免在WHERE子句中使用!=或\u0026lt;\u0026gt;操作符，否则将引擎放弃使用索引而进行全表扫描\n14.对于连续数值，使用BETWEEN不用IN：SELECT id FROM t WHERE num BETWEEN 1 AND 5\n15.列表数据不要拿全表，要使用LIMIT来分页，每页数量也不要太大\n原开发人员已经跑路，程序已经完成上线，我无法修改sql，故：该措辞无法执行，放弃！\n引擎 目前广泛使用的是MyISAM和InnoDB两种引擎：\nMyISAM\nMyISAM引擎是MySQL 5.1及之前版本的默认引擎，它的特点是：\n1.不支持行锁，读取时对需要读到的所有表加锁，写入时则对表加排它锁\n2.不支持事务\n3.不支持外键\n4.不支持崩溃后的安全恢复\n5.在表有读取查询的同时，支持往表中插入新纪录\n6.支持BLOB和TEXT的前500个字符索引，支持全文索引\n7.支持延迟更新索引，极大提升写入性能\n8.对于不会进行修改的表，支持压缩表，极大减少磁盘空间占用\nInnoDB\nInnoDB在MySQL 5.5后成为默认索引，它的特点是：\n1.支持行锁，采用MVCC来支持高并发\n2.支持事务\n3.支持外键\n4.支持崩溃后的安全恢复\n5.不支持全文索引\n总体来讲，MyISAM适合SELECT密集型的表，而InnoDB适合INSERT和UPDATE密集型的表\nMyISAM速度可能超快，占用存储空间也小，但是程序要求事务支持，故InnoDB是必须的，故该方案无法执行，放弃！\n3.分区\nMySQL在5.1版引入的分区是一种简单的水平拆分，用户需要在建表的时候加上分区参数，对应用是透明的无需修改代码\n对用户来说，分区表是一个独立的逻辑表，但是底层由多个物理子表组成，实现分区的代码实际上是通过对一组底层表的对象封装，但对SQL层来说是一个完全封装底层的黑盒子。MySQL实现分区的方式也意味着索引也是按照分区的子表定义，没有全局索引\n用户的SQL语句是需要针对分区表做优化，SQL条件中要带上分区条件的列，从而使查询定位到少量的分区上，否则就会扫描全部分区，可以通过EXPLAIN PARTITIONS来查看某条SQL语句会落在那些分区上，从而进行SQL优化，我测试，查询时不带分区条件的列，也会提高速度，故该措施值得一试。\n分区的好处是：\n1.可以让单表存储更多的数据\n2.分区表的数据更容易维护，可以通过清楚整个分区批量删除大量数据，也可以增加新的分区来支持新插入的数据。另外，还可以对一个独立分区进行优化、检查、修复等操作\n3.部分查询能够从查询条件确定只落在少数分区上，速度会很快\n4.分区表的数据还可以分布在不同的物理设备上，从而搞笑利用多个硬件设备\n5.可以使用分区表赖避免某些特殊瓶颈，例如InnoDB单个索引的互斥访问、ext3文件系统的inode锁竞争\n6.可以备份和恢复单个分区\n分区的限制和缺点：\n1.一个表最多只能有1024个分区\n2.如果分区字段中有主键或者唯一索引的列，那么所有主键列和唯一索引列都必须包含进来\n3.分区表无法使用外键约束\n4.NULL值会使分区过滤无效\n5.所有分区必须使用相同的存储引擎\n分区的类型：\n1.RANGE分区：基于属于一个给定连续区间的列值，把多行分配给分区\n2.LIST分区：类似于按RANGE分区，区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择\n3.HASH分区：基于用户定义的表达式的返回值来进行选择的分区，该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含MySQL中有效的、产生非负整数值的任何表达式\n4.KEY分区：类似于按HASH分区，区别在于KEY分区只支持计算一列或多列，且MySQL服务器提供其自身的哈希函数。必须有一列或多列包含整数值\n5.具体关于mysql分区的概念请自行google或查询官方文档，我这里只是抛砖引玉了。\n我首先根据月份把上网记录表RANGE分区了12份，查询效率提高6倍左右，效果不明显，故：换id为HASH分区，分了64个分区，查询速度提升显著。问题解决！\n结果如下：PARTITION BY HASH (id)PARTITIONS 64\nselect count() from readroom_website; \u0026ndash;11901336行记录\n/ 受影响行数: 0 已找到记录: 1 警告: 0 持续时间 1 查询: 5.734 sec. /\nselect * from readroom_website where month(accesstime) =11 limit 10;\n/ 受影响行数: 0 已找到记录: 10 警告: 0 持续时间 1 查询: 0.719 sec. */\n4.分表\n分表就是把一张大表，按照如上过程都优化了，还是查询卡死，那就把这个表分成多张表，把一次查询分成多次查询，然后把结果组合返回给用户。\n分表分为垂直拆分和水平拆分，通常以某个字段做拆分项。比如以id字段拆分为100张表： 表名为 tableName_id%100\n但：分表需要修改源程序代码，会给开发带来大量工作，极大的增加了开发成本，故：只适合在开发初期就考虑到了大量数据存在，做好了分表处理，不适合应用上线了再做修改，成本太高！！！而且选择这个方案，都不如选择我提供的第二第三个方案的成本低！故不建议采用。\n5.分库\n把一个数据库分成多个，建议做个读写分离就行了，真正的做分库也会带来大量的开发成本，得不偿失！不推荐使用。\n方案二详细说明：升级数据库，换一个100%兼容mysql的数据库 mysql性能不行，那就换个。为保证源程序代码不修改，保证现有业务平稳迁移，故需要换一个100%兼容mysql的数据库。\n开源选择\n1.tiDB https://github.com/pingcap/tidb\n2.Cubrid https://www.cubrid.org/\n3.开源数据库会带来大量的运维成本且其工业品质和MySQL尚有差距，有很多坑要踩，如果你公司要求必须自建数据库，那么选择该类型产品。\n云数据选择\n1.阿里云POLARDB\n2.https://www.aliyun.com/produc\u0026hellip;\n官方介绍语：POLARDB 是阿里云自研的下一代关系型分布式云原生数据库，100%兼容MySQL，存储容量最高可达 100T，性能最高提升至 MySQL 的 6 倍。POLARDB 既融合了商业数据库稳定、可靠、高性能的特征，又具有开源数据库简单、可扩展、持续迭代的优势，而成本只需商用数据库的 1/10。\n我开通测试了一下，支持免费mysql的数据迁移，无操作成本，性能提升在10倍左右，价格跟rds相差不多，是个很好的备选解决方案！\n1.阿里云OcenanBase\n2.淘宝使用的，扛得住双十一，性能卓著，但是在公测中，我无法尝试，但值得期待\n3.阿里云HybridDB for MySQL (原PetaData)\n4.https://www.aliyun.com/produc\u0026hellip;\n官方介绍：云数据库HybridDB for MySQL （原名PetaData）是同时支持海量数据在线事务（OLTP）和在线分析（OLAP）的HTAP（Hybrid Transaction/Analytical Processing）关系型数据库。\n我也测试了一下，是一个olap和oltp兼容的解决方案，但是价格太高，每小时高达10块钱，用来做存储太浪费了，适合存储和分析一起用的业务。\n1.腾讯云DCDB\n2.https://cloud.tencent.com/pro\u0026hellip;\n官方介绍：DCDB又名TDSQL，一种兼容MySQL协议和语法，支持自动水平拆分的高性能分布式数据库——即业务显示为完整的逻辑表，数据却均匀的拆分到多个分片中；每个分片默认采用主备架构，提供灾备、恢复、监控、不停机扩容等全套解决方案，适用于TB或PB级的海量数据场景。\n腾讯的我不喜欢用，不多说。原因是出了问题找不到人，线上问题无法解决头疼！但是他价格便宜，适合超小公司，玩玩。\n方案三详细说明：去掉mysql，换大数据引擎处理数据 数据量过亿了，没得选了，只能上大数据了。\n开源解决方案 hadoop家族。hbase/hive怼上就是了。但是有很高的运维成本，一般公司是玩不起的，没十万投入是不会有很好的产出的！\n云解决方案\n这个就比较多了，也是一种未来趋势，大数据由专业的公司提供专业的服务，小公司或个人购买服务，大数据就像水/电等公共设施一样，存在于社会的方方面面。\n国内做的最好的当属阿里云。\n我选择了阿里云的MaxCompute配合DataWorks，使用超级舒服，按量付费，成本极低。\nMaxCompute可以理解为开源的Hive，提供sql/mapreduce/ai算法/python脚本/shell脚本等方式操作数据，数据以表格的形式展现，以分布式方式存储，采用定时任务和批处理的方式处理数据。DataWorks提供了一种工作流的方式管理你的数据处理任务和调度监控。\n当然你也可以选择阿里云hbase等其他产品，我这里主要是离线处理，故选择MaxCompute，基本都是图形界面操作，大概写了300行sql，费用不超过100块钱就解决了数据处理问题。\n","permalink":"https://ktzxy.top/posts/cj9x4f82zk/","summary":"大型数据表的优化过程","title":"大型数据表的优化过程"},{"content":"1. MongoDB 官网下载地址 MongoDB 的下载地址：\nhttps://www.mongodb.com/try/download http://dl.mongodb.org/dl/win32/x86_64 下载的安装包也有两种形式，一种是一键安装的 msi 文件，还有一种是解压缩就能使用的 zip 文件，哪种形式均可\n2. MongoDB（windows 版）安装与使用 2.1. msi 安装 运行 mongodb-win32-x86_64-2008plus-ssl-4.0.8-signed.msi 安装\n直接默认即可，点击next\n取消勾选，不安装图形化工具，否则时间非常非常长。\n2.2. zip 解压安装 下载 zip 包后，解压即可。与安装版一样，其中 bin 目录包含了所有 mongodb 的可执行命令。mongodb 在运行时需要指定一个数据存储的目录，所以创建一个数据存储目录，通常放置在安装目录中，此处创建 data 的目录用来存储数据。解压缩与创建完毕后会得到如下文件：\n2.3. 安装过程可能出现的问题 在 win7 系统安装 mongodb 需要 vc++ 运行库，如果没有则会提示“无法启动此程序，因为计算机中丢失“VCRUNTIME140.dll”。上网查找安装\n在浏览器中搜索提示缺少的名称对应的文件，并下载，将下载的文件拷贝到 windows 安装目录的 system32 目录下，然后在命令行中执行 regsvr32 命令注册此文件。根据下载的文件名不同，执行命令前更改对应名称。\n1 regsvr32 vcruntime140_1.dll 2.4. 启动 MongoDB 2.4.1. 进入MongoDB的安装目录，创建相关文件 有几个文件夹具体如下（如果安装的版本没有，则手动创建）\n数据库路径（data目录） 日志路径（log目录） 日志文件（log目录下，创建mongo.log文件） 2.4.2. 创建配置文件mongo.conf 增加如下内容\n1 2 3 4 5 6 7 8 9 10 11 12 # 数据库路径 dbpath=D:\\development\\MongoDB\\Server\\4.0\\data # 日志输出文件路径 logpath=D:\\development\\MongoDB\\Server\\4.0\\log\\mongo.log # 错误日志采用追加模式 logappend=true # 启用日志文件，默认启用 journal=true # 这个选项可以过滤掉一些无用的日志信息，若需要调试使用请设置为false quiet=true # 端口号 默认为27017 port=27017 2.4.3. 安装 MongoDB服务 打开 CMD 命令窗口，进入MongoDB 的安装位置的 bin 目录中\n1 cd /d D:\\xxxx\\MongoDB\\Server\\4.0\\bin 执行 bin/mongod.exe，并指定以下参数\n--install 参数选项，表示安装服务 --config 参数选项，用于指定之前创建的配置文件。 1 mongod.exe --config \u0026#34;D:\\development\\MongoDB\\Server\\4.0\\mongo.conf\u0026#34; --install 注：如果不创建上面的配置文件，在启动服务器时，也可以通过参数 --dbpath 指定数据存储位置，可以根据需要自行设置数据存储路径。默认服务端口 27017\n1 mongod.exe --dbpath=..\\data\\db 2.4.4. 启动 MongoDB 服务（需要使用管理员打开） 1 2 3 4 # 使用显示名称 net start \u0026#34;MongoDB Server\u0026#34; # 或者使用服务名称 net start MongoDB 2.4.5. 关闭 MongoDB 服务（需要使用管理员打开） 1 2 3 4 # 使用显示名称 net stop \u0026#34;MongoDB Server\u0026#34; # 或者使用服务名称 net start MongoDB 2.4.6. 移除 MongoDB 服务 1 \u0026#34;d:\\MongoDB\\Server\\3.4\\bin\\mongod.exe\u0026#34; --remove 2.4.7. 测试是否启动成功 启动 mongodb 服务，命令执行后，浏览器中输入 http://127.0.0.1:27017 看到如下界面即说明启动成功\n2.4.8. 启动客户端 也可以通过 bin 目录下的 mongo.exe 连接 mongodb\n1 mongo --host=127.0.0.1 --port=27017 3. MongoDB（docker版）安装与启动 3.1. 查看可用的 MongoDB 版本 访问 MongoDB 镜像库地址： https://hub.docker.com/_/mongo?tab=tags\u0026amp;page=1。\n3.2. 安装dokcer版本MongoDB 创建MongoDB容器 1 2 3 4 5 6 7 8 9 10 11 12 13 # 搜索镜像 docker search mongo # 拉取镜像 docker pull mongo:4.0.18 # 查看本地的镜像 docker images # 创建容器运行挂载的目录 mkdir -pv /usr/local/software/mongodb/data/db mkdir -pv /usr/local/software/mongodb/log # 运行容器。--auth：需要密码才能访问容器服务。 docker run -id --name mongo -v /usr/local/software/mongodb/data/db:/usr/local/software/mongodb/data/db -v /usr/local/software/mongodb/log:/usr/local/software/mongodb/log -p 27017:27017 mongo:4.0.18 --auth 查看容器运行情况 1 docker ps -a 3.3. 进入容器 连接容器 1 2 # 登陆容器 docker exec -it mongo /bin/bash 修改配置文件 1 2 3 4 5 6 7 8 # docker版本的mongoDb配置文件位置 /etc/mongod.conf.orig cd /etc # 更新源 apt-get update # 安装 vim(因为容器没有vim) apt-get install vim # 修改mongoDb的配置文件 vim /etc/mongod.conf.orig 修改注意点\n1 2 3 4 1.确保注释掉`# bindIp: 127.0.0.1` 或者改成`bindIp: 0.0.0.0` 即可开启远程连接 2.开启权限认证 security： authorization: enabled # 注意缩进，参照其他的值来改，若是缩进不对可能导致后面服务不能重启 注：以下配置文件的格式是v4.0版本以后的配置\n重启容器 1 docker restart mongo 3.4. 进入mongoDB 启动容器之后，使用admin进入 1 docker exec -it mongo mongo admin 创建管理员用户 1 2 3 4 5 6 7 8 # 切换数据库 use admin # 创建一个名为 root，密码为 123 的用户。 db.createUser({ user:\u0026#39;root\u0026#39;,pwd:\u0026#39;123\u0026#39;,roles:[ { role:\u0026#39;root\u0026#39;, db: \u0026#39;admin\u0026#39;}]}); # 尝试使用上面创建的用户信息进行连接。 db.auth(\u0026#39;root\u0026#39;, \u0026#39;123\u0026#39;) # 退出 exit 开放27017端口 1 firewall-cmd --zone=public --add-port=27017/tcp --permanent 3.5. 测试docker容器是否已经对外开放服务 4. studio3t 客户端使用 studio3t 是 mongodb 优秀的客户端工具。官方网站：https://studio3t.com/\n下载安装后运行程序，创建一个新连接： 填写连接信息： 修改字体：默认Studio3t的字体太小，需要修改字体。点击菜单：Edit \u0026ndash;\u0026gt; Preferences 5. Robot3t 客户端使用 Robot3t 是一款绿色软件，无需安装，解压缩即可。解压缩完毕后进入安装目录双击 robot3t.exe 即可使用。\n打开软件首先要连接 MongoDB 服务器，选择【File】菜单，选择【Connect\u0026hellip;】\n进入连接管理界面后，选择左上角的【Create】链接，创建新的连接设置\n如果输入设置值即可连接（默认不修改即可连接本机 27017 端口）\n连接成功后在命令输入区域输入命令即可操作 MongoDB\n创建数据库：在左侧菜单中使用右键创建，输入数据库名称即可 创建集合：在 Collections 上使用右键创建，输入集合名称即可，集合等同于数据库中的表的作用 新增文档：文档是一种类似 json 格式的数据，初学者可以先把数据理解为就是 json 数据 ","permalink":"https://ktzxy.top/posts/68vsn79kl7/","summary":"MongoDB 安装与使用","title":"MongoDB 安装与使用"},{"content":"Go的数组 Array数组介绍 数组是指一系列同一类型数据的集合。数组中包含的每个数据被称为数组元素（element），这种类型可以是意的原始类型，比如int、string等，也可以是用户自定义的类型。一个数组包含的元素个数被称为数组的长度。在Golang中数组是一个长度固定的数据类型，数组的长度是类型的一部分，也就是说[5]int和[10]int是两个不同的类型。Golang中数组的另一个特点是占用内存的连续性，也就是说数组中的元素是被分配到连续的内存地址中的，因而索引数组元素的速度非常快。\n和数组对应的类型是Slice（切片），Slice是可以增长和收缩的动态序列，功能也更灵活，但是想要理解slice工作原理的话需要先理解数组，所以本节主要为大家讲解数组的使用。\n数组定义 1 var 数组变量名 [元素数量] T 示例\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // 数组的长度是类型的一部分 var arr1 [3]int var arr2 [4]string fmt.Printf(\u0026#34;%T, %T \\n\u0026#34;, arr1, arr2) // 数组的初始化 第一种方法 var arr3 [3]int arr3[0] = 1 arr3[1] = 2 arr3[2] = 3 fmt.Println(arr3) // 第二种初始化数组的方法 var arr4 = [4]int {10, 20, 30, 40} fmt.Println(arr4) // 第三种数组初始化方法，自动推断数组长度 var arr5 = [...]int{1, 2} fmt.Println(arr5) // 第四种初始化数组的方法，指定下标 a := [...]int{1:1, 3:5} fmt.Println(a) 遍历数组 方法1\n1 2 3 4 5 // 第四种初始化数组的方法，指定下标 a := [...]int{1:1, 3:5} for i := 0; i \u0026lt; len(a); i++ { fmt.Print(a[i], \u0026#34; \u0026#34;) } 方法2\n1 2 3 4 5 // 第四种初始化数组的方法，指定下标 a := [...]int{1:1, 3:5} for _, value := range a { fmt.Print(value, \u0026#34; \u0026#34;) } 数组的值类型 数组是值类型，赋值和传参会赋值整个数组，因此改变副本的值，不会改变本身的值\n1 2 3 4 5 // 数组 var array1 = [...]int {1, 2, 3} array2 := array1 array2[0] = 3 fmt.Println(array1, array2) 例如上述的代码，我们将数组进行赋值后，该改变数组中的值时，发现结果如下\n1 [1 2 3] [3 2 3] 这就说明了，golang中的数组是值类型，而不是和java一样属于引用数据类型\n切片定义(引用类型) 在golang中，切片的定义和数组定义是相似的，但是需要注意的是，切片是引用数据类型，如下\n1 2 3 4 5 // 切片定义 var array3 = []int{1,2,3} array4 := array3 array4[0] = 3 fmt.Println(array3, array4) 我们通过改变第一个切片元素，然后查看最后的效果\n1 [3 2 3] [3 2 3] 二维数组 Go语言支持多维数组，我们这里以二维数组为例（数组中又嵌套数组）：\n1 var 数组变量名 [元素数量][元素数量] T 示例\n1 2 3 // 二维数组 var array5 = [2][2]int{{1,2},{2,3}} fmt.Println(array5) 数组遍历 二维数据组的遍历\n1 2 3 4 5 6 7 // 二维数组 var array5 = [2][2]int{{1,2},{2,3}} for i := 0; i \u0026lt; len(array5); i++ { for j := 0; j \u0026lt; len(array5[0]); j++ { fmt.Println(array5[i][j]) } } 遍历方式2\n1 2 3 4 5 for _, item := range array5 { for _, item2 := range item { fmt.Println(item2) } } 类型推导 另外我们在进行数组的创建的时候，还可以使用类型推导，但是只能使用一个 \u0026hellip;\n1 2 // 二维数组（正确写法） var array5 = [...][2]int{{1,2},{2,3}} 错误写法\n1 2 // 二维数组 var array5 = [2][...]int{{1,2},{2,3}} 完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 package main import \u0026#34;fmt\u0026#34; func main() { // 数组的长度是类型的一部分 var arr1 [3]int var arr2 [4]string fmt.Printf(\u0026#34;%T, %T \\n\u0026#34;, arr1, arr2) // 数组的初始化 第一种方法 var arr3 [3]int arr3[0] = 1 arr3[1] = 2 arr3[2] = 3 fmt.Println(arr3) // 第二种初始化数组的方法 var arr4 = [4]int {10, 20, 30, 40} fmt.Println(arr4) // 第三种数组初始化方法，自动推断数组长度 var arr5 = [...]int{1, 2} fmt.Println(arr5) // 第四种初始化数组的方法，指定下标 a := [...]int{1:1, 3:5} fmt.Println(a) for i := 0; i \u0026lt; len(a); i++ { fmt.Print(a[i], \u0026#34; \u0026#34;) } for _, value := range a { fmt.Print(value, \u0026#34; \u0026#34;) } fmt.Println() // 值类型 引用类型 // 基本数据类型和数组都是值类型 var aa = 10 bb := aa aa = 20 fmt.Println(aa, bb) // 数组 var array1 = [...]int {1, 2, 3} array2 := array1 array2[0] = 3 fmt.Println(array1, array2) // 切片定义 var array3 = []int{1,2,3} array4 := array3 array4[0] = 3 fmt.Println(array3, array4) // 二维数组 var array5 = [...][2]int{{1,2},{2,3}} for i := 0; i \u0026lt; len(array5); i++ { for j := 0; j \u0026lt; len(array5[0]); j++ { fmt.Println(array5[i][j]) } } for _, item := range array5 { for _, item2 := range item { fmt.Println(item2) } } } ","permalink":"https://ktzxy.top/posts/mu1dg60nbb/","summary":"6 Go的数组","title":"6 Go的数组"},{"content":"Java案例 1. 将小数类型转成金额形式的字符串 使用 NumberFormat 类进行数值格式，再对截取字符串\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.moon.testproj; import java.text.NumberFormat; import java.util.Locale; public class Test { public static void main(String[] args) { double num = 1102834.83343434; NumberFormat format = NumberFormat.getCurrencyInstance(Locale.CHINA); // 输出：Locale.CHINA：￥1,102,834.83 System.out.println(\u0026#34;Locale.CHINA：\u0026#34;+format.format(num)); StringBuilder sb = new StringBuilder(format.format(num)); sb.deleteCharAt(0); // 输出：1102834.83343434 System.out.println(num); // 输出：1,102,834.83 System.out.println(sb.toString()); } } 2. 获取项目的所在的操作系统 1 2 3 4 5 6 String os = System.getProperties().getProperty(\u0026#34;os.name\u0026#34;); if (os.startsWith(\u0026#34;win\u0026#34;) || os.startsWith(\u0026#34;Win\u0026#34;)) { prefixFont = \u0026#34;C:\\\\Windows\\\\Fonts\u0026#34; + File.separator; } else { prefixFont = \u0026#34;/usr/share/fonts/chinese\u0026#34; + File.separator; } ","permalink":"https://ktzxy.top/posts/enxjjazcur/","summary":"Java扩展 程序案例","title":"Java扩展 程序案例"},{"content":" 监控系统选型，这篇不可不读！ 一、监控基础知识 监控是需要站在公司的业务角度去考虑，而不是针对某个监控技术的使用。\n1.1 监控系统的7大作用 正所谓「无监控，不运维」，监控系统的地位不言而喻。\n不管你是监控系统的开发者还是使用者，首先肯定要清楚：监控系统的目标是什么？它能发挥什么作用？\n实时采集监控数据：包括 硬件、操作系统、中间件、应用程序等各个维度的数据。 实时反馈监控状态：通过对采集的数据进行多维度统计和可视化展示，能实时体现监控对象的状态是正常还是异常。 预知故障和告警： 能够提前预知故障风险，并及时发出告警信息。 辅助定位故障：提供故障发生时的各项指标数据，辅助故障分析和定位。 辅助性能调优：为性能调优提供数据支持，比如慢SQL，接口响应时间等。 辅助容量规划：为服务器、中间件以及应用集群的容量规划提供数据支撑。 辅助自动化运维：为自动扩容或者根据配置的SLA进行服务降级等智能运维提供数据支撑。 1.2 使用监控系统的正确姿势 出任何线上事故，先不说其他地方有问题，监控部分一定是有问题的。\n听着很甩锅的一句话，仔细思考好像有一定道理。我们在事故复盘时，通常会思考这3个和监控有关的问题：有没有做监控？监控是否及时？监控信息是否有助于快速定位问题？\n可见光有一套好的监控系统还不够，还必须知道 「如何用好它」。一个成熟的研发团队通常会定一个监控规范，用来统一监控系统的使用方法。\n了解监控对象的工作原理：要做到对监控对象有基本的了解，清楚它的工作原理。比如想对JVM进行监控，你必须清楚JVM的堆内存结构和垃圾回收机制。 确定监控对象的指标 ：清楚使用哪些指标来刻画监控对象的状态？比如想对某个接口进行监控，可以采用请求量、耗时、超时量、异常量等指标来衡量。 定义合理的报警阈值和等级：达到什么阈值需要告警？对应的故障等级是多少？不需要处理的告警不是好告警，可见定义合理的阈值有多重要，否则只会降低运维效率或者让监控系统失去它的作用。 建立完善的故障处理流程：收到故障告警后，一定要有相应的处理流程和oncall机制，让故障及时被跟进处理。 1.3 监控的对象和指标都有哪些？ 监控已然成为了整个产品生命周期非常重要的一环，运维关注硬件和基础监控，研发关注各类中间件和应用层的监控，产品关注核心业务指标的监控。 可见，监控的对象已经越来越立体化。\n这里，我对常用的监控对象以及监控指标做了分类整理，供大家参考。\n3.1 硬件监控\n包括：电源状态、CPU状态、机器温度、风扇状态、物理磁盘、raid状态、内存状态、网卡状态\n3.2 服务器基础监控\nCPU：单个CPU以及整体的使用情况 内存：已用内存、可用内存 磁盘：磁盘使用率、磁盘读写的吞吐量 网络：出口流量、入口流量、TCP连接状态 3.3 数据库监控\n包括：数据库连接数、QPS、TPS、并行处理的会话数、缓存命中率、主从延时、锁状态、慢查询\n3.4 中间件监控\nNginx：活跃连接数、等待连接数、丢弃连接数、请求量、耗时、5XX错误率 Tomcat：最大线程数、当前线程数、请求量、耗时、错误量、堆内存使用情况、GC次数和耗时 缓存 ：成功连接数、阻塞连接数、已使用内存、内存碎片率、请求量、耗时、缓存命中率 消息队列：连接数、队列数、生产速率、消费速率、消息堆积量 3.5 应用监控\nHTTP接口：URL存活、请求量、耗时、异常量 RPC接口：请求量、耗时、超时量、拒绝量 JVM ：GC次数、GC耗时、各个内存区域的大小、当前线程数、死锁线程数 线程池：活跃线程数、任务队列大小、任务执行耗时、拒绝任务数 连接池：总连接数、活跃连接数 日志监控：访问日志、错误日志 业务指标：视业务来定，比如PV、订单量等 1.4 监控系统的基本流程 无论是开源的监控系统还是自研的监控系统，监控的整个流程大同小异，一般都包括以下模块：\n数据采集：采集的方式有很多种，包括 日志埋点进行采集（通过Logstash、Filebeat等进行上报和解析）， JMX标准接口输出监控指标，被监控对象提供REST API进行数据 采集（如Hadoop、ES），系统命令行，统一的SDK进行侵入式的埋点和上报等。 数据传输：将采集的数据以TCP、UDP或者HTTP协议的形式上报给监控系统，有主动Push模式，也有被动Pull模式。 数据存储：有使用MySQL、Oracle等RDBMS存储的，也有使用时序数据库RRDTool、OpentTSDB、InfluxDB存储的，还有使用HBase存储的。 数据展示：数据指标的图形化展示。 监控告警：灵活的告警设置，以及支持邮件、短信、IM等多种通知通道。 1.5 监控目标 先来了解什么是监控，监控的重要性以及监控的目标，当然每个人所在的行业不同、公司不同、业务不同、岗位不同、对监控的理解也不同，但是我们需要注意，监控是需要站在公司的业务角度去考虑，而不是针对某个监控技术的使用。\n对系统不间断实时监控:实际上是对系统不间断的实时监控(这就是监控) 实时反馈系统当前状态:我们监控某个硬件、或者某个系统，都是需要能实时看到当前系统的状态，是正常、异常、或者故障 保证服务可靠性安全性:我们监控的目的就是要保证系统、服务、业务正常运行 保证业务持续稳定运行:如果我们的监控做得很完善，即使出现故障，能第一时间接收到故障报警，在第一时间处理解决，从而保证业务持续性的稳定运行。 1.6 监控方法 了解监控对象:我们要监控的对象你是否了解呢？比如CPU到底是如何工作的？ 性能基准指标:我们要监控这个东西的什么属性？比如CPU的使用率、负载、用户态、内核态、上下文切换。 报警阈值定义:怎么样才算是故障，要报警呢？比如CPU的负载到底多少算高，用户态、内核态分别跑多少算高？ 故障处理流程:收到了故障报警，那么我们怎么处理呢？有什么更高效的处理流程吗？ 1.7 监控核心 发现问题:当系统发生故障报警，我们会收到故障报警的信息 定位问题:故障邮件一般都会写某某主机故障、具体故障的内容，我们需要对报警内容进行分析，比如一台服务器连不上:我们就需要考虑是网络问题、还是负载太高导致长时间无法连接，又或者某开发触发了防火墙禁止的相关策略等等，我们就需要去分析故障具体原因。 解决问题:当然我们了解到故障的原因后，就需要通过故障解决的优先级去解决该故障。 总结问题:当我们解决完重大故障后，需要对故障原因以及防范进行总结归纳，避免以后重复出现。 1.8 监控流程 数据采集: Zabbix通过SNMP、Agent、ICMP、SSH、IPMI等对系统进行数据采集 数据存储: Zabbix存储在MySQL上，也可以存储在其他数据库服务 数据分析: 当我们事后需要复盘分析故障时，zabbix能给我们提供图形以及时间等相关信息，方面我们确定故障所在。 数据展示: web界面展示、(移动APP、java_php开发一个web界面也可以) 监控报警:电话报警、邮件报警、微信报警、短信报警、报警升级机制等（无论什么报警都可以） 报警处理:当接收到报警，我们需要根据故障的级别进行处理，比如:重要紧急、重要不紧急，等。根据故障的级别，配合相关的人员进行快速处理。 二、主流监控系统介绍 下面再来认识下主流的开源监控系统，由于篇幅有限，我挑选了3款使用最广泛的监控系统：Zabbix、Open-Falcon、Prometheus，会对它们的架构进行介绍，同时总结下各自的优劣势。\n老牌监控:\nMRTG（Multi Route Trffic Grapher）是一套可用来绘制网络流量图的软件，由瑞士奥尔滕的Tobias Oetiker与Dave Rand所开发，以GPL授权。 MRTG最好的版本是1995年推出的，用perl语言写成，可跨平台使用，数据采集用SNMP协议，MRTG将手机到的数据通过Web页面以GIF或者PNG格式绘制出图像。\nGrnglia是一个跨平台的、可扩展的、高性能的分布式监控系统，如集群和网格。它基于分层设计，使用广泛的技术，用RRDtool存储数据。具有可视化界面，适合对集群系统的自动化监控。其精心设计的数据结构和算法使得监控端到被监控端的连接开销非常低。目前已经有成千上万的集群正在使用这个监控系统，可以轻松的处理2000个节点的集群环境。\nCacti（英文含义为仙人掌）是一套基于PHP、MySQL、SNMP和RRDtool开发的网络流量监测图形分析工具，它通过snmpget来获取数据使用RRDtool绘图，但使用者无须了解RRDtool复杂的参数。提供了非常强大的数据和用户管理功能，可以指定每一个用户能查看树状结构、主机设备以及任何一张图，还可以与LDAP结合进行用户认证，同时也能自定义模板。在历史数据展示监控方面，其功能相当不错。 Cacti通过添加模板，使不同设备的监控添加具有可复用性，并且具备可自定义绘图的功能，具有强大的运算能力（数据的叠加功能）\nNagios是一个企业级监控系统，可监控服务的运行状态和网络信息等，并能监视所指定的本地或远程主机状态以及服务，同时提供异常告警通知功能等。 Nagios可运行在Linux和UNIX平台上。同时提供Web界面，以方便系统管理人员查看网络状态、各种系统问题、以及系统相关日志等 Nagios的功能侧重于监控服务的可用性，能根据监控指标状态触发告警。 目前Nagios也占领了一定的市场份额，不过Nagios并没有与时俱进，已经不能满足于多变的监控需求，架构的扩展性和使用的便捷性有待增强，其高级功能集成在商业版Nagios XI中。\nSmokeping主要用于监视网络性能，包括常规的ping、www服务器性能、DNS查询性能、SSH性能等。底层也是用RRDtool做支持，特点是绘制图非常漂亮，网络丢包和延迟用颜色和阴影来标示，支持将多张图叠放在一起，其作者还开发了MRTG和RRDtll等工具。 Smokeping的站点为：http://tobi.oetiker.cn/hp\n开源监控系统OpenTSDB用Hbase存储所有时序（无须采样）的数据，来构建一个分布式、可伸缩的时间序列数据库。它支持秒级数据采集，支持永久存储，可以做容量规划，并很容易地接入到现有的告警系统里。 OpenTSDB可以从大规模的集群（包括集群中的网络设备、操作系统、应用程序）中获取相应的采集指标，并进行存储、索引和服务，从而使这些数据更容易让人理解，如Web化、图形化等。\n王牌监控\nZabbix是一个分布式监控系统，支持多种采集方式和采集客户端，有专用的Agent代理，也支持SNMP、IPMI、JMX、Telnet、SSH等多种协议，它将采集到的数据存放到数据库，然后对其进行分析整理，达到条件触发告警。其灵活的扩展性和丰富的功能是其他监控系统所不能比的。相对来说，它的总体功能做的非常优秀。 从以上各种监控系统的对比来看，Zabbix都是具有优势的，其丰富的功能、可扩展的能力、二次开发的能力和简单易用的特点，读者只要稍加学习，即可构建自己的监控系统。 小米的监控系统：open-falcon。open-falcon的目标是做最开放、最好用的互联网企业级监控产品。\n2.1 Zabbix（老牌监控的优秀代表） Zabbix 1998年诞生，核心组件采用C语言开发，Web端采用PHP开发。它属于老牌监控系统中的优秀代表，监控功能很全面，使用也很广泛，差不多有70%左右的互联网公司都曾使用过 Zabbix 作为监控解决方案。\n先来了解下Zabbix的架构设计：\nZabbix Server：核心组件，C语言编写，负责接收Agent、Proxy发送的监控数据，也支持JMX、SNMP等多种协议直接采集数据。同时，它还负责数据的汇总存储以及告警触发等。 Zabbix Proxy：可选组件，对于被监控机器较多的情况下，可使用Proxy进行分布式监控，它能代理Server收集部分监控数据，以减轻Server的压力。 Zabbix Agentd：部署在被监控主机上，用于采集本机的数据并发送给Proxy或者Server，它的插件机制支持用户自定义数据采集脚本。Agent可在Server端手动配置，也可以通过自动发现机制被识别。数据收集方式同时支持主动Push和被动Pull 两种模式。 Database：用于存储配置信息以及采集到的数据，支持MySQL、Oracle等关系型数据库。同时，最新版本的Zabbix已经开始支持时序数据库，不过成熟度还不高。 Web Server：Zabbix的GUI组件，PHP编写，提供监控数据的展现和告警配置。 下面是 Zabbix 的优势：\n产品成熟：由于诞生时间长且使用广泛，拥有丰富的文档资料以及各种开源的数据采集插件，能覆盖绝大部分监控场景。 采集方式丰富：支持Agent、SNMP、JMX、SSH等多种采集方式，以及主动和被动的数据传输方式。 较强的扩展性：支持Proxy分布式监控，有agent自动发现功能，插件式架构支持用户自定义数据采集脚本。 配置管理方便 ：能通过Web界面进行监控和告警配置，操作方便，上手简单。 下面是 Zabbix 的劣势：\n性能瓶颈：机器量或者业务量大了后，关系型数据库的写入一定是瓶颈，官方给出的单机上限是5000台，个人感觉达不到，尤其现在应用层的指标越来越多。虽然最新版已经开始支持时序数据库，不过成熟度还不高。 应用层监控支持有限：如果想对应用程序做侵入式的埋点和采集（比如监控线程池或者接口性能），zabbix没有提供对应的sdk，通过插件式的脚本也能曲线实现此功能，个人感觉zabbix就不是做这个事的。 数据模型不强大：不支持tag，因此没法按多维度进行聚合统计和告警配置，使用起来不灵活。 方便二次开发难度大 ：Zabbix采用的是C语言，二次开发往往需要熟悉它的数据表结构，基于它提供的API更多只能做展示层的定制。 2.2 Open-Falcon（小米出品，国内流行） Open-falcon 是小米2015年开源的企业级监控工具，采用Go和Python语言开发，这是一款灵活、高性能且易扩展的新一代监控方案，目前小米、美团、滴滴等超过200家公司在使用它。\n小米初期也使用的Zabbix进行监控，但是机器量和业务量上来后，Zabbix就有些力不从心了。因此，后来自主研发了Open-Falcon，在架构设计上吸取了Zabbix的经验，同时很好地解决了Zabbix的诸多痛点。\n先来了解下Open-Falcon的架构设计：\nFalcon-agent：数据采集器和收集器，Go开发，部署在被监控的机器上，支持3种数据采集方式。首先它能自动采集单机200多个基础监控指标，无需做任何配置；同时支持用户自定义的plugin获取监控数据；此外，用户可通过http接口，自主push数据到本机的proxy-gateway，由gateway转发到server. Transfer：数据分发组件，接收客户端发送的数据，分别发送给数据存储组件Graph和告警判定组件Judge，Graph和Judge均采用一致性hash做数据分片，以提高横向扩展能力。同时Transfer还支持将数据分发到OpenTSDB，用于历史归档。 Graph：数据存储组件，底层使用RRDTool（时序数据库）做单个指标的存储，并通过缓存、分批写入磁盘等方式进行了优化。据说一个graph实例能够处理8W+每秒的写入速率。 Judge和Alarm：告警组件，Judge对Transfer组件上报的数据进行实时计算，判断是否要产生告警事件，Alarm组件对告警事件进行收敛处理后，将告警消息推送给各个消息通道。 API：面向终端用户，收到查询请求后会去Graph中查询指标数据，汇总结果后统一返回给用户，屏蔽了存储集群的分片细节。 下面是Open-Falcon的优势：\n自动采集能力：**Falcon-agent 能自动采集服务器的200多个基础指标（比如CPU、内存等），无需在server上做任何配置，这一点可以秒杀Zabbix. 强大的存储能力 ：底层采用RRDTool，并且通过一致性hash进行数据分片，构建了一个分布式的时序数据存储系统，可扩展性强。 灵活的数据模型：借鉴OpenTSDB，数据模型中引入了tag，这样能支持多维度的聚合统计以及告警规则设置，大大提高了使用效率。 插件统一管理：Open-Falcon的插件机制实现了对用户自定义脚本的统一化管理，可通过HeartBeat Server分发给agent，减轻了使用者自主维护脚本的成本。 个性化监控支持：基于Proxy-gateway，很容易通过自主埋点实现应用层的监控（比如监控接口的访问量和耗时）和其他个性化监控需求，集成方便。 下面是Open-Falcon的劣势：\n**整体发展一般**：**社区活跃度不算高，同时版本更新慢，有些大厂是基于它的稳定版本直接做二次开发的，关于以后的前景其实有点担忧。 UI不够友好 ：对于业务线的研发来说，可能只想便捷地完成告警配置和业务监控，但是它把机器分组、策略模板、模板继承等概念全部暴露在UI上，感觉在围绕这几个概念设计UI，理解有点费劲。 **安装比较复杂：**个人的亲身感受，由于它是从小米内部衍生出来的，虽然去掉了对小米内部系统的依赖，但是组件还是比较多，如果对整个架构不熟悉，安装很难一蹴而就。 2.3 Prometheus（号称下一代监控系统） Prometheus（普罗米修斯）是由前google员工2015年正式发布的开源监控系统，采用Go语言开发。它不仅有一个很酷的名字，同时它有Google与k8s的强力支持，开源社区异常火爆。\nPrometheus 2016年加入云原生基金会，是继k8s后托管的第二个项目，未来前景被相当看好。它和Open-Falcon最大不同在于：数据采集是基于Pull模式的，而不是Push模式，并且架构非常简单。\n先来了解下Prometheus的架构设计：\nPrometheus Server：核心组件，用于收集、存储监控数据。它同时支持静态配置和通过Service Discovery动态发现来管理监控目标，并从监控目标中获取数据。此外，Prometheus Server 也是一个时序数据库，它将监控数据保存在本地磁盘中，并对外提供自定义的 PromQL 语言实现对数据的查询和分析。 Exporter：用来采集数据，作用类似于agent，区别在于Prometheus是基于Pull方式拉取采集数据的，因此，Exporter通过HTTP服务的形式将监控数据按照标准格式暴露给Prometheus Server，社区中已经有大量现成的Exporter可以直接使用，用户也可以使用各种语言的client library自定义实现。 Push gateway：主要用于瞬时任务的场景，防止Prometheus Server来pull数据之前此类Short-lived jobs就已经执行完毕了，因此job可以采用push的方式将监控数据主动汇报给Push gateway缓存起来进行中转。 Alert Manager：当告警产生时，Prometheus Server将告警信息推送给Alert Manager，由它发送告警信息给接收方。 Web UI：Prometheus内置了一个简单的web控制台，可以查询配置信息和指标等，而实际应用中我们通常会将Prometheus作为Grafana的数据源，创建仪表盘以及查看指标。 下面是Prometheus的优势：\n轻量管理：架构简单，不依赖外部存储，单个服务器节点可直接工作，二进制文件启动即可，属于轻量级的Server，便于迁移和维护。 较强的处理能力：监控数据直接存储在Prometheus Server本地的时序数据库中，单个实例可以处理数百万的metrics。 灵活的数据模型：同Open-Falcon，引入了tag，属于多维数据模型，聚合统计更方便。 强大的查询语句：PromQL允许在同一个查询语句中，对多个metrics进行加法、连接和取分位值等操作。 很好地支持云环境：能自动发现容器，同时k8s和etcd等项目都提供了对Prometheus的原生支持，是目前容器监控最流行的方案。 下面是Prometheus的劣势：\n功能不够完善：Prometheus从一开始的架构设计就是要做到简单，不提供集群化方案，长期的持久化存储和用户管理，而这些是企业变大后所必须的特性，目前要做到这些只能在Prometheus之上进行扩展。 网络规划变复杂：由于Prometheus采用的是Pull模型拉取数据，意味着所有被监控的endpoint必须是可达的，需要合理规划网络的安全配置。 三、监控指标 3.1 硬件监控 可以通过IPMI对硬件详细情况进行监控，并对CPU、内存、磁盘、温度、风扇、电压等设置报警设置报警阈值(自行对监控报警内容编写合理的报警范围)\nIPMI工具无法获取到硬件的状态，可以借助MegaCli工具探测Raid磁盘队列状态 zabbix提供IPMI监控模板：Zabbix IPMI Interface 系统自带的IPMI模板只能监控，风扇，电源，和部分温度\n3.2 系统监控 中小型企业基本全是Linux服务器，那么我们肯定是要监控起系统资源的使用情况，系统监控是监控体系的基础。 监控主要对象: CPU有几个重要的概念:上下文切换、运行队列和使用率。\n这也是我们CPU监控的几个重点指标。 通常情况，每个处理器的运行队列不要高于3，CPU 利用率中用“户态/内核态”比例维持在70/30，空闲状态维持在50%，上下文切换要根据系统繁忙程度来综合考量。\n针对CPU常用的工具有:htop、top、vmstat、mpstat、dstat、glances\n3.3 应用监控 应用服务监控也是监控体系中比较重要的内容，例如： LVS、Haproxy、Docker、Nginx、PHP、Memcached、Redis、MySQL、Rabbitmq等等，相关的服务都需要使用zabbix监控起来。\n3.4 网络监控 作为一个针对全国用户的电商网站，时刻掌握各地到机房的网络状态也是必须的。 网络监控是我们构建监控平台是必须要考虑的，尤其是针对有多个机房的场景，各个机房之间的网络状态，机房和全国各地的网络状态都是我们需要重点关注的对象，那么如何掌握这些状态信息呢？我们需要借助于网络监控工具Smokeping。\nSmokeping 是rrdtool的作者Tobi Oetiker的作品，是用Perl写的，主要是监视网络性能，www 服务器性能，dns查询性能等，使用rrdtool绘图，而且支持分布式，直接从多个agent进行数据的汇总。\n同时，由于自己监控点比较少，还可以借助很多商业的监控工具，比如监控宝、听云、基调、博瑞等。同时这些服务提供商还可以帮助你监控CDN的状态。\n3.5 流量分析 网站流量分析对于运维人员来说，更是一门必须掌握的知识了。比如对于一家电商公司来说： 通过对订单来源的统计和分析，可以了解我们在某个网站上的广告投入有没有收到预期的效果。 可以区分不同地区的访问人数、甚至商品交易额等。\n百度统计、google分析、站长工具等等，只需要在页面嵌入一个js即可。 但是，数据始终是在对方手中，个性化定制不方便，于是google出一个叫piwik的开源分析工具\n3.6 日志监控 通常情况下，随着系统的运行，操作系统会产生系统日志，应用程序会产生应用程序的访问日志、错误日志，运行日志，网络日志，我们可以使用ELK来进行日志监控。\n对于日志监控来说，最见的需求就是收集、存储、查询、展示，开源社区正好有相对应的开源项目： logstash（收集） + elasticsearch（存储+搜索） + kibana（展示） 我们将这三个组合起来的技术称之为ELK Stack，所以说ELK Stack指的是Elasticsearch、Logstash、Kibana技术栈的结合。\n如果收集了日志信息，那么如果部署更新有异常出现，可以立即在kibana上看到。\n3.7 安全监控 虽然Linux开源的安全产品不少，比如四层iptables，七层WEB防护nginx+lua实现WAF，最后将相关的日志都收至Elkstack，通过图形化进行不同的攻击类型展示。\n3.8 API监控 由于API变得越来越重要，很显然我们也需要这样的数据来分辨我们提供的 API是否能够正常运作。 监控API接口GET、POST、PUT、DELETE、HEAD、OPTIONS的请求 可用性、正确性、响应时间为三大重性能指标\n3.9 性能监控 全面监控网页性能，DNS响应时间、HTTP建立连接时间、页面性能指数、响应时间、可用率、元素大小等\n3.10 业务监控 没有业务指标监控的监控平台，不是一个完善的监控平台，通常在我们的监控系统中，必须将我们重要的业务指标进行监控，并设置阈值进行告警通知。比如电商行业：\n每分钟产生多少订单， 每分钟注册多少用户， 每天有多少活跃用户， 每天有多少推广活动， 推广活动引入多少用户， 推广活动引入多少流量， 推广活动引入多少利润， 今天商品打包出库多少， 今天退货商品有多少，\n四、监控报警 电话 短信 微信 邮件 钉钉 其他方式 五、报警处理 一般报警后我们故障如何处理，首先，我们可以通过告警升级机制先自动处理，比如nginx服务down了，可以设置告警升级自动启动nginx。 但是如果一般业务出现了严重故障，我们通常根据故障的级别，故障的业务，来指派不同的运维人员进行处理。 当然不同业务形态、不同架构、不同服务可能采用的方式都不同，这个没有一个固定的模式套用。\n报警后的处理方式：\n故障自愈 故障级别 业务划分 人员划分 六、监控系统的选型建议 通过上面的介绍，大家对主流的监控系统应该有了一定的认识。面对选型问题，我的建议是：\n1、先明确清楚你的监控需求：要监控的对象有哪些？机器数量和监控指标有多少？需要具备什么样的告警功能？\n2、监控是一项长期建设的事情，一开始就想做一个 All In One 的监控解决方案，我觉得没有必要。从成本角度考虑，在初期直接使用开源的监控方案即可，先解决有无问题。\n3、从系统成熟度上看，Zabbix属于老牌的监控系统，资料多，功能全面且稳定，如果机器数量在几百台以内，不用太担心性能问题，另外，采用数据库分区、SSD硬盘、Proxy架构、Push采集模式都可以提高监控性能。\n4、Zabbix在服务器监控方面占绝对优势，可以满足90%以上的监控场景，但是应用层的监控似乎并不擅长，比如要监控线程池的状态、某个内部接口的执行时间等，这种通常都要做侵入式埋点。相反，新一代的监控系统Open-Falcon和Prometheus在这一点做得很好。\n5、从整体表现上来看，新一代监控系统也有明显的优势，比如：灵活的数据模型、更成熟的时序数据库、强大的告警功能，如果之前对zabbix这种传统监控没有技术积累，建议使用Open-Falcon或者Prometheus.\n6、Open-Falcon的核心优势在于数据分片功能，能支撑更多的机器和监控项；Prometheus则是容器监控方面的标配，有Google和k8s加持。\n7、Zabbix、Open-Falcon和Prometheus都支持和Grafana做快速集成，想要美观且强大的可视化体验，可以和Grafana进行组合。\n8、用合适的监控系统解决相应的问题即可，可以多套监控同时使用，这种在企业初期很常见。\n9、到中后期，随着机器数据增加和个性化需求增多（比如希望统一监控平台、打通公司的CMDB和组织架构关系），往往需要二次开发或者通过监控系统提供的API做集成，从这点来看，Open-Falcon或者Prometheus更合适。\n10、如果非要自研，可以多研究下主流监控系统的架构方案，借鉴它们的优势。\n","permalink":"https://ktzxy.top/posts/tyayntpnj7/","summary":"企业级监控平台，监控系统选型","title":"企业级监控平台，监控系统选型"},{"content":"1. JDK 概述 JDK (Java Development Kit) 是 Java 语言的软件开发工具包(SDK)，主要用于移动设备、嵌入式设备上的 Java 应用程序。JDK 是整个 Java 开发的核心，它包含了 JAVA 的运行环境（JVM+Java 系统类库）和 JAVA 工具。\nJDK 官网：https://www.oracle.com/java/\n2. windows 系统安装 JDK 2.1. JDK变量环境配置 最好的配置方式：将位置切割成两段，一段用JAVA_HOME保存，一段用\\bin保存。如下例：\n1 2 JAVA_HOME = C:\\Program Files\\Java\\jdk1.8.0_91 %JAVA_HOME%\\bin 相等于 --\u0026gt; C:\\Program Files\\Java\\jdk1.8.0_91\\bin 2.2. 安装多个 JDK 安装过程都一样。只是配置环境变量时改动一下。分别将多个不同版本的jdk设置一个环境变量，然后最终让JAVA_HOME指定当前需要使用的版本的变量即可\n1 2 3 JAVA_HOME_8 = D:\\development\\Java\\jdk1.8.0_311 JAVA_HOME_11 = D:\\development\\Java\\jdk-11.0.13 JAVA_HOME = %JAVA_HOME_8% 2.3. JDK 11 手动生成 jre 目录 许多java软件的运行需要依赖jre，但是在安装jdk11后，发现jdk11并没有自动安装jre环境。其实 jdk11 的安装包里是自带 jre 的，只不过没有自动安装，手动安装一下就可以了。\n使用cmd命令行窗口进入jdk 安装目录输入以下命令，即可生成 jre 目录：\n1 bin\\jlink.exe --module-path jmods --add-modules java.desktop --output jre 2.4. 注意事项 如果是安装版，请务必到以下位置删除这几个文件。(二者其一有)\nC:\\Program Files (x86)\\Common Files\\Oracle\\Java\\javapath C:\\Program Files\\Common Files\\Oracle\\Java\\javapath 如果不删除上面这些文件，直接到环境变量PATH，将下面这些引用删除即可：\n1 2 3 C:\\Program Files\\Common Files\\Oracle\\Java\\javapath C:\\Program Files (x86)\\Common Files\\Oracle\\Java\\javapath C:\\ProgramData\\Oracle\\Java\\javapath 3. Linux 系统安装 JDK 详见《Linux》相关的笔记\n4. Oracle JDK 与 Open JDK Oracle JDK 是基于 Open JDK 源代码的商业版本。要学习 Java 新技术可以去 Open JDK 官网学习。\nOpen JDK 官网：http://openjdk.java.net/\nJDK Enhancement Proposals(JDK增强建议)。通俗的讲JEP就是JDK的新特性\n5. JDK 版本号的选择 Oracle JDK 8u211 及以上版本进行具有商业用途(盈利目的)的应用或工具的开发时是要收费的\nJDK8 最后一个免费版本，JDK 8u202。但推荐下载 JDK 8u201？！说好的最后一个免费版本，为什么写了两个版本号 8u201 和 8u202 呢？到底要用哪一个？\n这就涉及到 Oracle 跟 Oracle JDK 的使用者之间的一个小小的约定或小常识了！下载奇数版本！！！\n从 2014 年 10 月发布 Java SE 7 Update 71 (Java SE 7u71) 开始，Oracle 在发布 Oracle JDK 关键补丁更新 (CPUs：Critical Patch Updates) 的同时一般会发布相应的补丁集更新 (PSUs：Patch Set Updates)。那么 CPUs 和 PSUs 之间有什么区别呢？\nOracle JDK 关键补丁更新 (CPUs) 包含安全漏洞修复和重要漏洞修复，Oracle 强烈建议所有 Oracle JDK 用户及时升级到最新的 CPU 版本，Oracle JDK 关键补丁更新 (CPUs) 版本号采用奇数编号！\nOracle JDK 补丁集更新 (PSUs) 包含相应 CPUs 中的所有修复以及其他非重要修复，仅当您受到Oracle JDK关键补丁更新 (CPUs)版本之外的其他漏洞的影响时才应当使用相应的补丁集更新 (PSUs) ，Oracle JDK 补丁集更新 (PSUs) 版本号采用偶数编号！\n所以，一般情况下只要下载奇数编号的最新版本更新就行了！但要记住：商业收费版本的不要用于商业用途！\n6. 综合扩展 6.1. JRE、JDK、JVM 及 JIT 的区别 JRE（Java run-time） 是 Java 运行时环境，是运行 Java 程序所必须的。 JDK（Java development kit）是 Java 程序开发工具集，如 Java 编译器，它也包含 JRE。 JVM（Java virtual machine）是 Java 虚拟机，它的责任是运行 Java 应用。 JIT（Just In Time compilation）是即时编译。为了提高热点代码的执行效率，在运行时，当代码执行的次数超过一定的阈值时，虚拟机会将 Java 字节码转换为与本地平台相关的机器码，并进行各种层次的优化。如：主要的热点代码会被准换为本地代码，这样有利大幅度提高 Java 应用的性能。 6.2. javap 反编译工具（待整理） 参考：Java编程教程-理解javap工具\n","permalink":"https://ktzxy.top/posts/d7rz4jwla7/","summary":"Java基础 JDK","title":"Java基础 JDK"},{"content":"👤 个人简介 不逐浮名，不争朝夕。\n于纷繁之中求秩序，于变化之中守原则。\n以工程为术，以思考为道。\n🌿 关于本站 世事翻涌，潮起潮落。\n有人逐浪，有人随波。\n我愿在变化之间，守一份秩序与笃定。\n技术为舟，时间为海。\n行远，亦行深。\n💻 技术与探索 编程 · 算法 · 系统设计\n阅读论文与深度思考\n构建长期主义知识体系\n✨ 生活与思考 崇尚自然与秩序\n记录摄影与旅行\n追求克制与审美\n🚀 建站历程 起于一念微光，行于无数深夜。\n网站不过载体，成长才是本意。\n","permalink":"https://ktzxy.top/about/","summary":"\u003ch2 id=\"-个人简介\"\u003e👤 个人简介\u003c/h2\u003e\n\u003cp\u003e不逐浮名，不争朝夕。\u003cbr\u003e\n于纷繁之中求秩序，于变化之中守原则。\u003cbr\u003e\n以工程为术，以思考为道。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"-关于本站\"\u003e🌿 关于本站\u003c/h2\u003e\n\u003cp\u003e世事翻涌，潮起潮落。\u003cbr\u003e\n有人逐浪，有人随波。\u003cbr\u003e\n我愿在变化之间，守一份秩序与笃定。\u003cbr\u003e\n技术为舟，时间为海。\u003cbr\u003e\n行远，亦行深。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"-技术与探索\"\u003e💻 技术与探索\u003c/h2\u003e\n\u003cp\u003e编程 · 算法 · 系统设计\u003cbr\u003e\n阅读论文与深度思考\u003cbr\u003e\n构建长期主义知识体系\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"-生活与思考\"\u003e✨ 生活与思考\u003c/h2\u003e\n\u003cp\u003e崇尚自然与秩序\u003cbr\u003e\n记录摄影与旅行\u003cbr\u003e\n追求克制与审美\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"-建站历程\"\u003e🚀 建站历程\u003c/h2\u003e\n\u003cp\u003e起于一念微光，行于无数深夜。\u003cbr\u003e\n网站不过载体，成长才是本意。\u003c/p\u003e","title":"About"}]