-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 233 KB
/
content.json
1
{"meta":{"title":"Zhiyang'blog","subtitle":"be yourself, be better","description":"本网站是个人兴趣爱好,总结分享经验,记录生活点滴的平台","author":"不烦","url":"https://codey.cc","root":"/"},"pages":[{"title":"","date":"2023-05-01T09:30:49.516Z","updated":"2023-05-01T09:30:49.516Z","comments":false,"path":"404.html","permalink":"https://codey.cc/404.html","excerpt":"","text":"404 你访问的页面被外星人叼走了 :("},{"title":"所有分类","date":"2023-04-28T17:00:33.894Z","updated":"2023-04-28T17:00:33.894Z","comments":true,"path":"categories/index.html","permalink":"https://codey.cc/categories/index.html","excerpt":"","text":""},{"title":"","date":"2023-04-26T15:43:32.106Z","updated":"2023-04-25T16:45:24.971Z","comments":true,"path":"bookmark/index.html","permalink":"https://codey.cc/bookmark/index.html","excerpt":"","text":"我是不烦,一名Java开发者,技术一般,经历平平,但是也一直渴望进步,同时也努力活着,为了人生不留遗憾,也希望能够一直做着自己喜欢的事情,得闲时分享心得、分享一些浅薄的经验。 🌟技能语言:Java 😴梦想那必须是成为一名技术大佬!(梦想还是要有的,没有梦想,何必远方) ✌ConnectEmail:codeycc@163.com Wechat:UUID-ONE"},{"title":"","date":"2023-05-30T15:00:22.321Z","updated":"2023-05-30T15:00:22.321Z","comments":false,"path":"about/index.html","permalink":"https://codey.cc/about/index.html","excerpt":"","text":"About Me 📃 关于我 & 我的博客 关于 摄影 代办 like 简介 IT攻城狮 👨💻 业余拍照爱好者 📷 喜欢旅行 ✈️ 爱好美食 🍽️ 网瘾重度者 🎮 沉迷代码 💻 : [email protected] : UUID-ONE 喜欢我的博客可以给我点赞/Star哦 也可以按快捷键 command+D 或者 ctrl+D 添加网站到浏览器书签 我的GitHub 👨💻 技术栈 💻 编程语言: Java、Python 框架: Springboot、SpringCloud 数据库: Mysql、Redis 站点历程 🗓️ 2023-04-26 解析域名 将博客部署到GitHub Pages,并解析到自定义域名 2023-04-25 迁移到Hexo 将博客从 Perfree 转移到 Hexo 并使用 volantis 作为本站主题 2022-07-01 购买域名 在 腾讯云 购买域名codey.cc并解析到腾讯服务器ip地址 2022-06-12 开始建站 在腾讯服务器上建站,使用 Perfree 博客引擎 使用CDN 尽可能每日一篇 Like tab is in buiding… To Be A Greater Person…"},{"title":"","date":"2023-05-30T11:22:43.073Z","updated":"2023-05-30T11:22:43.073Z","comments":false,"path":"friends/index.html","permalink":"https://codey.cc/friends/index.html","excerpt":"","text":"友链🧑🤝🧑 friends & dalao 博主 十一 好学博客 小码同学 小孙同学 大佬 张洪 杜老师说 Akilarの糖果屋"},{"title":"","date":"2023-04-30T17:29:30.499Z","updated":"2023-04-30T17:29:30.499Z","comments":true,"path":"messages/index.html","permalink":"https://codey.cc/messages/index.html","excerpt":"","text":"留言板📘 Comments"},{"title":"电影推荐","date":"2021-08-25T11:56:04.000Z","updated":"2023-04-26T14:13:48.821Z","comments":true,"path":"movies/index.html","permalink":"https://codey.cc/movies/index.html","excerpt":"","text":"精彩电影推荐灌篮高手灌篮高手2023,井上雄彦124分钟动画宫城良田、三井寿、流川枫、樱木花道和赤木刚宪终于站在全国大赛的赛场,代表湘北高中与日本最强球队山王工业展开激烈对决。面对强大的对手,湘北五人组没有退缩,在安西教练的指导下,他们抱着破釜沉舟的决心热血奋战,究竟湘北能否取得比赛的最终胜利? 泰坦尼克号泰坦尼克号2023,詹姆斯·卡梅隆194分钟爱情,剧情1912年4月10日,号称 “世界工业史上的奇迹”的豪华客轮泰坦尼克号开始了自己的处女航,从英国的南安普顿出发驶往美国纽约。17岁头等舱乘客露丝(凯特·温丝莱特 饰演)与她当时的未婚夫卡尔(比利·赞恩 饰演)、露丝的母亲(弗兰西丝·费舍 饰演)、卡尔以及露丝的仆人登上了泰坦尼克号;另外还有在赌博中赢得船票的三等舱乘客杰克·道森(莱昂纳多·迪卡普里奥)和他的朋友法布里奇欧(丹尼·努齐 饰演)。露丝厌倦了上流社会虚伪的生活,不愿嫁给卡尔,打算投海自尽,被杰克救起。很快,美丽活泼的露丝与英俊开朗的杰克相爱,杰克带露丝参加下等舱的舞会、为她画像,二人的感情逐渐升温。 卡尔发现了杰克为露丝所绘的人体素描与草图,与“海洋之心”一起放于他的保险箱内,他很愤怒,指示仆人对杰克插赃嫁祸。然而就在同时,“泰坦尼克号”撞上了冰山。泰坦尼克号的设计师托马斯·安德鲁斯向船员表示,由于有5个舱入水,所以泰坦尼克号将会在1-2小时内沉没。当露丝和卡尔准备登上救生艇的时候,露丝决定去寻找杰克,结果在警卫室找到他,他被手铐锁在一条水管上,萝丝找不到能开启手铐的钥匙,几经辛苦才发现找到一把消防斧,她用斧头劈断手铐后与杰克一起逃生。泰坦尼克号上一片混乱,在危急之中,人类本性中的善良与丑恶、高贵与卑劣更加分明。 很快,巨轮从中间断裂,船尾成竖直90度角下城,很多乘客因而从高处跌下,杰克和萝丝互相紧抓对方,但因为水的冲力太强而松开了。杰克后来找到萝丝,他们发现了一扇漂浮在海上的门板,但是门板上只能够容纳一人的重量,因此杰克把生存的机会让给了爱人罗丝,自己则在冰海中被冻死。露丝悲痛欲绝,但她答应了杰克要好好活下去,最终她被人救起,之后她过着平静的生活,与一名低下阶层的姓卡维特的男子结婚,婚后生儿育女。直到73岁之后,“泰坦尼克号”的沉船遗骸在北大西洋两英里半的海底被发现,美国探险家洛维特在船上发现了一幅画,这幅画中的少女正是当年的露丝,这段尘封的往事才被解开。在把拥有多年,外界一直寻找不到的项链“海洋之心”扔下海中,年迈的露丝终于向众人诉说了埋藏在自己心中84年的秘密,了却她的心事,在此之前她从未曾把自己这段刻骨铭心的往事跟任何人提及过,包括她已过世的丈夫。 流浪地球2流浪地球22023,郭帆173分钟科幻,冒险,灾难太阳即将毁灭,人类在地球表面建造出巨大的推进器,寻找新的家园。然而宇宙之路危机四伏,为了拯救地球,流浪地球时代的年轻人再次挺身而出,展开争分夺秒的生死之战。","author":"不烦"},{"title":"快捷导航","date":"2021-08-29T08:25:05.000Z","updated":"2023-04-26T13:52:57.399Z","comments":true,"path":"navigate/index.html","permalink":"https://codey.cc/navigate/index.html","excerpt":"","text":""},{"title":"","date":"2023-04-25T16:41:22.454Z","updated":"2023-04-25T16:41:22.454Z","comments":true,"path":"mylist/index.html","permalink":"https://codey.cc/mylist/index.html","excerpt":"","text":""},{"title":"所有标签","date":"2023-04-28T17:00:06.031Z","updated":"2023-04-28T17:00:06.031Z","comments":true,"path":"tags/index.html","permalink":"https://codey.cc/tags/index.html","excerpt":"","text":"没有你感兴趣的???🤔 立即发送邮箱告诉我吧😜"},{"title":"","date":"2023-05-14T11:11:45.245Z","updated":"2023-05-14T11:11:45.245Z","comments":true,"path":"archives/index.html","permalink":"https://codey.cc/archives/index.html","excerpt":"","text":""},{"title":"","date":"2023-05-29T13:29:38.000Z","updated":"2023-05-30T11:15:17.133Z","comments":false,"path":"fcircle/index.html","permalink":"https://codey.cc/fcircle/index.html","excerpt":"","text":"朋友圈 let UserConfig = { // 填写你的api地址 private_api_url: 'https://s.codey.cc', // 初始加载几篇文章 page_init_number: 20, // 点击加载更多时,一次最多加载几篇文章,默认10 page_turning_number: 10, // 头像加载失败时,默认头像地址 error_img: 'https://sdn.geekzu.org/avatar/57d8260dfb55501c37dde588e7c3852c', // 进入页面时第一次的排序规则 sort_rule: 'created', // 本地文章缓存数据过期时间(天) expire_days: 1, }"}],"posts":[{"title":"Mysql索引优化20招","slug":"Mysql索引优化20招","date":"2023-07-08T09:00:03.000Z","updated":"2023-07-08T09:40:35.350Z","comments":true,"path":"p/52870188c2ad/","link":"","permalink":"https://codey.cc/p/52870188c2ad/","excerpt":"","text":"索引的相信大家都听说过,但是真正会用的又有几人?平时工作中写SQL真的会考虑到这条SQL如何能够用上索引,如何能够提升执行效率? 当涉及到MySQL索引优化时,以下是20个常用的技巧和策略,每个技巧都结合了正例和反例的分析,并提供了具体的SQL语句示例。 选择合适的数据类型: 正例:将身高字段选择为FLOAT类型而不是DOUBLE类型,以减小存储空间。CREATE TABLE person ( id INT PRIMARY KEY, name VARCHAR(50), height FLOAT ); 反例:将性别字段选择为VARCHAR(100)类型,浪费了存储空间。CREATE TABLE person ( id INT PRIMARY KEY, name VARCHAR(50), gender VARCHAR(100) ); 选择适当的索引类型: 正例:对文本内容进行全文搜索时,使用全文索引而不是B-tree索引。CREATE FULLTEXT INDEX idx_content ON articles (content); 反例:对于较小的表,使用哈希索引而不是B-tree索引,可能会导致性能下降。CREATE HASH INDEX idx_id ON small_table (id); 为常用查询创建索引: 正例:对于经常查询的user表的username列,创建索引以提高查询性能。CREATE INDEX idx_username ON user (username); 反例:对于很少被查询的列创建索引,增加了索引维护的开销。CREATE INDEX idx_last_login ON user (last_login); 避免过多索引: 正例:只为经常查询和需要排序的列创建索引,避免过多的索引。CREATE INDEX idx_name ON employees (name); CREATE INDEX idx_salary ON employees (salary); 反例:为每个列都创建索引,导致写操作的性能下降。CREATE INDEX idx_name ON employees (name); CREATE INDEX idx_salary ON employees (salary); CREATE INDEX idx_age ON employees (age); 使用复合索引: 正例:为orders表的customer_id和order_date列创建复合索引,以支持多列查询。CREATE INDEX idx_customer_order ON orders (customer_id, order_date); 反例:为单个列创建多个单列索引,增加了索引的冗余。CREATE INDEX idx_customer_id ON orders (customer_id); CREATE INDEX idx_order_date ON orders (order_date); 避免在索引列上使用函数: 正例:在查询条件中避免使用函数,如WHERE date_column = '2022-01-01'。SELECT * FROM orders WHERE order_date = '2022-01-01'; 反例:在查询条件中使用函数,如WHERE YEAR(date_column) = 2022,导致索引失效。SELECT * FROM orders WHERE YEAR(order_date) = 2022; 使用覆盖索引: 正例:为orders表的order_id和total_amount列创建复合索引,以支持覆盖查询。CREATE INDEX idx_order_total ON orders (order_id, total_amount); SELECT order_id, total_amount FROM orders WHERE order_date = '2022-01-01'; 反例:没有合适的覆盖索引,导致查询时需要访问数据行。SELECT * FROM orders WHERE order_date = '2022-01-01'; 定期分析表和索引: 正例:使用ANALYZE TABLE命令来更新表和索引的统计信息,以优化查询计划。ANALYZE TABLE orders; 反例:长时间没有更新统计信息,导致查询计划不准确。SELECT * FROM orders WHERE order_date = '2022-01-01'; 避免使用通配符开头的模糊查询: 正例:使用LIKE 'value%'来进行模糊查询,以利用索引。SELECT * FROM products WHERE name LIKE 'apple%'; 反例:使用LIKE '%value'来进行模糊查询,导致索引失效。SELECT * FROM products WHERE name LIKE '%apple'; 使用连接查询优化: 正例:使用INNER JOIN而不是CROSS JOIN,以减少查询的数据集大小。SELECT * FROM orders INNER JOIN customers ON orders.customer_id = customers.id; 反例:使用CROSS JOIN导致结果集过大,影响查询性能。SELECT * FROM orders CROSS JOIN customers; 避免全表扫描: 正例:确保查询语句能够使用索引,避免全表扫描。SELECT * FROM products WHERE category = 'Electronics'; 反例:没有合适的索引,导致全表扫描,影响性能。SELECT * FROM products WHERE price > 1000; 合理设置索引的顺序: 正例:对于复合索引,将选择性高的列放在前面,以提高查询性能。CREATE INDEX idx_last_name_first_name ON employees (last_name, first_name); 反例:对于复合索引,将选择性低的列放在前面,导致索引失效。CREATE INDEX idx_first_name_last_name ON employees (first_name, last_name); 注意索引列的顺序: 正例:在查询条件中按照索引的列顺序使用列,以使索引能够有效利用。SELECT * FROM orders WHERE customer_id = 123 AND order_date = '2022-01-01'; 反例:在查询条件中颠倒索引的列顺序,导致索引失效。SELECT * FROM orders WHERE order_date = '2022-01-01' AND customer_id = 123; 避免使用SELECT 查询: 正例:只选择需要的列,减少数据传输和存储开销。SELECT order_id, order_date, total_amount FROM orders WHERE customer_id = 123; 反例:使用SELECT *查询所有列,增加了数据传输和存储开销。SELECT * FROM orders WHERE customer_id = 123; 使用LIMIT限制结果集: 正例:对于只需要部分结果的查询,使用LIMIT来限制返回的行数。SELECT * FROM products ORDER BY price DESC LIMIT 10; 反例:没有使用LIMIT限制结果集,导致返回大量数据,影响性能。SELECT * FROM products ORDER BY price DESC; 避免使用ORDER BY RAND(): 正例:使用其他方法来获取随机行,而不是使用ORDER BY RAND()。SELECT * FROM products ORDER BY RAND() LIMIT 10; 反例:使用ORDER BY RAND()来获取随机行,性能较差。SELECT * FROM products ORDER BY RAND(); 使用覆盖索引进行排序: 正例:为经常需要排序的列创建索引,以避免使用临时表进行排序。CREATE INDEX idx_price ON products (price); SELECT price FROM products ORDER BY price DESC; 反例:没有合适的索引,导致使用临时表进行排序,性能下降。SELECT price FROM products ORDER BY price DESC; 注意NULL值的处理: 正例:对于允许NULL值的列,使用IS NULL或IS NOT NULL进行查询。SELECT * FROM customers WHERE email IS NULL; 反例:对于允许NULL值的列,使用=或<>进行查询,导致索引失效。SELECT * FROM customers WHERE email = NULL; 定期清理无效的索引: 正例:定期检查和删除无效的索引,以减少索引维护的开销。DROP INDEX idx_unused ON table_name; 反例:保留无效的索引,增加了索引维护的开销。CREATE INDEX idx_unused ON table_name (column_name); 定期优化表结构: 正例:定期评估和优化表结构,包括删除不再使用的列和表,减少存储空间和提高性能。ALTER TABLE table_name DROP COLUMN column_name; 反例:保留不再使用的列和表,浪费了存储空间和资源。CREATE TABLE unused_table (id INT, name VARCHAR(50));","categories":[{"name":"mysql","slug":"mysql","permalink":"https://codey.cc/categories/mysql/"}],"tags":[{"name":"mysql","slug":"mysql","permalink":"https://codey.cc/tags/mysql/"}],"author":"不烦"},{"title":"Commit消息书写规范","slug":"自用 Commit 消息书写规范","date":"2023-05-28T13:41:59.000Z","updated":"2023-05-29T15:03:01.115Z","comments":true,"path":"p/4132844b2efe/","link":"","permalink":"https://codey.cc/p/4132844b2efe/","excerpt":"","text":"自用 Commit 消息书写规范Commit 消息 —— 也就是 Commit Message —— 在使用 Git 作为版本管理工具的时候对它绝对不陌生。一般来说,commit消息清晰明了,可以说明本次提交的目的,具体做了什么操作,在日后回顾的时候非常有用,Git 也强制每次提交都要提供消息。 但是在日常开发特别是小组作业中,大家的提交消息千奇百怪也十分正常;你觉得你的提交消息已经够抽象了,但是总能找到比你更抽象的;中英文混合使用、fixbug这种说了和没说一样的笼统的消息也层出不穷,这就导致后续代码维护成本特别大,比如这次维修博客观看之前的提交历史,只能说我都不知道我修了个啥==乱写 Commit 一时爽,后续维护火葬场。 其实也不能说是一时爽:很多时候也不能说是我们有意要瞎写提交消息的,有的时候这次 Commit确实做的事情小到不拿放大镜看不到,为了这点东西想一段日后看起来不会一头问号的消息其实还挺尴尬的;这种时候就需要完善的规则来背书,才能避免这种前后都尴尬的情况。 规范其实网上的规范还挺多,也有约定式提交这种看起来就很高大上的东西;但是实际上大多数人说到规范的第一反应还得是 Angular 规范;这里简单的讲讲: 自用 Angular 规范总的来说,一条消息应该由以下的部分组成 <type>(<scope>): <subject> <description> type(必须)用于说明本次提交的类别,可以在以下关键字中选择: 类别 含义 说明 feat/feature 新的功能 fix 修复 bug,已经解决了描述的问题 to 修复 bug,问题还没解决 适用于多次提交;这个 bug 我修了好久了 docs 修改了文档 style 调整了代码的格式,完全不影响代码的运行 refactor 对已有代码的重构 一个更改,即没有增加新功能,也没有修复 bug,那它就是重构 perf 优化代码的表现,提升了性能/体验 test 增加测试 chore 构建过程/辅助工具的变动 revert 版本回滚 merge 代码合并 sync 同步其他分支 其实还是挺抽象的…… 别的之后慢慢补充了…… scope(可选)用于说明本次提交影响的范围;可以具体到功能模块,这就取决于不同的项目了。 subject(必须)对本次提交进行简短的描述,往往不超过 50 个字符 —— 你超过了的话,Idea 都会提醒你。 虽然说很多文章里都说要用英文,但是对于本就英文不熟悉的国人来说实在是有点要求过高……个人感觉如果只是自己的项目,自己写的三脚猫英语可能本身就是代码审阅时候的很大的障碍,所以要不还是不要折磨自己,乖乖说母语吧 description*(可选)另起一行之后的 Commit Message 就不会出现在各大托管平台的提交历史页面了;而很多时候五十个字符也只能非常 High-level的概括一下你做了什么—— 虽然说在正经的多人合作项目里不太可能出现这种事情,但是毕竟自己也会搞七搞八对嘛==这种情况下就需要更加详细的说明。当然,我的的意思就是另起一行开始大段说明。 约定式提交 感觉区别不是很大,可以参见它们的网站。约定式提交 1.0 好处除了方便代码审阅之外,还有一些角度可以理解这样的好: 倒逼每次改动都专注于一个事情,使得最终的代码修改历史更为清晰当然,在一个人的项目里很多时候这种属于是给自己增加麻烦。 格式化的 Commit Message 可以用于自动化生成 CHANGELOG确实,怎么会有人手写 ChangeLog 呢? 参考资料 https://zhuanlan.zhihu.com/p/182553920 约定式提交 1.0","categories":[{"name":"java","slug":"java","permalink":"https://codey.cc/categories/java/"}],"tags":[{"name":"java","slug":"java","permalink":"https://codey.cc/tags/java/"}],"author":"不烦"},{"title":"三分钟搭建一个ChatGPT网页服务","slug":"ChatGPT 网页服务搭建","date":"2023-05-23T13:08:58.000Z","updated":"2023-05-29T15:03:01.118Z","comments":true,"path":"p/db216b4516c5/","link":"","permalink":"https://codey.cc/p/db216b4516c5/","excerpt":"","text":"使用 ChatGPT Next Web 搭建自己的 ChatGPT 网页服务 注册账号 准备一个国外的手机号,https://sms-activate.org/cn/getNumber 清除浏览器 cookie,VPN 使用国外节点(非香港台湾澳门) 访问 https://chat.openai.com/chat 进行注册 部署项目 获取 OpenAI API Key:https://platform.openai.com/account/api-keys 在开源项目中 https://github.com/Yidadaa/ChatGPT-Next-Web 的 README 中点击Deploy 进入 vercel 页面后,使用 Github 账户登录并验证手机号 在 Configure Project 中,输入步骤 1 中的 API Key,在 CODE 框中输入密码。多个密码用”;” 分隔 查看效果 进入配置页面修改为自己的域名 配置 DNS 解析,设置 CNAME 为 cname.vercel-dns.com","categories":[{"name":"ChatGPT","slug":"ChatGPT","permalink":"https://codey.cc/categories/ChatGPT/"}],"tags":[{"name":"ChatGPT","slug":"ChatGPT","permalink":"https://codey.cc/tags/ChatGPT/"}],"author":"不烦"},{"title":"内网穿透我用frp","slug":"frp实现内网穿透","date":"2023-05-14T06:33:01.000Z","updated":"2023-05-14T06:46:11.521Z","comments":true,"path":"p/66d2dc06dddc/","link":"","permalink":"https://codey.cc/p/66d2dc06dddc/","excerpt":"","text":"frp 是一个开源、简洁易用、高性能的内网穿透和反向代理软件,支持 tcp, udp, http, https等协议。 frp工作原理: 服务端运行,监听一个主端口,等待客户端的连接; 客户端连接到服务端的主端口,同时告诉服务端要监听的端口和转发类型; 服务端fork新的进程监听客户端指定的端口; 外网用户连接到客户端指定的端口,服务端通过和客户端的连接将数据转发到客户端; 客户端进程再将数据转发到本地服务,从而实现内网对外暴露服务的能力。 1、服务端(Linux)搭建1.1、下载服务端的frps:使用wget命令下载。如果wget command not found,则先安装wget,安装命令如下: yum -y install wget 下载frp到服务器,在 https://github.com/fatedier/frp/releases 这里可以查看最新版本和获取下载地址。下载命令: wget https://github.com/fatedier/frp/releases/download/v0.34.0/frp_0.34.0_linux_amd64.tar.gz 或者直接使用压缩包,这里使用0.34版本(必须保证服务端(frps)和客户端(frpc)版本一致) 1.2、解压tar -zxvf frp_0.34.0_linux_amd64.tar.gz 1.3、修改frps.ini文件:[common] # binde_addr是指定frp内网穿透服务器端监听的IP地址,默认为127.0.0.1, #如果使用IPv6地址的话,必须用方括号包括起来,比如 “[::1]:80”, “[ipv6-host]:http” or “[ipv6-host%zone]:80” #bind_addr = 0.0.0.0 #与客户端绑定的进行通信的端口 bind_port = 7000 #默认7000,如果使用阿里云或者腾讯云服务器注意把端口打开 systemctl status firewalld #http的访问端口 vhost_http_port = 6781 #https的访问端口(如果需要的话) vhost_https_port = 6782 dashboard_user = admin #管理面板账号 dashboard_pwd = admin #管理面板密码 # 这个是frp内网穿透服务器的web界面的端口,可以通过http://你的ip:7500查看frp内网穿透服务器端的连接情况,和各个frp内网穿透客户端的连接情况。 dashboard_port = 7500 auth_token = asjdgjwye1213werhjr738 # 日志输出文件路径地址 可修改位置 这里默认当前路径 log_file = ./frps.log # 日志等级 默认info 如果觉得日志过多可以调整日志级别 log_level = info # 日志文件保留天数 log_max_days = 3 # 只允许客户端绑定端口 格式为 1000-2000,2001,3000-4000 可以根据自己需求修改 #allow_ports = 2000-3000,3001,3003,4000-50000 # 自定义 404 错误页面地址 # custom_404_page = /path/to/404.html #[配置拆分] 通过 includes 参数可以在主配置中包含其他配置文件,从而实现将代理配置拆分到多个文件中管理 # includes = ./confd/*.ini [web01] #http类型的内网穿透,必须设置vhost_http_port,并且所有的http类型的客户端都将通过同一个vhost_http_port访问。 type = http # custom_domains是通过frp服务器端访问客户端的域名,必须输入完整的域名, # 并且不能是subdomain_host的子域名,否则frp服务不能启动,并且相应的域名需要解析到frp服务器端的公网IP并等待解析生效后,才可以使用。 custom_domains = www.zzzzy.cn #公网访问域名 这里说明一下:frpc相关的是客户端文件 1.4、启动#后台启动 nohup ./frps -c frps.ini & > frpsinfo.log 2、客户端搭建2.1、重复以上步骤2.2、修改frpc.ini文件[common] #服务器地址 server_addr = xx.xx.xx.xx #与服务端绑定的进行通信的端口bind_prot server_port = 7000 [demo] #随便命名id但不能重复 type = tcp local_ip = 127.0.0.1 local_port = 3389 remote_port = 3389 #远程服务器端口(防火墙需打开此端口) #use_compression = true #使用压缩 #use_encryption = true #使用加密 [web01] type = http #http类型的内网穿透,必须设置vhost_http_port, #并且所有的http类型的客户端都将通过同一个vhost_http_port访问。 local_port = 8089 remote_port = 8089 #远程服务器端口(防火墙需打开此端口) #域名必须要有,并解析到你的服务器地址 custom_domains = www.zzzzy.cn #公网访问域名 2.3、启动./frpc.exe -c frpc.ini","categories":[{"name":"java","slug":"java","permalink":"https://codey.cc/categories/java/"}],"tags":[{"name":"java","slug":"java","permalink":"https://codey.cc/tags/java/"}],"author":"不烦"},{"title":"mysql主从复制之docker","slug":"mysql主从复制之docker","date":"2023-05-14T05:48:01.000Z","updated":"2023-05-16T13:50:56.978Z","comments":true,"path":"p/c11a99d2c881/","link":"","permalink":"https://codey.cc/p/c11a99d2c881/","excerpt":"","text":"一、原理 mysql支持哪些复制: 基于语句的复制:在主服务器上执行的sql语句,在从服务器上执行同样的语句。mysql默认采用基于语句的复制,效率边角高。一旦发现没法精确复制时,会自动选着基于行的复制。 基于行的复制:把改变的内容复制过去,而不是把命令在从服务器上执行一遍。(从mysql 5.0开始支持) 混合类型的复制:默认采用基于语句的复制,一旦发现基于语句的无法精确复制时,就会采用基于行的复制。(常用) mysql复制解决的问题: 数据分布(data distribution) 负载平衡(load balancing) 数据备份(backup),保证数据安全 高可用性与容错行(high availability and failover) 实现读写分离,缓解数据库压力 mysql主从复制原理: master服务器将数据的改变记录在二进制Binlog日志,当master上的数据发生改变时,则将其改变写入二进制日志中;slave服务器会在一定时间间隔内master二进制日志进行探测其是否发生改变, 如果发生改变,则开始一个I/O Thread请求master二进制事件,同时主节点为每个I/O线程启动一个dump线程,用于向其发送二进制事件,并保存至从节点本地的中继日志Relaylog中,从节点将启动SQL线程从中继日志中读取二进制日志,使得其数据和主节点的保持一致,最后I/O Thread和SQL Thread将进入睡眠状态,等待下一次被唤醒。 注意几点: 1.master将操作语句记录到binlog日志中,然后授予slave远程连接的权限(master一定要开启binlog二进制日志功能;通常为了数据安全考虑,slave也开启binlog功能) 2.slave开启两个线程:IO线程和SQL线程。其中:IO线程负责读取master的binlog内容到中继日志relay log里;SQL线程负责从relay log日志里读出binlog内容,并更新到slave的数据库里,这样就能保证slave数据和 master数据保持一致了 3.Mysql复制至少需要两个Mysql的服务,当然Mysql服务可以分布在不同的服务器上,也可以在一台服务器上启动多个服务 4.Mysql复制最好确保master和slave服务器上的Mysql版本相同(如果不能满足版本一致,那么要保证master主节点的版本低于slave从节点的版本) 5.master和slave两节点的时间需同步 Mysql复制的流程图如下: 二、实战(docker) 新建主服务器容器实例 docker run -p 3307:3306 --name mysql-master --privileged=true \\ -v /usr/mydata/mysql-master/log:/var/log/mysql \\ -v /usr/mydata/mysql-master/data:/var/lib/mysql \\ -v /usr/mydata/mysql-master/conf:/etc/mysql/ \\ -e MYSQL_ROOT_PASSWORD=mysql123 \\ -e LANG=C.UTF-8 -d mysql:5.7.36 主实例my.cnf [mysqld] ## 设置server_id,同一局域网中需要唯一 server_id=101 ## 指定不需要同步的数据库名称 binlog-ignore-db=mysql ## 开启二进制日志功能 log-bin=mall-mysql-bin ## 设置二进制日志使用内存大小(事务) binlog_cache_size=1M ## 设置使用的二进制日志格式(mixed,statement,row) binlog_format=mixed ## 二进制日志过期清理时间。默认值为0,表示不自动清理。 expire_logs_days=7 ## 跳过主从复制中遇到的所有错误或指定类型的错误,避免slave端复制中断。 ## 如:1062错误是指一些主键重复,1032错误是因为主从数据库数据不一致 slave_skip_errors=1062 修改后重启主实例 进入mysql-master容器 docker exec -it mysql-master /bin/bash mysql -uroot -proot ## master容器实例内创建数据同步用户 CREATE USER 'slave'@'%' IDENTIFIED BY '123456'; GRANT REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'slave'@'%'; 新建从服务器容器实例3308 docker run -p 3308:3306 --name mysql-slave --privileged=true \\ -v /usr/mydata/mysql-slave/log:/var/log/mysql \\ -v /usr/mydata/mysql-slave/data:/var/lib/mysql \\ -v /usr/mydata/mysql-slave/conf:/etc/mysql/ \\ -e MYSQL_ROOT_PASSWORD=mysql123 \\ -e LANG=C.UTF-8 -d mysql:5.7.36 主实例my.cnf [mysqld] ## 设置server_id,同一局域网中需要唯一 server_id=102 ## 指定不需要同步的数据库名称 binlog-ignore-db=mysql ## 开启二进制日志功能,以备Slave作为其它数据库实例的Master时使用 log-bin=mall-mysql-slave1-bin ## 设置二进制日志使用内存大小(事务) binlog_cache_size=1M ## 设置使用的二进制日志格式(mixed,statement,row) binlog_format=mixed ## 二进制日志过期清理时间。默认值为0,表示不自动清理。 expire_logs_days=7 ## 跳过主从复制中遇到的所有错误或指定类型的错误,避免slave端复制中断。 ## 如:1062错误是指一些主键重复,1032错误是因为主从数据库数据不一致 slave_skip_errors=1062 ## relay_log配置中继日志 relay_log=mall-mysql-relay-bin ## log_slave_updates表示slave将复制事件写进自己的二进制日志 log_slave_updates=1 ## slave设置为只读(具有super权限的用户除外) read_only=1 在主数据库中查看主从同步状态 show master status; 进入mysql-slave容器 docker exec -it mysql-slave /bin/bash 在从数据库中配置主从复制 执行以下命令: change master to master_host='宿主机ip', master_user='slave', master_password='123456', master_port=3307, master_log_file='mall-mysql-bin.000001', master_log_pos=617, master_connect_retry=30; 各个参数释义: master_host:主数据库的IP地址; master_port:主数据库的运行端口; master_user:在主数据库创建的用于同步数据的用户账号; master_password:在主数据库创建的用于同步数据的用户密码; master_log_file:指定从数据库要复制数据的日志文件,通过查看主数据的状态,获取File参数; master_log_pos:指定从数据库从哪个位置开始复制数据,通过查看主数据的状态,获取Position参数; master_connect_retry:连接失败重试的时间间隔,单位为秒。 在从数据库中查看主从同步状态 show slave status \\G;(只能在DOS窗口使用) 在从数据库开启主从同步start slave 测试建库建表插入数据 三、issues1.’Could not find first log file name in binary log index file’的解决办法 数据库主从出错: Slave_IO_Running: No 一方面原因是因为网络通信的问题也有可能是日志读取错误的问题。以下是日志出错问题的解决方案: 解决方法: slave机器停止slave: mysql> stop slave; master机器登陆mysql,并记录master的bin的位置: mysql> show master status; mysql> flush logs; #刷新日志 slave执行 mysql> CHANGE MASTER TO MASTER_LOG_FILE='mall-mysql-bin.000003',MASTER_LOG_POS=1078; mysql> start slave; mysql> SHOW SLAVE STATUS\\G","categories":[{"name":"docker","slug":"docker","permalink":"https://codey.cc/categories/docker/"}],"tags":[{"name":"docker","slug":"docker","permalink":"https://codey.cc/tags/docker/"}],"author":"不烦"},{"title":"jenkins浅析","slug":"jenkins浅析","date":"2023-05-13T14:04:41.000Z","updated":"2023-05-14T10:32:42.263Z","comments":true,"path":"p/5134766ba3d1/","link":"","permalink":"https://codey.cc/p/5134766ba3d1/","excerpt":"","text":"一、背景安装在实际开发中,我们经常要一边开发一边测试,当然这里说的测试并不是程序员对自己代码的单元测试,而是同组程序员将代码提交后,由测试人员测试; 或者前后端分离后,经常会修改接口,然后重新部署; 这些情况都会涉及到频繁的打包部署; 手动打包常规步骤: 1.提交代码 2.问一下同组小伙伴有没有要提交的代码 3.拉取代码并打包(war包,或者jar包) 4.上传到Linux服务器 5.查看当前程序是否在运行 6.关闭当前程序 7.启动新的jar包 8.观察日志看是否启动成功 9.如果有同事说,自己还有代码没有提交……再次重复1到8的步骤!!!!!(一上午没了) 那么,有一种工具能够实现,将代码提交到git后就自动打包部署勒,答案是肯定的:Jenkins 当然除了Jenkins以外,也还有其他的工具可以实现自动化部署,如Hudson等 只是Jenkins相对来说,使用得更广泛。 1.简介 Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开放易用的软件平台,使软件的持续集成变成可能。参考链接 Jenkins自动化部署实现原理 部署环境: 1.jdk环境,Jenkins是java语言开发的,因需要jdk环境。 2.git/svn客户端,因一般代码是放在git/svn服务器上的,我们需要拉取代码。 3.maven客户端,因一般java程序是由maven工程,需要maven打包,当然也有其他打包方式,如:gradle。 2.检查卸载cd /root #卸载之前残留的jenkins rpm -e jenkins find / -iname jenkins | xargs -n 1000 rm -rf #查看是否卸载完毕 rpm -ql jenkins 3.安装 jenkins默认安装的配置目录在:/etc/sysconfig/jenkins sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo --no-check-certificate sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key --no-check-certificate yum -y install epel-release yum -y install jenkins 4.配置#安装完毕,进入到jenkins配置文件内,配置端口及用户名 vim /etc/sysconfig/jenkins #找到这两行,修改成指定的端口 JENKINS_USER="用户名" #示例: root JENKINS_PORT="端口号" #示例: 9999 #启动jenkins服务 systemctl start jenkins #查看启动状态 systemctl status jenkins #如果报错 Starting Jenkins File "/usr/bin/java" is not executable. #查看当前Java的环境变量 echo $JAVA_HOME vim /etc/init.d/jenkins #在/usr/bin/java下添加 /usr/java/jdk1.8.0_181/bin/java #/usr/java/jdk1.8.0_181是Java的环境变量 systemctl daemon-reload systemctl start jenkins systemctl status jenkins 5.通用平台配置#首次登陆查找密码 cat /var/lib/jenkins/secrets/initialAdminPassword #选择安装推荐插件 #创建管理员账号 全选root #Jenkins URL: http://192.168.56.10:9999/ #点击高级升级站点 https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json #重启 systemctl restart jenkins #安装汉化插件 Jenkins->Manage Jenkins->Manage Plugins,点击Available,搜索”Chinese” #安装Credentials Binding插件 #安装Pipeline插件 #配置jdk和maven分别找到对应的环境变量地址 echo $JAVA_HOME echo $MAVEN_HOME 在全局工具配置配置Jenkins关联环境变量和设置常量 JAVA_HOME MAVEN_HOME GIT 安装gitee插件并配置 二、实战 General(基础配置)–>源码管理–>构建触发器–>构建环境–>构建–>构建后操作 1.创建工程 2.基础配置 当提交代码时会触发Jenkins自动构建部署 三、复盘 使用Maven Integration插件构建项目,只有首次会打包,之后打包需要手动删除项目空间,即使我已经在项目配置构建前删除工作空间还是不行 构建时需要添加BUILD_ID=DONTKILLME(不要杀我)参数,否则项目启动立即被杀死 打包 第一种:可以在项目设置时调用顶层Maven目标打包 第二种:可以在调用的脚本自定义打包","categories":[{"name":"java","slug":"java","permalink":"https://codey.cc/categories/java/"}],"tags":[{"name":"java","slug":"java","permalink":"https://codey.cc/tags/java/"}],"author":"不烦"},{"title":"Spring的事件机制真香!","slug":"spring事件监听机制","date":"2023-05-13T13:24:47.000Z","updated":"2023-05-13T13:47:04.107Z","comments":true,"path":"p/368c8b7cab21/","link":"","permalink":"https://codey.cc/p/368c8b7cab21/","excerpt":"","text":"前言本文主要是简单的讲述了Spring的事件机制,基本概念,讲述了事件机制的三要素事件、事件发布、事件监听器。如何实现一个事件机制,应用的场景,搭配@Async注解实现异步的操作等等。希望对大家有所帮助。 Spring的事件机制的基本概念Spring的事件机制是Spring框架中的一个重要特性,基于观察者模式实现,它可以实现应用程序中的解耦,提高代码的可维护性和可扩展性。Spring的事件机制包括事件、事件发布、事件监听器等几个基本概念。其中,事件是一个抽象的概念,它代表着应用程序中的某个动作或状态的发生。事件发布是事件发生的地方,它负责产生事件并通知事件监听器。事件监听器是事件的接收者,它负责处理事件并执行相应的操作。在Spring的事件机制中,事件源和事件监听器之间通过事件进行通信,从而实现了模块之间的解耦。 举个例子:用户修改密码,修改完密码后需要短信通知用户,记录关键性日志,等等其他业务操作。 如下图,就是我们需要调用多个服务来进行实现一个修改密码的功能。 使用了事件机制后,我们只需要发布一个事件,无需关心其扩展的逻辑,让我们的事件监听器去处理,从而实现了模块之间的解耦。 事件通过继承ApplicationEvent,实现自定义事件。是对 Java EventObject 的扩展,表示 Spring 的事件,Spring 中的所有事件都要基于其进行扩展。其源码如下。 我们可以获取到timestamp属性指的是发生时间。 事件发布事件发布是事件发生的地方,它负责产生事件并通知事件监听器。ApplicationEventPublisher用于用于发布 ApplicationEvent 事件,发布后 ApplicationListener 才能监听到事件进行处理。源码如下。 需要一个ApplicationEvent,就是我们的事件,来进行发布事件。 事件监听器ApplicationListener 是 Spring 事件的监听器,用来接受事件,所有的监听器都必须实现该接口。该接口源码如下。 Spring的事件机制的使用方法下面会给大家演示如何去使用Spring的事件机制。就拿修改密码作为演示。 如何定义一个事件新增一个类,继承我们的ApplicationEvent。 如下面代码,继承后定义了一个userId,有一个UserChangePasswordEvent方法。这里就定义我们监听器需要的业务参数,监听器需要那些参数,我们这里就定义那些参数。 /** * @Author JiaQIng * @Description 修改密码事件 * @ClassName UserChangePasswordEvent * @Date 2023/3/26 13:55 **/ @Getter @Setter public class UserChangePasswordEvent extends ApplicationEvent { private String userId; public UserChangePasswordEvent(String userId) { super(new Object()); this.userId = userId; } } 如何监听事件实现监听器有两种方法 新建一个类实现ApplicationListener接口,并且重写onApplicationEvent方法。注入到Spring容器中,交给Spring管理。如下代码。新建了一个发送短信监听器,收到事件后执行业务操作。**** /** * @Author JiaQIng * @Description 发送短信监听器 * @ClassName MessageListener * @Date 2023/3/26 14:16 **/ @Component public class MessageListener implements ApplicationListener<UserChangePasswordEvent> { @Override public void onApplicationEvent(UserChangePasswordEvent event) { System.out.println("收到事件:" + event); System.out.println("开始执行业务操作给用户发送短信。用户userId为:" + event.getUserId()); } } 使用 @EventListener 注解标注处理事件的方法,此时 Spring 将创建一个 ApplicationListener bean 对象,使用给定的方法处理事件。源码如下。参数可以给指定的事件。这里巧妙的用到了@AliasFor的能力,放到了@EventListener身上 注意:一般建议都需要指定此值,否则默认可以处理所有类型的事件,范围太广了。 代码如下。新建一个事件监听器,注入到Spring容器中,交给Spring管理。在指定方法上添加@EventListener参数为监听的事件。方法为业务代码。使用 @EventListener 注解的好处是一个类可以写很多监听器,定向监听不同的事件,或者同一个事件。 /** * @Author JiaQIng * @Description 事件监听器 * @ClassName LogListener * @Date 2023/3/26 14:22 **/ @Component public class ListenerEvent { @EventListener({ UserChangePasswordEvent.class }) public void LogListener(UserChangePasswordEvent event) { System.out.println("收到事件:" + event); System.out.println("开始执行业务操作生成关键日志。用户userId为:" + event.getUserId()); } @EventListener({ UserChangePasswordEvent.class }) public void messageListener(UserChangePasswordEvent event) { System.out.println("收到事件:" + event); System.out.println("开始执行业务操作给用户发送短信。用户userId为:" + event.getUserId()); } } @TransactionalEventListener来定义一个监听器,他与@EventListener不同的就是@EventListener标记一个方法作为监听器,他默认是同步执行,如果发布事件的方法处于事务中,那么事务会在监听器方法执行完毕之后才提交。事件发布之后就由监听器去处理,而不要影响原有的事务,也就是说希望事务及时提交。我们就可以使用该注解来标识。注意此注解需要spring-tx的依赖。 注解源码如下:主要是看一下注释内容。 // 在这个注解上面有一个注解:`@EventListener`,所以表明其实这个注解也是个事件监听器。 @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @EventListener public @interface TransactionalEventListener { /** * 这个注解取值有:BEFORE_COMMIT(指定目标方法在事务commit之前执行)、AFTER_COMMIT(指定目标方法在事务commit之后执行)、 *AFTER_ROLLBACK(指定目标方法在事务rollback之后执行)、AFTER_COMPLETION(指定目标方法在事务完成时执行,这里的完成是指无论事务是成功提交还是事务回滚了) * 各个值都代表什么意思表达什么功能,非常清晰, * 需要注意的是:AFTER_COMMIT + AFTER_COMPLETION是可以同时生效的 * AFTER_ROLLBACK + AFTER_COMPLETION是可以同时生效的 */ TransactionPhase phase() default TransactionPhase.AFTER_COMMIT; /** * 表明若没有事务的时候,对应的event是否需要执行,默认值为false表示,没事务就不执行了。 */ boolean fallbackExecution() default false; /** * 这里巧妙的用到了@AliasFor的能力,放到了@EventListener身上 * 注意:一般建议都需要指定此值,否则默认可以处理所有类型的事件,范围太广了。 */ @AliasFor(annotation = EventListener.class, attribute = "classes") Class<?>[] value() default {}; @AliasFor(annotation = EventListener.class, attribute = "classes") Class<?>[] classes() default {}; @AliasFor(annotation = EventListener.class, attribute = "condition") String condition() default ""; @AliasFor(annotation = EventListener.class, attribute = "id") String id() default ""; } 使用方式如下。phase事务类型,value指定事件。 /** * @Author JiaQIng * @Description 事件监听器 * @ClassName LogListener * @Date 2023/3/26 14:22 **/ @Component public class ListenerEvent { @EventListener({ UserChangePasswordEvent.class }) public void logListener(UserChangePasswordEvent event) { System.out.println("收到事件:" + event); System.out.println("开始执行业务操作生成关键日志。用户userId为:" + event.getUserId()); } @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT,value = { UserChangePasswordEvent.class }) public void messageListener(UserChangePasswordEvent event) { System.out.println("收到事件:" + event); System.out.println("开始执行业务操作给用户发送短信。用户userId为:" + event.getUserId()); } } 如何发布一个事件 使用ApplicationContext进行发布,由于ApplicationContext 已经继承了 ApplicationEventPublisher ,因此可以直接使用发布事件。源码如下 直接注入我们的ApplicationEventPublisher,使用@Autowired注入一下。 三种发布事件的方法,我给大家演示一下@Autowired注入的方式发布我们的事件。 @SpringBootTest class SpirngEventApplicationTests { @Autowired ApplicationEventPublisher appEventPublisher; @Testjava void contextLoads() { appEventPublisher.publishEvent(new UserChangePasswordEvent("1111111")); } } 我们执行一下看一下接口。 测试成功。 搭配@Async注解实现异步操作监听器默认是同步执行的,如果我们想实现异步执行,可以搭配@Async注解使用,但是前提条件是你真的懂@Async注解,使用不当会出现问题的。 后续我会出一篇有关@Async注解使用的文章。这里就不给大家详细的解释了。有想了解的同学可以去网上学习一下有关@Async注解使用。 使用@Async时,需要配置线程池,否则用的还是默认的线程池也就是主线程池,线程池使用不当会浪费资源,严重的会出现OOM事故。 下图是阿里巴巴开发手册的强制要求。 简单的演示一下:这里声明一下俺没有使用线程池,只是简单的演示一下。 在我们的启动类上添加@EnableAsync开启异步执行配置 @EnableAsync @SpringBootApplication public class SpirngEventApplication { public static void main(String[] args) { SpringApplication.run(SpirngEventApplication.class, args); } } 在我们想要异步执行的监听器上添加@Async注解。 /** * @Author JiaQIng * @Description 事件监听器 * @ClassName LogListener * @Date 2023/3/26 14:22 **/ @Component public class ListenerEvent { @Async @EventListener({ UserChangePasswordEvent.class }) public void logListener(UserChangePasswordEvent event) { System.out.println("收到事件:" + event); System.out.println("开始执行业务操作生成关键日志。用户userId为:" + event.getUserId()); } } 这样我们的异步执行监听器的业务操作就完成了。 Spring的事件机制的应用场景 告警操作,比喻钉钉告警,异常告警,可以通过事件机制进行解耦。 关键性日志记录和业务埋点,比喻说我们的关键日志需要入库,记录一下操作时间,操作人,变更内容等等,可以通过事件机制进行解耦。 性能监控,比喻说一些接口的时长,性能方便的埋点等。可以通过事件机制进行解耦。 …….一切与主业务无关的操作都可以通过这种方式进行解耦,常用的场景大概就上述提到的,而且很多架构的源码都有使用这种机制,如GateWay,Spring等等。 Spring的事件机制的注意事项 对于同一个事件,有多个监听器的时候,注意可以通过@Order注解指定顺序,Order的value值越小,执行的优先级就越高。 如果发布事件的方法处于事务中,那么事务会在监听器方法执行完毕之后才提交。事件发布之后就由监听器去处理,而不要影响原有的事务,也就是说希望事务及时提交。我们就可以 @TransactionalEventListener来定义一个监听器。 监听器默认是同步执行的,如果我们想实现异步执行,可以搭配@Async注解使用,但是前提条件是你真的懂@Async注解,使用不当会出现问题的。 对于同一个事件,有多个监听器的时候,如果出现了异常,后续的监听器就失效了,因为他是把同一个事件的监听器add在一个集合里面循环执行,如果出现异常,需要注意捕获异常处理异常。","categories":[{"name":"java","slug":"java","permalink":"https://codey.cc/categories/java/"}],"tags":[{"name":"java","slug":"java","permalink":"https://codey.cc/tags/java/"}],"author":"不烦"},{"title":"RabbitMq高级","slug":"RabbitMq高级","date":"2023-05-11T16:24:07.000Z","updated":"2023-05-13T14:01:28.497Z","comments":true,"path":"p/a3dc247d52a5/","link":"","permalink":"https://codey.cc/p/a3dc247d52a5/","excerpt":"","text":"一、消息确认1、可靠抵达在分布式系统中,比如现在有很多微服务,微服务连接上消息队列服务器,其它微服务可能还要监听这些消息, 但是可能会因为服务器抖动、宕机,MQ 的宕机、资源耗尽,以及无论是发消息的生产者、还是收消息的消费者,它们的卡顿、宕机等各种问题,都会导致消息的丢失,比如发送者发消息的时候,给弄丢了 ,看起来消息是发出去了,MQ网络抖动没接到, 或者MQ接到了,但是它消费消息的时候,因为网络抖动又没拿到,等等各种问题。所以在分布式系统里面,一些关键环节,我们需要保证消息一定不能不丢失,比如:订单消息发出去之后,该算库存的、该算积分的、该算优惠的等等 ,这些消息千万不能丢,因为这都是经济上的问题。 所以,想要保证不丢失,也就是可靠抵达,无论是发消息,可靠的抵达MQ,还是收消息,MQ的消息可靠抵达到我们的消费端,我们一定要保证消息可靠抵达,包括如果出现错误,我们也应该知道哪些消息丢失了,以前我们要做这种事情,可以使用事务消息,比如我们在发消息的时候,我们发消息的客户端首先会跟 MQ 建立一个连接,会在通道里面发消息,可以将通道设置成事务模式,这样发消息,只有整个消息发送过去,MQ消费成功给我们有完全的响应以后,我们才算消息成功。 但是使用事务消息,会使性能下降的很严重,官方文档说,性能会下降250倍… 为了保证在高并发期间能很快速的,确认哪些消息成功、哪些消息失败,我们引入了消息确认机制 2、消息准确送达的流程 首先生产者准备一个消息,消息只要投递给 MQ 服务器,服务器收到以后,消息该怎么存怎么存,该投给哪投给哪,所以 Broker 首先会将消息交给 Exhchange,再有 Exchange 送达给 Queue,所以整个发送消息的过程,牵扯到两个(Product->Broker->Exange->Queue) P端到B端的过程 E端到Q端的过程 3、如何保证消息的可靠送达为了保证消息的可靠送达,每个消息被成功, 我们引入了发送者的两个确认回调 第一个是确认回调,叫 confirmCallback,就是P端给B端 发送消息的过程,Broker 一旦收到了消息,就会回调我们的方法 confirmCallback,这是第一个回调时机,这个时机就可以知道哪些消息到达服务器了. 但是服务器收到消息以后,要使用 Exchange 交换机,最终投递给 Queue,但是投递给队列这个过程可能也会失败,比如我们指定的路由键有问题,或者我们队列正在使用的过程中,被其它的一些客户端删除等操作,可能都会投递失败,投递失败就会调用 returnCallback 当然,这两种回调都是针对的发送端 同样的,消费端,只要消息安安稳稳的存到了消息队列,接下来就由我们消费端进行消费了,但是消费端引用消费,会引入 ack 机制(消息确认机制) 这个机制能保证, 让 Broker 知道哪些消息都被消费者正确的拿到了,如果消费者正确接到,这个消息就要从队列里面删除,如果没有正确接到,可能就需要重新投递消息 总结 其实整个可靠抵达,分为两端处理,第一种是发送端的两种确认模式,第二个是消费端的 ack机制 4、发送端确认确认回调-ConfimCallback 成功与否都会触发 生产者发送给broker后的回调 spring: rabbitmq: publisher-confirms: true # 开启发送端确认 在创建 connectionFactory 的时候设置 PublisherConfirms(true) 选项,开启confirmcallback 。 CorrelationData:用来表示当前消息唯一性。 消息只要被 broker 接收到就会执行 confirmCallback,如果是 cluster 模式,需要所有broker 接收到才会调用 confirmCallback。 被 broker 接收到只能表示 message 已经到达服务器,并不能保证消息一定会被投递到目标 queue 里。所以需要用到接下来的 returnCallback 。 @Configuration public class MyRabbitConfig { @Autowired private RabbitTemplate rabbitTemplate; //自定义初始化initRabbitTemplate @PostConstruct //MyRabbitConfig对象构造器完成之后调用 public void initRabbitTemplate(){ rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { /** * 生产者发送给broker后的回调 * @param correlationData 当前消息的唯一关联数据(这个是消息的唯一id) * @param b 消息是否成功收到 * @param s 失败的原因 */ @Override public void confirm(CorrelationData correlationData, boolean b, String s) { /** * 1、做好消息确认机制(publisher,consumer【手动ack】) * 2、每一个发送的消息都在数据库做好记录。定期将失效的消息再次发送 */ //服务器收到了 } }); }); } } 退回回调-ReturnCallback 成功不会触发 broker发送给队列(Queue)后的失败回调 spring: rabbitmq: publisher-returns: true # 开启发送端消息抵达队列的确认 template: mandatory: true # 只要消息抵达了队列,以异步发送优先回调这个returnconfirm 在创建 connectionFactory 的时候设置 publisher-returns(true) 选项,开启returnCallback @Configuration public class MyRabbitConfig { @Autowired private RabbitTemplate rabbitTemplate; //自定义初始化initRabbitTemplate @PostConstruct //MyRabbitConfig对象构造器完成之后调用 public void initRabbitTemplate(){ rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() { /** * 只要消息没有投递给指定的队列,就触发这个失败回调 * @param message 哪个投递失败的消息信息 * @param i 回复码 * @param s 回复的文本内容 * @param s1 当时这个消息发送给哪个交换机 * @param s2 但是这个消息用的哪个路由键 */ @Override public void returnedMessage(Message message, int i, String s, String s1, String s2) { //报错误了。修改数据库当前消息的错误状态->错误 } }); } } 5、消费端确认 保证每个消息被正确消费,此时broker才可以删除这个消息 消费端默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息 queue无消费者,消息依然会被存储,直到消费者消费 带来的问题: 消费端收到很多消息,自动回复给服务器ack,只有一个消息处理成功,消费端突然宕机了,结果MQ中剩下的消息全部丢失了 解决: 消费端如果无法确定此消息是否被处理完成,可以手动确认消息,即处理一个确认一个,未确认的消息不会被删除 开启手动确认模式: rabbitmq: listener: direct: acknowledge-mode: manual #手动确认模式 只要我们没有明确告诉MQ收到消息。没有 Ack,消息就一直是 Unacked 状态,即使 consumer 宕机,消息也不会丢失,会重新变为 Ready,等待下次有新的 Consumer 连接进来时,全部都会被重新投递给新的 Consumer 消费者获取到消息,成功处理,可以回复 Ack 给 Broker ack() 用于肯定确认;broker 将移除此消息 nack() 用于否定确认;可以指定broker 是否丢弃此消息,可以批量 reject() 用于否定确认;同上,但不能批量 //方法变量需包括channel,message try { wareSkuService.unlockStock(to); //签收,业务成功,消费信息,保证幂等 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception e) { //拒签,业务失败,信息重新归队 channel.basicReject(message.getMessageProperties().getDeliveryTag(), true); } 总结: 结合消费端与发送端的消息确认机制,就能保证消息一定发出去,也能一定让别人接收到,即使没接收到,也可以重新发送,最终达到消息百分百不丢失! 二、延迟队列 比如这个库存锁成功了,我们害怕订单后续操作失败了,导致库存没法回滚,我们库存要自己解锁,那么可以把锁成功的消息,先发给消息队列,但是让消息队列先暂存一会儿。 比如我们存上三十分钟,因为我们现在的订单有可能是成功了,也有可能是失败。无论是成功失败,我们三十分钟以后,再对订单进行操作,比如订单不支付,我们都给它关了。 所以三十分钟以后订单肯定就见分晓了。四十分钟以后我们把这个消息再发给解锁库存服务,解锁库存服务,一看这个订单早都没有了,或者订单都没有支付,被人取消了。 它就可以把当时锁的库存自动的解锁一下。相当于我们整了一个定时的扫描订单、一个定时的库存解锁消息。 因为订单是保存30分钟之后,再对其进行彻底检查,这个检查是需要时间的,我们需要确保所有订单都处理完了,再对库存进行操作,所以设置为40分钟 延时队列最大的特点 TTL 死信路由 1、 消息的 TTL 消息的TTL(Time To Live)就是消息的存活时间 无论给哪个设置,它的意思都是一样的 就是指这个消息只要在我们指定的时间里边没有被人消费,那这个消息就相当于没用了,我们就把称为死信,然后这个消息相当于死了。 死了以后我们就可以把它做一个单独处理。 我们如果是给队列设置了过期时间, 队列里边的这些消息,我们存到里边。 如果这些消息一直没有被人监听,那一有人监听肯定就拿到了。 如果我们这个消息呢没有连上任何的消费者,队列里面的消息,只要三十分钟一过,那这些消息呢就会成为死信。 那就可以把它扔掉了,服务器默认就会把它进行丢弃。 那我们给消息设置,也是一样的效果。 如果我们给单独的每一个消息设置,设置了三十分钟过期时间,存到这个队列里边, 只要这个队列没人消费这个消息,消息三十分钟没有人消费,它就自己过期了。 设置消息的 TTL 的目的就是,在我们指定的这段时间内,没人把这个消息取走,这个消息就会被服务器认为是一个死信。 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果。 2、死信路由一个消息如果满足以下条件,那它就会成为死信: 被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。 ( basic.reject/ basic.nack ) requeue=false 消息的TTL到了,消息过期了 假设队列的长度限制满了,排在前面的消息就会被丢弃或者扔到死信路由上 但是如果这个消息死了,就直接把它丢掉,那就没有延时队列的功能了 死信路由的实现: 假设我给队列设一个过期时间三十分钟,然后三十分钟以后,只要这个消息没人消费,我们就认为是死信,我们让服务器把死信扔给隔壁的一个交换机。这个交换机我们称为死信路由,它专门来收集这些死信,然后死信路由会把这些死信再送到另外一个新队列里边,别人专门监听这个新队列 注意:一开始我们设置30分钟过期的那个队列,我们不会让任何人监听,因为只要被人一监听,消息就算设置了过期时间,被人一拿,也就什么都没了。 死信去的那个新队列,里面其实存的都是初始队列里边过了三十分钟以后的这些消息,都是被死信路由给送过去的 这样就模拟了一个延迟,我们三十分钟让它在一开始的队列存一下,因为没人消费它,存完了以后又移到一个新的队列里。 如果我们解锁订单的服务,一直来监听那个新队列, 订单一下成功,先把订单消息放到初始队列延迟上三十分钟,延迟以后呢,交给交换机,交换机再路由到新队列。 那么收到的这些订单消息一定都是过了30分钟的。 死信路由呢就是一个非常普通的路由而已。 只要 TTL 跟 死信路由两者结合,我们就能模拟出延时队列,消息在初始队列保持三十分钟,这个队列一直不被人消费,然后三十分钟一过,消息被服务器认为是死信,再丢给交换机,然后,这个交换机再丢给我们指定的队列,然后这个指定的队列再被人消费。 3、实现队列过期时间 生产者:P 非常普通的交换机:X 死信队列:delay.SM.queue 死信路由:delay.exchange 死信路由键:deal.message 新队列(使用死信路由键绑定死信路由):test.queue 消费者:C 首先发送者发消息的时候,指定了一个路由键,然后这个消息先会被交给一个交换机,交换机会按照路由键把它交给一个队列,那这个队列跟交换机的绑定关系就是那个路由键,但这个队列很特殊,它有这么几项设置: x-dead-letter-exchange:死信交换机,就是死信路由,意思就是告诉服务器当前这个队列里边的消息死了,别乱丢,扔给隔壁的死信路由 x-dead-letter-routing-key:死信队列往死信路由那扔消息用的路由键 x-message-ttl:队列的存活时间,它以毫秒为单位,相当于300秒,也就是五分钟以后消息过期 所以我们的死信就会通过delay.message这个路由键交给我们的死信路由,一个普普通通的路由,然后死信路由收到死信之后,一看路由键delay.message,它就会找哪个队列绑定的路由键叫delay.message。然后,死信路由发现test.queue这个队列的路由键是delay.message,它就把这个消息就交给了它,以后只要有人监听test.queue这个队列,那这个人收到的消息都是在死信队列存过五分钟以后的过期消息,这是我们延时队列的第一种实现,设置队列过期时间。 消息过期时间 生产者:P 非常普通的交换机:X 死信队列:delay.SM.queue 死信路由:delay.exchange 死信路由键:deal.message 新队列(使用死信路由键绑定死信路由):test.queue 消费者:C 比如我们这个消费者发了一个消息,它为发的这个消息,单独设置了一个过期时间,比如它是三百秒,五分钟。然后,这个消息经过交换机交给我们这个的死信队列,由于消息存到死信队列以后,没有人会永远去监听里边的内容。所以这个消息就会一直呆在死信队列里边。服务器就会来检查这个消息,结果发现它是五分钟以后过期的,所以五分钟以后服务器就会把第一个消息拿出来,然后把这个消息,按照我们队列指定的:死了的消息交给死信路由,然后再交给test.queue所以消费者最终收到的消息也都是五分钟以后的过期消息。 由于 Rabbit MQ 采用的是惰性检查机制,也就是懒检查。 什么叫懒检查, 假设我们这个 MQ 这个队列里边存了第一个消息。 第一个消息,我们指定它是一个五分钟以后过期的,我们给这个队列连发了三条消息。 第一个是五分钟以后过期, 第二个是一分钟以后过期 第三个是一秒以后过期, 我们按照正常情况,应该是一秒过期的,我们优先弹出这个队列,但是服务器不是这么检查的。 服务器是这样,它从队列里边呢先来拿第一个消息。 第一个消息呢,它刚一拿,发现是五分钟过期,然后呢,它又放回去了。五分钟以后再来拿 服务器呢五分钟以后会把第一个消息拿出来,那第一个消息呢相当于就过期了,变成死信交到交换机,最终进入死信队列,被消费者拿到 所以第一个消息过期了以后,服务器来拿第二个消息。 第二个消息,它说一分钟过期,但是服务器也不用等它一分钟,因为它发消息的时候又一个时间,服务器一看发现早过期了,然后就赶紧把它扔了。 然后,第三个它说一秒以后过期了。那我们也给它扔了, 但是我们会发现,扔后面的这两个消息,就会在五分钟以后才扔。 所以我们应该使用给整个队列设置一个过期时间,这样队列里边所有的消息都是这个过期时间,我们服务器直接批量全部拿出来,往后放就行了。 总结: 推荐给队列设置过期时间 三、消息丢失、重复、积压1、如何处理消息丢失 消息发送出去,由于网络问题没有抵达服务器 做好容错方法(try-catch),发送消息可能会网络失败,失败后要有重试机制,可记录到数据库,采用定期扫描重发的方式 做好日志记录,每个消息状态是否都被服务器收到都应该记录,可以创建一张关于消息的数据表,存到数据库里 CREATE TABLE `mq_message` ( `message_id` char(64) NOT NULL, `content` text, `to_exchane` varchar(255) DEFAULT NULL, `routing_key` varchar(255) DEFAULT NULL, `class_type` varchar(255) DEFAULT NULL, `message_status` int(1) DEFAULT '0' COMMENT '0-新建 1-已发送 2-错误抵达 3-已抵达', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, PRIMARY KEY (`message_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 消息抵达Broker之后,Broker要将消息写入磁盘(持久化)时宕机publisher-returns必须加入确认回调机制,确认成功的消息,修改数据库消息状态。 自动ACK的状态下。消费者收到消息,但没来得及消费然后宕机一定开启手动ACK,消费成功再移除,失败或者没来得及处理就reject并重新入队 2、如何解决消息重复分以下几种情况: 消息消费成功,事务已经提交,ack时,机器宕机。导致没有ack成功,Broker的消息重新由unack变为ready,并发送给其他消费 消息消费失败,由于重试机制,自动又将消息发送出去 解决: 消费者的业务消费接口应该设计为幂等性的。比如扣库存有工作单的状态 使用防重表(redis/mysql),发送消息每一个都有业务的唯一标识,处理过就不用处理 rabbitMQ的每一个消息都有redelivered字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的 3、如何解决消息积压分以下几种情况: 消费者宕机积压 消费者消费能力不足积压 发送者发送流量太大 解决: 上线更多的消费者,进行正常消费 上线专门的队列消费服务,将消息先批量取出来,记录数据库,离线慢慢处理","categories":[{"name":"rabbitmq","slug":"rabbitmq","permalink":"https://codey.cc/categories/rabbitmq/"}],"tags":[{"name":"中间件","slug":"中间件","permalink":"https://codey.cc/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"}],"author":"不烦"},{"title":"玩转Stream","slug":"玩转Stream","date":"2023-05-04T14:52:01.000Z","updated":"2023-05-14T06:35:39.058Z","comments":true,"path":"p/c1a24517aa76/","link":"","permalink":"https://codey.cc/p/c1a24517aa76/","excerpt":"","text":"1 Stream概述 Stream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。 Stream可以由数组或集合创建,对流的操作分为两种: 中间操作,每次返回一个新的流,可以有多个。 终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值。 另外,Stream有几个特性: stream不存储数据,而是按照特定的规则对数据进行计算,一般会输出结果。 stream不会改变数据源,通常情况下会产生一个新的集合或一个值。 stream具有延迟执行特性,只有调用终端操作时,中间操作才会执行。 2 Stream创建Stream可以通过集合数组创建。 1、通过 java.util.Collection.stream() 方法用集合创建流 List<String> list = Arrays.asList("a", "b", "c"); // 创建一个顺序流 Stream<String> stream = list.stream(); // 创建一个并行流 Stream<String> parallelStream = list.parallelStream(); 2、使用**java.util.Arrays.stream(T[] array)**方法用数组创建流 int[] array={1,3,5,6,8}; IntStream stream = Arrays.stream(array); 3、使用Stream的静态方法:of()、iterate()、generate() Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6); Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(4); stream2.forEach(System.out::println); Stream<Double> stream3 = Stream.generate(Math::random).limit(3); stream3.forEach(System.out::println); stream和parallelStream的简单区分: stream是顺序流,由主线程按顺序对流执行操作,而parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。例如筛选集合中的奇数,两者的处理不同之处: 如果流中的数据量足够大,并行流可以加快处速度。 除了直接创建并行流,还可以通过**parallel()**把顺序流转换成并行流: Optional<Integer> findFirst = list.stream().parallel().filter(x->x>6).findFirst(); 3 Stream使用案例使用的员工类: List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, "male", "New York")); personList.add(new Person("Jack", 7000, "male", "Washington")); personList.add(new Person("Lily", 7800, "female", "Washington")); personList.add(new Person("Anni", 8200, "female", "New York")); personList.add(new Person("Owen", 9500, "male", "New York")); personList.add(new Person("Alisa", 7900, "female", "New York")); class Person { private String name; // 姓名 private int salary; // 薪资 private int age; // 年龄 private String sex; //性别 private String area; // 地区 // 构造方法 public Person(String name, int salary, int age,String sex,String area) { this.name = name; this.salary = salary; this.age = age; this.sex = sex; this.area = area; } // 省略了get和set,请自行添加 } 3.1 遍历/匹配(foreach/find/match) Stream也是支持类似集合的遍历和匹配元素的,只是Stream中的元素是以Optional类型存在的。Stream的遍历、匹配非常简单。 // import已省略,请自行添加,后面代码亦是 public class StreamTest { public static void main(String[] args) { List<Integer> list = Arrays.asList(10, 11, 12, 13, 14, 15, 16, 12, 13, 14, 18, 111, 7, 6, 9, 3, 8, 9, 2, 1); List<String> listString = new ArrayList<>(); listString.add("zzy"); listString.add("zy"); listString.add("zz"); // 遍历输出符合条件的元素 list.stream().filter(x -> x > 6).sorted().distinct().skip(1).limit(2).forEach(System.out::println); //list.stream().filter(x -> x > 6).forEach(System.out::println); listString.stream().filter(x -> x.equals("zzy")).forEach(System.out::println); System.out.println(listString.stream().filter(x -> x.equals("zzy")).sorted()); // 匹配第一个 Optional<Integer> findFirst = list.stream().filter(x -> x > 6).findFirst(); // 匹配任意一个(适用于并行流) Optional<Integer> findAny = list.parallelStream().filter(x -> x > 1).findAny(); // 是否包含符合特定条件的元素 boolean anyMatch = list.stream().anyMatch(x -> x > 6); System.out.println("匹配第一个值:" + findFirst.get()); System.out.println("匹配任意一个值:" + findAny.get()); System.out.println("是否存在大于6的值:" + anyMatch); } } 3.2 筛选(filter) 筛选,是按照一定的规则校验流中的元素,将符合条件的元素提取到新的流中的操作。 示例1:筛选出Integer集合中大于7的元素,并打印 public class StreamTest { public static void main(String[] args) { List<Integer> list = Arrays.asList(6, 7, 3, 8, 1, 2, 9); Stream<Integer> stream = list.stream(); stream.filter(x -> x > 7).forEach(System.out::println); } } 示例2:筛选员工中工资高于8000的人,并形成新的集合。 形成新集合依赖collect(收集) public class StreamTest { public static void main(String[] args) { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); personList.add(new Person("Owen", 9500, 25, "male", "New York")); personList.add(new Person("Alisa", 7900, 26, "female", "New York")); List<String> fiterList = personList.stream().filter(x -> x.getSalary() > 8000).map(Person::getName) .collect(Collectors.toList()); System.out.print("高于8000的员工姓名:" + fiterList); } } 3.3 聚合(max/min/count) max、min、count这些字眼你一定不陌生,没错,在mysql中我们常用它们进行数据统计。Java stream中也引入了这些概念和用法,极大地方便了我们对集合、数组的数据统计工作。 示例1:获取String集合中最长的元素。 public class StreamTest { public static void main(String[] args) { List<String> list = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd"); Optional<String> max = list.stream().max(Comparator.comparing(String::length)); System.out.println("最长的字符串:" + max.get()); } } 示例2:获取Integer集合中的最大值。 public class StreamTest { public static void main(String[] args) { List<Integer> list = Arrays.asList(7, 6, 9, 4, 11, 6); // 自然排序 Optional<Integer> max = list.stream().max(Integer::compareTo); // 自定义排序 Optional<Integer> max2 = list.stream().max(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1.compareTo(o2); } }); System.out.println("自然排序的最大值:" + max.get()); System.out.println("自定义排序的最大值:" + max2.get()); } } 示例3:获取员工工资最高的人。 public class StreamTest { public static void main(String[] args) { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); personList.add(new Person("Owen", 9500, 25, "male", "New York")); personList.add(new Person("Alisa", 7900, 26, "female", "New York")); Optional<Person> max = personList.stream().max(Comparator.comparingInt(Person::getSalary)); System.out.println("员工工资最大值:" + max.get().getSalary()); } } 示例4:计算Integer集合中大于6的元素的个数。 public class StreamTest { public static void main(String[] args) { List<Integer> list = Arrays.asList(7, 6, 4, 8, 2, 11, 9); long count = list.stream().filter(x -> x > 6).count(); System.out.println("list中大于6的元素个数:" + count); } } 3.4 映射(map/flatMap) 映射,可以将一个流的元素按照一定的映射规则映射到另一个流中。分为map和flatMap: map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。 flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。 示例1:英文字符串数组的元素全部改为大写。整数数组每个元素+3。 public class StreamTest { public static void main(String[] args) { String[] strArr = { "abcd", "bcdd", "defde", "fTr" }; List<String> strList = Arrays.stream(strArr).map(String::toUpperCase).collect(Collectors.toList()); List<Integer> intList = Arrays.asList(1, 3, 5, 7, 9, 11); List<Integer> intListNew = intList.stream().map(x -> x + 3).collect(Collectors.toList()); System.out.println("每个元素大写:" + strList); System.out.println("每个元素+3:" + intListNew); } } 示例2:将员工的薪资全部增加1000。 public class StreamTest { public static void main(String[] args) { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); personList.add(new Person("Owen", 9500, 25, "male", "New York")); personList.add(new Person("Alisa", 7900, 26, "female", "New York")); // 不改变原来员工集合的方式 List<Person> personListNew = personList.stream().map(person -> { Person personNew = new Person(person.getName(), 0, 0, null, null); personNew.setSalary(person.getSalary() + 10000); return personNew; }).collect(Collectors.toList()); System.out.println("一次改动前:" + personList.get(0).getName() + "-->" + personList.get(0).getSalary()); System.out.println("一次改动后:" + personListNew.get(0).getName() + "-->" + personListNew.get(0).getSalary()); // 改变原来员工集合的方式 List<Person> personListNew2 = personList.stream().map(person -> { person.setSalary(person.getSalary() + 10000); return person; }).collect(Collectors.toList()); System.out.println("二次改动前:" + personList.get(0).getName() + "-->" + personListNew.get(0).getSalary()); System.out.println("二次改动后:" + personListNew2.get(0).getName() + "-->" + personListNew.get(0).getSalary()); } } 示例3、将两个字符数组合并成一个新的字符数组。 public class StreamTest { public static void main(String[] args) { List<String> list = Arrays.asList("m,k,l,a", "1,3,5,7"); List<String> listNew = list.stream().flatMap(s -> { // 将每个元素转换成一个stream String[] split = s.split(","); Stream<String> s2 = Arrays.stream(split); return s2; }).collect(Collectors.toList()); list.forEach(System.out::println); listNew.forEach(System.out::println); } } 3.5 归约(reduce) 归约,也称缩减,顾名思义,是把一个流缩减成一个值,能实现对集合求和、求乘积和求最值操作。 示例1:求Integer集合的元素之和、乘积和最大值。 public class StreamTest { public static void main(String[] args) { List<Integer> list = Arrays.asList(1, 3, 2, 8, 11, 4); // 求和方式1 Optional<Integer> sum = list.stream().reduce((x, y) -> x + y); // 求和方式2 Optional<Integer> sum2 = list.stream().reduce(Integer::sum); // 求和方式3 Integer sum3 = list.stream().reduce(0, Integer::sum); // 求乘积 Optional<Integer> product = list.stream().reduce((x, y) -> x * y); // 求最大值方式1 Optional<Integer> max = list.stream().reduce((x, y) -> x > y ? x : y); // 求最大值写法2 Integer max2 = list.stream().reduce(1, Integer::max); System.out.println("list求和:" + sum.get() + "," + sum2.get() + "," + sum3); System.out.println("list求积:" + product.get()); System.out.println("list最大值:" + max.get() + "," + max2); } } 示例2:求所有员工的工资之和和最高工资。 public class StreamTest { public static void main(String[] args) { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); personList.add(new Person("Owen", 9500, 25, "male", "New York")); personList.add(new Person("Alisa", 7900, 26, "female", "New York")); // 求工资之和方式1: Optional<Integer> sumSalary = personList.stream().map(Person::getSalary).reduce(Integer::sum); // 求工资之和方式2: Integer sumSalary2 = personList.stream().reduce(0, (sum, p) -> sum += p.getSalary(), (sum1, sum2) -> sum1 + sum2); // 求工资之和方式3: Integer sumSalary3 = personList.stream().reduce(0, (sum, p) -> sum += p.getSalary(), Integer::sum); // 求最高工资方式1: Integer maxSalary = personList.stream().reduce(0, (max, p) -> max > p.getSalary() ? max : p.getSalary(), Integer::max); // 求最高工资方式2: Integer maxSalary2 = personList.stream().reduce(0, (max, p) -> max > p.getSalary() ? max : p.getSalary(), (max1, max2) -> max1 > max2 ? max1 : max2); System.out.println("工资之和:" + sumSalary.get() + "," + sumSalary2 + "," + sumSalary3); System.out.println("最高工资:" + maxSalary + "," + maxSalary2); } } 3.6 收集(collect) collect,收集,可以说是内容最繁多、功能最丰富的部分了。从字面上去理解,就是把一个流收集起来,最终可以是收集成一个值也可以收集成一个新的集合。 3.6.1 归集(toList/toSet/toMap) 因为流不存储数据,那么在流中的数据完成处理后,需要将流中的数据重新归集到新的集合里。toList、toSet和toMap比较常用,另外还有toCollection、toConcurrentMap等复杂一些的用法。 下面用一个案例演示toList、toSet和toMap: public class StreamTest { public static void main(String[] args) { List<Integer> list = Arrays.asList(1, 6, 3, 4, 6, 7, 9, 6, 20); List<Integer> listNew = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toList()); Set<Integer> set = list.stream().filter(x -> x % 2 == 0).collect(Collectors.toSet()); List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); personList.add(new Person("Anni", 8200, 24, "female", "New York")); Map<?, Person> map = personList.stream().filter(p -> p.getSalary() > 8000) .collect(Collectors.toMap(Person::getName, p -> p)); System.out.println("toList:" + listNew); System.out.println("toSet:" + set); System.out.println("toMap:" + map); } } 3.6.2 统计(count/averaging)Collectors提供了一系列用于数据统计的静态方法: 计数:count 平均值:averagingInt、averagingLong、averagingDouble 最值:maxBy、minBy 求和:summingInt、summingLong、summingDouble 统计以上所有:summarizingInt、summarizingLong、summarizingDouble 统计员工人数、平均工资、工资总额、最高工资。 public class StreamTest { public static void main(String[] args) { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); // 求总数 Long count = personList.stream().collect(Collectors.counting()); // 求平均工资 Double average = personList.stream().collect(Collectors.averagingDouble(Person::getSalary)); // 求最高工资 Optional<Integer> max = personList.stream().map(Person::getSalary).collect(Collectors.maxBy(Integer::compare)); // 求工资之和 Integer sum = personList.stream().collect(Collectors.summingInt(Person::getSalary)); // 一次性统计所有信息 DoubleSummaryStatistics collect = personList.stream().collect(Collectors.summarizingDouble(Person::getSalary)); System.out.println("员工总数:" + count); System.out.println("员工平均工资:" + average); System.out.println("员工工资总和:" + sum); System.out.println("员工工资所有统计:" + collect); } } 3.6.3 分组(partitioningBy/groupingBy) 分区:将stream按条件分为两个Map,比如员工按薪资是否高于8000分为两部分。 分组:将集合分为多个Map,比如员工按性别分组。有单级分组和多级分组。 案例:将员工按薪资是否高于8000分为两部分;将员工按性别和地区分组 public class StreamTest { public static void main(String[] args) { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, "male", "New York")); personList.add(new Person("Jack", 7000, "male", "Washington")); personList.add(new Person("Lily", 7800, "female", "Washington")); personList.add(new Person("Anni", 8200, "female", "New York")); personList.add(new Person("Owen", 9500, "male", "New York")); personList.add(new Person("Alisa", 7900, "female", "New York")); // 将员工按薪资是否高于8000分组 Map<Boolean, List<Person>> part = personList.stream().collect(Collectors.partitioningBy(x -> x.getSalary() > 8000)); // 将员工按性别分组 Map<String, List<Person>> group = personList.stream().collect(Collectors.groupingBy(Person::getSex)); // 将员工先按性别分组,再按地区分组 Map<String, Map<String, List<Person>>> group2 = personList.stream().collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getArea))); System.out.println("员工按薪资是否大于8000分组情况:" + part); System.out.println("员工按性别分组情况:" + group); System.out.println("员工按性别、地区:" + group2); } } 3.6.4 接合(joining) joining`可以将stream中的元素用特定的连接符(没有的话,则直接连接)连接成一个字符串。 public class StreamTest { public static void main(String[] args) { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); String names = personList.stream().map(p -> p.getName()).collect(Collectors.joining(",")); System.out.println("所有员工的姓名:" + names); List<String> list = Arrays.asList("A", "B", "C"); String string = list.stream().collect(Collectors.joining("-")); System.out.println("拼接后的字符串:" + string); } } 3.6.5 归约(reducing)Collectors类提供的reducing方法,相比于stream本身的reduce方法,增加了对自定义归约的支持。 public class StreamTest { public static void main(String[] args) { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Tom", 8900, 23, "male", "New York")); personList.add(new Person("Jack", 7000, 25, "male", "Washington")); personList.add(new Person("Lily", 7800, 21, "female", "Washington")); // 每个员工减去起征点后的薪资之和(这个例子并不严谨,但一时没想到好的例子) Integer sum = personList.stream().collect(Collectors.reducing(0, Person::getSalary, (i, j) -> (i + j - 5000))); System.out.println("员工扣税薪资总和:" + sum); // stream的reduce Optional<Integer> sum2 = personList.stream().map(Person::getSalary).reduce(Integer::sum); System.out.println("员工薪资总和:" + sum2.get()); } } 3.7 排序(sorted)sorted,中间操作。有两种排序: sorted():自然排序,流中元素需实现Comparable接口 sorted(Comparator com):Comparator排序器自定义排序 案例:将员工按工资由高到低(工资一样则按年龄由大到小)排序 public class StreamTest { public static void main(String[] args) { List<Person> personList = new ArrayList<Person>(); personList.add(new Person("Sherry", 9000, 24, "female", "New York")); personList.add(new Person("Tom", 8900, 22, "male", "Washington")); personList.add(new Person("Jack", 9000, 25, "male", "Washington")); personList.add(new Person("Lily", 8800, 26, "male", "New York")); personList.add(new Person("Alisa", 9000, 26, "female", "New York")); // 按工资升序排序(自然排序) List<String> newList = personList.stream().sorted(Comparator.comparing(Person::getSalary)).map(Person::getName) .collect(Collectors.toList()); // 按工资倒序排序 List<String> newList2 = personList.stream().sorted(Comparator.comparing(Person::getSalary).reversed()) .map(Person::getName).collect(Collectors.toList()); // 先按工资再按年龄升序排序 List<String> newList3 = personList.stream() .sorted(Comparator.comparing(Person::getSalary).thenComparing(Person::getAge)).map(Person::getName) .collect(Collectors.toList()); // 先按工资再按年龄自定义排序(降序) List<String> newList4 = personList.stream().sorted((p1, p2) -> { if (p1.getSalary() == p2.getSalary()) { return p2.getAge() - p1.getAge(); } else { return p2.getSalary() - p1.getSalary(); } }).map(Person::getName).collect(Collectors.toList()); System.out.println("按工资升序排序:" + newList); System.out.println("按工资降序排序:" + newList2); System.out.println("先按工资再按年龄升序排序:" + newList3); System.out.println("先按工资再按年龄自定义降序排序:" + newList4); } } 3.8 提取/组合流也可以进行合并、去重、限制、跳过等操作。 public class StreamTest { public static void main(String[] args) { String[] arr1 = { "a", "b", "c", "d" }; String[] arr2 = { "d", "e", "f", "g" }; Stream<String> stream1 = Stream.of(arr1); Stream<String> stream2 = Stream.of(arr2); // concat:合并两个流 distinct:去重 List<String> newList = Stream.concat(stream1, stream2).distinct().collect(Collectors.toList()); // limit:限制从流中获得前n个数据 List<Integer> collect = Stream.iterate(1, x -> x + 2).limit(10).collect(Collectors.toList()); // skip:跳过前n个数据 List<Integer> collect2 = Stream.iterate(1, x -> x + 2).skip(1).limit(5).collect(Collectors.toList()); System.out.println("流合并:" + newList); System.out.println("limit:" + collect); System.out.println("skip:" + collect2); } }","categories":[{"name":"java","slug":"java","permalink":"https://codey.cc/categories/java/"}],"tags":[{"name":"java","slug":"java","permalink":"https://codey.cc/tags/java/"}],"author":"不烦"},{"title":"Linux私藏命令","slug":"Linux私藏命令","date":"2023-05-01T09:03:07.000Z","updated":"2023-05-01T09:26:33.194Z","comments":true,"path":"p/e4a7b2375fef/","link":"","permalink":"https://codey.cc/p/e4a7b2375fef/","excerpt":"","text":"ctrl+a: 光标跳到行首 ctrl+b: 光标左移一个字母 ctrl+c: 杀死当前进程 ctrl+d: 删除提示符后一个字符或exit或logout ctrl+e: 光标跳到行尾 ctrl+f: 后移一个字符 ctrl+h: 删除光标前一个字符,同backspace键相同 ctrl+k: 清除光标后至行尾的内容。 ctrl+l: 清屏,相当于clear Ctrl+p: 重复上一次命令 ctrl+r: 搜索之前打过的命令。会有一个提示,根据你输入的关键字进行搜索bash的history ctrl+u: 清除光标前至行首间的所有内容 ctrl+w: 不是删除光标前的所有字符,它删除光标前的一个单词 ctrl+t: 交换光标位置前的两个字符 ctrl+y: 粘贴或者恢复上次的删除 ctrl+d: 删除光标所在字母;注意和backspace以及ctrl+h的区别,这2个是删除光标前的字符 ctrl+f: 光标右移 ctrl+z: 把当前进程转到后台运行,使用’ fg ‘命令恢复。比如top -d1 然后ctrl+z,到后台,然后fg,重新恢复 Ctrl+x: 同上但再按一次会从新回到原位置 Ctrl+o: Ctrl+y Ctrl+i Crtl+m这4个没搞清楚怎么用 ctrl+i: 等同于按制表符键 ctrl+V: 使下一个特殊字符可以插入在当前位置,如CTRL-V 可以在当前位置插入一个字符,其ASCII是9,否则一般情况下按结果是命令补齐 ctrl+s: 暂时冻结当前shell的输入 ctrl+q: 解冻 #设置时区UTC+8 sudo ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime #查找日志几种方法 tail -1000f *.log :查看最近1000行日志,自下而上(**常用**) more *.log :按行查看日志,自上而下 less *.log :查看大文件日志,shift+g拉到最后一行(**常用**) head *.log :查看头部10行日志 cat *.log :查看小文件日志,直接打印至控制台 grep -inC 0 '*' *.log :过滤忽略大小写关键字上下0行并显示行数的打印 zgrep -inC 0 '*' *.gz :同上,不过是过滤gz文件 #设置unix格式 set ff=unix #tar解压文件 tar -xvf file.tar -C 指定解压后文件存放地址 tar -zxvf file.tar.gz #tar压缩文件 tar -cvf file.tar dir/files #zip压缩文件 zip -vr z.zip dir/files -d a.txt (-d 压缩过程中剔除指定文件) #unzip解压文件 unzip -n file.zip -d dir/ (解压到指定路径,不覆盖已有同名文件) #awk:默认的分隔符是一个空格或者多个空格或者是制表符。 $n指定第n列 -F指定分割符 awk [选项参数] '/parttern1/{action1} /parttern2/{action2}...' 文件名 ps -ef | grep -v grep | grep mysql | awk '{print $2}' #获取进程名为mysql的进程pid ifconfig|grep -A 1 eth1 |awk '{print$2}'|tail -1 #获取ip awk -F : '/^root/{print}' ./passwd #搜索passwd文件以root关键字开头的所有行并以:分割,输出该行的第 7 列 #cut:默认的分隔符是制表符 -d指定分隔符; -f指定第n列 netstat -nltp | grep 3306 | awk '{print $7}' | cut -d '/' -f 1 #获取占用3306端口号的进程pid #定时任务 crontab -e 编辑定时任务 crontab -l 查看定时任务列表 cat /etc/crontab 查看用法 #远程传输文件 scp -r file [email protected]:/home #编码格式转换 file b.csv 查看编码格式 iconv -f UTF-8 -t GBK b.csv -o b.csv #-f原始格式,-t输出格式,-o指定输出文件 #查看进程运行时长 ps -p pid -o etimes,etime #删除乱码文件 ll -i 可以显示文件在系统中的唯一inode编号 find . -inum ***|xargs rm -rf #获取本机公网地址ip curl ip.sb #历史命令使用技巧 !pw:重复执行最近一次,以pw开头的历史命令 !!:重复执行上条命令 !$:表示最近一次命令的最后一个参数 #history不记录操作命令 在用户的环境变量添加HISTCONTROL = ignorespace或者执行命令export HISTCONTROL=ignorespace 之后输入的命令,如果前面有空格都不会被记录 #删除文件空行 sed -i '/^$/d' nginx.conf cat file1 |tr -s "\\n" > file2 #file1和file2不能相同 #查找时间范围内的文件 find / -name 'abc.pdf' -newermt '2022-11-05' ! -newermt '2023-01-06' #查找3天内更新过的文件 find . -ctime -3 ---(+n)----------|----------(n)----------|----------(-n)--- (n+1)*24H前| (n+1)*24H~n*24H间 |n*24H内 [a|c|m]min -- [最后访问|最后状态(时间,权限等等)修改|最后内容修改]min(分钟) [a|c|m]time -- [最后访问|最后状态修改|最后内容修改]time(天) #过滤目录 ls -l |grep -v ^d |wc -l 过滤目录查找文件总数 -v排除 ^d以d开头 ls -l |grep ^- |grep -v 'log$' 查找文件类型不以log结尾的文件 ls -l |awk '{if($2>1) print $0}' 过滤目录 #根据pid获取执行文件目录 1,pwdx pid 2,ll /proc/pid |grep cwd #每个进程都会在/proc生成一个目录,cwd执行文件,exe是执行程序,cmdline是执行命令 #查看系统版本信息 1,uname -a 2,cat /proc/version 3,cat /etc/redhat-release lscpu 查看cpu信息核数等等 #文件上传下载工具 安装 yum -y install lrzsz 使用 rz上传 sz file下载 #nohup命令 nohup command > /dev/null 2>&1 & 后台执行命令 在这个语句中,出现了2、1两个数字,要说明的是,& 0表示键盘输入|1表示屏幕输出|2表示错误输出 假设没有&,2>1,2与>结合代表错误重定向,而1代表1个文件,这句话意思是错误输出重定向到一个文件1中,而不代表标准输出;换成2>&1,&与1就代表标准输出了。这句话的意思就是,后台执行命令,把标准错误重定向到标准输出,然后扔到/dev/null中去,即:把所有标准输出和错误输出都扔到垃圾桶里。 #分割文件为1G大小的x00数字形式,文件名后缀为.log的小文件 split -b 1G -d --additional-suffix=.log filename #建立软连接 ln -s /目标文件或目录 /本文件或目录 #周期性查看命令结果 watch -n 3 -d + 命令 vim命令优化 vim ~/.vimrc '设置编码' set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936 set termencoding=utf-8 set encoding=utf-8 '显示行号' set number '设置Tab长度为4空格' set tabstop=4 '自动缩进继承上一行' set autoindent '符号匹配' inoremap ( ()<Esc>i inoremap { {}<Esc>i inoremap [ []<Esc>i inoremap " ""<Esc>i inoremap ' ''<ESC>i SSH免密登录 #首先需要安装工具 ssh-keygen和ssh-copy-id若没有yum安装即可 1,直接执行命令ssh-keygen -t rsa 一路回车 2,会在~/.ssh/目录下生成两个文件,id_rsa和id_rsa.pub。第一个是私钥文件,第二个是公钥文件。 3,使用命令ssh-copy-id [email protected],之后会在另一台机器~.ssh目录下生成authorized_keys文件(如果不传入 -i 参数,ssh-copy-id 使用默认 ~/.ssh/identity.pub 作为默认公钥) 也可以使用scp -p ~/.ssh/id_rsa.pub root@<remote_ip>:/root/.ssh/authorized_keys 4,服务器B端chmod 700 -R .ssh 5,配置成功 ssh [email protected]尝试登陆就不需要密码了 设置服务开机自启1.进入 /lib/systemd/system/ 目录下2.创建文件:nginx.service3.编辑文件 [Unit] Description=nginx service After=network.target [Service] Type=forking ExecStart=/usr/local/nginx/sbin/nginx ExecReload=/usr/local/nginx/sbin/nginx -s reload ExecStop=/usr/local/nginx/sbin/nginx -s quit PrivateTmp=true [Install] WantedBy=multi-user.target 4.systemctl enable nginx 开机自启systemctl start nginx.service 启动nginx服务systemctl stop nginx.service 停止服务systemctl restart nginx.service 重新启动服务systemctl list-units –type=service 查看所有已启动的服务systemctl status nginx.service 查看服务当前状态systemctl enable nginx.service 设置开机自启动systemctl disable nginx.service 停止开机自启动","categories":[{"name":"linux","slug":"linux","permalink":"https://codey.cc/categories/linux/"}],"tags":[{"name":"linux","slug":"linux","permalink":"https://codey.cc/tags/linux/"}],"author":"不烦"},{"title":"","slug":"卡片","date":"2023-04-30T17:39:11.479Z","updated":"2023-05-14T13:15:53.030Z","comments":true,"path":"p/b2937a13eef6/","link":"","permalink":"https://codey.cc/p/b2937a13eef6/","excerpt":"","text":"","categories":[],"tags":[]},{"title":"CentOS如何增加虚拟内存","slug":"CentOS如何增加虚拟内存","date":"2023-04-30T11:40:07.000Z","updated":"2023-05-01T08:43:50.148Z","comments":true,"path":"p/223a80ba41d6/","link":"","permalink":"https://codey.cc/p/223a80ba41d6/","excerpt":"","text":"增加虚拟内存空间,来增加内存容量 一、swap分区的创建1、查看磁盘使用情况free -h 哎,都是因为穷啊,2g服务器,跑俩应用就费劲了,不多说,干就完了!!! 2、添加Swap分区使用dd命令创建名为swapfile 的swap交换文件(文件名和目录任意): dd if=/dev/zero of=/var/swapfile bs=1024 count=4194304 dev/zero是Linux的一种特殊字符设备(输入设备),可以用来创建一个指定长度用于初始化的空文件,如临时交换文件,该设备无穷尽地提供0,可以提供任何你需要的数目。 bs=1024 :单位数据块(block)同时读入/输出的块字节大小为1024 个字节即1KB,bs(即block size)。 count = 4194304 表示的是4G 具体计算公式为:1KB * 4194304 =1KB *1024(k)10244 = 4194304 =4G 如果小伙伴需要调整交换区的大小的话,可以自行设置其他的 执行完命令后,会进行4G读写操作,所以会有一些卡顿,小伙伴耐心等待 3、对交换文件格式化并转换为swap分区mkswap /var/swapfile 4、挂载并激活分区swapon /var/swapfile 执行以上命令可能会出现:“不安全的权限 0644,建议使用 0600”类似提示,不要紧张,实际上已经激活了,可以忽略提示,也可以听从系统的建议修改下权限: chmod -R 0600 /var/swapfile 5、查看新swap分区是否正常添加并激活使用free -h 6、修改 fstab 配置,设置开机自动挂载该分区echo \"/var/swapfile swap swap defaults 0 0\" >> /etc/fstab 7、查看是否已经使用了交换内存top 二、更改Swap配置一般默认的情况,当我们开启交换虚拟内存空间后,默认好像是当内存使用百分50的时候,就会开始使用交换空间,这样就会造成一个情况,就是本身物理内存还没有使用完成, 就去使用虚拟内存,这样肯定会影响我们的使用效率,那么我们怎么避免这个情况的发生呢? 答案就是:可以通过swappiness值进行管理,swappiness表示系统对Swap分区的依赖程度,范围是0~100,数值越大,依赖程度越高,也就是越高越会使用Swap分区。 所以,我们现在并不希望我们的机器过度依赖Swap分区,只有当我们 负载超过某个百分比的时候,才使用交换空间,所以这也决定了,我们这个值并不是非常大,一般设置 10 ~50 左右。 当然如果小伙伴的是SSD的话,那么这个值可以稍微大一些。 1、下面我们查看当前的swappiness数值cat /proc/sys/vm/swappiness 2、修改swappiness值,这里以10为例sysctl vm.swappiness=10 3、设置永久有效,重启系统后生效echo \"vm.swappiness = 10\" >> /etc/sysctl.conf 三、swap分区的删除swap分区的删除,仅用于以后删除分区时候使用,如果你现在是增加虚拟内存,那么可以忽略这一步 1、停止正在使用swap分区swapoff /var/swapfile 2、删除swap分区文件rm -rf /var/swapfile 3、删除或注释掉我们之前在fstab文件里追加的开机自动挂载配置内容vim /etc/fstab #把下面内容删除 /var/swapfile swap swap defaults 0 0","categories":[{"name":"linux","slug":"linux","permalink":"https://codey.cc/categories/linux/"}],"tags":[{"name":"linux","slug":"linux","permalink":"https://codey.cc/tags/linux/"}],"author":"不烦"},{"title":"Mysql日志","slug":"Mysql日志","date":"2023-04-29T11:44:07.000Z","updated":"2023-04-29T15:08:40.971Z","comments":true,"path":"p/820530ad7203/","link":"","permalink":"https://codey.cc/p/820530ad7203/","excerpt":"","text":"MySQL 日志:常见的日志都有什么用? MySQL中常见的日志类型主要有下面几类(针对InnoDB存储引擎): 错误日志(error log):对MySQL的启动、运行、关闭过程进行了记录。 二进制日志(binlog):主要记录的是更改数据库的SQL语句。 一般查询日志(general query log):已建立连接的客户端发送给MySQL服务器的所有SQL记录,因为SQL量较大,一般默认不开启,也不建议开启。 慢查询日志(slow query log):执行时间超过 long_query_time秒钟的查询,解决SQL慢查询的时候会用到。 事务日志(redo log 和 undo log):redo log是重做日志,undo log 是回滚日志。 中继日志(relay log):relay log是复制过程中产生的日志,跟binlog类似。不过relay log是针对主从复制的从库。 DDL日志(metadata log):DDL语句执行的元数据操作。 二进制日志(binlog)和事务日志(redo log和 undo log)较为重要,重点关注。 慢查询日志(slow query log)慢查询日志的作用慢查询记录了所有查询时间大于 long_query_time (默认10s,通常设置为1s)的所有查询语句,在解决慢查询的时候会用到。 找到慢SQL是SQL优化的第一步,接下来在用 EXPLAIN 命令分析,获取执行计划的相关信息。 可以通过show variables like "slow_query_log"; 来查看慢查询日志是否开启,默认是关闭的。可以通过SET GLOBAL slow_query_log=ON 开启。通过SHOW VARIABLES LIKE '%long_query_time%' 查看慢查询的配置(默认为10s),SET GLOBAL long_query_time=1 将时间阈值设置为1s。通过show global status like '%Slow_queries%'; 查看慢查询语句的个数。 在实际的项目中,慢查询日志可能较大,直接分析不太方便,可以借助官方的慢查询分析调优工具 mysqldumpslow。 二进制日志(binlog)什么是binlog?binlog(二级制日志文件)主要记录了对MySQL执行更改的所有操作(DDL、DML),包括表结构修改(CREATE、ALERT、DROP TABLE…)、表数据修改(INSERT、UPDATE、DELETE…),但不包括SELECT、SHOW这种不会对数据库修改的操作。 不过并不是不对数据库修改就不会被记录进binlog。即使表结构变更和表数据修改操作并未对数据库造成修改,依然会记录进binlog。 可以通过 show binary logs; 查看所有的二进制日志列表,binlog日志文件一般被命名为文件名.00000* 形式。 可以通过 show binlog events in '文件名.00000*' limit 10; 查看具体内容,也可以通过MySQL内置的binlog查看工具mysqlbinlog解析二进制文件。 binlog通过追加的方式写入,大小没有限制。我们可以通过配置 max_binlog_size 设置每个binlog文件的最大容量,当文件大小大于阈值后会生成新的binlog文件,不会出现覆盖前面日志的情况。 binlog 配置简介 [mysqld] #设置日志三种格式:STATEMENT、ROW、MIXED 。 binlog_format = mixed #设置日志路径,注意路经需要mysql用户有权限写,这里可以写绝对路径,也可以直接写mysql-bin(后者默认就是在/var/lib/mysql目录下) log-bin = /data/mysql/logs/mysql-bin.log #设置binlog清理时间 expire_logs_days = 7 #binlog每个日志文件大小 max_binlog_size = 100m #binlog缓存大小 binlog_cache_size = 4m #最大binlog缓存大小 max_binlog_cache_size = 512m #配置serverid server-id=1 binlog 的格式有哪几种?一般有三种类型的二进制记录方式: Statement模式:每条会修改数据的SQL都会被记录在binlog中,如inserts,updates,deletes。 Row模式(推荐):每一行的具体变更事件都会被记录在binlog中。 Mixed模式:Statement模式和Row模式的混合。默认使用Statement模式,少数特殊场景自动切换到Row模式。 MySQL 5.1.5之前binlog的格式只有Statement模式,5.1.5开始支持Row模式,从5.1.8开始支持Mixed模式,在5.7.7之前默认使用Statement模式。5.7.7版本开始默认使用Row模式。 可以使用 show variables like '%binlog_format%'; 查看binlog使用的格式。 binlog主要用来做什么?binlog主要的应用场景是主从复制,主备、主主、主从都离不开binlog,需要 binlog来同步数据,保证数据的一致性。 主从复制的原理: 主库将数据库中数据变化写入到binlog。 从库连接主库。 从库创建一个I/O线程向主库请求更新的binlog。 主库创建一个binlog dump线程来发送binlog,从库中的I/O线程负责接收。 从库的I/O线程将接收的binlog 写入到relay log中。 从库的SQL线程读取 relay log 同步数据到本地(也就是执行一遍SQL)。 除了主从复制之外,binlog还能帮助我们实现数据恢复。当我们误删数据或数据库的情况下,就可以使用binlog来恢复数据。 binlog的刷盘时机是如何选择的?对于InnoDB存储引擎而言,事务在执行过程中会先将日志写入到binlog cache中,只有事务提交的时候才会把binlog cache中的日志写入到binlog文件中。 因为一个事务的binlog不能被拆开,所以无论这个事务多大,都要确保一次性写入,所以系统会给每个线程分配一块内存作为binlog cache 。我们可以通过 binlog_cache_size 参数控制单个线程的binlog cache 大小,超过了这个参数,就要暂存到磁盘中(Swap)。 可以通过 sync_binlog 参数控制刷盘的时机,取值范围在 0-N,默认为0: 0:不去强制要求,有系统判断何时写入磁盘。 1:每次提交事务都要将binlog写入磁盘。 N:每N个事务,将binlog写入磁盘。 MySQL5.7之前,sync_binlog默认值为0。MySQL5.7之后sync_binlog默认值为1。可以通过 show variables like 'sync_binlog'; 来查看配置。 通常情况下不建议将 sync_binlog设置为0。如果对性能要求较高可以适当调大sync_binlog,不过会增加数据丢失的风险。 什么情况下会重新生成binlog? MySQL服务器停止或重启; 使用flush logs 命令之后; binlog文件大小超过 max_binlog_size 阈值。 redo log(重做日志)redo log 如何保证事务的持久性?InnoDB存储引擎是以页为单位来管理存储空间的。我们向MySQL中插入数据都是存在于页中的,准确点来说是数据页类型,为了减少磁盘I/O开销,在内存中有一个Buffer Pool(缓冲池)的区域,当我们的数据对应的页不在缓冲池中的话,MySQL会先将磁盘中的页缓冲到Buffer Pool中,这样后面的操作就是直接操作Buffer Pool中的页,大大提高了读写性能。 但是会存在一个问题,当一个事务提交后我们对Buffer Pool中页的修改可能还未持久化到磁盘中,这个时候如果MySQL宕机,事务的更改是不是就直接消失了呢? MySQL InnoDB引擎使用了redo log来保证事务的持久性。redo log主要做的事情就是记录页的修改,比如某个页某个偏移量处修改了几个字节的值及具体被修改的内容,redo log中每一条记录包含了表空间号、数据页号、偏移量、具体修改的数据,可能还包含记录修改数据的长度(取决于redo log的类型)。在事务提交时,我们会将redo log按照刷盘的策略写入到磁盘中,这样即使MySQL宕机了,重启之后也能恢复未能写入到磁盘中的数据,从而保证事务的持久性。 不过要注意设置正确的刷盘策略 innodb_flush_log_at_trx_commit ,根据MySQL配置的刷盘策略的不通可能会存在轻微丢失数据的情况。刷盘策略innodb_flush_log_at_trx_commit 的默认值为1,这个时候不会丢失任何数据。我们为了保证事务的持久性必须将该值设为1。 redo log采用循环写的方式进行写入,如图所示: write pos 表示redo log当前记录写到的位置,check point 表示当前要擦除的位置。当write pos 追上check point 时,表示redo log 文件被写满了。这个时候MySQL没办法执行更新操作,也就是数据库更新操作会被阻塞,因为无法写入redo log日志。为了保证MySQL更新操作的正常执行,需要执行CheckPoint刷盘机制。CheckPoint会按照一定的时间将内存中的脏页刷到磁盘中,成功刷盘后,check point 回乡会向后移动(顺时针方向)。这样就能继续写入redo log日志,阻塞的更新操作才能继续执行。 页修改后为什么不直接刷盘?InnoDB页大小一般为16Kb,而页又是磁盘和数据库交互的基本单位,这样就导致即使我们只修改了页中的几个字节,一次刷盘也要将16Kb的整个页都刷新到磁盘中。而且这些修改的页可能还不相邻,也就是说这还是随机I/O。 采用redo log的方式就可以避免这个问题,因为redo log的刷盘性能很好。首先redo log的写入是顺序写入I/O,其次,一行redo log记录只占几十个字节。另外Buffer Pool中的页(脏页)在某些情况下(比如redo log快写满了)也会进行刷盘操作。不过这个的刷盘操作会合并写入,更高效的顺序写入磁盘。 binlog 和redolog的区别 binlog 主要用户数据库还原,属于数据级别的数据恢复,主从复制是最常见的使用场景。redo log 主要用于保证事务的持久性,属于事务级别的数据恢复。 redo log数据InnoDB存储引擎特有的,binlog属于所有存储引擎共有的,因为binlog是MySQL的Server层实现的。 redo log属于物理日志,主要记录某个页的修改。binlog属于逻辑日志,主要记录数据库执行的所有DDL、DML语句。 binlog 通过追加的方式进行写入,大小无限制。redo log采用循环写入,大小固定,当写到结尾时会从开头循环写日志。 …… undo log(撤销日志)undo log 如何保证事务的原子性?每一个事务对数据库的修改都会被记录的undo log中,当事务过程中出现错误,或需要回滚的话,MySQL可以利用undo log将数据恢复到事务开始之前的状态。 undo log 属于逻辑日志,记录的是SQL语句。 除了保证事务的原子性,undo log还有什么作用?InnoDB存储引擎中MVCC的实现用到了undo log。当用户读取一行记录时,若该记录已被其他事务占用,当前事务可以通过undo log读取之前的版本信息,以此实现非锁定读取。","categories":[{"name":"mysql","slug":"mysql","permalink":"https://codey.cc/categories/mysql/"}],"tags":[{"name":"mysql","slug":"mysql","permalink":"https://codey.cc/tags/mysql/"}],"author":"不烦"},{"title":"docker高级","slug":"docker高级","date":"2023-04-29T06:14:07.000Z","updated":"2023-05-14T10:32:42.270Z","comments":true,"path":"p/3582a0555ae1/","link":"","permalink":"https://codey.cc/p/3582a0555ae1/","excerpt":"","text":"一、Dockerfile Dockerfile是用来构建Docker镜像的文本文件,是由一条条构建镜像所需的指令和参数构成的脚本。 构建步骤: 编写Dockerfile文件 docker build命令构建镜像、 docker run依据镜像运行容器实例 1、构建过程1.1、Dockerfile编写: 每条保留字指令都必须为大写字母,且后面要跟随至少一个参数 指令按照从上到下顺序执行 #表示注释 每条指令都会创建一个新的镜像层并对镜像进行提交 1.2、Docker引擎执行Docker的大致流程: docker从基础镜像运行一个容器 执行一条指令并对容器做出修改 执行类似docker commit的操作提交一个新的镜像层 docker再基于刚提交的镜像运行一个新容器 执行Dockerfile中的下一条指令,直到所有指令都执行完成 2、Dockerfile保留字FROM基础镜像,当前新镜像是基于哪个镜像的,指定一个已经存在的镜像作为模板。Dockerfile第一条必须是FROM。 # FROM 镜像名 FROM hub.c.163.com/library/tomcat MAINTAINER镜像维护者的姓名和邮箱地址。 # 非必须 MAINTAINER ZhangSan [email protected] RUN容器构建时需要运行的命令。 有两种格式: shell格式 : # 等同于在终端操作的shell命令 # 格式:RUN <命令行命令> RUN yum -y install vim exec格式 : # 格式:RUN ["可执行文件" , "参数1", "参数2"] RUN ["./test.php", "dev", "offline"] # 等价于 RUN ./test.php dev offline RUN是在docker build时运行 ### EXPOSE 当前容器对外暴露出的端口。 ```dockerfile # EXPOSE 要暴露的端口 # EXPOSE <port>[/<protocol] .... EXPOSE 3306 33060 WORKDIR指定在创建容器后, 终端默认登录进来的工作目录。 ENV CATALINA_HOME /usr/local/tomcat WORKDIR $CATALINA_HOME USER指定该镜像以什么样的用户去执行,如果不指定,默认是root。(一般不修改该配置) # EXPOSE 要暴露的端口 # EXPOSE <port>[/<protocol] .... EXPOSE 3306 33060 ENV用来在构建镜像过程中设置环境变量。 这个环境变量可以在后续的任何RUN指令或其他指令中使用 # 格式 ENV 环境变量名 环境变量值 # 或者 ENV 环境变量名=值 ENV MY_PATH /usr/mytest # 使用环境变量 WORKDIR $MY_PATH VOLUME容器数据卷,用于数据保存和持久化工作。类似于 docker run 的-v参数。 # VOLUME 挂载点 # 挂载点可以是一个路径,也可以是数组(数组中的每一项必须用双引号) VOLUME /var/lib/mysql ADD将宿主机目录下(或远程文件)的文件拷贝进镜像,且会自动处理URL和解压tar压缩包。 COPY类似ADD,拷贝文件和目录到镜像中。 将从构建上下文目录中<源路径>的文件目录复制到新的一层镜像内的<目标路径>位置。 COPY src dest COPY ["src", "dest"] # <src源路径>:源文件或者源目录 # <dest目标路径>:容器内的指定路径,该路径不用事先建好。如果不存在会自动创建 CMD指定容器启动后要干的事情。 有两种格式: shell格式 # CMD <命令> CMD echo "hello world" exec格式 # CMD ["可执行文件", "参数1", "参数2" ...] CMD ["catalina.sh", "run"] 参数列表格式 # CMD ["参数1", "参数2" ....],与ENTRYPOINT指令配合使用 Dockerfile中如果出现多个CMD指令,只有最后一个生效。CMD会被docker run之后的参数替换。 例如,对于tomcat镜像,执行以下命令会有不同的效果: # 因为tomcat的Dockerfile中指定了 CMD ["catalina.sh", "run"] # 所以直接docker run 时,容器启动后会自动执行 catalina.sh run docker run -it -p 8080:8080 tomcat # 指定容器启动后执行 /bin/bash # 此时指定的/bin/bash会覆盖掉Dockerfile中指定的 CMD ["catalina.sh", "run"] docker run -it -p 8080:8080 tomcat /bin/bash CMD是在docker run时运行,而 RUN是在docker build时运行。 ENTRYPOINT用来指定一个容器启动时要运行的命令。 类似于CMD命令,但是ENTRYPOINT不会被docker run后面的命令覆盖,这些命令参数会被当做参数送给ENTRYPOINT指令指定的程序。 ENTRYPOINT可以和CMD一起用,一般是可变参数才会使用CMD,这里的CMD等于是在给ENTRYPOINT传参。 当指定了ENTRYPOINT后,CMD的含义就发生了变化,不再是直接运行期命令,而是将CMD的内容作为参数传递给ENTRYPOINT指令,它们两个组合会变成 ““。 例如: FROM nginx ENTRYPOINT ["nginx", "-c"] # 定参 CMD ["/etc/nginx/nginx.conf"] # 变参 对于此Dockerfile,构建成镜像 nginx:test后,如果执行; docker run nginx test,则容器启动后,会执行 nginx -c /etc/nginx/nginx.conf docker run nginx:test /app/nginx/new.conf,则容器启动后,会执行 nginx -c /app/nginx/new.conf 3、构建镜像创建名称为Dockerfile的文件,示例: FROM ubuntu MAINTAINER zzy<[email protected]> ENV MYPATH /usr/local WORKDIR $MYPATH RUN apt-get update RUN apt-get install net-tools EXPOSE 80 CMD echo $MYPATH CMD echo "install ifconfig cmd into ubuntu success ....." CMD /bin/bash 编写完成之后,将其构建成docker镜像。 命令: # 注意:定义的TAG后面有个空格,空格后面有个点 # docker build -t 新镜像名字:TAG . docker build -t ubuntu:1.0.1 . 4、虚悬镜像虚悬镜像:仓库名、标签名都是 的镜像,称为 dangling images(虚悬镜像)。 在构建或者删除镜像时可能由于一些错误导致出现虚悬镜像。 例如: # 构建时候没有镜像名、tag docker build . 列出docker中的虚悬镜像: docker image ls -f dangling=true 虚悬镜像一般是因为一些错误而出现的,没有存在价值,可以删除: # 删除所有的虚悬镜像 docker image prune 二、Docker网络docker安装并启动服务后,会在宿主机中添加一个虚拟网卡。 在Docker服务启动前,使用 ifconfig 或 ip addr 查看网卡信息: ens33或eth0:本机网卡 lo:本机回环网络网卡 可能有virbr0(CentOS安装时如果选择的有相关虚拟化服务,就会多一个以网桥连接的私网地址的virbr0网卡,作用是为连接虚拟网卡提供NAT访问外网的功能。如果要移除该服务,可以使用 yum remove libvirt-libs.x86_64) 使用 systemctl start docker启动Docker服务后,会多出一个 docker0 网卡。 作用: 容器间的互联和通信以及端口映射 容器IP变动时候可以通过服务名直接网络通信而不受到影响 Docker容器的网络隔离,是通过Linux内核特性 namespace和 cgroup 实现的。 1、Docker网络命令#查看网络模式: docker network ls #如果没有修改过docker network,则默认有3个网络模式: bridge host none #添加docker网络: docker network add xxx #删除Docker网络: docker network rm xxx #查看网络元数据: docker network inspect xxx #删除无效网络: docker network prune 2、Docker 网络模式Docker 的网络模式: 网络模式 简介 使用方式 bridge 为每一个容器分配、设置IP等,并将容器连接到一个docker0虚拟网桥,默认为该模式 –network bridge host 容器将不会虚拟出自己的网卡、配置自己的IP等,而是使用宿主机的IP和端口 –network host none 容器有独立的 Network namespace,但并没有对齐进行任何网络设置,如分配 veth pari和网桥连接、IP等 –network none container 新创建的容器不会创建自己的网卡和配置自己的IP,而是和一个指定的容器共享IP、端口范围等 –network container:NAME或者容器ID 查看某个容器的网络模式: #通过inspect获取容器信息,最后20行即为容器的网络模式信息 docker inspect 容器ID | tail -n 20 三、Docker-compose容器编排1、简介 Docker-Compose 是 Docker 官方的开源项目,负责实现对Docker容器集群的快速编排。 Docker建议我们每个容器中只运行一个服务,因为Docker容器本身占用资源极少,所以最好是将每个服务单独的分割开来。但是如果我们需要同时部署多个服务,每个服务单独构建镜像构建容器就会比较麻烦。所以 Docker 官方推出了 docker-compose 多服务部署的工具。 Compose允许用户通过一个单独的 docker-compose.yml 模板文件来定义一组相关联的应用容器为一个项目(project)。可以很容易的用一个配置文件定义一个多容器的应用,然后使用一条指令安装这个应用的所有依赖,完成构建。 一文件: Docker-Compose可以管理多个Docker容器组成一个应用。需要定义一个yaml格式的配置文件 docker-compose.yml,配置好多个容器之间的调用关系,然后只需要一个命令就能同时启动/关闭这些容器。 两要素: 服务(service):一个个应用容器实例 工程(project):由一组关联的应用容器组成的一个完整业务单元,在docker-compose.yml中定义 Compose使用的三个步骤: 编写 Dockerfile 定义各个应用容器,并构建出对应的镜像文件 编写 docker-compose.yml,定义一个完整的业务单元,安排好整体应用中的各个容器服务 执行 docker-compose up -d 命令,其创建并运行整个应用程序,完成一键部署上线 2、安装Compose# 例如从github下载 2.5.0版本的docker-compose # 下载下来的文件放到 /usr/local/bin目录下,命名为 docker-compose curl -L https://github.com/docker/compose/releases/download/v2.5.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose # 添加权限 chmod +x /usr/local/bin/docker-compose # 验证 docker-compose version 3、常用命令#检查配置 docker-compose config docker-compose config -q # 有问题才输出 #创建并启动docker-compose服务:(类似 docker run) docker-compose up docker-compose up -d #后台运行 #停止并删除容器、网络、卷、镜像:(类似 docker stop + docker rm) docker-compose down #服务 docker-compose /restart重启 /start启动 /stop停止 #进入容器内部 docker-compose exec <yml里面的服务id> /bin/bash #展示当前docker-compose编排过的运行的所有容器 docker-compose ps #展示当前docker-compose编排过的容器进程 docker-compose top #查看容器输出日志 docker-compose log <yml里面的服务id> 4、示例:# docker-compose文件版本号 version: "3" # 配置各个容器服务 services: microService: image: springboot_docker:1.0 container_name: ms01 # 容器名称,如果不指定,会生成一个服务名加上前缀的容器名 ports: - "6001:6001" volumes: - /app/microService:/data networks: - springboot_network depends_on: # 配置该容器服务所依赖的容器服务 - redis - mysql redis: image: redis:6.0.8 ports: - "6379:6379" volumes: - /app/redis/redis.conf:/etc/redis/redis.conf - /app/redis/data:data networks: - springboot_network command: redis-server /etc/redis/redis.conf mysql: image: mysql:5.7 environment: MYSQL_ROOT_PASSWORD: '123456' MYSQL_ALLOW_EMPTY_PASSWORD: 'no' MYSQL_DATABASE: 'db_springboot' MYSQL_USER: 'springboot' MYSQL_PASSWORD: 'springboot' ports: - "3306:3306" volumes: - /app/mysql/db:/var/lib/mysql - /app/mysql/conf/my.cnf:/etc/my.cnf - /app/mysql/init:/docker-entrypoint-initdb.d networks: - springboot_network command: --default-authentication-plugin=mysql_native_password # 解决外部无法访问 networks: # 创建 springboot_network 网桥网络 springboot_network: mysql示例: version: '3' networks: localnet: services: mysql: image: mysql:5.7.36 container_name: mysqldemo networks: - localnet command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_bin command: --init-file /docker-entrypoint-initdb.d/init.sql environment: MYSQL_ROOT_PASSWORD: 123456 TZ: Asia/Shanghai LANG: en_US.UTF-8 networks: - localnet ports: - 3309:3306 volumes: #/docker-entrypoint-initdb.d/ #这个目录是数据库官方提供的初始目录,以.sql .sh .bat结尾的文件放到这个目录下面,在数据库启动的时候会自动执行。 - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql","categories":[{"name":"docker","slug":"docker","permalink":"https://codey.cc/categories/docker/"}],"tags":[{"name":"docker","slug":"docker","permalink":"https://codey.cc/tags/docker/"}],"author":"不烦"},{"title":"text.md","slug":"text-md","date":"2023-04-27T14:45:14.000Z","updated":"2023-05-03T15:14:48.005Z","comments":true,"path":"p/3ee606c7fabf/","link":"","permalink":"https://codey.cc/p/3ee606c7fabf/","excerpt":"","text":"2020-07-24 2.6.6 -> 3.0 如果有 hexo-lazyload-image 插件,需要删除并重新安装最新版本,设置 lazyload.isSPA: true。2.x 版本的 css 和 js 不适用于 3.x 版本,如果使用了 use_cdn: true 则需要删除。2.x 版本的 fancybox 标签在 3.x 版本中被重命名为 gallery 。2.x 版本的置顶 top: true 改为了 pin: true,并且同样适用于 layout: page 的页面。如果使用了 hexo-offline 插件,建议卸载,3.0 版本默认开启了 pjax 服务。 2020-05-15 2.6.3 -> 2.6.6 不需要额外处理。 2020-04-20 2.6.2 -> 2.6.3 全局搜索 seotitle 并替换为 seo_title。group 组件的索引规则有变,使用 group 组件的文章内,group: group_name 对应的组件名必须是 group_name。group 组件的列表名优先显示文章的 short_title 其次是 title。","categories":[],"tags":[],"author":"不烦"},{"title":"docker基础","slug":"docker基础","date":"2023-04-26T17:44:07.000Z","updated":"2023-07-14T15:28:30.844Z","comments":true,"path":"p/10862679c3fa/","link":"","permalink":"https://codey.cc/p/10862679c3fa/","excerpt":"","text":"一、java8 在线安装1、联网下载jdk8: wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/8u131-b11/d54c1d3a095b4ff2b6607d096fa80163/jdk-8u131-linux-x64.rpm # 若提示-bash: wget: command not found 执行yum -y install wget 2、添加执行权限: chmod +x jdk-8u131-linux-x64.rpm 3、 执行rpm进行安装: rpm -ivh jdk-8u131-linux-x64.rpm 4、 java -version: java version "1.8.0_131" Java(TM) SE Runtime Environment (build 1.8.0_131-b11) Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode) 5、编辑环境变量(默认路径)vim /etc/profile export JAVA_HOME=/usr/java/jdk1.8.0_131 #当前jdk路径 export JRE_HOME=${JAVA_HOME}/jre export CLASSPATH=.:JAVAHOME/lib:JAVAHOME/lib:{JRE_HOME}/lib:$CLASSPATH export JAVA_PATH=JAVAHOME/bin:JAVAHOME/bin:{JRE_HOME}/bin 6、强制保存并退出 让profile立即生效: source /etc/profile 7、检查 echo $JAVA_HOME 压缩包安装1、在usr目录下创建一个java文件夹准备放置我们下载好的jdk安装包 mkdir /usr/java 2、把下载好的jdk安装包上传至创建好的目录并解压 tar -zxvf jdk-8u181-linux-x64.tar.gz 3、配置环境变量 vim /etc/profile #加到最后行 export JAVA_HOME=/usr/java/jdk1.8.0_181 export JRE_HOME=${JAVA_HOME}/jre export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib:$CLASSPATH export JAVA_PATH=${JAVA_HOME}/bin:${JRE_HOME}/bin export PATH=$PATH:${JAVA_PATH} 4、使得配置文件生效 source /etc/profile 5、java -version 检查 二、docker1、yum模式1、查看是否已安装docker列表 yum list installed | grep docker 2、安装docker yum -y install docker 3、启动docker systemctl start docker 4、查看docker服务状态 systemctl status docker 5、设置开机自启动 systemctl enable docker 2、离线安装模式 安装包官方地址:https://download.docker.com/linux/static/stable/x86_64/ 1、服务器上使用命令下载 wget https://download.docker.com/linux/static/stable/x86_64/docker-18.06.3-ce.tgz 2、解压 tar -zxvf docker-18.06.3-ce.tgz 3、 将解压出来的docker文件复制到 /usr/bin/ 目录下 cp docker/* /usr/bin/ 4、在/etc/systemd/system/目录下新增docker.service文件**,内容如下,这样可以将docker注册为service服务 [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=127.0.0.1 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 5、此处的--insecure-registry=127.0.0.1(此处改成你私服ip)设置是针对有搭建了自己私服Harbor时允许docker进行不安全的访问,否则访问将会被拒绝。 6、给docker.service文件添加执行权限 chmod +x /etc/systemd/system/docker.service 7、重新加载配置文件(每次有修改docker.service文件时都要重新加载下) systemctl daemon-reload 8、启动docker systemctl start docker 9、设置开机自启动 systemctl enable docker.service 10、查看docker服务状态 systemctl status docker 3、镜像加速sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://q9fvoay0.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-reload sudo systemctl restart docker 4、docker安装常用软件 docker常用指令 docker --help #docker指令大全 docker logs -f *** #查看容器日志 docker inspect *** #查看容器ip docker pull ***:latest #拉取最新镜像 docker pull ***:13.10 #拉取指定版本镜像 docker search *** #搜索镜像 docker images #罗列本机镜像 #REPOSITORY:表示镜像的仓库源 #TAG:镜像的标签 #IMAGE ID:镜像ID #CREATED:镜像创建时间 #SIZE:镜像大小 docker ps -a #罗列本机所有容器 docker start <容器 ID> #启动容器 docker stop <容器 ID> #停止容器 docker restart <容器 ID> #重启容器 docker rm <容器 ID> #删除容器 docker rmi <镜像 ID> #删除镜像 首先删除已使用该镜像的容器 docker run --name nginx -p 8080:80 -d nginx #运行容器 --name nginx:容器名称。 -p 8080:80: 端口进行映射,将本地 8080 端口映射到容器内部的 80 端口。 -P: 随机端口映射,容器内部端口随机映射到主机的端口 -d nginx: 设置容器在在后台一直运行 -i: 以交互模式运行容器,通常与 -t 同时使用 -t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用 --dns 8.8.8.8: 指定容器使用的DNS服务器,默认和宿主一致; --dns-search example.com: 指定容器DNS搜索域名,默认和宿主一致 -h "mars": 指定容器的hostname -e username="ritchie": 设置环境变量 --env-file=[]: 从指定文件读入环境变量 --cpuset="0-2" or --cpuset="0,1,2": 绑定容器到指定CPU运行 -m :设置容器使用内存最大值 --net="bridge": 指定容器的网络连接类型,支持 bridge/host/none/container: 四种类型 --link=[]: 添加链接到另一个容器 --expose=[]: 开放一个端口或一组端口 -v: 绑定一个卷 docker exec -it *** bash #进入容器内部 docker commit [容器id] [镜像id] #容器保存为镜像 netstat -tulp 查看本机端口号 1.Mysql8.01、拉取最新mysql镜像 docker pull mysql:latest 2、创建mysql容器并运行 docker run --name mysql --privileged=true -p 3306:3306 --restart=always -e MYSQL_ROOT_PASSWORD=root -d mysql:latest --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --lower_case_table_names=1 --skip-name-resolve=1 --max_connections=1000 --wait_timeout=31536000 --interactive_timeout=31536000 --default-time-zone='+8:00' #参数说明: #-p 3306:3306 :(指定端口) 对外暴露端口:容器内部端口(内部配置端口) #MYSQL_ROOT_PASSWORD=root:设置 MySQL 服务 root 用户的密码。 3、查看状态 docker ps #进入容器 docker exec -it mysql bash #登录mysql mysql -u root -p ALTER USER 'root'@'localhost' IDENTIFIED BY 'root'; #添加远程登录用户 CREATE USER 'zzy'@'%' IDENTIFIED WITH mysql_native_password BY 'root'; GRANT ALL PRIVILEGES ON *.* TO 'zzy'@'%'; 2.Mysql5.7#创建挂载目录 mkdir -p /usr/mydata/mysql/log mkdir -p /usr/mydata/mysql/data mkdir -p /usr/mydata/mysql/conf touch /usr/mydata/mysql/conf/my.cnf #挂载目录 docker run \\ --name mysql \\ -e MYSQL_ROOT_PASSWORD=root \\ -p 3306:3306 \\ -v /usr/mydata/mysql/log:/var/log/mysql \\ -v /usr/mydata/mysql/data:/var/lib/mysql \\ -v /usr/mydata/mysql/conf:/etc/mysql/conf.d \\ --restart=always -d mysql:5.7.36 #注: #注: docker run -d mysql:5.7.36 以后台的方式运行 mysql 版本的镜像,生成一个容器。 --name mysql 容器名为 mysql -e MYSQL_ROOT_PASSWORD=root 设置登陆密码为root,登陆用户为 root -p 3306:3306 (指定端口) 对外暴露端口:容器内部端口(内部配置端口) -v /usr/mydata/mysql/log:/var/log/mysql 将容器的 日志文件夹 挂载到 主机的相应位置 -v /usr/mydata/mysql/data:/var/lib/mysql 将容器的 数据文件夹 挂载到 主机的相应位置 -v /usr/mydata/mysql/conf:/etc/mysql/conf.d 将容器的 自定义配置文件夹 挂载到主机的相应位置 --restart=aways 重启docker时,自动启动相关容器 #区分大小写 show variables like '%lower_case_table_names%'; docker exec -it mysql /bin/bash apt-get update && apt-get install vim -y vim /etc/mysql/mysql.conf.d/mysqld.cnf #添加一行 lower_case_table_names=1 docker restart mysql my.cnf [mysql] default-character-set=utf8 [client] default-character-set=utf8mb4 [mysqld] #skip-grant-tables symbolic-links=0 explicit_defaults_for_timestamp=true socket=/var/mysqld/mysqld.sock #basedir=/usr/local/mysql datadir=/var/lib/mysql pid-file=/var/mysqld/mysqld.pid #log-error=/var/log/mysqld.log port=3306 character-set-server=utf8 #secure_file_priv= lower_case_table_names=1 skip-external-locking sql_mode=STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION innodb_log_file_size=640M max_allowed_packet=128M default-storage-engine=INNODB innodb_large_prefix=on innodb_file_format=Barracuda max_connections=1000 max_user_connections=500 wait_timeout=200 [mysqld_safe] !includedir /etc/my.cnf.d 3.Redis1、拉取最新redis镜像 docker pull redis:latest 2、运行redis #创建挂载目录 mkdir /usr/mydata/redis mkdir /usr/mydata/redis/data touch /usr/mydata/redis/redis.conf vi /usr/mydata/redis/redis.conf docker run -p 6379:6379 --name redis --restart=always --privileged=true \\ -v /usr/mydata/redis/redis.conf:/etc/redis/redis.conf \\ -v /usr/mydata/redis/data:/data \\ -d redis:latest redis-server /etc/redis/redis.conf #注: -p (指定端口) 对外暴露端口:容器内部端口(redis.conf内部配置端口) -v 挂载目录 将当前宿主机的conf文件挂载到容器conf文件 -v .. 将当前宿主机的data目录挂载到容器的data目录 -d 在后台启动并使用 容器的conf文件 注意!!这里不是宿主机的配置文件,而是容器挂载目录的配置文件, 因为容器做了隔离,没有权限直接读取宿主机的目录, 如果这里配置宿主机的文件,可能会有Fatal error, can't open config file错误 3、查看状态 docker ps 4、redis-cli 连接测试使用 redis 服务。 docker exec -it redis /bin/bash redis-cli redis.conf配置文件 # Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进程 daemonize no # 指定Redis监听端口,默认端口为6379 port 6379 # 绑定的主机地址,不要绑定容器的本地127.0.0.1地址,因为这样就无法在容器外部访问 bind 0.0.0.0 # 持久化 appendonly yes redis.bash配置文件(主要方便执行脚本 /usr/mydata/redis/redis.bash) docker run -p 6379:6379 --name redis -v /usr/mydata/redis/redis.conf:/etc/redis/redis.conf -v /usr/mydata/redis/data:/data -d redis redis-server /usr/mydata/redis/redis.conf 4.Nginx1、拉取最新nginx镜像 docker pull nginx:latest 2、运行nginx docker run --name nginx -p 8080:80 -d nginx 3、查看状态 docker ps #1,创建目录 mkdir -p /root/nginx/{conf,html,logs} #2,随便启动一个nginx实例,只是为了复制出配置 docker run -d --name nginx_dev -p 80:80 nginx:1.10 #3,/root/nginx/conf目录下 docker cp nginx_dev:/etc/nginx . #4,移动conf文件夹 mv nginx/* . rm -rf nginx #5,创建新的Nginx,执行以下命令 docker run -p 80:80 --name mynginx --restart=always \\ -v /root/nginx/html:/usr/share/nginx/html \\ -v /root/nginx/logs:/var/log/nginx \\ -v /root/nginx/conf/:/etc/nginx \\ --privileged=true -d nginx:1.10 5.Apache 直接拉取镜像 1、拉取最新httpd docker pull httpd:latest 2、运行http容器 docker run --name http -p 80:80 -d httpd 3、查看状态 docker ps 使用Dockerfile 构建 1、创建 Dockerfile mkdir -p ~/apache/www ~/apache/logs ~/apache/conf www 目录将映射为 apache 容器配置的应用程序目录。 logs 目录将映射为 apache 容器的日志目录。 conf 目录里的配置文件将映射为 apache 容器的配置文件。 进入创建的 apache 目录,创建 Dockerfile。 FROM debian:jessie # add our user and group first to make sure their IDs get assigned consistently, regardless of whatever dependencies get added #RUN groupadd -r www-data && useradd -r --create-home -g www-data www-data ENV HTTPD_PREFIX /usr/local/apache2 ENV PATH $PATH:$HTTPD_PREFIX/bin RUN mkdir -p "$HTTPD_PREFIX" \\ && chown www-data:www-data "$HTTPD_PREFIX" WORKDIR $HTTPD_PREFIX # install httpd runtime dependencies # https://httpd.apache.org/docs/2.4/install.html#requirements RUN apt-get update \\ && apt-get install -y --no-install-recommends \\ libapr1 \\ libaprutil1 \\ libaprutil1-ldap \\ libapr1-dev \\ libaprutil1-dev \\ libpcre++0 \\ libssl1.0.0 \\ && rm -r /var/lib/apt/lists/* ENV HTTPD_VERSION 2.4.20 ENV HTTPD_BZ2_URL https://www.apache.org/dist/httpd/httpd-$HTTPD_VERSION.tar.bz2 RUN buildDeps=' \\ ca-certificates \\ curl \\ bzip2 \\ gcc \\ libpcre++-dev \\ libssl-dev \\ make \\ ' \\ set -x \\ && apt-get update \\ && apt-get install -y --no-install-recommends $buildDeps \\ && rm -r /var/lib/apt/lists/* \\ \\ && curl -fSL "$HTTPD_BZ2_URL" -o httpd.tar.bz2 \\ && curl -fSL "$HTTPD_BZ2_URL.asc" -o httpd.tar.bz2.asc \\ # see https://httpd.apache.org/download.cgi#verify && export GNUPGHOME="$(mktemp -d)" \\ && gpg --keyserver ha.pool.sks-keyservers.net --recv-keys A93D62ECC3C8EA12DB220EC934EA76E6791485A8 \\ && gpg --batch --verify httpd.tar.bz2.asc httpd.tar.bz2 \\ && rm -r "$GNUPGHOME" httpd.tar.bz2.asc \\ \\ && mkdir -p src \\ && tar -xvf httpd.tar.bz2 -C src --strip-components=1 \\ && rm httpd.tar.bz2 \\ && cd src \\ \\ && ./configure \\ --prefix="$HTTPD_PREFIX" \\ --enable-mods-shared=reallyall \\ && make -j"$(nproc)" \\ && make install \\ \\ && cd .. \\ && rm -r src \\ \\ && sed -ri \\ -e 's!^(\\s*CustomLog)\\s+\\S+!\\1 /proc/self/fd/1!g' \\ -e 's!^(\\s*ErrorLog)\\s+\\S+!\\1 /proc/self/fd/2!g' \\ "$HTTPD_PREFIX/conf/httpd.conf" \\ \\ && apt-get purge -y --auto-remove $buildDeps COPY httpd-foreground /usr/local/bin/ EXPOSE 80 CMD ["httpd-foreground"] 2、Dockerfile文件中 COPY httpd-foreground /usr/local/bin/ 是将当前目录下的httpd-foreground拷贝到镜像里,作为httpd服务的启动脚本,所以我们本地创建一个脚本文件httpd-foreground #!/bin/bash set -e # Apache gets grumpy about PID files pre-existing rm -f /usr/local/apache2/logs/httpd.pid exec httpd -DFOREGROUND 3、赋予 httpd-foreground 文件可执行权限。 chmod +x httpd-foreground 4、通过 Dockerfile 创建一个镜像,替换成你自己的名字。 docker build -t httpd . 5、创建完成后,我们可以在本地的镜像列表里查找到刚刚创建的镜像。 docker images httpd 6、运行http容器 docker run -p 80:80 -v $PWD/www/:/usr/local/apache2/htdocs/ -v $PWD/conf/httpd.conf:/usr/local/apache2/conf/httpd.conf -v $PWD/logs/:/usr/local/apache2/logs/ -d httpd 6.Nacos 单节点启动 #创建挂载文件 mkdir -p /user/mydata/nacos/logs/ mkdir -p /user/mydata/nacos/init.d/ vim /user/mydata/nacos/init.d/custom.properties docker run \\ --name nacos -d \\ -p 8848:8848 \\ --privileged=true \\ --restart=always \\ -e JVM_XMS=256m \\ -e JVM_XMX=256m \\ -e MODE=standalone \\ -e PREFER_HOST_MODE=hostname \\ -v /usr/mydata/nacos/logs:/home/nacos/logs \\ -v /usr/mydata/nacos/init.d/custom.properties:/home/nacos/init.d/custom.properties \\ nacos/nacos-server custom.properties server.contextPath=/nacos server.servlet.contextPath=/nacos server.port=8848 spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://192.167.56.10:3306/nacos_prod?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user=user db.password=1234 nacos.cmdb.dumpTaskInterval=3600 nacos.cmdb.eventTaskInterval=10 nacos.cmdb.labelTaskInterval=300 nacos.cmdb.loadDataAtStart=false management.metrics.export.elastic.enabled=false management.metrics.export.influx.enabled=false server.tomcat.accesslog.enabled=true server.tomcat.accesslog.pattern=%h %l %u %t "%r" %s %b %D %{User-Agent}i nacos.security.ignore.urls=/,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.ico,/console-fe/public/**,/v1/auth/login,/v1/console/health/**,/v1/cs/**,/v1/ns/**,/v1/cmdb/**,/actuator/**,/v1/console/server/** nacos.naming.distro.taskDispatchThreadCount=1 nacos.naming.distro.taskDispatchPeriod=200 nacos.naming.distro.batchSyncKeyCount=1000 nacos.naming.distro.initDataRatio=0.9 nacos.naming.distro.syncRetryDelay=5000 nacos.naming.data.warmup=true nacos.naming.expireInstance=true 持久化sql文件 /* * Copyright 1999-2018 Alibaba Group Holding Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info */ /******************************************/ CREATE TABLE `config_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(255) DEFAULT NULL, `content` longtext NOT NULL COMMENT 'content', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(20) DEFAULT NULL COMMENT 'source ip', `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', `c_desc` varchar(256) DEFAULT NULL, `c_use` varchar(64) DEFAULT NULL, `effect` varchar(64) DEFAULT NULL, `type` varchar(64) DEFAULT NULL, `c_schema` text, `encrypted_data_key` text NOT NULL COMMENT '秘钥', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_aggr */ /******************************************/ CREATE TABLE `config_info_aggr` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(255) NOT NULL COMMENT 'group_id', `datum_id` varchar(255) NOT NULL COMMENT 'datum_id', `content` longtext NOT NULL COMMENT '内容', `gmt_modified` datetime NOT NULL COMMENT '修改时间', `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_beta */ /******************************************/ CREATE TABLE `config_info_beta` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL COMMENT 'content', `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(20) DEFAULT NULL COMMENT 'source ip', `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', `encrypted_data_key` text NOT NULL COMMENT '秘钥', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_info_tag */ /******************************************/ CREATE TABLE `config_info_tag` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', `tag_id` varchar(128) NOT NULL COMMENT 'tag_id', `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL COMMENT 'content', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(20) DEFAULT NULL COMMENT 'source ip', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = config_tags_relation */ /******************************************/ CREATE TABLE `config_tags_relation` ( `id` bigint(20) NOT NULL COMMENT 'id', `tag_name` varchar(128) NOT NULL COMMENT 'tag_name', `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', `nid` bigint(20) NOT NULL AUTO_INCREMENT, PRIMARY KEY (`nid`), UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = group_capacity */ /******************************************/ CREATE TABLE `group_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群', `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值', `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_group_id` (`group_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = his_config_info */ /******************************************/ CREATE TABLE `his_config_info` ( `id` bigint(64) unsigned NOT NULL, `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `data_id` varchar(255) NOT NULL, `group_id` varchar(128) NOT NULL, `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL, `md5` varchar(32) DEFAULT NULL, `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00', `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00', `src_user` text, `src_ip` varchar(20) DEFAULT NULL, `op_type` char(10) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', `encrypted_data_key` text NOT NULL COMMENT '秘钥', PRIMARY KEY (`nid`), KEY `idx_gmt_create` (`gmt_create`), KEY `idx_gmt_modified` (`gmt_modified`), KEY `idx_did` (`data_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造'; /******************************************/ /* 数据库全名 = nacos_config */ /* 表名称 = tenant_capacity */ /******************************************/ CREATE TABLE `tenant_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID', `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数', `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', `gmt_create` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT '2010-05-05 00:00:00' COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表'; CREATE TABLE `tenant_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `kp` varchar(128) NOT NULL COMMENT 'kp', `tenant_id` varchar(128) default '' COMMENT 'tenant_id', `tenant_name` varchar(128) default '' COMMENT 'tenant_name', `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc', `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source', `gmt_create` bigint(20) NOT NULL COMMENT '创建时间', `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info'; CREATE TABLE users ( username varchar(50) NOT NULL PRIMARY KEY, password varchar(500) NOT NULL, enabled boolean NOT NULL ); CREATE TABLE roles ( username varchar(50) NOT NULL, role varchar(50) NOT NULL, constraint uk_username_role UNIQUE (username,role) ); CREATE TABLE permissions ( role varchar(50) NOT NULL, resource varchar(512) NOT NULL, action varchar(8) NOT NULL, constraint uk_role_permission UNIQUE (role,resource,action) ); INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE); INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN'); 👻集群启动 nacos2.0版本之后得打开7848,9848,9849端口两机器之间通信; NACOS_SERVERS:实例节点ip:prot NACOS_SERVER_IP:本机ip(多网卡下的自定义nacos服务器IP) #集群启动 docker run -d --name nacos01 \\ -p 8848:8848 \\ -p 7848:7848 \\ -p 9848:9848 \\ -p 9849:9849 \\ --privileged=true \\ --restart=always \\ -e PREFER_HOST_MODE=hostname \\ -e NACOS_SERVERS="192.168.56.101:8848" \\ -e NACOS_SERVER_IP=192.168.56.10 \\ -e SPRING_DATASOURCE_PLATFORM=mysql \\ -e MYSQL_SERVICE_HOST=192.168.56.10 \\ -e MYSQL_SERVICE_PORT=3306 \\ -e MYSQL_SERVICE_DB_NAME=nacos_prod \\ -e MYSQL_SERVICE_USER=root \\ -e MYSQL_SERVICE_PASSWORD=1234 \\ -e MYSQL_DATABASE_NUM=1 \\ -e JVM_XMS=256m \\ -e JVM_XMX=256m \\ -e JVM_XMN=256m \\ -e MODE=cluster nacos/nacos-server:v2.1.0-BETA 👍 docker run -d --name nacos02 \\ -p 8848:8848 \\ -p 7848:7848 \\ -p 9848:9848 \\ -p 9849:9849 \\ --privileged=true \\ --restart=always \\ -e PREFER_HOST_MODE=hostname \\ -e NACOS_SERVERS="192.168.56.10:8848" \\ -e NACOS_SERVER_IP=192.168.56.101 \\ -e SPRING_DATASOURCE_PLATFORM=mysql \\ -e MYSQL_SERVICE_HOST=192.168.56.10 \\ -e MYSQL_SERVICE_PORT=3306 \\ -e MYSQL_SERVICE_DB_NAME=nacos_prod \\ -e MYSQL_SERVICE_USER=root \\ -e MYSQL_SERVICE_PASSWORD=1234 \\ -e MYSQL_DATABASE_NUM=1 \\ -e JVM_XMS=256m \\ -e JVM_XMX=256m \\ -e JVM_XMN=256m \\ -e MODE=cluster nacos/nacos-server:v2.1.0-BETA 7.Elasticsearch和Kibana1.下载镜像文件# 存储和检索数据 docker pull elasticsearch:7.1 # 可视化检索数据 docker pull kibana:7.4.2 2.配置挂载数据文件夹# 创建配置文件目录 mkdir -p /usr/mydata/elasticsearch/config # 创建数据目录 mkdir -p /usr/mydata/elasticsearch/data # 创建插件目录 mkdir -p /usr/mydata/elasticsearch/plugins # 将/mydata/elasticsearch/文件夹中文件都可读可写 chmod -R 777 /usr/mydata/elasticsearch/ # 配置任意机器可以访问 elasticsearch echo "http.host: 0.0.0.0" >/usr/mydata/elasticsearch/config/elasticsearch.yml 3. 启动Elasticsearchdocker run --name elasticsearch --restart=always -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e ES_JAVA_OPTS="-Xms64m -Xmx128m" \\ -v /usr/mydata/elasticsearch/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml \\ -v /usr/mydata/elasticsearch/data:/usr/share/elasticsearch/data \\ -v /usr/mydata/elasticsearch/plugins:/usr/share/elasticsearch/plugins \\ --privileged=true -d elasticsearch:7.10.1 4.启动可视化Kibanadocker run --name kibana --restart=always -e ELASTICSEARCH_HOSTS=http://192.168.56.10:9200 --privileged=true -p 5601:5601 -d kibana:7.4.2 8.rabbitmqdocker run -p 5672:5672 -p 15672:15672 --name rabbitmq --restart=always --privileged=true -d rabbitmq:3.7.15 进入容器并开启管理功能: docker exec -it rabbitmq /bin/bash rabbitmq-plugins enable rabbitmq_management #### 9.sentinel ```sh docker pull bladex/sentinel-dashboard:1.7.0 docker run --name sentinel -d -p 8858:8858 --restart=always --privileged=true bladex/sentinel-dashboard:1.7.0 10.seatamkdir -p /usr/mydata/seata/seata-config #该目录下存放registry.conf 和 file.conf两个文件,将被挂载到seata容器中相应的目录 mkdir -p /usr/mydata/seata/seata-config 该目录下存放registry.conf 和 file.conf两个文件,将被挂载到seata容器中相应的目录 registry.conf: registry { # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa type = "nacos" nacos { serverAddr = "192.168.56.10:8848" namespace = "public" cluster = "default" } eureka { serviceUrl = "http://localhost:1001/eureka" application = "default" weight = "1" } redis { serverAddr = "localhost:6379" db = "0" } zk { cluster = "default" serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } consul { cluster = "default" serverAddr = "127.0.0.1:8500" } etcd3 { cluster = "default" serverAddr = "http://localhost:2379" } sofa { serverAddr = "127.0.0.1:9603" application = "default" region = "DEFAULT_ZONE" datacenter = "DefaultDataCenter" cluster = "default" group = "SEATA_GROUP" addressWaitTime = "3000" } file { name = "file.conf" } } config { # file、nacos 、apollo、zk、consul、etcd3 type = "file" nacos { serverAddr = "localhost" namespace = "public" cluster = "default" } consul { serverAddr = "127.0.0.1:8500" } apollo { app.id = "seata-server" apollo.meta = "http://192.168.1.204:8801" } zk { serverAddr = "127.0.0.1:2181" session.timeout = 6000 connect.timeout = 2000 } etcd3 { serverAddr = "http://localhost:2379" } file { name = "file.conf" } } file.conf transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #vgroup->rgroup vgroup_mapping.my_test_tx_group = "default" #only support single node default.grouplist = "127.0.0.1:8091" #degrade current not support enableDegrade = false #disable disable = false #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" } client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 } ## transaction log store store { ## store mode: file、db mode = "file" ## file store file { dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions max-branch-session-size = 16384 # globe session size , if exceeded throws exceptions max-global-session-size = 512 # file buffer size , if exceeded allocate new buffer file-write-buffer-cache-size = 16384 # when recover batch read size session.reload.read_size = 100 # async, sync flush-disk-mode = async } ## database store db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "dbcp" ## mysql/oracle/h2/oceanbase etc. db-type = "mysql" url = "jdbc:mysql://127.0.0.1:3306/seata" user = "mysql" password = "mysql" min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } } lock { ## the lock store mode: local、remote mode = "remote" local { ## store locks in user's database } remote { ## store locks in the seata's server } } recovery { committing-retry-delay = 30 asyn-committing-retry-delay = 30 rollbacking-retry-delay = 30 timeout-retry-delay = 30 } transaction { undo.data.validation = true undo.log.serialization = "jackson" } ## metrics settings metrics { enabled = false registry-type = "compact" # multi exporters use comma divided exporter-list = "prometheus" exporter-prometheus-port = 9898 } 启动命令: docker run -d --name seata-server \\ -p 8091:8091 \\ -e SEATA_IP=192.168.56.10 \\ -e SEATA_PORT=8091 \\ -e STORE_MODE=file \\ --network=host \\ -e SEATA_CONFIG_NAME=file:/root/seata-config/registry \\ -v /usr/mydata/seata/seata-config:/root/seata-config \\ --privileged=true \\ docker.io/seataio/seata-server:1.2.0 5、idea连接docker一键部署1.修改配置文件,打开2375端口: vim /usr/lib/systemd/system/docker.service #直接替换某行(版本可能不一样,根据前面区分ExecStart=/usr/bin/dockerd*) ExecStart=/usr/bin/dockerd-current -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock \\ ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock #重新加载配置文件和启动 systemctl daemon-reload systemctl restart docker #直接curl看是否生效 curl http://127.0.0.1:2375/info #或者安装netstat命令工具查看配置的端口号(2375)是否开启 yum install -y net-tools netstat -nlpt #解决集成插件乱码问题 #找到IDEA安装目录的bin目录,在idea.exe.vmoptions和idea64.exe.vmoptions文件中追加以下内容: -Dfile.encoding=utf-8 #新版本的IDEA中,可能还需要在菜单栏Help -> Edit Custom VM Options中追加以上内容,IDEA会首先以该文件为准 2.idea安装docker插件(Docker integration)并配置: TCP socket tcp://101.132.134.165:2375 3.配置pom文件: <build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.*</include> </includes> </resource> <resource> <directory>src/main/webapp</directory> <targetPath>META-INF/resources</targetPath> <includes> <include>**/*.*</include> </includes> </resource> </resources> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> </configuration> </plugin> <!-- 跳过单元测试 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <skipTests>true</skipTests> </configuration> </plugin> <!--使用docker-maven-plugin插件--> <plugin> <groupId>com.spotify</groupId> <artifactId>docker-maven-plugin</artifactId> <version>1.2.2</version> <!--将插件绑定在某个phase执行--> <executions> <execution> <id>build-image</id> <!--用户只需执行mvn package ,就会自动执行mvn docker:build--> <phase>package</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <!--指定远程 docker api地址--> <dockerHost>http://192.168.56.10:2375</dockerHost> <!--指定生成的镜像名--> <imageName>${project.groupId}/${project.artifactId}</imageName> <!--指定标签--> <!--<imageTags> &lt;!&ndash;获取版本号&ndash;&gt; <imageTag>${project.version}</imageTag> </imageTags>--> <!-- 指定 Dockerfile 路径--> <dockerDirectory>${project.basedir}</dockerDirectory> <!-- 这里是复制 jar 包到 docker 容器指定目录配置 --> <resources> <resource> <targetPath>/</targetPath> <!--jar 包所在的路径 此处配置的 即对应 target 目录--> <directory>${project.build.directory}</directory> <!-- 需要包含的 jar包 ,这里对应的是 Dockerfile中添加的文件名 --> <include>${project.build.finalName}.jar</include> </resource> </resources> </configuration> </plugin> </plugins> </build> 4.根目录下编写Dockerfile ### 基础镜像,使用alpine操作系统,openjkd使用8u201 FROM openjdk:8u201-jdk-alpine3.9 #作者 MAINTAINER zzy<[email protected]> #系统编码 ENV LANG=C.UTF-8 LC_ALL=C.UTF-8 #设置时区UTC+8 #RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime RUN apk --update add tzdata && \\ cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \\ echo "Asia/Shanghai" > /etc/timezone && \\ apk del tzdata && \\ rm -rf /var/cache/apk/* ##声明一个挂载点,容器内此路径会对应宿主机的某个文件夹tomcat缓冲区 VOLUME /tmp #应用构建成功后的jar文件被复制到镜像内,名字也改成了app.jar ADD target/demo_springboot.jar app.jar #启动容器时的进程 ENTRYPOINT ["java","-jar","-Xms100M","-Xmx100M","app.jar","--spring.profiles.active=prod"] #暴露5001端口 EXPOSE 5001 5.根目录下编写docker-compose.yml version: '2' services: guli-fast: image: guli/fast ports: - "8080:8080" environment: - spring.profiles.active=dev 6、Dockerfile Dockerfile是由一系列命令和参数构成的脚本,这些命令应用于基础镜像并最终创 建一个新的镜像。 1、对于开发人员:可以为开发团队提供一个完全一致的开发环境; 2、对于测试人员:可以直接拿开发时所构建的镜像或者通过Dockerfile文件构建一 个新的镜像开始工作了; 3、对于运维人员:在部署时,可以实现应用的无缝移植 命令 作用 FROM image_name:tag 定义了使用哪个基础镜像启动构建流程 MAINTAINER user_name 声明镜像的创建者 ENV key value 设置环境变量 (可以写多条) RUN command 是Dockerfile的核心部分(可以写多条) ADD source_dir/file dest_dir/file 将宿主机的文件复制到容器内,如果是一个压缩文件,将会在复制后自动解压 COPY source_dir/file dest_dir/file 和ADD相似,但是如果有压缩文件并不能解压 WORKDIR path_dir 设置工作目录 注意:COPY 和 ADD 命令不能拷贝上下文之外的本地文件 链接 在使用 docker build 命令通过 Dockerfile 创建镜像时,会产生一个 build 上下文(context)。所谓的 build 上下文就是 docker build 命令的 PATH 或 URL 指定的路径中的文件的集合。在镜像 build 过程中可以引用上下文中的任何文件,比如我们要介绍的 COPY 和 ADD 命令,就可以引用上下文中的文件。 默认情况下 docker build -t test1 . 命令中的 . 表示 build 上下文为当前目录。当然我们可以指定一个目录作为上下文,比如下面的命令: docker build -t test1 /home/jkc 我们指定/home/jkc目录为build上下文,默认情况下 docker 会使用在上下文的根目录下找到的 Dockerfile 文件。 创建简单的jdk1.8的Dockerfile #依赖镜像名称和ID FROM centos:7 #指定镜像创建者信息 MAINTAINER zhangzy #切换到jdk安装包工作目录 WORKDIR /usr RUN mkdir /usr/local/java #ADD 是相对路径,把java添加到容器中 ADD jdk-8u181-linux-x64.tar.gz /usr/local/java/ #配置java环境变量 ENV JAVA_HOME /usr/local/java/jdk1.8.0_181 ENV JRE_HOME $JAVA_HOME/jre ENV PATH $JAVA_HOME/bin:$PATH 7、docker卸载1、卸载Docker,旧版本docker没安装成功直接卸载掉。 查看安装过的包:yum list installed | grep docker 本机安装过的旧版本:docker.x86_64,docker-client.x86_64,docker-common.x86_64 删除安装的Docker相关的软件包: yum -y remove docker.x86_64 yum -y remove docker-client.x86_64 yum -y remove docker-common.x86_64 8、docker-compose# docker-compose curl -L https://get.daocloud.io/docker/compose/releases/download/1.25.5/docker-compose-`uname -s`-`uname -m` > /usr/bin/docker-compose chmod +x /usr/bin/docker-compose docker-compose -v 9、docker网络 容器与容器之间相互通信的网络 查看docker创建的网络:docker network ls 创建bridge网络:docker network create -d bridge my_bridge 将已存在的容器加入my_bridge网络:docker network connect my_bridge 容器id 查看指定的网络链接信息:docker network inspect my_bridge 创建容器的时候约定链接指定网络:--network my_bridge 使得某容器链接至指定容器:--link 容器id 总结: 1.当我们新建容器时,如果没有显示指定其使用的网络,那么默认会使用bridge网络2.当一个容器link到另一个容器时,该容器可以通过IP或容器名称访问被link的容器,而被link容器可以通过IP访问该容器,但是无法通过容器名称访问3.当被link的容器被删除时,创建link的容器也无法正常使用4.如果两个容器被加入到我们手动创建的网络时,那么该网络内的容器相互直接可以通过IP和名称同时访问。 三、git和maven1、安装git yum -y install git 2、安装maven mkdir -p /opt/maven tar -xzvf apache-maven-3.6.3-bin.tar.gz mv apache-maven-3.6.3/* /opt/maven #配置阿里云镜像 vim conf/settings.xml <mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>aliyunrepositoty</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> #配置环境变量 vim /etc/profile export MAVEN_HOME=/opt/maven export PATH=$PATH:$JAVA_HOME/bin:$MAVEN_HOME/bin source /etc/profile mvn -v 四、jenkins1.检查卸载 cd /root #卸载之前残留的jenkins rpm -e jenkins find / -iname jenkins | xargs -n 1000 rm -rf #查看是否卸载完毕 rpm -ql jenkins 2.jenkins默认安装的配置目录在:/etc/sysconfig/jenkins sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo --no-check-certificate sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key --no-check-certificate yum -y install epel-release yum -y install jenkins 3.配置 #安装完毕,进入到jenkins配置文件内,配置端口及用户名 vim /etc/sysconfig/jenkins #找到这两行,修改成指定的端口 JENKINS_USER="用户名" #示例: root JENKINS_PORT="端口号" #示例: 9999 #启动jenkins服务 systemctl start jenkins #查看启动状态 systemctl status jenkins #如果报错 Starting Jenkins File "/usr/bin/java" is not executable. #查看当前Java的环境变量 echo $JAVA_HOME vim /etc/init.d/jenkins #在/usr/bin/java下添加 /usr/java/jdk1.8.0_181/bin/java #/usr/java/jdk1.8.0_181是Java的环境变量 systemctl daemon-reload systemctl start jenkins systemctl status jenkins 4.通用配置 #首次登陆查找密码 cat /var/lib/jenkins/secrets/initialAdminPassword #选择安装推荐插件 #创建管理员账号 全选root #Jenkins URL: http://192.168.56.10:9999/ #点击高级升级站点 https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json #重启 systemctl restart jenkins #安装汉化插件 Jenkins->Manage Jenkins->Manage Plugins,点击Available,搜索”Chinese” #安装Credentials Binding插件 #安装Pipeline插件 #配置jdk和maven分别找到对应的环境变量地址 echo $JAVA_HOME echo $MAVEN_HOME #配置Jenkins关联环境变量和设置常量 JAVA_HOME MAVEN_HOME PATH_EXTRA","categories":[{"name":"docker","slug":"docker","permalink":"https://codey.cc/categories/docker/"}],"tags":[{"name":"docker","slug":"docker","permalink":"https://codey.cc/tags/docker/"}],"author":"不烦"},{"title":"RabbitMq浅析","slug":"RabbitMq浅析","date":"2023-04-26T11:44:07.000Z","updated":"2023-04-29T07:00:06.476Z","comments":true,"path":"p/f45df1cd3575/","link":"","permalink":"https://codey.cc/p/f45df1cd3575/","excerpt":"","text":"一、简介 RabbitMQ是实现了高级消息队列协议(AMQP)的开源消息代理软件(亦称面向消息的中间件)。RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的 前置问题: 什么是消息队列? MQ全称为Message Queue,即消息队列。“消息队列”是在消息的传输过程中保存消息的容器。它是典型的:生产者、消费者模型。生产者不断向消息队列中生产消息,消费者不断的从队列中获取消息。因为消息的生产和消费都是异步的,而且只关心消息的发送和接收,没有业务逻辑的侵入,这样就实现了生产者和消费者的解耦。 有哪些应用场景? 异步解耦: 最常见的一个场景是用户注册后,需要发送注册邮件和短信通知,以告知用户注册成功 ,将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理,MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。 流量削峰: 一般在秒杀或团队抢购(高并发)活动中使用广泛,由于用户请求量较大,导致流量暴增,秒杀的应用在处理如此大量的访问流量后,下游的通知系统无法承载海量的调用量,甚至会导致系统崩溃等问题而发生漏通知的情况。为解决这些问题,可在应用和下游通知系统之间加入消息队列 MQ。 有哪些常见MQ产品? ActiveMQ:基于JMS RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好 RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会 Kafka:分布式消息系统,高吞吐量 1、RabbitMQ快速入门 RabbitMQ官方地址:http://www.rabbitmq.com 1.1、安装 window原生安装较为繁琐(需要 Erlang 环境),所以docker示例 使用Docker命令启动服务; docker run -p 5672:5672 -p 15672:15672 --name rabbitmq \\ -d rabbitmq:3.7.15 进入容器并开启管理功能; docker exec -it rabbitmq /bin/bash rabbitmq-plugins enable rabbitmq_management 1.2、配置访问RabbitMQ管理页面地址,查看是否安装成功(Linux下使用服务器IP访问即可):http://localhost:15672/ 输入账号密码并登录,这里使用默认账号密码登录:guest guest 创建帐号并设置其角色为管理员:mall mall 创建一个新的虚拟host为:/mall  消费者接收消息流程: 1、消费者和Broker建立TCP连接 2、消费者和Broker建立通道 3、消费者监听指定的Queue(队列) 4、当有消息到达Queue时Broker默认将消息推送给消费者。 5、消费者接收到消息。 6、ack回复 2、五种消息模型2.1、基本消息模型 简单模式是最简单的消息模式,它包含一个生产者、一个消费者和一个队列。生产者向队列里发送消息,消费者从队列中获取消息并消费 。 模式示意图: 在上图的模型中,有以下概念: P:生产者,也就是要发送消息的程序 C:消费者:消息的接受者,会一直等待消息到来。 queue:消息队列,图中红色部分。可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。 先创建一个队列: public static final String QUEUE_SIMPLE = "queue_simple"; //声明队列 @Bean(QUEUE_SIMPLE) public Queue helloQueue() { return new Queue(QUEUE_SIMPLE); } 再创建一个消费者: public static final String QUEUE_SIMPLE = "queue_simple"; @RabbitListener(queues = QUEUE_SIMPLE) public void process(String massage) { System.out.println("基本消息模型:Receiver : " + massage); } 再创建一个生产者: @Test public void sendSimpleMq() { String massage = sdf.format(new Date()) + " 简单信息模型"; //队列+massage rabbitTemplate.convertAndSend(MqSimpleConfig.QUEUE_SIMPLE, massage); System.out.println("Sent------>" + massage); } 测试成功 2.2、work消息模型 工作模式是指向多个互相竞争的消费者发送消息的模式,它包含一个生产者、两个消费者和一个队列。两个消费者同时绑定到一个队列上去,当消费者获取消息处理耗时任务时,空闲的消费者从队列中获取并消费消息 。 模式示意图: 两个消费端共同消费同一个队列中的消息,但是一个消息只能被一个消费者获取。 接下来模拟这个流程: P:生产者:任务的发布者 C1:消费者1:领取任务并且完成任务,假设完成速度较慢(模拟耗时) C2:消费者2:领取任务并且完成任务,假设完成速度较快 先创建一个队列: public static final String QUEUE_WORK = "queue_work"; @Bean public Queue queueWork() { return new Queue(QUEUE_WORK); } 再创建两个消费者: public static final String QUEUE_WORK = "queue_work"; @RabbitListener(queues = QUEUE_WORK) public void process1(String neo) { System.out.println("工作信息模型 Receiver 1: " + neo); } @RabbitListener(queues = QUEUE_WORK) public void process2(String neo) { System.out.println("工作信息模型 Receiver 2: " + neo); 再创建一个生产者: //work @Test public void sendWorkMq() throws InterruptedException { //队列+massage for (int i = 0; i <= 10; i++) { String massage = sdf.format(new Date()) + " 工作信息模型"; Thread.sleep(1000); rabbitTemplate.convertAndSend(MqWorkConfig.QUEUE_WORK, massage + i); System.out.println("Sent------>" + massage + i); } } 小结: 2.3、广播消息模型 发布/订阅模式是指同时向多个消费者发送消息的模式(类似广播的形式),它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列绑定到交换机上去,生产者通过发送消息到交换机,所有消费者接收并消费消息。 模式示意图: 在上图的模型中和之前类似,其他有以下概念: 一个生产者多个消费者 每个消费者都有一个自己的队列 生产者没有将消息直接发送给队列,而是发送给exchange(交换机、转发器) 每个队列都需要绑定到交换机上 生产者发送的消息,经过交换机到达队列,实现一个消息被多个消费者消费 例子:注册–>发邮件、发短信 Exchange类型有以下几种: Fanout:广播,将消息交给所有绑定到交换机的队列 Direct:定向,把消息交给符合指定routing key 的队列 Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列 Header:header模式与routing不同的地方在于,header模式取消routingkey,使用header中的 key/value(键值对)匹配队列 注:Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失! 接下来模拟这个流程: P:生产者:任务的发布者 C1:消费者1:领取任务并且完成任务,假设完成速度较慢(模拟耗时) C2:消费者2:领取任务并且完成任务,假设完成速度较快 先创建3个队列: public static final String QUEUE_FANOUT_A = "queue_fanoutA"; public static final String QUEUE_FANOUT_B = "queue_fanoutB"; public static final String QUEUE_FANOUT_C = "queue_fanoutC"; public static final String EXCHANGE = "fanoutExchange"; /* * 声明3个队列 */ @Bean(QUEUE_FANOUT_A) public Queue AMessage() { return new Queue(QUEUE_FANOUT_A); } @Bean(QUEUE_FANOUT_B) public Queue BMessage() { return new Queue(QUEUE_FANOUT_B); } @Bean(QUEUE_FANOUT_C) public Queue CMessage() { return new Queue(QUEUE_FANOUT_C); } /* * 声明交换机 */ @Bean(EXCHANGE) FanoutExchange fanoutExchange() { return new FanoutExchange(EXCHANGE); } /* * 把队列绑定至交换机 */ @Bean Binding bindingExchangeA(@Qualifier(QUEUE_FANOUT_A) Queue AMessage, @Qualifier(EXCHANGE) FanoutExchange fanoutExchange) { return BindingBuilder.bind(AMessage).to(fanoutExchange); } @Bean Binding bindingExchangeB(@Qualifier(QUEUE_FANOUT_B) Queue BMessage, @Qualifier(EXCHANGE) FanoutExchange fanoutExchange) { return BindingBuilder.bind(BMessage).to(fanoutExchange); } @Bean Binding bindingExchangeC(@Qualifier(QUEUE_FANOUT_C) Queue CMessage, @Qualifier(EXCHANGE) FanoutExchange fanoutExchange) { return BindingBuilder.bind(CMessage).to(fanoutExchange); } 再创建3个消费者: public static final String QUEUE_FANOUT_A = "queue_fanoutA"; public static final String QUEUE_FANOUT_B = "queue_fanoutB"; public static final String QUEUE_FANOUT_C = "queue_fanoutC"; @RabbitListener(queues = QUEUE_FANOUT_A) public void processA(String message) { System.out.println("广播消息模型 fanout Receiver A: " + message); } @RabbitListener(queues = QUEUE_FANOUT_B) public void processB(String message) { System.out.println("广播消息模型 fanout Receiver B: " + message); } @RabbitListener(queues = QUEUE_FANOUT_C) public void processC(String message) { System.out.println("广播消息模型 fanout Receiver C: " + message); 再创建一个生产者: //fanout @Test public void sendFanoutMq() throws InterruptedException { //队列+massage for (int i = 0; i <= 10; i++) { String massage = sdf.format(new Date()) + " 广播信息模型"; Thread.sleep(1000); rabbitTemplate.convertAndSend(MqFanoutConfig.EXCHANGE,"",massage + i); System.out.println("Sent------>" + massage + i); } } 2.4、路由模型 路由模式是可以根据路由键选择性给多个消费者发送消息的模式,它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列通过路由键绑定到交换机上去,生产者发送消息到交换机,交换机通过路由键转发到不同队列,队列绑定的消费者接收并消费消息。 模式示意图: 2.5、Topic通配符模型 通配符模式是可以根据路由键匹配规则选择性给多个消费者发送消息的模式,它包含一个生产者、两个消费者、两个队列和一个交换机。两个消费者同时绑定到不同的队列上去,两个队列通过路由键匹配规则绑定到交换机上去,生产者发送消息到交换机,交换机通过路由键匹配规则转发到不同队列,队列绑定的消费者接收并消费消息 。 模式示意图: 每个消费者监听自己的队列,并且设置带统配符的routingkey,生产者将消息发给broker,由交换机根据routingkey来转发消息到指定的队列。 Routingkey一般都是有一个或者多个单词组成,多个单词之间以“.”分割,例如:inform.sms 通配符规则: #:匹配一个或多个词 *:匹配一个词 示例: audit.#:能够匹配audit.irs.corporate 或者 audit.irs audit.*:只能匹配audit.irs 从示意图可知,我们将发送所有描述动物的消息。消息将使用由三个字(两个点)组成的Routing key发送。路由关键字中的第一个单词将描述速度,第二个颜色和第三个种类:“..”。 我们创建了三个绑定: Q1绑定了*.orange.* Q2绑定了*.*.rabbit和lazy.# Q1匹配所有的橙色动物。 Q2匹配关于兔子以及懒惰动物的消息。 接下来模拟这个流程: 详看三步骤。 二、Springboot整合RibbitMQ下面还是模拟注册服务当用户注册成功后,向短信和邮件服务推送消息的场景(Topic) 创建两个工程 mq-rabbitmq-producer和mq-rabbitmq-consumer,分别配置1、2、3(第三步本例消费者用注解形式,可以不用配) 1、生产者(mq-producer)1.1、添加AMQP的启动器:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <!--springboot测试包,方便测试--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring‐boot‐starter‐test</artifactId> </dependency> 1.2、在application.yml中添加RabbitMQ的配置:server: port: 10086 spring: application: name: mq-rabbitmq-producer rabbitmq: host: 192.168.1.103 port: 5672 username: kavito password: 123456 virtualHost: /kavito template: retry: enabled: true initial-interval: 10000ms max-interval: 300000ms multiplier: 2 exchange: topic.exchange publisher-confirms: true 属性说明: template:有关AmqpTemplate的配置 retry:失败重试 enabled:开启失败重试 initial-interval:第一次重试的间隔时长 max-interval:最长重试间隔,超过这个间隔将不再重试 multiplier:下次重试间隔的倍数,此处是2即下次重试间隔是上次的2倍 exchange:缺省的交换机名称,此处配置后,发送消息如果不指定交换机就会使用这个 publisher-confirms:生产者确认机制,确保消息会正确发送,如果发送失败会有错误回执,从而触发重试 *当然如果consumer只是接收消息而不发送,就不用配置template相关内容。 1.3、定义RabbitConfig配置类,配置Exchange、Queue、及绑定交换机。@Configuration public class RabbitmqConfig { public static final String QUEUE_EMAIL = "queue_email";//email队列 public static final String QUEUE_SMS = "queue_sms";//sms队列 public static final String EXCHANGE_NAME="topic.exchange";//topics类型交换机 public static final String ROUTINGKEY_EMAIL="topic.#.email.#"; public static final String ROUTINGKEY_SMS="topic.#.sms.#"; //声明交换机 @Bean(EXCHANGE_NAME) public Exchange exchange(){ //durable(true) 持久化,mq重启之后交换机还在 return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build(); } //声明email队列 /* * new Queue(QUEUE_EMAIL,true,false,false) * durable="true" 持久化 rabbitmq重启的时候不需要创建新的队列 * auto-delete 表示消息队列没有在使用时将被自动删除 默认是false * exclusive 表示该消息队列是否只在当前connection生效,默认是false */ @Bean(QUEUE_EMAIL) public Queue emailQueue(){ return new Queue(QUEUE_EMAIL); } //声明sms队列 @Bean(QUEUE_SMS) public Queue smsQueue(){ return new Queue(QUEUE_SMS); } //ROUTINGKEY_EMAIL队列绑定交换机,指定routingKey @Bean public Binding bindingEmail(@Qualifier(QUEUE_EMAIL) Queue queue, @Qualifier(EXCHANGE_NAME) Exchange exchange){ return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_EMAIL).noargs(); } //ROUTINGKEY_SMS队列绑定交换机,指定routingKey @Bean public Binding bindingSMS(@Qualifier(QUEUE_SMS) Queue queue, @Qualifier(EXCHANGE_NAME) Exchange exchange){ return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_SMS).noargs(); } } 为了方便测试,我直接把生产者代码放工程测试类:发送routing key是”topic.sms.email”的消息,那么mq-rabbitmq-consumer下那些监听的(与交换机(topic.exchange)绑定,并且订阅的routingkey中匹配了”topic.sms.email”规则的) 队列就会收到消息。 //topic @Test public void sendTopicMq() throws InterruptedException { for (int i = 0; i < 5; i++) { Thread.sleep(1000); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String massage = "恭喜你在" + sdf.format(new Date()) + "注册成功,userId= " + UUID.randomUUID().toString().substring(0, 5) + i; rabbitTemplate.convertAndSend(MqTopicConfig.EXCHANGE_NAME, "topic.sms.email", massage); System.out.println("Sent------>" + massage); } } 运行测试类发送5条消息: web管理界面: 可以看到已经创建了交换机以及queue_email、queue_sms 2个队列,并且向这两个队列分别发送了5条消息 : 2、消费者(mq-consumer)编写一个监听器组件,通过注解配置消费者队列,以及队列与交换机之间绑定关系。(也可以像生产者那样通过配置类配置) 在SpringAmqp中,对消息的消费者进行了封装和抽象。一个JavaBean的方法,只要添加@RabbitListener注解,就可以成为了一个消费者。 @Component public class ReceiveHandler { //监听邮件队列 @RabbitListener(bindings = @QueueBinding( value = @Queue(value = "queue_email", durable = "true"), exchange = @Exchange( value = "topic.exchange", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC ), key = {"topic.#.email.#","email.*"})) public void rece_email(String msg){ System.out.println(" [邮件服务] received : " + msg + "!"); } //监听短信队列 @RabbitListener(bindings = @QueueBinding( value = @Queue(value = "queue_sms", durable = "true"), exchange = @Exchange( value = "topic.exchange", ignoreDeclarationExceptions = "true", type = ExchangeTypes.TOPIC ), key = {"topic.#.sms.#"})) public void rece_sms(String msg){ System.out.println(" [短信服务] received : " + msg + "!"); } } 属性说明: @RabbitListener:方法上的注解,声明这个方法是一个消费者方法,需要指定下面的属性: bindings:指定绑定关系,可以有多个。值是@QueueBinding的数组。@QueueBinding包含下面属性: value:这个消费者关联的队列。值是@Queue,代表一个队列 exchange:队列所绑定的交换机,值是@Exchange类型 key:队列和交换机绑定的RoutingKey,可指定多个","categories":[{"name":"rabbitmq","slug":"rabbitmq","permalink":"https://codey.cc/categories/rabbitmq/"}],"tags":[{"name":"中间件","slug":"中间件","permalink":"https://codey.cc/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"}],"author":"不烦"},{"title":"Nginx浅析","slug":"Nginx浅析","date":"2023-04-25T12:44:07.000Z","updated":"2023-05-01T09:07:46.987Z","comments":true,"path":"p/7c4c0b8793a3/","link":"","permalink":"https://codey.cc/p/7c4c0b8793a3/","excerpt":"","text":"一、Nginx1、安装:1.1、docker安装#1,创建目录 mkdir -p /root/nginx/{conf,html,logs} #2,随便启动一个nginx实例,只是为了复制出配置 docker run -d --name nginx_dev -p 80:80 nginx:1.10 #3,/root/nginx/conf目录下 docker cp nginx_dev:/etc/nginx . #4,移动conf文件夹 mv nginx/* . rm -rf nginx #5,创建新的Nginx,执行以下命令 docker run -p 80:80 --name mynginx --restart=always \\ -v /root/nginx/html:/usr/share/nginx/html \\ -v /root/nginx/logs:/var/log/nginx \\ -v /root/nginx/conf/:/etc/nginx \\ --privileged=true -d nginx:1.10 1.2、普通安装 官网下载#安装nginx环境 yum install -y gcc-c++ yum install -y pcre pcre-devel yum install -y zlib zlib-devel yum install -y openssl openssl-devel #官网下载nginx-1.14.0.tar.gz tar -zxvf nginx-1.14.0.tar.gz #接着进入到解压之后的目录,进行编译安装。 ./configure --prefix=/usr/local/nginx make make install #进入到 sbin 目录(读取默认路径下的配置文件:nginx/conf/nginx.conf) 常用命令: ./nginx 启动 ./nginx -s stop 停止 ./nginx -s reload 重新加载配置文件并重启 ./nginx -t -c /usr/local/nginx/conf/nginx.conf 检查配置文件 ./nginx -c tmpnginx.conf 指定配置文件启动 2、nginx.conf#user nobody; worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; #gzip on; server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; location / { root 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's document root # concurs with nginx'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; # } #} } 开头的表示注释内容,我们去掉所有以 # 开头的段落,精简之后的内容如下: worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; server { listen 80; server_name localhost; location / { root html; index index.html index.htm; } error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } } 配置文件中的内容(包含三部分): 全局块:配置服务器整体运行的配置指令 events 块:影响 Nginx 服务器与用户的网络连接 http 块 2.1、全局块 从配置文件开始到 events 块之间的内容,主要会设置一些影响 nginx 服务器整体运行的配置指令,主要包括配 置运行 Nginx 服务器的用户(组)、允许生成的 worker process 数,进程 PID 存放路径、日志存放路径和类型以 及配置文件的引入等。 worker_processes 1; 例如: 这是 Nginx 服务器并发处理服务的关键配置,worker_processes 值越大,可以支持的并发处理量也越多,但是 会受到硬件、软件等设备的制约 2.2、events 块 events 块涉及的指令主要影响 Nginx 服务器与用户的网络连接,常用的设置包括是否开启对多 work process 下的网络连接进行序列化,是否允许同时接收多个网络连接,选取哪种事件驱动模型来处理连接请求,每个 word process 可以同时支持的最大连接数等。 events { worker_connections 1024; } 上述例子就表示每个 work process 支持的最大连接数为 1024,这部分的配置对 Nginx 的性能影响较大,在实际中应该灵活配置。 2.3、http 块 http段是由http相关模块支持的。以下是默认配置项。注意,http根段下使用相对路径是相对conf目录的,如”include extra/.conf”表示的是conf/extra/.conf;非根段内的相对路径如server段内使用相对路径是相对于的安装目录的,例如nginx安装在/usr/local/nginx下,当location中的root设置为html时,它表示的路径是*/usr/local/nginx/html/*。 需要注意的是:http 块也可以包括 http全局块、server 块。 http { include mime.types; # nginx支持的媒体文件类型。相对路径为同目录conf下的其他文件 default_type application/octet-stream; # 默认的媒体类型 #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # 访问日志的格式 # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; # 启用sendfile传输模式,此模式是"零拷贝" #tcp_nopush on; # 只在sendfile on时有效。让数据包挤满到一定程度才发送出去,挤满之前被阻塞 #keepalive_timeout 0; # keepalive的超时时间 keepalive_timeout 65; #gzip on; # 是否启用gzip压缩响应报文 server { # 定义虚拟主机 listen 80; # 定义监听套接字 server_name localhost; # 定义主机名加域名,即网站地址 #charset koi8-r; # 默认字符集 #access_log logs/host.access.log main; # 访问日志路径 location / { # location容器,即URI的根 root html; # 站点根目录,即DocumentRoot,相对路径时为/html index index.html index.htm; # 站点主页文件 } location /image/ { root html/data/image; #站点根目录 autoindex on; #列出当前目录的内容 } #error_page 404 /404.html; # 出现404 page not fount错误时,使用/404.html页响应客户端 # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; # 出现50x错误时,使用/50x.html页返回给客户端 location = /50x.html { # 定义手动输入包含/50x.html时的location root html; } # deny access to .htaccess files, if Apache's document root # concurs with nginx'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; # } #} } 2.3.1、http 全局块http全局块配置的指令包括文件引入、MIME-TYPE 定义、日志自定义、连接超时时间、单链接请求数上限等。 2.3.2、server 块这块和虚拟主机有密切关系,虚拟主机从用户角度看,和一台独立的硬件主机是完全一样的,该技术的产生是为了节省互联网服务器硬件成本。后面会详细介绍虚拟主机的概念。每个 http 块可以包括多个 server 块,而每个 server 块就相当于一个虚拟主机。而每个 server 块也分为全局 server 块,以及可以同时包含多个 locaton 块。 全局 server 块 最常见的配置是本虚拟机主机的监听配置和本虚拟主机的名称或IP配置。 location 块 一个 server 块可以配置多个 location 块。这块的主要作用是基于 Nginx 服务器接收到的请求字符串(例如 server_name/uri-string),对虚拟主机名称(也可以是IP别名)之外的字符串(例如 前面的 /uri-string)进行匹配,对特定的请求进行处理。地址定向、数据缓存和应答控制等功能,还有许多第三方模块的配置也在这里进行。 listen该指令用于配置网络监听。主要有如下三种配置语法结构: 一、配置监听的IP地址 listen address[:port] [default_server] [setfib=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [deferred] [accept_filter=filter] [bind] [ssl]; 二、配置监听端口 listen port[default_server] [setfib=number] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [ipv6only=on|off] [ssl]; 三、配置 UNIX Domain Socket listen unix:path [default_server] [backlog=number] [rcvbuf=size] [sndbuf=size] [accept_filter=filter] [deferred] [bind] [ssl]; listen *:80 | *:8080 #监听所有80端口和8080端口 listen IP_address:port #监听指定的地址和端口号 listen IP_address #监听指定ip地址所有端口 listen port #监听该端口的所有IP连接 root和alias root指令设置站点根目录,即httpd的documentroot,但又有所不同,因为nginx可以在多个上下文位置处使用root指令,例如Location容器中。 如果配置如下: location /i/ { root /data/w3; } 那么nginx将使用文件/data/w3/i/top.gif响应请求”/i/top.gif”。 root指令仅仅只是将匹配的URI追加在root路径后,如果要改变URI,应该使用alias指令,它会对URI进行替换。例如: location /i/ { alias /data/w3/images/; } 那么nginx将使用文件/data/w3/images/top.gif响应请求/i/top.gif。因此,如果alias指令的路径最后一部分包含了URI,则最好使用root指令,而非alias指令,虽然它们都能成功响应。 location /images/ { alias /data/w3/images/; } location /images/ { root /data/w3/; } 它们都能使用相对路径,相对的是prefix。例如编译路径为/usr/local/nginx,则”root html”指的是”/usr/local/nginx/html”。 与root和alias指令相关的变量为$document_root、$realpath_root。其中$document_root的值即是root指令、alias指令的值,而$realpath_root的值是对root、alias指令进行绝对路径换算后的值。 location容器该指令对规范化后的URI进行匹配,并对匹配的路径封装一系列指令。 语法: location [ = | ~ | ~* | ^~ ] uri { ... } location /uri/ {}:表示对/uri/目录及其子目录下的所有文件都匹配。所以”location / {}”的匹配范围是最大的。 location = /uri/ {}:表示只对目录或文件进行匹配,不对目录中的文件和子目录进行匹配。所以一般只用来做文件匹配。 location ~ /uri/ {}:表示区分大小写的正则匹配。 location ~* /uri/ {}:表示不区分大小写的正则匹配。 location ^~ /uri/ {}:表示禁用正则匹配,即精确字符串匹配,此时正则中的元字符被解释成普通字符。 它们的匹配优先级规则为:nginx先检查URI的前缀路径,在这些路径中找到最精确匹配请求URI的路径。然后nginx按在配置文件中的出现顺序检查正则表达式路径,匹配上某个路径后即停止匹配并使用该路径的配置,否则使用最大前缀匹配的路径的配置。 使用”=”前缀可以定义URI和路径的精确匹配。如果发现匹配,则终止路径查找。例如请求”/“很频繁,定义”location = /“可以提高这些请求的处理速度,因为查找过程在第一次比较以后即结束。 以下是一个优先级的示例。 location = / { [ configuration A ] } location / { [ configuration B ] } location /documents/ { [ configuration C ] } location ^~ /images/ { [ configuration D ] } location ~* \\.(gif|jpg|jpeg)$ { [ configuration E ] } 记一次错误: #假设nginx端口号为8000,html文件夹下有ss文件夹,其之下有a.png; location /ss/ { root /usr/share/nginx/html; index index.html index.htm; } #则访问192.168.10.56:8000/ss/a.png可以访问得到 #也就是当匹配到url路径有ss,就在root相对路径后面直接加ss,也就是访问这个路径/usr/share/nginx/html/ss/ location /ss/ { root /usr/share/nginx/html/ss; index index.html index.htm; } #此时访问192.168.10.56:8000/ss/a.png访问不到,除非添加文件ss/ss/a.png;因为访问路径已经变为/usr/share/nginx/html/ss/ss/ location /ss/ { alias /usr/share/nginx/html; index index.html index.htm; } #使用alias绝对路径访问192.168.10.56:8000/ss/a.png访问不到,因为此时是绝对路径,访问路径也就是/usr/share/nginx/html,而html文件夹下没有a.png location /ss/ { alias /usr/share/nginx/html/ss; index index.html index.htm; } #使用alias绝对路径访问192.168.10.56:8000/ss/a.png访问不到,因为没有加/usr/share/nginx/html/ss最后没有加/,注意alias需要识别/,需要改为 #location /ss/ { # alias /usr/share/nginx/html/ss/; # index index.html index.htm; #} location ss { alias /usr/share/nginx/html/ss; index index.html index.htm; } #使用alias绝对路径访问192.168.10.56:8000/ss/a.png可以访问得到,因为上面没有加/,下面可以识别 ###注意:上面不加/,root和alias下面加不加都可以; 上面加/,alias下面必须加,root加不加都可以 proxy_pass 指令用于设置被代理服务器的地址。可以是主机名称、IP地址加端口号的形式。 语法结构如下: proxy_pass URL; URL 为被代理服务器的地址,可以包含传输协议、主机名称或IP地址加端口号,URI等。 proxy_pass http://www.123.com/uri; index 该指令用于设置网站的默认首页。 语法为: index filename ...; 后面的文件名称可以有多个,中间用空格隔开。 index index.html index.jsp; 通常该指令有两个作用:第一个是用户在请求访问网站时,请求地址可以不写首页名称;第二个是可以对一个请求,根据请求内容而设置不同的首页。 error_page 当出现对应状态码的错误时,指定返回的URI路径。语法为: error_page code ... [=[response]] uri; 配置文件中的error_page部分默认为: location / { root html; index index.html index.htm; } #error_page 404 /404.html; error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } 上面的配置文件中,假如取消了404错误的error_page行注释,当出现404错误时,其uri为/404.html,然后会对其进行location的匹配,由于只有”location / {}”能匹配到,所以它的目录为/html/,即404.html文件路径为/html/404.html。对于50x的error_page,其uri为”/50x.html”,所以会对其进行location匹配,发现可以精确匹配到”location = /50x.html {}”,当然”location / {}”也能匹配到,但是它的优先级更低,所以当出现50x错误时,将从/html目录下寻找50x.html,这里正好和”location / {}”重复了,但它们的匹配过程是不一样的。假如改为如下配置: location / { root html; index index.html index.htm; } #error_page 404 /404.html; error_page 500 502 503 504 /50x.html; location = /50x.html { root /www/a.com/; } 出现50x错误时,将返回/www/a.com/50x.html文件,而不再是/html/50x.html。 allow和deny这两个指令由ngx_http_access_module模块提供,用于允许或限制某些IP地址的客户端访问。nginx中的allow和deny规则很简单,从上向下匹配,只要匹配到就停止。例如: allow 10.0.0.8 allow 192.168.100.0/24 deny all 允许10.0.0.8和192.168.100网段的访问,其他的都拒绝。 add_header 用于在响应首部中添加字段。例如: server { add_header RealPath $realpath_root; } 将添加一个名为RealPath的字段,值为变量realpath_root的值。 [root@xuexi ~]# curl -I http://localhost/ HTTP/1.1 200 OK Server: nginx/1.10.3 Date: Tue, 17 Oct 2017 08:10:14 GMT Content-Type: text/html Content-Length: 612 Last-Modified: Tue, 17 Oct 2017 03:20:10 GMT Connection: keep-alive ETag: "59e576ea-264" RealPath: /usr/local/nginx-1.12.1/html # 此为自定义添加字段 Accept-Ranges: bytes 2.3.3 server_name nginx使用server容器定义一个虚拟主机。在nginx中,没有严格区分基于IP和基于名称的虚拟主机,它们通过listen指令和server_name指令结合起来形成不同的虚拟主机。 例如: # 基于IP地址的虚拟主机 server { listen 80; server_name 192.168.100.25; location / { root /www/longshuai/; index index.html index.htm; } } server { listen 80; server_name 192.168.100.26; location / { root /www/xiaofang/; index index.html index.htm; } } # 基于名称的虚拟主机 server { listen 80; server_name www.longshuai.com; location / { root /www/longshuai/; index index.html index.htm; } } server { listen 80; server_name www.xiaofang.com; location / { root /www/xiaofang/; index index.html index.htm; } } # 基于端口的虚拟主机 server { listen 80; server_name 192.168.100.25; location / { root /www/longshuai/; index index.html index.htm; } } server { listen 8080; server_name 192.168.100.25; location / { root /www/xiaofang/; index index.html index.htm; } } 其中server_name指令可以定义多个主机名,第一个名字为虚拟主机的首要主机名。例如: server_name example.com www.example.com; 主机名中可以含有星号(’*‘),以替代名字的开始部分或结尾部分(只能是起始或结尾,如果要实现中间部分的通配,可以使用正则表达式)。例如”*.example.org”不仅匹配www.example.org,也匹配www.sub.example.org。下面两条指令是等价的。 server_name example.com *.example.com www.example.*; server_name .example.com www.example.*; 也可以在主机名中使用正则表达式,就是在名字前面补一个波浪线(“~”): server_name www.example.com ~^www\\d+\\.example\\.com$; nginx允许定义空主机名: server { listen 80; server_name ""; return 444; } 这种主机名可以让虚拟主机处理没有”Host”首部的请求,而不是让指定”地址:端口”的默认虚拟主机来处理,而这正是本指令的默认设置。即使用非默认的虚拟主机处理请求头中不含”Host”字段的请求。一般这样的请求处理方式是直接丢弃请求,并返回一个非标准的状态码来立即关闭连接,例如上面的444。 3、反向代理范例:使用 nginx 反向代理www.123.com直接跳转到127.0.0.1:8080 启动一个 tomcat,浏览器地址栏输入 127.0.0.1:8080,出现tomcat初始页面 通过修改本地 host 文件,将 www.123.com 映射到 127.0.0.1 127.0.0.1 www.123.com 在 nginx.conf 配置文件中增加如下配置: server { listen 80; server_name www.123.com; location / { proxy_pass http://127.0.0.1:8080; index index.html index.htm index.jsp; } } 如上配置,我们监听80端口,访问域名为www.123.com,不加端口号时默认为80端口,故访问该域名时会跳转到127.0.0.1:8080路径上,成功访问到tomcat初始页面 通过nginx代理端口,原先访问的是8080端口,通过nginx代理之后,通过80端口就可以访问了 4、负载均衡 Nginx 服务器是介于客户端和服务器之间的中介,客户端发送的请求先经过 Nginx ,然后通过 Nginx 将请求根据相应的规则分发到相应的服务器。而且 Nginx 提供了几种分配方式(策略) 1.1、普通轮询算法 每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器 down 掉,能自动剔除 upstream myserver { server 192.168.56.10:8081; server 192.168.56.10:8082; } server { listen 80; server_name 192.168.56.10; location / { root html; proxy_pass http://myserver; index index.html index.htm; } 1.2、weight weight 代表权重, 默认为 1,权重越高被分配的客户端越多 upstream myserver { server 192.168.56.10:8081 weight=10; server 192.168.56.10:8082 weight=10; } server { listen 80; server_name 192.168.56.10; location / { root html; proxy_pass http://zzyserver; index index.html index.htm; } 1.3、ip_hash ip_hash 每个请求按访问 ip 的 hash 结果分配,这样每个访客固定访问一个后端服务器 upstream myserver { ip_hash; server 208.208.128.122:8081 ; server 208.208.128.122:8082 ; } server { listen 80; server_name 208.208.128.122; location / { root html; proxy_pass http://myserver; index index.html index.htm; } 1.4、fair(第三方) fair(第三方),按后端服务器的响应时间来分配请求,响应时间短的优先分配。 upstream myserver { server 208.208.128.122:8081 ; server 208.208.128.122:8082 ; fair; } server { listen 80; server_name 208.208.128.122; location / { root html; proxy_pass http://myserver; index index.html index.htm; } 5、Nginx原理1、 mater 和 worker nginx 启动后,是由两个进程组成的。master(管理者)和worker(工作者)。 一个nginx 只有一个master。但可以有多个worker 过来的请求由master管理,worker进行争抢式的方式去获取请求。 2、master-workers 的机制的好处 首先,对于每个 worker 进程来说,独立的进程,不需要加锁,所以省掉了锁带来的开销, 同时在编程以及问题查找时,也会方便很多。 可以使用 nginx –s reload 热部署,利用 nginx 进行热部署操作 其次,采用独立的进程,可以让互相之间不会 影响,一个进程退出后,其它进程还在工作,服务不会中断,master 进程则很快启动新的 worker 进程。当然,worker 进程的异常退出,肯定是程序有 bug 了,异常退出,会导致当 前 worker 上的所有请求失败,不过不会影响到所有请求,所以降低了风险。 3、设置多少个 workerNginx 同 redis 类似都采用了 io 多路复用机制,每个 worker 都是一个独立的进程,但每个进 程里只有一个主线程,通过异步非阻塞的方式来处理请求, 即使是千上万个请求也不在话 下。每个 worker 的线程可以把一个 cpu 的性能发挥到极致。所以 worker 数和服务器的 cpu 数相等是最为适宜的。设少了会浪费 cpu,设多了会造成 cpu 频繁切换上下文带来的损耗。 worker 数和服务器的 cpu 数相等是最为适宜 4、连接数 worker_connection第一个:发送请求,占用了 woker 的几个连接数? 答案:2 或者 4 个 第二个:nginx 有一个 master,有四个 woker,每个 woker 支持最大的连接数 1024,支持的 最大并发数是多少? 普通的静态访问最大并发数是: worker_connections * worker_processes /2, 而如果是 HTTP 作 为反向代理来说,最大并发数量应该是 worker_connections * worker_processes/4。","categories":[{"name":"nginx","slug":"nginx","permalink":"https://codey.cc/categories/nginx/"}],"tags":[{"name":"nginx","slug":"nginx","permalink":"https://codey.cc/tags/nginx/"}],"author":"不烦"}],"categories":[{"name":"mysql","slug":"mysql","permalink":"https://codey.cc/categories/mysql/"},{"name":"java","slug":"java","permalink":"https://codey.cc/categories/java/"},{"name":"ChatGPT","slug":"ChatGPT","permalink":"https://codey.cc/categories/ChatGPT/"},{"name":"docker","slug":"docker","permalink":"https://codey.cc/categories/docker/"},{"name":"rabbitmq","slug":"rabbitmq","permalink":"https://codey.cc/categories/rabbitmq/"},{"name":"linux","slug":"linux","permalink":"https://codey.cc/categories/linux/"},{"name":"nginx","slug":"nginx","permalink":"https://codey.cc/categories/nginx/"}],"tags":[{"name":"mysql","slug":"mysql","permalink":"https://codey.cc/tags/mysql/"},{"name":"java","slug":"java","permalink":"https://codey.cc/tags/java/"},{"name":"ChatGPT","slug":"ChatGPT","permalink":"https://codey.cc/tags/ChatGPT/"},{"name":"docker","slug":"docker","permalink":"https://codey.cc/tags/docker/"},{"name":"中间件","slug":"中间件","permalink":"https://codey.cc/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"},{"name":"linux","slug":"linux","permalink":"https://codey.cc/tags/linux/"},{"name":"nginx","slug":"nginx","permalink":"https://codey.cc/tags/nginx/"}]}