阅读视图

战争与AI

感谢订阅陶其的个人博客!

本来按习惯,闲扯淡的文章一般都配个封面图的,但是我用AI半天都没有生成我想要的效果,加上网太卡,就放弃了,大家将就看。

好久没冒泡了,在这里先祝大家 元宵节快乐

真的好久没有冒泡了,转眼都2026年3月了,上一篇还是在2025年11月份发的。

主要是我在2025年11月份上旬失业了,这个时间点很尴尬,不仅错过了金九银十,甚至都快过年了,特别是在这种大环境不是那么好的情况下。

从11月上旬失业到我又找到工作,整整2个月。

中间其实有几家不错的,但是可能是我要价比其他人高一些,所以就错过了。

其实也不高,甚至比我之前的工资还低一点,但是我发现我们行业找工作的人都开始大幅自降薪资找工作。

这一点就很难受,真的就是内卷卷上瘾了,也不能这么说,这个年头薪资多少先不论,大家都想先有一个收入,其次再谈收入的多少。

最终我也是加入了这一行列,大幅降薪找了个初创公司的全栈岗位。

没招儿,家庭生活花销、娃的花销、以及房贷等等,都不可能等我慢慢找份好的工作的,其实两个月已经很久了。

我想之前那些降薪找工作的人也都有类似的苦衷吧。

算了,糟心的事就不多聊了。

反正这篇是闲谈,所以我想聊聊近期比较火热的话题:AI和战争。

我不是专业的军事博主,也没有什么一手的新闻资料,我所有的消息都是通过互联网获取的,所以你就全当我是闲得无聊在扯犊子。


先聊AI。

最近这一年,AI大模型的迭代速度是越来越快了,从AI刚出来的时候只能当成一个进阶版的聊天助手,到现在已经大范围的入侵互联网各个角落,用时不到三年。

近期人工智能领域,文生文的千问、豆包、Gemini;全模态的字节的Seedance2.0和SeedDream5.0、国外的Sora等纷至沓来,让人一时间眼花缭乱的。

甚至因为字节的豆包手机的偷袭事件,现在各个互联网派系都在开始打通自己的派系和自己大模型了。

比如现在千问可以直接通过语音交流点外卖、订机票了,就是打通了淘宝闪购(饿了么)、飞猪等平台。

感觉现在的时代越来越像未来了。

但是对应的,监管难度、行为习惯、信息茧房等等也会进一步加剧。

试想一下,当我习惯了动动口让大模型软件帮我点外卖后,由于我没有亲自参与选购环节,如果大模型给我订的并非最优惠的或者没有领取到最佳优惠时,或者其他平台同款餐品更便宜时,我们并不知道。

资本家,从来不会在乎个人的习惯和喜好,只会关注利润增长和资金入账,什么能让资本更赚钱,资本就会更加做什么。

有人说,人工智能的进化带来了第四次工业革命。

我只能说,屁。

第一次工业革命,蒸汽机的改良与广泛应用,让人类社会进入了蒸汽时代;
第二次工业革命,电力普及让人类社会进入电气时代;
第三次工业革命,计算机和互联网的普及,让整个人类社会进入信息化与自动化的时代。
但是,
如果说人工智能,是人类的第四次工业革命,那要看看人工智能带来了什么。

蒸汽时代、电气时代、信息化时代,这三次工业革命,将人类带入到三个全新的时代,最大的特点就是提高了人们的工作效率

但是人工智能时代,却是替代了人在这个社会的价值,或者换句话说,人工智能革命革掉的是人本身,而不是落后的技术。

比如,我们以服装厂为例,我感觉这个很有代表性。

在第一次工业革命之前,养殖场采集了羊毛之后,送去纺织厂,纺织女工们用纺纱机,先把羊毛纺成线,然后再织成布,然后再裁剪成衣服,再缝制。

在第一次工业革命之后,人力纺纱机变成了蒸汽纺纱机,只需要很少的几位工人,就可以维护好几台纺纱机的运行,只要有煤炭,这些纺纱机的效率就是人力的几十几百倍。

蒸汽让“珍妮纺纱机”彻底成为了历史。

第一次蒸汽革命,让很多人失去了工作,远远不只有纺织厂,但是这也让整个社会生产效率提升了,人们被迫去转行学习更具技术性的工作,力工的地位得到了削减。

第二次工业革命,让社会进去电气时代。

最大的特点就是,机械化的东西更多了,因为能源的改变,从蒸汽变成了电气。只要有电,工厂就能运行。

而电线比蒸汽管道更细,电路板和按钮比阀门更好控制,那么就能造出更多行业更多功能的机械母机。所以第二次工业革命后,大量的机械化工厂和流水线工厂纷纷落地。重复的劳动力的社会地位落到了底层。

由于流水线的出现,生产效率得到了进一步的提升,现在只有在一些机械还无法替代的,需要人的主观能动性的环节还需要人类员工之外,一切都在流水线上由机械自动完成。

第三次工业革命,让社会进入信息化时代。

信息化时代,最大的特点并不是更进一步的提高工厂的生产效率,而是提高了信息交换速度。

什么意思,比如之前即将进入冬天了,工厂的销售出去找经销商,为工厂拉来了10万件的销量,那么工厂的生产额就是10万件,剩下的时间因为没有拉来订单,即使生产出来也是堆在仓库。

但是进入信息化时代,各种电商平台的崛起,让工厂扩展了更多销售的渠道,可能通过一场直播,让天南海北的成千上万人看到自己工厂优质便宜的衣服。进而扩大工厂的销量,通过这种方式提高整个社会的沟通效率和生产总量。

这个时间很多人都已经从工厂走出,去参与到电商的运营、物流配送、线上客服等因为新时代而造就的新岗位上去了。

那么我们回过头来看人工智能。

人工智能带来了什么。

其实我们日常接触到的常见人工智能,是电脑上的文生各种的大模型,比如文生文、文生视频、图片生视频等。

但是人工智能其实早已扩展到我们的各行各业了。

比如现在路上越来越多的无人快递车、港口的无人物流车、马斯克准备推出的无人货运卡车、小米的黑灯造车工厂(因为无人所以不需要开灯)、炒菜机器人、机器人服务员等等。

你发现没,人工智能带来的更多的是“无人”的东西。

前三次工业革命,从人力到蒸汽,再从蒸汽到电气。信息从车马飞鸽到光纤瞬达。这些虽然也会带来当时时代人们的大量失业,但是同时也会造就一些新的岗位。

但是人工智能,更多是在替代人类本身,将人力成本压缩到极致。

你可能暂时没有感觉,或者感觉可能还有人们没有发现的新岗位等着人们去开发。

但是,你再仔细想一想。

农业上,依靠人工智能和北斗GPS等技术的大规模机械化无人化的翻地、播种、种植、养护、采摘、打包、运输,这些大规模的种植基地,其每亩种植成本早已低于农民在土地里面朝黄土背朝天整日整日辛勤劳作的成本,而科学化的种植带来的亩产也比小规模人力种植更高,而且这一套技术已经非常成熟了。

要不是因为中国各地都有耕地红线政策,以及中国为了照顾海量的农民而推出的三农政策,恐怕也早已像美国那样,土地都沦落到少数的农场主的手里,而原来的农民早已没了土地,也没了唯一的退路。

工业上,先进的技术和人工智能的智慧,让越来越多的工厂不再需要那么多的人力,甚至黑灯工厂也越来越多。

第三产业,服务业。我查了一下,我国服务业主要有15个行业门类,可以分为几个大类:

  • 流通服务类:批发和零售业、交通运输业、仓储和邮政业;
  • 生产性服务类:信息传输软件和信息技术服务业、金融业、租赁和商务服务业、科学研究和技术服务业;
  • 生活性服务类:住宿和餐饮业、房地产业、居民服务修理和其他服务业、教育、卫生和社会工作、文化体育和娱乐业;
  • 公共及其他服务类:水利环境和公共设施管理业、公共管理社会保障和社会组织、国际组织。

这么看不直观,我让AI给我总结服务业最常见、人数最多的前 10 类岗位:

  • 餐饮服务类:餐馆服务员、厨师、外卖员
  • 零售销售类:商场导购、超市店员、便利店员工
  • 住宿服务类:酒店前台、客房服务员
  • 物流快递类:快递员、分拣员、配送员
  • 交通运输类:网约车司机、出租车司机、公交司机
  • 家政服务类:保洁员、保姆、月嫂、护工
  • 美容美发类:理发师、美容师、美甲师
  • 教育培训类:教师、培训机构助教、辅导员
  • 医疗健康类:护士、药店店员、理疗师
  • 文体娱乐类:影院工作人员、导游、健身教练

哎?这么一看就很直观了,这都是我们身边经常能遇见的社会上的工作人员。

那么,你再仔细看看,这些岗位里哪些正在被人工智能、自动化、机器人所替代?

中央厨房统一标准的低价流水线产品正在挤压小餐馆的活路;
无人网约车也在逐步蚕食网约车司机的收入;
现在专业的AI诊疗大模型甚至比一些刚入行的医生给出的治疗方案更优质一些。

有人会说,人工智能替代了我们原本的岗位,我们可以去寻找那些更具价值的岗位啊?

比如呢?作家?画家?艺术家?

我们老百姓眼中的人工智能或者机器人应该是去代替人们去完成重复性的、危险性的、劳累的工作,让人们能抽出时间去享受生活,去追寻诗和远方,去创造,去感悟。

可是这些我们向往的行业却在人工智能刚出来的时候就被判了死刑了。

一个原画师花4-8小时绘制的原画稿,AI花费15秒钟就能生成4张,即使效果没达到,那再精进修改一下,大不了再生成4张,也不过再花15秒。

线上工作,直接变成了人工智能的天下,无论是劳动型的还是创作型的,都在以更快的速度被人工智能替代完成。

有人说,那我们可以当那个使用珍妮纺织机的啊?

人工智能能生成精美的图画,但是依旧需要人去使用它才行。

咱先不论,你有多少钱去支付那些高昂的大模型使用费。

咱们讲讲基础的。

当食品、日常工业品被资本完全无人化生产后,你猜是会涨价还是会降价?

当人们都去使用人工智能做一人公司,去做超级个体,去干自媒体。

那么消费的人从哪儿来?

想象一下,这个场景。

资本的无人工厂,在种植农作物、在无人工厂生产产品,通过无人车送到仓库。

然后你通过发布一些自媒体作品,插播资本家的广告来赚钱,然后用这个钱去购买资本家生产的食物和工业品。

那么你发现没,这个社会的生产力从全体人类变成了资本家+无人设备。

那么这个时候,人类自身的实物产出可能正在归零。

那么资本家为什么还要在这儿又生产又卖货还要打广告?

当财富和生产资料全部倾斜到少部分人的手里时,那剩下的人类唯一的价值,就是充当这些少部分人在这个地球online的npc。

基于一个权威统计,全球前12名富豪的总资产加起来已经超过全球最贫困40亿人(即全球底层50%人口)的财富总和。

那些国外科幻电影里的超级寡头们和底层人们即将正式出现,或者已经出现。

这造成的问题是什么?

那将是贫富差距极具拉大。

AI,先聊到这里,可能感觉说了一半,还有一种说了等于没说的感觉。

等一下下面会接着讲。


AI聊多了,我们再来聊聊战争

最近,北边的俄乌冲突还没结束,但是美国和以色列又开始打伊朗,然后又遭到了伊朗的反击。

我们因为生活在中国,所以只是吃瓜群众。

但是国外,那真的是热火朝天,大哥们哐哐刷火箭。

今天你炸我的首都,炸死我领导人和平民未成年,明天我炸你航母和军事基地。

可是,这场战争是为什么打起来?有人分析过吗?

当然,很多人都分析过。

伊朗是以色列在中东统治计划中的最大的威胁和阻碍,抵抗之弧的后台就是伊朗,同时伊朗是什叶派的中心,也是中东石油的中心,所以必然要搞掉伊朗,让伊朗唯美国和以色列马首是瞻。

那么伊朗会不会继续打下去?

这要看华尔街和美国军工复合体的态度,而不是伊朗以及什叶派的态度。

为什么。

这归根结底,这就是资本的博弈游戏。

战争自古以来就不是纯粹的。

战争是政治的延伸,是经济的狂欢,是权力的游戏,唯独不是人类的希望。

之所谓大炮一响,黄金万两。

只要大炮一响,风险资本必然出逃,避险资本就会增加,哪些东西避险?

黄金、石油。

特别是伊朗把霍尔木兹海峡一封锁,中东的石油想要运出去更加困难,走陆路的成本高的离谱。

这必然会导致全球油价飙升。

大家可别忘了,美元是和石油以及大宗商品绑定的,油价提升有利于美元的地位稳固,而且美国可也是能源出口国。

而黄金自古以来都作为避险品的第一首选,乱世黄金贵嘛。

那么,美国为什么要配合以色列打伊朗?

因为川普政府需要犹太资本救国,而以色列可是犹太人的老家。

同时,战争一响,只要不落下风,川普的中期选举就算是稳了。

至于伊朗,谁在乎呢。

大刘在自己的书里说过一句话:“我毁灭你,与你何干!”

即使现在伊朗的报复那么激烈。

如果伊朗最后如了美以之愿,选择归顺,那么伊朗将国不是国,资源和土地将会被无情的瓜分。

如果伊朗最后没有如美以之愿,选择抵抗,那么华尔街那些做多黄金的资本和美国的军工复合体们估计笑歪了嘴,只要有战争,他们就会源源不断的有进账。

至于被大量征税的美国红脖子、前线伤亡的美国大兵以及被轰炸的伊朗土地,除了被斩杀的流浪汉和废墟里的难民,没人会在乎。

落后就要挨打,这个真理是我国用几代人的生命为代价证明的。


那么,战争与AI又有什么关系呢?

当然有关系,你发现没。

被炮弹轰炸过的满是儿童尸体的学校废墟和具有文明未来之称的人工智能、机器人,处于同一时代。

一边是被疯狂轰炸的土地;一边是疯狂迭代的人工智能。

一边在物理上消灭人类,抢夺更多的资源为己用,借口是解放自由;一边在使用人工智能和机器人抢夺更多人的岗位,满嘴的未来已来。

所以这两件事,其实是同一件事:掠夺。

通过炮弹掠夺土地和资源,看起来肮脏且野蛮,人们纷纷指责施暴者。

但是通过技术和制度,剥夺人们的工作岗位和社会地位,却能光明正大地。然后锅甩给人们,说是那些人自己不努力,跟不上时代,这是个人问题,不能要求时代发展的脚步为这些人而停下。

多么讽刺啊。

都在说,现在我们正处于一个百年未有之大变局,之前我还不信,现在我深信不疑。

幸亏,我们生活在中国,一个用实力造就和平的国家,所以我们只需要考虑其中的一件事即可。

喜欢战争与AI这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

面试笔记:MySQL 相关03 – SQL语法与查询优化

感谢订阅陶其的个人博客!

SQL语法与查询优化

回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关02 – 索引》


01. 子查询与连接(join)查询

子查询:是嵌套在主查询中的查询,按“返回结果类型”和“是否依赖主查询”分类,核心是 “子结果驱动主查询”;
连接查询:是多表关联,核心是 “通过关联条件合并表数据”,按“是否保留不匹配行”分类,底层依赖驱动表和被驱动表的关联逻辑。

以下给出了4种核心子查询和5种核心连接查询,以及一种内部优化查询。

01. 标量子查询

返回单个值。

  • 写法格式:子查询返回 1 行 1 列,可作为主查询的字段或条件值;
    -- 示例:查询订单数最多的用户名称(子查询返回最大订单数)
    SELECT username FROM user WHERE user_id = (SELECT MAX(user_id) FROM order WHERE status = 1);
  • 性能开销:极低,仅执行 1 次子查询,结果直接代入主查询;
  • 适用场景:主查询需依赖单个聚合值(如最大值、平均值)或特定单行数据。

02. 列子查询

返回单列多行。

  • 写法格式:子查询返回 1 列 N 行,主查询用IN/NOT IN/ANY/ALL匹配;
    -- 示例:查询有未支付订单的用户(子查询返回未支付订单的用户ID列表)
    SELECT username FROM user WHERE user_id IN (SELECT DISTINCT user_id FROM order WHERE status = 0);
  • 性能开销:中低,MySQL 5.7 + 会将其优化为 “半连接”(避免逐行匹配),子查询结果会被物化(存入临时表);
  • 适用场景:主查询需匹配多个候选值(如 “在某个集合中”),子查询结果集不宜过大(建议 1 万行内)。

03. 行子查询

返回多行多列。

  • 写法格式:子查询返回 N 行 N 列(通常 1-2 列),主查询用IN匹配行数据;
    -- 示例:查询与“用户张三的上海地址”完全匹配的用户(返回id和city两列)
    SELECT username FROM user WHERE (user_id, city) IN (SELECT user_id, city FROM user_addr WHERE username = '张三' AND city = '上海');
  • 性能开销:中,子查询结果集需按行匹配,建议结果集控制在 1 千行内;
  • 适用场景:主查询需匹配 “多字段组合条件”(如联合主键、多维度筛选)。

04. 关联子查询

依赖主查询字段。

  • 写法格式:子查询用主查询的字段作为条件(EXISTS是典型用法),需用表别名区分;
    -- 示例:查询有有效订单的用户(子查询依赖主查询的user_id,找到匹配即停止)
    SELECT username FROM user u WHERE EXISTS (SELECT 1 FROM order o WHERE o.user_id = u.user_id AND o.status = 1);
  • 性能开销:中高,主查询每扫描 1 行,子查询触发 1 次(类似 “嵌套循环”);若子查询关联字段无索引,会导致全表扫描(如 10 万行主表触发 10 万次子查询);
  • 适用场景:主查询需 “存在性校验”(如 “是否有相关数据”),优先用EXISTS(找到匹配行立即停止)替代IN(需遍历全量结果集)。

05. 内连接

INNER JOIN,默认连接方式。

  • 写法格式:仅保留两表中 “关联条件完全匹配” 的行(取交集),INNER可省略;
    -- 示例:查询用户及其有效订单(仅返回有订单的用户和匹配的订单)
    SELECT u.username, o.order_no FROM user u
    INNER JOIN order o ON u.user_id = o.user_id  -- 关联条件(必须写ON),INNER 可省略
    WHERE o.status = 1;
  • 性能开销:低,MySQL 会自动选择 “小表作为驱动表”,若关联字段有索引,被驱动表可快速匹配(避免全表扫描);
  • 适用场景:需获取多表 “交集数据”(如用户 + 关联订单、商品 + 关联分类),是业务中最常用的连接方式。

06. 左外连接

LEFT JOIN / LEFT OUTER JOIN

  • 写法格式:保留左表所有行,右表无匹配时用NULL填充,OUTER可省略;
    -- 示例:查询所有用户及其订单(无订单的用户也会显示,订单字段为NULL)
    SELECT u.username, o.order_no FROM user u
    LEFT JOIN order o ON u.user_id = o.user_id
    WHERE o.status = 1 OR o.order_no IS NULL;  -- 注意:过滤右表需包含NULL
  • 性能开销:中低,驱动表是左表(需全扫左表),右表关联字段有索引则性能接近内连接;无索引则性能下降明显;
  • 适用场景:需保留主表全量数据(如用户表),关联从表(如订单表)的可选数据(如 “统计所有用户的订单情况,包括无订单用户”)。

07. 右外连接

RIGHT JOIN / RIGHT OUTER JOIN

  • 写法格式:保留右表所有行,左表无匹配时用NULL填充,逻辑与左连接相反;
    -- 示例:查询所有订单及其所属用户(无匹配用户的订单也显示,用户字段为NULL)
    SELECT u.username, o.order_no FROM user u
    RIGHT JOIN order o ON u.user_id = o.user_id
    WHERE o.create_time > '2024-01-01';
  • 性能开销:与左连接一致,驱动表是右表(需全扫右表);
  • 适用场景:需保留从表全量数据(如订单表),关联主表(如用户表)的可选数据(如 “统计所有近 3 个月订单,包括已删除用户的订单”)。

08. 全外连接

FULL JOIN / FULL OUTER JOIN

  • 写法格式:保留左右两表所有行,无匹配时用NULL填充;MySQL 不直接支持FULL JOIN,需用UNION合并左连接和右连接;
    -- 示例:查询所有用户和所有订单的关联数据(保留双方无匹配的行)
    SELECT u.username, o.order_no FROM user u
    LEFT JOIN order o ON u.user_id = o.user_id
    UNION  -- 去重合并
    SELECT u.username, o.order_no FROM user u
    RIGHT JOIN order o ON u.user_id = o.user_id;
  • 性能开销:高,需执行两次连接 + 去重操作,数据量大时效率低;
  • 适用场景:极少用,仅需 “完整保留两表所有数据” 的特殊场景(数据对账、全量统计)。

09. 交叉连接

CROSS JOIN,笛卡尔积

  • 写法格式:无关联条件,两表数据完全组合(行数 = 左表行数 × 右表行数),CROSS JOIN可省略;
    -- 示例:用户表(3行)和商品表(2行)交叉连接,结果为6行
    SELECT u.username, p.product_name FROM user u
    CROSS JOIN product p;
  • 性能开销:极高,行数呈指数级增长(1 万行 ×1 万行 = 1 亿行),几乎不用;
  • 适用场景:仅用于 “全组合场景”(如生成所有用户 + 所有商品的测试数据),必须配合WHERE过滤(否则会撑爆内存)。

10. 半连接

半连接不是独立的查询语法,而是 MySQL 5.7 + 默认启用的对列子查询(如IN子查询)的内部优化手段,目的是减少子查询与主查询的匹配开销。

用于优化“主查询匹配子查询结果集”的场景(如IN/EXISTS列子查询)。

核心逻辑是 “只判断匹配与否,不返回子查询的完整数据”,避免逐行匹配的低效问题。

简单说:普通子查询是 “子查询返回所有结果→主查询逐行比对”,半连接是 “主查询与子查询表直接关联→找到匹配行就停止”,本质是将子查询转化为类似JOIN的关联逻辑,提升性能。

触发条件:子查询是“列子查询”(返回单列多行),且子查询表与主查询表无关联(非关联子查询),例如WHERE id IN (SELECT uid FROM t2 WHERE status=1)

写法格式:无特殊语法,仍用普通IN/EXISTS子查询写法,MySQL 会自动触发半连接优化(可通过EXPLAIN查看type列是否有SIMPLE/HASH JOIN,而非SUBQUERY)。

-- 示例:查询有未支付订单的用户(MySQL会自动用半连接优化)
SELECT username FROM user u WHERE u.user_id IN (SELECT DISTINCT o.user_id FROM order o WHERE o.status = 0);

性能开销

  • 低 – 中:远优于未优化的子查询(避免子查询结果集物化后逐行匹配)。
  • 优化逻辑:MySQL 会选择以下方式之一执行半连接:
    • 哈希半连接:对小表(子查询表)建哈希表,主表逐行匹配哈希键(适合大表)。
    • 嵌套循环半连接:小表驱动大表,找到匹配行立即停止扫描(适合小表)。
    • 物化半连接:子查询结果存入临时表并建索引,主表关联临时表(适合子查询结果集较小)。

适用场景

  • 主查询需判断 “字段是否在子查询结果集中”(如IN/EXISTS)。
  • 子查询表数据量适中(1 万行内最佳),且子查询有过滤条件(如WHERE status=1),能减少结果集大小。
  • 替代场景:若子查询结果集极大(10 万行 +),建议手动改写为JOIN(半连接优化效果会下降)。

注意项:

  • 半连接是 MySQL 自动优化,无需专门手动去写语法,但需知道 “IN子查询在 5.7 + 会被优化为半连接”,避免面试官问 “INJOIN哪个快” 时只说表面结论。
  • 实践中,若EXPLAIN显示子查询类型为SUBQUERY(未触发半连接),可通过 “子查询加DISTINCT” 或 “改写为JOIN” 强制优化(如上述示例加DISTINCT让结果集唯一,更易触发半连接)。

总结

1. 子查询 vs 连接查询:

  • 简单场景(如单值匹配、存在性校验)用子查询(写法简洁);
  • 复杂多表关联(如 3 表以上、需筛选多字段)用连接查询(性能更优,易优化);
  • 关联子查询尽量改写为JOIN(避免嵌套循环,如EXISTS适合小表,JOIN适合大表)。

2. 性能优化关键:

  • 连接查询:关联字段必须建索引(如order.user_id),小表驱动大表(减少循环次数);
  • 子查询:避免NOT INNULL 值会导致结果异常),用NOT EXISTS替代;
  • 外连接:右表过滤条件写在ON(关联时过滤),左表过滤条件写在WHERE(关联后过滤)。

3. Java 开发实践场景:

  • 列表查询(如用户列表 + 订单数):用LEFT JOIN + GROUP BY
  • 详情查询(如订单详情 + 用户信息):用INNER JOIN
  • 存在性校验(如判断用户是否有未支付订单):用EXISTS子查询。

02. 子查询与join性能对比

子查询与join性能对比及适用场景

关键结论:

  • 简单场景(单表校验、单值匹配)用子查询(简洁);
  • 多表关联、复杂聚合、主表数据量大时,优先用 JOIN(性能更稳定)。

03. 复杂查询的执行逻辑

1. group by/having

GROUP BYHAVING 是 MySQL 中用于数据分组聚合的核心语法,执行逻辑: “数据过滤→分组→聚合→二次过滤” 。

1. 核心执行逻辑

GROUP BY 的作用是 “按指定字段将数据分组”;
HAVING 则是 “对分组后的结果进行过滤”。

整体执行流程分四步,顺序不可颠倒:

1) 原始数据过滤(WHERE 子句);

  • 先通过 WHERE 筛选符合条件的原始数据(如 WHERE status = 1 过滤无效数据),减少后续处理的数据量。
  • 原理:WHERE 是 “分组前过滤”,仅保留满足条件的行,不涉及聚合操作,可利用索引快速筛选(如 status 字段有索引时,直接定位有效行)。

2) 分组操作(GROUP BY 子句);

  • GROUP BY 后的字段(如 GROUP BY user_id)将上一步过滤后的数据分组,相同值的行被归为一组。
  • 底层处理:
    • 若分组字段有索引,MySQL 会直接按索引顺序分组(无需额外排序,效率高);
    • 若无索引,会创建临时表存储分组数据,或对数据进行文件排序(Using temporaryUsing filesort,可通过 EXPLAIN 查看)。

3) 聚合计算(聚合函数);

  • 对每个分组执行聚合函数(如 COUNT(*) 统计行数、SUM(amount) 计算总和),生成每个分组的聚合结果。
  • 原理:聚合函数仅作用于分组内的数据,每个分组最终输出一行结果(包含分组字段和聚合值)。

4) 分组结果过滤(HAVING 子句);

  • HAVING 筛选聚合后的分组(如 HAVING COUNT(*) > 5 保留行数超 5 的组),最终返回符合条件的分组。
  • 原理:HAVING 是 “分组后过滤”,可直接使用聚合函数(因聚合结果已生成)。

2. 关键区别与实践注意

1) WHERE 与 HAVING 的核心区别

  • WHERE 作用于分组前的原始数据,不能使用聚合函数(如 WHERE COUNT(*) > 5 会报错);
  • HAVING 作用于分组后的结果,可以使用聚合函数(如 HAVING SUM(amount) > 1000 合法)。

2) 性能优化实践(重点)

  • 优先用 WHERE 过滤:尽量在分组前通过 WHERE 减少数据量(如 GROUP BY user_id HAVING user_id > 100 可改为 WHERE user_id > 100 GROUP BY user_id,减少分组计算量)。
  • 分组字段加索引:避免临时表和文件排序(如 GROUP BY create_time 时,给 create_time 建索引,EXPLAIN 中无 Using temporary 即为优化生效)。
  • 避免 SELECT 非分组字段:MySQL 5.7+ 默认开启 ONLY_FULL_GROUP_BY 模式,SELECT 后只能出现 GROUP BY 字段或聚合函数(如 SELECT user_id, username, COUNT(*)username 未在 GROUP BY 中,会报错,需规范写法)。

3. 典型场景举例

  • 统计高频用户:查询 “订单数超 10 单的用户及其总消费”,用 GROUP BY user_id HAVING COUNT(*) > 10
  • 按时间分组分析:查询 “每月订单金额超 10 万的月份”,用 GROUP BY month(create_time) HAVING SUM(amount) > 100000

2. limit 分页

LIMIT 分页是 MySQL 中用于从查询结果中截取指定范围数据的核心语法(如 LIMIT offset, size 取第 offset + 1offset + size 行),其执行逻辑围绕 “全量查询→排序→截取” 展开,实际使用中需重点关注性能问题。

1. 基本执行逻辑

LIMIT 本身不参与数据筛选,而是对 “查询 + 排序后” 的结果集进行截取,核心流程分三步:

1) 执行主查询获取原始数据

  • 先执行 WHEREJOIN 等条件筛选,得到所有符合条件的行(如 SELECT * FROM order WHERE status=1 筛选有效订单)。

2) 排序处理(若有 ORDER BY

  • 若包含 ORDER BY(分页几乎必带,否则结果无序),MySQL 会按指定字段排序:
    • 若排序字段有索引(如 ORDER BY create_timecreate_time 有索引),直接利用索引顺序获取有序数据(效率高);
    • 若无索引,需在内存或磁盘中进行 “文件排序”(Using filesort,可通过 EXPLAIN 查看),排序过程会消耗额外 CPU/IO。

3) 截取指定范围数据

  • 从排序后的完整结果集中,跳过前 offset 行,返回接下来的 size 行(如 LIMIT 20, 10 即跳过前 20 行,返回 10 行)。

2. 核心性能问题:offset 过大导致效率低下

offset 很大时(如 LIMIT 100000, 10),性能会急剧下降,原因是:

  • MySQL 必须先扫描并排序前 100010 行数据,然后丢弃前 100000 行,仅返回最后 10 行,大量计算被浪费;
  • 若排序无索引(依赖文件排序),会产生临时文件,进一步加剧性能损耗(10 万行数据排序可能耗时数百毫秒)。

3. 实践优化方案(重点,体现工程经验)

针对 offset 过大的问题,结合 Java 开发中常见的分页场景(如列表页、历史记录查询),优化手段如下:

1) 基于 “主键 / 唯一索引” 分页(最常用)

利用主键或唯一索引的有序性,通过 WHERE 条件直接定位起始位置,避免全量扫描:

-- 代替 LIMIT 100000, 10(低效)
SELECT * FROM order 
WHERE id > 100000  -- 上一页最后一条数据的id
ORDER BY id 
LIMIT 10;

原理:主键索引是有序的,id > 100000 可直接定位到起始行,无需扫描前 10 万行,性能提升 10 倍以上。

2) 避免 SELECT *,只查必要字段

减少数据传输量和内存占用,尤其大表(如包含 text 字段的表):

-- 只查需要的字段(如订单号、金额、时间)
SELECT order_no, amount, create_time FROM order 
WHERE id > 100000 
LIMIT 10;

3) 确保排序字段有索引

分页必须带 ORDER BY,且排序字段需建索引(如 create_time 索引),避免文件排序:

-- 给 create_time 建索引:CREATE INDEX idx_order_create_time ON order(create_time)
SELECT * FROM order 
WHERE status=1 
ORDER BY create_time DESC  -- 利用索引排序
LIMIT 20, 10;

4) 限制最大分页页数

业务上避免允许用户访问过大页数(如 “只显示前 100 页”),超过则提示 “数据过多,请缩小范围”(如电商平台常见做法),从源头减少大 offset 场景。

04. order by 的排序原理

MySQL 的ORDER BY用于对查询结果按指定字段排序,其核心原理是 “利用索引有序性直接取数” 或 “无索引时通过内存 / 磁盘排序”,性能差异主要源于是否能借助索引避免额外排序操作。

1. 核心排序原理

1. 利用索引排序

Using index,高效。

若排序字段(或多字段排序的前缀)有有序索引(如 B + 树索引,本身按字段值有序存储),MySQL 会直接沿索引顺序读取数据,无需额外排序,这是最优情况。

  • 原理:B + 树索引的叶子节点按索引字段值升序(或降序,取决于建索引时的指定)排列,ORDER BY字段与索引顺序一致时,可直接通过索引定位并返回有序数据,避免排序开销。
  • 示例:
    订单表order有索引idx_create_timecreate_time升序),执行SELECT id, create_time FROM order ORDER BY create_time时,MySQL 直接沿idx_create_time的叶子节点顺序取数,无需排序(EXPLAINExtra列显示Using index)。

2. 文件排序

Using filesort,低效。

若排序字段无索引,或排序顺序与索引顺序不一致(如索引是升序,ORDER BY用降序且无对应降序索引),MySQL 会触发 “文件排序”:先将数据加载到内存 / 临时文件,完成排序后再返回结果。

  • 执行步骤:

    1. 从表中读取符合WHERE条件的行(无索引时全表扫描);
    2. 将 “排序字段值 + 行指针(指向原始数据位置)” 存入sort_buffer(排序缓冲区);
    3. sort_buffer装不下所有数据,会将数据分块,先在内存中排序,再写入临时文件(磁盘);
    4. 最后合并所有分块的排序结果,得到全局有序的结果集,再通过行指针取原始数据返回。
  • 示例:
    订单表ordercreate_time无索引,执行SELECT * FROM order ORDER BY create_time时,MySQL 会先扫描全表,将create_time和行指针存入sort_buffer,排序后再取完整数据(EXPLAINExtra列显示Using filesort)。

2. 影响排序性能的关键因素

  1. 是否使用索引:索引排序性能远高于文件排序(毫秒级 vs 秒级,数据量大时差距更大)。
  2. sort_buffer大小:由sort_buffer_size参数控制(默认 256KB),若数据量超过该值,会触发磁盘临时文件排序(IO 开销剧增)。
  3. 查询字段多少SELECT *会导致sort_buffer中存储更多字段(增加内存占用),而只查必要字段可减少sort_buffer压力,加速排序。

3. 实践优化建议

1. 给排序字段建合适的索引:

  • 单字段排序:直接给排序字段建索引(如ORDER BY create_time → 建idx_create_time)。
  • 多字段排序(如ORDER BY a, b DESC):建联合索引(a, b DESC),索引顺序与排序顺序完全一致(避免索引失效)。

2. 避免SELECT *,只查必要字段:

  • 例如,列表页只需idnamecreate_time,则SELECT id, name, create_time ...,减少sort_buffer中存储的数据量,避免磁盘排序。

3. 控制排序数据量:
先用WHERE过滤无效数据(如WHERE status=1),减少进入排序阶段的行数(如从 100 万行滤到 1 万行,排序效率提升 100 倍)。

4. 大结果集排序用分页配合索引:
如 “按时间排序分页查询订单”,建(create_time, id)联合索引,用WHERE create_time > ?定位分页起点,避免全量排序。

4. 总结

ORDER BY的优化核心是 “能用上索引就避免文件排序”:索引排序依赖 B + 树的有序性,高效低耗;文件排序需内存 / 磁盘排序,性能差。实际开发中,通过 “建匹配索引 + 精简查询字段 + 提前过滤数据” 可显著提升排序性能,这也是处理订单列表、用户日志等需排序场景的常规优化手段。

05. 慢查询的定位与分析

MySQL 的慢查询(执行时间超过阈值的 SQL)是性能优化的核心切入点,定位与分析需结合日志工具、执行计划和业务场景。

1. 慢查询的定位

慢查询的定位:通过日志捕获目标 SQL。

定位慢查询的核心是开启慢查询日志,记录执行时间超标的 SQL,为后续分析提供依据。

1. 慢查询日志配置(5.7 版本)
通过修改my.cnf(或my.ini)配置,开启并定制日志规则:

slow_query_log = 1  # 开启慢查询日志(默认:0,关闭)
slow_query_log_file = /var/log/mysql/slow.log  # 日志文件路径(需MySQL有写入权限)
long_query_time = 1  # 慢查询阈值(单位秒,默认10秒,建议生产设1-2秒)
log_queries_not_using_indexes = 1  # 记录未使用索引的查询(即使未超阈值,可选)
log_output = FILE  # 日志输出方式(FILE/table,5.7默认:FILE)

配置后重启 MySQL 生效,也可通过SET GLOBAL动态开启(无需重启,适合临时排查):

SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;

2. 日志收集工具

  • mysqldumpslow(自带工具):简单统计慢查询,适合快速定位高频问题:

    # 查看访问次数最多的10条慢查询
    mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
    # 查看总执行时间最长的10条慢查询
    mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

    优势:轻量,适合初步筛选;
    不足:无法分析趋势或复杂统计。

  • pt-query-digest(Percona 工具包):生产环境常用,可按执行时间、频率、用户等维度分析,输出 SQL 模板、平均耗时、扫描行数等关键信息:

    pt-query-digest /var/log/mysql/slow.log > slow_analysis.txt

    优势:能识别重复 SQL(如参数不同但结构相同的查询),定位 “隐形高频慢查询”(单条耗时 0.8 秒,但每秒执行 100 次,总耗时 80 秒)。

2. 慢查询的分析

慢查询的分析:通过执行计划定位根因。

捕获慢查询后,需用EXPLAIN分析其执行逻辑,重点关注是否全表扫描、是否用对索引、是否有额外排序 / 临时表。

1. 核心分析工具:EXPLAIN

对慢查询执行EXPLAIN,查看关键字段:

  • type:访问类型(性能从好到差:const > eq_ref > ref > range > ALL)。ALL表示全表扫描(需优先优化)。
  • key:实际使用的索引(NULL表示未用索引)。
  • rows:预估扫描行数(值越大,效率越低)。
  • Extra:额外信息(关键警告:Using filesort(需排序且无索引)、Using temporary(需创建临时表)、Using where; Using filesort(全表扫描后排序,性能极差))。

2. 常见慢查询原因及分析(结合EXPLAIN

  • 全表扫描(type=ALLkey=NULL
    • 原因:查询条件无索引(如:WHERE status=1status无索引),或索引失效(如:WHERE name LIKE '%abc'左模糊匹配导致索引失效)。
  • JOIN 关联无索引(rows极大)
    • 原因:被驱动表的关联字段无索引(如t1 JOIN t2 ON t2.uid = t1.idt2.uid无索引),导致被驱动表全表扫描。
  • 排序无索引(Extra=Using filesort
    • 原因:ORDER BY字段无索引,或排序顺序与索引不一致(如索引是create_time ASC,但查询用ORDER BY create_time DESC且无降序索引)。
  • 子查询嵌套(type=DEPENDENT SUBQUERY
    • 原因:关联子查询导致主表每行触发一次子查询(如WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.uid = t1.id)),无索引时性能骤降。

3. 慢查询的优化方向

分析出原因后,针对性优化:

1. 加索引

  • 为查询条件、JOIN 关联字段、排序字段建索引(如:WHERE+ORDER BY字段建联合索引,如idx_status_create_time (status, create_time))。

2. 改写 SQL

  • 子查询改 JOIN(如关联子查询改写为INNER JOIN,减少嵌套循环);
  • 避免左模糊匹配(LIKE '%abc'改为LIKE 'abc%',或用全文索引);
  • 大分页改范围查询(LIMIT 100000,10改为WHERE id > 100000 LIMIT 10)。

3. 控制数据量

  • WHERE提前过滤无效数据(如WHERE create_time > '2024-01-01'),减少扫描和排序行数。

4. 8.0 版本的优化

  • 慢查询日志支持表存储:除文件外,可将日志写入mysql.slow_log表(log_output = TABLE),支持 SQL 查询分析(如按时间范围统计)。
  • 日志内容更丰富:新增query_id(唯一标识)、rows_affected等字段,方便关联性能模式(Performance Schema)数据。
  • 动态调整更灵活:long_query_time支持小数(如 0.5 秒),且修改后立即生效(无需重启连接)。

5. 总结

慢查询定位与分析的核心流程是:开启日志捕获 → 工具统计筛选 → EXPLAIN分析执行计划 → 针对性优化(索引 / 改写 SQL)

实践中,需结合业务场景(如订单表慢查询多与user_idcreate_time索引相关),优先解决 “高频 + 高耗时” 的慢查询。

06. 复杂SQL的拆分与改写

大表分页优化:延迟关联、书签分页。

MySQL 大表分页是业务中高频痛点(如订单列表、日志查询),普通LIMIT offset, sizeoffset较大时(如LIMIT 100000,10)性能极差:因 MySQL 需扫描前 10 万行再丢弃,仅取最后 10 行,IO 开销剧增。

复杂 SQL 拆分与改写的核心思路是减少无效扫描行数,常用优化手段包括延迟关联书签分页

1. 普通分页的性能瓶颈

普通分页 SQL 示例(订单表order,按create_time逆序):

SELECT * FROM `order` ORDER BY create_time DESC LIMIT 100000, 10;

问题:当offset=100000时,MySQL 需先遍历索引找到第 10 万行的位置(即使有create_time索引),再回表读取 10 行数据,前 10 万行的扫描属于无效开销,数据量越大越慢。

2. 关键优化方法

1. 延迟关联(延迟回表)

概念:先通过覆盖索引获取目标数据的主键(或唯一键),再关联原表获取完整字段,避免 “早期回表” 带来的大量 IO。

原理:利用索引的 “覆盖查询” 特性(只查主键,无需回表)快速定位目标行,再通过主键关联原表取数,减少扫描和回表的数据量。

优化后 SQL 示例:

-- 子查询用索引获取主键(覆盖索引,无需回表)
SELECT o.* 
FROM `order` o 
JOIN (SELECT id FROM `order` ORDER BY create_time DESC LIMIT 100000, 10) tmp 
ON o.id = tmp.id;
  • 优势:子查询仅扫描索引获取id(轻量),关联原表时仅取 10 行完整数据,性能比普通分页提升 5-10 倍(10 万 + offset 场景)。
  • 适用场景:需展示完整字段、支持任意跳页(如第 100 页、第 200 页)的列表。

2. 书签分页(游标分页 / 键集分页)

概念:用 “上一页最后一条记录的唯一标识”(书签,如idcreate_time+id)作为条件,替代offset直接定位下一页起点,完全避免无效扫描。

原理:利用主键 / 唯一索引的有序性,通过WHERE条件跳过前面数据,直接从书签位置取数。

优化后 SQL 示例:假设上一页最后一条记录的create_time='2024-01-01 10:00:00'id=100000,下一页查询:

SELECT * 
FROM `order` 
WHERE create_time < '2024-01-01 10:00:00' 
   OR (create_time = '2024-01-01 10:00:00' AND id < 100000) 
ORDER BY create_time DESC, id DESC 
LIMIT 10;
  • 关键:用create_time+id作为书签(create_time可能重复,需加id保证唯一性),通过索引直接定位,无offset开销。
  • 优势:性能极致(无论翻多少页,扫描行数固定为LIMIT size);
  • 局限:不支持 “跳页”(如直接到第 100 页),仅适合 “上一页 / 下一页”“加载更多” 场景(APP / 移动端常用)。

3. 覆盖索引分页

覆盖索引分页:若查询字段(如id, order_no, create_time)全部包含在索引中,直接用索引查询,无需回表:

-- 索引`idx_create_time_order_no (create_time DESC, order_no, id)`覆盖所有查询字段
SELECT id, order_no, create_time 
FROM `order` 
ORDER BY create_time DESC 
LIMIT 100000, 10;

4. 限制最大 Offset

业务上禁止offset超过阈值(如 10000),引导用户通过筛选条件(如 “按时间范围筛选”)缩小数据范围,避免大 offset 查询。

5. 分表分页

超大规模表(亿级)需水平分表(如按create_time分表),分页时先定位分表,再在分表内分页,避免跨表扫描。

3. 总结

大表分页优化的核心是 “用索引定位替代无效扫描”

  • 需支持跳页选延迟关联,通过覆盖索引减少回表;
  • 移动端 / 加载更多选书签分页,用唯一键直接定位;
  • 极致性能选覆盖索引分页,避免回表开销。

07. JOIN优化

小表驱动大表、 避免 cross join

1. JOIN 优化的核心逻辑

MySQL 中 JOIN 的底层实现以Nested Loop Join(嵌套循环连接)为主:外层循环遍历 “驱动表” 的数据,内层循环用驱动表的字段去 “被驱动表” 中匹配数据。。就像Java里的双层for循环。

优化的核心是减少外层循环次数 + 降低内层匹配成本,这也是 “小表驱动大表” 的原理基础。

2. 优化1:小表驱动大表

1. 原理

“小表” 指参与 JOIN 的数据集更小的表(而非物理表大小)。
用小表做驱动表(外层循环),大表做被驱动表(内层循环),可显著减少外层循环次数,从而降低整体 IO 和匹配开销。

  • 举例:小表有 100 行,大表有 100 万行
    • 小表驱动大表:外层循环 100 次,内层每次匹配大表 → 总匹配 100 次;
    • 大表驱动小表:外层循环 100 万次,内层每次匹配小表 → 总匹配 100 万次;
    • 性能差异一目了然。

2. 实践判断与示例

  • 如何判断 “小表”:
    • 通过EXPLAINrows字段(预估扫描行数),行数少的就是 “小表”;
    • 或业务逻辑中明确数据量更小的表(如字典表、配置表)。
  • SQL 示例(用户表user是小表,订单表order是大表):
-- 推荐:小表user驱动大表order(INNER JOIN中MySQL会自动优化,但LEFT JOIN需显式控制)
SELECT u.username, o.order_no 
FROM user u 
INNER JOIN `order` o ON u.id = o.user_id;

-- LEFT JOIN需注意:左表是驱动表,因此左表必须是小表
SELECT u.username, o.order_no 
FROM user u  -- 小表放左表(驱动表)
LEFT JOIN `order` o ON u.id = o.user_id;

3. 注意点

  • INNER JOIN:MySQL 优化器会自动选择小表作为驱动表,无需手动调整;
  • LEFT JOIN:驱动表固定为左表,因此必须将小表放在左表位置(否则会用大表驱动小表,性能暴跌);
  • RIGHT JOIN:驱动表固定为右表,需将小表放在右表位置(实际开发中建议用LEFT JOIN替代,更符合阅读习惯)。

3. 优化2:避免 CROSS JOIN(笛卡尔积)

1. CROSS JOIN 的危害

CROSS JOIN会返回两张表的笛卡尔积(行数 = 表 A 行数 × 表 B 行数),若表 A 有 1 万行、表 B 有 10 万行,结果会有 10 亿行,直接导致数据库 CPU/IO 打满、查询超时。

2. 常见触发场景

  • 显式使用CROSS JOIN关键字且无ON条件;
  • JOIN时遗漏ON关联条件(如SELECT * FROM A JOIN B);
  • WHERE条件无法过滤笛卡尔积(如关联字段值全为 NULL)。

3. 如何避免

  • 任何JOIN必须加ON关联条件(即使是INNER JOIN);
  • 禁止显式使用无关联条件的CROSS JOIN
  • 若需关联但无直接字段,通过业务逻辑补充关联条件(如时间范围、状态过滤)。

4. 优化补充

1. 被驱动表的关联字段必须加索引
被驱动表的ON字段(如order.user_id)需建索引,否则内层循环会全表扫描,即使小表驱动大表也会很慢。

2. 避免SELECT *
只查询需要的字段,减少数据传输量(尤其大表有大字段如TEXT时)。

3. 提前过滤数据
WHERE先过滤驱动表和被驱动表的无效数据(如WHERE o.status=1),减少参与 JOIN 的行数。

5. 总结

MySQL JOIN 优化的核心是:

  • 小表驱动大表:利用嵌套循环的特性减少外层循环次数,LEFT JOIN需手动控制驱动表;
  • 杜绝笛卡尔积:JOIN必须加ON条件,避免CROSS JOIN
  • 索引兜底:被驱动表的关联字段加索引,是 JOIN 性能的基础保障。

08. count的性能差异

count(1)、count(*)、count(字段)

1. count 的本质

count 的本质:统计 “非 NULL 行数”。

count()是聚合函数,核心作用是统计查询结果集中符合条件的 “非 NULL 行数”,不同参数的差异在于 “统计范围” 和 “是否需要判断 NULL”,这直接决定性能。

2. 三种 count 的区别与性能对比

1. count(*)

统计所有行数(含 NULL)。

逻辑:专门用于统计表 / 结果集的总行数。
MySQL 对其有特殊优化:会优先选择最小的非聚集索引(覆盖索引)扫描,若没有则用聚集索引,避免全表扫描。

特点:包含 NULL 值(因为不判断字段,只统计行数),性能最优。

2. count(1)

统计所有行数(占位符)。

逻辑:用常量1作为占位符,统计所有行数(无论字段是否为 NULL)。
MySQL 优化器会将其与count(*)视为等价,底层执行计划完全相同。

特点:性能与count(*)几乎无差异,属于 “语法糖”。

3. count(字段)

统计字段非 NULL 的行数。

逻辑:需逐行检查该字段是否为 NULL,仅统计非 NULL 的行数。性能分两种情况:

  • 字段是索引字段:用索引扫描(比全表快),但需判断 NULL
  • 字段是非索引字段:全表扫描 + 判断 NULL,性能最差。

特点:性能低于count(*)/count(1),且结果可能不等于总行数(因为字段可能为 NULL)。

3. 性能排序

从快到慢:count(*)count(1) > ount(索引字段) > count(非索引字段)

关键原因:

  • count(*)/count(1)无需判断字段 NULL,且 MySQL 会选最优索引;
  • count(字段)需额外判断 NULL,非索引字段还需全表扫描。

4. 实践建议

  • 统计总行数:优先用count(*)(语义最清晰,MySQL 优化最好),而非count(1)count(主键)
  • 统计字段非 NULL 行数:若需此逻辑,确保字段加索引(如count(user_name)user_name有索引);
  • 避免误区:
    • 认为count(主键)更快:实际count(*)会选更小的索引,比主键索引(聚集索引)扫描更快;
    • 认为count(1)count(*)快:MySQL 优化后两者无区别,count(*)更符合语义。

5. 总结

  • 核心差异:count(*)/count(1)统计总行数(含 NULL),性能最优;count(字段)统计非 NULL 行数,性能较差;
  • 实践选择:统计总数用count(*),统计非 NULL 字段用count(索引字段)
  • 优化关键:利用 MySQL 对count(*)的索引优化,避免count(非索引字段)的全表扫描。

6. 扩展

统计useremailnull的总行数,email是普通索引,是SELECT COUNT(email) FROM user快,还是SELECT COUNT(*) FROM user WHERE email IS NOT NULL快?

答:基本相同,但是不推荐使用后者,推荐使用COUNT(email)。因为这个全程是索引,虽然需要索引扫描,但是后者使用了WHERE条件,同样也是需要进行索引扫描,同时还多出了一步条件过滤。同时COUNT(email)语义更明确。

09. INJOIN哪个快

先明确两者的底层逻辑

  • IN:属于 “子查询 / 值列表匹配”,本质是将子查询结果加载到内存后做匹配(或直接匹配值列表),适合 “单字段匹配” 场景;
  • JOIN:属于 “多表关联”,底层以Nested Loop Join为主(小表驱动大表),通过关联字段索引直接匹配,适合 “多字段关联取值” 场景。

  • IN的优势:语法简单,适合 “单字段匹配” 的简单场景;
  • JOIN的优势:支持多字段关联,大结果集下利用索引更高效,是复杂关联的首选。

10. MySQL的函数

MySQL 函数按功能可分为内置函数(系统提供,重点)和自定义函数(用户编写)。

1. 核心内置函数

1. 字符串函数

处理文本数据。

用于字符串拼接、截取、替换等,是业务中最常用的函数类别。

  • CONCAT(str1, str2,...):字符串拼接(如:拼接用户姓名和手机号:CONCAT(username, '-', mobile));
  • SUBSTRING(str, pos, len):截取子串(如:取手机号后 4 位:SUBSTRING(mobile, 8, 4));
  • REPLACE(str, old, new):替换字符串(如:清理内容中的特殊字符:REPLACE(content, '<', ''));
  • LENGTH(str):获取字符串长度(如:校验用户名长度:WHERE LENGTH(username) > 6)。

实践:用户信息格式化、文本内容清洗场景高频使用。

2. 数值函数

处理数字运算。

用于数值计算、取整、进制转换等。

  • ROUND(num, n):四舍五入(如:金额保留 2 位小数:ROUND(amount, 2));
  • ABS(num):取绝对值(如:计算差值的绝对值:ABS(score1 - score2));
  • CEIL(num)/FLOOR(num):向上 / 向下取整(如:订单数量向上取整:CEIL(total/10));
  • SUM(num)/AVG(num):求和 / 平均值(报表统计核心函数)。

实践:金额计算、数据统计场景必备。

3. 日期时间函数

处理时间数据。

用于时间获取、格式化、差值计算,是业务中仅次于字符串函数的高频函数。

  • NOW()/CURDATE():获取当前时间(含时分秒)/ 当前日期(仅年月日);
  • DATE_FORMAT(date, fmt):时间格式化(如:订单时间转字符串:DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s'));
  • DATEDIFF(date1, date2):计算日期差值(如:用户注册天数:DATEDIFF(NOW(), register_time));
  • DATE_ADD(date, INTERVAL expr unit):时间增减(如:计算 7 天后的日期:DATE_ADD(NOW(), INTERVAL 7 DAY))。

实践:订单时间筛选、会员有效期计算、报表时间维度统计。

4. 聚合函数

数据统计分析。

用于对多行数据进行聚合计算,需结合GROUP BY使用。

  • COUNT(*):统计总行数(用户数、订单数统计);
  • SUM(field):求和(销售额、销量统计);
  • MAX(field)/MIN(field):最大值 / 最小值(最高订单金额、最早注册时间);
  • GROUP_CONCAT(field):分组拼接字符串(如:查询用户的所有订单号:GROUP_CONCAT(order_no))。

实践:报表系统、数据看板的核心函数。

5. 条件函数

逻辑判断。

用于实现 SQL 中的条件分支,替代复杂的WHERE判断。

  • IF(condition, val1, val2):简单条件判断(如:判断用户状态:IF(status=1, '正常', '禁用'));
  • CASE WHEN:复杂条件分支(如:订单状态映射:CASE status WHEN 0 THEN '待支付' WHEN 1 THEN '已支付' ELSE '已取消' END);
  • IFNULL(val, default)NULL 值替换(如:用户昵称为空时显示默认值:IFNULL(nickname, '游客'))。

实践:查询结果格式化、动态状态展示。

6. 其他常用函数

  • UUID():生成唯一标识符(分布式场景临时 ID 生成);
  • INSTR(str, substr):查找子串位置(如:判断内容是否含关键词:INSTR(content, 'MySQL') > 0);
  • CAST(val AS type):类型转换(如:字符串转数字:CAST(score AS UNSIGNED))。

2. 面试高频考点

函数使用的性能陷阱。

1. 函数操作索引字段会导致索引失效

这是面试必问点!若对索引字段使用函数,MySQL 无法使用索引,会触发全表扫描。

  • 反例:WHERE DATE(create_time) = '2024-01-01'(对索引字段create_timeDATE()函数,索引失效);
  • 正例:WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'(直接匹配字段范围,利用索引)。这个之所以能利用索引,是因为没有使用函数破坏索引的有序性,同时MySQL将给出的查询条件值'2024-01-01''2024-01-02'隐式的转换为了DATETIME类型,从而避免了将整个表的create_time字段转成其他类型,保证了原索引的可用性。

2. 聚合函数的NULL处理

  • COUNT(field)会忽略 NULL 值,COUNT(*)统计所有行(含 NULL);
  • SUM(field)NULL 值会被视为 0,但建议用IFNULL(field, 0)显式处理(避免语义歧义)。

3. 自定义函数的慎用场景

  • 自定义函数(CREATE FUNCTION)可实现复杂逻辑,但执行效率低(无法并行执行),且可能导致锁表,高并发场景建议用存储过程或应用层代码替代。

3. 总结

MySQL 函数是提升 SQL 灵活性的核心工具。

  • 分类记忆:重点掌握字符串、日期、聚合、条件函数的常用用法;
  • 性能陷阱:索引字段避免使用函数,否则索引失效;
  • 实践优先:简单逻辑用内置函数,复杂逻辑优先应用层处理(避免 SQL 臃肿)。

回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关02 – 索引》

喜欢面试笔记:MySQL 相关03 – SQL语法与查询优化这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

面试笔记:MySQL 相关02 – 索引

感谢订阅陶其的个人博客!

索引

回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关01 – 基础核心》
下一篇: 《面试笔记:MySQL 相关03 – SQL语法与查询优化》


01. 各种树的原理和特性

学习树,是为了便于学习索引。索引的核心作用是“加速查询”,而高效的树结构正是实现这一点的关键。

答:

1. “树”形象的理解

可以从“现实中的树”类比数据结构的“树”,它和路边的树长得很像,只是“倒过来”了。

想象一棵简化的苹果树:

  • 最底下的 “树根” 是起点,对应数据结构中树的根节点(只有一个);
  • 从树根往上长的 “树干” 会分杈出 “树枝”,这些树枝就是父节点;
  • 树枝再分杈出更细的枝桠,这些细枝桠就是子节点(一个父节点可以有多个子节点);
  • 最顶端的 “苹果”(没有再分杈的部分),就是叶子节点。
    数据结构里的 “树”,就是这样一种 “分层、有分支” 的结构:
  • 所有节点(根、父、子、叶子)都像 “果实” 一样,存储着数据(或索引);
  • 节点之间的 “连接”(比如根到父、父到子)像 “树枝”,表示数据之间的关系;
  • 整个结构是 “单向” 的:只能从根往下找子节点,不能从子节点反推回根(就像苹果不会自己长回树枝)。

举个更具体的例子:

  • 如果用树存 “班级学生名单”,根节点可以是 “三年级”;
  • 根节点的子节点(父节点)可以是 “三班”“五班”;
  • “三班” 的子节点可以是 “男生组”“女生组”;
  • “男生组” 的子节点(叶子节点)就是具体的学生:“小明”“小李”…

这种结构的核心好处是:查找效率高。
比如想找 “三年级三班男生组的小明”,你不用遍历所有学生,只需从根(三年级)→ 三班 → 男生组 → 小明,一步一步按 “分支” 找,比在一堆乱序的名单里翻快得多。

2. 二叉查找树

“不合适”的树(Binary Search Tree)

特性:每个节点最多 2 个子节点(左小右大),查询时从根节点开始,比当前节点小就走左子树,大就走右子树。

问题:容易 “失衡”。比如插入一串递增数据,会退化成链表(左子树为空,只有右子树),查询效率从 O (logn) 暴跌到 O (n)(和遍历链表一样慢)。

为什么 MySQL 不用:数据库索引需要稳定高效的查询,二叉树的失衡问题无法满足。

3. 平衡二叉树

又叫:AVL树/红黑树
解决 “失衡”,但仍有局限

特性:在二叉查找树基础上,通过旋转保持 “平衡”(左右子树高度差不超过 1),确保查询效率稳定在 O (logn)。

问题:还是 “二叉”(每个节点最多 2 个子节点),导致树的 “高度过高”。
比如存 100 万条数据,平衡二叉树的高度大概是 20 层(2^20≈100 万)。

为什么 MySQL 不用:索引数据存在磁盘上,每次查询需要从磁盘读数据(一次 IO 读一个 “页”,比如 4KB)。树高 20 就意味着最多要读 20 次磁盘,IO 成本太高。

4. B 树

多路平衡查找树,降低高度

特性:

  • “多路”:每个节点可以有多个子节点(比如 100 个),不再是二叉。
  • “平衡”:所有叶子节点在同一层,避免某条路径过长。
  • 节点存 “数据”:每个节点不仅存索引键,还直接存对应的数据(或数据地址)。

优势:高度大大降低。比如每个节点有 100 个子节点,存 100 万数据,树高只需 3 层(100^3=100 万),最多 3 次 IO 就能查到数据,比平衡二叉树高效得多。

为什么 MySQL 不完全用它:范围查询不方便。比如查 “id 从 100 到 1000”,B 树需要回溯父节点找下一个范围,效率低。

5. B + 树

MySQL 索引的 “标准答案”
B + 树是 B 树的变种,专门为数据库索引优化设计,核心特性完美适配索引需求:

非叶子节点只存索引,不存数据:

  • 非叶子节点仅保留索引键(比如 id),不存实际数据,这样一个节点能存更多索引键,子节点数量更多(比如 200 个),树高更低(100 万数据只需 2-3 层),IO 次数更少。

叶子节点存完整数据,且首尾相连:

  • 所有实际数据只存在叶子节点,且叶子节点之间用链表连接(形成有序链表)。

对索引的核心价值:

  • 单值查询快:和 B 树一样,通过索引键快速定位到叶子节点,IO 少。
  • 范围查询无敌:比如查 “id>100”,找到第一个 id=100 的叶子节点后,直接顺着链表往后扫,不用回溯父节点,效率极高(MySQL 中范围查询很频繁,这是关键)。

6. MySQL 为什么选 B + 树做索引?

  1. 树高低,IO 次数少(磁盘读写效率高);
  2. 叶子节点有序相连,范围查询(如 between、in)效率远超其他树;
  3. 非叶子节点只存索引,节点存储密度高,进一步降低树高。

02. 索引的作用

答:

  1. 加速查询,降低IO:索引通过有序结构(如B+树)将分散的数据按索引键排序,查询时无需全表扫描,而是通过索引快速定位目标数据所在的磁盘位置,大幅减少磁盘IO次数(比如从扫描全表100万行降到扫描几十行);

  2. 保证数据唯一性,强化业务约束:通过唯一索引(含主键索引),数据库会强制约束索引键的值不重复,直接实现 “业务唯一标识” 需求(如用户 ID、订单号不可重复),避免手动校验的繁琐和风险。

  3. 优化排序与分组操作:索引本身是有序的,当查询包含order by(排序)、group by(分组)时,可直接利用索引的有序性避免全表数据的额外排序(减少内存 / 磁盘临时表开销),提升这类操作的效率。

03. 常见索引类型及其特性

答:

1. 主键索引

Primary Key

特性:

  • 强制唯一性(表中唯一标识一行数据),且不允许NULL值;
  • 一张表只能有一个主键索引;
  • InnoDB 中主键索引是聚簇索引(叶子节点直接存储整行数据),无需回表,查询效率最高。

适用场景:作为表的唯一标识(如用户 ID、订单 ID),确保数据唯一性并加速行查询。

2. 唯一索引

Unique Index

特性:

  • 确保索引列的值唯一,但允许NULL值(且NULL可出现多次);
  • 一张表可创建多个唯一索引;
  • 基于 B + 树结构,非聚簇索引(叶子节点存主键值,需回表查数据);
  • 查询时找到一个匹配值就停止扫描(无需确认是否有重复),比普通索引少了 “继续校验” 的步骤,回表前的索引定位效率更高。

适用场景:需唯一约束但非主键的字段(如手机号、邮箱,允许未填写即NULL)。

3. 普通索引

Normal Index

特性:

  • 最基础的索引类型,无唯一性约束,允许重复值和NULL;
  • 基于 B + 树结构,非聚簇索引,仅用于加速查询,不影响数据本身的约束;
  • 查询时需扫描所有匹配的索引节点(可能有多个重复值),再批量回表取数据,比唯一索引多了 “扫描重复索引” 的开销。

适用场景:高频查询的非唯一字段(如商品分类、用户昵称),单纯提升查询效率。

4. 联合索引

Composite Index

特性:

  • 由多个字段组合创建(如(a, b, c)),遵循最左前缀原则(查询需包含最左字段才能命中索引);
  • 索引按第一个字段排序,第一个字段相同则按第二个,以此类推;
  • 可覆盖多字段查询,减少回表(如查询a, b时,若联合索引包含a, b,则无需回表);
  • 效率依赖查询条件:仅命中最左前缀且为覆盖索引时(无需回表),效率接近普通索引;若未命中全部前缀或需回表,效率会下降。
  • 整体比普通索引慢的核心:索引结构是多字段排序,定位时需匹配多个字段的有序性,逻辑比单字段索引复杂。

适用场景:多字段组合查询(如 “按用户 ID + 订单状态查询”),需合理设计字段顺序(区分度高的字段放左侧)。

5. 前缀索引

Prefix Index

特性:

  • 仅对字符串字段的前 N 个字符创建索引(如index(name(10))),大幅节省存储空间,但索引选择性低(重复率高);
  • 适用于长字符串(如 URL、长文本),但可能降低索引选择性(重复率升高),且无法用于order by/group by或覆盖索引。
  • 查询时需扫描更多索引节点才能定位目标,且无法使用覆盖索引(必须回表),额外增加 IO 开销。

适用场景:长字符串字段的模糊查询(如like ‘abc%’),平衡空间与查询效率。

6. 全文索引

Full-Text Index

特性:

  • 针对大文本内容(如文章、评论)的关键词搜索,针对大文本分词匹配(而非like的字符匹配,非 B + 树精确查找,需先分词、匹配倒排索引,再关联原表数据);
  • 效率远高于like ‘%关键词%’,但仅支持MATCH() AGAINST()语法,且有最小 / 最大词长限制。
  • 逻辑复杂度远高于其他索引,仅适合关键词检索,单值精确查询效率远低于 B + 树结构的索引。

适用场景:全文检索需求(如博客系统的文章关键词搜索)。

7. 索引效率排序

这些索引类型,按照查询速度和效率,从高到低排序

基于InnoDB引擎、但是精确查询场景:
查询效率从高到低排序:主键索引 > 唯一索引 > 普通索引 > 联合索引(命中最左前缀 + 覆盖索引)> 前缀索引 > 全文索引
但是并非绝对,依旧需要结合使用场景进行判断。
排序前提:默认是「单值精确查询」,若为「范围查询」,主键索引和唯一索引的差距会缩小(范围查询需扫描多个节点)。

04. 聚簇索引与非聚簇索引

答:
聚簇索引与非聚簇索引的核心区别是:数据存储位置
聚簇索引,又叫做聚集索引,它的叶子节点存储整行数据;
非聚簇索引的叶子节点仅存储 “索引键 + 主键值”,需通过主键回表查询完整数据(覆盖索引除外)。

05. 最左前缀原则

答:
联合索引的最左前缀原则:
查询条件必须从联合索引的最左字段开始匹配,且连续匹配,才能用到索引的对应部分。

举个例子:联合索引(name, age, score)

  • 能命中索引的情况(从左到右连续匹配):

    • where name = '张三'(只用name,命中索引的name部分);
    • where name = '张三' and age = 20(用name+age,命中索引的name+age部分);
    • where name = '张三' and age = 20 and score > 90(用name+age+score,命中索引的全部三部分)。
  • 不能命中索引的情况(跳过左字段或不连续):

    • where age = 20(跳过最左的name,完全无法命中);
    • where name = '张三' and score > 90(跳过中间的age,只能命中name部分,score无法利用索引)。

联合索引含范围查询或模糊查询能否命中索引?
对于范围查询(大于、小于等)会导致“当前字段右侧的索引字段失效”,但左侧已匹配的字段仍能命中,(当前字段也能命中);
对于模糊查询,如果是前缀模糊(例如:"快乐大本营",like '快乐%'),能命中当前字段及左侧的索引,但当前字段右侧的索引失效;如果是后缀/全模糊查询(例如:%大本营%乐大%),会导致当前字段及右侧字段的索引都失效,仅当前字段左侧已匹配索引有效。

06. 索引的维护成本

答:
MySQL索引的维护成本本质是“空间换时间”的代价,主要集中在“写入性能损耗”、“存储空间占用”、“索引碎片维护”。索引数量越多,结构越复杂,则维护成本越高。

1. 写入操作(插入/更新/删除)的性能损耗

  • 写入数据时,不仅要修改表数据,还需同步维护索引的 B + 树结构。
  • 插入:可能触发 B + 树节点分裂(如节点满时),需重新组织索引排序;若为唯一索引,还需额外校验唯一性(扫描索引确认无重复)。
  • 更新:若更新的是索引列,需先删除旧索引条目,再插入新索引条目,相当于两次索引操作;非索引列更新不影响索引,但仍需维护聚簇索引的物理顺序(InnoDB)。
  • 删除:不会立即释放索引空间,仅标记为 “删除”,后续需通过碎片整理回收,且可能触发 B + 树节点合并(如节点数据过少时)。

2. 存储空间的额外占用

  • 索引需独立存储 B + 树结构,一张表的索引越多、字段越长,占用的磁盘空间越大。
  • 示例:大表的联合索引、长字符串的前缀索引,可能占用与数据本身相当的存储空间;聚簇索引虽无需额外存数据,但非聚簇索引(如唯一、普通索引)需存储主键值,叠加后空间开销显著。

3. 索引碎片与定期维护开销

  • 频繁插入 / 删除后,B + 树会产生 “空洞”(已删除但未释放的空间),导致索引碎片增多。
  • 碎片会降低查询效率(磁盘 IO 增多),需定期执行维护操作(如OPTIMIZE TABLE、重建索引),这些操作会锁表或占用大量 IO 资源,影响业务高峰期性能。

4. 优化器的决策负担

  • 表中索引过多时,MySQL 优化器需遍历所有可能的索引组合,评估最优查询计划,导致查询解析时间变长(尤其复杂查询场景)。

5. 如何优化维护策略

  • 不同索引的维护成本差异:联合索引 > 普通索引 > 唯一索引 > 主键索引(结构越复杂,维护时 B + 树调整逻辑越繁琐);前缀索引、全文索引的维护成本高于单字段索引(前者需处理部分字符,后者需维护分词倒排索引)。
  • 平衡维护成本的核心原则:避免 “过度索引”,仅为高频查询字段创建索引;优先选择窄索引(短字段、少字段组合),减少存储空间和维护开销;定期监控索引使用率,删除无效索引(如长期未被使用的索引)。

07. 回表查询与索引覆盖

答:
回表查询是 “非聚簇索引查询后需二次查聚簇索引拿完整数据” 的过程;
索引覆盖是 “查询字段(即所需的返回字段)全在索引中,无需二次查询” 的优化场景。
二者是 “需回表” 与 “免回表” 的对立关系,直接影响查询效率。

1. 回表查询(Bookmark Lookup)

1. 定义
当使用非聚簇索引(如唯一索引、普通索引、联合索引)查询时,若查询字段未完全包含在该索引中,需先通过非聚簇索引找到 “索引键 + 主键值”,再用主键值查询聚簇索引(主键索引) ,才能获取整行完整数据,这个 “二次查询” 的过程就是回表。

2. 核心逻辑(结合 InnoDB 引擎)

  • 非聚簇索引的叶子节点仅存储 “索引键 + 主键值”,不存完整行数据。
  • 若查询需要非索引字段(如用唯一索引email查user表的主键id和非索引字段name),必须通过主键值回聚簇索引 “兜底”,才能拿到name。

3. 示例

-- 表结构:id(主键)、email(唯一索引)、name(非索引字段)
select id, name from user where email = 'a@test.com';

第一步:通过唯一索引email(非聚簇)找到主键id=100
第二步:用id=100查询聚簇索引,拿到name字段,完成查询;
这两步共同构成回表查询,额外增加了一次聚簇索引查询的 IO 开销。

2. 索引覆盖(Covering Index)

1. 定义
当查询的所有字段(包括筛选条件、返回字段)都包含在某一个索引中时,则无需回表,仅通过该索引就能获取所有需要的数据,这个索引就是 “覆盖索引”,也叫紧凑索引,对应的查询就是覆盖索引查询

2. 核心优势
避免回表,减少一次磁盘 IO(聚簇索引查询),大幅提升效率;
索引数据量远小于全表数据,查询时扫描的数据量更少。

3. 示例

-- 联合索引:idx_email_name(email, name)(包含email和name字段)
select email, name from user where email = 'a@test.com';

查询的筛选字段email、返回字段name均在联合索引中;
直接通过该联合索引就能拿到所有需要的数据(返回字段),无需回聚簇索引,实现 “一次查询完成”。

3. 补充项

触发条件对比:

  • 回表:非聚簇索引 + 查询字段超出索引范围;
  • 索引覆盖:查询字段(筛选 + 返回)完全匹配某一索引(单字段索引或联合索引)。

实战价值:
索引覆盖是优化回表开销的核心手段,设计索引时可将高频查询字段加入联合索引(如idx_userid_status(user_id, status)),避免回表;
聚簇索引查询天然支持 “索引覆盖”(叶子节点存完整数据),无需回表,这也是其查询效率最高的原因之一。

易混点:
索引覆盖的关键是 “字段全包含”,与索引类型无关(单字段索引、联合索引均可作为覆盖索引);
前缀索引无法实现索引覆盖(仅存字段前 N 个字符,无法返回完整字段值)。

4. 不能触发索引覆盖示例

SELECT id, name, email FROM user WHERE email = 'xxx@qq.com';
-- 在user表中,id是主键,email是唯一索引,name是普通索引

结论:不能触发索引覆盖。
索引覆盖的核心要求是查询的所有字段(筛选条件 + 返回字段)必须完全包含在同一个索引中,跨索引无法实现覆盖。

如何修改才能触发索引覆盖?

  • 创建联合唯一索引 unique index idx_email_name(email, name)
  • 筛选字段email、返回字段emailnameid(主键会自动包含在非聚簇索引中),全部包含在这个联合索引里;
  • 查询时无需回表,直接通过该联合索引就能获取所有需要的数据,触发索引覆盖。

08. 如何合理使用索引?

以业务查询为导向,平衡查询效率与维护成本,避免 “过度索引” 或 “无效索引”。

  1. 优先为 “高频查询字段” 建索引,低频查询不建;
  2. 联合索引需遵循 “最左前缀 + 区分度优先(区分度高,重复值少)” 原则;
  3. 避免索引失效场景,确保索引被正确使用;
  4. 利用 “索引覆盖” 减少回表,提升效率;
  5. 根据字段特性选择合适的索引类型;
  6. 定期维护索引,清理无效索引。

合理使用索引的核心是 “按需设计、避免失效、注重维护”。
“索引不是越多越好,而是越合适越好”。

09. 索引失效场景

MySQL 索引失效的本质是:查询条件破坏了索引的有序性或匹配规则;
常见场景集中在:“字段操作”、“类型不匹配”、“查询语法不当”三类;
避免核心是:让查询条件贴合索引设计规则。

以下是常见索引失效场景 + 避免方法:

1. 对索引字段做函数 / 运算操作

失效场景:查询时对索引字段用函数(如substrdate_format)或数学运算,会导致 MySQL 无法利用索引的有序性,只能全表扫描。

  • 示例:where substr(name, 1, 3) = '张三'(name是普通索引)、where age + 1 = 20(age是普通索引)。

避免方法:将函数 / 运算移到等号右侧,或提前计算结果。

  • 优化后:where name like '张三%'where age = 19

2. 隐式类型转换

失效场景:查询条件中字段类型与传入值类型不匹配,MySQL 会自动做类型转换,导致索引失效。

  • 示例:where phone = '13800138000'(phone是int类型,字符串转数字)、where id = '100'(id是int,字符串转数字)。
    避免方法:确保查询值类型与字段类型完全一致。
  • 优化后:where phone = 13800138000where id = 100

3. 模糊查询(非前缀匹配)

失效场景:like的模糊查询若以%开头(后缀模糊 / 全模糊),会破坏索引有序性,导致索引失效;前缀模糊(%在末尾)可正常使用索引。

  • 示例:where name like '%张三'(后缀模糊,失效)、where name like '%张三%'(全模糊,失效)。
    避免方法:优先用前缀模糊查询;若需全模糊,改用全文索引(如fulltext index)或应用层分词。
  • 优化后:where name like '张三%'(前缀模糊)、MATCH(name) AGAINST('张三')(全文索引)。

4. 联合索引不满足最左前缀原则

失效场景:联合索引(如(a, b, c))需从左到右连续匹配,跳过左侧字段或不连续匹配,会导致索引失效或部分失效。

  • 示例:where b = 2 and c = 3(跳过最左a,全失效)、where a = 1 and c = 3(跳过中间b,仅a部分生效,c失效)。
    避免方法:查询条件需包含联合索引的最左字段,且按索引字段顺序匹配;若高频查询b + c,可单独建联合索引(b, c)
  • 优化后:where a = 1 and b = 2 and c = 3(全生效)、where b = 2 and c = 3(改用(b, c)联合索引)。

5. 范围查询后字段失效

失效场景:联合索引中,某字段用><>=<=between做范围查询后,其右侧的索引字段会失效。

  • 示例:where a = 1 and b > 2 and c = 3(联合索引(a, b, c)c失效)。
    避免方法:将范围查询字段放在联合索引的最右侧;若需多字段范围查询,改用覆盖索引或拆分查询。
  • 优化后:where a = 1 and b > 2(仅用a + b索引)、建联合索引(a, b, c)并确保查询字段覆盖(select a, b, c,避免回表)。

6. or连接非索引字段

失效场景:or连接的查询条件中,若有一个字段无索引,会导致整个查询无法使用索引(MySQL 会选择全表扫描)。

  • 示例:where name = '张三' or address = '北京'name有索引,address无索引,全失效)。
    避免方法:确保or连接的所有字段都有索引;或改用union all拆分查询(需字段一致)。
  • 优化后:where name = '张三' union all where address = '北京'address补建索引)。

7. is not null/not in/not exists

失效场景:对索引字段用is not null(部分场景失效)、not innot exists,会破坏索引的匹配逻辑,导致失效(is null通常可使用索引)。

  • 示例:where name is not nullname是普通索引,可能失效)、where id not in (1,2,3)id是主键索引,大数据量下失效)。
    避免方法:is not null改用union拼接非空结果;not in改用left join ... on ... is null;小数据量not in可接受,大数据量必优化。
  • 优化后:where id in (select id from user) union ...is not null替代)、select * from user u left join tmp t on u.id = t.id where t.id is nullnot in替代)。

8. 索引选择性差(失效等价场景)

失效场景:索引字段重复率极高(如 “性别” 字段,仅男 / 女),MySQL 优化器会判断 “全表扫描比索引查询更快”,主动放弃使用索引。

  • 示例:where gender = '男'gender建了索引,但全表 80% 是男性,索引失效)。
    避免方法:不针对低选择性字段建单字段索引;若需查询,将其作为联合索引的右侧字段(如(age, gender)),通过高选择性字段先过滤。

9. 补充

  1. 验证索引是否失效的核心方法:explain分析执行计划,若key字段为NULL或非目标索引,说明索引失效。
  2. 特殊情况:Innodb的聚簇索引(主键)即使有上述场景(如is not null),通常也不会完全失效,因聚簇索引的物理存储特性,优化器更倾向使用。
  3. 避免失效的核心原则:不破坏索引的有序性、不改变字段的原始形态、让查询条件贴合索引设计。

10. explain执行计划分析

explain 是 MySQL 分析 SQL 执行计划的核心工具。
通过输出 12 个字段(面试重点关注 7 个核心字段),可判断索引是否生效、查询是否全表扫描、是否存在文件排序 / 临时表等性能问题。

1. explain 核心作用

  1. 判断 SQL 是否使用了目标索引(避免索引失效);
  2. 识别全表扫描、文件排序、临时表等低效操作;
  3. 分析表的连接顺序、查询类型(简单 / 复杂查询);
  4. 预估查询扫描的行数,评估查询效率。

2. 7 个核心字段解析

1. type:访问类型

核心,判断查询效率的关键

含义:表示 MySQL 如何访问表中的数据(即查询方式),取值决定查询效率,从优到差排序:
system > const > eq_ref > ref > range > index > ALL

关键取值解读:

  • system:表中只有 1 行数据(如系统表),效率最高(罕见);
  • const:通过主键 / 唯一索引查询,匹配 1 行数据(如where id=100),高效;
  • eq_ref:多表连接时,被连接表通过主键 / 唯一索引匹配,每行只返回 1 行(如join on 主键);
  • ref:通过普通索引 / 联合索引前缀匹配,返回多行匹配数据(如where name=’张三’,name 是普通索引);
  • range:范围查询(><betweenin),只扫描索引的某一范围(比ref差,但比全表扫描好);
  • index:扫描整个索引树(索引全扫描),比ALL好(索引数据量小于全表);
  • ALL:全表扫描(最差),需避免(通常是索引失效或未建索引)。

判断标准:type至少要达到range级别,最优是refconst,出现ALL说明存在性能问题。

2. key:实际使用的索引

含义:表示 MySQL 实际选择的索引(若为NULL,说明未使用任何索引,索引失效或无合适索引);
判断标准:若key不是你设计的目标索引(如预期用idx_email,但keyNULL),说明索引失效,需排查原因(结合索引失效场景)。

3. rows:预估扫描行数

含义:MySQL 优化器预估的、查询需要扫描的行数(非精确值,但可反映效率);
判断标准:行数越少越好,若rows远大于表中实际数据量,可能是统计信息过时(需执行analyze table更新),或索引设计不合理。

4. extra:额外执行信息

核心!暴露性能隐患

含义:记录 SQL 执行的额外操作,重点关注 “好的标识” 和 “坏的标识”:

  • 优质标识:
    • Using index:触发覆盖索引,无需回表(高效,面试加分点);
    • Using index condition:索引下推(ICP),减少回表次数(高效)。
  • 性能隐患标识(必须避免):
    • Using filesort:需在内存 / 磁盘中排序(未利用索引有序性,如order by字段无索引);
    • Using temporary:创建临时表存储中间结果(如group by无索引、多表连接无合适索引,性能极差);
    • Using where:全表扫描后过滤数据(type=ALL时出现,说明无索引可用);
    • Using join buffer:多表连接时未用索引,使用连接缓冲区(低效)。

5. id:查询执行顺序

含义:表示查询中每个select子句的执行顺序(数字越大越先执行,相同数字按从上到下顺序);
应用场景:复杂查询(子查询、join)中,判断表的连接顺序是否合理(如小表驱动大表)。

6. select_type:查询类型

含义:区分简单查询和复杂查询,面试高频取值:

  • SIMPLE:简单查询(无子查询、无 union);
  • SUBQUERY:子查询(select中嵌套select);
  • DERIVED:派生表(from中嵌套select);
  • UNIONunion连接的第二个及以后的查询。

面试价值:说明查询的复杂程度,复杂查询(如多层子查询)可能导致优化器选择低效执行计划,需考虑拆分 SQL。

7. table:当前查询的表

含义:显示 SQL 查询的表名(或别名),多表连接时按id顺序显示表的执行顺序。

加项

  1. 如何判断索引是否生效:
    看key字段是否为目标索引(非NULL),同时type不是ALL/index

  2. 如何判断查询是否高效:
    typerange + keyNULL + rows值小 + extrafilesort/temporary

  3. 常见问题排查:

    • type=ALL + key=NULL:索引失效或未建索引,排查索引失效场景;
    • extrafilesortorder by/group by字段未建索引,需添加索引;
    • extratemporarygroup by无索引或多表连接无合适索引,优化索引设计。
  4. explain extended:在explain基础上增加filtered字段(过滤行数占比),filtered越高说明过滤效果越好;

  5. explain format=json:输出 JSON 格式的详细执行计划,适合复杂查询分析;

  6. 执行计划是 “预估” 而非 “实际”:优化器可能因统计信息过时、索引选择性差等误判,需结合实际执行耗时验证。

11. 升序索引与降序索引

在 MySQL 中,索引的升序(ASC)和降序(DESC)是通过创建索引时指定排序方向来设置的,核心目的是匹配查询中ORDER BY的排序需求,避免额外的文件排序(Using filesort)或反向扫描带来的额外开销。

ORDER BY排序时,如果排序字段存在索引,但是索引的顺序与排序的顺序不一致,则排序时导致优化器选择“反向扫描索引”,性能虽然比文件排序快非常多,但是依旧不如排序和索引同向的效果好。

比如:create_time字段存在索引idx_create_time,索引是升序(ASC)的,但是查询排序时,需要逆序排序(比如ORDER BY create_time DESC),那么优化器选择“反向扫描索引”。

1. 升序 / 降序索引的创建语法

MySQL 中创建索引时,默认是升序(ASC),可显式指定ASC(升序)或DESC(降序)。语法如下:

-- 升序索引(默认,可省略ASC)
CREATE INDEX idx_create_time_asc ON 表名(create_time ASC);

-- 降序索引(显式指定DESC)
CREATE INDEX idx_create_time_desc ON 表名(create_time DESC);

升序索引(ASC:索引中字段值按从小到大排序,适用于ORDER BY 字段 ASC的查询。
降序索引(DESC:索引中字段值按从大到小排序,适用于ORDER BY 字段 DESC的查询(如 “按创建时间逆序排序的列表”)。

这里面还存在着版本差异:

2. 5.7版本

5.7版本的限制与实践。

5.7 版本对降序索引的支持是 “语法层面” 的,实际存储仍为升序,查询时若用ORDER BY 字段 DESC,优化器会 “反向扫描升序索引”(而非直接使用降序索引),虽然比全表扫描 + 文件排序高效,但仍有一定开销。

场景:查询列表经常按create_time逆序排序(ORDER BY create_time DESC
在 5.7 中,即使创建DESC索引,实际效果与ASC索引差异不大,但仍建议显式创建DESC索引(为 8.0 升级兼容,且优化器可能优先选择):

-- 5.7中创建“伪降序索引”(语法支持,实际升序存储)
CREATE INDEX idx_create_time_desc ON order(create_time DESC);

-- 查询时,优化器会反向扫描该索引,避免文件排序
SELECT id, order_no FROM order 
WHERE status = 1 
ORDER BY create_time DESC 
LIMIT 10;

5.7 的替代方案(核心优化)

ORDER BY create_time DESC还需配合WHERE条件(如status=1),建议创建联合索引,将过滤字段放前面,排序字段放后面(显式指定DESC):

-- 联合索引:先过滤(status),再排序(create_time DESC)
CREATE INDEX idx_status_create_time_desc ON order(status, create_time DESC);

-- 该索引可同时优化WHERE和ORDER BY,避免全表扫描和文件排序
SELECT id, order_no FROM order 
WHERE status = 1 
ORDER BY create_time DESC 
LIMIT 10;

3. 8.0 版本

8.0 版本的关键优化:真正支持降序索引。

8.0 版本开始,DESC索引会真正按降序存储,查询时若ORDER BY 字段 DESC,可直接匹配降序索引,无需反向扫描,性能比 5.7 提升明显(尤其大数据量排序场景)。

场景:同样按create_time逆序排序

在 8.0 中,创建DESC索引后,ORDER BY create_time DESC会直接使用该索引,EXPLAINExtra列无Using filesort

-- 8.0中创建“真降序索引”(实际按create_time从大到小存储)
CREATE INDEX idx_create_time_desc ON order(create_time DESC);

-- 查询时直接匹配索引,无需排序
SELECT id, order_no FROM order 
ORDER BY create_time DESC 
LIMIT 10;

联合索引的降序支持(8.0 增强)
8.0 允许联合索引中部分字段指定DESC,更精准匹配复杂排序场景。

例如 “按status升序、create_time降序排序”:

-- 联合索引:status升序,create_time降序
CREATE INDEX idx_status_asc_create_time_desc ON order(status ASC, create_time DESC);

-- 完全匹配ORDER BY,无文件排序
SELECT id, order_no FROM order 
ORDER BY status ASC, create_time DESC 
LIMIT 10;

4. 实践建议

1. 优先匹配查询的排序方向:

  • 若查询常用ORDER BY create_time DESC(如订单列表默认 “最新在前”),直接创建create_time DESC索引(8.0 最佳,5.7 次之)。
  • 若排序字段同时有ASCDESC需求(极少),可分别创建升序和降序索引(但需平衡索引维护成本)。

2. 联合索引的排序字段放最后:

  • 索引遵循 “最左前缀原则”,过滤字段(WHERE条件)放前面,排序字段(ORDER BY)放后面,例如WHERE status=1 ORDER BY create_time DESC,联合索引为(status, create_time DESC)。

3. 避免过度创建降序索引:

  • 降序索引与升序索引的维护成本相同(都会占用磁盘空间,影响写入性能),仅对 “高频逆序排序” 的字段创建降序索引,低频场景无需单独创建(依赖优化器反向扫描即可)。

5. 总结

  • 创建方式:通过ASC(默认)或DESC关键字指定,语法简单。
  • 版本差异:5.7 仅语法支持降序索引(实际升序存储),8.0 真正支持(按降序存储,性能更优)。
  • 核心价值:匹配ORDER BY的排序方向,避免文件排序,尤其在大数据量的列表查询(如订单、日志)中,能显著提升性能。

回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关01 – 基础核心》
下一篇: 《面试笔记:MySQL 相关03 – SQL语法与查询优化》

喜欢面试笔记:MySQL 相关02 – 索引这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

面试笔记:MySQL 相关01 – 基础核心

感谢订阅陶其的个人博客!

基础核心

回目录: 《面试笔记:MySQL 相关目录》
下一篇: 《面试笔记:MySQL 相关02 – 索引》


01. 整体架构

以5.7版本为基础,8.0的更改会特别标注。

MySQL 采用分层架构,整体可分为 4 层。
连接层、服务层、存储引擎层、文件系统层。

1. 连接层

客户端连接层

  • 负责接收客户端(如Java程序、Navicat等)的TCP/IP连接请求,处理身份认证(用户名、密码、主机权限校验)。
  • 提供连接池机制,复用已建立的连接(避免频繁 TCP 握手 / 挥手的开销),同时管理连接状态(如空闲超时断开)。
  • 核心组件:连接器(验证用户名密码、权限)、连接池(复用已建立的连接,减少握手开销)。
  • 8.0版本变化:
    • 认证插件升级:默认使用caching_sha2_password替代 5.7 的mysql_native_password,加密强度更高,需客户端(如 JDBC 驱动)适配支持。
    • 连接池优化:增强连接复用效率,减少空闲连接占用的资源,同时支持 “连接属性动态修改”(无需重启连接即可调整部分参数)。

2. 服务层

核心处理层

  • 所有存储引擎共享的核心层,负责 SQL 的 “解析 – 优化 – 执行” 全流程,不直接操作数据,仅通过接口调用存储引擎。
  • 包含查询缓存(默认关闭)、解析器、优化器、执行器等核心逻辑。
  • 统一处理日志(如 binlog)、权限二次校验(执行 SQL 前再次确认权限)等通用逻辑。
  • 核心流程:接收 SQL 后,先查查询缓存(若命中直接返回)→ 解析器生成语法树(检查 SQL 语法)→ 优化器生成最优执行计划(如选择索引、连接方式)→ 执行器调用存储引擎接口执行计划。
  • 8.0版本变化:
    • 移除查询缓存组件:因 5.7 中查询缓存命中率极低(数据更新会清空对应表缓存),8.0 直接删除该模块,简化服务层逻辑,避免无效开销。
    • 优化器增强:架构层面支持更多优化规则(如复杂 JOIN 的执行计划调整)。
    • binlog 默认开启:5.7 中 binlog 默认关闭,8.0 默认启用,且架构上支持 binlog 的 “即时回放”(加速主从同步),无需额外配置。

3. 存储引擎层

数据存储层

  • 采用 “插件式架构”,可动态加载不同存储引擎(如 InnoDB、MyISAM),负责数据的实际存储、读取和事务管理。
  • 与服务层通过统一的handler接口交互,服务层无需关心底层数据存储格式(如 InnoDB 的表空间、MyISAM 的文件存储)。
  • 默认存储引擎为 InnoDB,架构上支持事务、行锁等特性的底层实现。
  • 8.0版本变化:
    • 强化 InnoDB 的架构适配:移除其他低效存储引擎(如 Federated),仅保留 InnoDB、MyISAM、Memory 等常用引擎,聚焦 InnoDB 的性能优化。
    • 架构层面支持 InnoDB 的新特性:如自增 ID 持久化(通过 redo log 架构实现)、隐藏索引(架构上支持索引的 “逻辑禁用”,不影响物理存储)。
    • 锁机制架构优化:行锁的范围判断逻辑在架构层面更精准,减少锁冲突(依赖与服务层的接口交互优化)。

4. 文件系统层

  • 负责将数据、日志等持久化到磁盘,依赖操作系统的文件系统(如 ext4、NTFS)。
  • 存储文件类型包括:表空间文件(InnoDB 的.ibd)、日志文件(redo log(重做日志)、binlog(二进制日志)、undo log(回滚日志))、配置文件(my.cnf)等。
  • 8.0版本变化:
    • 日志文件架构调整:redo log 默认存储路径优化,支持更大的日志文件大小(默认单个文件 1GB,5.7 默认 48MB),提升崩溃恢复效率。
    • 表空间文件优化:默认使用独立表空间(5.7 需手动配置),且架构上支持 “表空间加密”(透明数据加密 TDE),文件存储更安全。
    • 自增 ID 存储架构变更:5.7 中自增 ID 存于内存,8.0 架构上改为写入 redo log,重启后可通过日志恢复自增 ID 序列,避免重复。

02. 核心组件

1. 连接器

Connection Manager
核心功能:负责客户端连接的建立、管理与身份认证,是客户端与 MySQL 交互的 “入口”。

  • 接收 TCP 连接请求后,校验用户名、密码及客户端主机权限(基于mysql.user表),通过后分配连接线程。
  • 维护连接状态(如空闲、活跃),默认空闲超时时间为 8 小时(wait_timeout参数控制),超时后自动断开。
  • 认证依赖mysql_native_password插件(默认),加密强度一般,兼容性好。
  • 8.0 版本变化:
    • 默认认证插件升级为caching_sha2_password,采用 SHA-256 加密,安全性更高(需客户端驱动支持,如 JDBC 需 8.0 + 版本)。
    • 优化连接复用机制,减少空闲连接的资源占用,支持 “连接属性动态修改”(如无需重连即可调整部分会话参数)。

2. 查询缓存

Query Cache
核心功能:缓存 SQL 语句与结果集,相同 SQL(字节级一致)可直接返回缓存结果,减少重复计算。

  • 存在但默认关闭(query_cache_type=OFF),需手动开启;缓存以表为单位,表数据更新(增删改)时会清空该表所有缓存。
  • 局限性明显:仅适用于静态数据(如配置表),高并发写场景下命中率极低,反而因缓存维护消耗资源。
  • 8.0 版本变化:
    • 彻底移除查询缓存组件(相关参数如query_cache_size失效),原因是其实际应用价值低,移除后简化了服务层逻辑,减少无效开销。

3. 解析器

Parser
核心功能:对 SQL 语句进行语法分析,生成 “语法树”,确保 SQL 符合语法规则。

  • 解析过程包括:词法分析(识别关键字、表名、字段名等)→ 语法分析(检查 SQL 结构是否合法,如SELECT后是否有字段、WHERE是否搭配条件)。
  • 若语法错误(如关键字拼写错误),直接返回报错(如 “you have an error in your SQL syntax”)。
  • 8.0 版本变化:
    • 核心功能不变,但扩展了对新语法的支持(如WITH RECURSIVE递归查询、降序索引语法),解析效率略有优化(减少语法树生成的内存占用)。

4. 优化器

Optimizer
核心功能:基于语法树生成 “最优执行计划”,目标是最小化执行成本(CPU、IO 开销)。

  • 优化逻辑包括:选择合适的索引(如判断全表扫描 vs 索引扫描更快)、调整多表连接顺序(小表驱动大表减少中间结果集)、简化表达式(如a=1 and a=2直接判定为无效)。
  • 依赖表统计信息(如行数、数据分布),但统计信息更新不及时可能导致执行计划偏差。
  • 8.0 版本变化:
    • 增强统计信息:引入 “直方图”(Histogram),更精准记录数据分布(如字段值的频率),优化器对非均匀分布数据的索引选择更合理。
    • 优化规则扩展:支持复杂 JOIN(如多表嵌套连接)的执行计划调整,子查询优化更彻底(减少 “派生表” 的临时表开销)。

5. 执行器

Executor
核心功能:根据优化器生成的执行计划,调用存储引擎接口执行操作,并返回结果。

  • 执行前再次校验权限(避免连接建立后权限被修改导致的安全问题)。
    • 例如:检查用户是否有目标表的SELECT权限。
  • 通过统一接口(如handler::read_row)调用存储引擎,获取数据后进行过滤、聚合等处理(如WHERE条件过滤、GROUP BY分组)。
  • 8.0 版本变化:
    • 优化接口调用效率,减少与存储引擎的交互次数(如批量读取数据)。
    • 支持 “即时执行”(Instant Execution),对简单查询可跳过部分优化步骤,直接执行,提升响应速度。

6. 日志组件

核心日志模块
负责记录 MySQL 的操作和状态,支撑数据恢复、主从同步等功能。

1. binlog(二进制日志)

  • 5.7 版本:默认关闭,需手动开启(log_bin=ON)。记录所有数据修改操作(增删改、DDL),格式支持STATEMENT(语句)、ROW(行)、MIXED(混合),用于主从同步和时间点恢复。
  • 8.0 版本:默认开启。格式默认ROW(更安全),支持 “即时回放”(Binlog Instant),主从同步时可跳过部分无效日志,提升同步效率。

2. redo log(重做日志,InnoDB 依赖)

  • 5.7 版本:InnoDB 专属,记录数据页的物理修改,采用 “循环写” 机制(固定大小文件),保证崩溃后数据可恢复(先写日志再写磁盘,即 WAL 机制)。
  • 8.0 版本:默认单个日志文件大小从 48MB 增至 1GB,减少日志切换频率;支持 “并行写入”,提升高并发下的日志写入效率。

3. undo log(回滚日志,InnoDB 依赖)

  • 5.7 版本:记录数据修改前的状态,用于事务回滚和 MVCC(多版本并发控制),默认随表空间存储,可能因长期积累导致空间膨胀。
  • 8.0 版本:支持 “undo log 自动回收”(通过innodb_undo_log_truncate参数),无需手动清理,减少维护成本。

7. 权限组件

核心功能:管理用户权限,控制对数据库、表、字段的操作权限。

  • 权限存储在mysql库的系统表中(如userdbtables_priv),权限修改后需通过FLUSH PRIVILEGES刷新或重启生效(静态权限)。
  • 支持库级、表级、列级权限,但缺乏细粒度的动态权限(如管理特定日志的权限)。
  • 8.0 版本变化:
    • 引入 “动态权限”(如BINLOG_ADMINBACKUP_ADMIN),权限修改后即时生效,无需刷新或重启。
    • 权限检查逻辑优化,结合角色(Role)管理(5.7 后期引入但不完善,8.0 强化),可批量分配权限,简化权限管理。

8. 总结

MySQL 核心组件的核心逻辑(连接、解析、优化、执行、日志、权限)在 5.7 和 8.0 中保持一致。
8.0 的改进集中在:移除低效组件(查询缓存)、增强安全性(认证插件)、优化性能(优化器、日志)、简化维护(动态权限、undo 回收)等。

03. 存储引擎

MySQL 的存储引擎是负责数据存储、读取及底层特性实现的核心模块,采用 “插件式” 设计,不同引擎支持的功能(如事务、锁机制)差异显著。

1. InnoDB(默认)

InnoDB 是 MySQL 最常用(默认)的存储引擎,以事务支持、高并发为核心优势,5.7 和 8.0 均将其作为默认引擎,版本间优化集中在性能、可靠性和功能扩展。

核心通用特性(5.7 和 8.0 共通)

  • 支持事务(ACID 特性):通过 redo log(保证持久性)、undo log(保证原子性和隔离性)实现。
  • 行级锁:仅锁定修改的行(而非全表),适合高并发写场景(如电商订单更新)。
  • 聚簇索引:数据与主键索引物理存储在一起,查询主键时效率极高。
  • 外键约束:支持表间外键关联(如orders表关联users表的user_id)。

5.7 版本 InnoDB 特性

  • 自增 ID(AUTO_INCREMENT):存储在内存中,重启 MySQL 后可能因未持久化导致重复(需依赖 binlog 恢复,但存在风险)。
  • 索引限制:仅语法支持降序索引(DESC),实际仍按升序存储,查询降序数据时需额外排序。
  • 锁机制:间隙锁(Gap Lock)范围较宽泛,可能导致高并发下锁冲突增加(如批量插入相邻 ID 时)。
  • undo log:默认随表空间存储,长期运行后可能因未自动回收导致磁盘空间膨胀(需手动清理)。

8.0 版本 InnoDB 关键改进

  • 自增 ID 持久化:将自增 ID 写入 redo log,重启后可通过日志恢复,彻底解决 5.7 的重复问题。
  • 索引增强:
    • 真正支持降序索引(DESC),查询降序排序数据时无需额外排序,直接使用索引。
    • 新增 “隐藏索引”(INVISIBLE):可标记索引为隐藏(不影响物理存储),用于临时禁用索引测试性能(无需删除重建)。
  • 锁优化:间隙锁范围更精准,减少非必要锁定(如批量插入时仅锁定实际需要的区间),降低锁冲突。
  • undo log 自动回收:支持innodb_undo_log_truncate参数(默认开启),自动收缩过大的 undo log,减少人工维护成本。
  • 表空间加密:支持透明数据加密(TDE),表空间文件(.ibd)加密存储,提升数据安全性。

2. MyISAM

MyISAM 是早期 MySQL 的默认引擎,因不支持事务和行锁,逐渐被 InnoDB 替代,5.7 和 8.0 中仍保留但应用场景有限。

核心通用特性(5.7 和 8.0 共通)

  • 不支持事务和外键:仅适合无需事务保证的场景。
  • 表级锁:写操作(增删改)会锁定全表,读操作需等待写锁释放,并发写性能差。
  • 独立文件存储:数据存于.MYD文件,索引存于.MYI文件,可直接复制文件迁移表。

5.7 版本:仍有部分场景使用(如只读日志表),但已明确不推荐用于核心业务。
8.0 版本:进一步弱化 MyISAM,默认配置下性能优化倾向 InnoDB,且移除了部分对 MyISAM 的冗余支持(如全文索引的部分优化仅针对 InnoDB)。

3. Memory

内存引擎
数据存储在内存中,适合临时数据处理,性能极快但数据易失(重启丢失)。

5.7 版本特性

  • 支持哈希索引(默认)和 B + 树索引,哈希索引适合等值查询(=),不支持范围查询(>、<)。
  • 表大小受max_heap_table_size限制(默认 16MB),超出后会报错。

8.0 版本改进

  • 优化内存分配机制,减少小表的内存浪费。
  • 支持动态调整max_heap_table_size(无需重建表),更灵活适配临时数据大小。

4. CSV

逗号分割值引擎
数据以 CSV 格式文件存储(.csv),适合数据交换(如与 Excel、文本文件交互)。

5.7 版本特性

  • 表结构存于.frm文件,数据存于.csv文件,可直接用文本编辑器查看 / 修改。
  • 不支持索引和事务,仅适合简单的导入导出场景。

8.0 版本改进

  • 增强兼容性:支持 CSV 文件中包含换行符(需特殊处理),减少导入导出时的格式错误。

5. 总结

  • InnoDB:5.7 已具备事务、行锁核心能力,8.0 通过自增 ID 持久化、索引优化、锁细化等提升可靠性和性能,是所有业务的首选。
  • MyISAM:仅适合只读、低并发场景,8.0 中进一步被边缘化。
  • Memory/CSV:作为辅助引擎,8.0 主要优化了易用性(如动态调整内存表大小),核心功能无本质变化。

04. InnoDB存储引擎

InnoDB作为MySQL的默认且常用的搜索引擎,有如下的核心特性:

1. 事务支持(ACID)

  • 完全支持事务的原子性(A)、一致性(C)、隔离性(I)、持久性(D)。

2. 锁机制

  • 支持行级锁 + 间隙锁(Next-Key Lock),行级锁仅锁定修改行(高并发友好),间隙锁防止幻读。
  • 8.0 变化:间隙锁范围更精准,减少非必要锁定(如批量插入相邻 ID 时仅锁实际区间),降低锁冲突概率。

3. 聚簇索引

  • 数据与主键索引物理存储在一起,形成 B + 树结构,主键查询可直接获取数据。

4. 自增 ID(AUTO_INCREMENT)

  • 自增 ID 存储在内存中,MySQL 重启后可能因未持久化导致序列重复(需依赖 binlog 部分恢复)。
  • 8.0 变化:自增 ID 写入 redo log 持久化,重启后可通过日志恢复序列,彻底解决重复问题。

5. 索引特性

  • 支持 B + 树索引(默认)、全文索引、前缀索引;仅语法支持降序索引(DESC),实际仍按升序存储。
  • 8.0 变化:
    • 真正支持降序索引,查询降序排序数据时无需额外排序,直接复用索引。
    • 新增隐藏索引(INVISIBLE),可临时禁用索引(不删除),方便测试索引性能影响。

6. 日志依赖(redo log/undo log)

  • redo log:循环写的物理日志,记录数据页修改,保证崩溃恢复。
  • undo log:记录数据修改前的状态,用于事务回滚和 MVCC(多版本并发控制),默认随表空间存储,易膨胀需手动清理。
  • 8.0 变化:
    • redo log 默认单个文件大小从 48MB 增至 1GB,减少日志切换开销。
    • undo log 支持自动回收(innodb_undo_log_truncate默认开启),无需手动清理。

7. 外键约束

  • 支持表间外键关联(如:orders.user_id关联users.id),保证数据引用完整性。

8. 安全性增强

  • 无表空间加密功能,数据文件(.ibd)以明文存储。
  • 8.0 变化:新增透明数据加密(TDE),支持表空间文件加密存储,提升敏感数据安全性(如用户密码、支付信息表)。

05. 三大范式和反范式

MySQL 的三大范式(1NF、2NF、3NF)是关系型数据库设计的基础原则,目的是减少数据冗余、保证数据一致性、避免插入 / 更新 / 删除异常
反范式是有意打破范式规则,有策略地保留适量冗余,目的是减少多表 JOIN提升查询效率(尤其读多写少场景)。

1. 第一范式(1NF)

第一范式(1NF):字段原子化,不可再分。

  • 核心要求:表中所有字段的值必须是 “原子性” 的(不可再拆分为更小的数据单元)。
  • 目的:避免同一字段存储多维度信息,导致查询和修改混乱。
  • 例子:
    • 反例:用户表的address字段存储 “中国 – 北京 – 朝阳区”(可拆分为countrycitydistrict);
    • 正例:拆分为countrycitydistrict三个字段,每个字段不可再分。
  • 实践:1NF 是基础,几乎所有业务表都需满足(如:订单表的phone字段不存储 “固话 + 手机”,而是单独字段)。

2. 第二范式(2NF)

第二范式(2NF):消除 “部分依赖”,非主属性完全依赖主键。

  • 核心要求:在 1NF 基础上,表的主键必须是 “联合主键”(多字段组成),且所有非主属性必须完全依赖于整个主键,不能仅依赖主键的一部分(即消除 “部分依赖”)。
  • 目的:避免因主键部分字段变化导致的数据异常(如修改部分主键后,非主属性需联动更新)。
  • 例子:
    • 反例:订单项表(联合主键order_id+product_id)中,product_name仅依赖product_id(主键的一部分),属于部分依赖;
    • 正例:product_name应存储在产品表中,订单项表只存product_id,通过关联产品表获取名称(非主属性quantityprice完全依赖order_id+product_id)。
  • 实践:多对多关系的中间表(如 “用户 – 角色” 关联表)需满足 2NF,避免冗余存储角色名称等信息。

3. 第三范式(3NF)

第三范式(3NF):消除 “传递依赖”,非主属性不依赖其他非主属性。

  • 核心要求:在 2NF 基础上,所有非主属性必须直接依赖于主键,不能依赖于其他非主属性(即消除 “传递依赖”)。
  • 目的:避免因某个非主属性变化,导致其他非主属性需联动更新(如 A 依赖主键,B 依赖 A,则 B 传递依赖主键)。
  • 例子:
    • 反例:用户表(主键user_id)中,area_name依赖area_idarea_id依赖user_id,则area_name传递依赖user_id
    • 正例:area_name应存储在区域表中,用户表只存area_id,通过关联区域表获取名称(非主属性仅直接依赖user_id)。
  • 实践:用户表、商品表等核心表需满足 3NF,避免存储 “部门名称”“分类名称” 等可通过关联获取的字段。

4. 反范式

反范式是有意打破范式规则,有策略地保留适量冗余,目的是减少多表 JOIN提升查询效率(尤其读多写少场景)。

  • 核心思路
    通过在表中冗余存储其他表的字段,避免查询时关联多个表(JOIN 操作耗时,尤其大数据量时)。
  • 例子:
    • 电商商品列表页需展示 “商品名称 + 分类名称”,若严格遵循 3NF,需关联product表和category表;
    • 反范式优化:在product表中冗余category_name字段,查询时直接从product表获取,无需 JOIN。
  • 适用场景
    1. 高频查询、低频更新:如商品详情页(查询频繁,分类名称很少修改),冗余后查询性能提升 10 倍以上;
    2. 多表关联复杂:如订单列表需关联用户表、商品表、物流表,冗余 “用户名”“商品名” 后,查询从多表 JOIN 简化为单表查询;
    3. 统计分析场景:报表系统需聚合多维度数据,冗余存储聚合结果(如 “每月销售额”),避免实时计算。
  • 注意事项
    • 冗余字段需同步更新:如category_name修改后,需同步更新product表中的冗余字段(可通过触发器、Java 代码事务保证);
    • 控制冗余范围:只冗余高频查询的核心字段(如名称、状态),避免表过大(如冗余大文本字段会增加存储和 IO 成本)。

5. 总结

  • 范式:适合写多读少、数据一致性要求高的场景(如订单系统、用户中心),通过减少冗余降低更新异常风险;
  • 反范式:适合读多写少、查询性能敏感的场景(如电商列表、报表),通过可控冗余提升查询效率。
  • 实际开发中,很少严格遵循某一范式,而是混合使用(如核心交易表用 3NF 保证一致性,查询表用反范式提升性能)。

06. DDL、DML、DCL、DQL

MySQL 中 DDL、DML、DCL、DQL 是按操作类型划分的四大类 SQL 语言,分别对应:

  • DDL:数据库结构定义;
  • DML:数据操纵;
  • DCL:权限控制;
  • DQL:数据查询。

1. DQL:数据查询语言

Data Query Language。

  • 定义:用于从数据库中查询数据,不修改数据或结构,是业务系统中最频繁的操作。
  • 核心命令:SELECT(含WHEREJOINGROUP BYORDER BYLIMIT等子句)。
  • 作用:从表中提取所需数据,支撑业务展示(如列表页、详情页)、统计分析(如报表)等场景。
  • 实践要点:
    • 是 Java 开发中最常用的 SQL(如查询用户信息、订单列表),需结合索引优化(如WHERE条件加索引、避免SELECT *);
    • 复杂查询(多表关联、聚合)需用EXPLAIN分析执行计划,避免全表扫描或文件排序。

2. DML:数据操纵语言

Data Manipulation Language。

  • 定义:用于修改表中的数据(增、删、改),会改变数据内容,但不改变表结构。
  • 核心命令:
    • INSERT(新增);
    • UPDATE(修改);
    • DELETE(删除)。
  • 作用:处理业务数据的生命周期(如创建订单、更新状态、删除无效记录)。
  • 实践要点:
    • 操作会触发事务(默认自动提交,可通过BEGIN手动控制),需保证原子性(如订单创建时同时扣减库存,失败则回滚);
    • 批量操作优化:INSERTVALUES (),(),()批量插入(比单条循环高效),DELETE/UPDATE避免全表操作(加WHERE条件,如DELETE FROM log WHERE create_time < '2024-01-01');
    • 高频写入场景(如日志)需控制频率,避免锁表影响查询。

3. DDL:数据定义语言

Data Definition Language。

  • 定义:用于定义或修改数据库、表、索引等结构,会改变数据库的元数据(结构信息)。
  • 核心命令:
    • CREATE(创建,如CREATE TABLECREATE INDEX);
    • ALTER(修改,如ALTER TABLE ADD COLUMN);
    • DROP(删除,如DROP TABLE);
    • TRUNCATE(清空表)。
  • 作用:初始化数据库结构(如建表、加字段)、调整表结构(如新增索引、扩展字段)。
  • 实践要点:
    • 执行时可能锁表(尤其ALTER TABLE在 InnoDB 中,大表修改会阻塞读写),生产环境需在低峰期执行,大表建议用在线 DDL 工具(如 pt-online-schema-change);
    • 谨慎使用DROPTRUNCATE(不可逆,TRUNCATE会清空数据且不触发事务回滚);
    • 索引相关 DDL(CREATE INDEX)需评估必要性(索引提升查询但降低写入性能)。

4. DCL:数据控制语言

Data Control Language。

  • 定义:用于管理数据库用户权限和事务控制,控制谁能操作数据、操作范围。
  • 核心命令:`
    • GRANT`(授予权限);
    • REVOKE(撤销权限);
    • COMMIT(提交事务);
    • ROLLBACK(回滚事务)。
  • 作用:保障数据安全(如限制应用账号只能操作指定表)、控制事务一致性。
  • 实践要点:
    • 权限遵循 “最小原则”:应用程序账号只授予SELECT/INSERT/UPDATE等必要权限,禁止DROP/GRANT等高风险权限,避免用 root 账号直接连接业务系统;
    • 事务控制(COMMIT/ROLLBACK)需在 Java 代码中配合业务逻辑(如分布式事务场景,确保多库操作一致性)。

5. 总结

四类语言分工明确:

  • DQL:查数据(业务展示核心);
  • DML:改数据(业务操作核心);
  • DDL:改结构(架构维护核心);
  • DCL:控权限(安全保障核心)。

07. 数据库表字段类型

1. 核心字段类型及适用场景

1. 数值型:适合存储数字(整数、小数)

类型 特点(长度 / 范围) JDK8对应类型 适用场景
TINYINT 1 字节,范围 – 128 ~ 127
(无符号 0 ~ 255)
Byte 状态标识(如status:0 – 禁用、1 – 正常)、
性别(0 – 女、1 – 男)
INT 4 字节,范围 – 21 亿 ~ 21 亿 Integer 普通 ID(如user_idorder_id,中小规模业务足够)、数量(如count
BIGINT 8 字节,范围 ±9e18,
大约±90亿亿
Long 大整数 ID(如分布式 ID、雪花 ID)、
高频增长数据(如万亿级订单量)
DECIMAL(M,D) 高精度小数
(M 总长度,D 小数位)
java.math.BigDecimal 金额(如amountDECIMAL(10,2)表示
最多 10 位,2 位小数)、利率
FLOAT/DOUBLE 单 / 双精度浮点
(有精度损失)
Float/Double 非精确计算(如温度、重量,允许微小误差)

2. 字符串型:适合存储文本

类型 特点(长度 / 范围) JDK8对应类型 适用场景
CHAR(N) 固定长度(N 字节,0 ~ 255),
空格填充
String 长度固定的字符串(如手机号CHAR(11)、身份证号CHAR(18)
VARCHAR(N) 可变长度(0 ~ 65535),
存储实际长度
String 长度不固定的文本(如用户名VARCHAR(50)、地址VARCHAR(200)
TEXT 大文本(最大 64KB) String 较长文本(如商品描述、用户备注,不适合建索引)
MEDIUMTEXT/LONGTEXT 更大文本(16MB/4GB) String 超大文本(如文章内容、日志详情,慎用,会拖慢查询)

3. 日期时间型:适合存储时间

类型 特点(长度 / 范围) JDK8对应类型 适用场景
DATETIME 8 字节,范围 1000 ~ 9999 年,
无时区
java.time.LocalDateTime
(不推荐Date
业务时间(如订单创建时间create_time,不受服务器时区影响)
TIMESTAMP 4 字节,范围 1970 ~ 2038 年,
受时区影响
java.time.LocalDateTime
(不推荐Date
记录系统时间(如最后更新时间update_time,自动随时区转换)
DATE 3 字节,仅日期(年月日) java.time.LocalDate
(不推荐Timestamp
生日、到期日(如birthdayexpire_date
TIME 3 字节,仅时间(时分秒) java.time.LocalTime 时段记录(如会议时长、打卡时间)

4. 特殊类型:针对性场景

类型 特点(长度 / 范围) JDK8对应类型 适用场景
ENUM 枚举(存储整数,显示字符串) String 固定可选值(如pay_typeENUM('WECHAT','ALIPAY','CARD')
SET 集合(多选,最多 64 个值) String 多选项(如tagsSET('hot','new','discount')
BLOB 二进制数据(如图片、文件) byte[](字节数组) 小型二进制(如头像缩略图,大型文件建议存 OSS,库中只存 URL)

2. 经典面试点

1. 数值型:精度与范围陷阱

  • 金额为什么用DECIMAL而非FLOAT
    FLOAT是浮点型,存在精度损失(如0.1 + 0.2 = 0.300000004),而DECIMAL是精确小数,适合金额等强精度场景。

  • INTBIGINT怎么选?
    中小业务(千万级数据)用INT足够;分布式系统(如订单 ID 用雪花 ID,长度 18 位)必须用BIGINT,避免溢出。

2. 字符串型:长度与性能

  • CHARVARCHAR的核心区别?
    CHAR固定长度,查询快但浪费空间(如手机号用CHAR(11)VARCHAR(11)高效,无需计算长度);
    VARCHAR节省空间,但查询需额外解析长度,适合长度波动大的场景(如地址)。

  • 为什么不建议VARCHAR(255)滥用?
    MySQL 中VARCHAR(255)在某些引擎(如 InnoDB)中会按 255 字节分配临时内存,即使实际数据很短,也会浪费内存(尤其排序、JOIN 时),建议按实际需求设长度(如用户名VARCHAR(50))。

  • TEXT类型的坑?
    TEXT字段不适合建索引(即使建索引也只能取前 N 个字符),且查询时会额外 IO,大文本建议拆分表(如商品表存desc_id,关联单独的product_desc表存TEXT内容)。

3. 日期时间型:时区与范围

  • DATETIMETIMESTAMP怎么选?
    业务时间(如订单创建时间)用DATETIME(固定值,不受服务器时区影响);系统时间(如最后修改时间)用TIMESTAMP(自动更新,适配多时区部署)。

  • TIMESTAMP的 2038 年问题?
    因范围是 1970 ~ 2038 年,长期系统需注意(可改用DATETIME规避)。

4. 枚举类型ENUM的利弊

  • 优点:存储高效(用整数存字符串),约束数据合法性(只能选定义的值);
  • 缺点:修改枚举值需ALTER TABLE(DDL 操作,大表阻塞),不适合值频繁变化的场景(如活动状态,建议用TINYINT+ 字典表替代)。

3. 总结

字段类型选择的核心原则:“够用即可,避免冗余

  • 数字优先选小类型(如状态用TINYINT而非INT);
  • 字符串按长度固定与否选CHAR/VARCHAR,避免大文本字段拖累主表;
  • 日期根据时区需求选DATETIME/TIMESTAMP
  • 金额、ID 等关键字段必须保证精度和范围,避免溢出或精度丢失。

回目录: 《面试笔记:MySQL 相关目录》
下一篇: 《面试笔记:MySQL 相关02 – 索引》

喜欢面试笔记:MySQL 相关01 – 基础核心这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

面试笔记:MySQL 相关目录

感谢订阅陶其的个人博客!

MySQL高频问点

1. 基础核心

  • 整体架构
  • 核心组件
  • 存储引擎
  • InnoDB存储引擎
  • 三大范式和反范式
  • DDL/DML/DCL/DQL
  • 数据库表字段类型

2. 索引

  • 各种树
  • 索引的作用
  • 常见索引类型
  • 聚簇索引与非聚簇索引
  • 联合索引的最左前缀原则
  • 索引的维护成本
  • 回表查询与索引覆盖
  • 如何合理使用索引
  • 索引失效场景
  • explain执行计划分析
  • 升序索引与降序索引

3. SQL语法与查询优化

  • 子查询与连接查询
  • 子查询与join性能对比及适用场景
  • 复杂查询(group by/having、limit分页)的执行逻辑
  • order by 的排序原理
  • 慢查询的定位与分析
  • 复杂SQL的拆分与改写(大表分页优化:延迟关联、书签分页)
  • join优化(小表驱动大表、 避免 cross join)
  • count的性能差异(count(1)、count(*)、count(字段))
  • IN和JOIN哪个快
  • MySQL的函数

4. 事务和锁机制

  • 事务
    • ACID
    • 四大隔离级别的定义及实现
    • 默认隔离级别为何是可重复读?
    • 隔离级别与并发问题(脏读、不可重复读、幻读)的对应关系
  • 锁机制
    • 锁的分类
    • 行锁的触发条件
    • MVCC的原理
    • 死锁的产生原因与定位
    • 避免死锁的策略
    • 高并发下如何减少锁竞争

5. 性能调优

  • MySQL核心配置参数的含义与调优依据
  • 连接池配置
  • InnoDB 缓冲池
  • redo log 与 binlog 的协作
  • 刷盘策略对性能与安全性的影响
  • 磁盘IO优化
  • CPU与内存分配
  • 网络优化

6. 高可用与集群架构

  • 主从复制:
    • 复制原理
    • 复制模式
    • 复制延迟的原因与解决
  • 读写分离:
    • 读写分离的实现
    • 一致性问题
    • 分库分表的必要性与拆分策略
  • 高可用架构
    • 主从切换工具
    • 集群方案的适用场景
    • 云原生

7. 数据安全与运维

  • 备份与恢复:
    • 备份类型的选择
    • 基于binlog的时间点恢复流程
    • 大表备份的性能优化
  • 日志系统:
    • binlog
    • redo log
    • 慢查询日志与错误日志的分析
  • 故障付出:
    • 数据库宕机恢复流程
    • 数据一致性校验
    • 索引损坏的修复

8. 进阶特性与版本差异

  • MySQL 8.0新特性:
    • 窗口函数、
    • CTE、
    • 角色管理与权限细化、
    • innoDB的自增锁优化
  • 特殊场景处理:
    • 大表DDL
    • JSON类型的存储与查询优化
    • 地理信息(GIS)功能的实战应用

喜欢面试笔记:MySQL 相关目录这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

面试笔记:Spring Framework 相关

感谢订阅陶其的个人博客!

001. Spring 是如何解决循环依赖的?

答:
什么是循环依赖?
循环依赖指两个或多个 Bean 相互依赖(如 A 依赖 B,B 依赖 A)。
在创建A时,初始化A时需要注入B,那么就需要去创建B;
在创建B时,初始化B时需要注入A,那么就需要去创建A;
此时就形成了循环依赖(程序会死等),最终导致 Bean 创建失败。

如何解决循环依赖问题?
Spring 通过拆分Bean的创建过程,将实例化和初始化分开。

  1. 当创建A时,先实例化A,再对A进行初始化,此时发现需要注入实例B;
  2. Spring此时会将实例化但未完成初始化的A存到缓存中(此处引入三级缓存概念),此时的A是可以被引用的,但并不完整(因为没有完成初始化,b还没有注入);
  3. 此时Spring会去创建B,先实例化B,然后再对B进行初始化,此时发现需要注入实例A;
  4. 此时Spring会把存入缓存的A注入B中(虽然A未完成初始化,但是可以先行注入B,后面再完成A的初始化也是可以的),此时B就创建完成了;
  5. 当B的实例创建完成后,Spring会回到对A实例的初始化步骤来,将成功创建的B注入到A中,此时A也完成了初始化。
  6. 即此时完成了对A和B的实例化和初始化操作,成功解决了循环依赖的问题。

三级缓存是什么?如何被使用的?
这里存在三级缓存的概念,Spring寻找实例化好的bean时会按照一级、二级、三级的顺序依次查找。
注意:三级缓存中不会出现相同名称的bean,当上一级存入相同名称的bean时,下一级同名的bean会被自动删除。
一级缓存存放的是:实例化完成且初始化完成的实例;
二级缓存存放的是:实例化完成但初始化未完成的实例;
三级缓存存放的是:存放Bean 的工厂对象(ObjectFactory),用来生成早期的bean或其代理对象,同时保证三级缓存中不会出现同名的bean实例。

002. Spring Bean 的生命周期

答:
Spring 的完整生命周期(按执行顺序)
实例化 -> 属性注入 -> 生命周期回调 -> 初始化 -> 使用 -> 销毁

各核心阶段的作用

  1. 实例化:Spring 通过反射创建 Bean 的实例;
  2. 属性注入:Spring 将配置文件或注解中定义的属性值(如@Autowired 注入的依赖)赋值给 Bean 实例(此处会产生循环依赖问题);
  3. 生命周期回调(初始化前):Bean 实现 Aware 接口的回调,如 BeanNameAware(获取 Bean 的 id)、BeanFactoryAware(获取 BeanFactory 容器)、ApplicationContextAware(获取 ApplicationContext 上下文);
  4. 初始化:执行自定义初始化逻辑,优先级为:
    @PostConstruct 注解方法
    -> InitializingBean 接口的 afterPropertiesSet()
    -> XML 配置的 init-method 属性指定方法。
  5. 使用:Bean 实例存入 Spring 容器,供应用程序通过 getBean() 或依赖注入方法使用;
  6. 销毁:容器关闭时执行自定义销毁逻辑,优先级:
    @PreDestory 注解方法
    -> DisposableBean 接口的 destory()
    -> XML 配置的 destory-method 属性指定方法。

关键接口/注解说明

  • Aware 系列接口:用于 Bean 获取容器相关信息,无需手动调用,容器自动触发;
  • 初始化相关:@PostConstrut(注解式,最常用)、InitializingBean(接口式)、init-method(XML 配置式);
  • 销毁相关:@PreDestory(注解式)、DisposableBean(接口式)、destroy-method(XML 配置式)。

喜欢面试笔记:Spring Framework 相关这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

是“冒犯”,还是“艺术”?

感谢订阅陶其的个人博客!

刚才看了一篇关于脱口秀的公众号文章:《脱口秀变味记:当笑声里掺杂了太多杂音》。

里面发表了一些关于对现在脱口秀相关热点和舆论的看法。

我也有一些想要聊的,就简单聊一下。

我接触“脱口秀”不算早,但是也不算晚。

从王自健的《80后脱口秀》,到笑果的《脱口秀大会》和《吐槽大会》,再到现在的《喜剧之王》和《脱口秀和TA的朋友们》。

可以说,脱口秀是我平时比较喜欢看的一种综艺类型。同时我也喜欢听相声,只不过脱口秀我喜欢用看,而相声我喜欢用听,特别是入睡的时候。

一开始,我感觉脱口秀特别像是相声,特别是脱口秀里的“漫才”(即双人脱口秀),就更像是说相声了。

后来听多了,才发现脱口秀和相声有明显的不同,即使他们的表现形式确实很像。

以我现在粗浅的理解:

无论是传统的相声,还是创新的相声,其内容基本都具备一定的故事性。同时衍伸铺垫出包袱,使用扎实的基本功和老练的表演技巧表达出来。好的相声会让人听得津津有味,所以比较吃人生阅历、舞台经验和基本功。

而脱口秀首要注重的并非故事性,而是主题性。一段脱口秀可以围绕着一个主题各种展开,无论是横向用多个故事描述,还是死扣一个点进行深度挖掘,都是可以的。好的脱口秀能让人一直沉浸在某种情绪中,比如大量的梗让人笑的停不下来,或者段子让人感同身受从而产生共鸣等。但是脱口秀相对来说是比较吃文本、吃热点,甚至是吃表演技巧的。

好的相声就像是一杯醇香的酒,入口柔顺、回味甘甜、口齿生津、回味无穷;
好的脱口秀就像是一瓶夏日冷藏的可乐,巧妙的刺激多巴胺,能给人带来快乐、单纯的开心、让人精神愉悦、暂时忘掉烦恼。

当然这么区分这两者的区别是很粗浅的,这里也是只浅聊。

这一点比较深刻的区分可以从德云社相声演员阎鹤祥在脱口秀的舞台上说的脱口秀内容能听出来。


之前看脱口秀节目,可以说是我排解苦闷的一个很好的方式。

在这里我可以放松的去听,无防备的去感受段子里的精妙和笑点,我只负责笑就好了,不必像是上网冲浪那样需要带着明辨是非的眼睛和脑子。

以下只聊作品不聊人品。

卡姆那喷涌的原始蓬勃生命力;
庞博的发人深省的观察和高情商的表达;
呼兰的超强文本和缜密逻辑下的搞笑行为;
王建国的“谐音梗”盛世和“尼罗河畔法力无边的法老们”;
鸟鸟的“一个i人自述引发的集体共鸣”;
毛豆疯狂擦汗同时的“无比密集的梗和包袱”;
黑灯和小佳的“独特的另一视角的世界”;
漫才兄弟的“无厘头的搞怪表演”;
王勉的“颇具才华独树一帜的音乐脱口秀”;
童漠男的“无奈又搞笑的行业冥灯”;
周奇墨大魔王的一句“雷森图拜拜”;
杨蒙恩的“遍地是大王,短暂又辉煌”;
广智的“最起码这里他还有法律”的穷哥们段子;
小卉“无比炸场的淘汰感言”等等表演,都可以称之为脱口秀艺术。

即使有“冒犯”行为,也是为了服务脱口秀“艺术”本身。


可是现在的脱口秀,变味儿了。

可以说从“杨笠”开始,但其实在她之前就开始有了一些潜在的征兆,她是爆发舆论最猛的那一个。

而她之后,越来越多的“杂音”开始逐渐充斥脱口秀,让这一个新兴的还算单纯的艺术,迅速的像是清水里滴进了墨汁一样开始变色。

就感觉中文脱口秀这块蛋糕,前面那些人辛辛苦苦的做出来并做大了之后,现在谁都想来啃上一口,反正已经有人演示了“捷径”。

所以现在的脱口秀段子开始为了“冒犯”而“冒犯”,为了证明“政治正确”而去疯狂强调“政治正确”。

这个问题其实无论男女脱口秀演员,都有这样的问题。

他/她们的心思根本没有放在打磨文本、打磨表演上,而是在尝试如何能够快速的让自己有知名度,从而捞上一笔或者先火上一把再捞上一笔。

文本不行、表演不行、那就扯上“政治正确”的大旗、扯上“性别论”的大旗、扯上上班族“牛马”的大旗,从而与听众建立起“伪共鸣”,调动情绪比强化文本和表演来的容易太多了。

而且只要扯上了“政治正确”的大旗,便自然而然处于道德的“制高点”。此时即使有人发现了问题,也不敢直白的说出来,因为人们的情绪是很容易被调动的,此时的听众的理智是相对降低的(此处可以借鉴刘慈欣小说里的理论,当人群人数升高到一定程度的时候,集体意识的理智智商值反而会降低),这就让围攻“道德制高点”的人更容易被人们围攻,误解扭曲其原本的意思。


比如就用目前舆论最大的“两性”问题来具体谈谈。

其实我们欢迎女性脱口秀演员讲出两性的不同处境和问题,这其实是一个很好的主题。甚至是男性脱口秀演员也能去讲,这其实是一个值得深度挖掘探讨的,很好的一个社会话题或者说方向。

让每天上学或者上班的没有过多接触过异性的人们,多一些了解异性性格、行为、心理、处境等情况,也能为这些没怎么接触过异性的人们在接触异性的时候提供一些“科普级”的帮助。

但是现在的脱口秀节目中,部分演员完全是在通过疯狂消费“两性问题”并以“阅读理解式”的夹带私货、推行某性有罪论、捏造成为其受害者等方式去为观众带来更多的感官刺激和精神刺激,在这种刺激下,人们更容易忽略理性思考而产生所谓的“伪共鸣”,并被种下某种不正确观念的潜意识。

这其实早已脱离了表演艺术所需要的表达范畴,成为了为了火一把而不择手段的“便捷”方式。


在人们的朴素认知中,人们把rap普遍认为是不良艺术,rapper是一群不良少年。

那是因为rap圈子的一些人那种种“奇葩”行为造成的。

一手“临时抱佛脚”能成为“金曲”,叛逆脏话等成了行为标榜,这完全是在挑战社会的朴素的主流价值观。

自然不容易被社会所接受。

不信你看周杰伦的歌曲里也有rap,《听妈妈的话》、《霍元甲》、《本草纲目》等,都有rap的元素,你会认为这些歌曲的质量内容很差吗?

我想不会吧,这些无疑都是大众票选出来的金曲,所以你还认为rap本身有问题吗?

有问题的从来不是某种艺术形式,而是为了借助其为载体无所不用其极的火一把的“小丑”罢了。


脱口秀也是这样。

这些年来,脱口秀在中国的流媒体平台一直都是夹缝中求生存,究其原因并不是脱口秀本身存在某些根本性的问题,完全就是某些脱口秀演员的“作死”行为让脱口秀在人们朴素认知中渐渐滑向了“不良”的一边。

而现在的节目里,也开始充斥着“变味”的脱口秀,并且相比来看,那些“传统脱口秀”反而成了老实孩子。因为没有这些新兴观点的“刺激感”和“共鸣感”,造成了“劣币驱逐良币”的现象。

如果节目组或者这个行业的从业者不能及时的“刮骨疗毒”,那么当“良币”全部退出舞台的时候,也是脱口秀这种艺术形式彻底失去其生存土壤的时候。

喜欢是“冒犯”,还是“艺术”?这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

闲谈自话(4)

感谢订阅陶其的个人博客!

这又是一篇博主的“无病呻吟”,内容有些混乱,完全就是想到啥写啥,不喜欢的大家可以移步其他文章。


最近博主看了不少新闻。

或者换一种说法:博主为了不落后于时代,不被时代发展落下,“被迫”每天了解大量的新闻和信息。

每天看完热搜榜、新闻榜之后,会有一种“不够、我还要”的感觉。

不知道大家有没有一种感觉。

前些年,我们或在学校学习,或刚入职场工作。往往我们闷头去做一件事情,很久才能“抬起头”了解一次世界的时候,发现这个世界并没有什么太大的变化。

仿佛世界的发展速度和过去的几千年一样,至少不会有让人脱离认知的改变。

而近几年,时代的变化速度,正在呈现指数级的提升,和官方提出的“百年未有之大变局”理念无比契合。

别的不说,就说人工智能(聊天生成式人工智能),从chatgpt、到gpt-4、再到国产ai崛起、到gpt-4o、deepseek的横空出世、豆包等国产ai的普及使用,甚至据说现在deepseek已经跌落神坛。

这群雄逐鹿、风起云涌一般的变化,才历时短短的不到3年。

是不是感觉不可思议?

就感觉chatgpt已经是很久之前的东西了,其实chatgpt的首发时间是:2022年11月30日。

距这篇文章的发布时间,仅2年零8个月,满打满算还没到3年。

可以说,如果一个囚犯在2022年11月前被判入狱10年,等他2032年出来的时候,很可能像是穿越到未来世界一样,估计和刘姥姥初入大观园没什么两样。

哦不对,现在的监狱已经开始学习CAD制图啥的了,也挺与时俱进的。

话扯远了,咱们再说回来。

2年零8个月的时间够干什么的?

读完一个初中或者一个高中?

就连读完一个3年制的大专的时间都不够,更别说4年制的本科了。

听说现在很多本科院校甚至专科院校都加设了人工智能等相关的专业。

咱且先不论那些高校里教授人工智能课程的教师够不够“专业”。我先预测一波,估计很大一部分跟风的院校的人工智能课程,又是那些“人工智能的定义”、“人工智能的发展历史”、“人工智能的影响”等等可以说毫无用处的东西。

即使一些学校真心去教授一些实用的东西,但是按照现在的人工智能的发展速度,从编著印刷教科书、到开设课程教授学生,到学生毕业去实践,估计这些知识早已“过时”,或者说已经被时代淘汰了。

这一次或许并不是高校的课程落后于社会,而且社会发展太快,高校的教学模式跟不上。

我说这些并不是去否定高等院校的教学能力,而是在提出一种思考的方向:

当科技正以比人类学习接受其知识更快的速度发展,那么后面的人类如何才能学会并接着推动科技更进一步发展?

半路出家,不从底层学起?

可是多次的科技迭代告诉我们,颠覆性的科技创新,无一不是从底层颠覆的。

或许绝大部分的人只能停留在使用的层面上。

时代的车轮滚滚而过,势不可挡,不会因为任何人而有所停滞。

那我们普通人如何才能保证自己是坐在车里,而不是那被车轮无情碾过的蝼蚁?或者是燃烧室里那用于给“时代火车”提供动力的“煤炭”?

我们不得而知。

所以我才会“被迫”的每天查阅大量的新闻和信息,即使可能赶不上任何一个风口,但至少也能“死得明白”一些。


在接触了大量信息之后,我发现了很多陌生的但是挺有意思的东西。

比如二次元的“吧唧”、“痛包”、“谷子”、“Labubu”;“直播带货”、“短剧”、“文旅xx”、“搭子”、“饭圈”、“脱口秀”、“新国风”等。

这些不同于之前的网络流行语,而是实实在在的一种文化。

一种区别于传统概念上的文化。

一部部看起来无脑的短剧能让人一刷就是一天;
一句听着就不可信的“家人们”就能让人们纷纷掏钱包;
一种看上去就很廉价的别针铁皮徽章配上一个人物图案就能高价售卖、有价无市;
。。。

是现在人们的自持力或者抵抗诱惑的能力越来越不足,还是我们已经“老了”?看不懂现在“年轻人”的文化了?

我感觉,每一个时代人群有每一代的文化,这是无可厚非的,因为每一代人的生活环境不同,土壤不同,所以只要没有违反主流的社会价值观即可。比如饭圈文化,这就是不可取的。

英国科幻作家道格拉斯·亚当斯曾经说过一句话,完美地解释了年龄代沟的产生原因:

任何在我出生时已经有的科技都是稀松平常世界本来秩序的一部分;
任何在我 15-35 岁之间诞生的科技都是将会改变世界的革命性产物;
任何在我 35 岁之后诞生的科技都是违反自然规律甚至要遭天谴的!

代入现实就是:

70后出生记事时,生活都是困难的,所以带着一家人活下去是他们这一辈的任务,所以现在的很多70后不太能玩儿明白手机,但是却都还保留着勤俭节约的习惯;
80后出生记事时,赶上时代发展、制度改革,所以努力工作成为了他们的主要任务,他们这一辈的人基本没赶上什么好的政策,属于苦累和获得比最差的一代人;
90后出生记事时,开始了科技发展、信息化的发展,中国的网购也是起源于90年代,所以90后比70后80后更容易接受信息化的变化;
00后出生记事时,社会已经有了一定的信息化的基础建设,他们记事的时候,手机就已经是普遍之物,电脑也不再是不可及的物件,所以他们可以说出生就生活在信息化的时代;
10后出生时,全社会的信息化基建基本已经完成,他们记事开始,网购就已经是日常行为了,人工智能也开始萌芽了,所以他们开始无法理解“上个世纪”出生的人的理念;
而现在的20后,我女儿她们这一辈的人,从出生开始,人工智能已经在蓬勃发展了,机器人也已经达到了很高的水平。可以说对于我们来说,她们出生就在“罗马”。很难想象她们以后的生活是一种什么样的光景,她们的这一代人的新兴文化我们又是否能够接受。


看着这些的陌生的文化圈子和人声鼎沸的直播间,我真的感觉自己成了“守旧派”。

我印象中的社会不是这样子的,最起码主流不应该是这样的。

现在因为一些现实情况和决策影响问题,造成了现在学历和就业匹配度越来越大。

辛辛苦苦本科毕业,发现社会上根本不缺本科牛马,甚至是硕士学历在就业市场也在疯狂贬值。

反观自媒体,很多没有上过大学的人“都”能成为大主播,有挣大钱的机会。

这种幸存者偏差,导致学习无用论又开始在某些地方甚嚣尘上。

通过近两年研究生名额扩招和考研人数断崖式下跌的反差、以及考公人数爆发式增加就能看出来,现在的年轻人越来越清醒,学历再高也是为了工作服务的,工作是为了稳定的生活服务的,而没有什么工作比公务员更稳定和有保障的了,特别是在工作岗位如此“动荡”的时代。


所以,我很割裂。

一边是“全民娱乐”化带来的情绪冲击。

各种低质的短剧、短视频、直播等被算法裹挟着平等的分发给每一位身处信息茧房的年轻人,甚至针对性的推送。

给怀孕的妻子推送婆婆对儿媳妇儿如何如何恶毒;
给上班的丈夫推送自己工作辛苦妻子却如何如何不理解自己;
给婆婆推送儿媳妇会如何如何不孝顺自己;
给年幼的小孩推送大人们都是如何如何不理解自己,扼杀小孩子的天性;
给谈恋爱的女生推送各种节日不送礼物是“原罪”;
给谈恋爱的男生推送男方总在付出而女方总是索取;
等等。。。
算法的针对性可以说非常明显。

之前解释算法:算法会根据每个人喜欢的内容不同,进而有针对性的推送更多用户喜欢的东西,从而更加智能的服务用户;

而现在的算法,难都就真的没有夹带私货吗?真的没有在悄无声息之间引导和挑动用户的某些情绪吗?

就算真的没有夹带私货,那算法难道没有筛选和限制某种危险或者过度推送造成的风险的监管吗?

如果完全根据用户喜欢进而一味的进行推送,难道真的不怕造成更多的反社会人格出现吗?

不,他们不怕,他们还嫌不够。

因为这些算法都是商业公司开发的,他们的目的并不像他们宣传的那样光鲜亮丽。

更好的服务用户并不是他们最终的目的,他们最终的目的是让用户给他们掏钱、付费,而如何能让用户心甘情愿的掏钱?

那必然是调动情绪,人们在理性的时候会去判断得失,而一旦情绪“上头”,很多冲动消费就是这么来的。

至于无节制的被挑动情绪的后果,那就和算法“没关系”了,那是用户自己的事儿了,或者是整个社会的问题。你看,多讽刺。

另一边,是我还尚存一丝理智的传统观念。

好好上学、好好工作、努力赚钱、让家庭过上好日子。

走正路、走“稳”路、不走邪路、不投机取巧等等。

这些从小被灌输的思想和现实发生的碰撞,时常让我陷入迷茫。

从建国以来,这么多年,社会早已形成了完整的里外两面。

一边学校让你学习仁义礼智信,一边社会告诉你赚钱最重要,其他的都是屁话。

很少有人知道的社会真相:
老实人靠双手挣钱,永无出头之日;
聪明人靠钱生钱的赚钱,或许有发家的可能;
而资本家起家,更多的是靠“骗”,靠一个美好的但空虚的故事。

但是这些里层的规则违反了表层上学校教书育人,仁义礼智信的“初衷”,所以学校几乎都是只教知识,却从来不直接教人如何赚钱,甚至是如何挣钱。

导致很多工作常识只能到工作中才能学习,即使是大专或者职校也是。

因为它们不过是缓解就业压力的一种手段罢了。


果然,这个世界是复杂的、多面的。

小时候什么都不懂,所以看什么都是纯净的。

随着慢慢长大,知道的东西慢慢多了起来,了解的信息也越来越驳杂,对世界的认知反而是逐渐的模糊了,开始看不懂了。

也是,这毕竟是一个几十亿人组成的世界,想要一个人看得懂,那确实是不可能。

估计,我也就只能去尽力守护住自己的一方小世界的“安稳”了。

看不懂的,我尊重,但是我选择不看。

唯物了近30年,我发现唯心才是最舒服的生活方式。

自我、本我、超我?

无所吊谓,我就是我!

老了,我也是我。

喜欢闲谈自话(4)这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

分享一下本站的博主个人状态功能

感谢订阅陶其的个人博客!

我想在网站上显示自己当前的心情状态,就像微信的状态一样。

但是WordPress是不自带这个功能的,我就自己搞了一个。

效果:

一、配置清单

下面是我的配置清单,我只能保证最终效果在本配置下生效。

  • 博客平台: WordPress 6.8.0
  • 主题模板: Argon 1.3.5

以下是配置步骤:

二、编辑functions.php文件

打开博客后台 > 外观 > 主题文件编辑器 > functions.php,拉到最后,新增如下配置,并更新文件:

// ------------------------------------------------ 状态start ------------------------------------------------

// 添加状态设置菜单
function custom_status_menu() {
    add_menu_page(
        '状态设置',
        '状态',
        'edit_posts',
        'custom-status-settings',
        'custom_status_settings_page',
        'dashicons-smiley',
        6
    );
}
add_action('admin_menu', 'custom_status_menu');

// 注册设置(添加输入框验证逻辑)
function custom_status_register_settings() {
    register_setting(
        'custom-status-group', 
        'current_status',
        array(
            'sanitize_callback' => 'sanitize_custom_status' // 新增:处理状态值逻辑
        )
    );
    register_setting('custom-status-group', 'status_text_color');
    register_setting('custom-status-group', 'status_bg_color');
}
add_action('admin_init', 'custom_status_register_settings');

// 新增:处理状态值(优先使用输入框内容)
function sanitize_custom_status($input) {
    // 获取输入框的值
    $custom_input = isset($_POST['custom_status_input']) ? sanitize_text_field($_POST['custom_status_input']) : '';
    // 如果输入框有内容,返回输入框值;否则返回下拉框值
    return !empty($custom_input) ? $custom_input : $input;
}

// 设置页面内容(新增输入框)
function custom_status_settings_page() {
        $statuses = array(
            '<i class="fa fa-meh-o"></i> emo',                  // 情绪低落:中性表情(存在)
            '<i class="fa fa-bed"></i> 疲惫',                   // 疲惫:床(关联休息,存在)
            '<i class="fa fa-cloud"></i> 等天晴',               // 等天晴:云朵(存在)
            '<i class="fa fa-snapchat-ghost"></i> 活人微死',    // 消极状态:幽灵(存在)
            '<i class="fa fa-smile-o"></i> 美滋滋',             // 开心:微笑(存在)
            '<i class="fa fa-bolt"></i> 裂开',                  // 崩溃:闪电(存在)
            '<i class="fa fa-meh-o"></i> 发呆',                 // 茫然:中性表情(存在)
            '<i class="fa fa-lightbulb-o"></i> 胡思乱想',       // 思考:灯泡(存在)
            '<i class="fa fa-hand-peace-o"></i> 元气满满',      // 活力:比耶(存在)
            '<i class="fa fa-android"></i> bot',               // 机器人:安卓图标(存在)
            '<i class="fa fa-briefcase"></i> 搬砖ing',          // 工作:公文包(存在)
            '<i class="fa fa-book"></i> 沉迷学习',              // 学习:书本(存在)
            '<i class="fa fa-spinner"></i> 忙',                 // 忙碌:旋转加载(存在)
            '<i class="fa fa-coffee"></i> 摸鱼ing',             // 摸鱼:咖啡(存在)
            '<i class="fa fa-child"></i> 带娃',                 // 带娃:小孩图标(存在)
            '<i class="fa fa-shield"></i> 拯救世界',            // 守护:盾牌(存在)
            '<i class="fa fa-home"></i> 宅',                    // 宅家:家(存在)
            '<i class="fa fa-bed"></i> 想睡觉',                 // 困倦:床(存在)
            '<i class="fa fa-moon-o"></i> 困死',                // 极度困倦:月亮(存在)
            '<i class="fa fa-music"></i> 听歌ing',              // 听歌:音乐符号(存在)
            '<i class="fa fa-rocket"></i> 逃离地球'             // 逃离:火箭(存在)
        );

    $color_presets = array(
        'text' => array(
            '#FFFFFF' => '白色',
            '#D1D5DB' => '浅灰',
            '#9CA3AF' => '中灰',
            '#4B5563' => '深灰',
            '#000000' => '黑色',
        ),
        'bg' => array(
            '#FFF9C4' => '浅柠黄', '#E8F5E9' => '嫩芽绿',
            '#FFD6BA' => '暖橙', '#C8E6C9' => '浅草绿', '#B3E5FC' => '天青蓝', '#E1BEE7' => '淡罗兰', '#FFCDD2' => '浅绯红',
            '#FFE0B2' => '蜜橙', '#F5F5F5' => '米白灰', '#BBDEFB' => '雾蓝', '#D7CCC8' => '浅棕灰', '#F8BBD0' => '柔粉', '#C5CAE9' => '淡靛蓝',
            '#9FA8DA' => '灰蓝', '#CE93D8' => '灰紫', '#A1887F' => '深棕灰', '#EF9A9A' => '绯红', '#7986CB' => '靛蓝', '#5C6BC0' => '暗靛蓝', '#616161' => '深炭灰',
        )
    );
    ?>
    <div class="wrap">
        <h1>个人状态设置</h1>
        <form method="post" action="options.php">
            <?php settings_fields('custom-status-group'); ?>
            <?php do_settings_sections('custom-status-group'); ?>
            <table class="form-table">
                <tr valign="top">
                    <th scope="row">当前状态</th>
                    <td style="display: flex; gap: 10px; align-items: center;">
                        <!-- 原有下拉框 -->
                        <select name="current_status" id="current_status" style="padding:5px;min-width:200px;">
                            <option value="">-- 选择状态 --</option>
                            <?php 
                            $current = get_option('current_status');
                            foreach($statuses as $status) {
                                // 下拉框默认选中状态(排除手动输入的情况)
                                $is_custom = !in_array($current, $statuses);
                                $selected = (!$is_custom && $current == $status) ? 'selected' : '';
                                echo "<option value='$status' $selected>$status</option>";
                            }
                            ?>
                        </select>

                        <!-- 新增手动输入框 -->
                        <input 
                            type="text" 
                            name="custom_status_input" 
                            id="custom_status_input" 
                            placeholder="或手动输入状态" 
                            style="padding:5px;min-width:100px;"
                            value="<?php 
                                // 回显手动输入的值(如果是自定义状态)
                                $current = get_option('current_status');
                                echo !in_array($current, $statuses) ? esc_attr($current) : '';
                            ?>"
                        >
                    </td>
                </tr>
                <tr valign="top">
                    <th scope="row">文字颜色</th>
                    <td>
                        <select name="status_text_color" id="status_text_color" style="padding:5px;min-width:200px;">
                            <?php 
                            $current_text_color = get_option('status_text_color', '#ffffff');
                            foreach($color_presets['text'] as $value => $label) {
                                $selected = ($current_text_color == $value) ? 'selected' : '';
                                echo "<option value='$value' $selected style='color: $value;'>$label</option>";
                            }
                            ?>
                        </select>
                    </td>
                </tr>
                <tr valign="top">
                    <th scope="row">背景颜色</th>
                    <td>
                        <select name="status_bg_color" id="status_bg_color" style="padding:5px;min-width:200px;">
                            <?php 
                            $current_bg_color = get_option('status_bg_color', '#8d9deb');
                            foreach($color_presets['bg'] as $value => $label) {
                                $selected = ($current_bg_color == $value) ? 'selected' : '';
                                echo "<option value='$value' $selected style='background-color: $value; color: white;'>$label</option>";
                            }
                            ?>
                        </select>
                    </td>
                </tr>
            </table>
            <?php submit_button(); ?>
        </form>
    </div>
    <?php
}

// 为后台状态配置页面添加自定义CSS
function custom_status_admin_css() {
    // 只在状态配置页面加载该CSS
    global $pagenow;
    if ($pagenow == 'admin.php' && isset($_GET['page']) && $_GET['page'] == 'custom-status-settings') {
        ?>
        <style type="text/tailwindcss">
            @layer utilities {
                .status-color-option {
                    @apply relative pl-8 text-black;
                }
                .status-color-option::before {
                    @apply absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 rounded border border-gray-200;
                }
            }
        </style>
        <style type="text/css">
            /* 颜色选项显示优化 */
            #status_text_color option,
            #status_bg_color option {
                @apply status-color-option;
            }

            /* 文本颜色选项的文字和背景 */
            #status_text_color option[value="#FFFFFF"] { background-color: #6B7280 !important; color: #FFFFFF !important; }
            #status_text_color option[value="#D1D5DB"] { color: #D1D5DB !important; }
            #status_text_color option[value="#9CA3AF"] { color: #9CA3AF !important; }
            #status_text_color option[value="#4B5563"] { color: #4B5563 !important; }
            #status_text_color option[value="#000000"] { color: #000000 !important; }

            /* 文本颜色选项的颜色块 */
            #status_text_color option[value="#FFFFFF"]::before { background-color: #FFFFFF; }
            #status_text_color option[value="#D1D5DB"]::before { background-color: #D1D5DB; }
            #status_text_color option[value="#9CA3AF"]::before { background-color: #9CA3AF; }
            #status_text_color option[value="#4B5563"]::before { background-color: #4B5563; }
            #status_text_color option[value="#000000"]::before { background-color: #000000; }

            /* 背景颜色选项的颜色块 */
            #status_bg_color option[value="#FFF9C4"]::before { background-color: #FFF9C4; }
            #status_bg_color option[value="#E8F5E9"]::before { background-color: #E8F5E9; }
            #status_bg_color option[value="#FFD6BA"]::before { background-color: #FFD6BA; }
            #status_bg_color option[value="#C8E6C9"]::before { background-color: #C8E6C9; }
            #status_bg_color option[value="#B3E5FC"]::before { background-color: #B3E5FC; }
            #status_bg_color option[value="#E1BEE7"]::before { background-color: #E1BEE7; }
            #status_bg_color option[value="#FFCDD2"]::before { background-color: #FFCDD2; }
            #status_bg_color option[value="#FFE0B2"]::before { background-color: #FFE0B2; }
            #status_bg_color option[value="#F5F5F5"]::before { background-color: #F5F5F5; }
            #status_bg_color option[value="#BBDEFB"]::before { background-color: #BBDEFB; }
            #status_bg_color option[value="#D7CCC8"]::before { background-color: #D7CCC8; }
            #status_bg_color option[value="#F8BBD0"]::before { background-color: #F8BBD0; }
            #status_bg_color option[value="#C5CAE9"]::before { background-color: #C5CAE9; }
            #status_bg_color option[value="#9FA8DA"]::before { background-color: #9FA8DA; }
            #status_bg_color option[value="#CE93D8"]::before { background-color: #CE93D8; }
            #status_bg_color option[value="#A1887F"]::before { background-color: #A1887F; }
            #status_bg_color option[value="#EF9A9A"]::before { background-color: #EF9A9A; }
            #status_bg_color option[value="#7986CB"]::before { background-color: #7986CB; }
            #status_bg_color option[value="#5C6BC0"]::before { background-color: #5C6BC0; }
            #status_bg_color option[value="#616161"]::before { background-color: #616161; }
        </style>
        <?php
    }
}
add_action('admin_head', 'custom_status_admin_css');

// ------------------------------------------------ 状态end ------------------------------------------------

三、编辑sidebar.php文件

打开博客后台 > 外观 > 主题文件编辑器 > 编辑sidebar.php,找到你头像的位置,新增如下配置,并更新文件:

位置确定技巧:如果点击你的头像能跳转你的“关于我”的页面,那么你复制“关于我”的网址,搜索这个文件,就能确定:

<!-- 状态代码start -->
                                <?php $status = get_option('current_status'); ?>
                                <?php $text_color = get_option('status_text_color', '#ffffff'); ?>
                                <?php $bg_color = get_option('status_bg_color', '#8d9deb'); ?>
                                <?php if (!empty($status)): ?>
                                    <!-- 关键修改:用 wp_kses_post() 替换 esc_html(),允许<i>标签解析 -->
                                    <span class="status-badge" style="color: <?php echo esc_attr($text_color); ?>; background-color: <?php echo esc_attr($bg_color); ?>;">
                                        <?php echo wp_kses_post($status); // 保留HTML标签,正常渲染图标 ?>
                                    </span>
                                <?php endif; ?>
                                <!-- 状态代码end -->

四、新增自定义CSS样式

打开博客后台 > 外观 > 自定义 > 额外CSS > 拉到最后新增如下内容并发布:

/* 状态代码start */
.profile-avatar {
    position: relative;
    display: inline-block;
}

.status-badge {
    position: absolute;
    left: calc(100% - 90px);
    top: 63px;
    font-size: 12px;
    padding: 2px 8px;
    border-radius: 12px;
    box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    white-space: nowrap;
    text-align: left;
}

/* 状态代码end */

这个lefttop部分,可以根据“状态”标签的实际位置进行适当调整。

五、开始分享个人状态

上面配置好之后,刷新后台,会增加一个菜单:

选择好之后,保存更改。

前端页面就能显示:

预设的状态带有图标,输入的状态没有图标。

如果您想更换其他状态或者颜色,可以修改上面在functions.php的代码。

喜欢分享一下本站的博主个人状态功能这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

分享一下本站“朋友圈”的制作过程

感谢订阅陶其的个人博客!

本来我很少会写建站相关的博文,因为这些在网上一翻一大堆,写了也是重复造轮子。

不过昨天有个博主问到了本站的“朋友圈”是如何实现的,正巧这个“朋友圈”的创建并非是完全参考某一个教程搭建的,中间也有不少我的魔改,所以就记录一下。

PS1:本站“朋友圈”其实是对订阅的其他网站的RSS的内容的一个集合展示。

PS2:实现原理:

  1. 部署 FreshRSS 用于订阅其他博主/网站的 RSS 订阅源;
  2. 使用宝塔做了两个定时任务:
    1. 一个定时任务让 FreshRSS 刷新订阅源的最新内容;
    2. 另一个定时任务是将最新的前 n 条内容覆写到一个指定的 json 文件中。
  3. 在网站后台(WordPress)魔改了说说模块文件,其目的是读取的这个 json 内容并展示;
  4. 最后就形成了我现在的 “朋友圈” 的效果。

PS3:博主不会php语言,所以下面的魔改部分都是我摸索着配出来的,本着能用就行的态度写的,所以会有很多写的不对的地方,多多包涵,欢迎指正。

一、配置清单

下面是我的配置清单,我只能保证最终效果在本配置下生效。

  • 服务器系统: CentOS 7.9
  • 博客平台: WordPress 6.8.0及之后
  • 主题模板: Argon 1.3.5
  • 配置托管平台: 宝塔Linux面板(没有配置托管平台的,其过程也可以手搓)
  • RSS订阅工具: Fresh RSS

二、安装配置FreshRSS

点击跳转☞:【FreshRSS官网】

点击跳转☞:【FreshRSS官方安装文档】

点击跳转☞:【FreshRSS官方下载页面(发文时版本)】

2.1 下载FreshRSS

打开下载页面,拉到最下面,根据自己的需要下载zip或者tar.gz版本。

2.2 部署FreshRSS

  1. 将下载的压缩包上传到centos的某目录下并解压;
  2. 然后使用宝塔,点击“网站” > “添加站点”;
    1. “域名”:可以提前对主域名进行解析出一个二级域名,可以加SSL证书做成HTTP的;
    2. “根目录”:选择刚才解压的目录;
    3. 其他的默认即可。
  3. 创建好站点之后,点击站点后的“配置”,你可以根据需要进行后续配置,比如配置SSL证书;
  4. 默认使用80和443端口,记得开启防火墙,做好二级域名的DNS解析。这样的话,你就可以通过前面的二级域名访问到自己搭建的FreshRSS网站了。

2.3 添加RSS订阅源

根据需要添加“分类”,或者添加“订阅源”,此处的订阅源就是其他站点的RSS地址,订阅源的可选择分类就是上面自己添加的分类。

添加完订阅源之后,返回首页刷新就会加载最新的订阅源的内容。

到这一步,你就成功的能在FreshRSS看到其他站点的文章了。

但是FreshRSS有一个问题,就是不能主动刷新订阅源,需要我们手动点击页面的【⟳】按钮才能刷新。

我们要的是定时自动刷新,所以我们还需要做相关配置,详情见下面第六节。

2.4 配置FreshRSS

  1. 打开你的FreshRSS的域名,并进行登陆。

  2. 点击 设置 > 管理 > 认证;

    1. 勾选“允许 API 访问”并提交。
  3. 点击 设置 > 账户 > API管理;

    1. 设置密码并提交保存,记住设置的api密码。
    2. 复制网址,将在下一步配置文件中用于${网址1}

  4. 在自己站点(WordPress)根目录下创建一个php文件,用于放FreshRSS api调用函数,例如:rss.php。
    例如:

    • 我的博客网站根目录为:/www/wwwroot/www.tqazy.com
    • 我的FreshRSS根目录为:/www/wwwroot/rss.tqazy.com
      那么我就在 /www/wwwroot/www.tqazy.com 下创建创建一个文件:rss.php,内容如下:
<?php
/**
 * 获取最新订阅文章并生成JSON文件
 */
function getAllSubscribedArticlesAndSaveToJson($user, $password)
{
$apiUrl = '${网址1}';
    $loginUrl = $apiUrl . '/accounts/ClientLogin?Email=' . urlencode($user) . '&Passwd=' . urlencode($password);
    $loginResponse = curlRequest($loginUrl);

    // 处理可能的cURL错误
    if (isset($loginResponse['error'])) {
        die('登录请求失败: ' . $loginResponse['error']);
    }

    if (strpos($loginResponse, 'Auth=') !== false) {
        $authToken = substr($loginResponse, strpos($loginResponse, 'Auth=') + 5);
        $articlesUrl = $apiUrl . '/reader/api/0/stream/contents/reading-list?&n=1000';
        $articlesResponse = curlRequest($articlesUrl, $authToken);

        // 处理可能的cURL错误
        if (isset($articlesResponse['error'])) {
            die('获取文章失败: ' . $articlesResponse['error']);
        }

        $articles = json_decode($articlesResponse, true);
        if (isset($articles['items'])) {
            usort($articles['items'], function ($a, $b) {
                return $b['published'] - $a['published'];
            });
            $subscriptionsUrl = $apiUrl . '/reader/api/0/subscription/list?output=json';
            $subscriptionsResponse = curlRequest($subscriptionsUrl, $authToken);

            // 处理可能的cURL错误
            if (isset($subscriptionsResponse['error'])) {
                die('获取订阅失败: ' . $subscriptionsResponse['error']);
            }

            $subscriptions = json_decode($subscriptionsResponse, true);
            if (isset($subscriptions['subscriptions'])) {
                $subscriptionMap = array();
                foreach ($subscriptions['subscriptions'] as $subscription) {
                    $subscriptionMap[$subscription['id']] = $subscription;
                }
                $formattedArticles = array();
                foreach ($articles['items'] as $article) {
                    // 去掉以 http 或 https 开头的链接
                    $contentWithoutLinks = preg_replace('/https?:\/\/[^\s?#]*?\.(jpg|jpeg|png|gif|mp4|mp3|webm|ogg|wav|flac|svg|bmp)\b/i', '', $article['summary']['content']);
                    $desc_length = mb_strlen(strip_tags(html_entity_decode($contentWithoutLinks, ENT_QUOTES, 'UTF-8')), 'UTF-8');
                    if ($desc_length > 20) {
                        $short_desc = mb_substr(strip_tags(html_entity_decode($contentWithoutLinks, ENT_QUOTES, 'UTF-8')), 0, 99, 'UTF-8') . '...';
                    } else {
                        $short_desc = strip_tags(html_entity_decode($contentWithoutLinks, ENT_QUOTES, 'UTF-8'));
                    }

                    $formattedArticle = array(
                        'site_name' => $article['origin']['title'],
                        'title' => $article['title'],
                        'link' => $article['alternate'][0]['href'],
                        'time' => date('Y-m-d H:i', $article['published']),
                        'description' => $short_desc,
                    );

                    $subscriptionId = $article['origin']['streamId'];
                    if (isset($subscriptionMap[$subscriptionId])) {
                        $subscription = $subscriptionMap[$subscriptionId];
                        $iconUrl = $subscription['iconUrl'];
                        $filename = '${网址2}/' . substr($iconUrl, strrpos($iconUrl, '/') + 1);
                        $formattedArticle['icon'] = $iconUrl;
                    }

                    $formattedArticles[] = $formattedArticle;
                }

                saveToJsonFile($formattedArticles);
                return $formattedArticles;
            } else {
                die('Error retrieving subscriptions.');
            }
        } else {
            die('Error retrieving articles.');
        }
    } else {
        die('Login failed: ' . $loginResponse);
    }
    return null;
}

function curlRequest($url, $authToken = null)
{
    $ch = curl_init($url);
    if ($ch === false) {
        return ['error' => 'cURL初始化失败'];
    }

    $headers = [];
    if ($authToken) {
        $headers[] = 'Authorization: GoogleLogin auth=' . $authToken;
    }

    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FAILONERROR, true); // 捕获HTTP错误

    $response = curl_exec($ch);
    if ($response === false) {
        $error = curl_error($ch);
        curl_close($ch);
        return ['error' => $error];
    }

    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode >= 400) {
        return ['error' => "HTTP请求失败,状态码:$httpCode"];
    }

    return $response;
}

/**
 * 将数据保存到JSON文件中
 */
function saveToJsonFile($data)
{
    $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    file_put_contents('friends_rss.json', $json);
    echo '数据已保存到JSON文件中';
}

// 调用函数并提供用户名和密码
getAllSubscribedArticlesAndSaveToJson('${账户}', '${密码}');

代码中的替换项:

  • ${网址1}:上一步复制的网址;
  • ${网址2}:替换成你FreshRSS的域名地址,比如:https://rss.xxx.com
  • ${账户}:FreshRSS 的账户
  • ${密码}:这里是第3步设置的api密码

调用这个rss.php文件,就能从FreshRSS中把最新的订阅文章复写到一个叫friends_rss.json的文件中。

三、配置“朋友圈”模板文件

我们拿到了需要展示的数据,那么我们就要有一个展示的模板。

我的朋友圈设计是仅展示订阅文章的“文章名”、“发布时间”、“发布网站”、“文章前n个字符”等,并不会显示全文,如图:

这个模板其实是由WordPress的说说魔改的,去掉了翻页(仅展示前n条最新文章)、去掉了评论、去掉了点赞、新增了作者栏、修改了标题名自动加书名号、修改了内容超出部分自动加…等。

在目录/www/wwwroot/www.tqazy.com/wp-content/themes/argon-theme-master下创建文件:friend_rss.php。
内容如下:

<?php 
/* 
Template Name: 朋友圈 
*/ ?>

<?php get_header(); ?>

<div class="page-information-card-container">
    <div class="page-information-card card bg-gradient-secondary shadow-lg border-0">
        <div class="card-body">
            <h3 class="text-black"><?php _e('${页面名}', 'argon'); ?></h3>
            <?php if (the_archive_description() != '') { ?>
                <p class="text-black mt-3">
                    <?php the_archive_description(); ?>
                </p>
            <?php } ?>
            <p class="text-black mt-3 mb-0 opacity-8">
                <i class="fa fa-quote-left mr-1"></i>
                <?php _e('${页面提示语}', 'argon'); ?>
            </p>
        </div>
    </div>
</div>

<?php get_sidebar(); ?>

<div id="primary" class="content-area">
    <main id="main" class="site-main" role="main">
        <?php

    // 定义自定义模板标签函数
    function the_site_name() {
        global $post;
        echo esc_html($post->post_site_name);
    }

    function the_icon() {
        global $post;
        echo esc_url($post->post_icon);
    }

        // 获取JSON数据
        $jsonData = file_get_contents('${json所在目录地址}');
        // 检查文件读取是否成功
        if ($jsonData!== false) {
            // 将JSON数据解析为PHP数组
            $articles = json_decode($jsonData, true);
            if ($articles!== null) {
                // 对文章按时间排序(最新的排在前面)
                usort($articles, function ($a, $b) {
                    return strtotime($b['time']) - strtotime($a['time']);
                });
                // 设置每页显示的文章数量
                $itemsPerPage = ${文章数量};
                // 生成文章列表
                $displayArticles = array_slice($articles, 0, $itemsPerPage);
                if (!empty($displayArticles)) {
                    global $post;
                    foreach ($displayArticles as $article) {
                        // 模拟WordPress文章对象
                      // 将 $article['time'] 转换为 MySQL 时间戳格式
                      $formatted_time = date('Y-m-d H:i:s', strtotime($article['time']));
                      $new_article = "  作者:" . $article['site_name'];

                        $post = (object) [
                            'ID' => $article['id']?? uniqid(), // 如果JSON中没有id字段,使用唯一ID
                            'post_title' => "《" . $article['title'] . "》",
                            'post_date' => $formatted_time,
                            'post_content' => $article['description'],
                            'guid' => $article['link'],
                            'post_permalink' => $article['link'],
                            'post_site_name' => $new_article,
                            'post_icon' => $article['icon'],                         

                        ];
                        setup_postdata($post); // 设置当前文章数据

                        get_template_part('template-parts/content', 'friend');

                        wp_reset_postdata(); // 重置文章数据
                    }
                } else {
                    get_template_part('template-parts/content', 'none-tag');
                }
            } else {
                get_template_part('template-parts/content', 'none-tag');
            }
        } else {
            get_template_part('template-parts/content', 'none-tag');
        }
        ?>

<?php get_footer(); ?>

替换项:

  • ${页面名}:页面名称,比如:朋友圈;

  • ${页面提示语}:在页面名称下面显示的提示语,比如:这里是已交换友链的网站发布的内容,通过RSS方式订阅,每1小时更新一次内容。

  • ${json所在目录地址}:前面rss.php生成的json所在地址,比如:/www/wwwroot/www.tqazy.com/friends_rss.json

  • ${文章数量}:即你要展示的文章数量,比如:38。可以写50及以内,如果经常看朋友圈的话,设置展示太多了没意义。

四、配置“朋友圈”配套文件

在目录/www/wwwroot/www.tqazy.com/wp-content/themes/argon-theme-master/template-parts下创建文件:content-friend.php,上面的文件用得到,属于样式配置。

内容如下,这个文件没有替换项:

<div class="shuoshuo-container">
    <div class="shuoshuo-meta shadow-sm">
        <span>

            <i class="fa fa-calendar-o" aria-hidden="true"></i> 
             <span class="shuoshuo-date-month"><?php echo get_the_time('n')?></span> <?php _e('月', 'argon');?> 
            <span class="shuoshuo-date-date"><?php echo get_the_time('d')?></span> <?php _e('日', 'argon');?> , 
            <span class="shuoshuo-date-year"><?php echo get_the_time('Y')?></span>
            <div class="post-meta-devide">|</div>
            <i class="fa fa-clock-o" aria-hidden="true"></i> 
            <span class="shuoshuo-date-time"><?php echo get_the_time('G:i:s')?></span>
        </span>
        <?php if ( is_sticky() ) : ?>
            <div class="post-meta-devide">|</div>
            <div class="post-meta-detail post-meta-detail-words">
                <i class="fa fa-thumb-tack" aria-hidden="true"></i>
                <?php _ex('置顶', 'pinned', 'argon');?>
            </div>
        <?php endif; ?>
    </div>
    <article class="card shuoshuo-main shuoshuo-foldable bg-white shadow-sm border-0" id="post-<?php the_ID(); ?>" <?php post_class(); ?>>
        <?php if ( get_the_title() != '' ) : ?>
            <a class="shuoshuo-title" href="<?php the_guid(); ?>" target="_black"><?php the_title(); ?></a>
            <font style="font-weight:900;"><?php the_site_name(); ?></font>
        <?php endif; ?>

        <div class="shuoshuo-content">
            <?php the_content(); ?>
        </div>
        <?php
            global $withcomments;
            $withcomments = true;
//          comments_template( '/comments-shuoshuo-preview.php' );
        ?>
    </article>
</div>

此时,“朋友圈”的模板部分就编写完毕了。

在网站后台可以看到:外观 > 主题文件编辑器 > friend_rss.php

五、展示“朋友圈”页面

第一步:博客后台 > 页面 > 添加页面:

  1. 页面名称:朋友圈
  2. 正文不用填写任何内容
  3. 设置右边的页面属性:
    1. 父级:无
    2. 模板:朋友圈
    3. 发布

第二步:博客后台 > 外观 > 菜单 > 选择要编辑的菜单,然后在左侧的页面里找到“朋友圈”,添加到菜单,至于剩下的可以自行配置了。

比如我的朋友圈就是放在“友链”菜单下的:

到此,“朋友圈”功能已经成功的添加到了你的网站上了。

六、自动刷新朋友圈

最重要的一步来了,我们要想自动刷新朋友圈,而不是每次都去调用rss.php文件,那么我们就要做定时任务。

我也不知道为什么我单调rss.php文件是不能刷新FreshRSS的订阅源的,所以我做了两个定时任务。

我是使用宝塔的,如果您没有宝塔,可以通过写脚本方式实现。

打开宝塔面板 > 计划任务:

6.1 第一个定时任务

  1. 添加任务;
  2. 选择:Shell脚本;
  3. 任务名称:定时刷新FreshRSS;
  4. 执行周期:每小时 1分钟;
  5. 脚本内容:php /www/wwwroot/rss.tqazy.com/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
  6. 确定

需要把这里的/www/wwwroot/rss.tqazy.com替换成你的rss解压文件所在的目录地址。

6.2 第二个定时任务

  1. 添加任务;
  2. 选择:访问URL-GET;
  3. 任务名称:朋友圈订阅数据更新;
  4. 执行周期:每小时 5分钟;
  5. URL地址:https://xxx.com/xxx/xxx/rss.php
  6. 确定

需要把这里的https://xxx.com/xxx/xxx/rss.php换成你的博客网站能访问到rss.php的域名地址。

你可以执行一下,看看日志,是否成功的写入了。

至此,朋友圈的功能全部完成。

效果:
该功能会将在FreshRSS中订阅的订阅源,以每小时1次的频率自动刷新订阅源并读取到friends_rss.json中,然后模板通过读取friends_rss.json文件将最新的n条文章展示到“朋友圈”的页面中。

注意:每次您的网站新增友链时,需要将对方的rss地址手动的添加到你的FreshRSS中。

喜欢分享一下本站“朋友圈”的制作过程这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

闲谈自话(3)

感谢订阅陶其的个人博客!

很久没有更新了,主要是不知道更什么。

生活过得一团糟,没有任何起色,感觉每天都很忙碌,但是什么事情都没有干成。

想考个软考,把所有的书都准备好后看了两天便没再翻开过一次;

想考个学位英语,也是3天时间只背了几个单词还一个都没有记住;

工作工作没有起色,家庭家庭没有照顾好,职业规划也没有实际的行动。

感觉现在完全就是得过且过的状态,浑浑噩噩又是一天。

内心想做一些事情去改变,想让自己真正的忙起来,忙起来才有希望,可是又不知道方向在哪里,一次又一次的鼓起勇气又悄无声息的放弃。


所以,

活到现在,年近30,感觉自己总是忙忙碌碌的。

上学的时候,身边的每一个人都在说,努努力,多学一些,以后考个好大学,找个轻松的工作,剩下的就是享福了。所以时间是需要挤的,因为所有的科目都需要投入时间。

刚开始工作的时候,每天都在上班下班,生怕失去工作,也不敢随意辞掉工作。因为身处异地,租房住,但凡没有了稳定的收入,心里总是担心哪天连房租都交不起。所以脚步是加快的,害怕慢上一点就挤不上地铁了。所以我在上海的那几年,免费的外滩仅仅去过几次,一只手都能数得过来。其实当时下班后花几块钱,坐上一班地铁就能到。

现在回到了老家的城市,同样逃不过被命运追赶。无他,因为贷款买了房,因为结婚生了娃,因为在城市生活每天的生活成本都很高。甚至感觉一个工作的薪资已经不够了,现在我一份正式工作一份兼职,我仍然感觉不够。倒不是我贪得无厌,只是哪怕两份薪酬加起来已经不算低了,但是仍然没有办法给我自己提供足够的“安全感”,总感觉身后有什么一直“鞭打”我,告诉我,还有很多努力的空间,现在还远远不够,记得走的速度不够那就跑起来。


所以,我好像从来没有过一次彻底放松的出行过。

之前在上海时,陪女朋友(现在是老婆了)去迪士尼玩儿,要提前查尽攻略,然后早早的赶到门口排队入园,在不同的项目间小跑着前进,生怕错过预约好的快速通道。即使停下来拍照也是匆匆拍了就走,根本无心留恋沿途,只一心计算这一点到下一点的步行时间,以妄图通过合理的安排节省时间,能多玩儿几个项目。

后来陪她去南京旅游,哪怕是爬中山陵的时候,我也是随身带着电脑,甚至需要在景区休息处拿出电脑来解决问题,那天是周末。

之前每一个“小长假”,无论是五一、国庆、还是春节,更是忙的时候,总是忙着去完我老家再去老婆老家。一到放假,我们自己的家里从来都没有人,然后总在假期的最后一天,我俩拖着疲惫的身体回到家,往床上一躺,一睁眼又开始了上班生活。

包括前些天里,在我下班之后,陪着她一起下楼在小区里遛娃,我的脚步本能的加快,身体下意识的加快动作,想要尽快完成当前“任务”。

没错,散步、遛娃,本应该是一天中最为放松的行为,我的身体却总是下意识的把它当成一个任务去完成。

这些年来,也是习惯了被不停追赶,被提醒:快一些,跑起来,再快一些,这样你以后才能幸福。

上学时,你要考高分才能找到好工作;
刚工作,你要好好工作才能积累经验进而涨工资;
结婚生娃后,你要多多赚钱才在除去生活开支外,留下一笔钱为未来考虑。
因为你的孩子未来要成长、要上学、要报班、要生活、要结婚生娃、要。。。

所以,你要跑得再快一些。

路边的风景,每年都一样,等你老了,退休了,到时候自然有大把的时间去看。

所以,我们算是真的生活过吗?

我曾经看过一种解析。

生活 = 生存 + 活着

现在,我顶多算是在讨生存,为了自己,为了家庭能够得以生存而努力。

还远没有达到好好的活着的程度。

但是,在努力生活的同时,似乎也不妨碍我在散步的时候,步伐放慢一些,再慢一些,去看一看路边盛开的紫薇花。

它是如此的美丽,就像当年语文课本里描述的那样。

喜欢闲谈自话(3)这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

分享一次高速车祸经历

感谢订阅陶其的个人博客!

太离谱了,真的太离谱了。

今天是2025年4月7日,星期一。

本来应该上班的我,现在请假在家,准确的说刚从4s店回家。

很久没发文章了,主要原因是因为前段时间太忙了,主要还是工作方面的忙碌,这个不提也罢。

好不容易放假了,想着好久没回老家了,从出生起也没带着宝宝回过老家,正好趁着这三天假期开车带宝宝回家一趟。

结果就在回家的高速上被追尾了。

白车是我的车。

应该是因为假期高速免费,加之特殊节日,出行的人比较多,所以当时高速上车流还是挺大的。

因为当时慢车道上老是有慢速车和货车,一直来回变道也不安全,所以我当时走的是快车道。

我开车还是比较谨慎的,基本都会和前车保持出一段安全车距,毕竟高速车速都很快,保持安全车距还是很有必要的。

但是总有些车主会认为保持安全车距的车是没有油门的,司机是不懂加速的,所以老是加塞别人。

面对这种急不可耐的行为,我也都习惯了。但是这次,保持安全车距真的起到了作用。

当时,我正在快车道行驶,前方车辆突然紧急刹车,近似于原地刹停的那种。

我在发现前方急刹之后,我按住方向盘不动,也一脚踩死了刹车。

那种急刹带来的失重感和巨大惯性,让车里所有人都在向前涌。

我能感觉到,车辆应该是触发了ABS,整个车头在往下压,没有原地刹停,而是在以极快降速的速度向前行驶着,这应该就是ABS在介入。

终于,感觉距离前车仅剩一米多的时候刹住了。

我刚松了口气,准备转头问问家人有没有被受伤的时候。

只听“哐”的一声,同时感觉整个人整辆车都在被用力往前怼了一下,飞了起来。

又一声“哐”,我的车头顶住了前车的后屁股,刚才还庆幸刹住了没撞上,这次是真的撞上了。

幸亏车里杂物不多,我在仪表台也没有放任何东西的习惯,所以只有仪表台的防尘垫滑了下来,导航用的手机也掉了。

但是我听到了后排传来了惊呼声,急忙转头。

因为前面急刹,我妈抱着小宝,另一只胳膊顶住了前排座椅后背,还好一些。小宝仅受到了一点儿惊吓,小哭了一声。(本来想买安全座椅来着,一直没定好买哪一家,结果就。。。)

我老婆因为之前刚在车内喂完奶的缘故,当时解开了安全带,忘记系上,整个人滑到了座位底下。

我之前往车里装行李时,临时放在后备箱上层忘记放回下面的灭火器也从后排座椅头枕中间飞到了后排,差点砸到人。(此处我非常庆幸,可记得以后千万不能在后备箱上层放重物了,太危险了。)

在下一瞬间,我就从大脑空白中缓过神来。应该是发生了连环追尾了。

我往后视镜看了一眼,确定后面除了一辆车怼在我的车屁股上外,后面的车停了下来绕道走。

我轻呼了一口气,还好,最坏的情况没有发生。

我对家里人问了一句,都没事儿吧?在她们回答没事儿之后,我说我先下来看看情况。

我就先下车了,前后看了一眼,情况暂时稳定了。我就让家里人拿上衣服和手机先从车里出来,到路边等着。

后面就是联系保险和交警。不过因为我没经历过,流程不太熟悉,慢了一些。

不过还好,后面的车主也联系了交警和保险。

后面就是路边等着交警的到来,交警到了之后,先让能把车开走的把车开走,到交警队等着。不能开的,也联系了拖车给拉过去。

后面到交警队才知道。

前面的第一辆车是因为拥堵正常的缓慢停车。

第二辆车车主应该是走神了啥的,发现的时候就来不及了,只能紧急刹车,但是还是顶到了第一辆车,它俩算是轻微追尾。

而我也紧随着紧急刹车了,没有碰到第二辆车。

其实我和第二辆车之间原本还有一辆车的,我和前车的安全车距我本来留的很长,毕竟当时车流量很大。

它当时是从慢车道加塞到我前面的,所以我当时又降速多留了一些距离。

而它在发现前面刹停的时候,它点了一下刹车,然后方向盘向右打,擦着慢车道车的车头窜了出去。

所以留给我的反应时间就更少了,加之因为它加塞的缘故,车距也短了。

幸亏我为了多留安全距离降速了,不然根本刹不住车。

然后就是我后面的车因为距离我车太近,没留够安全车距,急刹根本刹不住,就怼我车上了。

后面交警划分责任也很简单。

前面第一第二辆车因为追尾事故而停车,他们算是一个事故。

我刹停了,但是我后车撞了我,又顶着我车撞了第二辆车。所以我车和我前后车算第二个事故。

我和前车因为客观原因原地刹停,所以我和我前车是无责的,我后车因为没有留够安全车距,导致三车追尾,他付全责。

就此责任划分完毕。

后续就是定损修车的事儿了。

我看着我的车还能开,况且我已经快到老家了,而全责方的保险指定的定损维修的4s店在我常住的市区,在多方商量过后,就决定是假日返程之后再去定损维修。

所以今天请了假,把车开去4s店定损维修了。

就此次事故,我得到以下几个教训:

  1. 开车不能急躁,谨慎驾驶,甚至是防御性驾驶保证自己和他人安全,因为即使是无责也不如不发生事故的好;
  2. 保持安全车距是很重要的。开车不要老是加塞别人,也快不了几分钟,反而会给自己和他人带来风险;
  3. 车里(乘员舱)不要放重物和太多的杂物,否则发生危险的时候这些东西往往会加剧危险发生;
  4. 发生事故之后,首先确认人员是否受伤,然后迅速转移到安全的地方或者路边报警等待交警。交通事故报警电话:122(别打110,否则还要转122);
  5. 发生事故之后,先不要接听或者理会保险给推的维修厂、定损机构等的电话。因为很可能大概率是推销。一定要先等交警的交通事故责任认定书出来之后再决定。如果感觉自己在这起事故有责任就报保险,否则先别急着报保险。先自己把事故现场拍好照片和视频即可。
  6. 发生事故之后,记得在交警来之前,先完整的拍一下事故现场的照片和视频,以方便后面定责有依据;
  7. 保存好行车记录仪,可能会有大用;
  8. 最后,一定要保持好心态,车子有保险,最多损失点钱和价值。人没事才是最值得庆幸的。

最后,祝大家出门都顺顺利利、平平安安的到达目的地。

喜欢分享一次高速车祸经历这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

微服务技术栈03 – eureka注册中心

感谢订阅陶其的个人博客!

视频课程地址:黑马程序员_微服务技术栈

微服务技术栈系列(目录):点击跳转
上一节:服务拆分及远程调用

一、提供者与消费者

二、服务调用出现的问题

三、eureka原理分析

四、搭建eureka服务

4.1 步骤

pom.xml

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>

application.yml

spring:
  application:
    name: eurekaserver   # 微服务名称

eureka:
  client:
    serviceUrl:  # eureka地址信息
      defaultZone: http://localhost:${server.port}/eureka

4.2 实践

4.2.1 项目结构

4.2.2 父pom

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-cloud.version>2021.0.8</spring-cloud.version>
    <mysql.version>5.1.47</mysql.version>
    <mybatis.version>2.1.1</mybatis.version>
</properties>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
</parent>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

4.2.3 本项目pom

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<parent>
    <groupId>com.tqazy</groupId>
    <artifactId>study_spring_cloud</artifactId>
    <version>1.0-SNAPSHOT</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
</dependencies>

4.2.4 主程序文件(EurekaApplication.java)

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}

4.2.5 application.yml

server:
  port: 7070  # 服务端口

spring:
  application:
    name: eurekaserver   # 微服务名称

eureka:
  client:
    serviceUrl:  # eureka地址信息
      defaultZone: http://localhost:${server.port}/eureka

4.2.6 运行访问

五、服务注册

5.1 步骤

pom.xml

<dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

application.yml

spring:
  application:
    name: userserver   # 微服务名称

eureka:
  client:
    serviceUrl:  # eureka地址信息
      defaultZone: http://localhost:${eureka-server.port}/eureka

5.2 实践

5.2.1 pom文件

在pom文件添加eureka客户端的依赖

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
    <!-- spring-boot-starter-web Spring Boot starter 上的 <dependency>。
     它们告诉 Spring Boot,该应用程序是Web应用程序。
     Spring Boot 会相应地形成自己的观点。-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!--mybatis-->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>

5.2.2 在application.yml添加配置

server:
  port: 8081
spring:
  application:
    name: user-server   # 微服务名称
  datasource:
    url: jdbc:mysql://192.168.100.241:3306/cloud-user?useSSL=false
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
mybatis:
  type-aliases-package: com.tqazy.user.pojo
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.tqazy: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS

eureka:
  client:
    serviceUrl:  # eureka地址信息
      defaultZone: http://localhost:7070/eureka

5.2.3 运行

重启程序,查看eureka

5.3 一个服务启动多个实例

-Dserver.port=8083

结果:

六、服务发现

6.1 步骤

@LoadBalanced注解负责负载均衡

负载均衡原理和策略下一章详细讲解

6.2 实践

6.2.1 原代码

原本order-service访问user-service代码

OrderApplication.java

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("com.tqazy.order.mapper")
@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

OrderService.java

public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);

    String url = "http://127.0.0.1:8081/user/" + order.getUserId();
    User user = restTemplate.getForObject(url, User.class);

    order.setUser(user);

    // 4.返回
    return order;
}

6.2.2 新代码

OrderApplication.java

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate();
}

OrderService.java

public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);

    String url = "http://userservice/user/" + order.getUserId();
    User user = restTemplate.getForObject(url, User.class);

    order.setUser(user);

    // 4.返回
    return order;
}

6.2.3 重新运行order-service

已实现负载均衡访问

七、总结

喜欢微服务技术栈03 – eureka注册中心这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

SpringBoot项目模板 [多环境配置][日志输出][windows系统构建打包运行]

感谢订阅陶其的个人博客!

本章完成的效果:

  1. 创建一个 Spring Boot 项目
  2. 多环境配置,选择环境打包
  3. 使用logback进行日志输出,按日期+大小进行滚动压缩
  4. 在Windows系统使用maven进行构建打包运行

本章使用的运行环境:

  • Java:JDK1.8.0_441
  • Maven:3.6.0
  • Spring Boot: 2.6.13
  • Logback:1.2.11
    • 已被 spring-boot-starter-web 间接引入

接下来我将使用idea进行创建项目:

一、新建项目

  1. 新建项目;
  2. 选择生成器:“Spring Boot”;
  3. 点击【服务器URL】后面的齿轮,把生成项目的地址换成阿里云,因为spring官方已经不支持Java8生成SpringBoot项目;
    • https://start.aliyun.com
  4. 填写好相关信息,点击下一步:
    • 项目名称
    • 项目代码位置(项目名不用填写)
    • 选择Java语言
    • 选择Maven管理依赖
    • 组:一般是域名倒置(不能有空格)
    • 工件:项目名(不能有空格)
    • 软件包名称:一般由[组]和[工件]自动生成
    • JDK:本地JDK8地址
    • Java:8(没有8,需要修改上一步的服务器URL)
    • 打包:Jar
  5. 选择依赖,点击创建:
    • 选择Spring Boot版本:2.6.13(
      • 这个版本集成logback1.2.11,足够我们使用
    • 选中:Developer Tools => Lombok
    • 选中:Web => Spring Web
  6. 项目创建好之后,删除自动生成的或不必要的文件,还原一个干净的项目结构
    • 删除 src\main\java\ *** 下的整个web目录
    • 删除 static\index.html
    • 修改 application.properties 重命名为 application.yml
      • 推荐使用yaml文件
    • 修改 logback.xml 重命名为 logback-spring.xml(
      • 因为后面会在这个文件中使用spring相关配置,重命名后将支持spring相关配置
    • 删除 test目录下的整个java目录,后续需要再新建
    • 删除 .gitignore,需要使用git时再新增
    • 删除 HELP.md,需要项目帮助文档时再新增

二、pom.xml文件

新的pom.xml文件内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- 项目基本信息 -->
    <groupId>com.tqazy</groupId>
    <artifactId>demo</artifactId>
    <version>v1.0</version>
    <name>demo</name>
    <description>demo</description>

    <!-- 项目属性配置 -->
    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.6.13</spring-boot.version>
        <maven-compiler-plugin.version>3.6.0</maven-compiler-plugin.version>
    </properties>

    <!-- 项目依赖配置 -->
    <dependencies>
        <!-- Spring Boot Web 启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring-boot.version}</version>
        </dependency>

        <!-- Lombok 依赖 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <!-- 依赖管理 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <!-- 项目构建配置 -->
    <build>
        <!-- 资源配置 -->
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>

        <!-- 插件配置 -->
        <plugins>
            <!-- Maven 编译器插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${maven-compiler-plugin.version}</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>

            <!-- Spring Boot Maven 插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring-boot.version}</version>
                <configuration>
                    <mainClass>com.tqazy.demo.DemoApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <id>repackage</id>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

    <!-- 项目环境配置 -->
    <profiles>
        <!-- 测试环境配置 -->
        <profile>
            <id>test</id>
            <properties>
                <profiles.active>test</profiles.active>
            </properties>
            <activation>
                <!-- 默认环境 -->
                <activeByDefault>true</activeByDefault>
            </activation>
        </profile>

        <!-- 生产环境配置 -->
        <profile>
            <id>prod</id>
            <properties>
                <profiles.active>prod</profiles.active>
            </properties>
        </profile>
    </profiles>

</project>

代码解析:

  • 项目基本信息: 需要修改成你的配置;
  • 项目属性配置: 配置了
    • Java版本:1.8
    • Maven 读取项目源文件的编码格式:UTF-8
    • Maven 生成报告时的输出编码格式:UTF-8
    • Spring Boot 版本:2.6.13
    • Maven 插件版本:3.6.0
  • 项目依赖配置: 配置了:
    • spring-boot-starter-web :已提供Restful API风格的的依赖,同时集成了logback的依赖
    • lombok :简化编码,设置<scope>provided</scope>,意为仅在编译阶段生效
  • 依赖管理:
    • spring-boot-dependencies :确保依赖版本的一致性和兼容性
  • 项目构建配置:
    • 资源配置: 此处的资源配置,是为了后面在【application.yml】文件中 @profiles.active@ 配置内容能读取到【pom.xml】中的 <profiles> 配置
    • 插件配置: 为了配置 Maven插件 和Spring Boot Maven插件的版本和相关配置
      • mainClass :配置这个项目的主文件地址,即被 @SpringBootApplication 修饰的程序主入口
  • 项目环境配置: 配置了两个环境:test和prod,如果有需要可以再新增一个dev。此处配置是为了方便在构建项目时,可以通过构建指令动态选择不同环境的配置进行打包。具体如何选择,在下面会讲解。

三、项目配置文件

3.1 application.yml(主配置文件)

spring:
  profiles:
    active: @profiles.active@

代码解析:
此处只对不同环境文件动态读取进行了配置。
后续如果有在不同环境下也需要相同配置的信息都可以在主配置文件中进行配置。

3.2 application-test.yml(分环境文件)

server:
  port: 8081

# 日志配置
logging:
  file:
    path: D:\code\test\logs\test
  level:
    com.tqazy: debug
    org.springframework: WARN
    org.spring.springboot.dao: debug

代码解析:

  • 配置项目端口:8081
  • 配置日志信息:
    • 日志输出地址
    • 不同包下输出的日志等级
      • 本地环境、测试环境可以比较宽松;
      • 建议生产环境配置等级高一些,尽量不使用debug,不然生成的日志量会非常大且敏感数据打印成日志不安全

3.3 application-prod.yml(分环境文件)

server:
  port: 8082

# 日志配置
logging:
  file:
    path: D:\code\test\logs\prod
  level:
    com.tqazy: info
    org.springframework: WARN
    org.spring.springboot.dao: info

代码解析:

  • 配置项目端口:8082
  • 配置日志信息:
    • 日志输出地址
    • 不同包下输出的日志等级

3.4 logback-spring.xml(日志配置文件)

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- 从 Spring Boot 配置文件中读取日志路径 -->
    <springProperty scope="context" name="log.path" source="logging.file.path"/>
    <!-- 规定日志格式,
        格式:月日 时分秒毫秒 [线程名称] 日志级别 记录器名称(类名) - [调用方法名,文件中行号] - 日志消息内容
        示例:03-20 15:30:00.456 [http-nio-8080-exec-1] INFO  com.example.controller.UserController - [getUser,50] - User retrieved successfully
    -->
    <property name="log.pattern" value="%d{MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30} - [%method,%line] - %msg%n"/>

    <!-- 控制台输出 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <charset>UTF-8</charset>
            <pattern>${log.pattern}</pattern>
        </encoder>
    </appender>

    <!-- 系统日志输出 -->
    <appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-info.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 按天回滚 daily -->
            <fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <!-- 日志最大的历史 60天 -->
            <maxHistory>60</maxHistory>
            <!--每个日志文件最大100MB-->
            <maxFileSize>100MB</maxFileSize>
            <!-- 总日志文件大小上限(可选) -->
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <charset>UTF-8</charset>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 按天回滚 daily -->
            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <!-- 日志最大的历史 60天 -->
            <maxHistory>60</maxHistory>
            <!--每个日志文件最大100MB-->
            <maxFileSize>100MB</maxFileSize>
            <!-- 总日志文件大小上限(可选) -->
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <charset>UTF-8</charset>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 用户访问日志输出  -->
    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${log.path}/sys-user.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 按天回滚 daily -->
            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
            <!-- 日志最大的历史 60天 -->
            <maxHistory>60</maxHistory>
            <!--每个日志文件最大100MB-->
            <maxFileSize>100MB</maxFileSize>
            <!-- 总日志文件大小上限(可选) -->
            <totalSizeCap>1GB</totalSizeCap>
        </rollingPolicy>
        <encoder>
            <charset>UTF-8</charset>
            <pattern>${log.pattern}</pattern>
        </encoder>
    </appender>

    <!-- com.tqazy 包及其子包下的日志记录器的日志级别为 INFO -->
    <logger name="com.tqazy" level="info"/>

    <!-- 定义了全局的日志级别和默认的日志输出行为 -->
    <!-- 设定级别:info,处理 INFO、WARN 和 ERROR 级别 -->
    <root level="info">
        <!-- 经过根日志记录器过滤后的日志消息能够输出到控制台 -->
        <appender-ref ref="console"/>
        <!-- 把 INFO 级别的日志输出到文件 sys-info.log 中 -->
        <appender-ref ref="file_info"/>
        <!-- 把 ERROR 级别的日志输出到文件 sys-info.log 中 -->
        <appender-ref ref="file_error"/>
        <!-- 把系统用户操作日志输出到文件 sys-user.log 中 -->
        <appender-ref ref="sys-user"/>
    </root>
</configuration>

代码解析:

  • 从 Spring Boot 分环境配置文件中读取日志路径,实现不同环境的日志文件输出地址不同。有效实现区分windows系统和linux系统的目录地址结构不同的特性。
  • 统一日志格式
  • 日志滚动压缩策略:按时间+大小混合滚动
  • 多维度日志输出:控制台、系统日志拆分(INFO、ERROR)、用户操作日志
  • 日志级别控制
  • 配置日志输出编码:UTF-8

四、编写业务代码

至此,项目的基本配置已经完成,接下来开始编写简单的业务代码,作为演示。

4.1 User.java

在src\main\java\com\tqazy\demo*\web\下新建domain.pojo.User.java。
注意:web目录要与主文件同级

import lombok.Data;

@Data
public class User {

    private String name;

    private String address;

}

使用Lombok之后,项目代码会变得很简洁。

4.2 UserController.java

为了演示方便,中间的逻辑层和持久层我就忽略了,这个不是本章重点。

在src\main\java\com\tqazy\demo*\web\下新建controller.UserController.java。

import com.tqazy.demo.demos.web.domain.pojo.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

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

    // 创建一个 Logger 实例,使用当前类的类名作为日志记录器的名称
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);

    // http://127.0.0.1:8080/hello?name=lisi
    @RequestMapping("/hello")
    @ResponseBody
    public String hello(@RequestParam(name = "name", defaultValue = "unknown user") String name) {
        logger.info("hello {}", name);
        return "Hello " + name;
    }

    // http://127.0.0.1:8080/user
    @RequestMapping("/getUser")
    @ResponseBody
    public User getUser() {
        User user = new User();
        user.setName("鲁班七号");
        user.setAddress("王者峡谷");
        System.out.println(user);
        logger.error(user.toString());
        return user;
    }
}

为了演示,我将第一个方法的日志使用INFO级别打印,将第二个方法的日志使用ERROR级别打印。

五、不同环境运行

项目中我们配置了测试环境和生产环境的文件,接下来我们通过maven命令,动态选择不同环境文件进行打包运行。

提前声明:

本项目地址:D:\code\test\demo
生成的jar包名:demo-v1.0.jar
本项目日志输出地址:D:\code\test\logs
下面的命令你需要更换成你自己的项目地址

5.1 相关命令

具体使用在下文中实践

# 使用maven清除并重新打包项目,使用test环境文件
mvn clean package -Ptest

# 使用maven清除并重新打包项目,使用prod环境文件
mvn clean package -Pprod

# 使用java命令运行jar包,但是窗口关闭,程序停止运行
java -jar D:\code\test\demo\target\demo-v1.0.jar

# 使用javaw命令运行jar包,程序后台运行
javaw -jar D:\code\test\demo\target\demo-v1.0.jar

# 查询jar包运行的进程PID
jps -l | findstr demo-v1.0.jar

# 终止jar包运行的进程(强制终止)
taskkill /f /pid '实际的PID'

# 终止jar包运行的进程(优雅终止)
taskkill /pid '实际的PID'

5.2 测试环境运行

  1. 按下键盘的[Win]键,输入“Windows PowerShell”并选择
  2. 在命令提示框中输入:
    cd D:\code\test\demo
    mvn clean package -Ptest

    看见 BUILD SUCCESS 字样,说明构建成功。

  3. 先进行java方式运行,查看是否报错:
    java -jar D:\code\test\demo\target\demo-v1.0.jar

    我们看到,运行成功,端口显示8081,是测试环境配置的端口。
    但是使用java运行,一旦关闭当前窗口,那么程序将停止运行。
    下面我们将在java运行不报错的情况下进行后台运行。
    此时我们先按下 Ctrl + C 键停止运行。

  4. 我们使用javaw命令可以实现后台运行
    在命令窗口中执行如下命令,javaw 是不会产生窗口输出的。
    javaw -jar D:\code\test\demo\target\demo-v1.0.jar

  5. 验证运行结果
    我们该如何验证运行结果呢?
    打开浏览器,我们尝试访问程序中的接口:
    • http://localhost:8081/user/getUser
    • http://localhost:8081/user/hello?name=lisi

      此时已生成日志:

sys-error.log内容:

03-20 11:15:42.402 [http-nio-8081-exec-1] ERROR c.t.d.d.w.c.UserController – [getUser,39] – User(name=鲁班七号, address=王者峡谷)

sys-info.log内容:(包含启动日志和接口访问日志)

03-20 11:15:36.016 [main] INFO com.tqazy.demo.DemoApplication – [logStarting,55] – Starting DemoApplication using Java 1.8.0_441 on DESKTOP-GB483S6 with PID 11268 (D:\code\test\demo\target\demo-v1.0.jar started by Admin in D:\code\test\demo)
03-20 11:15:36.018 [main] INFO com.tqazy.demo.DemoApplication – [logStartupProfileInfo,651] – The following 1 profile is active: "test"
03-20 11:15:36.660 [main] INFO o.a.c.http11.Http11NioProtocol – [log,173] – Initializing ProtocolHandler ["http-nio-8081"]
03-20 11:15:36.661 [main] INFO o.a.c.core.StandardService – [log,173] – Starting service [Tomcat]
03-20 11:15:36.665 [main] INFO o.a.c.core.StandardEngine – [log,173] – Starting Servlet engine: [Apache Tomcat/9.0.68]
03-20 11:15:36.752 [main] INFO o.a.c.c.C.[.[localhost].[/] – [log,173] – Initializing Spring embedded WebApplicationContext
03-20 11:15:36.937 [main] INFO o.a.c.http11.Http11NioProtocol – [log,173] – Starting ProtocolHandler ["http-nio-8081"]
03-20 11:15:37.012 [main] INFO com.tqazy.demo.DemoApplication – [logStarted,61] – Started DemoApplication in 1.284 seconds (JVM running for 1.548)
03-20 11:15:42.387 [http-nio-8081-exec-1] INFO o.a.c.c.C.[.[localhost].[/] – [log,173] – Initializing Spring DispatcherServlet ‘dispatcherServlet’
03-20 11:17:17.050 [http-nio-8081-exec-4] INFO c.t.d.d.w.c.UserController – [hello,27] – hello lisi

  1. 停止服务:
    jps -l | findstr demo-v1.0.jar

    查出PID:

    执行终止进程操作

    taskkill /pid 11268

5.3 生产环境运行

相关步骤和测试环境相同,只是最开始的打包命令有所区别。
注意:如果上面已经成功运行了一次jar,一定要停止运行后再尝试生产环境构建运行,因为正在运行的jar不能被 clean 删除。

  1. 在命令提示框中输入:

    cd D:\code\test\demo
    mvn clean package -Pprod

    看见 BUILD SUCCESS 字样,说明构建成功。

  2. 其余命令可看测试环境命令

  3. 验证运行结果
    我们该如何验证运行结果呢?
    打开浏览器,我们尝试访问程序中的接口:

    • http://localhost:8082/user/hello?name=zhangsan
    • http://localhost:8082/user/getUser
      注意:测试环境和生产环境的端口是不同的。

此时已生成日志:

具体日志内容参考 测试环境 日志内容,除了传参不同,其余相同。

  1. 停止服务:
    参考 测试环境 停止服务方式。

5.4 优雅(脚本)的方式启停

疯狂手写命令,还要手动查找PID太过麻烦,我们可以使用脚本代为执行。

在桌面新建两个文件并重命名(文件名可以自己改,但是后缀是 .bat ),然后右击使用记事本打开。

  • build_and_run.bat
  • stop_project.bat

注意:当文件内容编辑完成之后,不要直接保存。
需要点击左上角 [文件] => [另存为] => 保存的左边[编码,选择:ANSI] => 保存。
否则在控制台输出,中文会乱码。

5.4.1 启动脚本

@echo off
setlocal enabledelayedexpansion

rem 进入项目目录
echo 正在进入项目目录
cd /d D:\code\test\demo

rem 定义要查找的 JAR 包名称
set "JAR_NAME=demo-v1.0.jar"

rem 查找进程 ID
for /f "tokens=1" %%i in ('jps -l ^| findstr /i "%JAR_NAME%"') do (
    set "PID=%%i"
)

rem 检查是否找到进程
if defined PID (
    echo 错误:%JAR_NAME% 正在运行(PID: %PID%),请先停止后再构建!
    pause
    exit /b 1
)

rem 执行 Maven 打包命令
echo 正在执行 Maven 打包命令
call mvn clean package -Ptest >nul 2>&1

rem 检查 Maven 打包是否成功
if %errorlevel% neq 0 (
    echo Maven 打包失败,请检查错误信息
    pause
    exit /b 1
)
echo Maven 打包完成

rem 启动 JAR 包
echo 正在启动 JAR 包
start javaw -jar D:\code\test\demo\target\demo-v1.0.jar
echo JAR 包启动完成

echo 脚本执行完毕,按任意键返回...
pause >nul

脚本解析:

  • 进入项目地址
  • 检查jar运行情况
    • 如果jar在运行,返回错误信息
    • 如果jar没在运行,继续执行
  • 使用maven对项目进行构建
  • 使用javaw启动jar包

替换内容(将脚本中的信息换成你自己的):

  • D:\code\test\demo :项目地址
  • demo-v1.0.jar :jar包名称
  • -Ptest :运行环境,根据上面pom中配置的<profile>的id,根据需要换成:-Pdev-Ptest-Pprod
  • D:\code\test\demo\target\demo-v1.0.jar :jar所在的绝对路径

5.4.2 终止脚本

@echo off
setlocal enabledelayedexpansion

rem 定义要查找的 JAR 包名称
set "JAR_NAME=demo-v1.0.jar"

rem 查找进程 ID
for /f "tokens=1" %%i in ('jps -l ^| findstr /i "%JAR_NAME%"') do (
    set "PID=%%i"
    echo 找到进程 PID: !PID!
)

rem 检查是否找到进程
if not defined PID (
    echo 未找到 %JAR_NAME% 的运行进程。
    pause
    exit /b 0
)

rem 强制终止进程
echo 正在强制终止进程 %PID%...
taskkill /f /pid %PID%

rem 检查终止操作是否成功
if %errorlevel% equ 0 (
    echo 进程 %PID% 已成功终止。
) else (
    echo 终止进程失败,错误代码:%errorlevel%
)

pause
endlocal

5.4.3 运行效果

喜欢SpringBoot项目模板 [多环境配置][日志输出][windows系统构建打包运行]这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

RESTful API风格的Controller中的常用注解

感谢订阅陶其的个人博客!

在 RESTful API 风格的接口开发中,Spring 框架提供了一系列常用的注解,这些注解可以帮助我们更方便地定义和处理 HTTP 请求。

使用之前需要添加如下依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

以下是一些常用注解的介绍、使用场景和简单示例:

一、控制类相关注解

1.1 @RestController

@RestController 是 Spring 框架中一个非常重要的注解,主要用于创建 RESTful 风格的控制器。

1.1.1 介绍

@RestController 是一个组合注解,它结合了 @Controller@ResponseBody 的功能。
@Controller 用于标记一个类为 Spring MVC 的控制器。
@ResponseBody 用于将控制器方法的返回值直接作为 HTTP 响应体返回给客户端,不再经过视图解析器进行视图渲染。
因此,使用 @RestController 注解的类中的方法会直接返回数据,通常是 JSON 或 XML 格式。

1.1.2 使用场景

  • 前后端分离开发: 在前后端分离的项目中,前端通常使用 JavaScript 框架(如 Vue.js、React.js),后端负责提供数据接口。@RestController 可以方便地返回 JSON 数据给前端,满足前后端数据交互的需求。
  • 微服务架构: 在微服务架构中,各个服务之间通过 RESTful API 进行通信。@RestController 可以帮助快速构建服务的 API 接口,实现服务之间的数据交互。
  • 开发 RESTful API: 当需要开发 RESTful 风格的 API 时,@RestController 是首选的注解,它可以使代码更加简洁,符合 RESTful 规范。

1.1.3 简单使用示例

以下是一个使用 @RestController 创建 RESTful API 的简单示例:

@RestController
public class UserController {

    // 处理 HTTP GET 请求,返回用户列表
    @GetMapping("/users")
    public List<String> getUsers() {
        return Arrays.asList("Alice", "Bob", "Charlie");
    }

    // 处理 HTTP GET 请求,根据 ID 返回单个用户信息
    @GetMapping("/users/{id}")
    public String getUserById(Long id) {
        return "User with ID: " + id;
    }
}

在上述示例中,UserController 类被 @RestController 注解标记,其中的 getUsers 方法和 getUserById 方法会直接将返回值作为响应体返回给客户端。

例如,当客户端发送 GET 请求到 /users 时,会返回包含用户姓名的 JSON 数组。

1.1.4 原理

当 Spring 应用启动时,Spring 框架会扫描带有 @RestController 注解的类,并将其注册为控制器。
在处理请求时,Spring MVC 会根据请求的 URL 和 HTTP 方法匹配到相应的控制器方法。
由于 @RestController 包含 @ResponseBody 功能,控制器方法的返回值会被 HttpMessageConverter 转换为合适的格式(如 JSON、XML 等),然后直接作为 HTTP 响应体返回给客户端。

1.1.5 注意事项

  • 返回值处理: @RestController 注解的控制器方法返回值会直接作为响应体返回,因此要确保返回值类型可以被 HttpMessageConverter 正确处理。如果返回自定义对象,通常需要确保该对象的属性有对应的 getter 方法,以便正确转换为 JSON 或 XML 格式。
  • 异常处理: 由于 @RestController 主要用于返回数据,当发生异常时,需要进行合适的异常处理,避免将异常信息直接暴露给客户端。可以使用 @ExceptionHandler@RestControllerAdvice 来统一处理异常。
  • 路径映射: 要注意控制器方法的路径映射,避免出现路径冲突。可以使用 @RequestMapping@GetMapping@PostMapping 等注解来精确指定请求路径和 HTTP 方法。

二、请求类型相关注解

2.1 @RequestMapping

@RequestMapping 是 Spring 框架中用于映射 HTTP 请求到控制器方法的重要注解。

2.1.1 介绍

@RequestMapping 是一个通用的请求映射注解,可用于类和方法上。

当用于类上时,它为该类中的所有处理方法设置一个基础的请求路径;
当用于方法上时,它指定了该方法具体处理的请求路径。
通过 @RequestMapping 可以精确地定义哪些 HTTP 请求会被哪个控制器方法处理。

2.1.2 使用场景

  • 请求路径映射: 在开发 RESTful API 时,需要将不同的 URL 请求映射到相应的处理方法上,@RequestMapping 可以实现这一功能。
  • 支持多种 HTTP 方法: 可以通过设置 method 属性来指定处理的 HTTP 方法(如 GETPOSTPUTDELETE 等),满足不同业务场景下的请求处理需求。
  • 请求参数和请求头匹配: 可以通过 paramsheaders 属性进一步细化请求匹配规则,只有当请求的参数或请求头满足指定条件时,才会调用相应的处理方法。

2.1.3 使用方式

2.1.3.1 用于类上设置基础路径

@RestController
@RequestMapping("/api")
public class ApiController {

    @RequestMapping("/hello")
    @ResponseBody
    public String sayHello() {
        return "Hello, World!";
    }
}

在上述示例中,@RequestMapping("/api")ApiController 类设置了基础路径,sayHello 方法处理的请求路径是 /api/hello

2.1.3.2 指定 HTTP 方法

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

    // 处理 GET 请求
    @RequestMapping(value = "/", method = RequestMethod.GET)
    @ResponseBody
    public String getUsers() {
        return "Get all users";
    }

    // 处理 POST 请求
    @RequestMapping(value = "/", method = RequestMethod.POST)
    @ResponseBody
    public String createUser() {
        return "Create a new user";
    }
}

这里通过 method 属性分别指定了 getUsers 方法处理 GET 请求,createUser 方法处理 POST 请求。

2.1.3.3 基于请求参数和请求头匹配

@RestController
@RequestMapping("/products")
public class ProductController {

    // 只有当请求包含参数 "category=electronics" 时才会处理
    @RequestMapping(value = "/list", params = "category=electronics")
    @ResponseBody
    public String getElectronicsProducts() {
        return "List of electronics products";
    }

    // 只有当请求头中包含 "X-API-Version: 1.0" 时才会处理
    @RequestMapping(value = "/details", headers = "X-API-Version=1.0")
    @ResponseBody
    public String getProductDetails() {
        return "Product details for API version 1.0";
    }
}

使用curl命令

curl "http://localhost:8080/products/list?category=electronics"

当你执行这个命令时,因为请求中包含了参数 category=electronics,所以会匹配到 getElectronicsProducts 方法,最终会返回 "List of electronics products"

2.1.4 原理

在 Spring 应用启动时,Spring 框架会扫描所有带有 @RequestMapping 注解的类和方法,并将这些映射信息存储在 HandlerMapping 中。
当有 HTTP 请求到达时,Spring MVC 会根据请求的 URL、HTTP 方法、请求参数和请求头信息,在 HandlerMapping 中查找匹配的处理方法,然后调用该方法处理请求。

2.1.5 注意事项

  • 路径冲突: 要确保不同的请求映射路径不会发生冲突。如果多个方法的请求映射规则相同,Spring 会抛出异常。
  • 注解的组合使用: 可以将 @RequestMapping 与其他注解(如 @PathVariable@RequestParam@RequestBody 等)结合使用,以处理不同类型的请求参数。
  • 性能考虑: 过多复杂的请求映射规则可能会影响请求处理的性能,因此应尽量保持请求映射规则的简洁性。

2.2 @GetMapping

2.2.1 介绍

@GetMapping 是一个组合注解,相当于 @RequestMapping(method = RequestMethod.GET)

用于处理 HTTP GET 请求,它可以将 HTTP GET 请求映射到特定的处理方法上。

2.2.2 使用场景

主要用于从服务器获取资源,例如获取单个资源、资源列表等。

2.2.3 使用示例

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;

@RestController
public class ProductController {
    @GetMapping("/products")
    public List<String> getAllProducts() {
        return Arrays.asList("Product1", "Product2", "Product3");
    }
}

2.2.4 请求时的示例

当客户端发起请求时,使用如下地址:

GET http://localhost:8080/products

服务器会调用 getAllProducts 方法,并返回包含产品名称的列表。

2.3 @PostMapping

2.3.1 介绍

@PostMapping 相当于 @RequestMapping(method = RequestMethod.POST)

用于处理 HTTP POST 请求,通常用于创建新的资源。

2.3.2 使用场景

当需要向服务器提交数据以创建新资源时使用,如创建用户、创建订单等。

2.3.3 简单使用示例

User类代码此处省略。

@RequestBody注解在 3.3 小节会讲解。

@RestController
public class UserController {
    @PostMapping("/users")
    public String createUser(@RequestBody User user) {
        return "User created: " + user.getName();
    }
}

2.3.4 请求时的示例

客户端可以使用如下请求创建一个新用户:

POST http://localhost:8080/users
Content-Type: application/json

{
    "name": "John Doe",
    "age": 30
}

服务器会将请求体中的 JSON 数据映射到 User 对象,并调用 createUser 方法。

2.4 @PutMapping

2.4.1 介绍

@PutMapping 相当于 @RequestMapping(method = RequestMethod.PUT)

用于处理 HTTP PUT 请求,通常用于更新整个资源。

2.4.2 使用场景

当需要更新资源的所有属性时使用,例如更新用户的所有信息。

2.4.3 简单使用示例

User类代码此处省略。

@PathVariable注解在 3.1 小节会讲解。

@RestController
public class UserController {
    @PutMapping("/users/{id}")
    public String updateUser(@PathVariable Long id, @RequestBody User user) {
        return "User with ID " + id + " updated: " + user.getName();
    }
}

2.4.4 请求时的示例

客户端可以使用如下请求更新用户信息:

PUT http://localhost:8080/users/1
Content-Type: application/json

{
    "name": "Updated Name",
    "age": 35
}

这里的 1 是用户的 ID,服务器会将其作为 @PathVariable 绑定到 id 参数,同时将请求体中的 JSON 数据映射到 User 对象,然后调用 updateUser 方法。

2.5 @DeleteMapping

2.5.1 介绍

@DeleteMapping 相当于 @RequestMapping(method = RequestMethod.DELETE)

用于处理 HTTP DELETE 请求,通常用于删除资源。

2.5.2 使用场景

当需要从服务器删除某个资源时使用,例如删除用户、删除订单等。

2.5.3 简单使用示例

@RestController
public class ProductController {
    @DeleteMapping("/products/{id}")
    public String deleteProduct(@PathVariable Long id) {
        return "Product with ID " + id + " deleted";
    }
}

2.5.4 请求时的示例

客户端可以使用如下请求删除指定 ID 的产品:

DELETE http://localhost:8080/products/2

这里的 2 是产品的 ID,服务器会将其作为 @PathVariable 绑定到 id 参数,然后调用 deleteProduct 方法。

三、请求参数相关注解

3.1 @PathVariable

3.1.1 介绍

@PathVariable 用于从 URL 路径中获取变量值。

它允许在 URL 中定义参数占位符,并将实际的参数值绑定到方法的参数上。

@PathVariable 注解的参数默认是必填的。

当客户端发送的请求 URL 中没有包含该路径变量时,Spring 会将该请求视为不匹配任何处理方法,返回 404 状态码。不可设置可选,是参数也是请求路径,所以必填。

3.1.2 使用场景

当需要在 URL 中传递参数时使用,例如根据用户 ID 获取用户信息、根据产品 ID 删除产品等。

当传入的参数名与方法的参数名不同时,可以将传入的参数名与方法参数名绑定:

    @GetMapping("/{username}")
    public String searchByName(@PathVariable("username") String name) {
        return "name:" + name;
    }

或者写成:

    @GetMapping("/{username}")
    public String searchByName(@PathVariable(value = "username") String name) {
        return "name:" + name;
    }

3.1.3 简单使用示例

@RestController
public class OrderController {
    @GetMapping("/orders/{orderId}")
    public String getOrder(@PathVariable Long orderId) {
        return "Order with ID: " + orderId;
    }
}

3.1.4 请求时的示例

客户端可以使用如下请求获取指定 ID 的订单信息:

GET http://localhost:8080/orders/123

这里的 123 是订单的 ID,服务器会将其作为 @PathVariable 绑定到 orderId 参数,然后调用 getOrder 方法。

3.2 @RequestParam

3.2.1 介绍

@RequestParam 用于从 URL 的查询参数中获取值。

它可以将查询参数绑定到方法的参数上。

@RequestParam 注解的参数默认是必填的。

当客户端发送请求时,如果没有提供该参数,Spring 会抛出 MissingServletRequestParameterException 异常。

可以通过将 @RequestParamrequired 属性设置为 false 来将参数变为可选的。

同时,可以使用 defaultValue 属性为参数设置默认值。

3.2.2 使用场景

当需要在 URL 中传递简单的参数时使用,例如根据用户名搜索用户、根据价格范围筛选产品等。

当传入的参数名与方法的参数名不同时,可以将传入的参数名与方法参数名绑定:

    @GetMapping("/search")
    public String search(@RequestParam("username") String name) {
        return "name:" + name;
    }

或者写成:

    @GetMapping("/search")
    public String search(@RequestParam(value = "username") String name) {
        return "name:" + name;
    }

3.2.3 简单使用示例

@RestController
public class UserController {
    @GetMapping("/users/search")
    public String searchUsers(@RequestParam String name) {
        return "Searching for users with name: " + name;
    }
}

3.2.4 请求时的示例

客户端可以使用如下请求搜索指定名称的用户:

GET http://localhost:8080/users/search?name=John

服务器会将查询参数 name 的值 John 绑定到 searchUsers 方法的 name 参数上。

3.3 @RequestBody

3.3.1 介绍

@RequestBody 用于将 HTTP 请求体的内容绑定到方法的参数上。

通常用于处理 POST 和 PUT 请求。

它会根据请求的 Content-Type 自动将请求体中的数据转换为合适的 Java 对象。

3.3.2 使用场景

当需要传递复杂的对象数据时使用,例如创建用户时传递用户的详细信息、更新产品时传递产品的完整信息等。

3.3.3 简单使用示例

Product类代码省略。

@RestController
public class ProductController {
    @PostMapping("/products")
    public String createProduct(@RequestBody Product product) {
        return "Product created: " + product.getName();
    }
}

3.3.4 请求时的示例

客户端可以使用如下请求创建一个新的产品:

POST http://localhost:8080/products
Content-Type: application/json

{
    "name": "New Product",
    "price": 99.99
}

服务器会将请求体中的 JSON 数据映射到 Product 对象,并调用 createProduct 方法。

3.4 @RequestHeader

3.4.1 介绍

@RequestHeader 注解用于将 HTTP 请求头中的值绑定到方法的参数上。

通过该注解可以获取请求头中的特定信息,如 User - AgentAuthorization 等。

3.4.2 使用场景

当需要从请求头中获取一些必要的信息时使用,例如获取客户端的身份验证信息、请求的内容类型等。

3.4.3 简单使用示例

@RestController
public class HeaderController {
    @GetMapping("/headers")
    public String getHeaderInfo(@RequestHeader("User - Agent") String userAgent) {
        return "User - Agent: " + userAgent;
    }
}

3.4.4 请求时的示例

客户端发起请求时,请求头中会包含 User - Agent 信息:

GET http://localhost:8080/headers
User - Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36

服务器会将请求头中的 User - Agent 值绑定到 getHeaderInfo 方法的 userAgent 参数上。

3.5 @CookieValue

3.5.1 介绍

@CookieValue 注解用于将 HTTP 请求中的 Cookie 值绑定到方法的参数上。

可以通过该注解获取指定名称的 Cookie 值。

3.5.2 使用场景

当需要从请求的 Cookie 中获取某些信息时使用,例如获取用户的会话 ID、用户偏好设置等。

3.5.3 简单使用示例

@RestController
public class CookieController {
    @GetMapping("/cookies")
    public String getCookieValue(@CookieValue("sessionId") String sessionId) {
        return "Session ID: " + sessionId;
    }
}

3.5.4 请求时的示例

客户端发起请求时,请求头中包含 Cookie 信息:

GET http://localhost:8080/cookies
Cookie: sessionId=1234567890

服务器会将 Cookie 中 sessionId 的值绑定到 getCookieValue 方法的 sessionId 参数上。

3.6 @MatrixVariable

3.6.1 介绍

@MatrixVariable 注解用于从 URL 的矩阵变量中获取值。

矩阵变量是一种在 URL 路径中传递多个参数的方式,使用分号(;)分隔不同的参数。

3.6.2 使用场景

当需要在 URL 路径中传递多个相关参数时使用,例如在搜索商品时,同时传递商品的颜色、尺寸等参数。

3.6.3 简单使用示例

@RestController
@RequestMapping("/products")
public class ProductController {

    @GetMapping("/{category}/{productId}")
    public String getProduct(@PathVariable String category, @PathVariable String productId,
                             @MatrixVariable(name = "color", pathVar = "productId", required = false) String color,
                             @MatrixVariable(name = "size", pathVar = "productId", required = false) String size) {
        return "Category: " + category + ", Product ID: " + productId + ", Color: " + color + ", Size: " + size;
    }
}

3.6.4 请求时的示例

GET http://localhost:8080/products/clothes/123;color=red;size=medium

服务器会从 URL 中提取矩阵变量 colorsize 的值,并绑定到方法的相应参数上。

四、请求响应相关注解

4.1 @ResponseBody

4.1.1 介绍

@ResponseBody 注解用于将方法的返回值直接作为 HTTP 响应体返回给客户端,而不是进行视图解析。

通常与 @RestController 配合使用(@RestController 相当于 @Controller@ResponseBody 的组合)。

4.1.2 使用场景

当需要返回 JSON、XML 等数据格式给客户端时使用,例如返回用户信息列表、产品信息等。

4.1.3 简单使用示例

@RestController
public class ResponseBodyController {
    @GetMapping("/data")
    @ResponseBody
    public List<String> getData() {
        return Arrays.asList("Data1", "Data2", "Data3");
    }
}

4.1.4 请求时的示例

客户端发起请求:

GET http://localhost:8080/data

服务器会将 getData 方法返回的列表数据以 JSON 格式直接作为响应体返回给客户端。

如果不使用 @ResponseBody ,控制器方法的返回值会根据类型不同,被 Spring MVC 以视图解析、携带模型数据渲染视图或重定向等方式进行处理。

4.2 @ResponseStatus

4.2.1 介绍

@ResponseStatus 注解用于指定方法返回时的 HTTP 状态码。

可以在方法或类上使用,用于控制响应的状态信息。

4.2.2 使用场景

当需要自定义响应的状态码时使用。

例如在创建资源成功后返回 201 Created 状态码,在资源不存在时返回 404 Not Found 状态码等。

4.2.3 简单使用示例

@RestController
public class StatusController {
    @GetMapping("/status")
    @ResponseStatus(HttpStatus.CREATED) // 201
    public String createResource() {
        return "Resource created";
    }
}

4.2.4 请求时的示例

客户端发起请求:

GET http://localhost:8080/status

服务器会返回 201 Created 状态码和响应体 "Resource created"

五、异常处理相关注解

5.1 @ExceptionHandler

5.1.1 介绍

@ExceptionHandler 注解用于在控制器类中定义异常处理方法,当控制器中的方法抛出指定类型的异常时,会自动调用该注解标注的方法进行处理。

5.1.2 使用场景

当需要对控制器中可能出现的异常进行统一处理,避免异常信息直接暴露给客户端,同时给客户端返回友好的错误信息时使用。

5.1.3 简单使用示例

@RestController
@RequestMapping("/exception")
public class ExceptionController {

    @GetMapping("/error")
    public String throwException() {
        throw new RuntimeException("Something went wrong!");
    }

    @ExceptionHandler(RuntimeException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public String handleRuntimeException(RuntimeException ex) {
        return "Error: " + ex.getMessage();
    }
}

5.1.4 请求时的示例

GET http://localhost:8080/exception/error

throwException 方法抛出 RuntimeException 时,会自动调用 handleRuntimeException 方法进行处理,并返回相应的错误信息和 500 Internal Server Error 状态码。

5.2 @RestControllerAdvice

5.2.1 介绍

@RestControllerAdvice 是一个组合注解,相当于 @ControllerAdvice@ResponseBody 的结合。

它用于定义全局的异常处理类,能够处理所有控制器中抛出的异常。

5.2.2 使用场景

当需要对整个应用中的控制器异常进行统一处理时使用,避免在每个控制器中重复编写异常处理代码。

5.2.3 简单使用示例

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleGlobalException(Exception ex) {
        return new ResponseEntity<>("Global Error: " + ex.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

5.2.4 请求时的示例

当任何控制器中的方法抛出 Exception 类型的异常时,都会被 GlobalExceptionHandler 类中的 handleGlobalException 方法捕获并处理,返回统一的错误响应。

六、其他注解

6.1 @CrossOrigin

6.1.1 介绍

@CrossOrigin 注解用于解决跨域请求的问题。

在前后端分离的开发中,由于浏览器的同源策略,不同源的请求会受到限制,使用该注解可以允许跨域访问。

6.1.2 使用场景

当前后端项目运行在不同的域名或端口时,前端页面向后端 API 发送请求会产生跨域问题,此时可以使用 @CrossOrigin 注解来解决。

6.1.3 简单使用示例

@RestController
@CrossOrigin(origins = "https://www.tqazy.com")
public class CrossOriginController {
    @GetMapping("/cross")
    public String crossOriginRequest() {
        return "Cross - origin request allowed";
    }
}

6.1.4 请求时的示例

前端页面(运行在 https://www.tqazy.com )发起请求:

GET http://localhost:8080/cross

由于在控制器上使用了 @CrossOrigin 注解,允许 https://www.tqazy.com 域名的跨域请求,所以该请求可以正常访问。

喜欢RESTful API风格的Controller中的常用注解这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

Swagger的使用

感谢订阅陶其的个人博客!

我们日常都是基于接口开发项目,一般先试用postman或者其他接口管理工具和前端对好接口。然后双方按照接口开发内容再进行调试。

但是有时候,需要后端自己生成接口文档,那么我们就可以采用Swagger技术,在代码中植入依赖和注解。然后一键生成接口文档,同时可以用于调试和保存。

Swagger对于RestfulAPI风格的接口可以很好的适用。

那么下面我就介绍一下如何在项目中使用Swagger(以SpringBoot项目为例):

一、引入依赖

    <dependencies>
        <!-- RestfulWeb的依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Lombok的依赖 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Swagger的依赖 -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
            <version>4.1.0</version>
        </dependency>
    </dependencies>

二、配置文件

在 [application.yml] 中进行配置

server:
  port: 8080

knife4j:
  enable: true # 开启
  openapi:
    title: 用户管理接口文档
    description: "用户管理接口文档"
    email: 2451203736@qq.com
    concat: 陶其
    url: https://www.tqazy.com
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - com.tqazy.test_swagger.controller # 扫描Controller所在包地址

三、示例代码

UserController.java

import com.tqazy.test_swagger.domain.dto.UserFormDTO;
import io.swagger.annotations.*;
import org.springframework.web.bind.annotation.*;

@Api("用户管理接口")
@RestController
@RequestMapping("/users")
public class UserController {

    @ApiOperation(value = "新增用户接口", notes = "该方法用于在系统中新增一个用户的信息")
    @PostMapping("/addUser")
    @ApiResponses({
            @ApiResponse(code = 200, message = "新增用户成功"),
            @ApiResponse(code = 500, message = "服务器内部错误")
    })
    public String addUser(@ApiParam("用户表单模型信息") @RequestBody UserFormDTO userDTO) {
        return "演示代码,新增用户成功!";
    }
}

UserFormDTO.java

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(value = "用户表单模型", description = "该模型用于新增/修改用户时填写的用户表单信息")
public class UserFormDTO {

    @ApiModelProperty(value = "用户名", example = "蛮吉", required = true)
    private String username;

    @ApiModelProperty(value = "8")
    private String age;

    @ApiModelProperty(value = "用户地址", example = "神圣兽国游尾郡窝窝乡")
    private String address;
}

四、运行测试

启动SpringBoot项目,浏览器访问localhost:8080/doc.html

五、注解使用解析

5.1 控制器类注解

5.1.1 @Api

  • 用法: 用于对控制器类进行说明,描述该类的主要功能。
  • 参数: value 表示控制器的唯一标识;tags 表示该控制器的标签,用于分组展示。
  • 示例:
    @Api("用户管理接口")
  • 效果:

5.2 控制器方法注解

5.2.1 @ApiOperation

  • 用法: 用于对控制器中的方法进行说明,描述该方法的功能。
  • 参数: value 表示方法的简短描述;notes 表示方法的详细描述。
  • 示例代码:
    @ApiOperation(value = "新增用户接口", notes = "该方法用于在系统中新增一个用户的信息")
  • 效果:

5.2.2 @ApiResponses@ApiResponse

  • 用法: @ApiResponses 用于批量定义方法的响应信息,@ApiResponse 用于定义单个响应信息,描述方法可能返回的状态码及对应的含义。
  • 参数: code 表示状态码;message 表示状态码对应的描述信息。
  • 示例代码:
    @ApiResponses({
            @ApiResponse(code = 200, message = "新增用户成功"),
            @ApiResponse(code = 500, message = "服务器内部错误")
    })
  • 效果:

5.3 方法参数注解

5.3.1 @ApiParam

  • 用法: 用于对方法的参数进行说明,描述参数的含义、是否必传等信息。
  • 参数: value 表示参数的简短描述;required 表示该参数是否必传;example 表示参数的示例值。
  • 示例代码:
    @ApiParam("用户表单模型信息")

    效果:

5.4 模型类注解

5.4.1 @ApiModel

  • 用法: 用于对模型类进行说明,描述该类的主要用途。
  • 参数: value 表示模型类的唯一标识;description 表示模型类的详细描述。
  • 示例代码:
    @ApiModel(value = "用户表单模型", description = "该模型用于新增/修改用户时填写的用户表单信息")

    效果:

5.4.2 @ApiModelProperty

  • 用法: 用于对模型类的属性进行说明,描述属性的含义、示例值等信息。
  • 参数: value 表示属性的简短描述;example 表示属性的示例值;required = true 表示属性为必填,默认值:false
  • 示例代码:
    @ApiModelProperty(value = "用户名", example = "蛮吉", required = true)
    private String username;
  • 效果:

喜欢Swagger的使用这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

闲谈自话(2)

感谢订阅陶其的个人博客!

这是我闲得无聊,无厘头的自说自话。观众老爷们如果不喜欢,可以移步看看别的文章。

本来近期没有写文思路,但是昨天晚上有幸在obaby@mars看到一篇很有思考性的文章:《我不配拥有题目》。

标题别在意,这是被迫改了题目。

文章中聊到了现在社会对待AI的现状,我也来浅谈一下自己的观点,不保证“正确”。

从前两年的ChatGPT横空出世开始,新一代的人工智能仿佛又被端上了时代的餐桌,然后转手把元宇宙丢进了垃圾桶。

只不过因为一些众所周知的原因,ChatGPT包括后面的GPT-o1等都没有对中文互联网有什么太多的影响,网上讨论的也就是一些IT相关从业者和那些为了卖课制造焦虑的人。

最多就是一些营销号拿国内百度、抖音的AI产品和GPT进行横向对比,然后得出怎么怎么大的差距等等。

但是随着今年年初DeepSeek的火爆出圈,上到央视的各种报道刷屏,下到各种微信群里的热烈讨论。感觉DeepSeek就好像是救世主一般,给沉寂已久的中文互联网带来了新的生机。

我去了解过开源的DeepSeek对于国外AI产业的巨大影响,也浅浅的使用过DeepSeek。

不得不说,相比于之前国内的那些AI产品,DeepSeek的深度思考给我留下了很深刻的印象。

但是可能是我接触的不深的缘故,我感觉现在的国内社会和舆论对于DeepSeek的追捧和价值评估早已经超过了DeepSeek本身带来的实际价值,到达了另一个不属于它的高度。

就像是现在的《哪吒2》一样。

不可否认,《哪吒2》是一部非常不错的电影,可以看的出制作组的极致的用心和满满的诚意。无论从画面表现还是其影响力,在过去中国动漫电影历史中那也是当之无愧的No.1。

截止目前,《哪吒2》的票房已经达到了恐怖的146亿人民币了。这是一个之前的中国电影连奢望都不敢奢望的高度。

这个票房依然在增长,隐隐有拿下全球前5的趋势。

《哪吒2》的成功真的带来了很多正面的影响。

比如把贵州那个电影院重新盘活,比如让资本认识到快餐式的作品永远不可能得到全民的认可。

但是咱们沉下心仔细想想,当它的票房超过80亿的时候,那后面的票房真的还和《哪吒2》自身什么有关系吗?

还真有,它成了载体。

刚开始的票房是它自身实力的体现,可后面的完全就是全民狂欢的产物,而此时的《哪吒2》不过是那些全民狂欢的载体而已。

一个一定要突破100亿大关、一定要拿下全球动漫电影榜首、一定要突破全球电影总榜多少多少名干翻阿凡达的载体而已。

此时的舆论早已不在乎《哪吒2》的内容是什么,它想讲述和表达的是什么,而只想要疯狂,只想把它当成手里的工具,一把利刃,视图用它去屠杀面前的“敌人”,去屠杀全球电影榜,拿下第一。

从一开始全民主动的支持二刷三刷,到后面开始有意的带节奏,甚至官方都亲自下场助威。

全民都在把《哪吒2》当成一种狂欢节来过。

可这种把一样东西送到远不是它的高度的行为,这是捧杀,而非宠爱。

有人可能会说,我就喜欢,我就想把它高高举起怎么了?

可是被高高举起的东西往往容易被摔个粉碎。

别的不说,一开始的饺子导演还在疯狂的画海报,接受采访。

可是现在呢?据说已经开始“闭关”了,甚至是整个制作组都开始“闭关”了。

这是对《哪吒2》票房如此之高的正常庆祝行为吗?

显然不是,很显然饺子导演也认识到了这波非正常流量带来巨大利益和机遇的同时,背后隐藏着更加巨大的风险。

“流量”是个好东西,但是太多了就不那么好了。

水能载舟亦能覆舟,很显然饺子也认识到了,所以急流勇退是非常明智的做法。

很显然官方也认识到了,所以铺天盖地的宣传几乎瞬间变得悄无声息。前些天互联网上还都在叫嚣拿下阿凡达拿下榜首,短短几天便没再有那么多的讨论了。

可是英歌舞、DeepSeek、宇树机器人等却被轮番的推到了台前,进行再一轮的狂欢。

仿佛这些狂欢是没有止境的,舆论总是需要有一个东西一个锚点在被全民讨论吸引全民的目光才行。

相对于互联网上那盛世繁荣,现实中的各种实际问题却迟迟无法解决。

具体我就不摊开说了,我怕我这篇文也没了。

别的不多,我的上一家公司就倒闭了。原因无他,地方没钱,生生拖垮的。

线上的繁荣无法掩饰线下的实际问题。

话题回到人工智能。

随着DeepSeek开始被大量引入各行各业的各种产品,有人真的思考过它能做什么吗?

代替人工客服?代替程序员?代替设计师?还是提供引导和服务?

伴随的是什么呢?

让成本日益增高的企业削减成本,还是让海量的年轻人失业?

这个和珍妮纺织机不同,但也相同。

问题是我们在这次的“革命”中扮演的是被辞退的“纺织工人”,还是珍妮纺织机的“维修员”,亦或者是珍妮纺织机的“制造者”。

我感觉,现在无论是哪个人工智能产品,当成辅助助手是不错的选择,即使介入了各种产品,其依旧只是助手的角色,不要去指望人工智能成为决策层。想要在各行各业成为质变级别的更替和使用的人工智能还是有很长的路要走的。

那是“强人工智能”才能做到的。

但绝对不是现在。

可是放眼看看,现在各行各业对于人工智能的追捧和评价,和《哪吒2》的现状有什么区别?

我们身在局中,无法看清。但是通过历史的角度去看,这次的“盛宴”要么就像是元宇宙一样的巨大泡泡,要么就是第四次工业革命。

所以,我辈到底该如何自处?

恐怕能做的,唯有保持思考和学习

话说,我感觉现在的中文互联网早已经变了味,早已不是百花齐放、百家争鸣之势,而是需要或主动或被动的靠近主旋律才行。

看似言论自由,每个人都可以畅所欲言,但实际上却是思想越来越禁锢。

所以,我也就只能在自己的这块自留地里肆意奔跑了。

喜欢闲谈自话(2)这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

MybatisPlus(二)核心功能03 — IService接口

感谢订阅陶其的个人博客!

视频课程地址:黑马商城项目

上一节:MybatisPlus(二)核心功能02 — 自定义SQL

本节学习MybatisPlus(MP)的核心功能 — IService接口:

  • IService接口基本用法
  • IService开发基础业务接口
  • IService开发复杂业务接口
  • IService的lambda方法
  • IService批量新增

一、IService 接口基本用法

1.1 IService接口

1.1.1 新增接口

  • save(T) :保存一个实体类。
  • saveBatch(Collection<T>) :批量新增实体对象集合。
  • saveBatch(Collection<T>, int) :批量新增实体对象集合,按照第二个参数分批次新增。
    • 例如:集合中有90个实体对象,第二个参数为20,那么会分为5次进行保存,前4次每次20个,最后一次10个。
  • saveOrUpdateBatch(Collection<T>) :批量新增或修改实体对象集合。
    • 传入的参数会判断是否存在id参数,如果有就会认为是更新操作;如果没有就是新增操作。
  • saveOrUpdateBatch(Collection<T>, int) :分批次,批量新增或修改实体对象集合。效果同上。
  • saveOrUpdate(T) :新增或修改实体对象。
  • saveOrUpdate(T, Wrapper<T>) :新增或修改实体对象,第二个参数定义查询条件,不再是默认id。

1.1.2 删除接口

  • removeById(Serializable) :根据ID删除数据。
  • removeById(Serializable, boolean) :根据ID删除数据。第二个参数是控制逻辑删除或者物理删除。
    • 第二个boolean参数:true:逻辑删除,更新配置的[逻辑删除字段]为[逻辑删除值];false:物理删除。
  • removeById(T) :识别实体对象的主键进行删除。
  • removeByMap(Map<String, Object>) :根据给定的条件映射来构建WHERE子句删除数据。
  • remove(Wrapper<T>) :根据构造条件给定的WHERE子句删除数据。
  • removeByIds(Collection<?>) :根据主键集合批量删除数据。(适合主键集合元素较少时使用,采用DELETE+ID IN
  • removeByIds(Collection<?>, boolean) :根据主键集合批量删除数据。第二个参数控制true(逻辑删除)/false(物理删除)
  • removeBatchByIds(Collection<?>) :根据主键集合分批次批量删除数据。(适合主键集合元素较多时使用,采用DELETE+ID=? ,会采用JDBC批处理方案)
  • removeBatchByIds(Collection<?>, boolean) :根据主键集合分批次批量删除数据。第二个参数效果同上。
  • removeBatchByIds(Collection<?>, int) :根据主键集合分批次批量删除数据。第二个元素控制每批次数量。
  • removeBatchByIds(Collection<?>, int, boolean) :根据主键集合分批次批量删除数据。第二个元素控制每批次数量。第三个参数控制true(逻辑删除)/false(物理删除)。

1.1.3 修改接口

  • 前两个和最后一个就不说了,新增接口时已经介绍了
  • updateById(T) :根据ID进行修改,实体对象只要非空就会修改。
  • update(Wrapper<T>) :根据构造条件进行修改,实体对象只要非空就会修改。
  • update(T, Wrapper<T>) :根据构造条件对实体对象进行修改。
  • updateBatchById(Collection<T>) :根据主键ID批量修改。
  • updateBatchById(Collection<T>, int) :根据主键ID分批次批量修改,第二个参数定义每批次数量。

1.1.4 查询接口(查一个)

  • getById(Serializable) :根据主键ID查询。
  • getOne(Wrapper<T>) :根据构造条件查询。需要保证构造条件只能查到一个,否则会异常。
  • getOne(Wrapper<T>, boolean) :根据构造条件查询。第二个参数:控制异常抛出
    • 如果查询到不止一条数据,正常程序会抛出TooManyResultsException异常。
    • true :会抛出异常;
    • false :不会抛出异常,会返回第一条记录。

1.1.5 查询接口(查统计)

  • count() :查询统计总数
  • count(Wrapper<T>) :根据构造条件查询统计总数

1.1.6 查询接口(查多个/列表)

  • listByIds(Collection<? extends Serializable>) :根据ID集合查询。
  • listByMap(Map<String, Object>) :根据给定的条件映射作为 WHERE 条件查询
  • list(Wrapper<T>) :根据构造条件查询
  • list() :查询所有数据

1.1.7 查询接口(分页查询)

  • page(E, Wrapper<T>) :分页查询。
    • E :分页对象,通常需要实现IPage 接口,常用Page 类,查询结果也会封装到这个对象中。
    • Wrapper<T> :构造查询条件
  • page(E) :没有查询条件,直接分页查询。

1.1.8 Lambda接口

基于这种Lambda的接口,可以直接使用链式编程方式,不用自己去 new 一个 Wrapper

建议:

如果是基于ID等的简单查询或更新就用前面的方法;

如果是复杂查询,那么可以使用lambda进行链式编程查询。

  • lambdaQuery() :创建一个基于 Lambda 表达式的查询条件构造器 LambdaQueryWrapper
  • lambdaQuery(T) :创建基于 Lambda 表达式的查询条件构造器。构造器会根据实体对象中不为 null 的属性自动生成等值查询条件。
  • lambdaUpdate() :创建一个基于 Lambda 表达式的更新条件构造器 LambdaUpdateWrapper

1.2 IService的使用

继承和实现逻辑:

1.2.1 创建自定义Service接口

自定义接口继承IService 即可,需要给出 User 的泛型。

package com.itheima.mp.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

public interface IUserService extends IService<User> {

}

1.2.2 创建自定义Service实现类

自定义接口实现类,实现自定义接口UserService ,并继承ServiceImpl 类并给出两个泛型:UserMapperUser

ServiceImpl 通过UserMapper 调用它继承的BaseMapper 中的方法。

package com.itheima.mp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

}

1.2.3 测试 – 单个新增

package com.itheima.mp.service;

import com.itheima.mp.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;

@SpringBootTest
class IUserServiceTest {

    @Autowired
    private IUserService userService;

    @Test
    void testSaveUser() {
        User user = new User();
        user.setId(6L);
        user.setUsername("李白");
        user.setPassword("123456");
        user.setPhone("18688990012");
        user.setBalance(10086);
        user.setInfo("{\"age\": 1324, \"intro\": \"诗词老师\", \"gender\": \"male\"}");
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        userService.save(user);
    }
}

这里测试了一个 save(Entity) 的接口方法。

运行结果:

​ 成功添加数据。

1.2.4 测试 – 查询列表

    @Test
    void testQueryByIds() {
        List<Long> idList = List.of(1L, 3L, 6L);
        List<User> users = userService.listByIds(idList);
        users.forEach(System.out::println);
    }

运行结果:

1.3 总结

二、IService 开发基础业务接口

2.1 通过案例学习

本节只学习1-4项,第五项为第三节学习,为复杂业务接口。

2.2 引入依赖

因为我们需要实现Restful风格的接口,所以需要引入web依赖。

然后为了方便测试,我们使用swagger进行测试接口,然后需要引入swagger依赖。

pom.xml中加入如下依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
            <version>4.1.0</version>
        </dependency>

2.3 做swagger的配置

在application.yml文件中新增:

knife4j:
  enable: true # 开启
  openapi:
    title: 用户管理接口文档
    description: "用户管理接口文档"
    email: 2451203736@qq.com
    concat: 陶其
    url: https://www.tqazy.com
    version: v1.0.0
    group:
      default:
        group-name: default
        api-rule: package
        api-rule-resources:
          - com.itheima.mp.controller

顺便将mybatis的配置改成mybatis-plus的:

mybatis-plus:
  type-aliases-package: com.tiheima.mp.domain.po
  global-config:
    db-config:
      id-type: auto # ID自增

2.4 导入dto文件和vo文件

UserFormDTO.java

package com.itheima.mp.domain.dto;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户表单实体")
public class UserFormDTO {

    @ApiModelProperty("id")
    private Long id;

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("密码")
    private String password;

    @ApiModelProperty("注册手机号")
    private String phone;

    @ApiModelProperty("详细信息,JSON风格")
    private String info;

    @ApiModelProperty("账户余额")
    private Integer balance;
}

UserVo

package com.itheima.mp.domain.vo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户VO实体")
public class UserVO {

    @ApiModelProperty("用户id")
    private Long id;

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("详细信息")
    private String info;

    @ApiModelProperty("使用状态(1正常 2冻结)")
    private Integer status;

    @ApiModelProperty("账户余额")
    private Integer balance;
}

2.5 创建并编写控制层

UserController.java

import cn.hutool.core.bean.BeanUtil;
import com.itheima.mp.domain.dto.UserFormDTO;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.IUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;

@Api(tags = "用户管理接口")
@RequestMapping("/users")
@RestController
@RequiredArgsConstructor
public class UserController {

    private final IUserService userService;

    @ApiOperation("新增用户接口")
    @PostMapping
    public void saveUser(@RequestBody UserFormDTO userDTO) {
        // 1. 把DTO拷贝到PO
        User user = BeanUtil.copyProperties(userDTO, User.class);
        // 2. 新增
        userService.save(user);
    }

    @ApiOperation("删除用户接口")
    @DeleteMapping("{id}")
    public void deleteUserById(@ApiParam("用户id") @PathVariable("id") Long id) {
        userService.removeById(id, true);
    }

    @ApiOperation("根据id查询用户")
    @GetMapping("{id}")
    public UserVO getUserById (@ApiParam("用户id") @PathVariable("id") Long id) {
        User user = userService.getById(id);
        return BeanUtil.copyProperties(user, UserVO.class);
    }

    @ApiOperation("根据id批量查询")
    @GetMapping
    public List<UserVO> getAllUser(@ApiParam("用户id集合") @RequestParam("idList") List<Long> idList) {
        List<User> users = userService.listByIds(idList);
        return BeanUtil.copyToList(users, UserVO.class);
    }
}

代码注解:

  • @RequiredArgsConstructor : 配合private final IUserService userService;final,可以实现必须的依赖通过构造函数注入,必须的依赖使用final关键字修饰。比@Resources更推荐使用,@Resources是JavaEE的注解,更推荐Spring的注解。

2.6 创建并编写逻辑层

IUserService.java

public interface IUserService extends IService<User> {

}

UserServiceImpl.java

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

}

2.7 创建并编写持久层

UserMapper.java

public interface UserMapper extends BaseMapper<User>{

}

UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.itheima.mp.mapper.UserMapper">

</mapper>

2.8 运行

启动项目,打开浏览器访问:127.0.0.1:8080/doc.html。

2.8.1 新增用户

2.8.2 根据id查询用户

2.8.3 根据id批量查询

2.8.4 删除用户

三、IService开发复杂业务接口

将上面已删除的数据再加回。

3.1 控制层

UserController.java

    @ApiOperation("根据id扣减余额")
    @PostMapping("/{id}/deduction/{money}")
    public String deductionMoneyById(@ApiParam("用户id") @PathVariable("id") Long id, @ApiParam("扣减金额") @PathVariable("money") int money) {
        return userService.deductionMoneyById(id, money);
    }

3.2 逻辑层

IUserService.java

public interface IUserService extends IService<User> {

    String deductionMoneyById(Long id, int money);

}

UserServiceImpl.java

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private UserMapper userMapper;

    @Override
    public String deductionMoneyById(Long id, int money) {
        if(id != null) {
            User user = getById(id); // 调的是ServiceImpl的方法
            if (user != null) {
                if (!user.getStatus().equals(1)) {
                    return "用户状态为非正常状态,扣款失败!";
                } else if (user.getBalance() <= money) {
                    return "用户余额不足,扣款失败!现有余额为:" + user.getBalance();
                } else {
                    userMapper.deductionMoneyById(id, money);
                    user = getById(id);
                    return "扣款成功!现有余额为:" + user.getBalance();
                }
            } else {
                return "查询用户不存在!";
            }
        } else {
            return "参数ID为空!";
        }
    }
}

3.3 持久层

UserMapper.java

    @Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
    void deductionMoneyById(@Param("id") Long id, @Param("money") int money);

3.4 运行

四、IService 的 lambda 方法

4.1 lambda的查询方法

如果按照Mybatis的写法,需要在UserMapper.xml写如下SQL语句:

    <select id="queryUsers" resultType="com.itheima.mp.domain.po.User">
        SELECT *
        FROM user
        <where>
            <if test="name != null">
                AND username LIKE #{name}
            </if>
            <if test="status != null">
                AND `status` = #{status}
            </if>
            <if test="minBalance != null">
                AND balance >= #{minBalance}
            </if>
            <if test="maxBalance != null">
                AND balance <= #{maxBalance}
            </if>
        </where>
    </select>

4.1.1 导入查询条件实体类

UserQuery.java

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}

4.1.2 控制层

UserController.java

    @ApiOperation("根据条件批量查询")
    @GetMapping("/list")
    public List<UserVO> queryUsers(@ApiParam("用户查询条件") UserQuery query) {
        List<User> users = userService.queryUsers(
                query.getName(), query.getStatus(), query.getMinBalance(), query.getMaxBalance());
        return BeanUtil.copyToList(users, UserVO.class);
    }

GET 请求接收参数可以直接使用实体类接收,无需加注解。

4.1.3 逻辑层

IUserService.java

List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance);

UserServiceImpl.java

    @Override
    public List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance) {
        return lambdaQuery()
                .like(name != null, User::getUsername, name)
                .eq(status != null, User::getStatus, status)
                .ge(minBalance != null, User::getBalance, minBalance)
                .le(maxBalance != null, User::getBalance, maxBalance)
                .list();
    }

代码解析:

  • lambdaQuery()default LambdaQueryChainWrapper<T> lambdaQuery(),链式查询 lambda 式。
  • .like(name != null, User::getUsername, name) :模糊查询,第一参数为true时此项才算入查询条件;
  • .eq(status != null, User::getStatus, status) :精准查询,第一参数效果同上;
  • .ge(minBalance != null, User::getBalance, minBalance) :字段值大于等于参数值 查询,第一参数效果同上;
  • .le(maxBalance != null, User::getBalance, maxBalance) :字段值小于等于参数值 查询,第一参数效果同上;
  • .list() :查询列表,返回 List<T>。还可以选:
    • one():查询一条记录,返回 <T>
    • count():查询统计条数,返回 Long
    • page() :分页查询,返回 E page
    • exists():查询是否存在数据,返回 boolean

4.1.4 运行

未完待续。。。

喜欢MybatisPlus(二)核心功能03 — IService接口这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

MybatisPlus(二)核心功能02 — 自定义SQL

感谢订阅陶其的个人博客!

视频课程地址:黑马商城项目

上一节:MybatisPlus(二)核心功能01 — 条件构造器
下一节:MybatisPlus(二)核心功能03 — IService接口
本节学习MybatisPlus(MP)的核心功能 — 自定义SQL:

  • 在业务层拼接SQL的问题
  • 自定义SQL的用法

一、自定义SQL简介

自定义SQL:我们可以利用MyBatisPlus的Wrapper来构建复杂的Where条件,然后自己定义SQL语句中剩下的部分。

二、问题出现

下面的案例是完全手写SQL的方式。

上一节中我们遇到了相似的案例,使用了 LambdaUpdateWrapper ,写法如下:

    @Test
    void testLambdaUpdateWrapper() {
        List<Long> idList = List.of(1L, 2L, 4L);
        LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<User>()
                .setSql(true, "balance = (balance - 200)")
                .in(User::getId, idList);

        userMapper.update(null, wrapper);
    }

而上面的代码是有问题的

代码中 .setSql() 部分是写的SQL语句,但是这整段代码一般情况下是在service层的,也就是业务逻辑层。

而在业务逻辑层代码拼写应该在持久层出现的SQL语句,这是一种很不规范的写法。

那么怎么办呢?

那就将复杂的SQL拼写分为两个部分:

  • 一部分可以由MP直接构建的,交给MP
  • 另一部分相对复杂,MP无法构建的就自定义SQL(手写SQL)

当然这个手写的SQL不是在业务层进行拼写,而是在Mapper层。

只要将业务层中MP构建的传递到Mapper层和自定义SQL组装起来就可以了。

三、自定义SQL用法

用上面的代码进行改写:

3.1 基于Wrapper构建where条件

模拟在业务层进行MP构建:

    @Test
    void testCustomSqlSegment() {
        List<Long> idList = List.of(1L, 2L, 4L);
        int amount = 200;
        // 1. 构建条件
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>().in(User::getId, idList);
        // 2. 自定义SQL方法调用
        userMapper.updateBalanceByIds(wrapper, amount);
    }

3.2 Mapper.java中自定义方法

在mapper层的方法参数中用 @Param 注解声明wrapper变量名称,必须是ew

void updateBalanceByIds(@Param("ew")LambdaQueryWrapper<User> wrapper, @Param("amount") int amount);

3.3 Mapper.xml中自定义SQL

    <update id="updateBalanceByIds">
        UPDATE user SET balance = balance - #{amount} ${ew.customSqlSegment}
    </update>

代码解析:

  • ${ew.customSqlSegment}: 会拼接MP构建好的SQL片段

运行结果:

​ 成功修改数据。

3.4 总结

通过这个方式,我们成功将MP构建放在了业务层,把自定义SQL放在了Mapper层。分层正确。

使用场景:

​ SQL语句,where条件之外的部分,我们无法通过MP更方便的实现,只能用拼接。

​ 那么为了不违背编写规范,采用这种方式:where条件使用MP构建,其余部分进行自定义SQL。

喜欢MybatisPlus(二)核心功能02 — 自定义SQL这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •