注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

谈传统艺术的没落

最近想学快板。 现在这玩意儿竟然也算个冷门了,周边找不到人教。 怎么整?上网看视频,找帖子。 有个帖子就说有一个QQ群,里面有很多教程啊,板谱之类的。 我就加群了,等了三天,终于给通过了。 看资料,QQ群的标签是【60后】、【70后】。我想,这些半大老头是怎么...
继续阅读 »

最近想学快板。


现在这玩意儿竟然也算个冷门了,周边找不到人教。


怎么整?上网看视频,找帖子。


有个帖子就说有一个QQ群,里面有很多教程啊,板谱之类的。


我就加群了,等了三天,终于给通过了。


看资料,QQ群的标签是【60后】、【70后】。我想,这些半大老头是怎么成功建群的(群主已经是古稀之年),同时感慨我国互联网的普及程度之高。


待了一周,群里1000人,没有人发言,除了一些某某入群,不要发广告的公告等等这类通知。


忽然有一天,一个刚入群的小弟打破了宁静,吓得我都不敢登QQ了。



【吐槽】小弟


今年公司年会让我出节目~请各位老师指点一下~没有多长时间了~怎么样短时间内能练出来

我这着急呢


 


【管理员】大佬


临时抱佛脚、为了什么年会“应景”,零基础就想短期之内学会快板的,这些朋友,咱能换个别的练么?放过快板行么?知道这里有多不容易么?眼高手低会害你一辈子


我说的这些,肯定会招惹一部分人不高兴


这门艺术不简单


但凡快板打的、说的达到一定水平的,都是有毅力、坚韧之人


这门艺术需要用时间来不断积累


不断学习不断修正不断钻研,然后,一辈子


单点、双点、基本点、混合点,最基础的东西,有多少人还打不利索呢


更别说吐字归音、用气发声,上板演唱,表演表现,手眼身法步,舞台掌控以及改编、创作了


手眼身法步

每一样都不容易


【活跃】二哥 

表演是最难的


【管理员】大佬 

其实最难的到不是表演

最难的是思想、情感


简单说就是,理解力

任何想表达、表演出来的东西

都是先有一个想法

举个例子

大家很喜欢武松打店


前头对话

武松他闻听一瞪眼……


贤弟你们说话理不合

这是武松的开脸,如何表演表现?

他是什么内心情感?

他的情绪是高是低?


穿的什么?

戴的什么?


跟董平薛霸是什么关系?

用什么语气?

什么眼神?

手放哪儿?

脖子上可是带着枷锁呢


怎么塑造

等等等等

这些只有你先理解到、想到

才可能去表现、表演


当然即便想到了,也需要大量的练习


给自己的“定位”很关键,自己要达到什么艺术水准?如果你的定位是“成为一名快板爱好者”,那么好,你的水平一定是业余中比较“业余”的……如果你的定位是“具有专业水平”的爱好者,那么你的水平一定是业余爱好者中的高水平。所以说,也别埋怨没人教啊、别人教的不行啊,什么什么的,第一,快板并不适合所有人学习(这是说要达到一定的程度,起码有模有样);第二,多找自己的原因


【话唠】三哥


不要埋怨别人不教,别人没有义务去教



这是一个90后沉入70后的世界,进行放肆的请教,结果被众人教育了的事件。


如果某个70后,不慎落入90后的世界,进行无端的说教,估计下场也不会太乐观。起码会收到几百兆的斗图。


回过头来看,一个新人想在公司年会上表演快板,他认为呱嗒呱嗒很好学,又很别致,于是找了个交流群去问问。结果群里反馈你别糟蹋快板了,这是一辈子的艺术,不要包含功利心,它这么的高深,别人也没有义务教你。


再回过头来看,“老先生”们说的确实没错。快板确实不好学,里面有很多的表演技巧和艺术素养,想要有模有样地表演确实很难。



新人的需求是:我如何以最短的时间学会快板表演?


对方的回复是:呸!白日做梦!



这件事情,让我想起来很多人找我做软件。


我老是感觉对方的诉求,达不到开发的标准,以至于解决不了他的问题。


比如,对方想做个馒头,要求里面要有馅儿,馅儿可以是肉的,可以是豆沙的。我说,那你是要馒头还是要包子,或者豆沙包。


对方说,就是个馒头,可以放馅儿,不用点菜了。


我说,算了吧,做不了,我会做馒头,会做包子,但是不会做带有包子特征的馒头。


结果,对方最后找别人做出来了,跟我说你看就是这样子。我一看,就是一个包子。


你会感觉,自己是不是在坚守一些什么,比如《新华字典》里的字词释义之类的东西。


所以,那个新人问,怎么短时间内,在公司年会表演快板。我觉得,不一定要打成李润杰大师那样,也不用单点、双点、混合点纵横交错。没有开场板也无所谓,找一段趣味性强的文本,掌握好节奏,嘀嗒,嘀嗒,一个节奏一句话,在非商演的情况下,也不算是糊弄人,年会的场合不就图个乐呵嘛,演好了演砸了都是个喜剧。


老先生又说了,你那个不叫正宗的快板,快板一定要先有开场板,开场板一定要是“哒嘀咯嘀哒嘀嘀哒”。


老先生息怒,您那正宗快板,能帮小弟解决,五天后,年会表演的问题吗?


为什么非要帮他解决问题?


问得好。


《中国有嘻哈》rap火了,为什么快板没有火?


《一人我饮酒醉》喊麦火了,为什么快板没有火?


仔细看看rap,喊麦,快板这三者,其实从表演形式上是比较接近,是很容易混淆的。


现在人们精神压力都很大,你需要情感发泄释放。


看一个rap,点头哈腰,观众跟着节奏也能点点头,代入感很强。看完了,心情大好。


来一段喊麦,一人我饮酒醉,醉把佳人成双对……是不是跟着喊出来了。


那天一个小孩从喷泉池子上跳下来,喊着:败帝王,斗苍天,夺得皇位已成仙。吓了我一跳,我以为我遇到神仙了。


这个调子是标准的123、123、1234567。数来宝的调子:打竹板,进街来,住户买卖两边排。也有买,也有卖,也有幌子和招牌。


为什么人家火了,你没有火。


我觉得就是坚守的东西太多了。



  1. 形式上,太固定。“哒嘀咯嘀哒嘀嘀哒”对了。“哒咯咯嘀哒嘀嘀哒”错了。全国一个样。不这样,就不是快板。

  2. 内容上,太陈旧。“华蓥山,巍峨耸立万丈多,嘉陵江水,滚滚地东流像开锅。”当年听,可能有共鸣。但是现在人听着没有啥感觉了。另外,第二字念什么?

  3. 思想上,太封闭。自认为这门艺术造诣很高,自己把终身奉献给它,别人也得这样,不允许其他人有丝毫地轻视,并且建立起一道壁垒,阻挡想来尝试的新人。对于传承,坚持宁缺勿滥。就跟谈对象一样,两个人一见面,女的说,除非你忠于我一辈子,我才和你交往。小伙子:你呀?我呀?


我的父辈们,他们小时候边跑边唱的是:闲言碎语不多讲,表一表山东好汉武二郎。因为,他们别无选择。


我的子辈们,他们边跑边唱的千差万别,因为他们不知道怎么选择。谁能解决自己问题,比如好记又能装逼,他们选择谁。


曾经一个深入民间的艺术,随着时代的发展变得鲜为人知,然而它却丝毫没有一点改变。


或许也正是这种坚持,才保持了它的原汁原味。同时,伴随着的,也是它变成了一片标本,靠着国家的文化扶持资金传承。


其实,它是可以自己繁殖的。


原来觉得穿着大褂说单口相声受约束的人,换成西装,说了脱口秀。


原来打不好快板,但是节奏掌握的还挺好的人,带上帽子,唱起了rap。


为什么传统艺术发展不起来?


做煤油灯的,坚持用煤油当燃料,用柴油都不行,电灯更谓之不伦不类。等到这个世界不再生产煤油,他才放弃做灯。他依然还在坚持煤油灯正宗,而大家都在用LED。


作者:TF男孩
来源:juejin.cn/post/7167705542188597285
收起阅读 »

为何在中国MySQL远比PostgreSQL流行

首先在全球范围内,MySQL 一直是领先于 PostgreSQL (下文简称 PG) 的。下图是 DB-Engines 的趋势图,虽然 PG 是近 10 年增长最快的数据库,但 MySQL 依然保持着优势。再来看一下 Google Trends 过去一年的对比...
继续阅读 »

首先在全球范围内,MySQL 一直是领先于 PostgreSQL (下文简称 PG) 的。下图是 DB-Engines 的趋势图,虽然 PG 是近 10 年增长最快的数据库,但 MySQL 依然保持着优势。

再来看一下 Google Trends 过去一年的对比
MySQL 也依然是明显领先的。而进一步看一下地域分布的话
绝大多数地区依然是 MySQL 领先,份额对比在 60:40 ~ 70:30 之间;少数几个国家如俄罗斯不分伯仲;印度的对比是 85:15;而中国则是达到了 96:4,也是 Google Trends 上差异最明显的国家。
笔者从 2009 年左右开始学习数据库相关知识,接触到了 MySQL 5.1 和 PG 8.x。而深度在工作中使用则是 2013 年,那时加入 Google Cloud SQL 开始维护数据库,MySQL 从 5.5 开始,到之后 2017 年 Cloud SQL 推出了 PG 服务,从 9.6 开始,后来一直同时维护 Google 内部的 MySQL 和 PG 分支,也就一直关注着两边的发展。18 年回国后,进一步熟悉了国内的生态。
下面就来尝试分析一下 MySQL 在中国流行度遥遥领先于 PG 的原因。

Windows

MySQL 在 1998 年就提供了 Windows 版本,而 PostgreSQL 则到了 2005 年才正式推出。之前读到的原因是 Windows 早期的版本一直无法很好支持 PostgreSQL 的进程模型。

上手门槛

MySQL 上手更简单,举几个例子:
  1. 连 PG,一定需要指定数据库,而 MySQL 就不需要。psql 大家碰到的问题是尝试连接时报错 FATAL Database xxx does not exist。而 mysql 碰到的问题是连接上去后,执行查询再提示 no database selected。

  2. 访问控制的配置,首先 PG 和 MySQL 都有用户系统,但 PG 还要配置一个额外的 pg_hba (host-based authentication) 文件。

  3. MySQL 的层级关系是:实例 -> 数据库 -> 表,而 PG 的关系是:实例(也叫集群)> 数据库 > Schema > 表。PG 多了一层,而且从行为表现上,PG 的 schema 类似于 MySQL 数据库,而 PG 的数据库类似于 MySQL 的实例。PG 的这个额外层级在绝大多数场景是用不到的,大家从习惯上还是喜欢用数据库作为分割边界,而不是 schema。所以往往 PG 数据库下,也就一个 public schema,这多出来的一层 schema 就是额外的负担。

  4. 因为上面机制的不同,PG 是无法直接做跨库查询的,早年要通过 dblink 插件,后来被 FDW (foreign data wrapper) 取代。

  5. PG 有更加全面的权限体系,数据库对象都有明确的所有者,但这也导致在做测试时,更经常碰到权限问题。
虽然 PostgreSQL 的设计更加严谨,但也更容易把人劝退。就像问卷设计的一个技巧是第一题放一个无脑就能答上来的二选一,这个的目的在于让对方开始答题。

性能

最早 Google 搜索和广告业务都是跑在 MySQL 上的,我读到过当时选型的备忘。其实一开始团队是倾向于 PG 的(我猜测是 PG 的工程质量更加符合团队的技术品味),但后来测试发现 MySQL 的性能要好不少,所以就选型了 MySQL。
现在两者的性能对比已经完全不一样了,而且性能和业务关联性很强,取决于 SQL 复杂度,并发,延迟这些不同的组合。目前在大部分场景下,MySQL 和 PG 的性能是相当的。有兴趣可以阅读 Mark Callaghan (https://smalldatum.blogspot.com/) 的文章。


互联网

最重要的是 LAMP 技术栈,Linux + Apache + MySQL + PHP,诞生于 1998 年,和互联网崛起同步,LAMP 技术栈的普及也带火了 MySQL。这个技术栈的绑定是如此之深,所以时至今日,MySQL 官方客户端 MySQL Workbench 也还是不及 phpMyAdmin 流行。


大厂的号召力

前面提到的 Mark Callaghan 一开始在 Google 的 MySQL 团队,他们给生态做了很多贡献,后来 Google 内部开始用 Spanner 替换 MySQL,Mark 他们就跑到了 Facebook 继续做,又进一步发展了 MySQL 的生态,像当时互联网公司都需要的高可用方案 MHA (Master High Availability) 就是 Mark 在 FB 时期打磨成熟的。当时整个互联网技术以 Google 为瞻,传播链差不多是 Google > Facebook / Twitter > 国内互联网大厂 > 其他中小厂。MySQL 在互联网公司的垄断就这样形成了。
相对的,那段时间 PG 有影响力的文章不多,我唯一有印象的是 Instagram 分享他们 sharding 的方案,提到用的是 PostgreSQL (https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c)。


生态

有了大量使用后,自然就有人去解决碰到的各种问题。先是 InnoDB 横空出世,解决了事务和性能问题。主从,中间件分库分表方案解决了海量服务的扩展和高可用问题。各种 MySQL 相关书籍,培训资料也冒了出来,应该不少人都读过高性能 MySQL (High Performance MySQL) 这本书。
业界有 Percona 这样专注于做 MySQL 技术咨询的公司,他们还研发了一系列工具,比如做大表变更的 pt-online-schema-change(后来 GitHub 还发布了改良版 gh-ost),做备份的 xtrabackup。
国内也做了不少的贡献,阿里给上游贡献了许多 replication 的改进。SQL 审核优化这块,有去哪儿研发的 Inception,小米团队的 SOAR。Parser 有 PingCAP 的 MySQL Parser。
相对而言 PG 在工具链的生态还是差不少,比如 PG 生态里没有开箱即用的 Parser,没有 Parser 也就无法做 SQL 审核。Bytebase 在实现相关功能时,就只能从头开始做。当然这也成为了 Bytebase 产品的核心竞争力,我们是市面上对 PG 变更审核,查询脱敏支持最好的工具,除了大表变更外,功能完全对标 MySQL。


总结和展望

回到中国 MySQL 远比 PostgreSQL 流行的原因,在上面所有列出的要素里,我觉得最核心的还是第一条,MySQL 很早就能跑在 Windows 上,而 PG 不能。因为有了能跑 Windows 这个点,MySQL 成为了 LAMP 的一部分,到后来成为了支撑整个互联网的基石。当时国内大家手头装的都是 windows 操作系统,要开发 web 应用,都用 LAMP 架构,就顺便把 MySQL 带上了。
此外国内还有更明显的头部效应。国内所有互联网公司的技术体系都源自阿里,比如拿研发环境来说,SIT (System Integration Test) 是我回国加入蚂蚁后才接触到的名词,但后来在其他各个地方又都反复遇到。数据库方案也是如此,全套照搬了阿里的 MySQL 方案。就连技术职级也是,找工作先确认对标 P 几。
就在上月,MySQL 5.7 宣布了 EOL,算是给 MySQL 5 系,这个支撑了过去 15 年中国互联网的功勋做了一个告别。
随着 MySQL 的辞旧,PG 的崛起,在这 AI 的黎明,VR 的前夜,下一个 15 年,MySQL 和 PG 之间相爱相杀的故事又该会如何演绎呢。


作者:OSC开源社区
来源:mp.weixin.qq.com/s/i08gzmCtJwpUfuf_sVUTWw

收起阅读 »

有这3个迹象,你就该离职了!

我的前下属小P,在当前这样的环境下,裸辞了。小P在这家公司呆了3年,平时任劳任怨,努力踏实。对一些“007”加班、背黑锅等“非常规”任务,也来者不拒,积极配合。本以为这样就可以获得领导的青睐,能让自己的职场之路更顺畅。但现实却没如他所愿,前段时间公司有个主管空...
继续阅读 »

我的前下属小P,在当前这样的环境下,裸辞了。

小P在这家公司呆了3年,平时任劳任怨,努力踏实。对一些“007”加班、背黑锅等“非常规”任务,也来者不拒,积极配合。

本以为这样就可以获得领导的青睐,能让自己的职场之路更顺畅。但现实却没如他所愿,前段时间公司有个主管空缺,无论从哪个角度看,小P都是最佳人选,小P自己也以为十拿九稳,但没想到领导却提拔了一个刚入职不到半年的新同事,真实的原因并不是那个人能力有多强,而是因为他是领导以前的旧下属,是嫡系“子弟兵”。

小P输给一个寸功未立的“关系户”,心里憋屈,找领导理论。但领导就是领导,一下子指出N条小P在工作中的不足,说他工作效率低、有时候还犯错、而且还缺少大局观。小P听了更感到委屈,自己平时干的都是其他同事不愿接的“脏乱差”的活,有时候一整个项目都靠自己一个人顶着,现在反而成了多做多错,而那位被领导提拔的“嫡系”,手里没啥正事,整天和领导一起吹牛抽烟,反倒成了有大局观。

小P情绪非常低落,天天上班如上坟,对工作提不起丝毫兴趣,而且身体和心理都出现明显不适,于是就下定决心,裸辞了。

小P的这个决定,我是支持的。如果公司的环境对你的发展不利,你也看不到任何改观的迹象,那么止损可能是当下最好的选择。下面跟你聊聊,遇到哪类情况,你应该果断离开。



01

领导故意打压你

脏活累活想着你,升职加薪没你份


人在职场,要能分辨两个最起码的事实,领导是“总用你”还是“重用你”;是喜欢 “你能干活”,还是喜欢“能干活的你”。

领导让你做事的时候,简直把你当成公司“继承人”,技术需要你攻关、客户需要你维护、团队需要你协调、项目需要你加班,好像哪里都需要你,什么活儿你都要负责。

直到每次升职加薪的机会都和自己无缘时,上面说的小P就是典型的例子,脏活累活都丢给他,升职加薪全留给亲信,明显厚此薄彼,不公平、不厚道。出现这种情况,通常有以下几种原因:

1、你是个不会说“不”的软柿子老实人

很多单位都一些老实本分的打工人,他们在基层踏实工作,与人为善,任何人都不想得罪,对领导更是言听计从,从不讨价还价。

这种人确实是大家眼中的“好人”,但这样的人不懂得说“不”,不会向别人展示自己的“边界”,以至于领导或其他人会不断对他试探,把脏活累活、不该他干的活全都丢给他。

最后,所有人都对此习以为常,而他却慢慢变成了多做多错、出力还可能不讨好的“杨白劳”。至于升职加薪,是不太可能轮到他的,他真上去了,这些脏活累活以后丢给谁干?

2、你不是领导的“自己人”

有人的地方就有江湖,没办法,谁让中国是个人情社会呢。不管在哪个团队、哪个群体,人与人之间都会有个亲疏远近,最大的差别只不过是表现的是否明显而已。团队的各种资源、机会都是有限的,拥有权力且怀有私心的某些领导,在分配这些资源的时候,就极有可能向所谓的“自己人”倾斜。

上文中的小P不就是败给了领导的前下属了吗?当然,不是说所有的公司领导,都像小P的领导一样不公平公正,但如果真遇到这种明显不讲规则的人,就算你干的再多,也很难分到肉吃,有时能不能喝口汤,都要看他心情。

3、你是个“能力有限”的人

这里说的“能力有限”,不是说工作能力有限,而是指你只会“工作”,而没有背景、资源、人脉,等其他方面的可交换价值。我曾见过一个行政部前台,半年时间被提拔为部门经理,不是说她工作能力有多强,而是她的亲叔叔是某重点中学校长,她利用这个关系为公司老总的孩子解决了上重点的问题。

我还听过很多带“资”入职的例子,这些人到了公司,就能利用自己的资源,为公司或领导个人,解决很多棘手的问题;而你,只擅长本本分分在自己工位上按部就班的工作(而且能干这种活的人一抓一大把),即使你做再多的工作,和前一类相比,稀缺性和竞争力都是明显不能比的。有了好机会,自然会被那些有“特殊贡献”的人先抢走。



02

部门集体摸鱼,人浮于事


在当前的职场生态中,不少企业人浮于事,充斥着“坐班不做事”、“领导不走我不走”等伪加班、伪忙碌的形式主义。出现这种情况,通常因为两种原因:

1、员工“形式主义”严重

我曾听一位年轻朋友讲过他经历的一件奇葩事。他曾和另一个同事一起负责公司的某个项目,某次两人加班到9点回家,但第二天朋友却发现那位同事发了这样一条朋友圈“一不小心搞到现在,终于忙完了”,配图是办公桌上开着的电脑,电脑上显示的是一份打开的文档,而发布时间则显示“某年某月某日凌晨2点”。而且在这条朋友圈下,公司领导不仅点了赞,还写下“辛苦了”三个字的留言。

这让朋友心里非常不爽,活儿明明是两人一起干的,功劳就全成了同事的了。除了想给领导留下勤奋努力的好印象外,还有人有其他一些目的。比如,没结婚成家的想蹭蹭公司的空调,反正回去也是打游戏,在哪玩不是玩;还有一些“勤俭持家”的是为了蹭公司的班车、食堂,甚至是为了熬点加班补贴,生活不易,能省点就省点,能捞点就捞点。总之,大家就这样互相耗着,早走你就输了。

2、公司领导的“官僚主义”

曾有一位粉丝朋友向我分享过他辞职的故事。他的领导就是一位见不得别人比他早下班的人。只要别人比他早离开公司,他就觉得为别人的工作不饱和,就会在开会的时候含沙射影批评几句,或者给那个“早走”的人多安排些工作。

在一些企业中出现这种集体摸鱼、人浮于事的情况,虽然并不是每个人都故意为之,但就像在电影院里看电影,一旦前排的人站起来,后面的人就必须跟着站起来一样,很多人被周围的环境裹挟,不得不一边讨厌着,一边照样干着。



03

长期情绪内耗,个人成长为零


耐克创始人菲尔·奈特在他的自传中有句名言:人生即是成长,不成长即死亡。我们进入一家公司,除了挣钱以外,还有另外一个非常重要的诉求,即积累经验、学习技能,实现个人成长。但如果我们发现,自己的工作除了赚几两并不值得激动的碎银外,其余全是资源的消耗和精神的内耗,而不能给自己带来任何成长,这样的公司肯定也不是久留之地。

我的一个读者,原来是某互联网公司的技术骨干,后来被一家传统企业挖走,但做了不到一年,就觉得坚持不下去了。老板请他的时候,饼画的很好,说自己认清了趋势,要大力发展搞数字化,一定会给侄子最大的工作支持。

但事实远非如此,他过去之后才发现,自己基本是一个光杆司令,公司里里技术好还有追求只有他一个。老板的想法天天变,又不给资源,就只会让他自己想办法,KPI完不成就各种PUA他。

在这种工作环境下,侄子上班度日如年,每天都充满焦虑,并开始变得越来越不自信。

我知道后,劝他立刻离开这家公司。再呆下去只会增加内耗,没有任何成长可言。

后来,我通过朋友把他推荐到另一家业内知名的公司,小伙子很快又找回了状态,在那里他每天都能从同事、团队那里汲取新的养分,而不是一味的输出、做大量纯消耗的无用功。



04

离职前,给你3个建议


1、不要因为暂时性的困难而离职

王阳明先生有句名言:积之不久,发之必不宏,得之不难,失之必易。意思是说,如果你积累的不深,发挥的时候就不会太持续;一样东西如果得来不费劲,那么失去它也会很容易。

但很多职场人,往往没有这个觉悟,但凡遇到点不称心的事,就会在心里打退堂鼓。每当这个时候,你不妨问自己两个问题:

一是这个问题是不是暂时的,是不是真的不能克服?

二是,这个问题是不是这家公司独有的,当你换另一个公司时,是不是能确保不再出现?这些问题想清楚了,相信答案也就有了。

千万不要轻言放弃,越能够经得起挑战,越能够真正拥有。

2、离职是因为有更好的机会,而不是逃避困难

马克思曾经说过:“人不是出于逃避某种消极力量,而是出于展现自身积极个性,才能获得真正的自由。”离职跳槽从来都不应该出于简单的消极逃避,而应该是因为有了更积极的选择。

什么是更好的选择?比如,有更大更有影响力的平台,愿意以30%的薪水涨幅,以更高的title邀请你加盟,这就非常值得考虑。而不能仅仅情绪上不喜欢现在公司,而慌不择路,毫无规划的随便选一家新公司做“避风港”,甚至不惜平跳、打折跳,这样做既草率又不负责。

3、要有随时离开公司的能力

职场上的成功,不是永远不被解雇,而是永远拥有主动选择职业、随时可以离开公司的能力。这就要求我们在职场中,要注意学习新的技能和认知,要能清晰客观的认识自己,知道哪些是自己真正具备的可复制、可转移、可嫁接的个人能力,而不至于出现“错把平台当能力”的“误会”。

同时,还要在职场中为自己积累一些能拿得出手、更有说服力的“硬通货”,比如过往的出色业绩,良好的业内口碑,这些都能作为你的背书,让你在新的舞台上,更受器重和尊重。良好的开始就是成功的一半,真正有实力的你,搞定成功的另外“一半”,想必也不是什么难事。

大哲学家康德说过:真正的自由,不是你想做什么;而是当你不想做什么时,可以不做什么。有底气、负责任的离职,就是这句话最好的诠释,希望小伙伴们在职场中,都能从事自己喜欢的工作,受到良好的尊重和对待,有丰厚的薪水收入,还能有让人满意的个人成长。加油!

作者:军哥手记
来源:mp.weixin.qq.com/s/BR_uzYx6sEFwMsBRRgsAiw

收起阅读 »

前端小练:kiss小动画分享

web
最近的学习中,哈士奇学习了简单的动画的平移和旋转的用法,同时对于z-index有了一部分了解,那么这次就通过学习写了个kiss动画 人狠话不多,哈士奇给大家献上代码先 <!DOCTYPE html> <html lang="en"> ...
继续阅读 »

最近的学习中,哈士奇学习了简单的动画的平移和旋转的用法,同时对于z-index有了一部分了解,那么这次就通过学习写了个kiss动画


image.png


人狠话不多,哈士奇给大家献上代码先


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div class="container">
<div class="ball" id="l-ball">
<div class="face face-l">
<div class="eye-l eye-ll"></div>
<div class="eye-l eye-lr"></div>
<div class="mouth"></div>
</div>
</div>
<div class="ball" id="r-ball">
<div class="face face-r">
<div class="eye-r eye-rl"></div>
<div class="eye-r eye-rr"></div>
<div class="mouth-r"></div>
<div class="kiss-r">
<div class="kiss"></div>
<div class="kiss"></div>
</div>
</div>
</div>
</div>
</body>
</html>

body{
background-color:#78e08f;
margin: 0;
padding: 0px;
}
.container{
width: 232px;
height: 200px;

position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
.ball{
width: 100px;
height: 100px;
border: 6px solid #000;
border-radius: 50%;
background-color: white;
position: relative;
display: inline-block;/*令块元素变为具有行内块元素的特点表现为可以使得div处于同一行*/
}
.face-l{
width: 70px;
height: 30px;
position: absolute;
right: 0px;
top: 30px;
}
.face-r{
width: 70px;
height: 30px;
position: absolute;
top: 30px;
}
.eye-l{
width: 15px;
height: 14px;
border-bottom:5px solid #000;
border-radius: 50%;
position: absolute;
}
.eye-r{
width: 15px;
height: 14px;
border-top:5px solid #000;
border-radius: 50%;
position: absolute;
}
.eye-ll{
left: 10px;
}
.eye-lr{
right: 5px;
}
.eye-rl{
left: 10px;
}
.eye-rr{
right: 5px;
}
.mouth{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
}
.face-l::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
right: -8px;
top: 20px;
}
.face-l::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
left:-5px;
top: 20px;

}
.face-r::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius:100%;
background-color: red;
position: absolute;
right: 3px;
top: 20px;
}
.face-r::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius: 100%;
background-color: red;
position: absolute;
top: 20px;
}
#l-ball{
z-index: 2;/*设置元素层级,使得左边的小人可以覆盖在右边的小人脸上*/
animation: close-l 4s ease infinite;/*平移4s*/
}
@keyframes close-l{
0%{
transform: translate(0);
}
20%{
transform: translate(20px);
}
35%{
transform: translate(20px);
}
55%{
transform: translate(0);
}
100%{
transform: translate(0);
}

}
.face-l{
animation:face-l 4s ease infinite;
}
@keyframes face-l{
0%{
transform: translate(0) rotate(0);/*translate 平移 rotate旋转*/
}
10%{
transform: translate(0) rotate(0);
}
20%{
transform: translate(5px) rotate(2deg);
}
28%{
transform: translate(0) rotate(0);
}
35%{
transform: translate(5px) rotate(2deg);/*需要写清楚像素,否则没有动画*/
}
50%{
transform: translate(0) rotate(0);
}
100%{
transform: translate(0) rotate(0);
}
}
.kiss-r{
margin: 0 auto;
position: absolute;
left: 35px;
right: 0;
bottom: -5px;
opacity: 0;
animation: kiss-r 4s ease infinite;
}
.kiss{
width: 13px;
height: 10px;
border-radius: 50%;
border-left: 5px solid #000;
display: block;
}
.mouth-r{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
animation: mouth-r 4s ease infinite;
}
#r-ball{
animation: close-r 4s ease infinite;
}
@keyframes close-r{
0%{
transform: translate(0);
}
50%{
transform: translate(0);
}
70%{
transform: translate(-45px)rotate(15deg);
}
85%{
transform: translate(-45px)rotate(-10deg);
}
100%{
transform: translate(0);
}

}
@keyframes mouth-r{
0%{
opacity: 1;
}
50%{
opacity: 1;
}
50.5%{
opacity: 0;
}
/*70%{
opacity: 1;
}*/

84.9%{
opacity: 0;
}
85%{
opacity: 1;
}
100%{
opacity: 1;
}
}
@keyframes kiss-r{
0%{
opacity: 0;
}
50%{
opacity: 0;
}
50.5%{
opacity: 1;
}
84.9%{
opacity: 1;
}
85%{
opacity: 0;
}
100%{
opacity: 0;
}
}

接下来哈士奇为大家依次聊聊这段代码
首先是html的部分:


html主要是使用div对整个页面做出一个布局,哈士奇此次的小人主要是小人的两张脸和五官,因此我们在html的代码创建的过程中需要留出脸 眼睛 嘴巴的部分进行后面的css代码的操作。


在这里有些同学可能会问到,这里的kiss和mouth怎么回事,稍后我们就知道了!


那么再给大家讲讲css的部分:


首先我们通过整个页面的设置,将整个页面背景设置,也就是body部分,去除之前的默认值。


body{
background-color:#78e08f;
margin: 0;
padding: 0px;
}

接下来就是容器container的设置,我们的设置一个大容器用于放下两个小人,通过position中的absolute对于父容器(此处的是body)进行定位,使用translate函数将容器移到页面的正中心的位置


.container{
width: 232px;
height: 200px;

position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}

将大致的位置确定好了以后,我们就可以开始对于两个kiss小人进行操作了


首先确定两个小球的设置


.ball{
width: 100px;
height: 100px;
border: 6px solid #000;
border-radius: 50%;
background-color: white;
position: relative;
display: inline-block;/*令块元素变为具有行内块元素的特点表现为可以使得div处于同一行*/
}

通过border将外形线条确定,这样一来就可以制造出小人外面一圈的线,通过border-radius确定弧度,最后通过relative的相对定位,针对元素的原本位置进行定位。那么为什么要使用display呢?我们都知道,inlie-block可以使得块级元素div转化为具有行内块的特点的元素,因此div中的两个ball小球就能处于同一行了


确定两个小球的位置以后我们开始确定小球的脸


.face-l{
width: 70px;
height: 30px;
position: absolute;
right: 0px;
top: 30px;
}
.face-r{
width: 70px;
height: 30px;
position: absolute;
top: 30px;
}

通过左右两脸的设置确定他们相对于他们父容器l-ball 和r-ball的位置


接下来设置眼睛的相同元素的设置


.eye-l{
width: 15px;
height: 14px;
border-bottom:5px solid #000;
border-radius: 50%;
position: absolute;
}
.eye-r{
width: 15px;
height: 14px;
border-top:5px solid #000;
border-radius: 50%;
position: absolute;
}

对于我们来说,这里的两个眼睛其实就是两个弧线,所以我们只需要确定两根线,然后使用boder-radius进行弯曲,就能把眼睛制造出来了,再通过absolute对于自己的父容器进行定位


.eye-ll{
left: 10px;
}
.eye-lr{
right: 5px;
}
.eye-rl{
left: 10px;
}
.eye-rr{
right: 5px;
}

微调设置眼睛的具体位置


再进行嘴巴的设计


.mouth{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute;
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
}

也是进行一个弯曲的曲线的设计


接下来难度要升级了,两个脸颊红红的部分应该如何实现呢?
这里我们使用到了伪元素进行创建


.face-l::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
right: -8px;
top: 20px;
}
.face-l::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 18px;
height: 8px;
border-radius: 50%;
background-color: red;
position: absolute;
left:-5px;
top: 20px;

}
.face-r::before{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius:100%;
background-color: red;
position: absolute;
right: 3px;
top: 20px;
}
.face-r::after{/*伪元素 必须有定位才能显现*/
content: '';
width: 10px;
height: 10px;
border-radius: 100%;
background-color: red;
position: absolute;
top: 20px;
}

给大家复习一下伪元素的用法,在哈士奇的这个代码中,before和after分别是对于父容器的第一个子元素进行操作,也就是face里面的左眼睛进行操作,针对左眼定位脸颊的位置(记住哦,如果没有给出伪元素的定位,也就是父容器的话,是无法显示伪元素的


这样一来,我们的脸颊也做好了
最后就是属于亲脸颊时的嘴巴部分


.kiss-r{
margin: 0 auto;
position: absolute;
left: 35px;
right: 0;
bottom: -5px;
opacity: 0;
animation: kiss-r 4s ease infinite;
}
.kiss{
width: 13px;
height: 10px;
border-radius: 50%;
border-left: 5px solid #000;
display: block;
}
.mouth-r{
width: 30px;
height: 14px;
border-bottom: 5px solid #000;
border-radius: 50%;
position: absolute
margin: 0 auto;
left: 0;
right: 0;
bottom: -5px;
animation: mouth-r 4s ease infinite;
}

哈士奇在设计中,想要右边的小球能够在最后亲到左边小球,那么光移动嘴巴是不行的,还需要让嘴巴变形成为嘟嘴的样子,因此哈士奇在mouth-r中设置了嘴巴的样式,接下来又在kiss中设置了嘴巴转变后的样式。这就是为什么右边的小球要设置kiss和mouth的原因了!


在kiss-r中大家看到opacity,如果opacity:0; 那么代表着这个块是隐藏状态,如果opacity:1; 那么就是显示的状态


最后就是动画设置的部分了,有些小伙伴已经看出来了哈士奇已经写过的
animation: mouth-r 4s ease infinite;


那么在这给大家讲讲这是个啥意思


animation(声明动画): mouth-r(动画的名字) 4s(时间) ease(规定慢速开始,然后变快,然后慢速结束的过渡效果) infinite(永久执行:动画会循环播放)


先聊聊左脸的动画,哈士奇希望它平移过去,然后做出小鸟依人的蹭一蹭的动作,于是就有了


#l-ball{
z-index: 2;/*设置元素层级,使得左边的小人可以覆盖在右边的小人脸上*/
animation: close-l 4s ease infinite;/*平移4s*/
}
@keyframes close-l{
0%{
transform: translate(0);
}
20%{
transform: translate(20px);
}
35%{
transform: translate(20px);
}
55%{
transform: translate(0);
}
100%{
transform: translate(0);
}

}
.face-l{
animation:face-l 4s ease infinite;
}
@keyframes face-l{
0%{
transform: translate(0) rotate(0);/*translate 平移 rotate旋转*/
}
10%{
transform: translate(0) rotate(0);
}
20%{
transform: translate(5px) rotate(2deg);
}
28%{
transform: translate(0) rotate(0);
}
35%{
transform: translate(5px) rotate(2deg);/*需要写清楚像素,否则没有动画*/
}
50%{
transform: translate(0) rotate(0);
}
100%{
transform: translate(0) rotate(0);
}
}

首先就是在元素中声明需要准备动画,然后在下方使用@keyframes 动画名 写出一个动画的具体内容(我们需要写出什么时候动画要做什么)


比如4s中0%的时候我希望动画开始平移,就写transform:translate()写出平移的位置是多少像素,那么在下一个%出现前,浏览器就会执行你的操作,表示在%~%之间执行动画的操作


rotate()则是进行旋转,使用以后动画将会根据一定的比例进行旋转


最后就是右脸小球的亲亲操作了


#r-ball{
animation: close-r 4s ease infinite;
}
@keyframes close-r{
0%{
transform: translate(0);
}
50%{
transform: translate(0);
}
70%{
transform: translate(-45px)rotate(15deg);
}
85%{
transform: translate(-45px)rotate(-10deg);
}
100%{
transform: translate(0);
}

}
@keyframes mouth-r{
0%{
opacity: 1;
}
50%{
opacity: 1;
}
50.5%{
opacity: 0;
}
/*70%{
opacity: 1;
}*/

84.9%{
opacity: 0;
}
85%{
opacity: 1;
}
100%{
opacity: 1;
}
}
@keyframes kiss-r{
0%{
opacity: 0;
}
50%{
opacity: 0;
}
50.5%{
opacity: 1;
}
84.9%{
opacity: 1;
}
85%{
opacity: 0;
}
100%{
opacity: 0;
}
}

前面还是进行平移操作,到了后面,小球需要进行亲亲,那么哈士奇通过opacity实时操作嘴巴的出现时间,最后在亲的前面的时间把嘟嘴展现出来。


你学会了吗?快去给你的女友写个亲亲动画吧!!


总结与联想


总结


今天哈士奇给大家分享了一个前端小动画的展现,并且逐步为大家解释了一个前端小动画应该如何写出来,在这其中涉及到了transform opacity animation z-index的使用,大家可以简单上手做做哦


联想


那么动画是否还有其他的关键词呢?ease就能解决所有的平移问题吗?我们是否可以通过其他方式展示不同效果呢?


作者:疯犬丨哈士奇
来源:juejin.cn/post/7300460850010734646
收起阅读 »

周爱民:告前端同学书

web
一年前,InfoQ的编辑约请我对前端技术做了些回顾总结,说了三个方面的话题:其一,前端过去的15年大致可以怎样划分;其二,前端的现状以及面临的挑战;其三,前端会有怎样的未来。后来刊发成综述,是《技术15年》。缘于文体变动,访谈的味道十不存一,所以这里再次整理成...
继续阅读 »

一年前,InfoQ的编辑约请我对前端技术做了些回顾总结,说了三个方面的话题:其一,前端过去的15年大致可以怎样划分;其二,前端的现状以及面临的挑战;其三,前端会有怎样的未来。后来刊发成综述,是《技术15年》。缘于文体变动,访谈的味道十不存一,所以这里再次整理成文,是为《告前端同学书》。



作者:周爱民 / aimingoo


各位前端同学,就我的所知来看,每⼀个具体的技术,在其⽅向上都有着不同的标志事件,也因此有着不同的阶段划分。但是我想,如果从我们这个领域对“前端”的认识来观察这件事,⼤概会对不同的前端阶段能有更清晰的认识。


早期前端的从业⼈员⼤多来⾃后端开发者、应⽤软件开发者,或者⽹⻚设计师,⽽并没有专职的前端开发。例如说阿⾥巴巴在 2010 年之前都把前端归在产品部⻔,可⻅前端⼯程师的来源和定位⼀直都很模糊。这个时代,ECMAScript 还陷在 Ed4 的泥坑中没有⾛出来,IE 浏览器带来的标准分裂还没有得到全⾯的修补,源于对这个领域的漠视,⼤⼚优势也没有体现出来,前端开发者们基本上各⾃为战,框架和中间件层出不穷⽽⼜良莠难分,开发⼯具和环境却荒草凄凄以⾄于乏善可陈。但是也正是在这个时代,ES6、CSS3、HTML5 等等都在筑基、渗透与蓄势。


随着专⽤⼯具链和开发流程的成熟,前后端分离的运动从项⽬内部开始蔓延到整个领域,出现了专⻔的前端开发⼯程师、团队,以及随之⽽来的⻆⾊细分,很多独⽴的技术社区就是在这个时代出现的。前后端分离不仅仅是⼀种技术表现,更是⼀种⾏业协作的模式与规范,并且反过来推动了⼯具和框架的⼤发展。信⼼满满的前端不拘于⼀城⼀地,⼀⽅⾯向前、向专业领域推进,从⽽影响到交互与接触层。因此更丰富的界⾯表现,以及从移动设备到⼈机交互界⾯等领域都成了前端的研究⽅向,是所谓“⼤前端”。⽽另⼀⽅⾯则向后、向系统领域渗透,有了所谓⼯程师“全栈化”运动。这个时候的“全栈”,在⼯程上正好符合敏捷团队的需求,在实践上正好⼜叠加上DevOPS、云端开发和⼩应⽤的⼏阵助⼒,前端因此⼀⽚繁华景象。


所以 2008 年左右开始的前后端分离是整个前端第⼆阶段的起点,这场运动改变了软件开发的体系与格局,为随后⼗年的前端成熟期拓开了局⾯。那⼀年的 SD2C 我谈了《VCL 已死、RAD 已死》,⽽⼗年后阿⾥的圆⼼在GMTC 上讲了《前端路上的思考》,可算作对这个时代的预⾔和反思。


相对于之前所说的第⼀、第⼆阶段,我认为如今我们正⾏进在⼀个全新阶段中。这个阶段初起的主要表现是:前端分离为独⽴领域,并向前、后两个⽅向并进之举已然势微。其关键在于,前端这个领域中的内容已经逐渐复杂,⽽其应⽤的体量也将愈加庞⼤,因此再向任何⽅向发展都难尽全⼒、难得全功。


摊⼦铺得⼤了,就需要再分家。所以下⼀个阶段中,将再次发⽣横向的领域分层,⼀些弥合层间差异的技术、⽅法与⼯具也将出现,类似于 Babel 这样的“嵌缝膏”产品将会再次成为⼀时热⻔。但⻓期来说,领域分层带来的是更专精的职业与技能,跨域协作是规约性的、流程化的,以及⼯具适配的。从 ECMAScript 的实践来看,规范的快速更新和迭代已经成为现实,因此围绕规范与接⼝的新的开发技术与⼯程模型,将会在这个阶段中成为主要⼒量,并成为维持系统稳定性的主要⼿段。


这是在⼀个新阶段的前夜。故此,有很多信息并不那么明朗,⽐如说像前后端分离这样的标志性事件并没有出现,亦或者出现了也还没有形成典型影响。我倾向于认为引领新时代的,或者说开启下⼀个阶段的运动将会发⽣在交互领域,也就是说新的交互⽅式决定了前端的未来。之前⾏业⾥在讲的 VR 和 AR(虚拟现实和增强实境)是在这个⽅向上的典型技术,但不唯于此。⼏乎所有在交互⽅式上的变⾰,都会成为⼈们认识与改变这个世界的全新动⼒,像语⾳识别、视觉捕捉、脑机接⼝等等,这些半成熟的或者实验性的技术都在影响着我们对“交互”的理解,从⽽也重新定义了前端。


⾏业⽣态也会重构,如同今天的前端⼤会已经从“XX技术⼤会”中分离出来⼀样,不久之后“交互”也会从前端分化出来,设计、组件化、框架与平台等等也会成体系地分化出来。前端会变得⽐后端更复杂、更多元,以及更加的⽣机勃勃。这样的⽣态起来了,⼀个新的时代也就来临了。简单地说,1、要注重领域与规范,2、要跟进交互与体验,3、要在⽣态中看到机会。


然而,前端的同学们,我们也不要忘记在这背景中回望自身,正视我们前端自己的问题。


其⼀,底⼦还是薄,前端在技术团队与社区的积累上仍然不够。看起来摊⼦是铺开了,但是每每只在“如何应⽤”上下功夫,真正在⽹络、系统、语⾔、编译、机器学习等等⽅⾯有深⼊研究的并不多。⼀直以来,真正有创建性或预⻅性的思想、⽅法与理论鲜⻅于前端,根底薄是⾸要原因。


其⼆,思维转换慢,有些技术与思想抛弃得不够快,不够彻底。不能总是把核⼼放在“三⼤件(JS+CSS+HTML)”上⾯,核⼼要是不变,前端的⾰命也就不会真正开始。要把“Web 前端”前⾯的“Web”去掉,就现实来说,很多⼈连“观望”都没有开始。


其三,还没有找到跟“交互”结合起来的有效⽅法与机制。前端过去⼗年,在 IoT、机器学习、云平台等等每⼀次潮流都卡上了点⼉,但是如果前端的下⼀次转型起于“交互”,那么我们⽬前还没有能⼒适应这样的变化。当然,契机也可能不在于“交互”,但如果这样,我们的准备就更不充分了。


其四,向更多的应⽤领域渗透的动机与动⼒不明确。⻓期以来,前端在各个领域上都只是陪跑,缺乏真正推动这些领域的动机与动⼒。往将来看,这些因素在前端也将持续缺乏。寻求让前端持续发展,甚⾄领跑某些领域的内驱⼒量,任重⽽道远。


同学们,我想我们必须有一种共同的、清醒的认识与认知:浏览器是未来。去操作系统和云化是两个⼤的⽅向,当它们达成⽬标时,浏览器将成为与⽤户接触的唯⼀渠道。研究浏览器,其本质就是研究交互和表现,是前端的“终极私活”。但不要局限于“Web 浏览器”,它必将成为历史,如同操作系统的“⽂件浏览器”⼀样。


要极其关注 JavaScript 的类型化,弱类型是这⻔语⾔在先天条件上的劣势,是它在⼤型化和系统化应⽤中的明显短板。这个问题⼀旦改善,JavaScript 将有⼒量从其它各种语⾔中汲取营养,并得以⾯向更多的开发领域,这是 JavaScript 的未来。


AI 和 WASM 在前端可以成为⻬头并进的技术,⼀个算法,⼀个实现。对于前端来说,性能问题⼀直是核⼼问题,⽽交互与表现必将“⼤型与复杂化”,例如虚拟现实交互,以及模拟反馈等等,⽽ WASM 是应对这些问题的有效⼿段。


所谓交互与表现,本质上都是“空间问题”。亦即是说,前端表现中的所谓布局、块、位置、流等等传统模式与技术,与将来的交互技术在问题上是同源的。就像“盒模型”确定了 CSS 在前端的核⼼地位⼀样,新的空间定位技术,以及与之匹配的表现与交互⽅法是值得关注和跟进的。


前端要有更强的组织⼒,才能应付更⼤规模的系统。这⾥的组织⼒主要是针对⼯程化⽽⾔,所有⼯程化⼯具,其最终的落脚点都在快速、可靠,并以体系化的⽅式来组织⼯程项⽬。这包括⼈、资源、信息、时间、能⼒与关系等等⼯程因素,每个⽅⾯都有问题,都值得投⼊技术⼒量。


相较于新入行的前端的同学们,我能从没有前端走到如今前端的⼤发展,何其幸也。以我⼀路之所⻅,前端真正让我钦佩的是持久的活⼒。前端开发者⼏乎总是⼀个团队中“新鲜⾎液”的代名词,因此前端在业界的每个阶段都⾛在时代的前列。如今看 C 语⾔的⽼迈,操作系统的封闭,后台的保守,以及业务应⽤、产品市场等等各个领域都在筑城⾃守,再看前端种种,便总觉得开放与探索的信念犹在。


曾经与我⼀道的那些早期的前端开发者们,如今有做了主管的,有搞了标准的,有带了团队的,有转了后端的,有做架构做产品做运维等等⼀肩担之,也有开了公司做了顾问从商⼊政的,但也仍然还有在前端⼀线上做着努⼒,仍看好于这⼀个⽅向并在具体事务上勉⼒前⾏的。我曾经说,“任何事情做个⼗年,总会有所成绩的”,如今看来,这个时间还是说少了,得说是:⼏个⼗年地做下去,前端总能做到第⼀。


惟只提醒⼤家,领域分层的潮流之下,层间技术的核⼼不是功能(functional),⽽是能⼒(capabilities)。向应⽤者交付能⼒,需要有体系性的思维,要看向系统的全貌。我们专精于细节没错,专注于⼀城⼀地也没错,然而眼光⾼远⽽脚踏实地,是前端朋友们当有之势。


亦是这个时代予我们的当为之事!


周爱民/aimingoo


初稿于2022.06


此稿于2023.10


作者:裕波
来源:juejin.cn/post/7290751135903236137
收起阅读 »

写给想入门单元测试的你

✨这里是第七人格的博客✨小七,欢迎您的到来~✨ 🍅系列专栏:【架构思想】🍅 ✈️本篇内容: 写给想入门单元测试的你✈️ 🍱本篇收录完整代码地址:gitee.com/diqirenge/s…🍱 一、为什么要进行单元测试 首先我们来看一下标准的软件开发流程是什么样...
继续阅读 »

✨这里是第七人格的博客✨小七,欢迎您的到来~✨


🍅系列专栏:【架构思想】🍅


✈️本篇内容: 写给想入门单元测试的你✈️


🍱本篇收录完整代码地址:gitee.com/diqirenge/s…🍱


一、为什么要进行单元测试


首先我们来看一下标准的软件开发流程是什么样的


01_开发流程规范.png
从图中我们可以看到,单元测试作为开发流程中的重要一环,其实是保证代码健壮性的重要一环,但是因为各种各样的原因,在日常开发中,我们往往不重视这一步,不写或者写的不太规范。那为什么要进行单元测试呢?小七觉得有以下几点:



  • 便于后期重构。单元测试可以为代码的重构提供保障,只要重构代码之后单元测试全部运行通过,那么在很大程度上表示这次重构没有引入新的BUG,当然这是建立在完整、有效的单元测试覆盖率的基础上。

  • 优化设计。编写单元测试将使用户从调用者的角度观察、思考,特别是使用TDD驱动开发的开发方式,会让使用者把程序设计成易于调用和可测试,并且解除软件中的耦合。

  • 文档记录。单元测试就是一种无价的文档,它是展示函数或类如何使用的最佳文档,这份文档是可编译、可运行的、并且它保持最新,永远与代码同步。

  • 具有回归性。自动化的单元测试避免了代码出现回归,编写完成之后,可以随时随地地快速运行测试,而不是将代码部署到设备之后,然后再手动地覆盖各种执行路径,这样的行为效率低下,浪费时间。


不少同学,写单元测试,就是直接调用的接口方法,就跟跑swagger和postMan一样,这样只是对当前方法有无错误做了一个验证,无法构成单元测试网络。


比如下面这种代码


@Test
public void Test1(){
xxxService.doSomeThing();
}

接下来小七就和大家探讨一下如何写好一个简单的单元测试。


小七觉得写好一个单元测试应该要注意以下几点:


1、单元测试是主要是关注测试方法的逻辑,而不仅仅是结果。


2、需要测试的方法,不应该依赖于其他的方法,也就是说每一个单元各自独立。


3、无论执行多少次,其结果是一定的不变的,也就是单元测试需要有幂等性。


4、单元测试也应该迭代维护。


二、单元测试需要引用的jar包


针对springboot项目,咱们只需要引用他的starter即可


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>

下面贴出这个start包含的依赖


<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starters</artifactId>
<version>2.1.0.RELEASE</version>
</parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.1.0.RELEASE</version>
<name>Spring Boot Test Starter</name>
<description>Starter for testing Spring Boot applications with libraries including
JUnit, Hamcrest and Mockito</description>
<url>https://projects.spring.io/spring-boot/#/spring-boot-parent/spring-boot-starters/spring-boot-starter-test</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>https://spring.io</url>
</organization>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
<developers>
<developer>
<name>Pivotal</name>
<email>info@pivotal.io</email>
<organization>Pivotal Software, Inc.</organization>
<organizationUrl>http://www.spring.io</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/spring-projects/spring-boot.git/spring-boot-starters/spring-boot-starter-test</connection>
<developerConnection>scm:git:ssh://git@github.com/spring-projects/spring-boot.git/spring-boot-starters/spring-boot-starter-test</developerConnection>
<url>http://github.com/spring-projects/spring-boot/spring-boot-starters/spring-boot-starter-test</url>
</scm>
<issueManagement>
<system>Github</system>
<url>https://github.com/spring-projects/spring-boot/issues</url>
</issueManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.1.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<version>2.1.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test-autoconfigure</artifactId>
<version>2.1.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.4.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.11.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>1.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>1.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.skyscreamer</groupId>
<artifactId>jsonassert</artifactId>
<version>1.5.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.2.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.xmlunit</groupId>
<artifactId>xmlunit-core</artifactId>
<version>2.6.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

三、单元测试解析与技巧


1、单元测试类注解解析


下面是出现频率极高的注解:


/*
* 这个注解的作用是,在执行单元测试的时候,不是直接去执行里面的单元测试的方法
* 因为那些方法执行之前,是需要做一些准备工作的,它是需要先初始化一个spring容器的
* 所以得找这个SpringRunner这个类,来先准备好spring容器,再执行各个测试方法
*/

@RunWith(SpringRunner.class)
/*
* 这个注解的作用是,去寻找一个标注了@SpringBootApplication注解的一个类,也就是启动类
* 然后会执行这个启动类的main方法,就可以创建spring容器,给后面的单元测试提供完整的这个环境
*/

@SpringBootTest
/*
* 这个注解的作用是,可以让每个方法都是放在一个事务里面
* 让单元测试方法执行的这些增删改的操作,都是一次性的
*/

@Transactional
/*
* 这个注解的作用是,如果产生异常那么会回滚,保证数据库数据的纯净
* 默认就是true
*/

@Rollback(true)

2、常用断言


Junit所有的断言都包含在 Assert 类中。


void assertEquals(boolean expected, boolean actual)检查两个变量或者等式是否平衡
void assertTrue(boolean expected, boolean actual)检查条件为真
void assertFalse(boolean condition)检查条件为假
void assertNotNull(Object object)检查对象不为空
void assertNull(Object object)检查对象为空
void assertArrayEquals(expectedArray, resultArray)检查两个数组是否相等
void assertSame(expected, actual)查看两个对象的引用是否相等。类似于使用“==”比较两个对象
assertNotSame(unexpected, actual)查看两个对象的引用是否不相等。类似于使用“!=”比较两个对象
fail()让测试失败
static T verify(T mock, VerificationMode mode)验证调用次数,一般用于void方法

3、有返回值方法的测试


@Test
public void haveReturn() {
// 1、初始化数据
// 2、模拟行为
// 3、调用方法
// 4、断言
}

4、无返回值方法的测试


@Test
public void noReturn() {
// 1、初始化数据
// 2、模拟行为
// 3、调用方法
// 4、验证执行次数
}

四、单元测试小例


以常见的SpringMVC3层架构为例,咱们分别展示3层架构如何做简单的单元测试。业务场景为用户user的增删改查。


(1)dao层的单元测试


dao层一般是持久化层,也就是与数据库打交道的一层,单元测试尽量不要依赖外部,但是直到最后一层的时候,DAO层的时候,还是要依靠开发环境里的基础设施,来进行单元测试。


@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
@Rollback
public class UserMapperTest {

/**
* 持久层,不需要使用模拟对象
*/

@Autowired
private UserMapper userMapper;

/**
* 测试用例:查询所有用户信息
*/

@Test
public void testListUsers() {
// 初始化数据
initUser(20);
// 调用方法
List<User> resultUsers = userMapper.listUsers();
// 断言不为空
assertNotNull(resultUsers);
// 断言size大于0
Assert.assertThat(resultUsers.size(), is(greaterThanOrEqualTo(0)));
}

/**
* 测试用例:根据ID查询一个用户
*/

@Test
public void testGetUserById() {
// 初始化数据
User user = initUser(20);
Long userId = user.getId();
// 调用方法
User resultUser = userMapper.getUserById(userId);
// 断言对象相等
assertEquals(user.toString(), resultUser.toString());
}

/**
* 测试用例:新增用户
*/

@Test
public void testSaveUser() {
initUser(20);
}

/**
* 测试用例:修改用户
*/

@Test
public void testUpdateUser() {
// 初始化数据
Integer oldAge = 20;
Integer newAge = 21;
User user = initUser(oldAge);
user.setAge(newAge);
// 调用方法
Boolean updateResult = userMapper.updateUser(user);
// 断言是否为真
assertTrue(updateResult);
// 调用方法
User updatedUser = userMapper.getUserById(user.getId());
// 断言是否相等
assertEquals(newAge, updatedUser.getAge());
}

/**
* 测试用例:删除用户
*/

@Test
public void testRemoveUser() {
// 初始化数据
User user = initUser(20);
// 调用方法
Boolean removeResult = userMapper.removeUser(user.getId());
// 断言是否为真
assertTrue(removeResult);
}

private User initUser(int i) {
// 初始化数据
User user = new User();
user.setName("测试用户");
user.setAge(i);
// 调用方法
userMapper.saveUser(user);
// 断言id不为空
assertNotNull(user.getId());
return user;
}
}

(2)service层的单元测试


@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceImplTest {

@Autowired
private UserService userService;

/**
* 这个注解表名,该对象是个mock对象,他将替换掉你@Autowired标记的对象
*/

@MockBean
private UserMapper userMapper;

/**
* 测试用例:查询所有用户信息
*/

@Test
public void testListUsers() {
// 初始化数据
List<User> users = new ArrayList<>();

User user = initUser(1L);

users.add(user);
// mock行为
when(userMapper.listUsers()).thenReturn(users);
// 调用方法
List<User> resultUsers = userService.listUsers();
// 断言是否相等
assertEquals(users, resultUsers);
}

/**
* 测试用例:根据ID查询一个用户
*/

@Test
public void testGetUserById() {
// 初始化数据
Long userId = 1L;

User user = initUser(userId);
// mock行为
when(userMapper.getUserById(userId)).thenReturn(user);
// 调用方法
User resultUser = userService.getUserById(userId);
// 断言是否相等
assertEquals(user, resultUser);

}

/**
* 测试用例:新增用户
*/

@Test
public void testSaveUser() {
// 初始化数据
User user = initUser(1L);
// 默认的行为(这一行可以不写)
doNothing().when(userMapper).saveUser(any());
// 调用方法
userService.saveUser(user);
// 验证执行次数
verify(userMapper, times(1)).saveUser(user);

}

/**
* 测试用例:修改用户
*/

@Test
public void testUpdateUser() {
// 初始化数据
User user = initUser(1L);
// 模拟行为
when(userMapper.updateUser(user)).thenReturn(true);
// 调用方法
Boolean updateResult = userService.updateUser(user);
// 断言是否为真
assertTrue(updateResult);
}

/**
* 测试用例:删除用户
*/

@Test
public void testRemoveUser() {
Long userId = 1L;
// 模拟行为
when(userMapper.removeUser(userId)).thenReturn(true);
// 调用方法
Boolean removeResult = userService.removeUser(userId);
// 断言是否为真
assertTrue(removeResult);
}

private User initUser(Long userId) {
User user = new User();
user.setName("测试用户");
user.setAge(20);
user.setId(userId);
return user;
}

}

(3)controller层的单元测试


@RunWith(SpringRunner.class)
@SpringBootTest
@Slf4j
public class UserControllerTest {

private MockMvc mockMvc;

@InjectMocks
private UserController userController;

@MockBean
private UserService userService;

/**
* 前置方法,一般执行初始化代码
*/

@Before
public void setup() {

MockitoAnnotations.initMocks(this);

this.mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
}

/**
* 测试用例:查询所有用户信息
*/

@Test
public void testListUsers() {
try {
List<User> users = new ArrayList<User>();

User user = new User();
user.setId(1L);
user.setName("测试用户");
user.setAge(20);

users.add(user);

when(userService.listUsers()).thenReturn(users);

mockMvc.perform(get("/user/"))
.andExpect(content().json(JSONArray.toJSONString(users)));
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 测试用例:根据ID查询一个用户
*/

@Test
public void testGetUserById() {
try {
Long userId = 1L;

User user = new User();
user.setId(userId);
user.setName("测试用户");
user.setAge(20);

when(userService.getUserById(userId)).thenReturn(user);

mockMvc.perform(get("/user/{id}", userId))
.andExpect(content().json(JSONObject.toJSONString(user)));
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 测试用例:新增用户
*/

@Test
public void testSaveUser() {
Long userId = 1L;

User user = new User();
user.setName("测试用户");
user.setAge(20);

when(userService.saveUser(user)).thenReturn(userId);

try {
mockMvc.perform(post("/user/").contentType("application/json").content(JSONObject.toJSONString(user)))
.andExpect(content().string("success"));
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 测试用例:修改用户
*/

@Test
public void testUpdateUser() {
Long userId = 1L;

User user = new User();
user.setId(userId);
user.setName("测试用户");
user.setAge(20);

when(userService.updateUser(user)).thenReturn(true);

try {
mockMvc.perform(put("/user/{id}", userId).contentType("application/json").content(JSONObject.toJSONString(user)))
.andExpect(content().string("success"));
} catch (Exception e) {
e.printStackTrace();
}
}

/**
* 测试用例:删除用户
*/

@Test
public void testRemoveUser() {
Long userId = 1L;

when(userService.removeUser(userId)).thenReturn(true);

try {
mockMvc.perform(delete("/user/{id}", userId))
.andExpect(content().string("success"));
} catch (Exception e) {
e.printStackTrace();
}
}

}


五、其他


1、小七认为不需要对私有方法进行单元测试。


2、dubbo的接口,在初始化的时候会被dubbo的类代理,和单测的mock是两个类,会导致mock失效,目前还没有找到好的解决方案。


3、单元测试覆盖率报告


(1)添加依赖


<dependency>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
</dependency>


(2)添加插件


<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
<executions>
<execution>
<id>pre-test</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>post-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>


(3)执行mvn test命令


报告生成位置


image.png


4、异常测试


本次分享主要是针对正向流程,异常情况未做处理。感兴趣的同学可以查看附录相关文档自己学习。


六、附录


1、user建表语句:


CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
`name` VARCHAR(32) NOT NULL UNIQUE COMMENT '用户名',
`age` INT(3) NOT NULL COMMENT '年龄'
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='user示例表';

2、文章小例源码地址:gitee.com/diqirenge/s…


3、mockito官网:site.mockito.org/


4、mockito中文文档:github.com/hehonghui/m…


作者:第七人格
来源:juejin.cn/post/7297608084306821132
收起阅读 »

时光匆匆独白

  每天在固定的时间醒来,不用闹铃,叫醒我的不再是闹铃,可能真的只是年纪大了,习惯了早睡,也习惯了早起。今天不写技术,只想随便写一写,写我平时所想所念。其实想的最多的一个问题就是:生活的意义是什么?的确,这是一个挺傻的问题,但是又情不自禁的去问,或许这正如有些...
继续阅读 »

  每天在固定的时间醒来,不用闹铃,叫醒我的不再是闹铃,可能真的只是年纪大了,习惯了早睡,也习惯了早起。今天不写技术,只想随便写一写,写我平时所想所念。其实想的最多的一个问题就是:生活的意义是什么?的确,这是一个挺傻的问题,但是又情不自禁的去问,或许这正如有些人所说,生活哪有什么意义?不过只是苟且。



0099.png


  或许真的是这样吧,大家怎么想都无所谓,因为这说到底也只是一种感受而已。意义也好,幸福感也罢,每个人对其理解不同,无论怎么想我觉得都没有错。我自认是一个对待生活蛮透亮的一个人,胜不骄败不馁,面对得失也没有太大的情绪波动,但是在外人看来是活的比较傻。


  记得上高中那会儿,面对每天重复的生活,我就会想起韩寒的小说《三重门》,感觉未来的路和现在的路真的也就这样了,固定的套路,每天游走在教室,宿舍,食堂,想起这些内心未免心生波澜,感觉心塞不已,后来想的多了也就不再想了,因为我知道自己跳不出去,作为局内人只能老老实实的在这个圈子里待着,想多了也只是徒增烦恼罢了。


  后来上了大学,第一次出那么远的门,跨越几千公里来到了陌生的城市,还记得刚下火车的那一刻怀揣着对大学的期待,内心激动不已,那时候傻傻的想终于跳出来了,不再只是重复的生活了。现在想想那时候之所以有这样的想法,一方面是对现实的逃避,另一方面还是因为自己too young too simple !后来准备考研,仿佛又回到了高中时候的生活,但是内心却没有了波澜,或许是自己成长了吧,对于学习和生活而言,有了一点点理解,这是一种好听的说法,说的不好听的话,其实自己是对生活有些妥协了。


  再后来呢,自己工作了,却发现自己所面对的生活更加复杂了,那时候为了所谓的户口,拿着现在看来只能勉强度日的薪水,说实话挺累的,这种累不是说工作累,早九晚六的工作并没有多累,其实最累的是心。那时候就在想这就是我所期待的生活吗?难道真的要一条路走到黑,对于普通家庭出身的自己而言,我深知有些东西及时自己取得了,那也将是遍体鳞伤,搭上父母,哎,何必呢。


  于是我选择跳了出来,做了一个外人看来很傻的决定,其实随着后面收入的日渐增加,也并没有为当初的决定感到后悔,因为我知道这就是自己要走的路,而那条在外人看来繁花似锦的大道真的不适合我。生活,是一幅多彩的绘画,细看其中的每个色彩和纹理,都是自己所经历的不同阶段所留下的痕迹。我们走过岁月的长河,感受着生活中的喜怒哀乐,也在不断思考。对也好,错也罢,一切都终将在时间的冲刷之下变成过眼云烟,所以呢,不要为过去的决定感到悔恨。


  在到现在,女儿马上就要上小学了,父母也要退休了,每周末自己会驱车回到老家,这样的生活真的很平淡,少了社交,多了一些陪伴,说实话:这样挺好。现在想生活的意义是什么?我觉得是让他如何再变的充实起来,我想带着家人在闲暇的日子里出去转转,我想能第一时间满足他们的需要。面对未来诸多的不确定性,活在当下,或许生活才能变得更加“骚气”一点。


  好了,又要准备睡觉了!说些题外话,最近刚刚把战神5的主线打完,不得不感叹游戏的制作真的很精良,剧情也很饱满。恰逢最近黑神话线下试玩,大部分反馈都很好,但是也有一部分人言辞激烈,什么抄袭魂类游戏,什么看视频动作坚硬等等,对于这样的声音,我们没必要较真,每个人的喜恶都是他的权利,但是能看国产游戏的点滴进步,希望大家多多给予鼓励吧。


  明天又是新的一天,加油,铁子们!


作者:mikezhu
来源:juejin.cn/post/7270701917838360587
收起阅读 »

风格是一种神秘的力量

看马未都先生讲古董鉴定: 这个落款虽然写的是明朝,其实是现代仿的。这个没落款,一打眼看上去像清康熙年间的,其实是民国仿康熙。民国的也有些价值,建议继续收藏。 我感觉不可思议。 他一看就知道是什么年代的,而且还知道是从那个年代仿了另一个年代。 这专家是不是胡...
继续阅读 »

看马未都先生讲古董鉴定:



这个落款虽然写的是明朝,其实是现代仿的。这个没落款,一打眼看上去像清康熙年间的,其实是民国仿康熙。民国的也有些价值,建议继续收藏。



我感觉不可思议


他一看就知道是什么年代的,而且还知道是从那个年代仿了另一个年代。


这专家是不是胡说八道?


马先生接着讲,每个年代都有自己的风格,虽然每个时代都极力模仿古代,但风格却模仿不了。我看它第一眼立马就知道这东西不真。


对于这个风格,我理解不了。现在科技都这么发达了,我查好了明朝的风格,照着去做,你还能分辨出来?


另外,你不仔细看看色泽,纹理,胎质,一瞅就知道是假的。就靠风格?这是一种感觉啊……


后来,有一件事,让我稍微理解这种感觉了。


朋友圈里出现一些派发红包之类的分享信息。


比如某集团上市,派发红包。当看到这个logo+标题的时候,我立马就感觉这活动是假的。


为了验证猜想,我决定点进去一探究竟。


图片


链接转到浏览器打开,按F12调试发现:域名,是个野域名,一串随机数那种。网页是静态页面,根本没有数据的上传和下载。活动说明上有繁体字,这是规避敏感词审查用的吧。链接最终指向了小广告页面。


假活动。


第一眼看到的时候,我就觉得是假的。


哎,奇怪,我又是靠什么判断的呢?我想,好像我也是靠风格进行的评判。因为有多年互联网从业经验,会有一种行业嗅觉,感觉这不是正常互联网产品的风格。


此时,到自己的行业中,我有些理解马先生所讲的了。


看下面一个风格。


图片


这是杀马特风格,形成于2008年。我小时候,大街小巷很多这样的青年,我当时很羡慕,但是没钱搞。因为当时青年片面模仿日本动漫和欧美摇滚,于是就形成了一种独特的风格。


那么,杀马特在唐朝有没有?肯定没有,因为没有相关的材料和烫染工具。


那么以后会不会有杀马特?可能会有,但是再出现时,必定会结合那时的审美和工艺,不会和2008年那时完全一样。我现在看他都已经感觉丑了。


所以,你看,风格居然是一种独一无二的神秘力量,它结合了特定的时代审美和特定的生产力水平。


其实,就像看一个人,他一开口,你就知道他是一个什么水平的人。


看似是一种感觉,实际上却有很多的客观标准。这些标准都隐藏在了你的判断指标里,最后你对外说那是一种感觉。


作者:TF男孩
来源:juejin.cn/post/7177972605452812343
收起阅读 »

【微信小程序】 token 无感刷新

web
⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖 一、【实现思路】🚩 小程序端登录时,除了返回用户信息,还需返回两个 token 信息 accessToken:用于验证用户身份 refreshToken:用于刷新 a...
继续阅读 »

⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖


一、【实现思路】🚩



  1. 小程序端登录时,除了返回用户信息,还需返回两个 token 信息

    • accessToken:用于验证用户身份

    • refreshToken:用于刷新 accessToken



  2. 当请求返回状态码为401(即 accessToken 过期)时,使用 refreshToken 发起刷新请求

  3. 刷新成功会拿到新的 accessToken 和 refreshToken

  4. 然后使用新的 accessToken 将失败的请求重新发送


二、【流程图】🚩


图中为大致流程,为了看起来简洁直观,略去了与本文内容不相关的步骤


image.png


三、【后端代码】🚩



1. ⌈签发和验证 token⌋🍥




  • 签发 accessToken 时,设置的过期时间是4个小时

  • 签发 refreshToken 时,设置的过期时间是7天

  • 自己在测试的时候,可以把时间改短一点,例如30s

  • 正常实现的效果是:登录时间超过4个小时之后,再次发送需要身份验证的请求,会使用 refreshToken 去请求刷新 accessToken;如果距离上次登录已经超过了7天,则会提示重新登录

  • 这样的话,实现了一定的安全性(因为 accessToken 每隔4个小时就会更新),同时又没有让用户频繁地重新登录



2. ⌈登录⌋🍥




  • 拿到请求参数中的登录凭证(code),以及保存的 appId 和 appSecret

  • 基于上述三个参数发送请求到微信官方指定的服务器地址,获取 openid

  • openid 是小程序登录用户的唯一标识,每次登录时的登录凭证(code)会变,但是获取到的 openid 是不变的

  • 根据 openid 在数据库中查找用户,如果没有查找到,说明本次登录是当前用户的首次登录,需要创建一个新用户,存入数据库中

  • 然后根据用户 id 以及设置的签发密钥进行 accessToken 和 refreshToken 的签发

  • 签发密钥可以是自己随意设置的一段字符串,两个 token 要设置各自对应的签发密钥

  • 这个签发密钥,在进行 token 验证的时候会使用到


四、【前端代码】🚩



1. ⌈封装的登录方法⌋🍥




  • 在创建微信小程序项目时,默认是在根目录下 app.js 的 onLaunch 生命周期函数中进行了登录

  • 也就是说每次在小程序初始化的时候都会进行登录操作

  • 作者这里是把登录操作单独提取出来了,这样可以在个人主页界面专门设置一个登录按钮

  • 当本地存储的用户信息被清除,或者上面提到的 refereshToken 也过期的情况下,我们点击登录按钮进行登录操作


import { loginApi } from '@/api/v1/index'

const login = async () => {
try {
// 登录获取 code
const {code} = await wx.login()
// 调用后端接口,获取用户信息
const {user, accessToken, refreshToken} = await loginApi(code)
wx.setStorageSync('profile', user)
wx.setStorageSync('accessToken', `Bearer ${accessToken}`)
wx.setStorageSync('refreshToken', refreshToken)
} catch (error) {
wx.showToast({
title: '登录失败,请稍后重试',
icon: 'error',
duration: 2000
})
}
}

export default login



2. ⌈封装的请求方法⌋🍥






  • 接收5个参数:url、method、params、needToken、header(点击展开)

    • url: 请求地址,是部分地址(例如:/auth/login),后面处理时会将其与设置的 baseUrl(例如:http://localhost:4000/api/v1) 进行拼接

    • method:请求方法,默认值为 'GET'

    • params:请求参数,数据格式为 object,例如: {name: 'test'}

    • needToken:是否需要需要携带 token(即是否需要身份验证),默认值为 false

    • header:请求头信息,数据格式为 object(例如: {'Content-Type': 'application/json'}),默认值为 null






  • 需要携带 token 的请求,先从本地存储中取出 accessToken 信息,然后将其赋值给 header 中的 Authorization 属性(注意:首字母要大写)。在上面的验证 token 代码中,会根据 req.headers.authorization.split(' ')[1] 获取到请求头中传递的 accessToken 信息




  • 调用 wx.request 发送请求,在 success 回调函数中判断请求返回信息中的状态码,根据状态码的不同做对应的操作,这里只讨论401 token 过期的情况





  • 当 token 过期时,从本地存储中获取到 refreshToken,然后调用对应后端接口刷新 token(点击展开)

    • 在刷新请求发送前,需要先判断是否已经有刷新请求被发送且正在处理中(基于 isTokenRefreshing 标识)

    • 如果有,则不必再重复发送刷新请求,但是需要把本次因为 401 token 过期而导致失败的请求存起来(放入 failedRequests 数组中),等待当前正在处理的 token 刷新请求完成后,使用新的 accessToken 重新发送本次请求

    • 如果没有,则发送刷新请求,同时修改 isTokenRefreshing 标识的值为 true






  • 等待刷新请求完成,将返回的新 accessToken 和 refreshToken 存储起来




  • 然后将 failedRequests 中因为等待 token 刷新而存储起来的失败请求,基于新的 accessToken 重新发送





  • 最后将本次因为 401 token过期导致失败的请求,基于新的 accessToken 重新发送(点击展开)

    • 本次操作正常进行 token 刷新请求,说明本次请求也是 token 过期了,而且因为 isTokenRefreshing 标识为 false, 没有将本次失败的请求存入 failedRequests 中





【源码】🚩



【说明】🚩



  • 文中涉及到的代码都是作者本人的书写习惯与风格,若有不合理的地方,欢迎指出

  • 如果本文对您有帮助,烦请动动小手点个赞,谢谢


作者:Libra0809
来源:juejin.cn/post/7299357353538486291
收起阅读 »

【Taro】【微信小程序】token 无感刷新

web
⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖 一、【实现思路】🚩 小程序端登录时,除了返回用户信息,还需返回两个 token 信息 accessToken:用于验证用户身份 refreshToken:用于刷新 a...
继续阅读 »

⌈本文是作者学习过程中的笔记总结,若文中有不正确或需要补充的地方,欢迎在评论区中留言⌋🤖


一、【实现思路】🚩



  1. 小程序端登录时,除了返回用户信息,还需返回两个 token 信息

    • accessToken:用于验证用户身份

    • refreshToken:用于刷新 accessToken



  2. 当请求返回状态码为401(即 accessToken 过期)时,使用 refreshToken 发起刷新请求

  3. 刷新成功会拿到新的 accessToken 和 refreshToken

  4. 然后使用新的 accessToken 将失败的请求重新发送



👉具体实现可以翻阅作者的上一篇文章:【微信小程序】token 无感刷新



👉实现思路中的后三步,微信小程序中是在请求的 success 回调函数中做的处理,Taro 中则是设置了响应拦截器,在拦截器中做的对应处理,本文仅讨论有区别的这部分



二、【前端代码】🚩



1. ⌈封装的请求方法⌋🍥






  • 接收5个参数:url、method、params、needToken、header(点击展开)

    • url: 请求地址,是部分地址(例如:/auth/login),后面处理时会将其与设置的 baseUrl(例如:http://localhost:4000/api/v1) 进行拼接

    • method:请求方法,默认值为 'GET'

    • params:请求参数,数据格式为 object,例如: {name: 'test'}

    • needToken:是否需要需要携带 token(即是否需要身份验证),默认值为 false

    • header:请求头信息,数据格式为 object(例如: {'Content-Type': 'application/json'}),默认值为 null






  • 需要携带 token 的请求,先从本地存储中取出 accessToken 信息,然后将其赋值给 header 中的 Authorization 属性(注意:首字母要大写)。后端接口在验证 token 时,会根据 req.headers.authorization.split(' ')[1] 获取到请求头中传递的 accessToken 信息




  • 先通过 Taro.addInterceptor 设置拦截器,然后调用 Taro.request 发送请求。这样的话,当请求真正发送之前以及获取到响应信息时,都会先进入到拦截器中,我们就是在这里进行的 token 刷新操作





  • 具体代码(点击展开)
    import Taro from '@tarojs/taro'
    import { baseUrl } from '../config'
    import responseInterceptor from '../http/interceptors'

    // 添加拦截器
    Taro.addInterceptor(responseInterceptor)

    // 封装的请求方法
    const request = (url, method = 'GET', params = {}, needToken = false, header = null) => {
    const {contentType = 'application/json'} = header || {}
    if (url.indexOf(baseUrl) === -1) url = baseUrl + url

    const option = {
    url,
    method,
    data: method === 'GET' ? {} : params,
    header: {'Content-Type': contentType}
    }

    // 处理 token
    if (needToken) {
    const token = Taro.getStorageSync('accessToken')

    if (token) {
    option['header']['Authorization'] = token
    } else {
    Taro.setStorageSync('profile', null)
    Taro.showToast({
    title: '请登录',
    icon: 'error',
    duration: 2000
    })

    return
    }
    }

    // 发起请求
    return Taro.request(option)
    }

    export default request





2. ⌈拦截器⌋🍥



拦截器是一个函数,接受 chain 对象作为参数。chain 对象中含有 requestParams 属性,代表请求参数。拦截器最后需要调用 chain.proceed(requestParams) 以调用下一个拦截器或者发起请求。Taro 中的这个拦截器没有请求拦截器和响应拦截器之分,具体看你是在调用 chain.proceed(requestParams) 之前还是之后做的操作。具体说明可查阅官方文档




  • 拦截器中先调用 chain.proceed(requestParams) 发送请求,其返回的是一个 promise 对象,所以可以在 .then 中做响应处理




  • .then 中先判断响应状态码,这里我们只讨论 401 token 过期的情况





  • 当 token 过期时,获取本地存储的 refreshToken,然后调用对应后端接口刷新 token(点击展开)

    • 在刷新请求发送前,需要先判断是否已经有刷新请求被发送且正在处理中(基于 isTokenRefreshing 标识)

    • 如果有,则不必再重复发送刷新请求,但是需要把本次因为 401 token 过期而导致失败的请求存起来(放入 failedRequests 数组中),等待当前正在处理的 token 刷新请求完成后,使用新的 accessToken 重新发送本次请求

    • 如果没有,则发送刷新请求,同时修改 isTokenRefreshing 标识的值为 true






  • 等待刷新请求完成,将返回的新 accessToken 和 refreshToken 存储起来




  • 然后将 failedRequests 中因为等待 token 刷新而存储起来的失败请求,基于新的 accessToken 重新发送





  • 最后将本次因为 401 token过期导致失败的请求,基于新的 accessToken 重新发送(点击展开)

    • 本次操作正常进行 token 刷新请求,说明本次请求也是 token 过期了,而且因为 isTokenRefreshing 标识为 false, 没有将本次失败的请求存入 failedRequests 中







  • 具体代码(点击展开)
    import Taro from '@tarojs/taro'
    import { statusCode } from '../config'
    import request from './request'

    // 标识 token 刷新状态
    let isTokenRefreshing = false

    // 存储因为等待 token 刷新而挂起的请求
    let failedRequests = []

    // 设置响应拦截器
    const responseInterceptor = chain => {
    // 先获取到本次请求的参数,后面会使用到
    let {requestParams} = chain

    // 发起请求,然后进行响应处理
    return chain.proceed(requestParams)
    .then(res => {
    switch (res.statusCode) {
    // 404
    case statusCode.NOT_FOUND:
    return Promise.reject({message: '请求资源不存在'})
    // 502
    case statusCode.BAD_GATEWAY:
    return Promise.reject({message: '服务端出现了问题'})
    // 403
    case statusCode.FORBIDDEN:
    return Promise.reject({message: '没有权限访问'})
    // 401
    case statusCode.AUTHENTICATE:
    // 获取 refreshToken 发送请求刷新 token
    // 刷新请求发送前,先判断是否有已发送的请求,如果有就挂起,如果没有就发送请求
    if (isTokenRefreshing) {
    const {url: u, method, params, header} = requestParams
    return failedRequests.push(() => request(u, method, params, true, header))
    }

    isTokenRefreshing = true
    const url = '/auth/refresh-token'
    const refreshToken = Taro.getStorageSync('refreshToken')
    return request(url, 'POST', {refreshToken}, false)
    .then(response => {
    // 刷新成功,将新的 accesToken 和 refreshToken 存储到本地
    Taro.setStorageSync('accessToken', `Bearer ${response.accessToken}`)
    Taro.setStorageSync('refreshToken', response.refreshToken)

    // 将 failedRequests 中的请求使用刷新后的 accessToken 重新发送
    failedRequests.forEach(callback => callback())
    failedRequests = []

    // 再将之前报 401 错误的请求重新发送
    const {url: u, method, params, header} = requestParams
    return request(u, method, params, true, header)
    })
    .catch(err => Promise.reject(err))
    .finally(() => {
    // 无论刷新是否成功,都需要将 isTokenRefreshing 重置为 false
    isTokenRefreshing = false
    })
    // 500
    case statusCode.SERVER_ERROR:
    // 刷新 token 失败
    if (res.data.message === 'Failed to refresh token') {
    Taro.setStorageSync('profile', null)
    Taro.showToast({
    title: '请登录',
    icon: 'error',
    duration: 2000
    })
    return Promise.reject({message: '请登录'})
    }

    // 其他问题导致失败
    return Promise.reject({message: '服务器错误'})
    // 200
    case statusCode.SUCCESS:
    return res.data
    // default
    default:
    return Promise.reject({message: ''})
    }
    })
    .catch(error => {
    console.log('网络请求异常', error, requestParams)
    return Promise.reject(error)
    })
    }

    export default responseInterceptor




【源码】🚩



【说明】🚩



  • 文中涉及到的代码都是作者本人的书写习惯与风格,若有不合理的地方,欢迎指出

  • 如果本文对您有帮助,烦请动动小手点个赞,谢谢


作者:Libra0809
来源:juejin.cn/post/7300592516759306291
收起阅读 »

那些阻碍我们前进的学生思维!

每个人都是从学生时代过来的,但是有人在短暂的时间里面脱胎换骨,而有人却保持学生思维五年,十年甚至更久。 说点心里话! 一、务虚与务实 埋头苦干,只能感动自己! 遗憾的事,我上大学的时候并没有参加过任何比赛,倒不是因为我的能力不如别的同学强,而是过于务实,觉得这...
继续阅读 »

每个人都是从学生时代过来的,但是有人在短暂的时间里面脱胎换骨,而有人却保持学生思维五年,十年甚至更久。


说点心里话!


一、务虚与务实


埋头苦干,只能感动自己!


遗憾的事,我上大学的时候并没有参加过任何比赛,倒不是因为我的能力不如别的同学强,而是过于务实,觉得这些太虚了,相反,很多能力不如我的同学,甚至啥也不懂的,他们也参加了不少比赛,也拿了不少奖,这在他们后续的求职中带了不少亮光!


因为大学生参加的比赛,特别是普通大学,说白了,都没什么难度,学校也不会要求做出什么牛逼的东西,更多的是让大学生能够积极参与,扩展自己的视野。


所以,这时候就别去在乎什么技术深浅,因为你做的东西不会有人拿去用的,而抓住每一次演讲,每一次表演,那才是真的锻炼你的能力的时候。


现在,曾经那些我觉得太务虚的人,有的人已经开始创业并且做得不错了,他们的技术虽然不强,但是他们的思维已经发生了改变,说白了,吹牛逼的时候都要硬气一点,这是实话。


职场也是,并不是你埋头苦干就能得到老板的赏识,老板一天那么忙,你不找机会展示自己,他怎么记住你?


换句话来说,PPT的好坏程度大于埋头苦干,无声的付出不如技巧性的展示。


不过话又说回来,务实是一种品质,也是我们应该坚守的,但是更多的时候要做到有策略的务实,而不是埋头的务实,因为我们大多人搞不了科研,做不了什么轰动的学术。


二、人与人的本质是价值的交换


人与人之间要么有情绪价值的交换,要么有经济价值的交换,如果因为情怀,因为理想主义,那么是毫无意义的。


起初不少同学找我解决问题的时候,我都会花费大量的时间去帮他们解决问题,我免费服务的同学个数不下于100个,那段时间我也才毕业参加工作,所以下班时候都花费了很多时间去无偿服务。


一开始我觉得挺开心,毕竟那时候觉得帮助他人,快乐自己嘛!


但是我却花费了这个世界上最宝贵的东西,那就是时间,我本可以去学习,去提升自己,但是却为了自己所谓的情怀,白白浪费时间,可笑的是免费帮助很多人,却没有得到一句感谢,那么这样的情怀有何意义!


后面我改为付费服务,用时间来换取金钱,这才是成年人该做的事。


这一点对于我的改变很大,我觉得最重要的是我有了付费的习惯,告别白嫖思维,我也会花费金钱去购买服务,购买知识,因为我明白免费的大多都不值钱。


三、以弱势来当挡箭牌


之前一个学弟说,他到了新公司,就给他安排了很有难度的工作,他抱怨道,为什么不能对毕业生友好一点,我回了他一句,“去你妈的,想做简单的,那就滚蛋”,拿毕业生来当挡箭牌,首先就是对自己极度不肯定,不相信自己。


也遇到一些同事和朋友总是抱怨道,“我他妈就这点工资,干嘛给我安排这么多,干嘛让我总是出差,干嘛让我做脏活累活”。


如果总是以工资低,自己工作年限短这些来抱怨,那么就别加入职场,职场本来就是残酷的,并不是请你来过好日子的,如果觉得不公,那么就换,没能力换,那么就要硬着头皮干。


四、别特立独行


“同事都是一群傻逼,一群卷狗,我才不想和他们有任何交集,不想同流合污!”


最近看到朋友圈有人骂同事,领导,但是何不自己回想一下,难道自己就那么清高,那么正确吗?


当你在骂别人的时候,其实大多时候骂的是自己。


在职场中如果太特立独行,总觉得谁不对,谁不行,那么就容易树敌,对于自己的职业发展很不好。


可能你离开了这个公司就觉得逃离了,但是如果把矛盾搞大,对于自己后面的职业都或多或少有一定的影响,试想,如果背调比较严格,那么对自己有利吗?


就算我的同事是傻逼,我也依然要和他做“好朋友”,这是一句很有智慧的话!


圆滑当道的世界,太过于尖锐未必是一件好事!


作者:追梦人刘牌
来源:juejin.cn/post/7300781073376509987
收起阅读 »

如何提升人的思考能力和深度?

除非是天才型如爱因斯坦、图灵等可以推动一个时代的人,否则绝大多数人和人之间的差异并不是很大,大家都是聪明的、有能力的。唯一影响人的是思维方式、自律能力和执行能力。而思维方式是其中最影响人的。那么如何提升人的思考能力和深度呢? 如果我们给到两个看起来同等聪明的人...
继续阅读 »

除非是天才型如爱因斯坦、图灵等可以推动一个时代的人,否则绝大多数人和人之间的差异并不是很大,大家都是聪明的、有能力的。唯一影响人的是思维方式、自律能力和执行能力。而思维方式是其中最影响人的。那么如何提升人的思考能力和深度呢?


如果我们给到两个看起来同等聪明的人同样的时间,那么思维方式更高效的人一定是成长更快的。我们在考虑一个问题的时候,我们应当是思考用什么的方式去思考这个问题更好。考虑思考方式,就是思考用什么思路、思维模式才能得到结果。也就是说先思考工作的方法然后再实施。


举个例子,我们想要将工作安排进一个日程表中,那么什么样的工作安排方式才能保证做出来的日程表上没有疏漏呢?那在安排之前我们就需要先确认工作顺序。如果这个日程表还涉及到了其它人,我们可能要先有大致的计划找到其它相关人确认和同意后再去安排时间更好。


比如一个简单的例子:“三个人都是朋友打算一起去海外旅行,应该选择哪个国家作为目的地比较好?”



  • 首先,对照大家的日程,看看三个人的假期的交集是哪些天数。

  • 列出 10 个在交集天数能去的国家,并且通过搜索等方式提取出这些国家的三个特色的关键词(如海边、法餐、卢浮宫等等具有特色的词语。)

  • 三人分别从观光、美食、娱乐、费用等作出预估和评价、一起讨论。

  • 将三人评价最高的国家作为最终去的地方。


从上述的例子中我们可以看出来,即使是一个复杂的、庞大的问题我们都可以将其分解为小问题,针对每个小问题设计论点,分析每个论点就能得到答案。


无论是什么课题,它需要的方法和流程都一样。首先整理和分解;然后对每个方法做数据分析;最后在每一种方法中找出重点,把重点落实到方案上。


那么我们要如何去训练呢?你可以在看到广告啊、新闻的时候立马设计一个课题,比如“香飘飘奶茶_一年卖出三亿多杯,能_环绕地球一圈”,那它是怎么计算出来的呢?是将销售额 x 杯子大小算出来的长度吗?绕地球一圈是指赤道吗?等等。可以哪个小本子记下来所有想到的问题。我们在看一个新闻时可以先对着标题思考几分钟,再去看内容,如此反复你就会越来越厉害。


为了帮助思考,我们可以利用脑图工具,将问题逐步的梳理到更细节的地方。当然我们是需要反馈的,我们可以找到这些领域的专家、朋友帮忙看看有没有什么建议或者存在有没有什么问题。


如果是一个比较大的课题,我们可以把很多现象(事实)收集起来,然后对他们进行分析,得到结论后给出行动建议。在《靠谱》中有提到“天上出现乌云,眼看就要下雨,带上伞比较好。”,这其实是对事实、分析和行动三者的比喻。


其次我们要特别区分现状事实和意见建议。这个意见是基于客观的数据得出来的,还是你个人的推测?还是最近的普遍趋势?谁都不知道。这样的话也就不能进行严密的讨论。区分事实现状、分析研究、行动方案,明确回答“结论”和“依据”。


《简单聊聊因果推断》


但就像我在因果推断里聊的一样,“厘清影响问题的各个元素并理解其间的关系 , 我们才能更好找出所谓问题的杠杆解。” 。我们在解决问题时需要关注问题所处的上下文、系统关系,以及各个元素之间的内部因果与联系。复杂问题之间互为因果、环环嵌套,因而有时难以分析和筛选、聚焦于某个单一问题而创造的方案,往往只是局部优化。为此我们需要分析每个问题的产生原因和影响结果,找到某个问题所关联的上下游问题。我们只能判断发生事件在时间上的先后是否有统计显著性,并不能判断因果。相关性并不能准确的说明因果关系。


我们人工推论出来的是一个在我们收集到的数据上的局部的(我们不是上帝视角,无法全知全能)相关性分析成果,它不一定是事实,可能只是一个猜测、预测。


我们做个复杂一点的课题的训练吧:“Louis Vuitton 将于 2 月 18 日起上调全球产品售价,幅度将在 8% 至 20% 之间。”


我们可以把这个课题拆解为下面几个问题:



  • 为什么 LV 要涨价?

  • 涨价之后会有什么影响?

  • 是否会影响未来销量?


对于第一个问题来说,无论 LV 为什么要涨价,它一定是基于未来奢侈品市场的销售增长预计与成本之间的博弈决定的,所以我们要先分析出未来的销售额预计。这里引申一个概念叫需求弹性。



一个人对某种商品的需求的唯一普遍规律就是:如果其他情况不变,他对此商品的需求会随着对其拥有量的增加而递减。这种递减也许缓慢也许迅速。如果缓慢,那么他对此商品所出的价格,就不会因为他对此商品的拥有量的大量增加而大幅度下降;而且价格的小幅度下降会使他的购买量大幅度增加。



在下方的问题下,我会补上一些来自于权威新闻网站的 “证据”,大家可以自行看看问题的答案是什么。


为什么 LV 要涨价?



  • 未来奢侈品的销售额预测是怎么样的?


欧洲时报11月16日,咨询公司贝恩(Bain)15 日发布的一项研究显示,到 2022 年,全球奢侈品行业的收入将达到 1.4 万亿欧元,按固定汇率计算,比 2021 年增长13%,这一增长应该使 95% 的奢侈品品牌受益。在美国和欧洲市场的推动下,这是“相当可观”的一年。研究预测,2030 年之前,奢侈品行业增长将保持在 3% 到 8% 。



  • 同类品牌是否涨价?


2022 年1月,Chanel 在去年十一月已经提价的基础上,部分款式将再度涨价。其中,Coco Handle 涨价 2000 元,BusinessAffinity 与 Le Boy 等包型的价格会有8%-12% 的涨幅‍‍。过去三年,Chanel 的涨幅已经高达 60%。


Hermès 的涨价基本固定在每年的1月进行,本轮调价每款涨价范围从 500 元至 4300 元不等。Lindy mini 从原来的 46500 元涨到 48700 元,升幅为 4.7%;


Dior 于 1 月 18 日,小号 Lady Dior 手袋售价从 3.6 万涨至 4.1 万,增幅达 13.9%。



  • 官方的理由是什么?


价格调整考虑了生产成本、原材料、运输以及通货膨胀的变化。



  • 官方的理由是否成立?


根据 LVMH 2022 年的财报来看,经常性业务的利润方面,除了下降 3% 的香水和化妆品外,其他业务的利润均有两位数增长,其中精品零售 2022 年的利润较 2021 年激增 48%,时装与皮具的利润增长 22%,手表和珠宝增长 20%,酒类业务的增速 16% 垫底,但酒类的营收和盈利都创自身年度新高。


管全年总销售额增长 17%,但 LVMH 的利润率却持平。该公司决定将其数十亿美元的营销预算增加三分之一,这已经成为奢侈品行业的普遍趋势。品牌需要在广告上投入巨资,以继续证明价格大幅上涨的合理性。瑞银(UBS)分析师估计,去年奢侈品品牌平均涨价 8%,与全球通胀水平大致一致。



  • 中国放开疫情管控是否有影响?


中国在全球个人奢侈品消费中的份额激增,从 2019 年的 38% 至 39% 猛增到 80%。


在12月26日中国内地宣布将于1月8日取消将近三年的防疫政策后,据《华尔街日报》报导,这不仅让世界各国迎来全球最大的旅游消费来源群体,放鬆政策的消息还提振了全球股市,奢侈品股尤其受益,因这意味佔奢侈品市场约很大部份的中国消费者正式归来。


著名奢侈品巨头 LVMH 集团(LVMH Moet Hennessy Louis Vuitton)早前表示,其亚洲(不包括日本)第一季度的收入增长放缓至8%,但仍是其最大的单一市场地区,佔全球总收入的 37%。


为了迎合日益增加的中国消费者,许多欧洲奢侈品牌自疫情前几年已增聘了会中文的员工,并开始专注开发中国游客喜爱的产品。国际管理谘询公司贝恩公司(Bain & Company)的数字显示,虽然中国消费者受疫情限制而有所放缓,今年只佔全球奢侈品支出最多19%,但预计到2030年他们将佔多达四成的市场份额。


预计中国游客要到2024年才会大规模重返欧洲,进一步提振公司销售业绩。


涨价之后有什么影响?


我们拿它在 2022 年 2 月涨价后的表现来看看:



  • 2022 年四季度营业收入 226.99 亿欧元,同比增长 13.3 %,分析师预期 223.5 亿欧元,全年营收 791.84 亿欧元,较 2021 年增长 23%,分析师预期 787.2 亿欧元。

  • 2022年净利润140.84亿欧元,同比增长17%;来自经常性业务的利润为210.55亿欧元,同比增长23%。


是否会影响未来销量?


2022 财年 LVMH 集团在地缘政治和经济形势的不利影响下,仍然创下历史业绩新高,集团销售收入达 792 亿欧元,营业利润达 211 亿欧元,皆增长 23%。


尽管全年成绩单亮眼,但相较第三季度数据,第四季度所有地区的增长有所放缓。去年第四季度,除日本外,亚洲地区的内生性营收同比下降了8%(该公司只报告每季度的内生性变化)。奢侈品研究所的 Milton Pedraza 分享说:“对于所有顶级奢侈品牌和集团来说,2022年的增长主要来自强劲的定价,而来自同店销量增长的比例则要小得多。”


随着中国政府计划逐步重新开放国际旅游,可能还要有几个月时间,中国游客才能重新大量出现在欧洲。这应当会释放消费,因为考虑到价差和退税,巴黎或米兰等城市的奢侈品价格要比中国便宜多达 40%。


按照 LVMH 的预估,中国要到 2024 年才会完全恢复对奢侈品的消费。


英国央行周四也将关键利率上调 0.5 个百分点,连续第 10 次上调关键利率,但释出信号可能很快就会暂停这一系列加息行动,因同比通胀率下降,经济步履蹒跚。


欧洲央行此举是连续第五次大幅加息,使关键利率达到2.5%,创下2008年以来的最高水平。不过这一水平仍然低于美联储和英国央行的利率,前者在周三将利率提高到4.5%至4.75%,后者则在周四早些时候将利率提高到4%。


天达(Investec) 经济学家在给客户的一份报告中写道,中国放松严格的动态清零政策提振了增长前景,而欧洲天气转暖已帮助缓和能源危机的严重程度。他们把对今年全球经济增长的预测从 2.2% 提高到 2.4%。


总结


LVMH 的涨价本质上是希望在通货膨胀下保持增长,如果希望保持增长就需要投放更多的广告和营销,如果要有钱投入就必须要涨价,而要证明涨价是对的那就得继续投入更多的钱。好在是奢侈品集团们对价格的调控都比较“自由”。


其次考虑到欧洲加息和天气转暖,欧洲的整体区域市场份额也会获得提升。按照现在的预测来看全球的经济似乎是在往好的方向增长,中国放开了疫情管控后以及全球旅游业的逐渐恢复可以有效的给 LVMH 带来一个更大的前景,那么在市场仍然是有很大的需要的情况下,即使维持目前的销售量不变,当价格提升时盈利就会上涨。综合上述在经济回暖的情况下、中国疫情管控放开销售量可能会增加,各大奢侈品集团自然而然就会开始进一步涨价追求更高的利润。LVMH 公司表示,即使面对不确定的地缘政治和宏观经济环境,该公司也有信心通过成本控制和选择性投资的政策,保持目前的增长水平。


作者:Andy_Qin
来源:juejin.cn/post/7196140566110470203
收起阅读 »

创业一年 | 一名普通前端的血泪史

前言 年初我裸辞创业了,跟一个朋友一起合伙做项目,我主要还是做技术部分,开发一个回收类的项目 也是第一次创业,虽然听过很多道理,自己经历过又是另外一回事 我们的项目经历过高峰,现在算是谷底,基本的情况基本就是在苦苦挣扎 这篇文章我会把我所经历的过程讲述出来,在...
继续阅读 »

前言


年初我裸辞创业了,跟一个朋友一起合伙做项目,我主要还是做技术部分,开发一个回收类的项目


也是第一次创业,虽然听过很多道理,自己经历过又是另外一回事


我们的项目经历过高峰,现在算是谷底,基本的情况基本就是在苦苦挣扎


这篇文章我会把我所经历的过程讲述出来,在最后也会总结一些创业过程的一些经验和避坑指南


希望对你有所帮助


自我介绍 & 背景


我是一名快35岁的前端码农,离职前是在银行做的外包开发。2014年开始从事开发,不过刚开始是做的iOS开发,后来又转了web前端开发


眼看35岁大限快到,内心比较着急,着急的去探索一条以后的出路,我想这应该是码农们以后必须要面对的问题


只是年初的时候,正好有一个契机,我就想趁着还没到35岁之前,主动出击,做一番尝试


当前也是做好了充分的准备


契机


这件事的契机就是我给朋友开发一个小程序,业务量在慢慢的起来;一商量就决定去做这件事了,当然也跟家里商量一下,用一年时间来试错


我的理由是:


对于码农来说,年龄是个很难跨过去的坎,这一步迟早是要来的,我不太想被动的等到那一天,我还是想主动的寻找出路


离开上海


我走之前跟一些朋友做了道别,把自己的行李全部塞进来车里,开着车又去了一个陌生的城市(去朋友的城市),离开的时候真的是感慨万千


有家人的地方才有温暖,老婆小孩都走了,一个人呆在上海特不是滋味


业务的高速发展


我们的业务在上半年还是发展很顺利的,通过抖音直播等方式,吸引用户,用户越来越多,业务也慢慢放量了


我们的注册用户有28万多,高峰时期Api调用量有250万


我们的收入来源有以下几部分


会员费


我们的利润来源之一是 会员费


截止目前为止,合伙人有350个左右,早期的话一个合伙人大概赚1000块,后期一个会员赚2000块,代理商有1700个,前期一个赚100,后来涨价一个赚288元,具体前期多少个,已经不清楚了


细算下来,我们收的会员费都大几十万了,应该是赚钱的


产品的差价


我们利润来源之二是产品的差价


我们回收的产品也是有一点利润的,只是不多,一个才几毛钱,甚至几分钱,但也都是有利润的,需要走量


最高峰的时候,一天的订单有10万单,目前已经萎缩到了5万单左右


这其实也是一部分利润,到目前为止,累计应该有1000万单


广告费


我们利润来源之三就是小程序广告费


想必大家都开发过小程序,小程序后台是有广告费的,我们的广告费也不少


但是很遗憾我刚开始开发的时候用的是 别人的 serverless 的框架,对的,就是 LeanCould,这玩意每天有3W的免费的Api调用量,而我们的量非常大,所以被收取的服务器也是不少的


小程序的广告费基本覆盖了服务器的费用,还略微有些结余


最高的一天广告费有1000多,后期慢慢稳定了就是300左右,而服务器的费用大概在200左右


关于小程的广告费,如果没有新人进入的话。他的广告费会下降的很厉害,做过的都知道


下面贴几张图,下面的小程序的广告收入,大部分就在这里了


image.png


image.png


前期一直用的LeanCould也是有对应服务器的 5W多


image.png


后来我自己买了服务器,写了后端,就迁移了平台,也就是目前在用的平台


image.png


隐患


随着时间的不断的推移,慢慢的发现平台有点入不敷出了,太奇怪了


现在才知道,其实出货不太好出了,我们负责出货的朋友,没有及时沟通,导致我们的囤货大量积压,卖不出去


其实这个现象提前就已经有了,但是负责推广和运营的朋友并不知情,一直都是在推广,提现秒到,风控没有做好


等发现问题的时候,其实是有点晚了,我们的货囤了很多了


现在我们的现金流已经非常的吃紧了


最可怕的是我们囤货的东西市场价值越来越低,很多甚至下游都不收了


失败总结


失败的原因肯是很多的,但总结下来有以下几点


1.公司的流水一定要清晰透明


因为很多小团队,甚至就2-3个人,不管有没有盈利,都要做到财务公开和透明,这样有两个好处


第一 防止钱进入私人口袋


第二 每日的现金流可以让随时有危机感,并且可以随时意识到危机


每日公开现金流真的很重要,重要的事情说三遍


每日公开现金流真的很重要,重要的事情说三遍


每日公开现金流真的很重要,重要的事情说三遍


如果实在做不到每日对账,最少一个礼拜一次


2.每个人的股份也要分清楚


很多时候都是好朋友,熟人,就没有太谈这个方面的事情。但是确切的告诉你一定要大方的谈,因为这个影响到后面的利润分成,很容易出现矛盾


关于技术方面


1. 先用LeanClund 开发


为什么一开始是使用serverLess开发



  • 因为速度快,前期产品从0到1,全部都一个人写,功能还是蛮多的如果又去写前端,又去写后端,还要运维,确实开发速度很慢

  • 自己做前端出生,后端的不太会,虽说增删改查比较简单,但是那只是最基础的功能,这是真实的线上项目,从没有弄过,还是不太会

  • serverLess 我比较熟,之前也开发过一个线上项目,只是 调用量比较小


2. 自己写买服务器,全部自己开发


为什么要自己开发全部代码



  • Api调用量大了之后,LeanCould的费用太高了,我们需要节省成本

  • 我们要做自己的APP,因为小程序很多时候会被投诉被封,APP可以规避风险(当然我们运营的好,一直都比较稳定,没有被封过,虽然也开发了APP,后来还是主要用小程序,而当时用LeanCould里面有一些代码与小程序耦合了一点)

  • LeanCould 无法没有实现事务等,随着使用的加深,发现它在底层上有些不足,自己写云端写很多逻辑也是比较麻烦(比如微信里面的很多都是要 服务器去调用腾讯的接口,不得不在云端写代码)、


主要技术栈



  • 前端小程序用 uni-app

  • 后台管理系统 是用 Vue

  • 后端使用Koa

  • 数据库使用 MySqlRedies


系统的一些数据



  • 目前系统有 28W用户

  • 目前300W的订单量,每日月5W的新增订单(以前的平台有1000多W的订单量,暂停了)


后面的打算


虽然还有2个月时候才过年,但是 今天的尝试 基本算失败了,正在思考明年干什么


看看还有什么项目做,或者有什么生意做(前提是有一些资源,否则就不做)


实在不行,可能还得去先找个工作


希望我的经历可以帮助到你,大家有什么问题可以评论区交流


作者:三年三月
来源:juejin.cn/post/7295353579001806902
收起阅读 »

完爆90%的性能毛病,数据库优化八大通用绝招!

毫不夸张的说咱们后端工程师,无论在哪家公司,呆在哪个团队,做哪个系统,遇到的第一个让人头疼的问题绝对是数据库性能问题。如果我们有一套成熟的方法论,能让大家快速、准确的去选择出合适的优化方案,我相信能够快速准备解决咱么日常遇到的80%甚至90%的性能问题。 从解...
继续阅读 »

毫不夸张的说咱们后端工程师,无论在哪家公司,呆在哪个团队,做哪个系统,遇到的第一个让人头疼的问题绝对是数据库性能问题。如果我们有一套成熟的方法论,能让大家快速、准确的去选择出合适的优化方案,我相信能够快速准备解决咱么日常遇到的80%甚至90%的性能问题。


从解决问题的角度出发,我们得先了解到**问题的原因;其次我们得有一套思考、判断问题的流程方式,**让我们合理的站在哪个层面选择方案;最后从众多的方案里面选择一个适合的方案进行解决问题,找到一个合适的方案的前提是我们自己对各种方案之间的优缺点、场景有足够的了解,没有一个方案是完全可以通吃通用的,软件工程没有银弹。


下文的我工作多年以来,曾经使用过的八大方案,结合了平常自己学习收集的一些资料,以系统、全面的方式整理成了这篇博文,也希望能让一些有需要的同行在工作上、成长上提供一定的帮助。



文章首发公众号:码猿技术专栏



为什么数据库会慢?


慢的本质:


慢的本质
查找的时间复杂度查找算法
存储数据结构存储数据结构
数据总量数据拆分
高负载CPU、磁盘繁忙

无论是关系型数据库还是NoSQL,任何存储系统决定于其查询性能的主要有三种:



  • 查找的时间复杂度

  • 数据总量

  • 高负载


而决定于查找时间复杂度主要有两个因素:



  • 查找算法

  • 存储数据结构


无论是哪种存储,数据量越少,自然查询性能就越高,随着数据量增多,资源的消耗(CPU、磁盘读写繁忙)、耗时也会越来越高。


从关系型数据库角度出发,索引结构基本固定是B+Tree,时间复杂度是O(log n),存储结构是行式存储。因此咱们对于关系数据库能优化的一般只有数据量。


而高负载造成原因有高并发请求、复杂查询等,导致CPU、磁盘繁忙等,而服务器资源不足则会导致慢查询等问题。该类型问题一般会选择集群、数据冗余的方式分担压力。



应该站在哪个层面思考优化?



从上图可见,自顶向下的一共有四层,分别是硬件、存储系统、存储结构、具体实现。层与层之间是紧密联系的,每一层的上层是该层的载体;因此越往顶层越能决定性能的上限,同时优化的成本也相对会比较高,性价比也随之越低。以最底层的具体实现为例,那么索引的优化的成本应该是最小的,可以说加了索引后无论是CPU消耗还是响应时间都是立竿见影降低;然而一个简单的语句,无论如何优化加索引也是有局限的,当在具体实现这层没有任何优化空间的时候就得往上一层【存储结构】思考,思考是否从物理表设计的层面出发优化(如分库分表、压缩数据量等),如果是文档型数据库得思考下文档聚合的结果;如果在存储结构这层优化得没效果,得继续往再上一次进行考虑,是否关系型数据库应该不适合用在现在得业务场景?如果要换存储,那么得换怎样得NoSQL?


所以咱们优化的思路,出于性价比的优先考虑具体实现,实在没有优化空间了再往上一层考虑。当然如果公司有钱,直接使用钞能力,绕过了前面三层,这也是一种便捷的应急处理方式。


该篇文章不讨论顶与底的两个层面的优化,主要从存储结构、存储系统中间两层的角度出发进行探讨


八大方案总结



 数据库的优化方案核心本质有三种:减少数据量用空间换性能选择合适的存储系统,这也对应了开篇讲解的慢的三个原因:数据总量、高负载、*查找的时间复杂度。*


  这里大概解释下收益类型:短期收益,处理成本低,能紧急应对,久了则会有技术债务;长期收益则跟短期收益相反,短期内处理成本高,但是效果能长久使用,扩展性会更好。


  静态数据意思是,相对改动频率比较低的,也无需过多联表的,where过滤比较少。动态数据与之相反,更新频率高,通过动态条件筛选过滤。


减少数据量


减少数据量类型共有四种方案:数据序列化存储、数据归档、中间表生成、分库分表。


就如上面所说的,无论是哪种存储,数据量越少,自然查询性能就越高,随着数据量增多,资源的消耗(CPU、磁盘读写繁忙)、耗时也会越来越高。目前市面上的NoSQL基本上都支持分片存储,所以其天然分布式写的能力从数据量上能得到非常的解决方案。而关系型数据库,查找算法与存储结构是可以优化的空间比较少,因此咱们一般思考出发点只有从如何减少数据量的这个角度进行选择优化,因此本类型的优化方案主要针对关系型数据库进行处理。



数据归档



注意点:别一次性迁移数量过多,建议低频率多次限量迁移。像MySQL由于删除数据后是不会释放空间的,可以执行命令OPTIMIZE TABLE释放存储空间,但是会锁表,如果存储空间还满足,可以不执行。



关注公众号:码猿技术专栏,回复关键词:1111 获取阿里内部的java性能调优手册



建议优先考虑该方案,主要通过数据库作业把非热点数据迁移到历史表,如果需要查历史数据,可新增业务入口路由到对应的历史表(库)。



中间表(结果表)



中间表(结果表)其实就是利用调度任务把复杂查询的结果跑出来存储到一张额外的物理表,因为这张物理表存放的是通过跑批汇总后的数据,因此可以理解成根据原有的业务进行了高度的数据压缩。以报表为例,如果一个月的源数据有数十万,我们通过调度任务以月的维度生成,那么等于把原有的数据压缩了几十万分之一;接下来的季报和年报可以根据月报*N来进行统计,以这种方式处理的数据,就算三年、五年甚至十年数据量都可以在接受范围之内,而且可以精确计算得到。


那么数据的压缩比率是否越低越好?下面有一段口诀:



  • 字段越多,粒度越细,灵活性越高,可以以中间表进行不同业务联表处理。

  • 字段越少,粒度越粗,灵活性越低,一般作为结果表查询出来。


数据序列化存储




在数据库以序列化存储的方式,对于一些不需要结构化存储的业务来说是一种很好减少数据量的方式,特别是对于一些M*N的数据量的业务场景,如果以M作为主表优化,那么就可以把数据量维持最多是M的量级。另外像订单的地址信息,这种业务一般是不需要根据里面的字段检索出来,也比较适合。


这种方案我认为属于一种临时性的优化方案,无论是从序列化后丢失了部份字段的查询能力,还是这方案的可优化性都是有限的。


分库分表


分库分表作为数据库优化的一种非常经典的优化方案,特别是在以前NoSQL还不是很成熟的年代,这个方案就如救命草一般的存在。


如今也有不少同行也会选择这种优化方式,但是从我角度来看,分库分表是一种优化成本很大的方案。这里我有几个建议:



  1. 分库分表是实在没有办法的办法,应放到最后选择。

  2. 优先选择NoSQL代替,因为NoSQL诞生基本上为了扩展性与高性能。

  3. 究竟分库还是分表?量大则分表,并发高则分库

  4. 不考虑扩容,一部做到位。因为技术更新太快了,每3-5年一大变。


拆分方式



只要涉及到这个拆,那么无论是微服务也好,分库分表也好,拆分的方式主要分两种:垂直拆分、水平拆分


垂直拆分更多是从业务角度进行拆分,主要是为了**降低业务耦合度;**此外以SQL Server为例,一页是8KB存储,如果在一张表里字段越多,一行数据自然占的空间就越大,那么一页数据所存储的行数就自然越少,那么每次查询所需要IO则越高因此性能自然也越慢;因此反之,减少字段也能很好提高性能。之前我听说某些同行的表有80个字段,几百万的数据就开始慢了。


水平拆分更多是从技术角度进行拆分,拆分后每张表的结构是一模一样的,简而言之就是把原有一张表的数据,通过技术手段进行分片到多张表存储,从根本上解决了数据量的问题。




路由方式



进行水平拆分后,根据分区键(sharding key)原来应该在同一张表的数据拆解写到不同的物理表里,那么查询也得根据分区键进行定位到对应的物理表从而把数据给查询出来。


路由方式一般有三种区间范围、Hash、分片映射表,每种路由方式都有自己的优点和缺点,可以根据对应的业务场景进行选择。


区间范围根据某个元素的区间的进行拆分,以时间为例子,假如有个业务我们希望以月为单位拆分那么表就会拆分像 table_2022-04,这种对于文档型、ElasticSearch这类型的NoSQL也适用,无论是定位查询,还是日后清理维护都是非常的方便的。那么缺点也明显,会因为业务独特性导致数据不平均,甚至不同区间范围之间的数据量差异很大。


Hash也是一种常用的路由方式,根据Hash算法取模以数据量均匀分别存储在物理表里,缺点是对于带分区键的查询依赖特别强,如果不带分区键就无法定位到具体的物理表导致相关所有表都查询一次,而且在分库的情况下对于Join、聚合计算、分页等一些RDBMS的特性功能还无法使用。



一般分区键就一个,假如有时候业务场景得用不是分区键的字段进行查询,那么难道就必须得全部扫描一遍?其实可以使用分片映射表的方式,简单来说就是额外有一张表记录额外字段与分区键的映射关系。举个例子,有张订单表,原本是以UserID作为分区键拆分的,现在希望用OrderID进行查询,那么得有额外得一张物理表记录了OrderID与UserID的映射关系。因此得先查询一次映射表拿到分区键,再根据分区键的值路由到对应的物理表查询出来。可能有些朋友会问,那这映射表是否多一个映射关系就多一张表,还是多个映射关系在同一张表。我优先建议单独处理,如果说映射表字段过多,那跟不进行水平拆分时的状态其实就是一致的,这又跑回去的老问题。


用空间换性能


该类型的两个方案都是用来应对高负载的场景,方案有以下两种:分布式缓存、一主多从。


与其说这个方案叫用空间换性能,我认为用空间换资源更加贴切一些。因此两个方案的本质主要通数据冗余、集群等方式分担负载压力。


对于关系型数据库而言,因为他的ACID特性让它天生不支持写的分布式存储,但是它依然天然的支持分布式读



分布式缓存



缓存层级可以分好几种:客户端缓存API服务本地缓存分布式缓存,咱们这次只聊分布式缓存。一般我们选择分布式缓存系统都会优先选择NoSQL的键值型数据库,例如Memcached、Redis,如今Redis的数据结构多样性,高性能,易扩展性也逐渐占据了分布式缓存的主导地位。


缓存策略也主要有很多种:Cache-AsideRead/Wirte-ThroughWrite-Back,咱们用得比较多的方式主要**Cache-Aside,**具体流程可看下图:



我相信大家对分布式缓存相对都比较熟悉了,但是我在这里还是有几个注意点希望提醒一下大家:



关注公众号:码猿技术专栏,回复关键词:1111 获取阿里内部的java性能调优手册



避免滥用缓存


缓存应该是按需使用,从28法则来看,80%的性能问题由主要的20%的功能引起。滥用缓存的后果会导致维护成本增大,而且有一些数据一致性的问题也不好定位。特别像一些动态条件的查询或者分页,key的组装是多样化的,量大又不好用keys指令去处理,当然我们可以用额外的一个key把记录数据的key以集合方式存储,删除时候做两次查询,先查Key的集合,然后再遍历Key集合把对应的内容删除。这一顿操作下来无疑是非常废功夫的,谁弄谁知道。



避免缓存击穿


当缓存没有数据,就得跑去数据库查询出来,这就是缓存穿透。假如某个时间临界点数据是空的例如周排行榜,穿透过去的无论查找多少次数据库仍然是空,而且该查询消耗CPU相对比较高,并发一进来因为缺少了缓存层的对高并发的应对,这个时候就会因为并发导致数据库资源消耗过高,这就是缓存击穿。数据库资源消耗过高就会导致其他查询超时等问题。


该问题的解决方案也简单,对于查询到数据库的空结果也缓存起来,但是给一个相对快过期的时间。有些同行可能又会问,这样不就会造成了数据不一致了么?一般有数据同步的方案像分布式缓存、后续会说的一主多从、CQRS,只要存在数据同步这几个字,那就意味着会存在数据一致性的问题,因此如果使用上述方案,对应的业务场景应允许容忍一定的数据不一致。


不是所有慢查询都适用


一般来说,慢的查询都意味着比较吃资源的(CPU、磁盘I/O)。举个例子,假如某个查询功能需要3秒时间,串行查询的时候并没什么问题,我们继续假设这功能每秒大概QPS为100,那么在第一次查询结果返回之前,接下来的所有查询都应该穿透到数据库,也就意味着这几秒时间有300个请求到数据库,如果这个时候数据库CPU达到了100%,那么接下来的所有查询都会超时,也就是无法有第一个查询结果缓存起来,从而还是形成了缓存击穿。


一主多从



常用的分担数据库压力还有一种常用做法,就是读写分离、一主多从。咱们都是知道关系型数据库天生是不具备分布式分片存储的,也就是不支持分布式写,但是它天然的支持分布式读。一主多从是部署多台从库只读实例,通过冗余主库的数据来分担读请求的压力,路由算法可有代码实现或者中间件解决,具体可以根据团队的运维能力与代码组件支持视情况选择。


一主多从在还没找到根治方案前是一个非常好的应急解决方案,特别是在现在云服务的年代,扩展从库是一件非常方便的事情,而且一般情况只需要运维或者DBA解决就行,无需开发人员接入。当然这方案也有缺点,因为数据无法分片,所以主从的数据量完全冗余过去,也会导致高的硬件成本。从库也有其上限,从库过多了会主库的多线程同步数据的压力。



选择合适的存储系统


NoSQL主要以下五种类型:键值型、文档型、列型、图型、搜素引擎,不同的存储系统直接决定了查找算法存储数据结构,也应对了需要解决的不同的业务场景。NoSQL的出现也解决了关系型数据库之前面临的难题(性能、高并发、扩展性等)。


例如,ElasticSearch的查找算法是倒排索引,可以用来代替关系型数据库的低性能、高消耗的Like搜索(全表扫描)。而Redis的Hash结构决定了时间复杂度为O(1),还有它的内存存储,结合分片集群存储方式以至于可以支撑数十万QPS。


因此本类型的方案主要有两种:**CQRS、替换(选择)存储,**这两种方案的最终本质基本是一样的主要使用合适存储来弥补关系型数据库的缺点,只不过切换过渡的方式会有点不一样。



CQRS


CQS(命令查询分离)指同一个对象中作为查询或者命令的方法,每个方法或者返回的状态,要么改变状态,但不能两者兼备 



讲解CQRS前得了解CQS,有些小伙伴看了估计还没不是很清晰,我这里用通俗的话解释:某个对象的数据访问的方法里,要么只是查询,要么只是写入(更新)。而CQRS(命令查询职责分离)基于CQS的基础上,用物理数据库来写入(更新),而用另外的存储系统来查询数据。因此我们在某些业务场景进行存储架构设计时,可以通过关系型数据库的ACID特性进行数据的更新与写入,用NoSQL的高性能与扩展性进行数据的查询处理,这样的好处就是关系型数据库和NoSQL的优点都可以兼得,同时对于某些业务不适于一刀切的替换存储的也可以有一个平滑的过渡。


从代码实现角度来看,不同的存储系统只是调用对应的接口API,因此CQRS的难点主要在于如何进行数据同步。


数据同步方式



一般讨论到数据同步的方式主要是分拉:


推指的是由数据变更端通过直接或者间接的方式把数据变更的记录发送到接收端,从而进行数据的一致性处理,这种主动的方式优点是实时性高。


拉指的是接收端定时的轮询数据库检查是否有数据需要进行同步,这种被动的方式从实现角度来看比推简单,因为推是需要数据变更端支持变更日志的推送的。


而推的方式又分两种:CDC(变更数据捕获)和领域事件。对于一些旧的项目来说,某些业务的数据入口非常多,无法完整清晰的梳理清楚,这个时候CDC就是一种非常好的方式,只要从最底层数据库层面把变更记录取到就可。


对于已经服务化的项目来说领域事件是一种比较舒服的方式,因为CDC是需要数据库额外开启功能或者部署额外的中间件,而领域事件则不需要,从代码可读性来看会更高,也比较开发人员的维护思维模式。



替换(选择)存储系统


因为从本质来看该模式与CQRS的核心本质是一样的,主要是要对NoSQL的优缺点有一个全面认识,这样才能在对应业务场景选择与判断出一个合适的存储系统。这里我像大家介绍一本书马丁.福勒《NoSQL精粹》,这本书我重复看了好几遍,也很好全面介绍各种NoSQL优缺点和使用场景。


当然替换存储的时候,我这里也有个建议:加入一个中间版本,该版本做好数据同步与业务开关,数据同步要保证全量与增加的处理,随时可以重来,业务开关主要是为了后续版本的更新做的一个临时型的功能,主要避免后续版本更新不顺利或者因为版本更新时导致的数据不一致的情况出现。在跑了一段时间后,验证了两个不同的存储系统数据是一致的后,接下来就可以把数据访问层的底层调用替换了。如此一来就可以平滑的更新切换。


结束


本文到这里就把八大方案介绍完了,在这里再次提醒一句,每个方案都有属于它的应对场景,咱们只能根据业务场景选择对应的解决方案,没有通吃,没有银弹。


这八个方案里,大部分都存在数据同步的情况,只要存在数据同步,无论是一主多从、分布式缓存、CQRS都好,都会有数据一致性的问题导致,因此这些方案更多适合一些只读的业务场景。当然有些写后既查的场景,可以通过过渡页或者广告页通过用户点击关闭切换页面的方式来缓解数据不一致性的情况。


作者:码猿技术专栏
来源:juejin.cn/post/7185338369860173880
收起阅读 »

早起、冥想、阅读、写作、运动

周岭在《认知觉醒》一书中提出了快速改变人生的五件事,即:「早起」、「冥想」、「阅读」、「写作」、「运动」。低调务实优秀中国好青年交流群也正是从这 5 件事入手,帮你养成好习惯。我也试着实践了有将近一年的时间,今谈谈收获与心得。 早起 一日之计在于晨,一年之计在...
继续阅读 »

周岭在《认知觉醒》一书中提出了快速改变人生的五件事,即:「早起」、「冥想」、「阅读」、「写作」、「运动」。低调务实优秀中国好青年交流群也正是从这 5 件事入手,帮你养成好习惯。我也试着实践了有将近一年的时间,今谈谈收获与心得。


早起


一日之计在于晨,一年之计在于春;早起是个老生常谈的话题了,鲁迅先生小时候为了上课不迟到,还把「早」刻在桌上告诫自己。我小时候每天晚上吃完饭,没什么事早早地就睡了,甚至觉得十点睡觉都是一件很可怕的事。如今呢,自从步入互联网时代,十点?不好意思,十点夜生活才刚刚开始。


秉承着先僵化、后优化、再固化的原则,我决定尝试一段时间。起初几天是真的很难受,白天浑浑噩噩的完全提不起精神。不过慢慢的,晚上倒是越来越早的睡了。差不多半个月时间几乎都习惯了 10 点左右睡觉,6 点前起床。正常早上六点起床后,稍微锻炼一会回来坐那下下汗,冲个凉水澡,然后吃个早饭就去工作了。


持续了有半年时间,直观感受就是身体越来越好,精神头越来越棒;但我并不认为这是早起带来的,潜移默化改变了我的是生活规律。毕竟美国人时差和咱们完全反着来,也没见几个英年嗝屁的。现在为止,我想早起也许就真的只是早点起来罢了。


但有一天,我翻看着旧日的朋友圈:星光不问赶路人,豁然开朗。也深刻地认识到了自己的肤浅,早起其实并不只意味着早点起来罢了。想象一下,如果明天要和女神约会?或者新工作的第一天?不用考虑肯定早早的就起来收拾了,因为你开心,快乐,幸福;甚至要迎来人生新阶段了。所以早起真谛可能不仅仅是早点起来,更重要的是进一步寻找人生的意义,创造生命的价值,为我所热爱奋斗终生!


冥想


关于冥想,老实说太高端了,高端到有点不接地气,反正 100 个人有 100 个见解。刚开始还看了各种视频、翻了有关的书、试了各种动作体验冥想、有没有效果我不清楚,不过睡得倒很快。


感受呼吸、扫描身体、提升专注力,但越努力就越凌乱……由于不能形成持续的正反馈,所以我有点消极。去你的冥想,浪费生命。后续冥想也是断断续续的持续了好久,那天想起来就尝试一下,想不起来就算了。


直到有阵子,忘记具体在做什么,总之就是在写代码。从上班来坐那,要不是同事喊我,还真没感觉一个上午都过去了……也是瞬间明白了《十分钟冥想》中:心流。


我把冥想定义为心无杂念、极致专注。但是早期的努力只是停留在表面上而没有透彻地理解。我认为冥想最重要的一点:感知力、尝试学会深入感受身体各个部位,体会情绪在大脑波动,品尝茶水在身体流淌,体会世间万物。


一个小和尚问得道的师父:“您得道之前做什么?”

老和尚说:“砍柴、挑水、做饭。”

“那得道之后呢?”小和尚继续问道。

老和尚回答:“还是砍柴、挑水、做饭。”

小和尚一脸疑惑:“师父,这不是一样吗?”

老和尚说:“当然不一样。得道前,我砍柴时惦记着挑水,挑水时惦记着做饭;得道后,砍柴即砍柴,挑水即挑水,做饭即做饭。”

阅读


生命是有限的,但书籍却是无限的,怎么用有限的生命阅读无限的书籍呢?根据不科学统计,人的一生最多只能阅读 15000 本书籍,那估计是没有一个人可以活着读完。所以我们应该要追求精读细阅和高质量的阅读。


首先要会读书,读好书。《如何阅读一本书》就非常详细的讨论了怎么样阅读一本书,尽管有关读书的方法论确实很好,但我觉得阐述得太过重复啰嗦。其实读书喜欢什么就读什么,不要拘泥于阅读世界名著,人文哲理。但我建议少读都市言情,穿越爽文,其可吸收的营养价值较少。具体想怎么读就怎么读,咬文嚼字、一目十行都无所谓,但是这一种读法仅限于是一本好书的情况下。可是究竟什么是好书呢?追随那些走的快的人,阅读其推荐的书单。


假如面临的是一本新书,那么你可以尝试:



  1. 深入了解书的作者、写作的背景。

  2. 详细阅读书的自序、引言、大纲、目录等重要信息。

  3. 快速翻阅书中的部分章节。如果感觉这本书很有价值,那就接着继续。

  4. 带着疑问追随作者的步伐,选择最适合的方式阅读。

    1. 这本书讲了什么?

    2. 作者细说了什么?

    3. 作者的观点是否正确?

    4. 作者讲的和我有什么关系?



  5. 收获体会,记录笔记。


再分享一种进一步的阅读方法:主题阅读。在某个类目中挑选同方向的若干本书,然后确认自己研究的主题和方向。



  1. 依次阅读每本书。

  2. 理清问题、界定主题。

  3. 与不同作者达成共识。

  4. 分析讨论。


写作



我学生时期其实最厌恶写作了……为什么会是你给我段话,让我来研究一下它怎么想的,然后再为你阐述一下自己的观点。我 TM 怎么知道他想什么,爱想什么想什么。



写作实际上可以和阅读相结合,从而构成完美的闭环。


不知道是不是只有自己写作效率低,感觉自己就像间歇泉,总是时不时的迸发灵感。但有时候喷多了,我还写不下来。所以我一般阅读书籍的时候总是会主动掺杂一些技术类目书籍,这样既有助于提高专业技能,又能留足思考时间。


写作我倒没啥可分享心得的,随心所欲,不必整的很累。但必须重视以下三点:



  1. 务必不要出现错字。

  2. 一定要正确地运用标点符号和合理地分段。

  3. 确保文章整体阅读流畅性。


运动


生命在于运动,如只老乌龟一样冲击活 100 年!


运动锻炼不局限于任何形式,爬楼梯也可以,最重要的是生活态度。千万不要眼高手低,今天运动明天就想超越博尔特,持续保持正反馈,日拱一卒,冲吧骚年!


如果不知道如何下手,可以参考我的 wiki 手册:健身手册




其实吧,哪怕你尝试了「早起」、「冥想」、「阅读」、「写作」、「运动」,也不可能立刻获得收获。过去既然无法改变,未来更不知道何去。


那么请尝试着慢一点,慢一点,再慢一点,也许当你回头那刻,轻舟已过万重山。



来源:早起、冥想、阅读、写作、运动 - 7Wate‘s blog



作者:7Wate
来源:juejin.cn/post/7210298403070722105
收起阅读 »

低p程序员互联网求生指南

大家好,我小j。 先做个自我介绍,我在国内大厂担任数年的开发工作,但是回顾我的职业生涯,我认为还是充满遗憾和失望的,中间做过几次错误的选择。在此,刚好借助这个文章,我想回顾下之这数年的职业生涯,点出失败的教训,描述下在我认为的互联网公司能安全度日,谋求晋升的要...
继续阅读 »

大家好,我小j。


先做个自我介绍,我在国内大厂担任数年的开发工作,但是回顾我的职业生涯,我认为还是充满遗憾和失望的,中间做过几次错误的选择。在此,刚好借助这个文章,我想回顾下之这数年的职业生涯,点出失败的教训,描述下在我认为的互联网公司能安全度日,谋求晋升的要点以及说点大实话,希望能给各位读者学习的地方。想法很跳跃,大家根据章节观看。如果你有不认可的地方,都是你对,是我太失败。


观点


要对大厂祛魅


我一定要第一个提这个点。

起因是最近在很多技术群看到大家都对大厂开发的身份非常崇拜,觉得大厂的人一定比小厂中厂优秀,大厂的人说话一定是对的云云,大厂的技术一定更好,而且在国内论坛上和学生辩论过大厂的观念,让我这个前员工深受震撼。所以一定要找机会聊一下这个话题,可能会伤害朋友们的感情,但是还是想聊一下。



  1. 大厂人是不是一定非常优秀

    不是,国内大厂在黄金时期大部分是冗余性招人,以应对每年的绩效考核和未来可能的业务拓展,一个管理一定要懂怎么要新财年的hc,这样才能保住完成手下大部分的开发任务。

    大厂面试的默认逻辑会导致大量学校好、学历好、掌握面试技巧的人进入大厂,这也是为什么很多知识星球,面试指南,面经,小册盛行的原因。

  2. 剖析大厂开发

    在我们了解了这个前提下,解析下大厂开发,其实也符合二八原则,大厂20%左右的员工是真的有经验有天赋的超能力者,他们去实现架构,完成整个开发流程套件、开发系统的开发。而大量的员工实际上是在其他大佬规划好的线路上填api糊业务罢了,完成基础职务,之后再开发各种okr项目来满足绩效的要求。从技术的角度来看,大部分大厂开发实际平均水平也没有那么高,也是业务仔罢了。

  3. 大厂真正优秀的是什么


    • 大厂真正优秀的是有一些内部架构大佬完成一套完善的开发套件以及设置开发流程,让每个参与大厂的开发都有相对不难受的开发体验以及完整的上线监控流程。让不同水平的开发都足以完成他们被要求完成的任务。




    • 大厂优秀的是有远超中小厂的业务体量、薪资福利。




    • 大厂优秀的是身边同事基本都很优秀,都有自己能学习的点,也是未来的社交圈子。




    • 大厂优秀的是能让你熟悉一整套成熟的开发流程,需求评审-开发评审-业务开发-发布提测-正式上线-日志监控-热修回滚。让你了解一整个应用的开发方式。




    • 大厂优秀的是能给你简历加分,带来未来可能的发展机会。




    • 大厂优秀的还有像ata、学城这种前人留下的各种资料可供学习,虽然很多水文但是也远比简中外面的文章靠谱。




    • 大厂优秀的是有更多owner项目的机会,让你能有机会发挥自己的能力大展拳脚。






身为大厂人,应该清楚现在的成就是自己的能力还是平台给的机会,戒骄戒躁。

身为非大厂人,也不要太神话大厂,其实屎山一样很多,大家还是理性讨论。


说句政治不正确的,很多大厂的成功除了依托本身人才和领导层的慧眼以外,更多还是依托时代背景,时势造英雄。 为什么目前环境小厂发育艰难,因为一旦你业务达到一定水平足以引起大厂注意以后,大部分大厂都会提出收购,如果你统一收购就会并入大厂之中。如果你不同意收购,他们会利用自己的雄厚财力定点爆破你的员工,抄袭你的创意,诋毁你的业务,抢走你的客户。当前创业不仅要选对市场,还要顶得住大厂的摧残。


高考很重要,学历很重要,专业很重要


虽然可能看到文章的人大多数已经就业或者在大学学习,但是我还是想提这个点。

诚然,互联网开发已经算是不看学历,不看出身的几个职业之一,但是在相同水平的一群求职者中,面试官还是更愿意招自己的校友、学历好看的人、专业对口的人。这个也算是一个潜规则,从好学校毕业中得到一个好员工的概率确实比从一般的学校中挑到前几名的概率大。虽然我们说宁做宁做鸡头不做凤尾,但是现实生活往往是凤尾的平均境遇比鸡头被伯乐适中的概率高,不要去赌自己能被人发掘,要尽量凑到高水平人群中,往往被人选择的机会更大。


选择高校的排名大概就是综合排名>行业内专业知名度>高校所在城市(影响你的实习选择)。


要承认和接受互联网里的圈子


首先叠个甲,这块并不是说圈子一定是好事,但是目前的环境圈子确实能在职业发展中帮助你迅速提高,这个圈子包括老乡圈、学校圈、公司圈(比如bidu帮、ali帮)、技术圈、老同事圈(etc.),大家在一个陌生环境中还是会倾向去找自己有关系的人,结成圈子,铁打一块,在一个圈子里,对你面试过关,绩效提高,晋升都有帮助。


互联网也需要情商,也有嫡系


很多人包括我之前对程序员的理解也是不用跟人打交道,只需要在电脑上完成开发任务即可,但是实际的工作生涯中,因为你的绩效是人评,你的晋升是人评。不可避免还是要多跟人打交道。跟+1(组长)的关系,跟+2(部门老板)的关系或多或少还是对你的结果有一些影响,我并不是说让大家去舔,但是起码要有一些眼力见,做该做的事情。


聊完了前面几个很油的话题之后,我们回归到实际开发生活中


尽量选择大厂,注意识别子公司和外包


虽然我们之前想让大家对大厂祛魅,但是目前来看进入大厂还是能带来更多的收入和晋升机会以及未来的跳槽机会,而且你未来的同事圈层也会更为优秀,要知道这些人就是你未来的内推池,在互联网,互帮互助永远比单打独斗更好。在同等情况下,我们肯定推荐大厂offer>小厂offer,核心offer>边缘bu offer。大厂的卷虽然不一定能一定让你收益,但是很多小厂卷都卷不出收益,从roi来看,大厂核心部门是我们的就职首选。

但是也要分清大厂、大厂子公司和外包。有些公司虽然名义上是大厂子公司,但是无法享受大厂的权益,属于是披着羊皮卖狗肉,环境不好的时候选择先去子公司呆着无可厚非,但是如果你一心想参加大厂,却选错了bu,可能会浪费一段时间。

尽量不要选择外包,国内目前对外包的开发经历还是或多或少有一些歧视的,这个歧视不会表现在jd里,而是hr简历关、面试中可能因为你的背景一键否定。


领导的意义远大于你的想象


一定要珍惜一个好的领导
在相同水平的公司选择下,重要性上我认为有资源的部门>领导nice程度>>>>>>其他因素。

有潜力/大老板亲自抓的业务能带来更多的晋升机会,而且窗口期进入也很容易,一旦做大了容易成为骨干,后续owner项目机会大(前提不被后续老板亲信空降摘桃子)。
不好但是有资源的部门也能分一杯羹。
但是领导作为你天天见面的人,对你的影响比任何都大,一个理想中的领导不一定技术非常牛逼,但是一定是懂得对外抢肉抢功劳,对内帮助内部成员成长,懂得规划成员的晋升路径,及时跟进组员问题,适当提携帮助的人。由此可以看出来,跟着一个好的领导,不仅有利于工作情绪,也会让你一路顺利的走上晋升之路。

相反,遇见一个不合适的领导,不仅经常pua,不下班,还经常让你背c,没有晋升机会,不如趁早活水骑驴找马。离职原因无非钱给少了,心受委屈了,坏领导能让你同时体会两种感受。


学会和领导汇报工作


新人经常做错的一个事情就是闷头干活,不会汇报,不会报功。要知道领导不可能了解每个人的进度和开发内容,每周的周报是唯一的汇报途径。如果你所做的内容不被领导知道,那么又怎么表现你的价值呢?所以,要学会跟领导汇报进度,可以是每次做完一个阶段后发一个简略的阶段报告,亦或是遇到问题时及时和老板沟通目前的困难以及可能能解决的方法。让老板充分了解你的工作,才能帮你去谋求进一步向+2的汇报,不要做一个只会闷头干活的老黄牛。


学会跟领导提出想法


承接上个话题,举一个例子。如果我们想晋升涨薪,完全可以oneone的时候跟老板提出想法:老板你看我如果想晋升/涨薪,我应该去做哪些内容/完成哪些目标呢。从领导的回答也可以看得出他对你的态度



  1. 如果他认真回答,给你列好路径,那么说明晋升/涨薪还是很有希望的,这也是身为领导应该去做的事——规划自己小弟晋升,那么就按着他的路子付出努力实现

  2. 如果他给你画饼,打哈哈。说明你不是嫡系,可能需要在多做一些事情引起他的注意。

  3. 如果他完全无视这个话题,说明他完全没考虑你的晋升情况,那么这个时候就该考虑后路了


要学会onoene找老板沟通,不仅是让老板知道你最近的情况,也是了解老板对你的态度的时候,要学会双向沟通。


不要过多的嫌弃分配的业务


大部分大厂的业务并没有太高的技术含量,尤其像业务部门的活动业务和基建部门的客服业务,我们要清楚的认识到工作就是给自己赚窝囊费的,只要钱给足,业务什么样都是可以接受的。但是在完成日常业务的时候,我们可以考虑如何优化自己手里的活,怎么让自己手里的活效果更好,这方面的助力是有助于老板看到你的亮点,理解到你的能力的。而不是经常抱怨任务烂,不想做。


学会与人为善,维护自己的朋友圈


要知道,你身边的朋友大多技术不弱于你,未来这些人都是你可能的内推对象和被内推对象,要学会与人为善,尽量不要和同事闹冲突,最好之后也经常保持联系,万一之后有内推的机会这些都是潜在的大腿,要知道无论是国内还是国外,招人的第一选择永远是内推,维护好自己的朋友圈,早晚会得到助力。


多贴近业务,了解业务流程


不要只会做一个执行者,在日常的业务开发中要尽量的去学习业务的流程,了解整个bu的运转方法,盈利方法,这样在需求会上你也能提出自己的意见。多和产品和运营聊天,了解业务数据。这样你也能对bu下一步是进一步发展还是收缩有一定预期,提前规划下一步自己的努力方向。


要学会投巧的发展


首先感谢群友大编制的提出。人不可能是全能的,一定要有一定的不可替代性和独特性,如何在一个团队中脱颖而出,除了本身真的足够优秀以外,还可以投巧的发展,举个例子,在一个前端业务团队,普遍大家都会脚手架配置和组件化,拥有这些技能不稀奇,但是如果A会可视化开发,B会nodejs,那么这两个同学在这个团队中就容易显得更亮眼,如果大家的业务都是糊业务,这两个同学在所学技能上稍微多点优化就容易获得更好的绩效。


要有技术追求,但不要太沉迷在公司成长


虽然现在大家也基本上认识到成长不能靠公司了,大部分公司的日常业务开发技术含量并没有那么高,不要妄想在日常日复一日的业务中提高自己的技术水平,那只能提高你糊业务的熟练度和经验。如果想追求技术的提高,还是要靠工作之余的okr项目或者是自己的私下的学习。但是,想在目前国内这个环境中稳定成长厚积薄发,还是不能放弃技术追求,技术经验在身,就算面对裁员风险也不慌。我们要卷,要以提高技术、增加晋升机会,有目的的卷,无效的卷不仅带不来收益,还能带来同事的鄙夷和icu的风险。


承认天赋的差距,在团队中不掉队


程序开发确实是一个天赋的职业,要承认天赋和能力的差距,达到日常业务线上0bug、0delay的60分目标不难,但是想更进一步确实需要正确的方向和努力,我们做不到比所有人都优秀,只需要做到在团队中不掉队,不是最差的几个,就能尽可能的保证在裁员大潮幸存。


在开发之余,考虑自己的长处


目前国内环境比较恶劣,35的达摩克利斯之剑悬在每个人头上,一方面大厂hc在缩小,创新项目在关停,就算你再自信,一但没有hc,也没有换岗位的机会。另一方面随着年龄的增长,初级中级开发的大门也随之关闭,一但你在某个年龄段没有达到对应的职级,就容易被视作失败。而跳槽更要看目标公司有没有对应职级的坑位,职级越高坑位越少。目前高龄开发的环境还是比较恶劣的。

在这之上,我们要考虑是否找到另一个赛道,发挥自己的长处。能有效延缓焦虑,降低未来的风险


总结


说了这么多,也是我目前的一些浅薄经验纸上谈兵,至少从我的职业经验来看,并没有做到以上的内容,还是一个owner项目的大头兵,写了这些内容,也是希望新人不撞我的南墙,老人提前规划后路。欢迎大家多多交流,让国内有一个更好的程序员成长环境。

永远不要忘了,家人爱人和朋友是你永远的后盾,在你坚持不住,想投降的时候,记得你永远有一个避风港存在,一个成功的人永远离不开背后默默支持的人,工作只是我们生活中的一小方面,善待朋友,珍爱亲人,才是我们一直要做的事情。不要因为工作伤害爱你的人。


作者:valkyrja
来源:juejin.cn/post/7299853894733168681
收起阅读 »

开发企业微信群机器人,实现定时提醒

大家好,我是鱼皮,今天分享一个用程序解决生活工作问题的真实案例。 说来惭愧,事情是这样的,在我们公司,每天都要轮流安排一名员工(当然也包括我)去楼层中间一个很牛的饮水机那里接水。但由于大家每天都有自己的工作,经常出现忘记接水的情况,导致大家口渴难耐。 怎么解决...
继续阅读 »

大家好,我是鱼皮,今天分享一个用程序解决生活工作问题的真实案例。


说来惭愧,事情是这样的,在我们公司,每天都要轮流安排一名员工(当然也包括我)去楼层中间一个很牛的饮水机那里接水。但由于大家每天都有自己的工作,经常出现忘记接水的情况,导致大家口渴难耐。


怎么解决这个问题呢?


我想到了几种方法:


1)每天大家轮流提醒。但是别说提醒别人了,自己都不记得什么时候轮到自己接水。


2)由一个员工负责提醒大家接水,必要时招募一个 “接水提醒员”。


3)在企业微信的日历功能给员工安排接水日程,就像下面这样:



但问题是我们的人数和天数不是完全对应的、反复安排日程也很麻烦。


你觉得上面哪种方案好呢?其实我觉得第二个方案是最好的 —— 招募一个 “接水提醒员”。


别笑,我认真的!


只不过这个 “接水提醒员” 何必是人?


没错,作为一名程序员,我们可以搞一个机器人,让它在企业微信群聊中每天提醒不同的员工去接水即可。


其实这个功能和员工排班打卡系统是很类似的,只不过更轻量一些。我也调研了很多排班系统,但是都要收费,索性自己开发一个好了。


在企业微信中接入机器人其实非常简单,因为企业微信官方就支持群聊机器人功能,所以这次的任务我就安排给了实习生,他很快就完成了,所以我相信大家应该也都能学会~


企微群聊机器人开发


学习开发第三方应用时,一定要先完整阅读官方文档,比如企业微信群机器人配置文档。



指路:developer.work.weixin.qq.com/document/pa…




设计 SDK 结构


虽然我们的目标是做一个提醒接水机器人,但是企业微信群聊机器人其实是一个通用的功能,所以我们决定开发一个企微机器人 SDK,以后公司其他业务需要时都能够快速复用。(比如开发一个定时喝水提醒机器人)


设计好 SDK 是需要一定技巧的,之前给大家分享过:如何设计一个优秀的 SDK ,可以阅读参考。


在查阅企微机器人文档后,了解到企业微信机器人支持发送多种类型的消息,包括文本、 Markdown 、图片、图文、文件、语音和模块卡片等,文档中对每一种类型的请求参数和字段含义都做了详尽的解释。



吐槽一下,跟微信开发者文档比起来,企微机器人的文档写得清晰多了!



企微文本消息格式


企微文本消息格式


由于每种消息最终都是要转换成 JSON 格式作为 HTTP 请求的参数的,所以我们可以设计一个基础的消息类(Message)来存放公共参数,然后定义各种不同的具体消息类来集成它(比如文本消息 TextMessage、Markdown 消息 MarkdownMessage 等)。


为了简化开发者使用 SDK 来发送消息,定义统一的 MessageSender 类,在类中提供发送消息的方法(比如发送文本消息 sendText),可以接受 Message 并发送到企业微信服务器。


最终,客户端只需调用统一的消息发送方法即可。SDK 的整体结构如下图所示:



值得一提的是,如果要制作更通用的消息发送 SDK。可以将 MessageSender 定义成接口,编写不同的子类比如飞书 MessageSender、短信 MessageSender 等。


开发 SDK


做好设计之后,接下来就可以开始开发 SDK 了。


步骤如下:



  1. 获取 webhook

  2. 创建 SDK 项目

  3. 编写代码

  4. SDK 打包

  5. 调用 SDK


1、获取 webhook


首先,必须在企业微信群聊中创建一个企业微信机器人,并获取机器人的 webhook。


webhook 是一个 url 地址,用于接受我们开发者自己服务器的请求,从而控制企业微信机器人。后续所有的开发过程,都需要通过 webhook 才可以实现。



复制并保存好这个 Webhook 地址,注意不要泄露该地址!



2、创建 SDK 项目


SDK 通常是一个很干净的项目,此处我们使用 Maven 来构建一个空的项目,并在 pom.xml 文件中配置项目信息。


需要特别注意的是,既然我们正在创建一个 SDK,这意味着它将被更多的开发者使用。因此,在配置 groupId 和 artifactId 时,我们应当遵循以下规范:



  • groupId:它是项目组织或项目开发者的唯一标识符,其实际对应的是 main 目录下的 Java 目录结构。

  • artifactId:它是项目的唯一标识符,对应的是项目名称,即项目的根目录名称。通常,它应当为纯小写,并且多个词之间使用中划线(-)隔开。

  • version:它指定了项目的当前版本。其中,SNAPSHOT 表示该项目仍在开发中,是一个不稳定的版本。


以下是我们配置好的项目信息:


<groupId>com.yupi</groupId>
<artifactId>rtx-robot</artifactId>
<version>1.0-SNAPSHOT</version>

为了让我们的项目更加易用,我们还要能做到让开发者通过配置文件来传入配置(比如 webhook),而不是通过硬编码重复配置各种信息。


所以此处我们把项目只作为 Spring Boot 的 starter,需要在 pom.xml 文件中引入依赖:


<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>

最后,我们还需要添加一个配置,配置项 <skip>true</skip> 表示跳过执行该插件的默认行为:


<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <skip>true</skip>
            </configuration>
        </plugin>
    </plugins>
</build>

这样,一个 SDK 项目的初始依赖就配置好了。


3、编写配置类


现在我们就可以按照之前设计的结构开发了。


首先,我们要写一个配置类,用来接受开发者在配置文件中写入的 webhook。


同时,我们可以在配置类中,将需要被调用的 MessageSender 对象 Bean 自动注入到 IOC 容器中,不用让开发者自己 new 对象了。


示例代码如下:


@Configuration
@ConfigurationProperties(prefix = "wechatwork-bot")
@ComponentScan
@Data
public class WebhookConfig {

    private String webhook;

    @Bean
    public RtxRobotMessageSender rtxRobotMessageSender() {
        return new RtxRobotMessageSender(webhook);
    }
}

接下来,为了让 Spring Boot 项目在启动时能自动识别并应用配置类,需要把配置类写入到 resources/META-INF/spring.factories 文件中,示例代码如下:


org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.yupi.rtxrobot.config.WebhookConfig

4、编写消息类


接下来,我们要按照官方文档的请求参数把几种类型的消息对象编写好。


由于每个消息类都有一个固定的字段 msgtype,所以我们定义一个基类 Message,方便后续将不同类型的消息传入统一的方法:


public class Message {

    /**
     * 消息类型
     **/

    String msgtype;
}

接下来编写具体的消息类,比如纯文本类型消息 TextMessage,示例代码如下:


@Data
public class TextMessage extends Message {

    /**
     * 消息内容
     */

    private String content;

    /**
     * 被提及者userId列表
     */

    private List<String> mentionedList;

    /**
     * 被提及者电话号码列表
     */

    private List<String> mentionedMobileList;
  
    /**
     * 提及全体
     */

    private Boolean mentionAll = false;

    public TextMessage(String content, List<String> mentionedList, List<String> mentionedMobileList, Boolean mentionAll) {
        this.content = content;
        this.mentionedList = mentionedList;
        this.mentionedMobileList = mentionedMobileList;
        this.mentionAll = mentionAll;

        if (mentionAll) {
            if (CollUtil.isNotEmpty(this.mentionedList) || CollUtil.isNotEmpty(this.mentionedMobileList)) {
                if (CollUtil.isNotEmpty(mentionedList)) {
                    this.mentionedList.add("@all");
                } else {
                    this.mentionedList = CollUtil.newArrayList("@all");
                }
            } else {
                this.mentionedList = CollUtil.newArrayList("@all");
            }
        }
    }

    public TextMessage(String content) {
        this(content, nullnullfalse);
    }
}

上面的代码中,有个代码优化小细节,官方文档是使用 “@all” 字符串来表示 @全体成员的,但 “@all” 是一个魔法值,为了简化调用,我们将其封装为 mentionAll 布尔类型字段,并且在构造函数中自动转换为实际请求需要的字段。


5、编写消息发送类


接下来,我们将编写一个消息发送类。在这个类中,定义了用于发送各种类型消息的方法,并且所有的方法都会依赖调用底层的 send 方法。send 方法的作用是通过向企微机器人的 webhook 地址发送请求,从而驱动企微机器人发送消息。


以下是示例代码,有很多编码细节:


/**
 * 微信机器人消息发送器
 * @author yuyuanweb
 */

@Slf4j
@Data
public class RtxRobotMessageSender {

    private final String webhook;
  
    public WebhookConfig webhookConfig;

    public RtxRobotMessageSender(String webhook) {
        this.webhook = webhook;
    }

    /**
     * 支持自定义消息发送
     */

    public void sendMessage(Message message) throws Exception {
        if (message instanceof TextMessage) {
            TextMessage textMessage = (TextMessage) message;
            send(textMessage);
        } else if (message instanceof MarkdownMessage) {
            MarkdownMessage markdownMessage = (MarkdownMessage) message;
            send(markdownMessage);
        } else {
            throw new RuntimeException("Unsupported message type");
        }
    }

    /**
     * 发送文本(简化调用)
     */
 
    public void sendText(String content) throws Exception {
        sendText(content, nullnullfalse);
    }
  
    public void sendText(String content, List<String> mentionedList, List<String> mentionedMobileList) throws Exception {
        TextMessage textMessage = new TextMessage(content, mentionedList, mentionedMobileList, false);
        send(textMessage);
    }
    
    /**
     * 发送消息的公共依赖底层代码
     */

    private void send(Message message) throws Exception {
        String webhook = this.webhook;
        String messageJsonObject = JSONUtil.toJsonStr(message);
       // 未传入配置,降级为从配置文件中寻找
        if (StrUtil.isBlank(this.webhook)) {
            try {
                webhook = webhookConfig.getWebhook();
            } catch (Exception e) {
                log.error("没有找到配置项中的webhook,请检查:1.是否在application.yml中填写webhook 2.是否在spring环境下运行");
                throw new RuntimeException(e);
            }
        }
        OkHttpClient client = new OkHttpClient();
        RequestBody body = RequestBody.create(
                MediaType.get("application/json; charset=utf-8"),
                messageJsonObject);
        Request request = new Request.Builder()
                .url(webhook)
                .post(body)
                .build();
        try (Response response = client.newCall(request).execute()) {
            if (response.isSuccessful()) {
                log.info("消息发送成功");
            } else {
                log.error("消息发送失败,响应码:{}", response.code());
                throw new Exception("消息发送失败,响应码:" + response.code());
            }
        } catch (IOException e) {
            log.error("发送消息时发生错误:" + e);
            throw new Exception("发送消息时发生错误", e);
        }
    }
}

代码部分就到这里,是不是也没有很复杂?


6、SDK 打包


接下来就可以对 SDK 进行打包,然后本地使用或者上传到远程仓库了。


SDK 的打包非常简单,通过 Maven 的 install 命令即可,SDK 的 jar 包就会被导入到你的本地仓库中。



在打包前建议先执行 clean 来清理垃圾文件。




7、调用 SDK


最后我们来调用自己写的 SDK,首先将你的 SDK 作为依赖引入到项目中,比如我们的接水提醒应用。


引入代码如下:


<dependency>
  <groupId>com.yupi</groupId>
  <artifactId>rtx-robot</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

然后将之前复制的 webhook 写入到 Spring Boot 的配置文件中:


wechatwork-bot:
  webhook: 你的webhook地址

随后你就可以用依赖注入的方式得到一个消息发送者对象了:


@Resource
public RtxRobotMessageSender rtxRobotMessageSender;

当然你也可以选择在一个非 Spring 环境中手动创建对象,自己传入 webhook:


String webhook = "你的webhook地址";
RtxRobotMessageSender rtxRobotMessageSender = new RtxRobotMessageSender(webhook);

现在,就可以轻松实现我们之前提到的提醒接水工具了。


这里我们就用最简单的方式,定义一个员工数组,分别对应到每周 X,然后用定时任务每日执行消息发送。


示例代码如下:


@Component
public class WaterReminderTask {

    @Resource
    public RtxRobotMessageSender rtxRobotMessageSender;

    private String[] names = {"员工a""员工b""员工c""员工d""员工e"};

    @Scheduled(cron = "0 55 9 * * MON-FRI")
    public void remindToGetWater() {
        LocalDate today = LocalDate.now();
        DayOfWeek dayOfWeek = today.getDayOfWeek();
        String nameToRemind;
        switch (dayOfWeek) {
            case MONDAY:
                nameToRemind = names[0];
                break;
            case TUESDAY:
                nameToRemind = names[1];
                break;
            case WEDNESDAY:
                nameToRemind = names[2];
                break;
            case THURSDAY:
                nameToRemind = names[3];
                break;
            case FRIDAY:
                nameToRemind = names[4];
                break;
            default:
                return;
        }
      
        String message = "提醒:" + nameToRemind + ",是你接水的时间了!";
        rtxRobotMessageSender.sendText(message);
    }
}

好了,现在大家每天都有水喝了,真不错 👍🏻



最后


虽然开发企微机器人 SDK 并不难,但想做一个完善的、易用的 SDK 还是需要两把刷子的,而且沉淀 SDK 对自己未来做项目帮助会非常大。


希望本文对大家有帮助,学会的话 点个赞在看 吧,谢谢大家~


作者:程序员鱼皮
来源:juejin.cn/post/7300611640017813513
收起阅读 »

听我一句劝,业务代码中,别用多线程。

你好呀,我是歪歪。 前几天我在网上冲浪,看到一个哥们在吐槽,说他工作三年多了,没使用过多线程。 虽然八股文背的滚瓜烂熟,但是没有在实际开发过程中写的都是业务代码,没有使用过线程池,心里还是慌得一比。 我只是微微一笑,这不是很正常吗? 业务代码中一般也使不上多线...
继续阅读 »

你好呀,我是歪歪。


前几天我在网上冲浪,看到一个哥们在吐槽,说他工作三年多了,没使用过多线程。


虽然八股文背的滚瓜烂熟,但是没有在实际开发过程中写的都是业务代码,没有使用过线程池,心里还是慌得一比。


我只是微微一笑,这不是很正常吗?


业务代码中一般也使不上多线程,或者说,业务代码中不知不觉你以及在使用线程池了,你再 duang 的一下搞一个出来,反而容易出事。


所以提到线程池的时候,我个人的观点是必须把它吃得透透的,但是在业务代码中少用或者不用多线程。


关于这个观点,我给你盘一下。


Demo


首先我们还是花五分钟搭个 Demo 出来。


我手边刚好有一个之前搭的一个关于 Dubbo 的 Demo,消费者、生产者都有,我就直接拿来用了:



这个 Demo 我也是跟着网上的 quick start 搞的:



cn.dubbo.apache.org/zh-cn/overv…




可以说写的非常详细了,你就跟着官网的步骤一步步的搞就行了。


我这个 Demo 稍微不一样的是我在消费者模块里面搞了一个 Http 接口:



在接口里面发起了 RPC 调用,模拟从前端页面发起请求的场景,更加符合我们的开发习惯。


而官方的示例中,是基于了 SpringBoot 的 CommandLineRunner 去发起调用:



只是发起调用的方式不一样而已,其他没啥大区别。


需要说明的是,我只是手边刚好有一个 Dubbo 的 Demo,随手就拿来用了,但是本文想要表达的观点,和你使不使用 Dubbo 作为 RPC 框架,没有什么关系,道理是通用的。


上面这个 Demo 启动起来之后,通过 Http 接口发起一次调用,看到控制台服务提供方和服务消费方都有对应的日志输出,准备工作就算是齐活儿了:



上菜


在上面的 Demo 中,这是消费者的代码:



这是提供者的代码:



整个调用链路非常的清晰:



来,请你告诉我这里面有线程池吗?


没有!


是的,在日常的开发中,我就是写个接口给别人调用嘛,在我的接口里面并没有线程池相关的代码,只有 CRUD 相关的业务代码。


同时,在日常的开发中,我也经常调用别人提供给我的接口,也是一把梭,撸到底,根本就不会用到线程池。


所以,站在我,一个开发人员的角度,这个里面没有线程池。


合理,非常合理。


但是,当我们换个角度,再看看,它也是可以有的。


比如这样:



反应过来没有?


我们发起一个 Http 调用,是由一个 web 容器来处理这个请求的,你甭管它是 Tomcat,还是 Jetty、Netty、Undertow 这些玩意,反正是个 web 容器在处理。


那你说,这个里面有线程池吗?


在方法入口处打个断点,这个 http-nio-8081-exec-1 不就是 Tomcat 容器线程池里面的一个线程吗:



通过 dump 堆栈信息,过滤关键字可以看到这样的线程,在服务启动起来,啥也没干的情况下,一共有 10 个:



朋友,这不就是线程池吗?


虽然不是你写的,但是你确实用了。


我写出来的这个 test 接口,就是会由 web 容器中的一个线程来进行调用。所以,站在 web 容器的角度,这里是有一个线程池的:



同理,在 RPC 框架中,不管是消费方,还是服务提供方,也都存在着线程池。


比如 Dubbo 的线程池,你可以看一下官方的文档:



cn.dubbo.apache.org/zh-cn/overv…




而对于大多数的框架来说,它绝不可能只有一个线程池,为了做资源隔离,它会启用好几个线程池,达到线程池隔离,互不干扰的效果。


比如参与 Dubbo 一次调用的其实不仅一个线程池,至少还有 IO 线程池和业务线程池,它们各司其职:



我们主要关注这个业务线程池。


反正站在 Dubbo 框架的角度,又可以补充一下这个图片了:



那么问题来了,在当前的这个情况下?


当有人反馈:哎呀,这个服务吞吐量怎么上不去啊?


你怎么办?


你会 duang 的一下在业务逻辑里面加一个线程池吗?



大哥,前面有个 web 容器的线程池,后面有个框架的线程池,两头不调整,你在中间加个线程池,加它有啥用啊?


web 容器,拿 Tomcat 来说,人家给你提供了线程池参数调整的相关配置,这么一大坨配置,你得用起来啊:



tomcat.apache.org/tomcat-9.0-…




再比如 Dubbo 框架,都给你明说了,这些参数属于性能调优的范畴,感觉不对劲了,你先动手调调啊:



你把这些参数调优弄好了,绝对比你直接怼个线程池在业务代码中,效果好的多。


甚至,你在业务代码中加入一个线程池之后,反而会被“反噬”。


比如,你 duang 的一下怼个线程池在这里,我们先只看 web 容器和业务代码对应的部分:



由于你的业务代码中有线程池的存在,所以当接受到一个 web 请求之后,立马就把请求转发到了业务线程池中,由线程池中的线程来处理本次请求,从而释放了 web 请求对应的线程,该线程又可以里面去处理其他请求。


这样来看,你的吞吐量确实上去了。


在前端来看,非常的 nice,请求立马得到了响应。


但是,你考虑过下游吗?


你的吞吐量上涨了,下游同一时间处理的请求就变多了。如果下游跟不上处理,顶不住了,直接就是崩给你看怎么办?



而且下游不只是你一个调用方,由于你调用的太猛,导致其他调用方的请求响应不过来,是会引起连锁反应的。


所以,这种场景下,为了异步怼个线程池放着,我觉得还不如用消息队列来实现异步化,顶天了也就是消息堆积嘛,总比服务崩了好,这样更加稳妥。


或者至少和下游勾兑一下,问问我们这边吞吐量上升,你们扛得住不。


有的小伙伴看到这里可能就会产生一个疑问了:歪师傅,你这个讲得怎么和我背的八股文不一样啊?


巧了,你背过的八股文我也背过,现在我们来温习一下我们背过的八股文。


什么时候使用线程池呢?


比如一个请求要经过若干个服务获取数据,且这些数据没有先后依赖,最终需要把这些数据组合起来,一并返回,这样经典的场景:



用户点商品详情,你要等半天才展示给用户,那用户肯定骂骂咧咧的久走了。


这个时候,八股文上是怎么说的:用线程池来把串行的动作改成并行。



这个场景也是增加了服务 A 的吞吐量,但是用线程池就是非常正确的,没有任何毛病。


但是你想想,我们最开始的这个案例,是这个场景吗?



我们最开始的案例是想要在业务逻辑中增加一个线程池,对着一个下游服务就是一顿猛攻,不是所谓的串行改并行,而是用更多的线程,带来更多的串行。


这已经不是一个概念了。


还有一种场景下,使用线程池也是合理的。


比如你有一个定时任务,要从数据库中捞出状态为初始化的数据,然后去调用另外一个服务的接口查询数据的最终状态。



如果你的业务代码是这样的:


//获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
//select * from order where order_status=0;
ArrayList initOrderInfoList = queryInitOrderInfoList();
//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
    //捕获异常以免一条数据错误导致循环结束
    try{
        //发起rpc调用
        String orderStatus = queryOrderStatus(orderInfo.getOrderId);
        //更新订单状态
        updateOrderInfo(orderInfo.getOrderId,orderStatus);  
    } catch (Exception e){
        //打印异常
    }
}

虽然你框架中使用了线程池,但是你就是在一个 for 循环中不停的去调用下游服务查询数据状态,是一条数据一条数据的进行处理,所以其实同一时间,只是使用了框架的线程池中的一个线程。


为了更加快速的处理完这批数据,这个时候,你就可以怼一个线程池放在 for 循环里面了:


//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
    //使用线程池
    executor.execute(() -> {
        //捕获异常以免一条数据错误导致循环结束
        try {
            //发起rpc调用
            String orderStatus = queryOrderStatus(orderInfo.getOrderId);
            //更新订单状态
            updateOrderInfo(orderInfo.getOrderId, orderStatus);
        } catch (Exception e) {
            //打印异常
        }
    });
}


需要注意的是,这个线程池的参数怎么去合理的设置,是需要考虑的事情。


同时这个线程池的定位,就类似于 web 容器线程池的定位。


或者这样对比起来看更加清晰一点:



定时任务触发的时候,在发起远程接口调用之前,没有线程池,所以我们可以启用一个线程池来加快数据的处理。


而 Http 调用或者 RPC 调用,框架中本来就已经有一个线程池了,而且也给你提供了对应的性能调优参数配置,那么首先考虑的应该是把这个线程池充分利用起来。


如果仅仅是因为异步化之后可以提升服务响应速度,没有达到串行改并行的效果,那么我更加建议使用消息队列。


好了,本文的技术部分就到这里啦。


下面这个环节叫做[荒腔走板],技术文章后面我偶尔会记录、分享点生活相关的事情,和技术毫无关系。我知道看起来很突兀,但是我喜欢,因为这是一个普通博主的生活气息。


荒腔走板



不知道你看完文章之后,有没有产生一个小疑问:最开始部分的 Demo 似乎用处并不大?


是的,我最开始构思的行文结构是是基于 Demo 在源码中找到关于线程池的部分,从而引出其实有一些我们“看不见的线程池”的存在的。


原本周六我是有一整天的时间来写这篇文章,甚至周五晚上还特意把 Demo 搞定,自己调试了一番,该打的断点全部打上,并写完 Demo 那部分之后,我才去睡觉的,想得是第二天早上起来直接就能用。


按照惯例周六睡个懒觉的,早上 11 点才起床,自己慢条斯理的做了一顿午饭,吃完饭已经是下午 1 点多了。


本来想着在沙发上躺一会,结果一躺就是一整个下午。期间也想过起来写一会文章,坐在电脑前又飞快的躺回到沙发上,就是觉得这个事情索然无味,当下的那一刻就想躺着,然后无意识的刷手机,原本是拿来写文章中关于源码的部分的时间就这样浪费了。


像极了高中时的我,周末带大量作业回家,准备来个悬梁刺股,弯道超车,结果变成了一睡一天,捏紧刹车。


高中的时候,时间浪费了是真的可惜。


现在,不一样了。


荒腔走板这张图片,就是我躺在沙发上的时候,别人问我在干什么时随手拍的一张。


我并不为躺了一下午没有干正事而感到惭愧,浪费了的时间,才是属于自己的时间。


很久以前我看到别人在做一些浪费时间的事情的时候,我心里可能会嘀咕几句,劝人惜时。


这两年我不会了,允许自己做自己,允许别人做别人。


作者:why技术
来源:juejin.cn/post/7297980721590272040
收起阅读 »

2023行情不好,大龄员工如何跳槽

每一次找工作,都像一次职场大考,既是一次好的整理机会,也是一场对过去工作全方位的检验。--2023年换工作记。 来新团队快两周了,从年初二月份开始准备,到现在近9个月时间,换工作这事才算告一段落。 我今年35了,本科毕业十多年了,级别也不高,一线大兵,职场人的...
继续阅读 »

每一次找工作,都像一次职场大考,既是一次好的整理机会,也是一场对过去工作全方位的检验。--2023年换工作记。


来新团队快两周了,从年初二月份开始准备,到现在近9个月时间,换工作这事才算告一段落。


我今年35了,本科毕业十多年了,级别也不高,一线大兵,职场人的窘境在我身上齐活了--大龄Low T。


这篇文章总结了此次跳槽前后的一些思考与实践,供朋友们参考。


充分的准备


因为这次跳槽中间穿插了内部转岗、换城市、搬家,所以时间比较长。实际有效的求职时间大概5个月。


内容时长
复习专业技术1个月
刷题1个月
写简历3天
看机会&面试3个月


客观上行情确实不好,机会少,所以要留更长的时间等待机会,注意!是等!并不是你刚好投简历,就能遇到好的机会,你要留足够的时间等市面上合适的机会放出来。




公司一般年初会做好招聘预算,所以有金三银四,有的团队是下半年做完调研定好战略,启动招聘为明年做准备,这大概在金九银十。除了这两个窗口,其他时间大部分是淡季,除了少数公司遇到风口了会临时增加预算,或者业绩很好继续追加人,比如OpenAI的窗口,国内今年很多公司在补大模型和算法相关的人才。



准备-复习专业技术


我在百度呆了8年多,培养了一个习惯,重要的事情,全力以赴。



人和人的差距,其实就是几个关键点的差距。冲上去了就上去了。把每一个能做好的点都做到极致,你大概率就能超过别人。



时间有限,复习要有重点。


复习之前,你要明确几个简单的问题:



  1. 你出去要应聘的岗位和级别是什么?

  2. 这个岗位的要求是什么?

  3. 和别人比,你有什么优势?


上常用的招聘app(我主要用的是boss直聘)上搜一下相关的岗位是否有招聘,看看都有哪些公司、哪些岗位在招人,有什么要求。


如果你不介意换城市,多看几个城市,可以增加面试成功的几率。


然后,明确你要打造自己的"人设",不管你过去在公司干的怎么样,通过这几个月的复习你想成为什么样的人?成为哪个领域的专家,精通哪些技术、熟悉哪些技术,列出来,挑两三样重点复习,没有的话,就现整一个,临阵磨枪不快也光!B站上大把的免费教程教你如何快速成为专家。



求职中,最忌讳样样都会、啥都一知半解,好的简历和面试通常是精通一两门,其他的能横向拓展、融会贯通。




能精通一点意味着候选人能长期有耐心的钻研技术、深入解决问题、追求卓越,面试官有理由相信其他的问题他也一定能做的好。



我从事的是图形领域的开发工作,我花了一个月的时间,整理了OpenGL的常见API及用法、常见的渲染算法、C++高频问题,以及游戏引擎的架构和渲染管线,算是准备的比较充分了。


图形C++
OpenGL知识点整理(1)c++进阶知识点复习(1)
OpenGL知识点整理(2)c++进阶知识点复习(2)
深入理解opengl坐标系统c++3
游戏引擎(1)-ECS模式/
PBR(Physically Based Rendering)-理论/
PBR(Physically Based Rendering)-IBL/
图形学自学之路/

另外还有实时渲染算法、业务工程架构、引擎分析之类的整理,涉及到公司工作就没有发到公众平台上。


刷算法题


平时工作用到算法的地方并不多,想趁这个机会,把算法再熟悉一遍,温故知新。算法赋予了计算机灵魂,大厂考算法是有道理的。


提前给自己打了预防针,做好了打持久战的准备,所以直接买了力扣一年的vip会员,方便刷题和看题解。


另外专门读了一本系统的讲算法的书--《计算之魂-吴军》,从认知上提升对计算机、算法的理解。差不多刷了170多道题,基本上够用了。


关于刷题,之前写过一篇文章:重学数据结构算法(刷题心得)


投简历-找到有效的工作机会


节省时间,在网上找了个付费的简历网站,把履历填进去,能很方便的生成整洁的简历。也是直接买一年的,哥我就打算死磕了。


我已经工作十多年了,不能像刚毕业那样海投,那时是广撒网,有机会就去。现在有非常明确的目标,能接得住总包的工作机会就那么几家,一只手都数得过来,连预演练手的机会都没有,面一家就少一家。


锚定了意向中的那几家公司和岗位,有熟人的找熟人推一推,没熟人的硬投。实际发现,这年头HR都不靠谱了,更不用说猎头了。BOSS直聘上,研发自己跑出来找简历的一般都是真实的,那是真的着急招人,几率也更大,至少他要了你的简历,简历筛选这一关是过了,比HR效率高。


我最后投的几个岗位,都是字节研发侧主动来要简历的,加微信问了下,都是有大量真实的HC。


多说两句,其他的公司我就不黑了,字节我问了招聘方,他们明确表示不太在意年龄,冲这个人才观,我也更笃定了去字节。


面试-全力以赴每一场面试


我当了很多年面试官,也参加了很多次校招。面试其实很看眼缘,很难做到完全公平,除非你特别牛逼,有绝对的优势过面试,或者特别垃圾狗都嫌弃你,大多数候选人都在中间徘徊。


面试你的人大概率以后是你的leader,或者peer,他看你气质顺眼很重要,他认可你了,只要你不太差,也会给你过的。不喜欢你的气质,就无意中会有些刁难,过和不过都是一念之差。


这里说几个面试相关的细节。


不会的问题怎么办


说不知道、不会、没做过,是最差的回答。我面试中,会坦言自己没遇到过,请给我几分钟思考下,尝试找到合理的答案。


没有思路怎么办?


我会和面试官沟通,能否给一些提示,或者换一道题。


感觉自己没面好,直接放弃吗?


人生不要轻易言弃!!举两个我这次面试中的案例:



  • 案例1.


我一面的算法题写的有点问题,面试完回到家9点多。回忆代码逻辑,重新写了一遍,调试没问题了微信上和面试官沟通了下。


我表达的意思是:我不会放过任何一个有问题的代码,永不放弃!



  • 案例2.


二面的面试官问的很细,问了几个游戏引擎中很深入的问题,没回答好,我感觉自己应该是跪了。回到家我找了之前自己学习和整理的相关笔记发给面试官,告诉他,这些问题我之前真的有认真研究过,只是这次面试没回答好。


另外,我把在当前这家公司的历史绩效也截图发给了面试官,连着几年都拿了团队最高绩效,告诉他我真的很靠谱,恳求再给一次面试机会。


大概是我的真诚和坚持打动了面试官,第二天电话聊了下,还真给过了,推到了第三面。后面的面试就都比较顺利了。


涨幅


今年的行情,我了解到的,大部分公司都是卡30%的涨幅。HR问我期望薪资时,我很坚决的说,我看中的是这个机会,我热爱这个领域,薪酬差不多就行。


我心里能接受的最差的结果是降薪20%。这个年纪了,还能去一个往上走、充满机会的团队,持续成长,对职业发展来说是莫大的幸运,单纯的追求薪资是在杀鸡取卵、饮鸩止渴。


题外话-天赋是什么?


整个求职过程有点坎坷,有一些机会面的很好没有后话也很费解,后面也释怀了,大概是用人团队没有HC、或者给不上价,给内推人一个面子、走走过场,然后随便找个借口fail掉。


一个好朋友,也是前同事,一直关注我的面试进展,比我还紧张,他说,如果我这么努力这么牛逼都找不到工作,他的职业发展该何去何从。


第二天早上,我给他发了一个易建联退役的演讲视频[1分钟]:

v.douyin.com/iRSfCUAe/


真正的天赋,是你有多少热爱和付出-易建联


有多少朋友抱怨职场不公、运气不好,请问,你对自己的热爱有多少坚持,你对你的职业又有多少热爱,你又有多少勇气和毅力去改变这一切。



"没有人能随随便便成功,我也不例外"。



这是那天早上,我还在等offer、去上家公司上班的地铁上,在微信上打给这位朋友的最后一句。


作者:sumsmile
来源:juejin.cn/post/7300118821533089807
收起阅读 »

一个小公司的技术开发心酸事

背景 长话短说,就是在2022年6月的时候加入了一家很小创业公司。老板不太懂技术,也不太懂管理,靠着一腔热血加上对实体运输行业的了解,加上盲目的自信,贸然开始创业,后期经营困难,最终散伙。 自己当时也是不察,贸然加入,后边公司经营困难,连最后几个月的工资都没给...
继续阅读 »

背景


长话短说,就是在2022年6月的时候加入了一家很小创业公司。老板不太懂技术,也不太懂管理,靠着一腔热血加上对实体运输行业的了解,加上盲目的自信,贸然开始创业,后期经营困难,最终散伙。


自己当时也是不察,贸然加入,后边公司经营困难,连最后几个月的工资都没给发。


当时老板的要求就是尽力降低人力成本,尽快的开发出来App(Android+IOS),老板需要尽快的运营起来。


初期的技术选型


当时就自己加上一个刚毕业的纯前端开发以及一个前面招聘的ui,连个人事、测试都没有。


结合公司的需求与自己的技术经验(主要是前端和nodejs的经验),选择使用如下的方案:



  1. 使用uni-app进行App的开发,兼容多端,也可以为以后开发小程序什么的做方案预留,主要考虑到的点是比较快,先要解决有和无的问题;

  2. 使用egg.js + MySQL来开发后端,开发速度会快一点,行业比较小众,不太可能会遇到一些较大的性能问题,暂时看也是够用了的,后期过渡到midway.js也方便;

  3. 使用antd-vue开发运营后台,主要考虑到与uni-app技术栈的统一,节省转换成本;


也就是初期选择使用egg.js + MySQL + uni-app + antd-vue,来开发两个App和一个运营后台,快速解决0到1的问题。


关于App开发技术方案的选择


App的开发方案有很多,比如纯原生、flutter、uniapp、react-native/taro等,这里就当是的情况做一下选择。



  1. IOS与Android纯原生开发方案,需要新招人,两端同时开发,两端分别测试,这个资金及时间成本老板是不能接受的;

  2. flutter,这个要么自己从头开始学习,要么招人,相对于纯原生的方案好一点,但是也不是最好的选择;

  3. react-native/taro与uni-app是比较类似的选择,不过考虑到熟练程度、难易程度以及开发效率,最终还是选择了uni-app。


为什么选择egg.js做后端


很多时候方案的选择并不能只从技术方面考虑,当是只能选择成本最低的,当时的情况是egg.js完全能满足。



  1. 使用一些成熟的后端开发方案,如Java、、php、go之类的应该是比较好的技术方案,但对于老板来说不是好的经济方案;

  2. egg.js开发比较简单、快捷,个人也比较熟悉,对于新成员的学习成本也很低,对于JS有一定水平的也能很快掌握egg.js后端的开发


中间的各种折腾


前期开发还算顺利,在规定的时间内,完成了开发、测试、上线。但是,老板并没有如前面说的,很快运营,很快就盈利,运营的开展非常缓慢。中间还经历了各种折腾的事情。



  1. 老板运营遇到困难,就到处找一些专家(基本跟我们这事情没半毛钱关系的专家),不断的提一些业务和ui上的意见,不断的修改;

  2. 期间新来的产品还要全部推翻原有设计,重新开发;

  3. 还有个兼职的领导非要说要招聘原生开发和Java开发重新进行开发,问为什么,也说不出什么所以然,也是道听途说。


反正就是不断提出要修改产品、设计、和代码。中间经过不断的讨论,摆出自己的意见,好在最终技术方案没修改,前期的工作成果还在。后边加了一些新的需求:系统升级1.1、ui升级2.0、开发小程序版本、开发新的配套系统(小程序版本)以及开发相关的后台、添加即时通信服务、以及各种小的功能开发与升级;


中间老板要加快进度了就让招人,然后又无缘无故的要开人,就让人很无奈。最大的运营问题,始终没什么进展,明显的问题并不在产品这块,但是在这里不断的折腾这群开发,也真是难受。


明明你已经很努力的协调各种事情、站在公司的角度考虑、努力写代码,却仍然无济于事。


后期技术方案的调整



  1. 后期调整了App的打包方案;

  2. 在新的配套系统中,使用midway.js来开发新的业务,这都是基于前面的egg.js的团队掌握程度,为了后续的开发规范,做此升级;

  3. 内网管理公用npm包,开发业务组件库;

  4. 规范代码、规范开发流程;


人员招聘,团队的管理


人员招聘


如下是对于当时的人员招聘的一些感受:



  1. 小公司的人员招聘是相对比较难的,特别是还给不了多少钱的;

  2. 好在我们选择的技术方案,只要对于JS掌握的比较好就可以了,前后端都要开发一点,也方便人员工作调整,避免开发资源的浪费。


团队管理


对于小团队的管理的一些个人理解:



  1. 小公司刚起步,就应该实事求是,以业务为导向;

  2. 小公司最好采取全栈的开发方式,避免任务的不协调,造成开发资源的浪费;

  3. 设置推荐的代码规范,参照大家日常的代码习惯来制定,目标就是让大家的代码相对规范;

  4. 要求按照规范的流程设计与开发、避免一些流程的问题造成管理的混乱和公司的损失;

    1. 如按照常规的业务开发流程,产品评估 => 任务分配 => 技术评估 => 开发 => 测试 => cr => 上线 => 线上问题跟踪处理;



  5. 行之有效可量化的考核规范,如开发任务的截止日期完成、核心流程开发文档的书写、是否有线上bug、严谨手动修改数据库等;

  6. 鼓励分享,相互学习,一段工作经历总要有所提升,有所收获才是有意义的;

  7. 及时沟通反馈、团队成员的个人想法、掌握开发进度、工作难点等;


最后总结及选择创业公司避坑建议!important



  1. 选择创业公司,一定要确认老板是一个靠谱的人,别是一个总是画饼的油腻老司机,或者一个优柔寡断,没有主见的人,这样的情况下,大概率事情是干不成的;

    1. 老板靠谱,即使当前的项目搞不成,也可能未来在别的地方做出一番事情;



  2. 初了上边这个,最核心的就是,怎么样赚钱,现在这种融资环境,如果自己不能赚钱,大概率是活不下去的@自己;

  3. 抓住核心矛盾,解决主要问题,业务永远是最重要的。至于说选择的开发技术、代码规范等等这些都可以往后放;

  4. 对上要及时反馈自己的工作进度,保持好沟通,老板总是站在更高一层考虑问题,肯定会有一些不一样的想法,别总自以为什么什么的;

  5. 每段经历最好都能有所收获,人生的每一步都有意义。


以上只是个人见解,请指教。


作者:qiuwww
来源:juejin.cn/post/7257085326471512119
收起阅读 »

语雀又崩了?今天咱们玩点花的,手把手教你写出令人窒息的“烂代码”

web
Hello,大家好,我是Sunday。 10月23日 2023年10月23日,语雀崩溃 10 个小时,作为一款知名度极高的产品,这样的一次崩溃可以说对语雀的口碑影响极大。 不过,好在语雀的公关团队处理的还不错,没有甩锅而是及时发布公告,明确是自己的问题。同时在...
继续阅读 »

Hello,大家好,我是Sunday。


10月23日


2023年10月23日,语雀崩溃 10 个小时,作为一款知名度极高的产品,这样的一次崩溃可以说对语雀的口碑影响极大。


不过,好在语雀的公关团队处理的还不错,没有甩锅而是及时发布公告,明确是自己的问题。同时在问题解决之后,给大家 六个月的会员补偿 也可以说是诚意满满(以下为10月24日语雀团队公告)。




毕竟大家都是程序员嘛,这种事也不是不能接受。毕竟:谁还没搞崩过系统呢?😂



本以为这件事就这么过去了,哪知道昨天的一个故障,再次让语雀登上了“风口浪尖”......


11月12日


昨天下午,我在正常使用语雀记录同学学习情况的时候,突然出现了无法保存的情况。心想:“这不会是又崩了吧~~”


看了眼语雀群的微信,果然......




说实话,当时我的第一反应是:“又有瓜可以吃了~~~~~,开心😂”



反正也写不成了,坐等吃瓜就行了。正好恰逢双十一,看看买的硬盘到哪了。


结果打开淘宝才发现,这次不对劲啊,淘宝也崩了!!!




最终我们了解了事情的全貌:



本次事故是由于阿里云 OSS 的故障导致的。钉钉、咸鱼、淘宝、语雀都崩了....



从语雀的公告也体现出了这点:



公告内容如下:



尊敬的客户:您好!北京时间2023年11月12日 17:44起,阿里云监控云产品控制台访问及API调用出现出现使用异常,阿里云工程师正在紧急介入排查。非常抱歉给您的使用带来不便,若有任何问题,请随时联系我们。



可以说,语雀这次有点躺枪了(谁让你刚崩过呢~~~)。


玩点花的!教你写出令人窒息的“烂代码”


好啦,瓜吃完啦。



关于语雀崩溃的反思,网上有很多文章,我就不凑这个热闹了,想要看的同学可以自行搜索~~



“回归正题”,接下来咱们就来看看咱们的文章正题:“如何写出烂代码”。



以下共有十三条烂代码书写准则,可能并没有面面俱到,如果大家发现有一些难以忍受的烂代码习惯,也可以留言发表意见~~



第一条:打字越少越好


  // Good 👍🏻
const a = 18

// Bad 👎
const age = 18

第二条:变量/函数混合命名风格


  // Good 👍🏻
const my_name = 'Suday'
const mName = 'Sunday'
const MnAme = 'Sunday'

// Bad 👎
const myName = 'Sunday'

第三条:不要写注释


  // Good 👍🏻
const cbdr = 666

// Bad 👎
// 666ms 是根据 UX A/B 测试结果进行经验计算的。
// 具体可查看 xxxxxx
const callbackDebounceRate = 666

第四条:使用母语写注释


  // Good 👍🏻
// 666 мс было эмпірычна вылічана на аснове вынікаў UX A/B.
const callbackDebounceRate = 666

// Bad 👎
// 666ms 是根据 UX A/B 测试结果进行经验计算的。
// 具体可查看 xxxxxx
const callbackDebounceRate = 666

第五条:尽可能混合不同的格式


  // Good 👍🏻
const n = 'Sunday';
const a = "18"
const g = "MAN"

// Bad 👎
const name = 'sunday'
const age = '18'
const gender = 'man'

第六条:尽可能把代码写成一行


  // Good 👍🏻
document.location.search.replace(/(^\?)/, '').split('&').reduce(function (o, n) { n = n.split('=') })

// Bad 👎
document.location.search
.replace(/(^\?)/, '')
.split('&')
.reduce((searchParams, keyValuePair) => {
keyValuePair = keyValuePair.split('=')
searchParams[keyValuePair[0]] = keyValuePair[1]
return searchParams
})

第七条:发现错误要保持静默


   // Good 👍🏻
try {
...
} catch () {🤐}

// Bad 👎
try {
...
} catch (error) {
setErrorMessage(error.message)
logError(error)
}

第八条:广泛使用全局变量


  // Good 👍🏻
let count = 1
function addCount() {
count += 1
}

// Bad 👎
function addCount() {
let count = 1
count += 1
}

第九条:构建备用变量


  // Good 👍🏻
let count = 1
function addCount() {
count += 1
}

// Bad 👎
function addCount() {
let count = 1
count += 1
}

第十条:Type 使用需谨慎


  // Good 👍🏻
function sum(a, b) {
return a + b
}

// Bad 👎
function sum(a: number, b: number) {
return a + b
}

第十一条:准备「Plan B」


  // Good 👍🏻
function square(num) {
if (typeof num === 'undefined') {
return undefined
} else {
return num ** 2
}
return null
}

// Bad 👎
function square(num) {
if (typeof num === 'undefined') {
return undefined
}
return num ** 2
}

第十二条:嵌套的三角法则


    // Good 👍🏻
function somFun(num) {
if (condition1) {
if (condition2) {
asyncFunction(param, (result) => {
if (result) {
for (; ;) {
if (condition3) {

}
}
}
})
}
}
}

// Bad 👎
async function somFun(num) {
if (!condition1 || !condition2) {
return;
}
const result = await asyncFunction(params);
if (!result) {
return;
}
for (; ;) {
if (condition3) {

}
}
}

第十三条:混合缩进


      // Good 👍🏻
const f = ['zs'
, 'lisi', 'wangwu']
const d = {
name: 'zs',
age: '18'
}

// Bad 👎
const f = ['zs'
, 'lisi', 'wangwu']
const d = {
name: 'zs',
age: '18'
}

总结


所谓的“烂代码”,是大家一定 不要 照着写的哈。


“教你写出令人窒息的“烂代码”“ 是一个反义,这个大家肯定是可以明白的哈~~~~。



”烂代码“内容参考自:zhuanlan.zhihu.com/p/516564022



作者:程序员Sunday
来源:juejin.cn/post/7300440002999435316
收起阅读 »

实战来了,基于DDD实现库存扣减~

大家好,让我们继续DDD&微服务系列,今天,我们看看在DailyMart项目中如何基于DDD实现库存扣减功能。 1. 库存模型 1.1 核心概念 库存是一个非常复杂的概念,涉及在仓库存,计划库存,渠道库存等多个领域实体,在我们《DailyMart微服务...
继续阅读 »

大家好,让我们继续DDD&微服务系列,今天,我们看看在DailyMart项目中如何基于DDD实现库存扣减功能。


1. 库存模型


1.1 核心概念


库存是一个非常复杂的概念,涉及在仓库存,计划库存,渠道库存等多个领域实体,在我们《DailyMart微服务&DDD》实战中,主要关注的是在仓库存模型。


image-20231028224038257


在这个模型中有三个重要的概念:可售库存、预售库存、占用库存,他们的定义如下:


可售库存数(Sellable Quantity,SQ)
可售库存即用户在客户端所见的商品可销售数量。当SQ为0时,用户不能下单。


预扣库存数(Withholding Quantity,WQ)
预扣库存是指被未付款的订单占用的库存数量。这种库存的存在是因为用户在下单后可能不会立刻付款。预扣库存的作用是为用户保留库存,直到用户完成付款,才会从中扣减相应数量的库存。如果用户未能在规定时间内付款,预扣库存WQ将被释放回可售库存SQ上。


占用库存数(Occupy Quantity,OQ)
占用库存是指已经完成付款,但尚未发货的订单所占用的库存数量。这种库存与仓库相关,并且牵涉到履约流程。一旦订单发货,占用库存会相应减少。


根据上述定义,对于一个商品,可售库存数量与预扣库存数量之间的关系是:可售库存(SQ) + 预扣库存(WQ) = 可用库存。


由于每种商品通常包含多个不同的 SKU,在商品交易链路中,无法通过商品id来精确定位库存。为了更高效地管理库存查询和更新请求,我们需要设计一个具有唯一标识能力的 ID,即库存 ID(inventory_id)。此外,在库存扣减操作中还需要存储库存扣减记录,一旦用户取消订单或退货时,可以根据扣减记录返还相应的库存数量。


1.2 领域模型


通过对库存领域概念的分析,我们很容易完成DDD领域建模,如下图所示:


image-20231030160921576


库存 (Inventory): 库存对象充当库存领域的聚合根,负责管理和跟踪商品的可售库存、预扣库存和占用库存等信息。库存对象也具备唯一标识能力,使用库存 ID(inventory_id)来标识不同库存。


库存记录 (InventoryRecord): 库存记录是一个实体,用于记录库存的各种操作,例如扣减、占用、释放、退货等。每个库存记录都有一个唯一的记录 ID(record_id)来标识。


库存 ID(InventoryId)和记录 ID(RecordId): 这两者都是值对象,它们负责提供唯一标识,分别用于标识库存和库存记录。


库存扣减状态(InventoryRecordStateEnum):这也是个值对象,用于标识扣减库存的状态。


2. 库存扣减


库存扣减看似简单,只需在用户支付后减少库存即可,但实际情况要复杂得多。不同的扣减顺序可能导致不同的问题。比如我们先减库存后付款,可能会出现用户下单后放弃支付,导致商品少买或未售出。另一方面,如果我们先付款后减库存,可能出现用户成功支付但商家没有足够的库存来满足订单,这又非常影响用户体验。


一般来说,库存扣减有三种主要模式:


2.1 库存扣减的三种模式




  • 拍减模式:在用户下单时,直接扣减可售库存(SQ)。这种模式不会出现超卖问题,但它的防御能力相对较弱。如果用户大量下单而不付款,库存会一直被占用,从而影响正常交易,导致商家少卖。




  • 预扣模式:在用户下单时,会预先扣减库存,如果订单在规定时间内未完成支付,系统将释放库存。具体来说,当用户下单时,会预扣库存(SQ-、WQ+),此时库存处于预扣状态;一旦用户完成付款,系统会减少预扣库存(WQ-、OQ+),此时库存进入扣减状态




  • 付减模式:在用户完成付款时,直接扣减可售库存(SQ)。这种模式存在超卖风险,因为无法确保用户付款后一定有足够的库存。




对于实物商品,库存扣减主要采用拍减模式预扣模式,付减模式应用较少,在我们DailyMart系统中采用的正是预扣模式。


2.2 预扣模式核心链路


接下来我们重点介绍库存预扣模式的核心链路,包括正向流程和逆向操作。


2.2.1 正向流程


正向流程涉及用户下单、付款和发货的关键步骤。以下是正向流程的具体步骤:


1)用户将商品加入购物车,点击结算后进入订单确认页,点击提交订单后,订单中心服务端发起交易逻辑。


2)调用库存服务执行库存预扣逻辑


3)调用支付服务发起支付请求


4)用户付款完成以后,调用库存平台扣减库存


5)订单服务发送消息给仓储中心,仓储中心收到消息后创建订单,并准备配货发货


6)仓储中心发货以后调用库存平台扣减占用库存数。


image-20231029215629997


2.2.2 逆向操作


逆向操作包括取消订单或退货等情况,我们需要考虑如何回补库存。逆向操作的步骤如下:


1)用户取消订单或退货。
2)更新扣减记录行,状态为释放状态。
3)同时更新库存行,以回补库存。


2.2 库存扣减的执行流程


每一件商品的库存扣减都至少涉及两次数据库写操作:更新库存表(inventory_item)和扣减记录表(inventory_record)。


image-20231030171653428


为了确保库存扣减操作的幂等性,通常需要在扣减记录表中给业务流水号字段创建唯一索引。此外,为了保证数据一致性,修改库存数量与操作流水记录的两个步骤必须在同一个事务中。



关于系统的幂等性实现方案,我在知识星球进行了详细介绍,感兴趣的可以通过文末链接加入。



在数据库层面,库存扣减操作包括以下关键步骤:




  • 用户下单时:insert 扣减记录行,状态为预扣中,同时 update 库存行(减少可销售库存,增加预扣库存,sq-,wq+);




  • 用户付款时:update 扣减记录行,状态为扣减状态,同时update库存行(减少预扣库存,增加占用库存,wq-,oq+);




  • 仓库发货时:update 扣减记录行,状态为发货状态,同时update库存行(减少占用库存数,oq-);




  • 逆向操作时:update 扣减记录行,状态为释放状态,同时update库存行(增加可销售库存,sq+);




通过下图可以清晰看到库存扣减时相关相关数据状态的变化。
image-20231030163042599


3. 核心代码实现


接下来,让我们从接口层、应用层、领域层和基础设施层的角度来分析库存扣减的代码实现。(考虑到篇幅原因,省略了部分代码。)


3.1 接口层


接口层是库存操作的入口,定义了库存操作的接口,如下所示:


@RestController
@Tag(name = "InventoryController", description = "库存API接口")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class InventoryController {
...

@Operation(summary = "库存预扣",description = "sq-,wq+,创建订单时调用")
@PostMapping("/api/inventory/withholdInventory")
public void withholdInventory(@Valid @RequestBody InventoryLockRequest lockRequest) {
inventoryService.withholdInventory(lockRequest);
}

@Operation(summary = "库存扣减",description = "wq-,oq+,付款时调用")
@PutMapping("/api/inventory/deductionInventory")
public void deductionInventory(@RequestParam("transactionId") Long transactionId) {
inventoryService.deductionInventory(transactionId);
}

@Operation(summary = "库存发货",description = "oq-,发货时调用")
@PutMapping("/api/inventory/shipInventory")
public void shipInventory(@RequestParam("transactionId") Long transactionId) {
inventoryService.shipInventory(transactionId);
}

@Operation(summary = "释放库存")
@PutMapping("/api/inventory/releaseInventory")
public void releaseInventory(@RequestParam("transactionId") Long transactionId) {
inventoryService.releaseInventory(transactionId);
}
...
}

3.2 应用层


应用层负责协调领域服务和基础设施层,完成库存扣减的业务逻辑。库存服务不涉及跨聚合操作,因此只需调用基础设施层的能力,并让领域层完成一些直接的业务逻辑。


@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Slf4j
public class InventoryServiceImpl implements InventoryService {
...
@Override
@Transactional
public void withholdInventory(InventoryLockRequest inventoryLockRequest) {
Long inventoryId = inventoryLockRequest.getInventoryId();
//1. 获取库存
Inventory inventory = Optional.ofNullable(inventoryRepository.find(new InventoryId(inventoryId)))
.orElseThrow(()->new BusinessException("No inventory found with id:" + inventoryId));

// 2. 幂等校验
boolean exists = inventoryRepository.existsWithTransactionId(inventoryLockRequest.getTransactionId());

if(exists ){
log.error("Inventory record with transaction ID {} already exists, no deduction will be made.", inventoryLockRequest.getTransactionId());
return;
}

//3. 库存预扣
inventory.withholdInventory(inventoryLockRequest.getQuantity());

//4. 生成扣减记录
InventoryRecord inventoryRecord = InventoryRecord.builder()
.inventoryId(inventoryId)
.userId(inventoryLockRequest.getUserId())
.deductionQuantity(inventoryLockRequest.getQuantity())
.transactionId(inventoryLockRequest.getTransactionId())
.state(InventoryRecordStateEnum.PRE_DEDUCTION.code())
.build();

inventory.addInventoryRecord(inventoryRecord);

inventoryRepository.save(inventory);
}
...
}

3.3 领域层


领域层负责处理直接涉及业务规则和逻辑的操作,将库存预扣、扣减、库存释放等操作封装在聚合对象 Inventory 中。同时,领域层定义了仓储接口,如下所示:


@Data
public class Inventory implements Aggregate {
@Serial
private static final long serialVersionUID = 2139884371907883203L;
private InventoryId id;

...

/**
* 库存预扣 sq-,wq+
*
@param quantity 数量
*/

public void withholdInventory(int quantity){
if (quantity <= 0) {
throw new BusinessException("扣减库存数量必须大于零");
}

if (getInventoryQuantity() - quantity < 0) {
throw new BusinessException("库存不足,无法扣减库存");
}

sellableQuantity -= quantity;
withholdingQuantity += quantity;
}

/**
* 释放库存
*
@param currentState 当前状态
*
@param quantity 数量
*/

public void releaseInventory(int currentState, Integer quantity) {
InventoryRecordStateEnum stateEnum = InventoryRecordStateEnum.of(currentState);
switch (stateEnum){
//sq+,wq-
case PRE_DEDUCTION -> {
sellableQuantity += quantity;
withholdingQuantity -= quantity;
}
//sq+,oq-
case DEDUCTION -> {
sellableQuantity += quantity;
occupyQuantity -= quantity;
}
//sq+
case SHIPPED -> {
sellableQuantity += quantity;
}
}
}
...
}

/**
* 仓储接口定义
*/

public interface InventoryRepository extends Repository {
boolean existsWithTransactionId(Long transactionId);

Inventory findByTransactionId(Long transactionId);
}

3.4 基础设施层


基础设施层负责数据库操作,持久化库存状态,如下所示:


@Repository
@Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class InventoryRepositoryImpl implements InventoryRepository {
...
@Override
public Inventory find(InventoryId inventoryId) {

InventoryItemDO inventoryItemDO = inventoryItemMapper.selectById(inventoryId.getValue());
return itemInventoryConverter.fromData(inventoryItemDO);
}

@Override
public Inventory save(Inventory aggregate) {
InventoryItemDO inventoryItemDO = itemInventoryConverter.toData(aggregate);

if(inventoryItemDO.getId() == null){
inventoryItemMapper.insert(inventoryItemDO);
}else{
inventoryItemMapper.updateById(inventoryItemDO);
}

InventoryRecord inventoryRecord = aggregate.getInventoryRecordList().get(0);
InventoryRecordDO inventoryRecordDO = inventoryRecordConverter.toData(inventoryRecord);

if(inventoryRecordDO.getId() == null){
inventoryRecordMapper.insert(inventoryRecordDO);
}else{
inventoryRecordMapper.updateById(inventoryRecordDO);
}

return aggregate;
}
...
}

小结


本文详细介绍了库存领域的关键概念以及库存扣减的三种模式,同时基于DDD的分层模型,成功实现了预扣模式的业务逻辑。在库存的预扣接口中,通过业务流水表确保了接口的幂等性,不过更新库存的接口暂时还没实现幂等,幂等会在下篇文章中统一解决。同时,值得注意的是,本文所展示的方案采用了纯数据库实现,可能在高并发情况下性能略有下降,当然这也是我们后面需要优化的点。


作者:飘渺Jam
来源:juejin.cn/post/7299037876636696615
收起阅读 »

独自一人时写代码 VS 朋友在旁边时写代码

网友评论:@TAO大鑫:程序员是不允许别人站在身后的@Bibala-Bong:朋友在身边,不停的ls ls ls ps ps ps top top top 怎么能让屏幕上东西多怎么来 @贼王卍冬:一个人写代码:百度有啥我会啥; 朋友在旁边:今天气不错啊@铁柱未...
继续阅读 »


网友评论:


@TAO大鑫:程序员是不允许别人站在身后的

@Bibala-Bong:朋友在身边,不停的ls ls ls ps ps ps top top top 怎么能让屏幕上东西多怎么来 

@贼王卍冬:一个人写代码:百度有啥我会啥; 朋友在旁边:今天气不错啊

@铁柱未来科技有限公司董事长:有时候感觉后边有人就是猛一回头

@新浪云: 程序员不要面子啊

作者:程序员的幽默
来源:mp.weixin.qq.com/s/A-MdLvqmOPxTFBPTJVZ0OQ
e>

收起阅读 »

21亿!李佳琦或面临破产,网友:想看他努力工作样子

双11还没有到来,一场由京东、李佳琦、小杨哥之间的大战却因1台烤箱拉开了大幕。而正是这台烤箱,在李佳琦迫使品牌方起诉京东,得罪小杨哥的那一刻,也扯下了超级头部主播“全网最低价”的遮羞布。事情起因是这样的:海氏向市管局举报京东近日,海氏(品牌方)向市管总局实名举...
继续阅读 »
收起阅读 »

Uniapp Record:获取手机号

web
前言:渠道 -> 产品:"我需要收集用户信息"。产品 -> 开发:"那就新增一个功能来获取用户的手机号地址相关信息"。最近项目中增加了获取用户信息相关需求,这个功能怎么说呢,对于我甚至是大部分人来说都是比较抵触的吧,毕竟无缘无故获取个人信息就是感觉...
继续阅读 »

前言:渠道 -> 产品:"我需要收集用户信息"。产品 -> 开发:"那就新增一个功能来获取用户的手机号地址相关信息"。最近项目中增加了获取用户信息相关需求,这个功能怎么说呢,对于我甚至是大部分人来说都是比较抵触的吧,毕竟无缘无故获取个人信息就是感觉不爽,哈哈!但是没也没法,身为打工人的无奈,照做呗。



由于目前项目技术栈是 uniapp,所以先去官方文档查阅相关资料,了解到目前有是三种方式涉及手机号相关的,自然也是能够获取到手机号。


1. uni一键登录


uni一键登录是DCloud公司联合个推公司推出的,整合了三大运营商网关认证能力的服务。实现流程如下:



  1. App 界面弹出请求授权,询问用户是否同意该App获取手机号。这个授权界面是运营商SDK弹出的,可以有限定制;

  2. 用户同意授权后,SDK底层访问运营商网关鉴权,获得当前设备access_token等信息;

  3. 在服务器侧通过 uniCloud 将access_token等信息 置换为当前设备的真实手机号码。然后服务器直接入库,避免手机号传递到前端发生的不可信情况。


对该方法大致了解了下,其中流程相对比较简单,但是结合当前项目来说:



  1. 每次验证需要收费,虽然很便宜(2分)

  2. 需要开通uni一键登录服务,uniCloud 服务


因为项目不涉及云开发,而且不考虑产品使用时产生的额外费用,所以暂时pass掉。


2. OAuth 登录鉴权


App端OAuth(登录鉴权)模块封装了市场上主流的三方登录SDK,提供JS API统一调用登录鉴权功能。也看下实现流程:



  1. 向三方登录平台申请开通,有些平台(如微信登录)申请成功后会获取appid;

  2. 在HBuilder中配置申请的参数(如appid等),提交云端打包生成自定义基座;

  3. 在App项目中用API进行登录,成功后获取到授权标识提交到业务服务器完成登录操作。


该方式需要在项目 mainifest.json 中去开启 OAuth 鉴权模块:


uni02.png


可以看到里面除了前面提到的 一键登录,还包含 苹果登录、微信登录、QQ登录等三方登录平台,因为要涉及开通相关服务,并且当前登录业务鉴权逻辑比较简单(手机号、密码验证),并且app也为上架应用市场,所以这种相对繁琐的方式也就不考虑了。


3. 微信小程序登录


前面两种方式都pass掉了,意味着要获取手机号相关信息在APP中是行不通了的,但是不慌,不是还有微信小程序版嘛,正好产品也包含小程序平台,前段时间做公众号网页开发时也是包含登录授权,所以小程序的授权登录应该也差不多,而且小程序对比APP来说相对便捷(缺点是某些涉及原生插件相关的功能暂时无法使用)。


同样,先去微信官方文档查阅,看到有两种方式可以获取:


uni03.png


下面具体介绍下实现方案:


3-1. 纯前端实现

<button open-type="getPhoneNumber" @getphonenumber="getPhoneNumber" plain="true">获取手机号</button>

这个 button 里面的一些属性及事件的具体用法说明可以去看文档说明:uniapp button 用法,文档解释的很清楚,写法也是固定的。


这里还需要用到一个加解密插件:WXBizDataCrypt,下载链接如下,


https://res.wx.qq.com/wxdoc/dist/assets/media/aes-sample.eae1f364.zip

可以去下载选择对应的版本,目前有 Java、C++、Node、Python四个版本,我们这里选择Node版本,将 WXBizDataCrypt.js 添加到项目中


完整代码如下:


<!-- testPhone.vue -->
<template>
<view class="wrap">
<view class="box-container">
<input v-model="phone" />
<view class="action-btn">
<button open-type="getPhoneNumber" @getphonenumber="getPhoneNumber" plain="true">获取手机号</button>
</view>
</view>
</view>
</template>

<script>
import WXBizDataCrypt from '@/utils/WXBizDataCrypt.js'

export default {
data() {
return {
phone: "",
phone_iv: "",
js_code: "",
session_key: "",
phone_encryptedData: null,
}
},
onShow() {
this.initLogin()
},
methods: {
initLogin() {
uni.login({
provider: 'weixin',
success: res => {
this.js_code = res.code
uni.request({
url: 'https://api.weixin.qq.com/sns/jscode2session', // 请求微信服务器
method: 'GET',
data: {
appid: 'xxxxxxxx', // 微信appid
secret: 'xxxxxxxxxxxxx', // 微信秘钥
js_code: this.js_code,
grant_type: 'authorization_code'
},
success: (res) => {
console.log('获取信息', res.data);
this.session_key = res.data.session_key
}
});
}
});
},
getPhoneNumber(res) {
console.log(res, '---手机号回调信息')
this.phone_encryptedData = res.detail.encryptedData;
this.phone_iv = res.detail.iv;
let pc = new WXBizDataCrypt('填写微信appid', this.session_key);
try {
let data = pc.decryptData(this.phone_encryptedData, this.phone_iv);
if (data.phoneNumber !== '') {
this.phone = data.phoneNumber;
}
} catch (error) {
console.error('获取失败:', error);
}
}
}
}
</script>

<style lang="less">
.wrap {
width: 100vw;
height: 100vh;
background-color: #F1F2F6;
display: flex;
align-items: center;
justify-content: center;

.box-container {
width: 70vw;
height: 30vh;

input {
border: 2rpx solid black;
}

.action-btn {
width: 50%;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
margin: 40rpx auto;
}
}
}
</style>

大致流程是:


先通过 uni.login 拿到一个 code,用这 code 作为js_code、appid(微信小程序后台设置中获取)、secret(微信公众号后台获取的密钥)、grant_type(固定值:authorization_code) 去请求 https://api.weixin.qq.com/sns/jscode2session 这个地址,返回结果如下:


{"session_key":"zkJJOfHPYHc\/cVK2kydibg==","openid":"oHXOj5NJMH78yWdVcf6loGOL4cno"}

然后点击按钮调起微信手机号授权页:


999.png


@getphonenumber 事件的回调中获取的信息打印结果如下:


888.png


框选的信息就是我们需要的,是一个加密后的数据。


最后使用 WXBizDataCrypt 对信息进行解密,解密后就是我们需要的手机号信息了。


3-2. 前后端实现


前端代码逻辑改了下:


<script>
export default {
data() {
return {
phone: "",
}
},
methods: {
getPhoneNumber(res) {
console.log(res, '---手机号回调信息')
// 注:这里的code和前面登录返回的code是不同的
const { code } = res.detail
// 根据code去请求后端提供的接口,即可从响应数据中拿到手机号
}
}
}
</script>

后端做了哪些事情呢?


首先会去 获取接口调用凭证 ,官方文档描述如下:


777.png


// 参数说明

{
grant_type: client_credential, // 固定值
appid: '', // 填写微信小程序的appid
secret: '', // 填写微信小程序的密钥
}

返回参数为:access_token(接口凭证)expire_in(过期时间,默认为2小时)


然后再去调获取手机号接口(getPhoneNumber),


666.png


参数携带前面返回的 access_token,再加上前端传过来的 code,即可获取到手机号信息。


下面是我用 Postman 对三个接口做了测试验证:


weixin08.png


weixin07.png


weixin06.png


对比两种方式,个人建议还是采用第二种好一点,让相关的业务都在后端去处理,除此之外还有一个原因就是涉及一个安全性相关问题,前面代码中可以看到我们在请求小程序登录接口是将 appid、screct等信息放在请求参数中的,这种极易通过源码拿到,所以存在相关信息泄露问题,事实证明这种方式也是不建议使用的:


555.png


踩坑点




  1. 注意区分登陆时返回的 code 和 button 按钮获取手机号回调返回的 code 是不相同的




  2. @getphonenumber 回调函数的返回信息如果信息为:api scope is not declared in the privacy agreement ,这种是小程序的【隐私保护策略】限制的,排查下你的小程序中用户隐私保护指引设置送是否添加了相关的用户隐私类型(手机号、通讯录、位置信息等)




444.png


以上就是结合项目需求场景对获取手机号的实现做的一个记录!


作者:瓶子丶
来源:juejin.cn/post/7300036605099343926
收起阅读 »

我们为什么要使用Java的弱引用?

哈喽,各位小伙伴们,你们好呀,我是喵手。   今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。   我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都...
继续阅读 »

哈喽,各位小伙伴们,你们好呀,我是喵手。



  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。


  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。



小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!



前言


在Java开发中,内存管理一直是一个重要的话题。由于Java自动内存分配和垃圾回收机制的存在,我们不需要手动去管理内存,但是有时候我们却需要一些手动控制的方式来减少内存的使用。本文将介绍其中一种手动控制内存的方式:弱引用。


摘要


本文主要介绍了Java中弱引用的概念和使用方法。通过源代码解析和应用场景案例的分析,详细阐述了弱引用的优缺点以及适用的场景。最后,给出了类代码方法介绍和测试用例,并进行了全文小结和总结。


Java之弱引用


简介


弱引用是Java中一种较为特殊的引用类型,它与普通引用类型的最大不同在于,当一个对象只被弱引用所引用时,即使该对象仍然在内存中存在,也可能被垃圾回收器回收。


源代码解析


在Java中,弱引用的实现是通过WeakReference类来实现的。该类的定义如下:


public class WeakReference<T> extends Reference<T> {
public WeakReference(T referent);
public WeakReference(T referent, ReferenceQueue<? super T> q);
public T get();
}

其中,构造方法分别是无参构造方法、有参构造方法和获取弱引用所引用的对象。


与强引用类型不同,弱引用不会对对象进行任何引用计数,也就是说,即使存在弱引用,对象的引用计数也不会增加。


  如下是部分源码截图:


在这里插入图片描述


应用场景案例


缓存


在开发中,缓存是一个很常见的场景。但是如果缓存中的对象一直存在,就会导致内存不断增加。这时,我们就可以考虑使用弱引用,在当缓存中的对象已经没有强引用时,该对象就会被回收。


Map<String, WeakReference<User>> cache = new HashMap<>();

public User getUser(String userId) {
User user;
// 判断是否在缓存中
if (cache.containsKey(userId)) {
WeakReference<User> reference = cache.get(userId);
user = reference.get();
if (user == null) {
// 从数据库中读取
user = db.getUserById(userId);
// 加入缓存
cache.put(userId, new WeakReference<>(user));
}
} else {
// 从数据库中读取
user = db.getUserById(userId);
// 加入缓存
cache.put(userId, new WeakReference<>(user));
}
return user;
}

上述代码中,我们在使用缓存时,首先判断该对象是否在缓存中。如果存在弱引用,我们先通过get()方法获取对象,如果对象不为null,则直接返回;如果对象为null,则说明该对象已经被回收了,此时需要从数据库中重新读取对象,并加入缓存。


监听器


在Java开发中,我们经常需要使用监听器。但是如果监听器存在强引用,当我们移除监听器时,由于其存在强引用,导致内存无法释放。使用弱引用则可以解决该问题。


public class Button {
private List<WeakReference<ActionListener>> listeners = new ArrayList<>();

public void addActionListener(ActionListener listener) {
listeners.add(new WeakReference<>(listener));
}

public void removeActionListener(ActionListener listener) {
listeners.removeIf(ref -> ref.get() == null || ref.get() == listener);
}

public void click() {
for (WeakReference<ActionListener> ref : listeners) {
ActionListener listener = ref.get();
if (listener != null) {
listener.perform();
}
}
}
}

上述代码中,我们使用了一个List来保存所有的监听器。在添加监听器时,我们使用了WeakReference进行包装,以保证该监听器不会导致内存泄漏。在移除监听器时,通过removeIf()方法来匹配弱引用是否已经被回收,并且判断是否与指定的监听器相同。在触发事件时,我们通过get()方法获取弱引用所引用的对象,并判断是否为null,如果不为null,则执行监听器的perform()方法。


优缺点分析


优点



  1. 可以有效地降低内存占用;

  2. 适用于一些生命周期较短的对象,可以避免内存泄漏;

  3. 使用方便,只需要将对象包装为弱引用即可。


缺点



  1. 对象可能被提前回收,这可能会导致某些操作失败;

  2. 弱引用需要额外的开销,会对程序的性能产生一定的影响。


类代码方法介绍


WeakReference类


构造方法


public WeakReference(T referent);
public WeakReference(T referent, ReferenceQueue<? super T> q);

其中,第一个构造方法是无参构造方法,直接使用该方法会创建一个没有关联队列的弱引用。第二个构造方法需要传入一个ReferenceQueue队列,用于关联该弱引用。在目标对象被回收时,该队列会触发一个通知。


get()方法


public T get();

该方法用于获取弱引用所包装的对象,如果对象已经被回收,则返回null。


ReferenceQueue类


构造方法


public ReferenceQueue();

无参构造方法,直接使用该方法可以创建一个新的ReferenceQueue对象。


poll()方法


public Reference<? extends T> poll();

该方法用于获取ReferenceQueue队列中的下一个元素,如果队列为空,则返回null。


测试用例


测试代码演示


package com.example.javase.se.classes.weakReference;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* @Author ms
* @Date 2023-11-05 21:43
*/

public class WeakReferenceTest {

public static void main(String[] args) throws InterruptedException {
testWeakReference();
testCache();
testButton();
}

public static void testWeakReference() throws InterruptedException {
User user = new User("123", "Tom");
WeakReference<User> weakReference = new WeakReference<>(user);
user = null;
System.gc();
Thread.sleep(1000);
assert weakReference.get() == null;
}

public static void testCache() throws InterruptedException {
User user = new User("123", "Tom");
Map<String, WeakReference<User>> cache = new HashMap<>();
cache.put(user.getId(), new WeakReference<>(user));
user = null;
System.gc();
Thread.sleep(1000);
assert cache.get("123").get() == null;
}

public static void testButton() {
Button button = new Button();
ActionListener listener1 = new ActionListener();
ActionListener listener2 = new ActionListener();
button.addActionListener(listener1);
button.addActionListener(listener2);
button.click();
listener1 = null;
listener2 = null;
System.gc();
assert button.getListeners().get(0).get() == null;
assert button.getListeners().get(1).get() == null;
button.click();
}

static class User {
private String id;
private String name;

public User(String id, String name) {
this.id = id;
this.name = name;
}

public String getId() {
return id;
}

public String getName() {
return name;
}
}

static class ActionListener {
public void perform() {
System.out.println("Button clicked");
}
}

static class Button {
private List<WeakReference<ActionListener>> listeners = new ArrayList<>();

public void addActionListener(ActionListener listener) {
listeners.add(new WeakReference<>(listener));
}

public void click() {
for (WeakReference<ActionListener> ref : listeners) {
ActionListener listener = ref.get();
if (listener != null) {
listener.perform();
}
}
}

public List<WeakReference<ActionListener>> getListeners() {
return listeners;
}
}
}

测试结果


  根据如上测试用例,本地测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加更多的测试数据或测试方法,进行熟练学习以此加深理解。


在这里插入图片描述


测试代码分析


  根据如上测试用例,在此我给大家进行深入详细的解读一下测试代码,以便于更多的同学能够理解并加深印象。


此代码演示了 Java 中弱引用的使用场景,以及如何使用弱引用来实现缓存和事件监听器等功能。主要包括以下内容:


1.测试弱引用:定义一个 User 类,通过 WeakReference 弱引用来持有此对象,并在程序运行时将 User 对象设为 null,通过 System.gc() 手动触发 GC,验证弱引用是否被回收。


2.测试缓存:定义一个 Map 对象,将 User 对象通过 WeakReference 弱引用的形式存入,保留 User 对象的 ID,在后续程序运行时手动触发 GC,验证弱引用是否被回收。


3.测试事件监听器:定义一个 Button 类,通过 List<WeakReference> 弱引用来持有 ActionListener 对象,定义一个 addActionListener 方法,用于向 List 中添加 ActionListener 对象,定义一个 click 方法,用于触发 ActionListener 中的 perform 方法。在测试中,向 Button 中添加两个 ActionListener 对象,将它们设为 null,通过 System.gc() 手动触发 GC,验证弱引用是否被回收。


总的来说,弱引用主要用于缓存、事件监听器等场景,可以避免内存泄漏问题,但需要注意使用时的一些问题,比如弱引用被回收后,需要手动进行相应的处理等。


全文小结


本文介绍了Java中弱引用的概念和使用方法,通过源代码解析和应用场景案例的分析,详细阐述了弱引用的优缺点以及适用的场景。同时,也给出了类代码方法介绍和测试用例,最后进行了全文小结和总结。


总结


本文介绍了Java中弱引用的概念和使用方法,弱引用是一种较为特殊的引用类型,与普通引用类型不同的是,当一个对象只被弱引用所引用时,即使该对象仍然在内存中存在,也可能被垃圾回收器回收。


弱引用主要适用于一些生命周期较短的对象,可以有效地降低内存占用。同时,在一些需要监听器、缓存等场景中,使用弱引用可以避免内存泄漏。


在使用弱引用时,我们可以使用WeakReference类来实现,并通过get()方法获取弱引用所包装的对象。同时,我们也可以使用ReferenceQueue类来关联弱引用,当目标对象被回收时,该队列会触发一个通知。


但是弱引用也有其缺点,例如对象可能被提前回收,这可能会导致某些操作失败,同时弱引用也需要额外的开销,会对程序的性能产生一定的影响。


因此,在使用弱引用时,我们需要根据具体场景具体分析,权衡其优缺点,选择合适的引用类型来进行内存管理。


... ...


文末


好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。


... ...


学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!


wished for you successed !!!




⭐️若喜欢我,就请关注我叭。


⭐️若对您有用,就请点赞叭。


⭐️若有疑问,就请评论留言告诉我叭。


作者:喵手
来源:juejin.cn/post/7299659033970851875
收起阅读 »

做好人的意义是什么?

今天看抖音上“三根葱”演绎了一个片段: 老张指使老王干活,别人问为啥老让他干,他说:谁让他听话呢? 老李骂老王磨磨蹭蹭,别人问为啥不骂老张,他说:老张脾气大,不好惹! 最后总结,懂事的会承担更多,脾气好的都在受气,照顾人的没人照顾。 看到这里,我多少有点共...
继续阅读 »

今天看抖音上“三根葱”演绎了一个片段:



老张指使老王干活,别人问为啥老让他干,他说:谁让他听话呢?


老李骂老王磨磨蹭蹭,别人问为啥不骂老张,他说:老张脾气大,不好惹!


最后总结,懂事的会承担更多,脾气好的都在受气,照顾人的没人照顾



看到这里,我多少有点共鸣。


我想到,我经历过两类不同的领导。


第一种,善用权衡之术,有时特意挑起内部斗争。比如挑起产品部和技术部对立,加剧技术部和市场部不和。一开始,我还不明白,他为什么要破坏团结


后来,我的技术领导给了我答案:



我们和产品部吵架,要找人评理,这个裁判就是他(副总),他会站在谁那边呢?这全看我们平时的表现。我们比产品部更讨好他,他就会偏向我们。下一次,产品部让他更满意了,他又会站在产品部那边。



这种权衡之术,是公开的。但是,只要你还有欲有求,你就无法破解。帮派越多,各方越对立,领导就越如帝王般尊贵。这一套,封建社会玩了几千年。看到这里,估计你不会再骂那些“昏君”了,因为他们比你清亮。


我还有一个领导。他从不挑起各部门的对立。反而,他默默地维持和谐,调和职能部门各司其职。这一点,我也能看出来。因为,如果产品部对技术部有意见,他不但不会煽风点火、添油加醋,而且都不说是产品部提的意见,他说是自己的想法。


结果呢,如同当年“不知尧舜是吾君”一样,老百姓说:我们自己过得好,和皇帝什么关系?真格的,我们皇帝是谁啊?


员工也觉得,我们部门间完全可以自驱运转。那位领导如不存在一般,不受敬畏,员工都不向他谄媚


在我们传统的观念里,会将挑事者定义为小人,将默默行善者定义为好人。


我们从小受的教育是要当个好人。然而,长大了,我们发现,当个小人却能少受罪,更容易获得世俗眼里的成功。



想要赢了竞争,设局毁了对手效果最快。


想要笼络人心,搬弄是非,一起诋毁对方,能够快速团结一批人。



你看,利用别人的人风生水起,成就别人的人却穷困潦倒。


既然做好人没有好结果,那么当好人的意义又是什么呢?


说实话,我也一度迷茫、动摇,甚至否定。


我想,历史长河中,有没有人和我有同样的疑惑,但是他却找到了答案,而且还流传了下来,并且还恰好被我能看到。


于是,我就去古籍中找。我喜欢去找古籍,解读古籍。最好只看文言文,别有翻译。因为有时候,你读不懂,读不懂又没有翻译,你就会自己去脑补。看白纸是脑补不出来的,有那么零星几个能看懂的字,其实是古人给你一个引子,其他是你自己想出来的。你想出来的答案,肯定合乎你的逻辑,你不会抵触,反而正好解决了你的困惑。你又不是想让答案变成金条,你不就是要一个能通顺的解释吗?


这个疑惑,我在《幽梦影》中找到了答案:



黑与白交,黑能污白,白不能掩黑。此君子小人相攻之大势也。


然人必喜白而恶黑,此又君子必胜小人之理也。



把君子比做白色,小人比作比黑色。黑色很容易就能污染白色,但是多少白色都很难掩盖住一点黑色


从这里看,君子是干不过小人。因为,小人的势力很大。


但是下一句神奇的事情来了,你喜欢君子?还是喜欢小人?


人们肯定是喜欢君子讨厌小人。谁又愿意和尔虞我诈的人一起共事呢?从这个角度看,“君子又必胜小人”。


现实中,大家虽然对小人前呼后拥,但是每个人心里却是向往光明的,选择是无奈的。


很多坚持当好人的人。并非不知道当小人的套路,只是坚守一份志向罢了。


那么,做好人的意义就有了:在这个纷纷扰扰的世态下,公序良俗依然是人们美好的向往。多数普通人只是向往,而好人却已经做到了。难道这不是碾压式的胜利吗?


至于代价,都是每个人自己选的。


作者:TF男孩
来源:juejin.cn/post/7175306788995072056
收起阅读 »

码农如何提高自己的品味

作者:京东科技 文涛 前言 软件研发工程师俗称程序员经常对业界外的人自谦作码农,一来给自己不菲的收入找个不错的说辞(像农民伯伯那样辛勤耕耘挣来的血汗钱),二来也是自嘲这个行业确实辛苦,辛苦得没时间捯饬,甚至没有驼背、脱发加持都说不过去。不过时间久了,行外人还真...
继续阅读 »

作者:京东科技 文涛


前言


软件研发工程师俗称程序员经常对业界外的人自谦作码农,一来给自己不菲的收入找个不错的说辞(像农民伯伯那样辛勤耕耘挣来的血汗钱),二来也是自嘲这个行业确实辛苦,辛苦得没时间捯饬,甚至没有驼背、脱发加持都说不过去。不过时间久了,行外人还真就相信了程序员就是一帮没品味,木讷的low货,大部分的文艺作品中也都是这么表现程序员的。可是我今天要说一下我的感受,编程是个艺术活,程序员是最聪明的一群人,我们的品味也可以像艺术家一样。


言归正转,你是不是以为我今天要教你穿搭?不不不,这依然是一篇技术文章,想学穿搭女士学陈舒婷(《狂飙》中的大嫂),男士找陈舒婷那样的女朋友就好了。笔者今天教你怎样有“品味”的写代码。



以下几点可提升“品味”


说明:以下是笔者的经验之谈具有部分主观性,不赞同的欢迎拍砖,要想体系化提升编码功底建议读《XX公司Java编码规范》、《Effective Java》、《代码整洁之道》。以下几点部分具有通用性,部分仅限于java语言,其它语言的同学绕过即可。


优雅防重


关于成体系的防重讲解,笔者之后打算写一篇文章介绍,今天只讲一种优雅的方式:


如果你的业务场景满足以下两个条件:


1 业务接口重复调用的概率不是很高


2 入参有明确业务主键如:订单ID,商品ID,文章ID,运单ID等


在这种场景下,非常适合乐观防重,思路就是代码处理不主动做防重,只在监测到重复提交后做相应处理。


如何监测到重复提交呢?MySQL唯一索引 + org.springframework.dao.DuplicateKeyException


代码如下:


public int createContent(ContentOverviewEntity contentEntity) {
try{
return contentOverviewRepository.createContent(contentEntity);
}catch (DuplicateKeyException dke){
log.warn("repeat content:{}",contentEntity.toString());
}
return 0;
}

用好lambda表达式


lambda表达式已经是一个老生常谈的话题了,笔者认为,初级程序员向中级进阶的必经之路就是攻克lambda表达式,lambda表达式和面向对象编程是两个编程理念,《架构整洁之道》里曾提到有三种编程范式,结构化编程(面向过程编程)、面向对象编程、函数式编程。初次接触lambda表达式肯定特别不适应,但如果熟悉以后你将打开一个编程方式的新思路。本文不讲lambda,只讲如下例子:


比如你想把一个二维表数据进行分组,可采用以下一行代码实现


List<ActionAggregation> actAggs = ....
Map<String, List<ActionAggregation>> collect =
actAggs.stream()
.collect(Collectors.groupingBy(ActionAggregation :: containWoNosStr,LinkedHashMap::new,Collectors.toList()));

用好卫语句


各个大场的JAVA编程规范里基本都有这条建议,但我见过的代码里,把它用好的不多,卫语句对提升代码的可维护性有着很大的作用,想像一下,在一个10层if 缩进的接口里找代码逻辑是一件多么痛苦的事情,有人说,哪有10层的缩进啊,别说,笔者还真的在一个微服务里的一个核心接口看到了这种代码,该接口被过多的人接手导致了这样的局面。系统接手人过多以后,代码腐化的速度超出你的想像。


下面举例说明:


没有用卫语句的代码,很多层缩进


if (title.equals(newTitle)){
if (...) {
if (...) {
if (...) {

}
}else{

}
}else{

}
}

使用了卫语句的代码,缩进很少


if (!title.equals(newTitle)) {
return xxx;
}
if (...) {
return xxx;
}else{
return yyy;
}
if (...) {
return zzz;
}

避免双重循环


简单说双重循环会将代码逻辑的时间复杂度扩大至O(n^2)


如果有按key匹配两个列表的场景建议使用以下方式:


1 将列表1 进行map化


2 循环列表2,从map中获取值


代码示例如下:


List<WorkOrderChain> allPre = ...
List<WorkOrderChain> chains = ...
Map<String, WorkOrderChain> preMap = allPre.stream().collect(Collectors.toMap(WorkOrderChain::getWoNext, item -> item,(v1, v2)->v1));
chains.forEach(item->{
WorkOrderChain preWo = preMap.get(item.getWoNo());
if (preWo!=null){
item.setIsHead(1);
}else{
item.setIsHead(0);
}
});

@see @link来设计RPC的API


程序员们还经常自嘲的几个词有:API工程师,中间件装配工等,既然咱平时写API写的比较多,那种就把它写到极致**@see @link**的作用是让使用方可以方便的链接到枚举类型的对象上,方便阅读


示例如下:


@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ContentProcessDto implements Serializable {
/**
* 内容ID
*/

private String contentId;
/**
* @see com.jd.jr.community.common.enums.ContentTypeEnum
*/

private Integer contentType;
/**
* @see com.jd.jr.community.common.enums.ContentQualityGradeEnum
*/

private Integer qualityGrade;
}

日志打印避免只打整个参数


研发经常为了省事,直接将入参这样打印


log.info("operateRelationParam:{}", JSONObject.toJSONString(request));

该日志进了日志系统后,研发在搜索日志的时候,很难根据业务主键排查问题


如果改进成以下方式,便可方便的进行日志搜索


log.info("operateRelationParam,id:{},req:{}", request.getId(),JSONObject.toJSONString(request));

如上:只需要全词匹配“operateRelationParam,id:111”,即可找到业务主键111的业务日志。


用异常捕获替代方法参数传递


我们经常面对的一种情况是:从子方法中获取返回的值来标识程序接下来的走向,这种方式笔者认为不够优雅。


举例:以下代码paramCheck和deleteContent方法,返回了这两个方法的执行结果,调用方通过返回结果判断程序走向


public RpcResult<String> deleteContent(ContentOptDto contentOptDto) {
log.info("deleteContentParam:{}", contentOptDto.toString());
try{
RpcResult<?> paramCheckRet = this.paramCheck(contentOptDto);
if (paramCheckRet.isSgmFail()){
return RpcResult.getSgmFail("非法参数:"+paramCheckRet.getMsg());
}
ContentOverviewEntity contentEntity = DozerMapperUtil.map(contentOptDto,ContentOverviewEntity.class);
RpcResult<?> delRet = contentEventHandleAbility.deleteContent(contentEntity);
if (delRet.isSgmFail()){
return RpcResult.getSgmFail("业务处理异常:"+delRet.getMsg());
}
}catch (Exception e){
log.error("deleteContent exception:",e);
return RpcResult.getSgmFail("内部处理错误");
}
return RpcResult.getSgmSuccess();
}



我们可以通过自定义异常的方式解决:子方法抛出不同的异常,调用方catch不同异常以便进行不同逻辑的处理,这样调用方特别清爽,不必做返回结果判断


代码示例如下:


public RpcResult<String> deleteContent(ContentOptDto contentOptDto) {
log.info("deleteContentParam:{}", contentOptDto.toString());
try{
this.paramCheck(contentOptDto);
ContentOverviewEntity contentEntity = DozerMapperUtil.map(contentOptDto,ContentOverviewEntity.class);
contentEventHandleAbility.deleteContent(contentEntity);
}catch(IllegalStateException pe){
log.error("deleteContentParam error:"+pe.getMessage(),pe);
return RpcResult.getSgmFail("非法参数:"+pe.getMessage());
}catch(BusinessException be){
log.error("deleteContentBusiness error:"+be.getMessage(),be);
return RpcResult.getSgmFail("业务处理异常:"+be.getMessage());
}catch (Exception e){
log.error("deleteContent exception:",e);
return RpcResult.getSgmFail("内部处理错误");
}
return RpcResult.getSgmSuccess();
}

自定义SpringBoot的Banner


别再让你的Spring Boot启动banner千篇一律,spring 支持自定义banner,该技能对业务功能实现没任何卵用,但会给枯燥的编程生活添加一点乐趣。


以下是官方文档的说明: docs.spring.io/spring-boot…


另外你还需要ASCII艺术字生成工具: tools.kalvinbg.cn/txt/ascii


效果如下:


   _ _                   _                     _                 _       
(_|_)_ __ __ _ __| | ___ _ __ __ _ | |__ ___ ___ | |_ ___
| | | '_ \ / _` | / _` |/ _ \| '_ \ / _` | | '_ \ / _ \ / _ \| __/ __|
| | | | | | (_| | | (_| | (_) | | | | (_| | | |_) | (_) | (_) | |_\__ \
_/ |_|_| |_|\__, | \__,_|\___/|_| |_|\__, | |_.__/ \___/ \___/ \__|___/
|__/ |___/ |___/

多用Java语法糖


编程语言中java的语法是相对繁琐的,用过golang的或scala的人感觉特别明显。java提供了10多种语法糖,写代码常使用语法糖,给人一种 “这哥们java用得通透” 的感觉。


举例:try-with-resource语法,当一个外部资源的句柄对象实现了AutoCloseable接口,JDK7中便可以利用try-with-resource语法更优雅的关闭资源,消除板式代码。


try (FileInputStream inputStream = new FileInputStream(new File("test"))) {
System.out.println(inputStream.read());
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}

利用链式编程


链式编程,也叫级联式编程,调用对象的函数时返回一个this对象指向对象本身,达到链式效果,可以级联调用。链式编程的优点是:编程性强、可读性强、代码简洁。


举例:假如觉得官方提供的容器不够方便,可以自定义,代码如下,但更建议使用开源的经过验证的类库如guava包中的工具类


/**
链式map
*/

public class ChainMap<K,V> {
private Map<K,V> innerMap = new HashMap<>();
public V get(K key) {
return innerMap.get(key);
}

public ChainMap<K,V> chainPut(K key, V value) {
innerMap.put(key, value);
return this;
}

public static void main(String[] args) {
ChainMap<String,Object> chainMap = new ChainMap<>();
chainMap.chainPut("a","1")
.chainPut("b","2")
.chainPut("c","3");
}
}

未完,待续,欢迎评论区补充


作者:京东云开发者
来源:juejin.cn/post/7197604280705908793
收起阅读 »

DDD学习与感悟——总是觉得自己在CRUD怎么办?

一、DDD是什么? DDD全名叫做Dominos drives Design;领域驱动设计。再说的通俗一点就是:通过领域建模的方式来实现软件设计。 问题来了:什么是软件设计?为什么要进行软件设计? 软件开发最主要的目的就是:解决一个问题(业务)而产生的一个交付...
继续阅读 »

一、DDD是什么?


DDD全名叫做Dominos drives Design;领域驱动设计。再说的通俗一点就是:通过领域建模的方式来实现软件设计。


问题来了:什么是软件设计?为什么要进行软件设计?


软件开发最主要的目的就是:解决一个问题(业务)而产生的一个交付物(系统)。而软件设计旨在高效的实现复杂项目软件。也就是说软件设计是从业务到系统之间的桥梁。


而DDD则是在复杂业务场景下一种更高效更合理的软件设计思维方式和方法论。


二、以前的软件设计思维是什么?


绝大部分从事软件开发的人,不管是在学校还是刚开始工作,都是从ER图开始。即直接通过业务设计数据库模型和数据关联关系。这种思维根深蒂固的印在了这些人的头脑里(包括我自己)。因此在软件设计过程中习惯性的直接将业务转化为数据模型,面向数据开发。也就是我们所说的CRUD。我们有时候也会看到一些博客看到或者听到一些同事在说:这个业务有什么难的,不就是CRUD么?


不可否认的是,在软件生命周期初期,通过CRUD这种方式我们可以快速的实现业务规则,交付项目。然而一个系统的生命周期是很长的并且维护阶段的生命周期占绝大部分比例。
随着业务的发展,业务规则越来越复杂,通过CRUD这种粗暴方式,让工程代码越来越复杂,通常一个方法可能会出现几百甚至上千行代码,各种胶水代码和业务逻辑混合在一起,导致很难理解。



这种系统交接给另一个同学或者新进来的同学后,可能需要花费很长的时间才能理解这个方法,原因就是因为这种胶水代码淹没了业务核心规则。所以在现实场景中,我们经常会听到,上一个开发是SB,或者自嘲自己是在屎山上面继续堆屎。



三、DDD思想下的软件设计


DDD的思想是基于领域模型来实现软件设计。那么,什么是领域模型?领域模型怎么得来呢?


DDD思想,将软件的复杂程度提前到了设计阶段。基于DDD思想,我们的设计方式完全变了。


统一语言


首先,将业务方、领域专家以及相关的产研人员都聚拢在一起,共同探讨出业务场景和要解决的问题,统一语言。来确保所有人对于业务的理解都是一致的。



这里的统一语言不是指某种具体的技术语言,而是一种业务规则语言。所有人必须要能够理解这种统一语言。



战略设计


其次,我们根据待解决的问题空间,进行战略设计。所谓的战略设计就是根据问题空间在宏观层面识别出限界上下文。比如说一个电商业务,我们需要交付一个电商系统,根据电商业务的特点,需要划分出用户、商品、订单、仓储等限界上下文,每一个限界上下文都是一个独立的业务单元,具有完整的业务规则。


识别领域模型


然后,再分别针对上下文内的业务领域进行建模,得到领域模型。在DDD思想中,领域模型中通常包含实体、值对象、事件、领域服务等概念。我们可以通过“事件风暴”的方式来识别出这些概念。



注意,“事件风暴”和“头脑风暴”是有区别的。“头脑风暴”的主要目的是通过发散思维进行创新,而“事件风暴”是DDD中的概念,其主要目的是所有人一起根据统一语言和业务规则识别出事件。再根据事件识别出实体、值对象、领域服务、指令、业务流等领域模型中的概念。




所谓事件指的是已经发生了的事情。比如用户下了一个订单、用户取消了订单、用户支付了订单等



根据事件,我们可以识别出实体,比如上面这个例子中的订单实体,以及指令:取消、支付、下单等。


程序设计


识别出领域模型之后,我们就可以根据领域模型来指导我们进行程序设计了。这里的程序设计包括业务架构、数据架构、核心业务流程、系统架构、部署架构等。需要注意的是,在进行程序设计时,我们依然要遵循DDD中的设计规范。否则很容易走偏方向。


编写代码


有了完整的程序设计之后,我们就可以进行实际的工程搭建以及代码编写了。


这个阶段需要注意的是,我们需要遵循DDD思想中的架构设计和代码设计。实际上这个阶段也是非常困难的。因为基于DDD思想下的工程架构和我们传统的工程架构不一样。



基于DDD思想下,编码过程中我们经常会遇到的一个问题是:这个代码应该放在哪里合适。



工程结构


在DDD中,标准的工程结构分为4层。用户接口层、应用层、领域层和基础设施层。


截屏2023-06-22 18.00.33.png

DDD中,构建软件结构思维有六边形架构、CQRS架构等,它们是一种思想,是从逻辑层面对工程结构进行划分,而我们熟知的SOA架构以及微服务架构是从物理逻辑层面对工程结构进行划分,它们有着本质的区别,但是目标都是一样的:构建可维护、可扩展、可测试的软件系统。


代码编写


在DDD中,最为复杂的便是领域层,所有的业务逻辑和规则都在这里实现。因此我们经常会遇到一个问题就是代码应该放在哪里。


在具体落地过程中会遇到这些问题,解决这些问题没有银弹,因为不同的业务有不同的处理方式,这个时候我们需要与领域专家们讨论,得出大家都满意的处理方案。


代码重构


没有不变的业务。因此我们需要结合业务的发展而不断迭代更新我们的领域模型,通过重构的方式来挖掘隐形概念,再根据这些隐形概念去不断的调整我们的战略设计以及领域模型。使得整个软件系统的发展也是螺旋式迭代更新的过程。


通过以上的介绍,我们实现DDD的过程如下:


截屏2023-06-22 18.14.26.png


四、总结


通过对于DDD的理解,其实不难发现,程序员的工作重心变了,程序员其实不是在编写代码,而是在不断的摸索业务领域知识,尤其是复杂业务。


所以如果你总是觉得自己在CRUD,有可能不是你做的业务没价值,而是自己对于业务的理解还不够深;如果你总是沉迷于代码编写,可能你的发展空间就会受限了。


作者:浪漫先生
来源:juejin.cn/post/7299741943441457192
收起阅读 »

没用过微服务?别慌,丐版架构图,让你轻松拿捏面试官

大家好,我是哪吒。 很多人都说现在是云原生、大模型的时代,微服务已经过时了,但现实的是,很多人开发多年,都没有在实际的开发中用过微服务,更别提搭建微服务框架和技术选型了。 面试的时候都会问,怎么办? 今天分享一张微服务的丐版架构图,让你可以和面试官掰扯掰扯~ ...
继续阅读 »

大家好,我是哪吒。


很多人都说现在是云原生、大模型的时代,微服务已经过时了,但现实的是,很多人开发多年,都没有在实际的开发中用过微服务,更别提搭建微服务框架和技术选型了。


面试的时候都会问,怎么办?


今天分享一张微服务的丐版架构图,让你可以和面试官掰扯掰扯~


脑中有图,口若悬河,一套组合拳下来,面试官只能拍案叫好,大呼快哉,HR更是惊呼,我勒个乖乖,完全听不懂。


话不多说,直接上图。



由此可见,Spring Cloud微服务架构是由多个组件一起组成的,各个组件的交互流程如下。



  1. 浏览器通过查询DNS服务器,获取可用的服务实例的网络位置信息,从而实现服务的自动发现和动态更新;

  2. 通过CDN获取静态资源,提高访问速度,解决跨地域请求速度慢的问题;

  3. 通过LVS负载均衡器,实现负载均衡和网络协议;

  4. 通过Nginx反向代理服务器,将请求转发到gateway做路由转发和安全验证​;

  5. 访问注册中心和​配置中心Nacos,获取后端服务和配置项;

  6. 通过Sentinel进行限流;

  7. 通过Redis进行缓存服务、会话管理、分布式锁控制;

  8. 通过Elasticsearch进行全文搜索,存储日志,配合Kibana,对ES中的数据进行实时的可视化分析​。


一、域名系统DNS


在微服务中,域名系统DNS的作用主要是进行服务发现和负载均衡。



  1. 每个微服务实例在启动时,将自己的IP地址和端口号等信息注册到DNS服务器,浏览器通过查询DNS服务器,获取可用的服务实例的网络位置信息,从而实现服务的自动发现和动态更新。

  2. DNS服务器可以根据一定的策略,比如轮询、随机等,将请求分发到不同的负载均衡器LVS上,提高系统的并发处理能力和容错性。


二、LVS(Linux Virtual Server),Linux虚拟服务器


LVS是一个开源的负载均衡软件,基于Linux操作系统实现。它在Linux内核中实现负载均衡的功能,通过运行在用户空间的用户进程实现负载均衡的策略。



  1. LVS支持多种负载均衡算法,例如轮询、随机、加权轮询、加权随机等。

  2. LVS支持多种网络协议,例如TCP、HTTP、HTTPS,可以满足不同应用的需求。

  3. LVS具有高可用和可扩展性。它支持主从备份和冗余配置,当主服务器出现故障时,备份服务器可以自动接管负载,确保服务的连续性。此外,LVS还支持动态添加和删除服务器节点,方便管理员进行扩容和缩容的操作。


三、CDN静态资源


CDN静态资源图片、视频、JavaScript文件、CSS文件、静态HTML文件等。这些静态资源的特点是读请求量极大,对访问速度的要求很高,并占据了很高的宽带。如果处理不当,可能导致访问速度慢,宽带被占满,进而影响动态请求的处理。


CDN的作用是将这些静态资源分发到多个地理位置的机房的服务器上。让用户就近选择访问,提高访问速度,解决跨地域请求速度慢的问题。


四、Nginx反向代理服务器


1、Nginx的主要作用体现在以下几个方面:



  1. 反向代理,Nginx可以作为反向代理服务器,接收来自客户端的请求,然后将请求转发到后端的微服务实例。

  2. 负载均衡,Nginx可以根据配置,将请求分发到微服务不同的实例上,实现负载均衡。

  3. 服务路由,Nginx可以根据不同的路径规则,将请求路由到不同的微服务上。

  4. 静态资源服务,Nginx可以提供静态资源服务,如图片、视频、JavaScript文件、CSS文件、HTML静态文件等,减轻后端服务的压力,提高系统的响应速度和性能。


2、Nginx静态资源服务和CDN静态资源服务,如何选择?


在选择Nginx静态资源服务和CDN静态资源服务时,可以根据以下几个因素进行权衡和选择:



  1. 性能和速度:CDN静态资源服务通常具有更广泛的分布式节点和缓存机制,可以更快地响应用户的请求,并减少传输距离和网络拥塞。如果静态资源的加载速度和性能是首要考虑因素,CDN可能是更好的选择。

  2. 控制和自定义能力:Nginx静态资源服务提供更高的灵活性和控制能力,可以根据具体需求进行定制和配置。如果需要更精细的控制和自定义能力,或者在特定的网络环境下进行部署,Nginx可能更适合。

  3. 成本和预算:CDN静态资源服务通常需要支付额外的费用,而Nginx静态资源服务可以自行搭建和部署,成本相对较低。在考虑选择时,需要综合考虑成本和预算的因素。

  4. 内容分发和全球覆盖:如果静态资源需要分发到全球各地的用户,CDN静态资源服务的分布式节点可以更好地满足这个需求,提供更广泛的内容分发和全球覆盖。


选择Nginx静态资源服务还是CDN静态资源服务取决于具体的需求和场景。如果追求更好的性能和全球覆盖,可以选择CDN静态资源服务;如果更需要控制和自定义能力,且对性能要求不是特别高,可以选择Nginx静态资源服务。


五、Gateway网关


在微服务架构中,Gateway的作用如下:



  1. 统一入口:Gateway作为整个微服务架构的统一入口,所有的请求都会经过Gateway,这样做可以隐藏内部微服务的细节,降低后台服务受攻击的概率;

  2. 路由和转发:Gateway根据请求的路径、参数等信息,将请求路由到相应的微服务实例。这样可以让服务解耦,让各个微服务可以独立的开发、测试、部署;

  3. 安全和认证:Gateway通常集成了身份验证和权限验证的功能,确保只有经过验证的请求才能访问微服务。Gateway还具备防爬虫、限流、熔断的功能;

  4. 协议转换:由于微服务架构中可以使用不同的技术和协议,Gateway可以作为协议转换中心,实现不同协议之间的转换和兼容性;

  5. 日志和监控,Gateway可以记录所有的请求和响应日志,为后续的故障排查、性能分析、安全审计提供数据支持。Gateway还集成了监控和报警功能:实时反馈系统的运行状态;

  6. 服务聚合:在某些场景中,Gateway可以将来自多个微服务的数据进行聚合,然后一次性返回给客户端,减少客户端和微服务之间的交互次数,提高系统性能;


六、注册中心Nacos


在微服务架构中,Nacos的作用主要体现在注册中心、配置中心、服务健康检查等方面。



  1. 注册中心:Nacos支持基于DNS和RPC的服务发现,微服务可以将接口服务注册到Nacos中,客户端通过nacos查找和调用这些服务实例。

  2. 配置中心:Nacos提供了动态配置服务,可以动态的修改配置中心中的配置项,不需要重启后台服务,即可完成配置的修改和发布,提高了系统的灵活性和可维护性。

  3. 服务健康检查:Nacos提供了一系列的服务治理功能,比如服务健康检查、负载均衡、容错处理等。服务健康检查可以阻止向不健康的主机或服务实例发送请求,保证了服务的稳定性和可靠性。负载均衡可以根据一定的策略,将请求分发到不同的服务实例中,提高系统的并发处理能力和性能。


七、Redis缓存


1、在微服务架构中,Redis的作用主要体现在以下几个方面:



  1. 缓存服务:Redis可以作为高速缓存服务器,将常用的数据存储在内存中,提高数据访问速度和响应时间,减轻数据库的访问压力,并加速后台数据的查询;

  2. 会话管理:Redis可以存储会话信息,并实现分布式会话管理。这使会话信息可以在多个服务之间共享和访问,提供一致的用户体验;

  3. 分布式锁:Redis提供了分布式锁机制,可以确保微服务中多个节点对共享资源的访问的合理性和有序性,避免竞态条件和资源冲突;

  4. 消息队列:Redis支持发布订阅模式和消息队列模式,可以作为消息中间件使用。微服务之间可以通过Redis实现异步通信,实现解耦和高可用性;


2、竞态条件


竞态条件是指在同一个程序的多线程访问同一个资源的情况下,如果对资源的访问顺序敏感,就存在竞态条件。


竞态条件可能会导致执行结果出现各种问题,例如计算机死机、出现非法操作提示并结束程序、错误的读取旧的数据或错误的写入新数据。在串行的内存和存储访问能防止这种情况,当读写命令同时发生的时候,默认是先执行读操作的。


竞态条件也可能在网络中出现,当两个用户同时试图访问同一个可用信道的时候就会发生,系统同意访问之前没有计算机能得到信道被占用的提示。统计上说这种情况通常是发生在有相当长的延迟时间的网络里,比如使用地球同步卫星。


为了防止这种竞态条件发生,需要制定优先级列表,比如用户的用户名在字母表里排列靠前可以得到相对较高的优先级。黑客可以利用竞态条件这一弱点来赢得非法访问网络的权利。


竞态条件是由于多个线程或多个进程同时访问共享资源而引发的问题,它可能会导致不可预测的结果和不一致的状态。解决竞态条件的方法包括使用锁、同步机制、优先级列表等。


3、Redis会话管理如何实现?


Redis会话管理的一般实现步骤:



  1. 会话创建:当用户首次访问应用时,可以在Redis中创建一个新的会话,会话可以是一个具有唯一标识符的数据结构,例如哈希表或字符串;

  2. 会话信息存储:将会话信息关联到会话ID存储到Redis中,会话信息可以包括用户身份、登录状态、权限等。

  3. 会话过期时间设置:为会话设置过期时间,以确保会话在一定时间后自动失效。Redis提供了设置键值对过期时间的机制,可以通过EXPIRE命令为会话设置过期时间;

  4. 会话访问和更新:在每次用户访问应用时,通过会话ID获取相应的会话信息,并对其进行验证和更新。如果会话已过期,可以要求用户重新登录;

  5. 会话销毁:当用户主动退出或会话到期后,需要销毁会话,通过删除Redis中存储的会话信息即可。


八、Elasticsearch全文搜索引擎


在微服务架构中,Elasticsearch全文搜索引擎的应用主要体现在如下几个方面:



  1. 全文搜索引擎:ES是一个分布式的全文搜索引擎,它可以对海量的数据进行实时的全文搜索,返回与关键词相关的结果;

  2. 分布式存储:ES提供了分布式的实时文件存储功能,每个字段都可以被索引并可被搜索,这使得数据在ES中的存储和查询都非常高效;

  3. 数据分析:配合Kibana,对ES中的数据进行实时的可视化分析,为数据决策提供数据支持;

  4. 日志和监控:ES可以作为日志和监控数据的存储和分析平台。通过收集系统的日志信息,存入ES,可以实现实时的日志查询、分析、告警、展示;

  5. 扩展性:ES具有很好的扩展性,可以水平扩展到数百台服务器,处理PB级别的数据,使得ES能够应对海量数据的挑战。


九、感觉Redis和Elasticsearch很像?微服务中Redis和Elasticsearch的区别



  1. 数据存储和查询方式:Redis是一种基于键值对的存储系统,它提供高性能的读写操作,适用于存储结构简单、查询条件同样简单的应用场景。而Elasticsearch是一个分布式搜索和分析引擎,适用于全文搜索、数据分析等复杂场景,能够处理更复杂的查询需求;

  2. 数据结构与处理能力:Redis支持丰富的数据结构,如字符串、哈希、列表、集合等,并提供了原子性的操作,适用于实现缓存、消息队列、计数器等功能。而Elasticsearch则是基于倒排索引的数据结构,提供了强大的搜索和分析能力。但相对于Redis,Elasticsearch的写入效率较低;

  3. 实时性和一致性:Redis提供了很高的实时性,Redis将数据存储到内存中,能够很快的进行读写操作;而Elasticsearch是一个近实时的搜索平台,实时性不如Redis;

  4. 扩展性:Redis是通过增加Redis实例的形式实现扩展,对非常大的数据集可能要进行数据分片;而Elasticsearch具有水平扩展的能力,可以通过添加更多的节点来提高系统的处理能力,适用于大量数据的场景;



作者:哪吒编程
来源:juejin.cn/post/7299357353543450636
收起阅读 »

token 和 cookie 还在傻傻分不清?

web
token 概念和作用 Token是一种用于身份验证和授权的令牌。在Web应用程序中,当用户进行登录或授权时,服务器会生成一个Token并将其发送给客户端。客户端在后续的请求中将Token作为身份凭证携带,以证明自己的身份。 Token可以是一个字符串,通常是...
继续阅读 »

token 概念和作用


Token是一种用于身份验证和授权的令牌。在Web应用程序中,当用户进行登录或授权时,服务器会生成一个Token并将其发送给客户端。客户端在后续的请求中将Token作为身份凭证携带,以证明自己的身份。


Token可以是一个字符串,通常是经过加密和签名的,以确保其安全性和完整性。服务器收到Token后,会对其进行解析和验证,以验证用户的身份并授权对特定资源的访问权限。


Token的使用具有以下特点:



  • 无状态:服务器不需要在数据库中存储会话信息,所有必要的信息都包含在Token中。

  • 可扩展性:Token可以存储更多的用户信息,甚至可以包含自定义的数据。

  • 安全性:Token可以使用加密算法进行签名,以确保数据的完整性和安全性。

  • 跨域支持:Token可以在跨域请求中通过在请求头中添加Authorization字段进行传递。


Token在前后端分离的架构中广泛应用,特别是在RESTful API的身份验证中常见。它比传统的基于Cookie的会话管理更灵活,并且适用于各种不同的客户端,例如Web、移动应用和第三方接入等。


cookie 和 token 的关系


Cookie和Token是两种不同的概念,但它们在身份验证和授权方面可以有关联。


Cookie是服务器在HTTP响应中通过Set-Cookie标头发送给客户端的一小段数据。客户端浏览器将Cookie保存在本地,然后在每次对该服务器的后续请求中将Cookie作为HTTP请求的一部分发送回服务器。Cookie通常用于在客户端和服务器之间维护会话状态,以及存储用户相关的信息。


Token是一种用于身份验证和授权的令牌。它是一个包含用户身份信息的字符串,通常是服务器生成并返回给客户端。客户端在后续的请求中将Token作为身份凭证发送给服务器,服务器通过验证Token的有效性来确认用户的身份和权限。


Cookie和Token可以结合使用来实现身份验证和授权机制。服务器可以将Token存储在Cookie中,然后发送给客户端保存。客户端在后续的请求中将Token作为Cookie发送给服务器。服务器通过验证Token的有效性来判断用户的身份和权限。这种方式称为基于Cookie的身份验证。另外,也可以将Token直接存储在请求的标头中,而不是在Cookie中进行传输,这种方式称为基于Token的身份验证。


需要注意的是,Token相对于Cookie来说更加灵活和安全,可以实现跨域身份验证,以及客户端和服务器的完全分离。而Cookie则受到一些限制,如跨域访问限制,以及容易受到XSS和CSRF攻击等。因此,在实现身份验证和授权机制时,可以选择使用Token替代或辅助Cookie。


token 一般在客户端存在哪儿


Token一般在客户端存在以下几个地方:



  • Cookie:Token可以存储在客户端的Cookie中。服务器在响应请求时,可以将Token作为一个Cookie发送给客户端,客户端在后续的请求中会自动将Token包含在请求的Cookie中发送给服务器。

  • Local Storage/Session Storage:Token也可以存储在客户端的Local Storage或Session Storage中。这些是HTML5提供的客户端存储机制,可以在浏览器中长期保存数据。

  • Web Storage API:除了Local Storage和Session Storage,Token也可以使用Web Storage API中的其他存储机制,比如IndexedDB、WebSQL等。

  • 请求头:Token也可以包含在客户端发送的请求头中,一般是在Authorization头中携带Token。


需要注意的是,无论将Token存储在哪个地方,都需要采取相应的安全措施,如HTTPS传输、加密存储等,以保护Token的安全性。


存放在 cookie 就安全了吗?


存放在Cookie中相对来说是比较常见的做法,但是并不是最安全的方式。存放在Cookie中的Token可能存在以下安全风险:



  • 跨站脚本攻击(XSS) :如果网站存在XSS漏洞,攻击者可以通过注入恶意脚本来获取用户的Cookie信息,包括Token。攻击者可以利用Token冒充用户进行恶意操作。

  • 跨站请求伪造(CSRF) :攻击者可以利用CSRF漏洞,诱使用户在已经登录的情况下访问恶意网站,该网站可能利用用户的Token发起伪造的请求,从而执行未经授权的操作。

  • 不可控的访问权限:将Token存放在Cookie中,意味着浏览器在每次请求中都会自动携带该Token。如果用户在使用公共计算机或共享设备时忘记退出登录,那么其他人可以通过使用同一个浏览器来访问用户的账户。


为了增加Token的安全性,可以采取以下措施:



  • 使用HttpOnly标识:将Cookie设置为HttpOnly,可以防止XSS攻击者通过脚本访问Cookie。

  • 使用Secure标识:将Cookie设置为Secure,只能在通过HTTPS协议传输时发送给服务器,避免明文传输。

  • 设置Token的过期时间:可以设置Token的过期时间,使得Token在一定时间后失效,减少被滥用的风险。

  • 使用其他存储方式:考虑将Token存储在其他地方,如Local Storage或Session Storage,并采取加密等额外的安全措施保护Token的安全性。


token 身份验证代码实现


服务端使用 JWT 进行 token 签名和下发


可以参考使用这个库 node-jsonwebtoken


后端代码示例 (Node.js / Express),代码简单实现如下:


const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

const secretKey = 'mysecretkey';

app.use(express.json());

app.post('/api/login', (req, res) => {
// 从请求中获取用户名和密码
const { username, password } = req.body;

// 验证用户名和密码
if (username === 'admin' && password === 'password') {
// 用户名和密码验证成功,生成Token并返回给前端
const token = jwt.sign({ username }, secretKey, { expiresIn: '1h' });
res.json({ token });
} else {
// 用户名和密码验证失败,返回错误信息给前端
res.status(401).json({ error: 'Authentication failed' });
}
});

app.get('/api/protected', verifyToken, (req, res) => {
// Token验证成功,可以访问受保护的路由
res.json({ message: 'Protected API endpoint' });
});

function verifyToken(req, res, next) {
const token = req.headers.authorization;

if (!token) {
return res.status(401).json({ error: 'Missing token' });
}

// 验证Token
jwt.verify(token, secretKey, (err, decoded) => {
if (err) {
return res.status(401).json({ error: 'Invalid token' });
}

// Token验证通过,将解码后的数据存储在请求中,以便后续使用
req.user = decoded;
next();
});
}

app.listen(3000, () => {
console.log('Server is running on port 3000');
});

在上述后端代码中,我们使用了jsonwebtoken库来生成和验证Token。在登录路由/api/login中,验证用户名和密码成功后,生成一个Token并返回给前端。在受保护的路由/api/protected中,我们使用verifyToken中间件来验证请求中的Token,只有通过验证的请求才能访问该路由。


当然实际开发中, 可以使用中间件来进行 jwt 的验证, 下发方式也因人而异, 可以放在 cookie 中, 也可以作为 response 返回均可, 上述代码仅作参考;


前端代码实现示范如下


前端获取到了Token后将其存储在Cookie中,并在后续请求中自动发送给后端,可以通过以下方式实现前端代码:


import React, { useState, useEffect } from 'react';

function App() {
const [token, setToken] = useState('');

useEffect(() => {
// 检查本地是否有保存的Token
const savedToken = localStorage.getItem('token');
if (savedToken) {
setToken(savedToken);
}
}, []);

const handleLogin = async () => {
// 发送请求到后端进行登录验证
const response = await fetch('http://example.com/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username: 'admin', password: 'password' }),
});

if (response.ok) {
// 登录成功,获取Token并保存到前端
const data = await response.json();
setToken(data.token);
// 保存Token到本地
localStorage.setItem('token', data.token);
}
};

const handleLogout = () => {
// 清除保存的Token
setToken('');
// 清除本地保存的Token
localStorage.removeItem('token');
};

return (
<div>
{token ? (
<div>
<p>Token: {token}</p>
<button onClick={handleLogout}>Logout</button>
</div>
) : (
<button onClick={handleLogin}>Login</button>
)}
</div>

);
}

export default App;

作者:晴小篆
来源:juejin.cn/post/7299731897626443785
收起阅读 »

3 个技巧,让你像技术专家一样解决编码问题

「我应该如何提高解决问题的能力?尽管我掌握了 JavaScript,却无法解决实际问题或理解复杂的 JavaScript 代码。」 经常有年轻的开发者朋友问我类似的问题。对开发者来说,解决问题非常重要。编写优秀的代码是一门创造性的艺术,而要成为出色的开发者,就...
继续阅读 »

「我应该如何提高解决问题的能力?尽管我掌握了 JavaScript,却无法解决实际问题或理解复杂的 JavaScript 代码。」


经常有年轻的开发者朋友问我类似的问题。对开发者来说,解决问题非常重要。编写优秀的代码是一门创造性的艺术,而要成为出色的开发者,就必须成为富有创造力的能够解决问题的人。



我发现新手程序员犯的最大的错误是专注于学习语法,而不是学习如何解决问题。—— V. Anton Spraul



尽管我观察到,解决问题的技能需要时间和经验的积累,但我坚信掌握它并不需要很多年;只要勇敢地直面问题,就会有所提高。我曾与许多初级开发人员一起工作,年轻人们也比他们的老伙计更善于解决问题。


本文将详细讲解三个技巧,让开发者可以像技术专家一样解决问题。重头戏开始之前,我们先来看看技术专家和技术小白在解决问题方面有哪些不同。


专家思维 vs 新手思维



大多数人会回避或胡乱处理问题。优秀的思考者和领导者则会主动寻找问题,他们也拥有能够更好地解决问题的方法。—— Michael Simmons



Robert J. Sternberg 教授根据美国心理学家 Herbert A. Simon、Robert Glaser 和 Micheline Chi 等人的研究,揭示了技术专家解决问题比技术小白更有效的秘密。



Arnaud Chevallier 在 Work forward in solving problems, not backward 一文中犀利写道,「逆向工作法是一种从假设出发的方法。如果想要提高利润率,逆向工作法会指引你去寻找增加收入的办法,因为增加收入可以带来更高的利润率。那减少成本呢?难道在确定最终的解决方案之前,我们不应该先全面地了解各种可以实现目标的办法吗?」


可以看到,技术专家通常会花大量的时间寻找、明确和定义问题,并且使用正向工作法解决问题,同时密切关注问题解决的过程。下面就跟大家分享,技术专家们常用的问题解决技巧。


三个专家级的问题解决技巧


01 问题十二连 The 12 What Elses


提问题听上去没什么难度,但要找到正确的、缺失的问题并准确地描述出来却不容易。Lenedra J. Carroll 介绍的「问题十二连 The 12 What Elses」可以有效帮助我们摆脱这个苦恼。


在头脑风暴时,先提出一个问题,并生成 12 个答案;然后选取其中一个答案转化为下一个问题,再生成 12 个答案。不断重复此过程,直到获得一个明确的解决方案。


通过连续地提问,我们会得到一个「问题回答地图」,它对假设的测试和结构化解决复杂问题很有帮助。


使用「问题回答地图」测试假设


提问和追问是如何将我们往正确方向上引导的?下面两个例子可能会给你答案。




02 根本原因分析法 Root Cause Analysis


我们经常在多次解决失败后,才发现问题的情况跟预期有所不同,所以在开始解决问题之前,就要先了解其根本原因是什么。


只有消除错误的选择,才能更好地定义问题并找到有效的解决方案。根本原因分析法有助于避免在错误的方向上浪费时间和精力。


根本原因分析的几种方法


根本原因分析法的示例


当需要修复 Bug 时,开发者可以使用以下任意方式,进行根本原因分析:



  • 确定问题在哪个环境出现,并尝试在相同和不同的环境中重现它,以掌握更好的理解。

  • 如果与 Web 性能有关,可以分析捆绑文件。

  • 进行单元测试和集成测试。

  • 进行日志文件分析。

  • 进行交互式调试。


03 使用多元思维 Spectrum Thinking


二元思维认为事情的状态是非黑即白的,只有互相对立的两种可能。有些时候它是正确的,但其他时候,它可能是一种错误的简化。


二元思维


与二元思维对应的是多元思维,也可以称作频谱思维(Spectrum Thinking)。它会考虑更多选择、更多替代方案和可能性,比如「两者共存」「介于两者之间」「其他的可能性」或「二者皆否」等。


频谱思维


通过培养多元思维,开发者可以有效提升创造力;你会惊讶地发现,修复 Bug、解决冲突、设计/执行客户需求的实现方案等居然会有这么多种解决方案和方式。


以展示信息详情为例,二元思维认为,信息详情要么通过弹窗展示,要么跳转到一个带返回箭头的新页面进行展示。




多元思维认为还有其他可能性,比如新增 Tab 页直接查看和更新信息,无需关闭当前列表页面。



多元思维还可能认为,可以提供一个支持三种布局的动态模板,让用户自主选择要用以上哪种方式。


二元思维和多元思维各有利弊,在实际工作中可以配合使用。


写在最后


解决问题能力是一个超出软件开发范畴的话题,它高度取决于我们的心态和态度。要想培养和提高解决(复杂)问题的能力,首先要对问题和挑战充满好奇心,而不是感到沮丧。


就像 Tim Hicks 说的那样,「问题就像赛车道上的弯道。处理得好,便可以在接下来的直道中状态满分;如果过弯太快,很可能会引发侧翻,影响后续赛程。」


(原文作者:Rakia Ben Sassi)




了解更多开发者提效、研发效能管理、前沿技术等消息,欢迎关注 LigaAI@稀土掘金


LigaAI 助力开发者扬帆远航,欢迎体验我们的产品,期待与你一路同行!


作者:LigaAI
来源:juejin.cn/post/7243592123803009083
收起阅读 »

看完还学不会正则,快来锤我!

web
前言 各位同仁在表单验证规则,或者在验证数据的时候,是不是经常都是xxx.xxxx,然后对数据处理来处理去,最后进行后续操作……,不仅费时费力,而且消耗精神,于是乎: ps(我):要不要用用正则,但是想不起来怎么写啊,百度搜一下吧,chatGpt搜一下,贴上去...
继续阅读 »

前言


各位同仁在表单验证规则,或者在验证数据的时候,是不是经常都是xxx.xxxx,然后对数据处理来处理去,最后进行后续操作……,不仅费时费力,而且消耗精神,于是乎:


ps(我):要不要用用正则,但是想不起来怎么写啊,百度搜一下吧,chatGpt搜一下,贴上去


我:欧哟?刚刚好啊,效果还怪好的嘞,哈哈哈,天助我也!


这还是好的情况,刚刚好符合,要是不好……


ps(我):服了,这正则不得行,chatGpt搜的错的,自己写吧又不会,找吧又没有……
我:乖乖写if else吧


又是啃哧啃哧,耗时耗力……可能大家伙都是差不多的哈,谁也别说谁(大佬除外)。本着多一事不如少一事的原则,直接学习一波!


js正则表达式


正则(正则表达式)是一种用于描述文本模式的工具,它通过使用特定的符号和语法规则来定义一个字符串的模式。正则表达式通常由各种字符和特殊元字符组成,用于进行字符串匹配、查找、替换和验证等操作。


使用正则表达式,可以执行以下操作:



  1. 模式匹配:正则表达式可以用于查找和匹配具有特定模式的字符串。通过定义一个模式,可以搜索和识别符合该模式的字符串。

  2. 字符串查找与替换:正则表达式可以用于在文本中进行字符串查找和替换。通过指定要查找或替换的模式,可以对目标字符串进行修改和处理。

  3. 数据验证:正则表达式可以用于验证用户输入或其他数据的格式和有效性。例如,可以使用正则表达式验证电子邮件地址、电话号码、日期等的格式是否符合预期。

  4. 文本提取:在文本处理中,可以使用正则表达式从大量文本数据中提取出所需的信息。例如,可以使用正则表达式从日志文件中提取特定的时间戳或关键字。

  5. 数据清洗与转换:使用正则表达式,可以进行文本数据的清洗和转换。可以根据模式匹配和替换规则,删除非法字符、规范化日期格式、提取关键信息等。


正则表达式提供了一种强大和灵活的文本处理工具,它被广泛应用于编程语言、文本编辑器、数据处理工具等各种软件中。虽然正则表达式的语法可能会显得复杂,但掌握它可以极大地提高对文本模式处理的能力。


应用


正则表达式在计算机科学和文本处理中具有广泛的应用。以下是一些常见的正则表达式应用:



  • 模式匹配:正则表达式可用于检测字符串是否与特定模式匹配。例如,可以使用正则表达式来验证电子邮件地址、检查电话号码的格式、识别日期等。

  • 字符串搜索与替换:正则表达式可以用于在文本中搜索特定的模式,并进行替换或提取。这对于在大量文本中进行批量操作非常有用,如查找和替换文本文件中的特定单词或短语。

  • 表单验证:在前端开发中,可以使用正则表达式验证用户输入的表单数据。例如,验证用户名是否只包含字母和数字、检查密码是否符合指定的复杂度要求等。

  • URL路由:许多Web框架使用正则表达式来解析URL路由和处理动态路由。它们通过正则表达式匹配URL字符串并将其映射到相应的处理程序或控制器。

  • 日志分析:使用正则表达式可以解析和提取日志文件中的有用信息。例如,可以使用正则表达式从服务器日志中提取IP地址、日期时间戳、错误消息等。

  • 数据清洗与转换:正则表达式可用于清洗和转换数据,如从多种格式的文本数据中提取特定字段、规范化日期格式、去除特殊字符等。

  • 编程工具与编辑器:许多编程工具和文本编辑器支持正则表达式搜索和替换功能。这使得开发人员能够更灵活地进行代码重构和批量修改操作。


创建正则


js 中内置了正则表达式对象 RegExp,我们要创建一个正则表达式,可以:



  • 第一种创建方式,接收patternmodifiers两个参数


// 构造函数RegExp
var regex = new RegExp(pattern, modifiers);
// 构造函数创建
var regex = new RegExp('xyz', 'i');
// 等价于 字面量创建
var regex = /xyz/i;

RegExp 构造函数接收两个参数,pattern 描述了表达式的模式,为字符串,modifiers 是正则表达式的修饰符,用于执行区分大小写和全局匹配。



  • 第二种创建方式,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。


var regex = new RegExp(/xyz/i);
// 等价于
var regex = /xyz/i;

在构造函数创建时 pattern 是正则字符串,字面量创建时,pattern 是一个类似 /正则规则/ 表达式,是放在双斜杠里的。
modifiers有三个值,分别为


描述
i不区分大小写。在匹配时忽略大小写
g全局匹配。查找所有可能的匹配,而不仅仅是第一个匹配。
s单行匹配。启用单行模式,使点号(.)可以匹配包括换行符在内的任意字符。
u启用完整的 Unicode 匹配支持,会正确处理四个字节的 UTF-16 编码。
m多行匹配。启用多行模式,^ 和 $ 可以匹配行的开始和结束。

例如,使用 ig 修饰符创建一个忽略大小写和全局匹配的正则表达式:


javascript
var pattern = new RegExp("pattern", "ig");

或者使用字面量表示法也可以传递修饰符:


var pattern = /pattern/ig;

正则对象的方法


正则表达式对象通常提供一组方法,用于在字符串上执行不同的操作。以下是一些常见的正则表达式对象的方法:




  1. test(string): 检测指定字符串是否与正则表达式匹配。返回一个布尔值,表示是否找到匹配项。一般开发当中,我们使用这种方法较多。


    const regex = /apple/;
    console.log(regex.test("I love apples")); // true
    console.log(regex.test("I prefer oranges")); // false



  2. exec(string): 在给定字符串中搜索匹配项,并返回一个数组或 null。数组包含有关匹配项的详细信息,如匹配的子字符串、捕获组等。


    const regex = /\d+/;
    console.log(regex.exec("I have 100 apples")); // ["100"]
    console.log(regex.exec("There are no numbers here")); // null



  3. match(regexp): 在字符串中查找与正则表达式匹配的内容,并返回一个数组或 null。类似于 exec() 方法,但是 match() 是在字符串上调用,而不是在正则表达式上调用。


    const string = "I have 100 apples";
    const regex = /\d+/;
    console.log(string.match(regex)); // ["100"]



  4. search(string): 在字符串中搜索与正则表达式匹配的内容,并返回匹配项的索引。如果没有找到匹配项,则返回 -1。


    const string = "I prefer oranges";
    const regex = /oranges/;
    console.log(string.search(regex)); // 8



  5. replace(regexp, replacement): 替换字符串中与正则表达式匹配的部分。可以将匹配项替换为指定的字符串或使用函数进行替换。


    const string = "I like cats and dogs";
    const regex = /cats/;
    const replacement = "birds";
    const newString = string.replace(regex, replacement);
    console.log(newString); // "I like birds and dogs"



  6. split(regexp): 将字符串分割为由正则表达式匹配的子字符串组成的数组。正则表达式定义了分隔符。


    const string = "apple,banana,orange";
    const regex = /,/;
    const parts = string.split(regex);
    console.log(parts); // ["apple", "banana", "orange"]



正则规则


分为基本字符匹配;元字符匹配,如\w;锚点匹配指定匹配发生的位置, 如^ 表示匹配行的开头;量词和限定符, 如*; 分组和捕获();零宽断言:正向肯定断言 (?=...):匹配满足断言条件的位置,但不会消耗字符;


接下来一一进行介绍。


基本字符匹配


匹配字面量字符/ /


如果想在javaScript当中直接匹配java,可以直接在我们的字面量当中写入想要匹配的值,即java直接进行匹配。


正则: /java/


可以匹配的不能匹配的
javascriptJavascript
javajaava

字符组[ ]


如果不仅仅想要匹配java还想要匹配Java,那光光/java/是不够的。这时候还需要用到我们的字符组。


正则:/[Jj]ava/


可以匹配的不能匹配的
javascriptjaava
Javascriptjvav

[]匹配规则当中,目标字符可以匹配中括号里面的任意一个字符即可,转为javaScript语言就是 ||的意思。观察两个目标字符串,java与Java的区别也仅仅是首字母不同,那么只需要兼容开头的大小字母即可。


拓展

若是想匹配java Java JAva,正则需要如何编写?通过观察各个字符当中的差别,即前两个字母的可能性都可能为大小写,便得出前两个位置的匹配使用字符组即可。


正则:/[J][Aa]va/


字符组区间 -


如果说只想匹配前缀为123,后面是二十六个字母当中任何一个的字符怎么办?


这简单,刚刚学完字符组,我直接一手/123[a,b,c,d....]/把二十六个字母全部列一遍,话虽如此,但大可不必!


此处若是可选匹配字母过多的话,可直接使用字符组区间连接


正则: /123[a-zA-Z]/


可以匹配的不能匹配的
123a123
123B12345

同时还可以匹配多个数字,比如我只想匹配[3-9]的数字,那么也可以使用连接符


正则123[3-9][a-zA-Z]


可以匹配的不能匹配的
1233a123a9
1236B123B

字符组取反:[^]


有的时候你可能也不想匹配某些字符,比如只晕小写字母,那么这个时候你可以对你所要匹配的字符组进行取反,那就匹配不到了。


正则:/[^a-z]/


可以匹配的不能匹配的
1233ABCDEabcde
12345678adasd
123adasdadasd


注意: 此处需要全部为小写字母test匹配结果才是false,若字符包含其他的字符,test的匹配结果仍然为true。



const pattern = /[^a-z]/ // 表示的意思为所有字符都不是小写
const string = '123adasd' // 此处还有数字
pattern.test(string) // true

元字符匹配


日常开发当中,元字符单独使用的情况并不多,更多的是跟随后续的量词一块使用,最终形成限定字符格式的正则。


单点 .


. 是一个特殊的元字符,可以用于匹配除了换行符 \n(或其他行终止符,如 \r\n)之外的任意单个字符。


正则:/./


可以匹配的不能匹配的
1\n(换行)
a\r(回车)

数字 \d


\d 可以匹配任意一个数字字符,包括 0 到 9 的数字。


字符 \w


用于匹配字母字符、数字和下划线。


具体来说,\w 匹配以下字符:



  • 小写字母(a-z)

  • 大写字母(A-Z)

  • 数字(0-9)

  • 下划线(_)


空白符 \s


用于匹配空白字符



  • 空格符(Space)

  • 制表符(\t)

  • 换行符(\n)

  • 回车符(\r)

  • 垂直制表符(\v)

  • 换页符(\f)



注意:如果说想要匹配正则当中的匹配规则符号,例如只想匹配单点字符.,则需要使用反斜杠进行转义,即/\./ 任何匹配正则当中具有意义的字符都需要进行转义。



量词


量词用于指定模式重复出现的次数。允许你匹配一定数量的字符或子模式,是正则当中见怪不怪的玩意。与上述字符相互搭配,能获得意想不到的结果。


量词 {}


用于匹配前面的字符或子表达式指定的精确的重复次数。


比如需要匹配重复多个字符,如需要匹配出现两次a的字符串。


正则:/a{2}/


可以匹配的不能匹配的
aaabab
aabbabb

但是我只知道会出现a字符,可能是两到三个呢?这个时候就可以使用区间来表示,囊括出现的次数。


正则:/a{2,3}/


可以匹配的不能匹配的
aaabbbb
aabbabb
aaababab

如果只知道出现一次,但是不清楚具体有几次,便直接可以不写右区间,表示至少出现n次,比如下面的正则就表示至少出现3次a


正则:/a{3,}/


可以匹配的不能匹配的
aaabbbb
baaaaaabb

量词 +


用于匹配前面的字符或子表达式至少一次或多次出现。
实际上,+的表现形式,还可以用{1,}来表示


正则: /a+/ 等价于 /a{1,}/


可以匹配的不能匹配的
abbbb
aabbb

量词 *


用于匹配前面的字符或子表达式出现0次或多次出现。实际上,*的表现形式,也可以用{0,}来表示


正则: /a*/ 等价于 /a{0,}/


可以匹配的不能匹配的
abbbb
aabbb

量词 ?


用于匹配前面的字符或子表达式零次或一次。实际上,*的表现形式,也可以用{0,1}来表示


正则: /a?b/ 等价于 /a{0,1}b/


可以匹配的不能匹配的
babcde
bad


正则表达式的贪婪匹配和非贪婪匹配是用来描述匹配模式时的两种不同行为。
贪婪匹配是指正则表达式尽可能地匹配更长的文本片段。它会尽量多地消耗输入字符串,并尝试匹配满足整个正则表达式模式的最长可能结果,是默认的行为,
反之,非贪婪匹配(也称为懒惰匹配或最小匹配) 则是指正则表达式尽可能地匹配更短的文本片段。它会尽量少地消耗输入字符串,并尝试匹配满足整个正则表达式模式的最短可能结果。
通常非贪婪匹配通过在正则字符串后面加?号来表示。



示例
正则表达式 /a+/,它表示匹配一个或多个连续出现的字符 "a"。


对于字符串 "aaa",贪婪匹配将尽量匹配更长的连续的 "a" 字符串,在这种情况下会匹配整个字符串 "aaa"。


使用非贪婪匹配需要在量词后面添加 ?。正则表达式 /a+?/ 表示非贪婪匹配,将匹配一个或多个连续出现的字符 "a",但只尽量匹配最短的结果。非贪婪匹配将尽量匹配最短的连续的 "a" 字符串。在这个例子中,非贪婪匹配会匹配第一个 "a" 字符,因为它是最短的满足正则表达式模式的子串。


锚点匹配


锚点是正则表达式中的特殊字符,用于匹配字符串的位置而不是具体的字符,可用于指定匹配发生的位置,常用的锚点有^$\b


^ 起始位置


表示匹配行的开头。下面正则表示匹配以a为开头的字符


正则:^a


可以匹配的不能匹配的
ada
abbc

$ 结束位置


表示匹配行的结尾。下面正则表示匹配以a为结尾的字符


正则:a$


可以匹配的不能匹配的
aab
dabc

\b 边界


表示匹配单词边界。下面正则表示匹配独立的单词


正则:/\bapple\b/


可以匹配的不能匹配的
I love applepineapple
applepinapple

\b还有很多其他的应用,比如



  • \b\w+\b:匹配一个或多个连续的单词字符,可以用来分割句子为单词数组。

  • \b\d{4}\b:匹配仅包含4位数字的字符串


在转义\b的时候需要使用\\b


分组和捕获:


分组 ()


括号 ( ):用于将一组模式作为单个单元进行匹配,并将其视为一个分组。


比如,我要匹配以jstsjava后缀的文件
正则:/.*\.(js|ts|java)/


可以匹配的不能匹配的
index.js1.png
1.ts2.jpg
calss.java3.mp3

再比如 正则:/(ab){1,}/,可以匹配一个或出现多个连续的ab,利用分组实现的


可以匹配的不能匹配的
abaa
ababba

捕获组


通过圆括号捕获分组内的内容,可以在后续操作中进行引用。


可能这比较难理解,我们举例说明,比如,我们有1-82-2这种类型的数据,我们可以使用正则的分组将两边的数据包裹,并使用exec进行捕获。分组符号的数据就是把这些想要捕获的数据标记出来。


如果我们想要 () 的分组能力,但是又不想捕获数据,可以使用 (?:) 表达式。可以提高正则表达式的性能和简洁性。


image.png


零宽断言



  1. 正向肯定预查(?=...):表示在当前位置后面,如果满足括号内的表达式,则继续匹配成功。

  2. 正向否定预查(?!...):表示在当前位置后面,如果不满足括号内的表达式,则继续匹配成功。

  3. 反向肯定预查(?<=...):表示在当前位置前面,如果满足括号内的表达式,则继续匹配成功。

  4. 反向否定预查(?<!...):表示在当前位置前面,如果不满足括号内的表达式,则继续匹配成功。



  • /(?=\d)\w+/ 匹配由数字紧随其后的单词字符。
    | 可以匹配的 | 不能匹配的 |
    | --- | --- |
    | 1 | w |
    | 1w | ww |


为什么这里能匹配1呢?1首先同样属于字符,其次还是数字,在断言的时候,不消耗字符,符合数字随其后的规则(本身)



  • /(?<!\d)\w+/ 匹配没有数字紧随在前面的单词字符。(js不支持)



js并不支持反向预查,只支持正向预查。这是因为正向预查在匹配时,可以当前位置后面的内容进行断言判断,如果不符合预期,则无法继续匹配成功。这种类型的预查可以通过回溯来实现。


然而,反向否定预查需要从当前位置回溯到前面的位置进行条件判断,这就使得正则引擎需要逆序地扫描前面的内容,增加了匹配的复杂度。因此,实现反向否定预查的算法相对更为复杂,并且可能导致性能下降。


反向否定预查在某些特定情况下可以被其他模式替代,比如使用捕获组结合后续的处理代码来达到类似的效果。



正则表达式大全



  1. 邮箱验证


/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(.\w{2,3})+$/

^\w+匹配以字符开头,([.-]?\w+)* 部分出现两次,品牌包含一个或多个由-或点.连接的部分,(.\w{2,3})+匹配域名




  1. URL 验证:包括 HTTP 和 HTTPS 协议。


/^(https?://)?[\w-]+(.[\w-]+)+[/#?]?.*$/


  1. 身-份-证号码验证:验证中国大陆身-份-证号码的有效性。


低配:
/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/

高配:
身-份-证号匹配
/^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[1-2]\d|3[0-1])\d{3}[0-9Xx]$/


  • ^[1-9]\d{5}:匹配 6 位行政区划代码、

  • (19|20)\d{2}:年份,匹配以 19 或 20 开头的四位数字、

  • (0[1-9]|1[0-2]):月份,取值范围为 01 到 12、

  • (0[1-9]|[1-2]\d|3[0-1]):日期,取值范围为 01 到 31、

  • \d{3}:顺序码,任意三位数字、

  • [0-9Xx]:校验码,可以是数字或字母 X 或 x、



  1. 数字验证:用于验证一个字符串是否只由数字组成。


`/^\d+$/`


  1. 字母验证:用于验证一个字符串是否只由字母组成。


`/^[a-zA-Z]+$/`


  1. 小数验证:匹配的数字可包含小数点,此处转义了小数点,


/^\d+(\.\d+)?$/


  1. 整数验证(包括负数):用于验证一个字符串是否为整数,可以包含正负号。


`/^[-+]?\d+$/`


  1. IP 地址验证: 用于验证 IPv4 地址的有效性。


/^((25[0-5]|2[0-4]\d|[01]?\d\d?).){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/


  1. 手机号码验证:


低配版本,仅表示11位数字


```
^\d{11}$ 低配版本,11位数字
```

高配版本


```
/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$/

如果不想这么复杂,可以写为
/^1[3-9]d{9}$/


还能匹配*特殊符号的,但是会失去匹配11位数功能
/^1[3-9]\d{1}(?:\*{1,})*\d+$/
```

如果确定符号个数,可改为/^1[3-9]\d{1}((?:\*{4})|\d{4})\d{4}$/,就能匹配固定11位数的号码



  • 可以匹配152702365242

  • 可以匹配152****65242


10.密码复杂度要求




  • 8位任意密码


    /^.{8,}$/



  • 包括至少8个字符,包含大写字母、小写字母和数字


    /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/



?=为正向断言,判断条件是否符合.*\d,即任意字符但是需要出现一个数字,其余类似


这个正则表达式用于强制密码应至少包含一个数字(?=.*\d)、一个小写字母(?=.*[a-z])和一个大写字母(?=.*[A-Z]),并且长度至少为8个字符.{8,}



  • 包括至少8个字符,包含大写字母、小写字母和数字,包括特殊字符
    /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*?]).{8,}$/



11.以8结尾,且位数在6位以内的数字


/^\d{0,5}8$/


  1. 时间匹配,匹配时分,年月日的匹配建议还是按照Date的API,正则在匹配闰年的二月份时候无法匹配


/^(?:[01]\d|2[0-3]):(?:[0-5]\d)$/


  • 可以匹配09:10 12:12 23:01 23:59


/^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$/



  • (?!0000) 表示后面不能跟着四个0,即年份不能为0000。

  • [0-9]{4} 表示匹配四个数字,即年份的格式为四位数字。

  • - 表示匹配“-”字符。

  • (?:…) 表示非捕获型分组,用于提高正则表达式的效率。

  • (?:0[1-9]|1[0-2]) 表示匹配01-12月份,其中0[1-9]表示01-09月份,1[0-2]表示10-12月份。

  • (?:0[1-9]|1[0-9]|2[0-8]) 表示匹配01-28日,其中0[1-9]表示01-09日,1[0-9]表示10-19日,2[0-8]表示20-28日。

  • (?:0[13-9]|1[0-2])-(?:29|30) 表示匹配01、03、05、07、08、10、12月份的29或30日。

  • (?:0[13578]|1[02])-31 表示匹配01、03、05、07、08、10、12月份的31日。

  • (?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29 表示匹配闰年的2月29日,其中[0-9]{2}表示匹配两位数字的年份,(?:0[48]|[2468][048]|[13579][26])表示匹配闰年的年份,即能被4整除但不能被100整除,或者能被400整除。

  • $ 表示匹配字符串的结束位置。



  1. 用户名:4-10位的用户名,包含下划线、连接符


/^[a-zA-Z0-9_-]{4,10}$/

总结


以上就是目前能想到的常用的正则,大家如果也有或者说常用的正则,也可以在评论区反馈,谢谢各位!


作者:原野风殇
来源:juejin.cn/post/7299376141451411490
收起阅读 »

如何正确遍历删除List中的元素

删除List中元素这个场景很场景,很多人可能直接在循环中直接去删除元素,这样做对吗?我们就来聊聊。 for循环索引删除 删除长度为4的字符串元素。    List<String> list = new ArrayList<String>...
继续阅读 »

删除List中元素这个场景很场景,很多人可能直接在循环中直接去删除元素,这样做对吗?我们就来聊聊。


for循环索引删除


删除长度为4的字符串元素。


    List<String> list = new ArrayList<String>();
   list.add("AA");
   list.add("BBB");
   list.add("CCCC");
   list.add("DDDD");
   list.add("EEE");

   for (int i = 0; i < list.size(); i++) {
       if (list.get(i).length() == 4) {
           list.remove(i);
      }
  }
   System.out.println(list);
}

实际上输出结果:


[AA, BBB, DDDD, EEE]

DDDD 竟然没有删掉!


原因是:删除某个元素后,list的大小size发生了变化,而list的索引也在变化,索引为i的元素删除后,后边元素的索引自动向前补位,即原来索引为i+1的元素,变为了索引为i的元素,但是下一次循环取的索引是i+1,此时你以为取到的是原来索引为i+1的元素,其实取到是原来索引为i+2的元素,所以会导致你在遍历的时候漏掉某些元素。


比如当你删除第1个元素后,继续根据索引访问第2个元素时,因为删除的关系后面的元素都往前移动了一位,所以实际访问的是第3个元素。不会报出异常,只会出现漏删的情况。


foreach循环删除元素


for (String s : list) {
       if (s.length() == 4) {
           list.remove(s);

      }
  }
   System.out.println(list);

如果没有break,会报错:



java.util.ConcurrentModificationException at java.util.ArrayListItr.checkForComodification(ArrayList.java:911)atjava.util.ArrayListItr.checkForComodification(ArrayList.java:911) at java.util.ArrayListItr.next(ArrayList.java:861) at com.demo.ApplicationTest.testDel(ApplicationTest.java:64) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)



报ConcurrentModificationException错误的原因:


看一下JDK源码中ArrayList的remove源码是怎么实现的:


public boolean remove(Object o) {
       if (o == null) {
           for (int index = 0; index < size; index++)
               if (elementData[index] == null) {
                   fastRemove(index);
                   return true;
              }
      } else {
           for (int index = 0; index < size; index++)
               if (o.equals(elementData[index])) {
                   fastRemove(index);
                   return true;
              }
      }
       return false;
  }

一般情况下程序会最终调用fastRemove方法:


private void fastRemove(int index) {
       modCount++;
       int numMoved = size - index - 1;
       if (numMoved > 0)
           System.arraycopy(elementData, index+1, elementData, index,
                            numMoved);
       elementData[--size] = null; // clear to let GC do its work
  }

在fastRemove方法中,可以看到第2行把modCount变量的值加一,但在ArrayList返回的迭代器会做迭代器内部的修改次数检查:


final void checkForComodification() {
    if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }

而foreach写法是对实际的Iterable、hasNext、next方法的简写,因为上面的remove(Object)方法修改了modCount的值,所以才会报出并发修改异常。


阿里开发手册也明确说明禁止使用foreach删除、增加List元素。


迭代器Iterator删除元素


    Iterator<String> iterator = list.iterator();
   while(iterator.hasNext()){
       if(iterator.next().length()==4){
           iterator.remove();
      }
  }
   System.out.println(list);


[AA, BBB, EEE]



这种方式可以正常的循环及删除。但要注意的是,使用iterator的remove方法,而不是List的remove方法,如果用list的remove方法同样会报上面提到的ConcurrentModificationException错误。


总结


无论什么场景,都不要对List使用for循环的同时,删除List集合元素,要使用迭代器删除元素。


作者:程序员子龙
来源:juejin.cn/post/7299384698883620918
收起阅读 »

iOS 仿花小猪首页滑动效果

iOS
一. 背景 首页改版,想要做一个类似花小猪首页滑动效果,具体如下所示: 二. 分析 从花小猪首页交互我们可以分析出如下信息: 首页卡片分为三段式,底部、中间、顶部。 当首页卡片在底部,只能先外部视图整体往上滑动,滑动到顶部后,内部卡片头部悬浮,内部卡...
继续阅读 »

一. 背景


首页改版,想要做一个类似花小猪首页滑动效果,具体如下所示:



二. 分析


从花小猪首页交互我们可以分析出如下信息:




  • 首页卡片分为三段式,底部、中间、顶部。




  • 当首页卡片在底部,只能先外部视图整体往上滑动,滑动到顶部后,内部卡片头部悬浮,内部卡片滚动视图依然可以滚动。




  • 当首页卡片在中间,可以先外部视图整体往上或者往下滑动,往下滑动到底部后,禁止滑动,滑动到顶部,内部视图卡片头部悬浮,内部滚动视图可以滚动。




  • 当首页卡片在顶部,可以拖动卡片外部视图整体下滑,也可以通过内部视图向下滚动,滚动到跟内部头部底部持平,变成整体一起向下滑动。而当内部滚动视图向上滚动,内部卡片头部悬浮固定。




  • 首页卡片滑动过程中,如果停在中间位置,依据卡片停止位置,距离底部、中间、顶部位置远近,向距离近的一端,直接移动到相应位置,比如移动到中间和顶部位置之间,如果距离顶部近,则直接移动到顶部。




  • 当首页卡片在底部,上滑速度很快超过一定值,就直接到顶部。同样在顶部下滑也一样。




  • 当首页卡片在顶部,内部滚动视图快速下滑,下滑到跟卡片头部分开,产生弹簧效果,不直接一起下滑,但其他部分如果慢慢滑动,下滑到跟卡片头部即将分开时,变成整体一起下滑。




三. 实现


理清了首页卡片的滑动交互细节之后,我们开始设计对应类和相关职责。



从上面结构图我们可以看出,主要分为三部分




  • 卡片外层容器externalScrollView,限定为UIScrollView类型。




  • 卡片内头部insideHeaderView,限定为UIView类型。




  • 卡片内滚动视图insideTableView,由于滚动视图所以insideTableView一定是UIScrollView类型,为了复用,这里我们限定为UITableView




这里其实我们不关心头部视图insideHeaderView,因为内部头部视图insideHeaderView和内部滚动视图insideTableView之间的关系是固定,就是内部滚动视图insideTableView一直在头部视图 insideHeaderView下面。


同样我们也不关心滚动视图insideTableView里面的内容,我们需要处理的就是卡片外层容器externalScrollView和内部滚动视图insideTableView之间交互关系。


因为所有这种类型交互处理逻辑是一致的,因此我们抽出FJFScrollDragHelper类。



  • 首先我们来认识下滚动辅助类FJFScrollDragHelper相关属性


    /// scrollView 显示高度
public var scrollViewHeight: CGFloat = kScreenH
/// 限制的高度(超过这个高度可以滚动)
public var kScrollLimitHeight: CGFloat = kScreenH * 0.51
/// 滑动初始速度(大于该速度直接滑动到顶部或底部)
public var slideInitSpeedLimit: CGFloat = 3500.0
/// 当前 滚动 视图 位置
public var curScrollViewPositionType: FJFScrollViewPositionType = .middle
/// 最高 展示 高度
public var topShowHeight: CGFloat = 0
/// 中间 展示 高度
public var middleShowHeight: CGFloat = 0
/// 最低 展示 高度
public var lowestShowHeight: CGFloat = 0
/// 当前 滚动 视图 类型
private var currentScrollType: FJFCurrentScrollViewType = .externalView
/// 外部 滚动 view
public weak var externalScrollView: UIScrollView?
/// 内部 滚动 view
public weak var insideScrollView: UIScrollView?
/// 拖动 scrollView 回调
public var panScrollViewBlock: (() -> Void)?
/// 移动到顶部
public var goToTopPosiionBlock: (() -> Void)?
/// 移动到 底部 默认位置
public var goToLowestPosiionBlock: (() -> Void)?
/// 移动到 中间 默认位置
public var goToMiddlePosiionBlock: (() -> Void)?

我们看到FJFScrollDragHelper内部弱引用了外部滚动视图externalScrollView和内部滚动视图insideScrollView




  1. 关联对象,并给外部externalScrollView添加滑动手势




/// 添加 滑动 手势 到 外部滚动视图
public func addPanGestureRecognizer(externalScrollView: UIScrollView){
let panRecoginer = UIPanGestureRecognizer(target: self, action: #selector(panScrollViewHandle(pan:)))
externalScrollView.addGestureRecognizer(panRecoginer)
self.externalScrollView = externalScrollView
}



  1. 处理滑动手势




// MARK: - Actions
/// tableView 手势
@objc
private func panScrollViewHandle(pan: UIPanGestureRecognizer) {
/// 当前 滚动 内部视图 不响应拖动手势
if self.currentScrollType == .insideView {
return
}
guard let contentScrollView = self.externalScrollView else {
return
}
let translationPoint = pan.translation(in: contentScrollView.superview)

// contentScrollView.top 视图距离顶部的距离
contentScrollView.y += translationPoint.y
/// contentScrollView 移动到顶部
let distanceToTopH = self.getTopPositionToTopDistance()
if contentScrollView.y < distanceToTopH {
contentScrollView.y = distanceToTopH
self.curScrollViewPositionType = .top
self.currentScrollType = .all
}
/// 视图在底部时距离顶部的距离
let distanceToBottomH = self.getBottomPositionToTopDistance()
if contentScrollView.y > distanceToBottomH {
contentScrollView.y = distanceToBottomH
self.curScrollViewPositionType = .bottom
self.currentScrollType = .externalView
}
/// 拖动 回调 用来 更新 遮罩
self.panScrollViewBlock?()
// 在滑动手势结束时判断滑动视图距离顶部的距离是否超过了屏幕的一半,如果超过了一半就往下滑到底部
// 如果小于一半就往上滑到顶部
if pan.state == .ended || pan.state == .cancelled {

// 处理手势滑动时,根据滑动速度快速响应上下位置
let velocity = pan.velocity(in: contentScrollView)
let largeSpeed = self.slideInitSpeedLimit
/// 超过 最大 力度
if velocity.y < -largeSpeed {
gotoTheTopPosition()
pan.setTranslation(CGPoint(x: 0, y: 0), in: contentScrollView)
return
} else if velocity.y < 0, velocity.y > -largeSpeed {
if self.curScrollViewPositionType == .bottom {
gotoMiddlePosition()
} else {
gotoTheTopPosition()
}
pan.setTranslation(CGPoint(x: 0, y: 0), in: contentScrollView)
return
} else if velocity.y > largeSpeed {
gotoLowestPosition()
pan.setTranslation(CGPoint(x: 0, y: 0), in: contentScrollView)
return
} else if velocity.y > 0, velocity.y < largeSpeed {
if self.curScrollViewPositionType == .top {
gotoMiddlePosition()
} else {
gotoLowestPosition()
}
pan.setTranslation(CGPoint(x: 0, y: 0), in: contentScrollView)
return
}
let scrollViewDistanceToTop = kScreenH - contentScrollView.top
let topAndMiddleMeanValue = (self.topShowHeight + self.middleShowHeight) / 2.0
let middleAndBottomMeanValue = (self.middleShowHeight + self.lowestShowHeight) / 2.0

if scrollViewDistanceToTop >= topAndMiddleMeanValue {
gotoTheTopPosition()
} else if scrollViewDistanceToTop < topAndMiddleMeanValue,
scrollViewDistanceToTop > middleAndBottomMeanValue {
gotoMiddlePosition()
} else {
gotoLowestPosition()
}
}
pan.setTranslation(CGPoint(x: 0, y: 0), in: contentScrollView)
}

处理滑动手势需要当前视图滚动类型currentScrollType和卡片当前所处的位置curScrollViewPositionType来分别进行判断。


/// 当前 滚动 视图 类型
public enum FJFCurrentScrollViewType {
case externalView /// 外部 视图
case insideView /// 内部 视图
case all /// 内部外部都可以响应
}

/// 当前 滚动 视图 位置 属性
public enum FJFScrollViewPositionType {
case top /// 顶部
case middle /// 中间
case bottom /// 底部
}

如下是对应的判断逻辑:


暂时无法在飞书文档外展示此内容


A. 在底部


 /// 回到 底部 位置
private func gotoLowestPosition() {
self.curScrollViewPositionType = .bottom
self.goToLowestPosiionBlock?()
}

private func gotoLowestPosition(withAnimated animated: Bool = true) {
self.insideTableView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
if animated {
UIView.animate(withDuration: 0.18, delay: 0, options: .allowUserInteraction) {
self.externalScrollView.top = self.scrollDragHelper.getBottomPositionToTopDistance()
}
} else {
self.externalScrollView.top = self.scrollDragHelper.getBottomPositionToTopDistance()
}
}

只能滚动外部视图,内部滚动视图偏移量是0.


B. 在中间


/// 回到 中间 位置
private func gotoMiddlePosition() {
self.curScrollViewPositionType = .middle
self.goToMiddlePosiionBlock?()
}

private func gotoMiddlePosition(withAnimated animated: Bool = true) {
self.insideTableView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
if animated {
UIView.animate(withDuration: 0.18, delay: 0, options: .allowUserInteraction) {
self.externalScrollView.top = self.scrollDragHelper.getMiddlePositionToTopDistance()
}
} else {
self.externalScrollView.top = self.scrollDragHelper.getMiddlePositionToTopDistance()
}
}

只能滚动外部视图,内部滚动视图偏移量是0.


C. 在顶部



  • 开始滚动判断:


    /// 更新 当前 滚动类型 当开始拖动 (当在顶部,开始滑动时候,判断当前滑动的对象是内部滚动视图,还是外部滚动视图)
public func updateCurrentScrollTypeWhenBeginDragging(_ scrollView: UIScrollView) {
if self.curScrollViewPositionType == .top {
if scrollView.contentOffset.y <= 0 {
self.currentScrollType = .externalView
} else {
self.currentScrollType = .insideView
}
}
}


  • 滚动过程中判断


/// 更新 滚动 类型 当滚动的时候,并返回是否立即停止滚动
public func isNeedToStopScrollAndUpdateScrollType(scrollView: UIScrollView) -> Bool {
if scrollView == self.insideScrollView {
/// 当前滚动的是外部视图
if self.currentScrollType == .externalView {
self.insideScrollView?.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
return true
}
if self.curScrollViewPositionType == .top {
if self.currentScrollType == .all { /// 在顶部的时候,外部和内部视图都可以滑动,判断当内部滚动视图视图的位置,如果滚动到底部了,则变为外部滚动视图跟着滑动,内部滚动视图不动
if scrollView.contentOffset.y <= 0 {
self.currentScrollType = .externalView
} else {
self.currentScrollType = .insideView
}
} else if scrollView.isDecelerating == false,
self.currentScrollType == .insideView { /// 在顶部的时候,当内部滚动视图,慢慢滑动到底部,变成整个外部滚动视图跟着滑动下来,内部滚动视图不再滑动
if scrollView.contentOffset.y <= 0 {
self.currentScrollType = .externalView
}
}
}
}
return false
}


  • 滚动结束判断


/// 当在顶部,滚动停止时候 更新 当前 滚动类型 ,如果当前内部滚动视图,已经滚动到最底部,
/// 则只能滚动最外层滚动视图,如果内部滚动视图没有滚动到最底部,则外部和内部视图都可以滚动
public func updateCurrentScrollTypeWhenScrollEnd(_ scrollView: UIScrollView) {
if self.curScrollViewPositionType == .top {
if scrollView.contentOffset.y <= 0 {
self.currentScrollType = .externalView
} else {
self.currentScrollType = .all
}
}
}

以上就是具体滚动判断相关处理逻辑,对应实现效果如下。



作者:果哥爸
来源:juejin.cn/post/7299731897626345481
收起阅读 »

读完一个人的朝圣,我觉得每一个人身上都有男主的影子

莫琳:”你会去很久吗?“ 哈罗德:”到街尾就回来“     一个童年缺少母爱,父爱,中年丧子,一生都在低头做事,不敢正视他人,可以在一个小酒厂工作四十年,却不求升职加薪,与人无争,与人无交的退休老头,跋涉六百多公里,穿越整个大不列颠,拿着一封信去拯救患了癌症的...
继续阅读 »

莫琳:”你会去很久吗?“


哈罗德:”到街尾就回来“


    一个童年缺少母爱,父爱,中年丧子,一生都在低头做事,不敢正视他人,可以在一个小酒厂工作四十年,却不求升职加薪,与人无争,与人无交的退休老头,跋涉六百多公里,穿越整个大不列颠,拿着一封信去拯救患了癌症的老友,听起来应该是无聊透了。远远没有,帅气潇洒,带领古巴人民与美国死磕几十年,躲过n = 700次暗杀,成功睡到美国中情局女特工,并令其爱上自己,传闻睡过35000个女性的菲德尔*卡斯特罗的故事令人感到有种有趣有料。的确,哈罗德是一个平凡以至于差劲糊涂的人,但远没有那么简单,哈罗德、哈罗德,作者是借哈罗德写我们的父母,我们的孩子,我们的朋友,我们亲爱的自己,我们每个人身上可能都有哈的影子,如今这个影子开始征程,或叫朝圣,我更倾向于叫“做梦”。因为这真的像是哈罗德的一场梦。


  这里没有艺术,没有历史,儿女情长,伟大,浪漫。


  这里只有回忆,哈罗德 距离终点很近很远,近的是,一生真的很短,远的是,回忆又是那么漫长。


”没有爱的生活不叫生活“


    哈罗德有一个酒鬼父亲,从不关心他的儿子,在十六岁生日那天,将哈赶出了家门,而他的母亲,早在很久以前已经抛弃了这个家庭去周游世界。在父母那里哈罗德没有得到一丝爱,好像还不如很多人养的宠物的到的关爱多,没有得到爱的哈罗德好像也吝啬于给予爱,路上他在回忆道:“他欠过去一点点慷慨”,但他本人是“喜欢和人交往的,希望明白她们之所爱,之所失”。哈不是一个自私心眼坏的人,(和生活中大多人一样,心眼不坏,好多父母也说,自己孩子“心眼不坏”,这个词有参考价值吗?)他只是不会表达,这种表达好像也不仅仅只是 羞于对儿子说:“爸爸爱你”,也不是:“儿子,你真棒,爸爸为你自豪”(他的儿子没有过任何亲昵的经历)。更不是对爱人说一句:”亲爱的“,好像这个人本身:”没有多少爱“,就好像爱是水,爱是食物,是力气,我们能看到摸到能展示给别人看,”看我有的是力气,我有的是爱“,就像没有力气的男人,哈罗德是一个没有爱的男人。哈罗德倒不是冷漠,也不是自私,在我看来,哈罗德一个60岁的老头,其实是个孩子,一个没有被给予爱与鼓励多的孩子,一个没有受到爱的给予的孩子,是不会轻易去爱别人,爱这个社会的。他们没有爱人的能力,更很难让人感受到来自他内心的热情与爱(友爱,情爱,亲情……)。


  哈罗德是一个“没有妈的孩子”,上学时,哈总是被小伙伴们这样嘲笑。它是一个极其脆弱敏感的孩子,他不敢交朋友,他不敢直视别人,甚至整个屋子的人看他一眼,都会脸红到脖子,他对儿子很少给予父爱,对妻子关怀,理解不够,这些都源于童年,缺失的亲情。


    一个人的成长的表现就是爱越来越多的人,事。即使受到伤害。


  (我不是鼓吹爱与自由可以战胜一切,不是让别人去滥情,只想说,帮帮我们身边的人,每个人,哪怕一个微笑,这也是脆弱的哈罗德坚持走到终点的原因之一)


”婚姻“


  莫琳,哈的爱人,总爱重复哈的话,要不就是”我不这么认为“,两人因为儿子的悲剧分道扬镳,只是名义上的夫妻,在哈走后,莫琳经历了一系列的情绪波动,先是惊讶,抱怨,而后焦虑不安,后是平静的回忆,直到对哈的日夜思念,莫琳因为哈罗德对儿子的不作为,而抱怨,恨着哈罗德,二十年之久,莫琳本身是一个普通的女性,无论作为母亲,妻子,都算是合格的,那什么让两人冷漠二十年,让婚姻只剩下房子一个躯壳,是哈罗德的性格缺陷?两人沟通不够?是残酷的现实?…………,


  哈的徒步之旅让他们重新走到了一起,值得一提的是,两人对于初见时的回忆时如此的不同。(男人女人是两个物种)


  莫琳的回忆:"他微弯下腰,嘴唇贴近她的耳朵,伸手拨开她的一绺头发,才开口说话。这大胆的举动让她感到一股强烈的电流顺着脖子传上来,甚至今日想起,肌肤下仍能感受到那一份悸动。他说了什么?无论说了什么,都肯定是极其有趣的内容,因为两人都笑得歇斯底里,还尴尬地打起嗝来。她想起他转身走向酒吧取水时衣角扬起的样子,想起自己乖乖地站在原地等他。那时好像只有当哈罗德在附近,世界才有光。那两个跳得、笑得如此畅快的年轻人如今去了哪里?"


  哈罗德的回忆:“哈罗德在跳舞,突然发现隔着一整个舞池的莫琳在看着他。他还记得那一刻疯狂地挥舞四肢的感觉,仿佛要在这个美丽女孩的见证下甩掉过去的一切。他鼓起勇气,越跳越起劲,双腿踢向空中,双手像滑溜溜的海鳗扭动。他停下来仔细观察,她还在看着他,这次她碰到他的目光,忽然笑了。她笑得那样乐不可支,抖着肩膀,秀发拂过脸庞。他生平第一次不由自主地穿过舞池,去触碰一个完全的陌生人。天鹅绒一样的秀发下,是苍白而柔软的肌肤。她没有回避。“


  维系婚姻的到底是什么?A爱情,B浪漫的回忆,C未来的期盼,D,子女 E,两个人的时候感觉孤独,一个人时更孤独(莫琳回忆道)F,安于现状G,人丑又穷离不起(老婆太厉害,不敢提)


……敲黑板!!!这道题超纲,考试不考……


信仰与理智


  读完之后,我问过自己,哈罗德为什么要徒步去远在千里的贝里克?支撑他的是什么?


  他在经历一路的跋涉到底吃了多少苦?值不值?关键是他是个沉闷、死气沉沉没有活力的哈罗德。做出这个选择不可思议,作者提到将这归为“期待一种比不言自明的现实更大,更疯狂也更美好的可能”。在我看来,哈罗德身上有很多缺陷,他自己给自己贴了太多标签,沉默逆来顺受,软弱,羞涩……,但人不是一个个标签构成的,通过一个个的标签并不能完全解释一个人的行为,事实上,作者没有解释也不用解释,哈为什么做了这个决定,因为包括这件事在内的很多事都是有一种奇思妙想甚至吓到自己,自己也不知从哪冒出来的想法开始,首先,在那一时一刻,理想梦想狂想疯想轮奸了理智,从此孕育了伟大,自己被惊讶、震撼、感动,从而迈出了一步,两步,就像疑问自行车就两个轮子为什么可以冲的那么快,疑问它为什么不倒一样,没有人告诉你为什么?而你同样可以骑上去,跑的比风还快,还自由。你做到了,哈罗德的决定也做的那么简单与自然。


  做决定容易,简单的决定,简单的计划不一定成功,仅仅一双帆布鞋(属于海洋,却行走于陆地),一路上,哈曾羞于解释自己的计划,但一路的陌生人,即使有再多的不理解,不可思议,也都给予了支持,或物质上,或精神上,他们都是哈罗德走下去的动力,哈罗德感谢他们,正是这些动力让他尝试去冲破囚禁它一生的牢笼,束缚,释放了年轻时候,转瞬即逝的野性,冒险与激情,但是哈罗德毕竟只是哈罗德,标签解释不了全部,却注定哈罗德在那么一个阶段,否定自我,否定这次徒步的意义。


  人生就像一个缓冲区,就像数据库buffer,操作系统buffer,各种buffer一样,缓冲区存在的意义,在于解决通信双方处理信息的速度不同的问题,人生的buffer,怎么讲?得与失,的是写入buffer,失  是移除buffer。


  朋友有失有得,太多照顾不过来,就要失去热度低得,太少,你会感到孤独,又会去交新朋友,


从人生来讲,不只是朋友,童年,青年,中年,老年,童年的我们是空的buffer,接受各种各样的信息,我们每天都在快速的成长,不曾感到失去,直到青年,我们开始困惑,感伤,为了长大,活下去,为了责任,不得不得到很多不想得到的,失去很多宝贵的,(大话西游的主题,成长,得与失)。到了中年,可能渐渐的习惯了得失,不再多愁善感,这是我们可能是很多人心中的英雄和榜样,只有我们自己知道,我是个什么样的混蛋,到了老年,得到的可能仅仅是白发,失去的与日俱增,缓冲区越来越小,我们没有了憧憬,只剩下回忆,我们已经不是年轻的感伤,更多是一种超然的释怀。因为除了生命,任何都不属于我。


哈罗德在救赎曾经的哈罗德


  哈罗德呢?他老了,他在徒步,他在救赎自己,他在朝圣,但他并不是英雄,他正在失去很多,跟普通人不同的是,他早就失去了很多,甚至很多不曾得到,父爱,母爱,朋友,儿子,爱人,这些失去放在有这么一个性格缺陷的老头身上,有点残忍,但上帝在决定你命运的时候,才不会管你是不是很可怜了,更不会征询你的意见,就这样 ,哈罗德在失去唯一的陪伴——小狗后(一条小狗,它的名字就叫“小狗”,哈罗德就这么叫),崩溃了,没有了时间概念,没有了距离,以前用英里丈量到终点的距离,现在用回忆,在一幅幅回忆前 ,哈罗德真的要放弃了,莫琳在哈走的日子里也在回忆,回忆年轻,回忆自己对哈罗德过分要求,与缺失的理解,在哈崩溃时,莫琳恢复了以前的温情与鼓励,哈罗德蹒跚的走到了终点,老朋友,结束了,但她们的婚姻好像重新开始。


后记:


在我观察,这本小说,有爱需要表达,婚姻,信仰,三个主题,这是一本鸡汤小说,出自英国作家蕾秋乔伊斯,这是第二本我非常喜欢的女作家写的小说,以前以为女作家只会写给女孩的鸡崽文学,什么女孩你必须优秀…………,XXX,或者儿女情长,纯粹YY的爱情小说,现在证明,我错了,女作家也可以写出能体现对社会对历史对人性的深刻理解的作品(《穆斯林的葬礼》),他们对人心理的细腻把握,同样让人感到有趣,这本小说里,我并没有对哈罗德,旅途的见闻,中间媒体炒作,投机分子借机包装,哈罗德与荃妮的友谊做过多感想,因为我觉得这只是路上的故事,大树的枝叶,任何一部小说都要有插曲丰满主人公的形象,但主题从头贯穿到结尾,其中婚姻的主题很明显,但是我经历有限,不能提出特别的见解,望请见谅。


作者:五阳神功
来源:juejin.cn/post/7299491065395789863
收起阅读 »

耗时七天,我写完了自己的第一个小程序

web
一入红尘深似海。 自2016年参加工作时申请了第一张信用卡,至今已有七年矣。在这七年之中,自己信用卡的数量由1张逐渐增加到12张,后又慢慢减少到如今的5张。七年的卡海浮沉让我从初入社会的一无所有,到如今的负债累累。 当然,这篇文章并不是来记录自己七年的负债之旅...
继续阅读 »

一入红尘深似海。


自2016年参加工作时申请了第一张信用卡,至今已有七年矣。在这七年之中,自己信用卡的数量由1张逐渐增加到12张,后又慢慢减少到如今的5张。七年的卡海浮沉让我从初入社会的一无所有,到如今的负债累累。


当然,这篇文章并不是来记录自己七年的负债之旅,而是在经历了多年“钱都去哪儿了?”的内心呼唤后的心灵觉醒:还是要有个账本记账啊!


我的需求并不复杂:



1、可以快速的知道如今自己卡的总额度是多少,还需要还的欠款是多少(清楚负债情况)


2、可以快速知道每张卡的可用额度是多少,账单日是哪天(便于刷卡时明确知道该刷哪张卡,不至于出现今天刚刷了卡,明天就出了账单要还的现象)


3、可以知道每个月刷卡的总手续费是多少(清楚损益,明白每个月的损耗)


4、记录收支(了解每一分钱都去了哪里)



基于以上四个简单的需求,在尝试现在市面上十几款记账软件后,我惊奇的发现:竟然没有一款合适的软件可以满足我的需求!


于是,我做了一个【XXXX】的决定:自己来写一个工具吧!


然后,就诞生了我发布的第一个小程序:了账


小程序简介


了(liao)账是一款简洁的记账小程序。了账中的了字,是明了之意,清楚明白自己的账目,亦是了结之意,祝愿各位卡友早日上岸。


了账的页面相对简洁,有【未登录】【已登录】两种状态展示,如下图所示:


【未登录】


截屏2023-10-23 08.19.09.png


【已登录】


截屏2023-10-23 08.27.30.png


了账只有账户账单两个tab页,分别用来展示当前账户信息和查看收支记录。


账户页面展示了用户(目前只有作者本人😄)较为关心的几个数据:【当前额度】、【可用额度】、【现金额度】、【信用卡总览】、【当前账户】。


账单页面除了查看每一笔收支记录外,在顶部也展示了当月总出账、总入账信息。


截屏2023-10-29 14.53.05.png


新增账户


用户可通过【新增】按键创建账户,在账户页面,顶部账户信息会随之动态改变。如下图所示:


截屏2023-10-29 15.05.44.png


在新增页面,用户可点击账户类型修改新增账户的类型,目前【了账】共包含【信用卡】、【储蓄卡】、【支付宝】、【微信】、【其他】共五类账户。除信用卡外,其余四类账户额度统一归类为【现金额度】。


信用卡除了【固定额度】之外有时会给一部分【临时额度】,因此,在新增账户页面,除了【固定额度】之外,添加了【当前额度】字段。【当前额度】是包含【固定额度】和【临时额度】的账户总额度。


新增收支


当用户创建过账户后,就可以点击【账户】页面右下角【记一笔】浮块创建收支记录,并在【账单】页面查看。相应的,账户页面所展示的【账户信息】也会随之动态改变。如下图所示:


截屏2023-10-29 15.24.00.png


在记录收支时,不同的账户类型可选的账单类型也不相同。如:信用卡账户下可选择的账单类型为【日常支出】、【个人刷卡】、【账单还款】,储蓄卡账户下可选择的账单类型为【日常支出】、【日常收入】、【转账支出】、【转账收入】,支付宝账户微信账户其他账户则多出【提现】类型可供选择。如下图所示:


截屏2023-10-29 15.41.54.png


当账单类型为【日常支出】时,则须选择支出类型。目前共有【食】、【行】、【衣】、【住】、【娱乐】、【其他支出】六类支出可供选择。如下图所示:


截屏2023-10-29 15.35.20.png


信用卡账户账单类型为【个人刷卡】,以及支付宝账户微信账户其他账户账单类型为【提现】时,则需填写【收款金额】。收款账户为除【信用卡账户】外的其他账户,收款金额为除去手续费之外的实际到账金额。如下图所示:


截屏2023-10-29 15.53.45.png


账单的编辑、删除和账户的编辑、删除


用户可通过左滑对当前账户及当前收支进行编辑、删除。当收支被删除后,账户信息将会回退该笔收支。当账户被删除后,该账户下的所有收支将不可被编辑、删除。如下图所示:


截屏2023-11-11 12.27.15.png


账户详情和账单详情


点击每个账户和账单,可进入详情页,查看详情信息。如下图所示:


截屏2023-11-11 14.25.39.png


写在最后


账本只是工具,最主要的还是要诸位卡友调整好心态,量入为出。祝愿各位早日上岸!!!


写代码用了7天,备案发布将近一个月!!!最后上线认证居然还收了30块巨款!!!至今仍未明白:经历了实名注册小程序号,实名IPC备案后,最后上线认证的意义在哪里?难道只为承袭小马哥一贯的氪金传统?


作者:凡铁
来源:juejin.cn/post/7299733832413069347
收起阅读 »

整理了七八年的笔记后,感觉很累,好像并不值得......

🔊整理了七八年的笔记(包括收藏别人的),感觉很累,回顾后好像并不值得那么做,于是对收藏文章,做笔记,学知识有了一些不一样的感悟,于是写下此文,警示自己。 📚一、收藏怪 收藏不等于吸收,收藏的文章不消化,还是别人的,收藏不等于学会了知识点,警惕陷阱。 ...
继续阅读 »

🔊整理了七八年的笔记(包括收藏别人的),感觉很累,回顾后好像并不值得那么做,于是对收藏文章,做笔记,学知识有了一些不一样的感悟,于是写下此文,警示自己。



📚一、收藏怪




  1. 收藏不等于吸收,收藏的文章不消化,还是别人的,收藏不等于学会了知识点,警惕陷阱。




  2. 重复的事情不要做第二遍,当初偷懒没理解收藏的文章,到头来还得从头开始理解;但却做了一些重复的事情。生命其实很宝贵。




  3. 收藏文章要精选,收藏的文章过多会增加未来的负担; 再看是消耗未来的时间,未来的时间一定比现在的时间更加宝贵,请慎重地做文章收藏。好的文章点个赞,不想错过知识就及时总结消化。




去阅读了那些以前收藏的文章,当初没有懂的,现在也可能没有懂;现在懂的也并不是因为收藏了才懂的。




📒二、断舍离


那些似懂非懂的知识,要学会断舍离。收藏的文章,记录的笔记;并不是越多越好,数量可能带来质量的下降,可能充斥重复,甚至废话。



  1. 做减法,该丢掉就丢掉。 这是生活经验;也是哲学道理。

  2. 似懂非懂的知识,多了只会让自己难受。通透才能轻松。什么都想要,什么也达不到最好,反而容易让自己撑着。

  3. 少即是多,抓重点。什么都想学,什么也学不好。知识也是有二八定律的。

  4. 不要废话,保持简单,一句话能说清楚的事情,就不要两句话。

  5. 知识是会给增加烦恼、负担的。别让知识给自己制造无形的麻烦、也不给自己未来添堵。

  6. 允许自己有知识不会,也别想着什么都会;更别想着什么都学。




📑三、兴趣



  1. 不喜欢就不要学,讨厌的知识容易遗忘。学习知识也要取悦自己。

  2. 学习自己感兴趣的,这样才能深耕,深耕那些不喜欢的知识,会让自己痛苦。

  3. 学习是需要时间成本的,而且很大,对自己也要投其所好。




📜四、偷懒



  1. 记录再多不理解的笔记、知识,不及时消化,还是不会理解的,有用吗?这样建议别做。

  2. 摒弃那种笔记做越多越充实的自我欺骗行为。要做一个偷懒的人




✒️五、总结


最近整理大量以前的学习笔记,以及各种技术收藏文档;发现了很多无用的,累赘的;不懂的还是不懂;似懂非懂的还是似懂非懂。而做笔记的过程却花费了太多时间。 把事情寄托于未来,成本更高。思维上要学会偷懒。别用笔记上的勤奋来掩饰思维上的懒惰。


🔊注:有一些偏激的观点,但本文仅代表当下自己的想法。


作者:uzong
来源:juejin.cn/post/7299741943442112552
收起阅读 »

移动端APP版本治理

1、背景 在许多公司,APP版本都是不受重视的,产品忙着借鉴,开发埋头编码,测试想着不粘锅。 只有在用户反馈app不能用的时候,你回复客服说,让用户升级最新版本,是不是很真实。 而且业界也很少有听说版本治理的,但其实需求上线并不是终点,在用户数据回传之前,这中...
继续阅读 »

1、背景


在许多公司,APP版本都是不受重视的,产品忙着借鉴,开发埋头编码,测试想着不粘锅。


只有在用户反馈app不能用的时候,你回复客服说,让用户升级最新版本,是不是很真实。


而且业界也很少有听说版本治理的,但其实需求上线并不是终点,在用户数据回传之前,这中间还有一个更新升级的空档期,多数公司在这里都是一个“三不管”地带,而这个空档期,我称之为版本交付的最后一公里



2、价值



2.1、业务侧


总有人会挑战“有什么业务价值?”对吧,那就先从业务价值来看。


尽管有些app的有些业务是动态发布的,但也一定会有些功能是依赖跟版的,也就是说,你没办法让所有用户都用上你的新功能,那对于产运团队来说,业务指标就还有提升的空间。


举两个例子:



  1. 饿了么免单活动需要8.+版本以上的app用户才能参与,现在参与用户占比80%,治理一波后,免单用户参与占比提升到90%,对业务来说,免单数没变,但是订单量却是有实实在在的提升的。

  2. 再来一个,酷狗音乐8.+的app用户才可以使用扫码登录,app低版本治理之后,扫码登录的用户占比势必也会提升,那相应的,登录成功率也可以提升,登录流程耗时也会缩短,这都是实实在在的指标提升。



虚拟数据,不具备真实参考性。



2.2、技术侧


说完业务看技术,在技术侧也可以分为三个维度来看:



  1. 稳定性,老版本的crash、anr之类的问题在新版本大概率是修复了的,疑难杂症可能久一点;

  2. 性能优化,比如启动、包大小、内存,可以预见是比老版本表现更好的,个别指标一两个版本可能会有微量劣化,但是一直开倒车的公司早晚会倒闭;

  3. 安全合规,不管是老的app版本还是老的服务接口,都可能会存在安全问题,那么黑产就可能抓住这个漏洞从而对我们服务的稳定性造成隐患,甚至产生资损;


2.3、其他方面


除了上面提到的业务指标和用户体验之外,还有没有?


有没有想过,老版本的用户升上来之后,那些兼容老版本的接口、系统服务等,是不是可以下线了,除了减少人力维护成本之外,还能减少机器成本啊,这也都是实打实的经费支出。


对于项目本身来说,也可以去掉一些无用代码,减少项目复杂度,提升健壮性、可维护性。


3、方案



3.1、升级交互


采用新的设计语言和新的交互方式。


3.1.1、弹窗样式


样式上要符合app整体的风格,信息展示明确,主次分明。


bad casegood case

3.1.2、操作表达


按钮的样式要凸显出来,并放在常规易操作的位置上。


bad casegood case

3.1.3、提醒链路


从一级菜单到二级页面的更新提醒链路,并保持统一。



3.1.4、进度感知


下载进度一定要可查看并准确,如果点了按钮之后什么提示都没有,用户会进入一个很迷茫的状态,体验很差。



3.2、提醒策略


我们需要针对不同的用户下发不同的提醒策略,这种更细致的划分,不光是为了稳定性和目标的达成,也是为了更好的用户体验,毕竟反复提醒容易引起用户的反感。


3.2.1、提醒时机


提醒时机其实是有讲究的,原则上是不能阻塞用户的行为。


特别是有强制行为的情况,比如强更,肯定不能在app启动就无脑拉起弹窗。



bad case:双十一那天,用户正争分夺秒准备下单呢,结果你让人升级,这不扯淡的吗。



时机的考虑上有两个维度:



  1. 平台:峰时谷时;

  2. 用户:闲时忙时;


3.2.2、逻辑引擎


为什么需要逻辑引擎呢?逻辑引擎的好处是跨端,双端逻辑可以保持一致,也可以动态下发。




可以使用接口平替,约定好协议即可。



3.2.3、软强更


强制更新虽然有可以预见的好效果,但是也伴随着投诉风险,要做好风险管控。


而在2023-02-27日,工信部更是发布了《关于进一步提升移动互联网应用服务能力》的通知,其中第四条更是明确表示“窗口关闭用户可选”,所以强更弹窗并不是我们的最佳选择。



虽然强更不可取,但是我们还可以提高用户操作的费力度,在取消按钮上增加倒计时,再根据低版本用户的分层,来配置不同的倒计时梯度,比如5s、10s、20s这样。


用户一样可以选择取消,但是却要等待一会,所以称之为软强更



3.2.4、策略字段



  • 标题

  • 内容

  • 最新版本号

  • 取消倒计时时长

  • 是否提醒

  • 是否强更

  • 双端最低支持的系统版本

  • 最大提醒次数

  • 未更新最大版本间隔

  • 等等


3.3、提示文案


升级文案属于是ROI很高的了,只需要总结一下新版本带来了哪些新功能、有哪些提升,然后配置一下就好了,但是对用户来说,却是实打实的信息冲击,他们可以明确的感知到新版本中的更新,对升级意愿会有非常大的提升。



虽然说roi很高,但是也要花点心思,特别是大的团队,需要多方配合。


首先是需求要有精简的价值点,然后运营同学整合所有的需求价值点,根据优先级,出一套面向用户的提醒话术,也就是提示的升级文案了。



3.4、更新渠道


iOS用户一般在App Store更新应用,但是对于Android来说,厂商比较多,对应的渠道也多,还有一些三方的,这些碎片化的渠道自然就把用户人群给分流了,为了让每一个用户都有渠道可以更新到最新版本,那就需要在渠道的运营上下点功夫了,尽可能的多覆盖。



除了拓宽更新渠道之外,在应用市场的介绍也要及时更新。


还有一点细节是,优化应用市场的搜索关键词。


最后,别忘了自有渠道-官网。


3.5、触达投放


如果我们做了上面这么多,还是有老版本的用户不愿意升级怎么办?我们还有哪些方式可以告诉他需要升级?


触达投放是一个很好的方式,就像游戏里的公告、全局喇叭一样,但是像发短信这种需要预算的方式一般都是放在最后使用的,尽可能的控制成本。




避免过度打扰,做好人群细分,控制好成本。



3.6、其他方案


类型介绍效果
更新引导更新升级操作的新手引导😀
操作手册描述整个升级流程,可以放在帮助页面,也可以放在联系客服的智能推荐🙂
营销策略升级给会员体验、优惠券之类的😀
内卷策略您获得了珍贵的内测机会,您使用的版本将领先同行98%😆
选择策略「88%的用户选择升级」,替用户做选择😆
心理策略「预计下载需要15秒」,给用户心理预期😀
接口拦截在使用某个功能或者登录的时候拦截,引导下载最新版本😆
自动下载设置里面加个开关,wifi环境下自动下载安装包😀
版本故事类似于微信,每个版本都有一个功能介绍的东西,点击跳转加上新手引导🙂

好的例子:


淘宝拼多多

4、长效治理


制定流程SOP,形成一个完整链路的组合拳打法🐶。


白话讲,就是每当一个新版本发布的时候,明确在每个时间段要做什么事,达成什么样的目标。


让更多需要人工干预的环节,都变成流程化,自动化。



5、最后


一张图总结APP版本治理依次递进的动作和策略。



作者:yechaoa
来源:juejin.cn/post/7299799127625170955
收起阅读 »

为什么稳定的大公司不向Flutter迁移?

迁移很难, 但从头开始很简单 从Flutter的测试版开始, 我就一直在关注它, 从那时起, 我就看到了Flutter在开发者, 社区和公司中的采用. 大多数新兴开发人员都曾多次讨论过这个问题: 为什么大公司不使用 Flutter? 因此, 这篇小文只是我...
继续阅读 »

迁移很难, 但从头开始很简单


从Flutter的测试版开始, 我就一直在关注它, 从那时起, 我就看到了Flutter在开发者, 社区和公司中的采用. 大多数新兴开发人员都曾多次讨论过这个问题:



为什么大公司不使用 Flutter?



因此, 这篇小文只是我对这个问题的个人观察和回答.


转向新技术既困难又复杂, 而从最新技术重新开始则很容易. 这(据我观察)也是为什么稳定的大公司不会将其长期使用的应用程序迁移到新技术的最大原因之一, 除非它能带来惊人的利润.


你会发现大多数初创公司都在使用 Flutter, 这是因为 90% 以上的跨平台应用程序新创意都可以在Flutter中以经济高效的方式轻松实现. Flutter中的一切都非常快速, 令人惊叹, 而且具有我们在过去几年的Flutter之旅中听说过的所有令人惊叹的优点.


那么问题来了:


既然Flutter如此令人惊叹, 高性价比, 单一代码库, 更轻松的单一团队管理, 令人愉悦的开发者体验, 等等等等; 那么为什么大公司不将他们的应用程序迁移到Flutter呢? 从头开始迁移或重写, 拥有单一团队, 单一代码, 这不就是天堂么?



不, 没那么简单.



问题所在: 业务vs技术热情


Flutter 令人惊叹, 你最终可以说服开发人员在公司中使用Flutter构建应用程序. 问题在于公司的运营业务. 企业希望Flutter能立即为业务做出贡献. 他们不希望等待自己的团队完全重写应用程序, 然后将其付诸实践以繁荣业务.


但正如我前面所说, 对于技术团队来说, 重写是最理想的. 因此, 这是一个可以由公司利益相关者共同思考的问题. 公司内部需要在分析领域, 业务类型, 团队文化等所有因素后, 找到一个中间立场.


无论如何, 让我们来看看这两种情况的结果如何.


从头开始重写: 技术方面


一家大公司拥有庞大的产品, 这些产品已融入流程和领域, 工作完美无瑕, 为企业完成了工作.


从技术上讲, 对首席技术官来说, 最好的办法是在Flutter上从头开始重写应用程序, 并将其完成. 但是, 如果他们决定这样做, 就必须雇佣一个全新的Flutter开发团队, 向他们解释当前产品/领域的所有情况, 并让他们重写应用程序. 这看起来很容易, 其实不然. 当前代码库中有很多内部知识必须传授给新团队, 这样他们才能为应用程序构建完全相同的体验/UI/UX/流程.



为什么构建完全相同的东西如此重要?



这是因为用户总有一天会第一次收到Flutter构建的应用程序更新, 这对于一个拥有成千上万用户的应用程序来说是非常危险的.


其次, 新功能正在当前应用的基础上构建, 重写后的应用可能无法赶上当前应用. 但这是一个商业决策(是停止新开发并先进行重写, 还是继续在当前应用程序中添加功能, 无论如何都要权衡利弊).


每家公司在领域, 文化, 人员, 领导力, 思维过程, 智囊团等方面都是独一无二的. 内部可能存在数以百计的挑战, 只有进入系统后才能了解. 你不可能对每家科技公司都提出一个单一的看法.


从头开始重写: 业务方面


公司在采用新事物时, 有一个非常重要的想法:



在重写的过程中, 业务不仅不应受到影响, 而且还应保持增长.



这意味着你不能在运行中的应用程序的功能和开发上妥协. 在重写版本中, 运行正常的程序不应出现错误. 为确保这一点, 需要进行严格的原子级测试, 以确保用户从一开始就掌握的功能不会出现任何问题(我们谈论的是大公司, 这意味着应用程序已运行多年). 我们可以进行单元/集成/用户界面测试, 但当它关系到业务, 金钱和用户时, 没有人会愿意冒这个险.


简而言之, 大多数稳定的公司不会决定从头开始用Flutter重写他们稳定的应用程序. 如果是基于项目的公司, 他们可能会使用Flutter启动新赢得的客户项目.


迁移(业务上友好, 技术上却不友好)


公司决定迁移到Flutter的另一种方式是逐屏迁移到 Flutter, 并使用与本地端(Talabat 的应用程序)通信的方法渠道. 对于技术团队来说, 这可能是一场噩梦, 但对于业务的持续运行, 以及让Flutter部分从一开始就为业务做出贡献来说, 这是最可行的方法(在重写过程中, 应用程序的Flutter部分除非上线到生产, 否则对业务没有任何用处).


作为一名读者和Flutter爱好者, 你可能会认为逐屏迁移非常了不起, 但实际上, 当你在一个每天有成千上万用户使用的生产应用程序中工作时, 这真的非常复杂. 这就像开颅手术.


总结一下


根据我的观察, 对于一家以产品为基础的公司来说, 决定将自己多年的移动开发技术栈转换为新的技术栈是非常困难的. 因此, 如果大公司真的决定转换, 这个决定本身确实值得称赞, 勇气可嘉, 也很有激励作用.


如果业务非常重要(如 Talabat, Foodpanda, 或涉及日常大量使用, 支付, 安全, 多供应商系统等的用户关键型应用程序), 那么从业务角度来看, 最理想的做法是以混合方式慢慢迁移应用程序. 同样, 这也不一定对所有人都可行, 重写可能更好. 这完全取决于公司和业务的结构以及决策的力度.


对于以项目为基础的公司来说, 使用Flutter启动新项目是最理想的选择(因为他们拥有热情洋溢, 不断壮大的团队, 并致力于融入新技术). 当他们使用Flutter构建新项目时, 如果交付速度比以前更快, 效率更高, 他们就会自动扩大业务.


对于开发人员和技术团队来说, 任何新技术都是令人惊叹的, 但如果你是一位在结构合理, 稳定的公司工作的工程师, 你也应该了解公司的业务视角, 从而理解他们对转用新技术的看法.


如果你是一名高级工程师/资深工程师, 你应该用他们更容易理解的语言向业务部门传达你的热情. 对于推介 Flutter, 可以是更少的时间, 更少的成本, 更少的努力, 更快的交付, 一个团队, 一个代码库, 更少的公关审查等(如果你是Flutter人员, 你已经知道了所有这些).



业务部门在做决定时必须考虑多个方面, 因此作为工程师, 要告诉他们一些能让他们更容易做出决定的事情.



以上是我的个人观点和看法, 如果你有不同的看法或经验, 请随时在评论中与我分享, 很乐意参与该问题的讨论.


Stay GOLD!


作者:bytebeats
来源:juejin.cn/post/7299731886498349107
收起阅读 »

阿里云又崩了,这次送什么

双十一的烟味还没散,昨天下午至晚间(11月12日),阿里云又崩了,而且语雀又没逃过去,从出现故障到完全恢复长达3个半小时,这次又是怎么回事呢? 这次受影响最大的是OSS服务,也就是阿里云的对象存储服务,对外提供的是图片、视频,以及各种文件的读写服务,可千万别小...
继续阅读 »

双十一的烟味还没散,昨天下午至晚间(11月12日),阿里云又崩了,而且语雀又没逃过去,从出现故障到完全恢复长达3个半小时,这次又是怎么回事呢?


这次受影响最大的是OSS服务,也就是阿里云的对象存储服务,对外提供的是图片、视频,以及各种文件的读写服务,可千万别小看这个服务,我们平常聊天发图片、发文件,网站上展示的图片、视频,以及各种js、css、字体等文件可能都是放在OSS服务中的。


从网友的反馈来看,仅阿里系受影响的产品就包括:阿里云OSS、语雀、钉钉、阿里云盘、淘宝、闲鱼等,应该还有些用户相对少的服务没有爆出来,毕竟阿里系产品众多,对象存储在每个产品中都是不可或缺的。



使用阿里云OSS服务的外部公司产品估计就更多了,只是大家量小体微,没有掀起什么狂风巨浪,我这里也多多少少受了一点影响,上传文件出错。



对于这次故障阿里云发了一个公告,文字不少,大家看看是否清楚明白。



根源是某个底层管控服务组件出现了问题,不过没说这个组件是干什么的,起到什么作用。再看受影响的产品还是挺多的,队列消息服务都被波及了。我猜测可能是底层存储的某个控制程序又出问题了,而且是个广泛部署的服务,可能是鉴权,否则不会影响这么广泛,恢复这么缓慢。


话说同样是阿里系的语雀前段时间刚崩了七个多小时,这么快阿里云又崩了,还恰逢双十一,再往前翻,去年阿里云香港机房还挂了两天,这不免让人对阿里的技术心生疑虑,加之社会对阿里的管理文化颇多吐槽,如此下去,阿里在人们的心中可能就要堕落下去了,可以说有向悬崖边滑落的风险。


频繁的出现问题,可能不仅仅是技术上的问题了,管理制度上可能也存在一些风险,相信阿里的王博士和周大佬也想得到,这里就不瞎指挥了。



不知道阿里云这次怎么给大家交代,看看网友们的期待吧。



不过我们使用阿里云的产品,以前多多少少都出现过一些问题,前几年产品经理会主动给些补偿,不过最近几年,影响不大的话,用户不要求,也少见阿里云主动补偿。对于此事,后续我会继续关注。


作者:萤火架构
来源:juejin.cn/post/7300550397781147657
收起阅读 »

30岁之前透支,30岁之后还债。

前言 看到不少私信问我为啥没有更新了,我没有一一回复,实在是身体抱恙,心情沉重,加上应付于工作,周旋于家庭,自然挤压了自我空间。 今天思来想去,重新执键,决定久违地又一次写点分享,奉劝大家珍惜身体,愉悦生活。 愉悦二字说来容易,但各位都一样,奔波于现实,劳累于...
继续阅读 »

前言


看到不少私信问我为啥没有更新了,我没有一一回复,实在是身体抱恙,心情沉重,加上应付于工作,周旋于家庭,自然挤压了自我空间。


今天思来想去,重新执键,决定久违地又一次写点分享,奉劝大家珍惜身体,愉悦生活。


愉悦二字说来容易,但各位都一样,奔波于现实,劳累于生活,岂是三言两语就能改变的。


病来如山倒


我又病了,有些意外和突然的,令我措手不及。


一天早上我起来,脖子有些酸,就伸手揉揉捏捏,忽然发现脖颈左侧有一个肿块,仔细拿捏,发现竟然是在里面,而且硬邦邦的,伴有轻微的疼痛感。


当时早上对着镜子拍下来的肿块,我还保留了照片。


1.jpg


立马便一身冷汗冒出,我从未经历过这样的事情,去年身体毕竟出过问题,两相叠加之下,内心更是难以描述。


因为是周一,怀着忐忑的心情去上班了,接下来一直都有些神经兮兮,觉得自己身体出了大问题。


之前我有文章讲过自己去年其实已经检查出血脂的问题,停更半年之久,调养了一番,才真正感觉到身体有所恢复,根据我发文的日期可见一二。


恢复更新的这段时间,报复式地写作和分享,一度不知不觉地排到榜单第二,今天登录看了一下,居然还在月榜前三没下来,也是意外。


话说回来,人一旦身体冒出点病痛,整个心情都显得低沉萎靡,很快就能在方方面面反应出来。


我是硬着头皮上班的,抽空网上查了下好让自己有个心理准备。



百度一搜便是绝症,这是很多年前就知道的,但病急乱投医果然是人之本性,我毅然决然还是搜了。


然后,各种甲状腺之类的就来了,再搜,淋巴瘤也来了,再搜,好家伙,直接恶性肿瘤十有八九了。



面对未知而产生的接近绝望的心情,想必不少人有类似经验。


比如我,下意识先想到的竟然不是我是不是要完蛋了,而是想到自己是家中独子,父母年迈身体有恙,妻子操劳,孩子尚小,家中主要经济来源也是我。


我一旦倒下,实在不敢想,往深了一想各种负面因子都蜂拥而来。


我不知道有多少人和我的性格相似,就是身体出了这种未知的问题,一面觉得应该去医院看看,一面又怕折腾来去最后拿到最不可接受的结果,可能不知道反而能活久一点,大概就是这种心情了。


是的,我大体是个胆子还算大的人,也猛然间抗拒去医院了。


不去医院的结果,就是你每天都在意这个肿块,每天都要摸摸它是不是变小了,是不是消失了,每天都小心呵护着它,甚至还想对它说说话倾诉一下,像是自己偷养的小情人一样。


只盼着某天睡觉醒来,用手一摸,哈哈没有了这样。


我就是差不多一个月都这样惶惶不可终日地度过,直到这周六才被妻子赶去医院做了检查。


透支和还债



30岁之前透支,30岁之后还债。



说来好笑,摸到肿块的第二天吧,还有朋友私信找我合作,换做平时,我肯定欣然接受,并开始设计文稿。


但身体有问题,一切都索然无味了,再次真切地体会到这种被现实打碎一切欲望的撕裂感。


2.png


为什么我30岁之后身体慢慢开始出现各种问题,这两年我有静下心来思考过。


到底还是30岁之前透支太多了,30岁之后你依然养成30岁之前的生活习惯,无异于自杀行为。



我把身体比作一根橡皮筋,它大概只能扯那么长,我长期将它扯那么那么长,我以为它没事,直到有一次我将它扯那么那么那么长,砰的一声它就断了。


我们都无法知道自己的这根橡皮筋到底能扯多长,只要它没断,我们都觉得它还能扯很长,代价就是,只需断一次,你再也无法重来了。



30岁之前,我努力学习各种知识,熬夜那是家常便饭,睡一觉便生龙活虎。


我就像以前上学的三好学生一样,在学校我扎扎实实,放学了我还进补习班,补习班回来了我还上网学知识。


回头想想,真特么离谱啊,我上学都没这样,走上社会了竟然付出了之前在学校几倍的努力。


早知如此,我好好上学读书最后进入一个更优质的圈子,不就少走很多弯路了吗,但是谁又会听当年的老师和父母一番肺腑之言呢。


埋怨过去没有什么意义,只能偶尔借着都市小说幻想一下带着记忆重生回校园的自己。


细数下来,我30岁之前熬过的夜比我加的班还多,我不是天天加班,但好像真的天天熬夜。


可我身体一点问题都没有,我觉得自己不是那种被命运抛弃的人,内心一直这么侥幸,你是不是也和我一样呢。


30岁之后,该来的还是来了,32岁那年,我有一次咳嗽入院,反复高烧,退了又发烧,医生一度以为是新冠,或结核,或白血病什么的,后来全部检查了都不是,发现就是普通的肺部感染。


每天两瓶抗病毒的点滴,大概半个月才逐渐恢复,人都瘦脱相了,这是我人生头一次住院,躺在病床上像废人一样。


等到33岁也就是去年,偶然头晕了一次,那种眩晕,天旋地转,犯恶心,怎么站怎么坐怎么躺都不行,真正要死的感觉。


后面我一度以为是年纪轻轻得了高血压,结果查了下是血脂的问题,还不算严重,但继续下去很可能会变成一些心脑血管疾病。


我难以置信,这可都是老年病啊,我一个30几岁的程序员说来就来了?


调养半年多,肉眼可见身体有好转,我又开始没忍住熬夜了,想做自己的课题,想分享更多的东西,这些都要花时间,而且包括一些其他领域的内容,想得太多,自然花的时间就多。


一不小心就连续熬了一个多月,平均每晚都是2点左右躺下,有时中午还不午休,刷手机找素材。


终于,脖子上起了肿块,让我整个人都蒙圈了,觉得一切努力都是在玩弄自己,忽然间什么都没意思了。


我尽量把这种感受描述出来,希望你们能看明白,真切体会一二。


为什么30岁之后我一熬夜就有问题出现,说白了,30岁之前透支了已经,一来是身体负荷达到临界,二来养成了多年的坏习惯,一时想改还改不过来。



30岁之前真别玩弄自己的身体了xdm,橡皮筋断了就真断了,接不上了,接上了也没以前的弹性了。



健康取决于自律和心情



对于程序员来说,健康取决于两点:自律和心情。



30岁之前,请学会自律,学习时间自律,生活作息自律,一日三餐自律,养成这样的习惯,30岁之后的你会受益匪浅。


自律真的很难,我就是一个很难做到的人,我有倔强地适应过,却又悲哀地失败了。


就像你是一个歇斯底里的人,忽然让你温文尔雅,你又能坚持多久呢。


我用很多鸡汤说服过自己,对于已经30几岁的我来说,也只能维持一段时间。


想看的多,想玩的多,想学的也多,时间是真不够啊,真想向天再借五百年。


我应该算是幸运的那一类,至少我这般透支身体,我还活着,也没用余生去直面绝望。



我用这两年的身体故障给自己上了重要的一课,人死如灯灭。



如果能重来,我一定会学习时间规划,我一定会把每天的时间安排的好好的。


我一定会保证一日三餐不落下,少吃外卖,多吃水果蔬菜。


我一定会保证每晚充足的睡眠,早睡早起,绝不熬夜。


我一定会每天下班和放假抽出一些时间运动和锻炼。


我不是说给自己听的,因为我已经透支了。


我是说给在看文章的你们听的,还年轻点的,还没透支的,请用我的现在当做你可能更坏的未来,早点醒悟,为时不晚。


自律很难,但不自律可能等死,这个选择一点也不难。



工作压力大,作为程序员是避免不了的,所以我以前有劝过大家,薪水的重要性只占一半,你应该追寻一份薪水尚可,但压力一定在承受范围内的工作,这是我认为在国内对于程序员来说相对友好的途径。



我进入IT行业目前为止的整个生涯中,学习阶段听到过传智播客张孝祥老师的猝死,工作阶段听说过附近的4396游戏公司里面30多岁程序员猝死,今年又听到了左耳朵耗子先生的离世。


我想着,那一天,离我和你还有多远。


心情真的很重要,至少能快速反应在身体上。


当我这周六被妻子劝说去检查的时候,我内心一直是紧张的,妻子没去,就在家陪着孩子,跟我说你自己去吧,如果有坏消息就别回复了,等回来再说,如果没什么事那就发个微信。


我想我理解她的意思了,点了点头就骑车去了医院。


医院真不是什么好地方,我就是给医院干活的,我全身上下都讨厌这里。


最煎熬的时间是做彩超前的一个多小时,因为人太多,我得排队,盯着大屏上的号序,我脑子里想了很多事情,甚至连最坏的打算都想好了。


人就很奇怪,越是接近黑暗,越是能回忆起非常多的往事,连高中打篮球挥洒汗水的模样和搞笑的投篮姿势都能想起来。


喊到我的时候,我心跳了一下,然后麻木地进去了,躺下的时候,医生拿着仪器对着我的脖子扫描,此时的我是近一个月以来第一次内心平静,当真好奇怪的感觉。


随着医生一句:没什么事,就一个淋巴结。


犹如审判一般,我感觉一下无罪释放了。


当时听到这句话简直犹如天籁,这会儿想起来还感觉毛孔都在欢快地愉悦。


我问她不是什么肿瘤或甲状腺吧,她说不是,就一个正常的淋巴结,可能是炎症导致了增生,这种一般3个多月至半年才会完全消掉。


这是当时拍的结果


3.jpg


拿给主任医师看了之后,对方也说一点事没有,只是告诫我别再熬夜了。


我不知道人生还会给我几次机会,但我从20几岁到30几岁,都没有重视过这个问题,也没有认真思考过。


直到最近,我才发现,活着真好。


当晚是睡得最踏实的一晚,一点梦都没做,中途也没醒,一觉到天亮。


更离谱的是,早上我摸了一下脖子,竟然真的小了点,这才短短一天,说了都没人信。


我头一次相信,心情真的会影响身体,你心情好了,身体的器官和血液仿佛都欢腾了起来。


如何保持一个好心情,原来这般重要,我拿自己的身体给大家做实验了,有用!



希望大家每天在自律的基础上保持好心情,不负年华,不负自己。



总结


xdm,好好活着,快乐活着。


作者:程序员济癫
来源:juejin.cn/post/7300564263344128051
收起阅读 »

DDD落地之架构分层

一.前言 DDD系列Demo被好多读者催更。肝了一周,参考了众多资料,与众多DDD领域的大佬进行了结构与理念的沟通后,终于完成了改良版的代码层次结构。 本文将给大家展开讲一讲 为什么我们要使用DDD? 到底什么样的系统适配DDD? DDD的代码怎么做,为什么...
继续阅读 »

一.前言


DDD系列Demo被好多读者催更。肝了一周,参考了众多资料,与众多DDD领域的大佬进行了结构与理念的沟通后,终于完成了改良版的代码层次结构。


本文将给大家展开讲一讲



  • 为什么我们要使用DDD?

  • 到底什么样的系统适配DDD?

  • DDD的代码怎么做,为什么要这么做?


你可以直接阅读本文,但我建议先阅读一文带你落地DDD,如果你对DDD已经有过了解与认知,请直接阅读。


干货直接上,点此查看demo代码,配合代码阅读本文,体验更深


DDD系列博客

  1. 一文带你落地DDD
  2. DDD落地之事件驱动模型
  3. DDD落地之仓储
  4. DDD落地之架构分层

二.为什么我们要使用DDD


虽然我在第一篇DDD的系列文:一文带你落地DDD中已经做过介绍我们使用DDD的理由。但是对于业务架构不太熟悉的同学还是无法get到DDD的优势是什么。



作为程序员嘛,我还是比较提倡大家多思考,多扎实自己的基础知识的。面试突击文虽香,但是,面试毕竟像是考试,更多时候我们还是需要在一家公司里面去工作。别人升职加薪,你怨声载道,最后跳槽加小几千没有意义嘛。



image.png


言归正传,我相信基本上99%的java开发读者,不管你是计科专业出身还是跨专业,初学spring或者springboot的时候,接触到的代码分层都是MVC。


这说明了MVC有它自身独有的优势:



  • 开发人员可以只关注整个结构中的其中某一层;

  • 可以很容易的用新的实现来替换原有层次的实现;

  • 可以降低之间的依赖;

  • 有利于标准化;

  • 利于各逻辑的复用。


但是真实情况是这样吗?随着你系统功能迭代,业务逻辑越来越复杂之后。MVC三层中,V层作为数据载体,C层作为逻辑路由都是很薄的一层,大量的代码都堆积在了M层(模型层)。一个service的类,动辄几百上千行,大的甚至几万行,逻辑嵌套复杂,主业务逻辑不清晰。service做的稍微轻量化一点的,代码就像是胶水,把数据库执行逻辑与控制返回给前端的逻辑胶在一起,主次不清晰。


一看你的工程,类啊,代码量啊都不少,你甚至不知道如何入手去修改“屎山”一样的代码。


归根到底的原因是什么?


image.png


service承载了它这个年纪不该承受的业务逻辑。


举个例子: 你负责了一个项目的从0到1的搭建,后面业务越来越好,招了新的研发进来。新的研发跟你一起开发,service层逻辑方法类似有不完全相同,为了偷懒,拷贝了你的代码,改了一小段逻辑。这时候基本上你的代码量已经是乘以2了。同理再来一个人,你的代码量可能乘了4。然而作为数据载体的POJO繁多,里面空空如也,你想把逻辑放进去,却发现无从入手。POJO的贫血模型陷入了恶性循环。


那么DDD为什么可以去解决以上的问题呢?


DDD核心思想是什么呢?解耦!让业务不是像炒大锅饭一样混在一起,而是一道道工序复杂的美食,都有他们自己独立的做法。


DDD的价值观里面,任何业务都是某个业务领域模型的职责体现。A领域只会去做A领域的事情,A领域想去修改B领域,需要找中介(防腐层)去对B领域完成操作。我想完成一个很长的业务逻辑动作,在划分好业务边界之后,交给业务服务的编排者(应用服务)去组织业务模型(聚合)完成逻辑。


这样,每个服务(领域)只会做自己业务边界内的事情,最小细粒度的去定义需求的实现。原先空空的贫血模型摇身一变变成了充血模型。原理冗长的service里面类似到处set,get值这种与业务逻辑无关的数据载体包装代码,都会被去除,进到应用服务层,你的代码就是你的业务逻辑。逻辑清晰,可维护性高!


三.到底什么样的系统适配DDD


看完上文对于DDD的分析之后是不是觉得MVC一对比简直就是垃圾。但是你回过头来想想,DDD其实在10几年前就已经被提出来了,但为什么是近几年才开始逐渐进入大众的视野?


相信没有看过我之前DDD的文章的同学看了我上面的分析大概也能感觉的到,DDD这个系统不像MVC结构那么简单,分层肯定更加复杂。


因此不是适配DDD的系统是什么呢?


中小规模的系统,本身业务体量小,功能单一,选择mvc架构无疑是最好的。


项目化交付的系统,研发周期短,一天到晚按照甲方的需求定制功能。


相反的,适配DDD的系统是什么呢?


中大规模系统,产品化模式,业务可持续迭代,可预见的业务逻辑复杂性的系统。


总而言之就是:


你还不了解DDD或者你们系统功能简单,就选择MVC.


你不知道选用什么技术架构做开发,业务探索阶段,选用MVC.


其他时候酌情考虑上DDD。


四.DDD的代码怎么做,为什么要这么做


4.1.经典分层


image-20210913185730992.png
在用户界面层和业务逻辑层中间加了应用层(Application Layer) , 业务逻辑层改为领域层, 数据访问层改为基础设施层(Infrastructure Layer) , 突破之前数据库访问的限制。 固有的思维中,依赖是自顶向下传递的,用户界面依赖应用层,应用层依赖领域层和基础设施层,越往下的层,与业务越远,并更加通用;出于重用的考虑,通用的功能会剥离成框架或者平台,而在低层次(基础设施层)会调用、依赖这些框架,也就导致了业务对象(领域层)依赖外部平台或框架。


4.2.依赖倒置分层


image-20210913190943631.png


为了突破这种违背本身业务领域的依赖,将基础设施往上提,当领域服务与基础设置有交集时,定义一个接口(灰度接口),让基础设施去实现对应的接口。接口本身是介于应用服务与领域服务之间的,为了纯净化领域层而存在。


Image.png


这么做的好处就是,从分包逻辑来看,上层依赖下层,底层业务域不依赖任何一方,领域独立。


4.3.DDD分层请求调用链


未命名文件.png


4.3.1.增删改


1.用户交互层发起请求


2.应用服务层编排业务逻辑【仅做方法编排,不处理任何逻辑】


3.编排逻辑如果依赖三方rpc,则定义adapter,方式三方服务字段影响到本服务。


4.编排逻辑如果依赖其他领域服务,应用服务,可直接调用,无需转化。但是与当前框架不相符合的,例如发送短信这种,最好还是走一下适配器,运营商换了,依赖的应用服务没准都不同了。


5.聚合根本身无法处理的业务在领域层处理,依赖倒置原则,建立一层interfaces层(灰度防腐层),放置领域层与基础设置的耦合。


6.逻辑处理结束,调用仓储聚合方法。


4.3.2.查询


CQRS模型,与增删改不同的应用服务,是查询应用服务。不必遵守DDD分层规则(不会对数据做修改)。简单逻辑甚至可以直接由controller层调用仓储层返回数据。


五.总结


其实DDD在分层上从始至终一致在贯穿的一个逻辑就是,解耦。如果真的极端推崇者,每一层,每一步都会增加一个适配器。我觉得这个对于研发来说实在太痛苦了,还是要在架构与实际研发上做一个中和。


六.特别鸣谢


lilpilot


image.png


作者:柏炎
来源:juejin.cn/post/7007382308667785253
收起阅读 »

DDD落地之仓储

一.前言 hello,everyone。又到了周末了,没有出去玩,继续肝。从评论与粉丝私下的联系来看,大家对于DDD架构的热情都比较高。但是因为抽象化的概念较多,因此理解上就很困难。 昨天媳妇儿生病了在医院,她挂点滴的时候,我也没闲下来,抓紧时间做出了DDD的...
继续阅读 »

一.前言


hello,everyone。又到了周末了,没有出去玩,继续肝。从评论与粉丝私下的联系来看,大家对于DDD架构的热情都比较高。但是因为抽象化的概念较多,因此理解上就很困难。


昨天媳妇儿生病了在医院,她挂点滴的时候,我也没闲下来,抓紧时间做出了DDD的第一版demo,就冲这点,


大家点个关注,点个赞,不过分吧。


image.png


这个项目我会持续维护,针对读者提出的issue与相关功能点的增加,我都会持续的补充。


查看demo


DDD系列博客

  1. 一文带你落地DDD
  2. DDD落地之事件驱动模型
  3. DDD落地之仓储
  4. DDD落地之架构分层

本文将给大家介绍的同样是DDD中的一个比较好理解与落地的知识点-仓储



本系列为MVC框架迁移至DDD,考虑到国内各大公司内还是以mybatis作为主流进行业务开发。因此,demo中的迁移与本文的相关实例均以mybatis进行演示。至于应用仓储选型是mybatis还是jpa,文中会进行分析,请各位仔细阅读本文。


二.仓储


2.1.仓储是什么


原著《领域驱动设计:软件核心复杂性应对之道》 中对仓储的有关解释:



为每种需要全局访问的对象类型创建一个对象,这个对象就相当于该类型的所有对象在内存中的一个集合的“替身”。通过一个众所周知的接口来提供访问。提供添加和删除对象的方法,用这些方法来封装在数据存储中实际插入或删除数据的操作。提供根据具体标准来挑选对象的方法,并返回属性值满足查询标准的对象或对象集合(所返回的对象是完全实例化的),从而将实际的存储和查询技术封装起来。只为那些确实需要直接访问的Aggregate提供Repository。让客户始终聚焦于型,而将所有对象存储和访问操作交给Repository来完成。



上文通俗的讲,当领域模型一旦建立之后,你不应该关心领域模型的存取方式。仓储就相当于一个功能强大的仓库,你告诉他唯一标识:例如订单id,它就能把所有你想要数据按照设置的领域模型一口气组装返回给你。存储时也一样,你把整块订单数据给他,至于它怎么拆分,放到什么存储介质【DB,Redis,ES等等】,这都不是你业务应该关心的事。你完全信任它能帮助你完成数据管理工作。


2.2.为什么要用仓储


先说贫血模型的缺点:



有小伙伴之前提出过不知道贫血模型的定义,这里做一下解释。贫血模型:PO,DTO,VO这种常见的业务POJO,都是数据java里面的数据载体,内部没有任何的业务逻辑。所有业务逻辑都被定义在各种service里面,service做了各种模型之间的各种逻辑处理,臃肿且逻辑不清晰。充血模型:建立领域模型形成聚合根,在聚合根即表示业务,在聚合内部定义当前领域内的业务处理方法与逻辑。将散落的逻辑进行收紧。




  1. 无法保护模型对象的完整性和一致性: 因为对象的所有属性都是公开的,只能由调用方来维护模型的一致性,而这个是没有保障的;之前曾经出现的案例就是调用方没有能维护模型数据的一致性,导致脏数据使用时出现bug,这一类的 bug还特别隐蔽,很难排查到。

  2. 对象操作的可发现性极差: 单纯从对象的属性上很难看出来都有哪些业务逻辑,什么时候可以被调用,以及可以赋值的边界是什么;比如说,Long类型的值是否可以是0或者负数?

  3. 代码逻辑重复: 比如校验逻辑、计算逻辑,都很容易出现在多个服务、多个代码块里,提升维护成本和bug出现的概率;一类常见的bug就是当贫血模型变更后,校验逻辑由于出现在多个地方,没有能跟着变,导致校验失败或失效。

  4. 代码的健壮性差: 比如一个数据模型的变化可能导致从上到下的所有代码的变更。

  5. 强依赖底层实现: 业务代码里强依赖了底层数据库、网络/中间件协议、第三方服务等,造成核心逻辑代码的僵化且维护成本高。


image.png


虽然贫血模型有很大的缺陷,但是在我们日常的代码中,我见过的99%的代码都是基于贫血模型,为什么呢?



  1. 数据库思维: 从有了数据库的那一天起,开发人员的思考方式就逐渐从写业务逻辑转变为了写数据库逻辑,也就是我们经常说的在写CRUD代码

  2. 贫血模型“简单”: 贫血模型的优势在于“简单”,仅仅是对数据库表的字段映射,所以可以从前到后用统一格式串通。这里简单打了引号,是因为它只是表面上的简单,实际上当未来有模型变更时,你会发现其实并不简单,每次变更都是非常复杂的事情

  3. 脚本思维: 很多常见的代码都属于脚本胶水代码,也就是流程式代码。脚本代码的好处就是比较容易理解,但长久来看缺乏健壮性,维护成本会越来越高。


但是可能最核心的原因在于,实际上我们在日常开发中,混淆了两个概念:



  • 数据模型(Data Model): 指业务数据该如何持久化,以及数据之间的关系,也就是传统的ER模型。

  • 业务模型/领域模型(Domain Model): 指业务逻辑中,相关联的数据该如何联动。


所以,解决这个问题的根本方案,就是要在代码里区分Data Model和Domain Model,具体的规范会在后文详细描述。在真实代码结构中,Data Model和 Domain Model实际上会分别在不同的层里,Data Model只存在于数据层,而Domain Model在领域层,而链接了这两层的关键对象,就是Repository。


能够隔离我们的软件(业务逻辑)和固件/硬件(DAO、DB),让我们的软件变得更加健壮,而这个就是Repository的核心价值。


image.png


三.落地


3.1.落地概念图


1.png


DTO Assembler: 在Application层 【应用服务层】EntityDTO的转化器有一个标准的名称叫DTO Assembler 【汇编器】



DTO Assembler的核心作用就是将1个或多个相关联的Entity转化为1个或多个DTO。



Data Converter: 在Infrastructure层 【基础设施层】EntityDO的转化器没有一个标准名称,但是为了区分Data Mapper,我们叫这种转化器Data Converter。这里要注意Data Mapper通常情况下指的是DAO,比如Mybatis的Mapper。


3.2.Repository规范


首先聚合仓储之间是一一对应的关系。仓储只是一种持久化的手段,不应该包含任何业务操作。




  1. 接口名称不应该使用底层实现的语法


    定义仓储接口,接口中有save类似的方法,与面向集合的仓储的不同点:面向集合的仓储只有在新增时调用add即可,面向持久化的无论是新增还是修改都要调用save




  2. 出参入参不应该使用底层数据格式:


    需要记得的是 Repository 操作的是 Entity 对象(实际上应该是Aggregate Root),而不应该直接操作底层的 DO 。更近一步,Repository 接口实际上应该存在于Domain层,根本看不到 DO 的实现。这个也是为了避免底层实现逻辑渗透到业务代码中的强保障。




  3. 应该避免所谓的“通用”Repository模式


    很多 ORM 框架都提供一个“通用”的Repository接口,然后框架通过注解自动实现接口,比较典型的例子是Spring Data、Entity Framework等,这种框架的好处是在简单场景下很容易通过配置实现,但是坏处是基本上无扩展的可能性(比如加定制缓存逻辑),在未来有可能还是会被推翻重做。当然,这里避免通用不代表不能有基础接口和通用的帮助类




  4. 不要在仓储里面编写业务逻辑


    首先要清楚的是,仓储是存在基础设施层的,并不会去依赖上层的应用服务,领域服务等。




图片1.png


仓储内部仅能依赖mapper,es,redis这种存储介质包装框架的工具类。save动作,仅对传入的聚合根进行解析放入不同的存储介质,你想放入redis,数据库还是es,由converter来完成聚合根的转换解析。同样,从不同的存储介质中查询得到的数据,交给converter来组装。




  1. 不要在仓储内控制事务


    你的仓储用于管理的是单个聚合,事务的控制应该取决于业务逻辑的完成情况,而不是数据存储与更新情况。




3.3.CQRS仓储


2222.png
回顾一下这张图,可以发现增删改数据模型走了DDD模型。而查询则从应用服务层直接穿透到了基础设施层。


这就是CQRS模型,从数据角度来看,增删改数据非幂等操作,任何一个动作都能对数据进行改动,称为危险行为。而查询,不会因为你查询次数的改变,而去修改到数据,称为安全行为。而往往功能迭代过程中,数据修改的逻辑还是复杂的,因此建模也都是针对于增删改数据而言的。


那么查询数据有什么原则吗?




  1. 构建独立仓储


    查询的仓储与DDD中的仓储应该是两个方法,互相独立。DDD中的仓储方法严格意义上只有三个:save,delete,byId,内部没有业务逻辑,仅对数据做拆分组合。查询仓储方法可以根据用户需求,研发需求来自定义仓储返回的数据结构,不限制返回的数据结构为聚合,可以是限界范围内的任意自定义结构。




  2. 不要越权


    不要再查询仓储内做太多的sql逻辑,数据查询组装交给assember。




  3. 利用好assember


    类似于首页,一个接口可能返回的数据来源于不同的领域,甚至有可能不是自己本身业务服务内部的。


    这种复杂的结果集,交给assember来完成最终结果集的组装与返回。结构足够简单的情况下,用户交互层【controller,mq,rpc】甚至可以直接查询仓储的结果进行返回。


    当然还有很多其他博文中会说,如果查询结果足够简单,甚至可以直接在controller层调用mapper查询结果返回。除非你是一个固定的字典服务或者规则表,否则哪怕业务再简单,你的业务也会迭代,后续查询模型变化了,dao层里面的查询逻辑就外溢到用户交互层,显然得不偿失。




3.4.ORM框架选型


目前主流使用的orm框架就是mybatis与jpa。国内使用mybatis多,国外使用jpa多。两者框架上的比较本文不做展开,不清楚两个框架实现差异的,可以自行百度。


那么我们如果做DDD建模的话到底选择哪一种orm框架更好呢?


mybatis是一个半自动框架(当然现在有mybatis-plus的存在,mybatis也可以说是跻身到全自动框架里面了),国内使用它作为orm框架是主流。为什么它是主流,因为它足够简单,设计完表结构之后,映射好字段就可以进行开发了,业务逻辑可以用胶水一个个粘起来。而且在架构支持上,mybatis不支持实体嵌套实体,这个在领域模型建模结束后的应用上就优于mybatis。


当然我们今天讨论的是架构,任何时候,技术选型不是决定我们技术架构的关键性因素


jpa天生就具备做DDD的优势。但是这并不意味着mybatis就做不了DDD了,我们完全可以将领域模型的定义与orm框架的应用分离,单独定义converter去实现领域模型与数据模型之间的转换,demo中我也是这么给大家演示的。


image.png




当然,如果是新系统或者迁移时间足够多,我还是推荐使用JPA的,红红火火恍恍惚惚~


image.png


四.demo演示


需求描述,用户领域有四个业务场景



  1. 新增用户

  2. 修改用户

  3. 删除用户

  4. 用户数据在列表页分页展示



核心实现演示,不贴全部代码,完整demo可从文章开头的github仓库获取



4.1.领域模型


/**
* 用户聚合根
*
*
@author baiyan
*/

@Getter
@NoArgsConstructor
public class User extends BaseUuidEntity implements AggregateRoot {

  /**
    * 用户名
    */

  private String userName;

  /**
    * 用户真实名称
    */

  private String realName;

  /**
    * 用户手机号
    */

  private String phone;

  /**
    * 用户密码
    */

  private String password;

  /**
    * 用户地址
    */

  private Address address;

  /**
    * 用户单位
    */

  private Unit unit;

  /**
    * 角色
    */

  private List roles;

  /**
    * 新建用户
    *
    *
@param command 新建用户指令
    */

  public User(CreateUserCommand command){
      this.userName = command.getUserName();
      this.realName = command.getRealName();
      this.phone = command.getPhone();
      this.password = command.getPassword();
      this.setAddress(command.getProvince(),command.getCity(),command.getCounty());
      this.relativeRoleByRoleId(command.getRoles());
  }

  /**
    * 修改用户
    *
    *
@param command 修改用户指令
    */

  public User(UpdateUserCommand command){
      this.setId(command.getUserId());
      this.userName = command.getUserName();
      this.realName = command.getRealName();
      this.phone = command.getPhone();
      this.setAddress(command.getProvince(),command.getCity(),command.getCounty());
      this.relativeRoleByRoleId(command.getRoles());
  }

  /**
    * 组装聚合
    *
    *
@param userPO
    *
@param roles
    */

  public User(UserPO userPO, List roles){
      this.setId(userPO.getId());
      this.setDeleted(userPO.getDeleted());
      this.setGmtCreate(userPO.getGmtCreate());
      this.setGmtModified(userPO.getGmtModified());
      this.userName = userPO.getUserName();
      this.realName = userPO.getRealName();
      this.phone = userPO.getPhone();
      this.password = userPO.getPassword();
      this.setAddress(userPO.getProvince(),userPO.getCity(),userPO.getCounty());
      this.relativeRoleByRolePO(roles);
      this.setUnit(userPO.getUnitId(),userPO.getUnitName());
  }

  /**
    * 根据角色id设置角色信息
    *
    *
@param roleIds 角色id
    */

  public void relativeRoleByRoleId(List<Long> roleIds){
      this.roles = roleIds.stream()
              .map(roleId->new Role(roleId,null,null))
              .collect(Collectors.toList());
  }

  /**
    * 设置角色信息
    *
    *
@param roles
    */

  public void relativeRoleByRolePO(List roles){
      if(CollUtil.isEmpty(roles)){
          return;
      }
      this.roles = roles.stream()
              .map(e->new Role(e.getId(),e.getCode(),e.getName()))
              .collect(Collectors.toList());
  }

  /**
    * 设置用户地址信息
    *
    *
@param province 省
    *
@param city 市
    *
@param county 区
    */

  public void setAddress(String province,String city,String county){
      this.address = new Address(province,city,county);
  }

  /**
    * 设置用户单位信息
    *
    *
@param unitId
    *
@param unitName
    */

  public void setUnit(Long unitId,String unitName){
      this.unit = new Unit(unitId,unitName);
  }

}

4.2.DDD仓储实现


/**
*
* 用户领域仓储
*
* @author baiyan
*/

@Repository
public class UserRepositoryImpl implements UserRepository {

  @Autowired
  private UserMapper userMapper;

  @Autowired
  private RoleMapper roleMapper;

  @Autowired
  private UserRoleMapper userRoleMapper;

  @Override
  public void delete(Long id){
      userRoleMapper.delete(Wrappers.lambdaQuery().eq(UserRolePO::getUserId,id));
      userMapper.deleteById(id);
  }

  @Override
  public User byId(Long id){
      UserPO user = userMapper.selectById(id);
      if(Objects.isNull(user)){
          return null;
      }
      List userRoles = userRoleMapper.selectList(Wrappers.lambdaQuery()
              .eq(UserRolePO::getUserId, id).select(UserRolePO::getRoleId));
      List roleIds = CollUtil.isEmpty(userRoles) ? new ArrayList<>() : userRoles.stream()
              .map(UserRolePO::getRoleId)
              .collect(Collectors.toList());
      List roles = roleMapper.selectBatchIds(roleIds);
      return UserConverter.deserialize(user,roles);
  }


  @Override
  public User save(User user){
      UserPO userPo = UserConverter.serializeUser(user);
      if(Objects.isNull(user.getId())){
          userMapper.insert(userPo);
          user.setId(userPo.getId());
      }else {
          userMapper.updateById(userPo);
          userRoleMapper.delete(Wrappers.lambdaQuery().eq(UserRolePO::getUserId,user.getId()));
      }
      List userRolePos = UserConverter.serializeRole(user);
      userRolePos.forEach(userRoleMapper::insert);
      return this.byId(user.getId());
  }

}

4.3.查询仓储


/**
*
* 用户信息查询仓储
*
*
@author baiyan
*/

@Repository
public class UserQueryRepositoryImpl implements UserQueryRepository {

  @Autowired
  private UserMapper userMapper;

  @Override
  public Page<UserPageDTO> userPage(KeywordQuery query){
      Page<UserPO> userPos = userMapper.userPage(query);
      return UserConverter.serializeUserPage(userPos);
  }

}

五.mybatis迁移方案


以OrderDO与OrderDAO的业务场景为例



  1. 生成Order实体类,初期字段可以和OrderDO保持一致

  2. 生成OrderDataConverter,通过MapStruct基本上2行代码就能完成

  3. 写单元测试,确保Order和OrderDO之间的转化100%正确

  4. 生成OrderRepository接口和实现,通过单测确保OrderRepository的正确性

  5. 将原有代码里使用了OrderDO的地方改为Order

  6. 将原有代码里使用了OrderDAO的地方都改为用OrderRepository

  7. 通过单测确保业务逻辑的一致性。


六.总结



  1. 数据模型与领域模型需要正确区分,仓储是它们互相转换的抽象实现。

  2. 仓储对业务层屏蔽实现,即领域层不需要关注领域对象如何持久化。

  3. 仓储是一个契约,而不是数据访问层。它明确表明聚合所必需的数据操作。

  4. 仓储用于管理单个聚合,它不应该控制事务。

  5. ORM框架选型在迁移过程中不可决定性因此,可以嫁接转换器,但是还是优先推荐JPA。

  6. 查询仓储可以突破DDD边界,用户交互层可以直接进行查询。


七.特别鸣谢


lilpilot


image.png


作者:柏炎
来源:juejin.cn/post/7006595886646034463
收起阅读 »

DDD落地之事件驱动模型

一.前言 hello,everyone。一日不见,如隔24小时。 周末的时候写了一文带你落地DDD,发现大家对于新的领域与知识都挺感兴趣的。后面将会出几篇DDD系列文章给大家介绍mvc迁移DDD实际要做的一些步骤。 DDD系列博客一文带你落地DDDDDD落地...
继续阅读 »

一.前言


hello,everyone。一日不见,如隔24小时。


image.png


周末的时候写了一文带你落地DDD,发现大家对于新的领域与知识都挺感兴趣的。后面将会出几篇DDD系列文章给大家介绍mvc迁移DDD实际要做的一些步骤。


DDD系列博客

  1. 一文带你落地DDD
  2. DDD落地之事件驱动模型
  3. DDD落地之仓储
  4. DDD落地之架构分层

DDD的理念中有一个是贯穿始终的,业务边界与解耦。我最开始不了解DDD的时候,我就觉得事件驱动模型能够非常好的解耦系统功能。当然,这个是我比较菜,在接触DDD之后才开始对事件驱动模型做深度应用与了解。其实无论是在spring的框架中还是在日常MVC代码的编写过程中,巧用事件驱动模型都能很好的提高代码的可维护性。


image.png


因此,本文将对DDD中使用事件驱动模型建立与踩坑做一个系统性的介绍。从应用层面出发,帮助大家更好的去进行架构迁移。



我的第一本掘金小册《深入浅出DDD》已经在掘金上线,欢迎大家试读~



DDD的微信群我也已经建好了,由于文章内不能放二维码,大家可以加我微信baiyan_lou,备注DDD交流,我拉你进群,欢迎交流共同进步。


二.事件驱动模型


2.1.为什么需要事件驱动模型


一个框架,一门技术,使用之前首先要清楚,什么样的业务场景需要使用这个东西。为什么要用跟怎么样把他用好更加重要。


假设我们现在有一个比较庞大的单体服务的订单系统,有下面一个业务需求:创建订单后,需要下发优惠券,给用户增长积分


先看一下,大多数同学在单体服务内的写法。【假设订单,优惠券,积分均为独立service】


//在orderService内部定义一个放下
@Transactional(rollbackFor = Exception.class)
public void createOrder(CreateOrderCommand command){
 //创建订单
 Long orderId = this.doCreate(command);
 //发送优惠券
 couponService.sendCoupon(command,orderId);
 //增长积分
 integralService.increase(command.getUserId,orderId);
}

image.png


上面这样的代码在线上运行会不会有问题?不会!


image.png


那为什么要改呢?


原因是,业务需求在不断迭代的过程中,与当前业务非强相关的主流程业务,随时都有可能被替换或者升级。


双11大促,用户下单的同时需要给每个用户赠送几个小礼品,那你又要写一个函数了,拼接在主方法的后面。双11结束,这段要代码要被注释。有一年大促,赠送的东西改变,代码又要加回来。。。。


来来回回的,订单逻辑变得又臭又长,注释的代码逻辑很多还不好阅读与理解。


image.png


如果用了事件驱动模型,那么当第一步创建订单成功之后,发布一个创建订单成功的领域事件。优惠券服务,积分服务,赠送礼品等等监听这个事件,对监听到的事件作出相应的处理。


事件驱动模型代码


//在orderService内部定义一个放下
@Transactional(rollbackFor = Exception.class)
public void createOrder(CreateOrderCommand command){
//创建订单
Long orderId = this.doCreate(command);
publish(orderCreateEvent);
}

//各个需要监听的服务
public void handlerEvent(OrderCreateEvent event){
//逻辑处理
}

image.png


代码解耦,高度符合开闭原则


2.2.事件驱动模型选型


2.2.1.JDK中时间驱动机制


JDK为我们提供的事件驱动(EventListener、EventObject)、观察者模式(Observer)。


JDK不仅提供了Observable类、Observer接口支持观察者模式,而且也提供了EventObjectEventListener接口来支持事件监听模式。


观察者(Observer)相当于事件监听者(监听器) ,被观察者(Observable)相当于事件源和事件,执行逻辑时通知observer即可触发oberver的update,同时可传被观察者和参数。简化了事件-监听模式的实现


// 观察者,实现此接口即可
public interface Observer {

/**
* 当被观察的对象发生变化时候,这个方法会被调用
* Observable o:被观察的对象
* Object arg:传入的参数
**/

 void update(Observable o, Object arg);
}

// 它是一个Class
public class Observable {

 // 是否变化,决定了后面是否调用update方法
 private boolean changed = false;
 
 // 用来存放所有`观察自己的对象`的引用,以便逐个调用update方法
 // 需要注意的是:1.8的jdk源码为Vector(线程安全的),有版本的源码是ArrayList的集合实现;
 private Vector obs;

 public Observable() {
 obs = new Vector<>();
}

public synchronized void addObserver(Observer o); //添加一个观察者 注意调用的是addElement方法, 添加到末尾   所以执行时是倒序执行的
public synchronized void deleteObserver(Observer o);
public synchronized void deleteObservers(); //删除所有的观察者

// 循环调用所有的观察者的update方法
public void notifyObservers();
public void notifyObservers(Object arg);
 public synchronized int countObservers() {
 return obs.size();
}

 // 修改changed的值
 protected synchronized void setChanged() {
changed = true;
}
 
 protected synchronized void clearChanged() {
changed = false;
}
 
 public synchronized boolean hasChanged() {
return changed;
}
}

内部观察者队列啥的都交给Observable去处理了, 并且,它是线程安全的。但是这种方式其实使用起来并不是那么的方便,没有一个消息总线,需要自己单独去维护观察者与被观察者。对于业务系统而言,还需要自己单独去维护每一个观察者的添加。


2.2.2.spring中的事件驱动机制


spring在4.2之后提供了@EventListener注解,让我们更便捷的使用监听。


了解过spring启动流程的同学都知道,Spring容器刷新的时候会发布ContextRefreshedEvent事件,因此若我们需要监听此事件,直接写个监听类即可。


@Slf4j
@Component
public class ApplicationRefreshedEventListener implements ApplicationListener {

  @Override
  public void onApplicationEvent(ContextRefreshedEvent event) {
      //解析这个事件,做你想做的事,嘿嘿
  }
}

同样的我们也可以自己来定义一个事件,通过ApplicationEventPublisher发送。


/**
* 领域事件基类
*
*
@author baiyan
*
@date 2021/09/07
*/

@Getter
@Setter
@NoArgsConstructor
public abstract class BaseDomainEvent<T> implements Serializable {

  private static final long serialVersionUID = 1465328245048581896L;

  /**
    * 领域事件id
    */

  private String demandId;

  /**
    * 发生时间
    */

  private LocalDateTime occurredOn;

  /**
    * 领域事件数据
    */

  private T data;

  public BaseDomainEvent(String demandId, T data) {
      this.demandId = demandId;
      this.data = data;
      this.occurredOn = LocalDateTime.now();
  }

}

定义统一的业务总线发送事件


/**
* 领域事件发布接口
*
*
@author baiyan
*
@date 2021/09/07
*/

public interface DomainEventPublisher {

  /**
    * 发布事件
    *
    *
@param event 领域事件
    */

  void publishEvent(BaseDomainEvent event);

}

/**
* 领域事件发布实现类
*
* @author baiyan
* @date 2021/09/07
*/

@Component
@Slf4j
public class DomainEventPublisherImpl implements DomainEventPublisher {

  @Autowired
  private ApplicationEventPublisher applicationEventPublisher;

  @Override
  public void publishEvent(BaseDomainEvent event) {
      log.debug("发布事件,event:{}", event.toString());
      applicationEventPublisher.publishEvent(event);
  }

}

监听事件


@Component
@Slf4j
public class UserEventHandler {

  @EventListener
  public void handleEvent(DomainEvent event) {
      //doSomething
  }

}

芜湖,起飞~


image.png


相比较与JDK提供的观察者模型的事件驱动,spring提供的方式就是yyds。


2.3.事件驱动之事务管理


平时我们在完成某些数据的入库后,发布了一个事件。后续我们进行操作记录在es的记载,但是这时es可能集群响应超时了,操作记录入库失败报错。但是从业务逻辑上来看,操作记录的入库失败,不应该影响到主流程的逻辑执行,需要事务独立。亦或是,如果主流程执行出错了,那么我们需要触发一个事件,发送钉钉消息到群里进行线上业务监控,需要在主方法逻辑中抛出异常再调用此事件。这时,我们如果使用的是@EventListener,上述业务场景的实现就是比较麻烦的逻辑了。


为了解决上述问题,Spring为我们提供了两种方式:


(1)@TransactionalEventListener注解。


(2) 事务同步管理器TransactionSynchronizationManager


本文针对@TransactionalEventListener进行一下解析。


我们可以从命名上直接看出,它就是个EventListener,在Spring4.2+,有一种叫做@TransactionEventListener的方式,能够实现在控制事务的同时,完成对对事件的处理。


//被@EventListener标注,表示它能够监听事件
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EventListener
public @interface TransactionalEventListener {

//表示当前事件跟随消息发送方事务的出发时机,默认为消息发送方事务提交之后才进行处理。
  TransactionPhase phase() default TransactionPhase.AFTER_COMMIT;

  //true时不论发送方是否存在事务均出发当前事件处理逻辑
  boolean fallbackExecution() default false;

  //监听的事件具体类型,还是建议指定一下,避免监听到子类的一些情况出现
  @AliasFor(annotation = EventListener.class, attribute = "classes")
  Class[] value() default {};

  //指向@EventListener对应的值
  @AliasFor(annotation = EventListener.class, attribute = "classes")
  Class[] classes() default {};

  //指向@EventListener对应的值
  String condition() default "";

}

public enum TransactionPhase {
  // 指定目标方法在事务commit之前执行
  BEFORE_COMMIT,

  // 指定目标方法在事务commit之后执行
  AFTER_COMMIT,

  // 指定目标方法在事务rollback之后执行
  AFTER_ROLLBACK,

  // 指定目标方法在事务完成时执行,这里的完成是指无论事务是成功提交还是事务回滚了
  AFTER_COMPLETION
}

我们知道,Spring的事件监听机制(发布订阅模型)实际上并不是异步的(默认情况下),而是同步的来将代码进行解耦。而@TransactionEventListener仍是通过这种方式,但是加入了回调的方式来解决,这样就能够在事务进行Commited,Rollback…等时候才去进行Event的处理,来达到事务同步的目的。


三.实践及踩坑


针对是事件驱动模型里面的@TransactionEventListener@EventListener假设两个业务场景。


新增用户,关联角色,增加关联角色赋权操作记录。


1.统一事务:上述三个操作事务一体,无论哪个发生异常,数据统一回滚。


2独立事务:上述三个操作事务独立,事件一旦发布,后续发生任意异常均不影响。


3.1.统一事务


用户新增


@Service
@Slf4j
public class UserServiceImpl implements UserService {

  @Autowired
  DomainEventPublisher domainEventPublisher;

  @Transactional(rollbackFor = Exception.class)
  public void createUser(){
      //省略非关键代码
      save(user);
      domainEventPublisher.publishEvent(userEvent);
  }
}

用户角色关联


@Component
@Slf4j
public class UserEventHandler {

  @Autowired
  DomainEventPublisher domainEventPublisher;

  @Autowired
  UserRoleService userRoleService;

  @EventListener
  public void handleEvent(UserEvent event) {
      log.info("接受到用户新增事件:"+event.toString());
      //省略部分数据组装与解析逻辑
      userRoleService.save(userRole);
      domainEventPublisher.publishEvent(userRoleEvent);
  }

}

用户角色操作记录


@Component
@Slf4j
public class UserRoleEventHandler {

  @Autowired
  UserRoleRecordService userRoleRecordService;

  @EventListener
  public void handleEvent(UserRoleEvent event) {
      log.info("接受到userRole事件:"+event.toString());
      //省略部分数据组装与解析逻辑
      userRoleRecordService.save(record);
  }

}

以上即为同一事务下的一个逻辑,任意方法内抛出异常,所有数据的插入逻辑都会回滚。


image.png


给出一下结论,@EventListener标注的方法是被加入在当前事务的执行逻辑里面的,与主方法事务一体。


踩坑1:


严格意义上来说这里不算是把主逻辑从业务中拆分出来了,还是在同步的事务中,当然这个也是有适配场景的,大家为了代码简洁性与函数级逻辑清晰可以这么做。但是这样做其实不是那么DDD,DDD中应用服务的一个方法即为一个用例,里面贯穿了主流程的逻辑,既然是当前系统内强一致性的业务,那就应该在一个应用服务中体现。当然这个是属于业务边界的。举例的场景来看,用户与赋权显然不是强一致性的操作,赋权失败,不应该影响我新增用户,所以这个场景下做DDD改造,不建议使用统一事务。


踩坑2:


listener里面的执行逻辑可能比较耗时,需要做异步化处理,在UserEventHandler方法上标注@Async,那么这里与主逻辑的方法事务就隔离开了,监听器内的事务开始独立,将不会影响到userService内的事务。例如其他代码不变的情况下用户角色服务代码修改如下


@Component
@Slf4j
public class UserEventHandler {

  @Autowired
  DomainEventPublisher domainEventPublisher;

  @Autowired
  UserRoleService userRoleService;

  @EventListener
  @Async
  public void handleEvent(UserEvent event) {
      log.info("接受到用户新增事件:"+event.toString());
      //省略部分数据组装与解析逻辑
      userRoleService.save(userRole);
      domainEventPublisher.publishEvent(userRoleEvent);
      throw new RuntimeException("制造一下异常");
  }

}

发现,用户新增了,用户角色关联关系新增了,但是操作记录没有新增。第一个结果好理解,第二个结果就奇怪了把,事件监听里面抛了异常,但是居然数据保存成功了。


这里其实是因为UserEventHandlerhandleEvent方法外层为嵌套@TransactionaluserRoleService.save操作结束,事务就提交了,后续的抛异常也不影响。为了保持事务一致,在方法上加一个@Transactional即可。


3.2.独立事务


@EventListener作为驱动加载业务分散代码管理还挺好的。但是在DDD层面,事务数据被杂糅在一起,除了问题一层层找也麻烦,而且数据捆绑较多,还是比较建议使用@TransactionalEventListene


用户新增


@Service
@Slf4j
public class UserServiceImpl implements UserService {

  @Autowired
  DomainEventPublisher domainEventPublisher;

  @Transactional(rollbackFor = Exception.class)
  public void createUser(){
      //省略非关键代码
      save(user);
      domainEventPublisher.publishEvent(userEvent);
  }
}

用户角色关联


@Component
@Slf4j
public class UserEventHandler {

  @Autowired
  DomainEventPublisher domainEventPublisher;

  @Autowired
  UserRoleService userRoleService;

  @TransactionalEventListener
  public void handleEvent(UserEvent event) {
      log.info("接受到用户新增事件:"+event.toString());
      //省略部分数据组装与解析逻辑
      userRoleService.save(userRole);
      domainEventPublisher.publishEvent(userRoleEvent);
  }

}

用户角色操作记录


@Component
@Slf4j
public class UserRoleEventHandler {

  @Autowired
  UserRoleRecordService userRoleRecordService;

  @TransactionalEventListener
  public void handleEvent(UserRoleEvent event) {
      log.info("接受到userRole事件:"+event.toString());
      //省略部分数据组装与解析逻辑
      userRoleRecordService.save(record);
  }

}

一样的代码,把注解从@EventListener更换为@TransactionalEventListener。执行之后发现了一个神奇的问题,用户角色操作记录数据没有入库!!!


image.png


捋一捋逻辑看看,换了个注解,就出现这个问题了,比较一下·两个注解的区别。 @TransactionalEventListener事务独立,且默认注解phase参数值为TransactionPhase.AFTER_COMMIT,即为主逻辑方法事务提交后在执行。而我们知道spring中事务的提交关键代码在AbstractPlatformTransactionManager.commitTransactionAfterReturning


protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) {
  if (txInfo != null && txInfo.getTransactionStatus() != null) {
    if (logger.isTraceEnabled()) {
        logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]");
    }
    //断点处
    txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());
  }
}

配置文件中添加以下配置


logging:
level:
  org:
    mybatis: debug

在上述代码的地方打上断点,再次执行逻辑。


发现,第一次userService保存数据进入此断点,然后进入到userRoleService.save逻辑,此处不进入断点,后续的操作记录的事件处理方法也没有进入。


在来看一下日志


- 2021-09-07 19:54:38.166, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
- 2021-09-07 19:54:38.166, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77a74846]
- 2021-09-07 19:54:38.167, DEBUG, [,,], [http-nio-8088-exec-6], o.m.s.t.SpringManagedTransaction - JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1832a0d9] will be managed by Spring
- 2021-09-07 19:54:38.184, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77a74846]
- 2021-09-07 19:54:51.423, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77a74846]
- 2021-09-07 19:54:51.423, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77a74846]
- 2021-09-07 19:54:51.423, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77a74846]
- 2021-09-07 19:54:51.430, INFO, [,,], [http-nio-8088-exec-6], com.examp.event.demo.UserEventHandler - 接受到用户新增事件:com.examp.event.demo.UserEvent@385db2f9
- 2021-09-07 19:54:53.602, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
- 2021-09-07 19:54:53.602, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@9af2818] was not registered for synchronization because synchronization is not active
- 2021-09-07 19:54:53.603, DEBUG, [,,], [http-nio-8088-exec-6], o.m.s.t.SpringManagedTransaction - JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1832a0d9] will be managed by Spring
- 2021-09-07 19:54:53.622, DEBUG, [,,], [http-nio-8088-exec-6], org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@9af2818]

注意看接受到用户新增事件之后的日志,SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@9af2818] was not registered for synchronization because synchronization is not active说明当前事件是无事务执行的逻辑。再回过头去看一下@TransactionalEventListener,默认配置是在事务提交后才进行事件执行的,但是这里事务都没有,自然也就不会触发事件了。


看图捋一下代码逻辑


image-20210907200823192.png


那怎么解决上面的问题呢?


其实这个东西还是比较简单的:


1.可以对监听此事件的逻辑无脑标注@TransactionalEventListener(fallbackExecution = true),无论事件发送方是否有事务都会触发事件。


2.在第二个发布事件的上面标注一个@Transactional(propagation = Propagation.REQUIRES_NEW),切记不可直接标注@Transactional,这样因为userService上事务已经提交,而@Transactional默认事务传播机制为Propagation.REQUIRED,如果当前没有事务,就新建一个事务,如果已经存在一个事务,加入到这个事务中。


userService中的事务还存在,只是已经被提交,无法再加入,也就是会导致操作记录仍旧无法被插入。


将配置修改为


logging:
level:
  org: debug

可以看到日志


- 2021-09-07 20:26:29.900, DEBUG, [,,], [http-nio-8088-exec-2], o.s.j.d.DataSourceTransactionManager - Cannot register Spring after-completion synchronization with existing transaction - processing Spring after-completion callbacks immediately, with outcome status 'unknown'

四.DDD中的事件驱动应用


理清楚spring中事件驱动模型之后,我们所要做的就是开始解耦业务逻辑。


通过事件风暴理清楚业务用例,设计完成聚合根【ps:其实我觉得设计聚合根是最难的,业务边界是需要团队成员达成共识的地方,不是研发说了算的】,划分好业务领域边界,将原先杂糅在service里面的各个逻辑根据聚合根进行:



  1. 对于聚合的每次命令操作,都至少一个领域事 件发布出去,表示操作的执行结果

  2. 每一个领域事件都将被保存到事件存储中

  3. 从资源库获取聚合时,将根据发生在聚合上的 事件来重建聚合,事件的重放顺序与其产生顺序相同

  4. 聚合快照:将聚合的某一事件发生时的状态快 照序列化存储下来。


五.总结


本文着重介绍了事件驱动模型的概念与应用,并对实际可能出现的业务逻辑做了分析与避坑。最后对于DDD中如何进行以上事件驱动模型进行了分析。


当然我觉得到这里大家应该对事件模型有了一个清晰的认知了,但是对于DDD中应用还是有些模糊。千言万语汇成一句话:与聚合核心逻辑有关的,走应用服务编排,与核心逻辑无关的,走事件驱动模型,采用独立事务模式。至于数据一致性,就根据大家自己相关的业务来决定了,方法与踩坑都告诉了大家了。


你我都是架构师!!!


image.png


六.引用及参考


@TransactionalEventListener的使用和实现原理


【小家Spring】从Spring中的(ApplicationEvent)事件驱动机制出发,聊聊【观察者模式】【监听者模式】【发布订阅模式】【消息队列MQ】【EventSourcing】...


image.png


作者:柏炎
来源:juejin.cn/post/7005175434555949092
收起阅读 »

一文带你落地DDD

一.前言 hello,everyone,好久不见。最近几周部门有个大版本发布,一直没有抽出时间来写博。由于版本不断迭代,功能越做越复杂,系统的维护与功能迭代越来越困难。前段领导找我说,能不能在架构上动手做做文章,将架构迁移到DDD。哈哈哈哈,当时我听到这个话的...
继续阅读 »

一.前言


hello,everyone,好久不见。最近几周部门有个大版本发布,一直没有抽出时间来写博。由于版本不断迭代,功能越做越复杂,系统的维护与功能迭代越来越困难。前段领导找我说,能不能在架构上动手做做文章,将架构迁移到DDD。哈哈哈哈,当时我听到这个话的时候瞬间来了精神。说实话,从去年开始从大厂的一些朋友那里接触到DDD,自己平时也会时不时的阅读相关的文章与开源项目,但是一直没有机会在实际的工作中实施。正好借着这次机会可以开始实践一下。


image.png


本文由于本文的重点为MVC三层架构如何迁移DDD,因此将先对DDD做一个简要的概念介绍(细化的领域概念不做过多展开),然后对于MVC三层架构迁移至DDD作出迁移方案建议。如有不对之处,欢迎指出,共同进步。


本文尤其感谢一下lilpilot在DDD落地方案上给出的宝贵建议。


image.png


DDD系列博客



  1. 一文带你落地DDD

  2. DDD落地之事件驱动模型

  3. DDD落地之仓储

  4. DDD落地之架构分层

二.DDD是什么


2.1.DDD简介


相信了解过DDD的同学都听过网上那种官方的介绍:




  • Domain Drive Design(领域驱动设计)




  • 六边形架构模型




  • 领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具




  • ....


    说的都多多少少抽象点了,听君一席话,如听一席话,哈哈哈




在我看来常规在MVC三层架构中,我们进行功能开发的之前,拿到需求,解读需求。往往最先做的一步就是先设计表结构,在逐层设计上层dao,service,controller。对于产品或者用户的需求都做了一层自我理解的转化。


众所周知,人才是系统最大的bug。


image.png


image-20210904135645004.png


用户需求在被提出之后经过这么多层的转化后,特别是研发需求在数据库结构这一层转化后,将业务以主观臆断行为进行了转化。一旦业务边界划分模糊,考虑不全。大量的逻辑补充堆积到了代码层实现,变得越来越难维护,到处是if/else,传说中***一样代码。


image-20210904140321557.png


DDD所要做的就是



  • 消除信息不对称

  • 常规MVC三层架构中自底向上的设计方式做一个反转,以业务为主导,自顶向下的进行业务领域划分

  • 将大的业务需求进行拆分,分而治之



说到这里大家可能还是有点模糊DDD与常见的mvc架构的区别。这里以电商订单场景为例。假如我们现在要做一个电商订单下单的需求。涉及到用户选定商品,下订单,支付订单,对用户下单时的订单发货。


MVC架构里面,我们常见的做法是在分析好业务需求之后,就开始设计表结构了,订单表,支付表,商品表等等。然后编写业务逻辑。这是第一个版本的需求,功能迭代饿了,订单支付后我可以取消,下单的商品我们退换货,是不是又需要进行加表,紧跟着对于的实现逻辑也进行修改。功能不断迭代,代码就不断的层层往上叠。


DDD架构里面,我们先进行划分业务边界。这里面核心是订单。那么订单就是这个业务领域里面的聚合逻辑体现。支付,商品信息,地址等等都是围绕着订单而且。订单本身的属性决定之后,类似于地址只是一个属性的体现。当你将订单的领域模型构建好之后,后续的逻辑边界与仓储设计也就随之而来了。



2.2.为什么要用DDD



  • 面向对象设计,数据行为绑定,告别贫血模型

  • 降低复杂度,分而治之

  • 优先考虑领域模型,而不是切割数据和行为

  • 准确传达业务规则,业务优先

  • 代码即设计

  • 它通过边界划分将复杂业务领域简单化,帮我们设计出清晰的领域和应用边界,可以很容易地实现业务和技术统一的架构演进

  • 领域知识共享,提升协助效率

  • 增加可维护性和可读性,延长软件生命周期

  • 中台化的基石


2.3.DDD术语介绍


战略设计:限界上下文、通用语言,子域


战术设计:聚合、实体、值对象、资源库、领域服务、领域事件、模块


1595145053316-e3f10592-4b88-479e-b9b7-5f1ba43cadcb.jpeg


2.3.1.限界上下文与通用语言


限界上下文是一个显式的语义和语境上的边界,领域模型便存在于边界之内。边界内,通用语言中的所有术语和词组都有特定的含义。


通用语言就是能够简单、清晰、准确描述业务涵义和规则的语言。


把限界上下文拆解开看。限界就是领域的边界,而上下文则是语义环境。 通过领域的限界上下文,我们就可以在统一的领域边界内用统一的语言进行交流。


域是问题空间,限界上下文是解决空间


2.3.2.上下文组织和集成模式


防腐层(Anticorruption Layer):简称ACL,在集成两个上下文,如果两边都状态良好,可以引入防腐层来作为两边的翻译,并且可以隔离两边的领域模型。


image-20210904143337032.png


2.3.3.实体


DDD中要求实体是唯一的且可持续变化的。意思是说在实体的生命周期内,无论其如何变化,其仍旧是同一个实体。唯一性由唯一的身份标识来决定的。可变性也正反映了实体本身的状态和行为。


实体 = 唯一身份标识 + 可变性【状态 + 行为】


2.3.4.值对象


当你只关心某个对象的属性时,该对象便可作为一个值对象。 我们需要将值对象看成不变对象,不要给它任何身份标识,还应该尽量避免像实体对象一样的复杂性。


值对象=将一个值用对象的方式进行表述,来表达一个具体的固定不变的概念。


2.3.5.聚合


聚合是领域对象的显式分组,旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。


我们把一些关联性极强、生命周期一致的实体、值对象放到一个聚合里。


2.3.6.聚合根


聚合的根实体,最具代表性的实体


2.3.7.领域服务


当一些逻辑不属于某个实体时,可以把这些逻辑单独拿出来放到领域服务中 理想的情况是没有领域服务,如果领域服务使用不恰当慢慢又演化回了以前逻辑都在service层的局面。


可以使用领域服务的情况:



  • 执行一个显著的业务操作

  • 对领域对象进行转换

  • 以多个领域对象作为输入参数进行计算,结果产生一个值对象


2.3.8.应用服务


应用服务是用来表达用例和用户故事的主要手段。


应用层通过应用服务接口来暴露系统的全部功能。 在应用服务的实现中,它负责编排和转发,它将要实现的功能委托给一个或多个领域对象来实现,它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式,它隐藏了领域层的复杂性及其内部实现机制。


应用层相对来说是较“薄”的一层,除了定义应用服务之外,在该层我们可以进行安全认证,权限校验,持久化事务控制,或者向其他系统发生基于事件的消息通知,另外还可以用于创建邮件以发送给客户等。


应用层作为展现层与领域层的桥梁。展现层使用VO(视图模型)进行界面展示,与应用层通过DTO(数据传输对象)进行数据交互,从而达到展现层与DO(领域对象)解耦的目的。


2.3.9.工厂


职责是创建完整的聚合



  • 工厂方法

  • 工厂类


领域模型中的工厂



  • 将创建复杂对象和聚合的职责分配给一个单独的对象,它并不承担领域模型中的职责,但是领域设计的一部份

  • 对于聚合来说,我们应该一次性的创建整个聚合,并且确保它的不变条件得到满足

  • 工厂只承担创建模型的工作,不具有其它领域行为

  • 一个含有工厂方法的聚合根的主要职责是完成它的聚合行为

  • 在聚合上使用工厂方法能更好的表达通用语言,这是使用构造函数所不能表达的


聚合根中的工厂方法



  • 聚合根中的工厂方法表现出了领域概念

  • 工厂方法可以提供守卫措施


领域服务中的工厂



  • 在集成限界上下文时,领域服务作为工厂

  • 领域服务的接口放在领域模型内,实现放在基础设施层


2.3.10.资源库【仓储】


是聚合的管理,仓储介于领域模型和数据模型之间,主要用于聚合的持久化和检索。它隔离了领域模型和数据模型,以便我们关注于领域模型而不需要考虑如何进行持久化。


我们将暂时不使用的领域对象从内存中持久化存储到磁盘中。当日后需要再次使用这个领域对象时,根据 key 值到数据库查找到这条记录,然后将其恢复成领域对象,应用程序就可以继续使用它了,这就是领域对象持久化存储的设计思想


2.3.11.事件模型


领域事件是一个领域模型中极其重要的部分,用来表示领域中发生的事件。忽略不相关的领域活动,同时明确领域专家要跟踪或希望被通知的事情,或与其他模型对象中的状态更改相关联


领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。



比如下订单后,给用户增长积分与赠送优惠券的需求。如果使用瀑布流的方式写代码。一个个逻辑调用,那么不同用户,赠送的东西不同,逻辑就会变得又臭又长。这里的比较好的方式是,用户下订单成功后,发布领域事件,积分聚合与优惠券聚合监听订单发布的领域事件进行处理。



2.4.DDD架构总览


2.4.1.架构图


严格分层架构:某层只能与直接位于的下层发生耦合。


松散分层架构:允许上层与任意下层发生耦合


依赖倒置原则


高层模块不应该依赖于底层模块,两者都应该依赖于抽象


抽象不应该依赖于实现细节,实现细节应该依赖于接口


简单的说就是面向接口编程。


按照DIP的原则,领域层就可以不再依赖于基础设施层,基础设施层通过注入持久化的实现就完成了对领域层的解耦,采用依赖注入原则的新分层架构模型就变成如下所示:


image-20210904145125083.png


从上往下


第一层为用户交互层,web请求,rpc请求,mq消息等外部输入均被视为外部输入的请求,可能修改到内部的业务数据。


第二层为业务应用层,与MVC中的service不同的不是,service中存储着大量业务逻辑。但在应用服务的实现中(以功能点为维度),它负责编排、转发、校验等。


第三层为领域层,聚合根是里面最高话事人。核心逻辑均在聚合根中体现【充血模型】,如果当前聚合根不能处理当前逻辑,需要其他聚合根的配合时,则在聚合根的外部包一层领域服务去实现逻辑。当然,理想的情况是不存在领域服务的。


第四层为基础设施层,为其他层提供技术实现支持


相信这里大家还看见了应用服务层直接调用仓储层的一条线,这条线是什么意思呢?


领域模型的建立是为了控制对于数据的增删改的业务边界,至于数据查询,不同的报表,不同的页面需要展示的数据聚合不具备强业务领域,因此常见的会使用CQRS方式进行查询逻辑的处理。


2.4.2.六边形架构(端口与适配器)


对于每一种外界类型,都有一个适配器与之对应。外界接口通过应用层api与内部进行交互。


对于右侧的端口与适配器,我们可以把资源库看成持久化的适配器。


image-20210904150651866.png


2.4.3.命令和查询职责分离--CQRS



  • 一个对象的一个方法修改了对象的状态,该方法便是一个命令(Command),它不应该返回数据,声明为void。

  • 一个对象的一个方法如果返回了数据,该方法便是一个查询(Query),不应该通过直接或者间接的手段修改对象状态。

  • 聚合只有Command方法,没有Query方法。

  • 资源库只有add/save/fromId方法。

  • 领域模型一分为二,命令模型(写模型)和查询模型(读模型)。

  • 客户端和查询处理器 客户端:web浏览器、桌面应用等 查询处理器:一个只知道如何向数据库执行基本查询的简单组件,查询处理器不复杂,可以返回DTO或其它序列化的结果集,根据系统状态自定

  • 查询模型:一种非规范化的数据模型,并不反映领域行为,只用于数据显示

  • 客户端和命令处理器 聚合就是命令模型 命令模型拥有设计良好的契约和行为,将命令匹配到相应的契约是很直接的事情

  • 事件订阅器更新查询模型

  • 处理具有最终一致性的查询模型


2.4.4.事件驱动架构


落地指导与实践:DDD落地之事件驱动模型




  • 事件驱动架构可以融入六边型架构,融合的比较好,也可以融入传统分层架构




  • 管道和过滤器




  • 长时处理过程



    1. 主动拉取状态检查:定时器和完成事件之间存在竞态条件可能造成失败

    2. 被动检查,收到事件后检查状态记录是否超时。问题:如果因为某种原因,一直收不到事件就一直不过期




  • 事件源



    1. 对于聚合的每次命令操作,都至少一个领域事 件发布出去,表示操作的执行结果

    2. 每一个领域事件都将被保存到事件存储中

    3. 从资源库获取聚合时,将根据发生在聚合上的 事件来重建聚合,事件的重放顺序与其产生顺序相同

    4. 聚合快照:将聚合的某一事件发生时的状态快 照序列化存储下来。以减少重放事件时的耗时




三.落地分享


3.1.事件风暴


EventStorming则是一套Workshop(可以理解成一个类似于头脑风暴的工作坊)方法。DDD出现要比EventStorming早了10多年,而EventStorming的设计虽然参考了DDD的部分内容,但是并不是只为了DDD而设计的,是一套独立的通过协作基于事件还原系统全貌,从而快速分析复杂业务领域,完成领域建模的方法。


image-20210904152542121.png


针对老系统内的业务逻辑,根据以上方式进行业务逻辑聚合的划分


例如电商场景下购车流程进行事件风暴


image-20210904152737731.png


3.2.场景识别


事件风暴结束明确业务聚合后,进行场景识别与层级划分


image-20210904153035722.png


3.3.包模块划分


图片2.png


3.4.迁移说明


3.4.1.仓储层


在我们日常的代码中,使用Repository模式是一个很简单,但是又能得到很多收益的事情。最大的收益就是可以彻底和底层实现解耦,让上层业务可以快速自发展。


以目前逆向模型举例,现有



  • OrderDO

  • OrderDAO


可以通过以下几个步骤逐渐的实现Repository模式:



  1. 生成Order实体类,初期字段可以和OrderDO保持一致

  2. 生成OrderDataConverter,通过MapStruct基本上2行代码就能完成

  3. 写单元测试,确保Order和OrderDO之间的转化100%正确

  4. 生成OrderRepository接口和实现,通过单测确保OrderRepository的正确性

  5. 将原有代码里使用了OrderDO的地方改为Order

  6. 将原有代码里使用了OrderDAO的地方都改为用OrderRepository

  7. 通过单测确保业务逻辑的一致性。


有一点要注意,目前我们用mybatis,dao操作都是含有业务含义的,正常的repository不应该有这种方法,目前repository中的含有业务含义的方法只是兼容方案,最终态都要干掉的。


极端DDD推崇者要求在repository中只存在save与byId两个聚合方法。这个当然需要根据实际业务场景来决定,但是还是建议仅保存这两个方法,其他业务需求查询聚合的方法单独开一个queryRepository实现不同数据的查询聚合与页面数据展示。保证数据的增删改入口唯一


3.4.2. 隔离三方依赖-adapter


思想和repository是一致的,以调用payApi为例:



  1. 在domain新建adapter包

  2. 新建PayAdapter接口

  3. 在infrastructure中定义adapter的实现,转换内部模型和外部模型,调用pay接口,返回内部模型dto

  4. 将原先业务中调用rpc的地方改成adapter

  5. 单测对比rpc和adapter,保证正确性


3.4.3. 抽离技术组件


同样是符合六边形架构的思想,把mqProducer,JsonUtil等技术组件,在domain定义接口,在infrastructure写实现,替换步骤和adapter类似。


3.4.4. 业务流程模块化-application


如果是用能力链的项目,能力链的service就可以是application。如果原先service中的业务逻辑混杂,甚至连参数组装都是在service中体现的。那么需要把逻辑归到聚合根中,当前聚合根无法完全包裹的,防止在领域模型中体现。在应用服务层中为能力链的体现。


3.4.5. CQRS参数显式化


能力链项目,定义command,query包,通过能力链来体现Command,Query,包括继承CommandService、QueryService,Po继承CommandPo,QueryPo


非能力链项目,在application定义command,query包,参数和类名要体现CQRS。


3.4.6. 战略设计-domain


重新设计聚合和实体,可能和现有模型有差异,如果模型差距不大,直接将能力点内的逻辑,迁移到实体中,


将原来调用repository的含业务含义的方法,换成save,同时删除含业务含义的方法,这个时候可以考虑用jpa替换mybatis,这里就看各个子域的选择了,如果用jpa的话 dao层可以干掉。至此,原biz里的大多数类已迁移完成。


四.迁移过程中可能存在的疑问


迁移过程中一定会存在或多或少不清楚的地方,这里我分享一下我在迁移的过程中遇到的问题。


image.png


1.领域服务与应用服务的实际应用场景区别


应用服务:可以理解为是各种方法的编排,不会处理任务业务逻辑,比如订单数修改,导致价格变动,这个逻辑体现在聚合根中,应用服务只负责调用。


领域服务:聚合根本身无法完全处理这个逻辑,例如支付这个步骤,订单聚合不可能支付,所以在订单聚合上架一层领域服务,在领域服务中实现支付逻辑。应用服务调用领域服务。


2.聚合根定义的业务边界是什么?


不以表结构数据进行业务逻辑的划分,一个业务体为一块业务。比如一个订单涉及商品,收货地址,发货地址,个人信息等等。以实体与值对象的方式在聚合内进行定义。


3.一个command修改一个聚合时,会关联修改到别的关联表,这个关联表算不算聚合


关联表不算聚合,算值对象


4.应用服务层如果调用rpc是否必须使用adapter


是的,必须使用,屏蔽外部依赖对于当前业务逻辑的影响。设想一下,你现在需要调用rpc接口,返回的字段有100,你要取其中50个字段。隔了一段时间,调用方改了接口逻辑的返回,数据被包含在实体内。而你调用这个接口的地方特别多,改动就很大了。但是如果有了适配器这一层,你只要定义本身业务需要的数据结构,剩下的业务不需要考虑,完全新人适配器可以将你想要的数据从rpc中加载到。


5.聚合根内部逻辑无法单独处理时,放到领域服务内的话,是否可以调用其他聚合根的领域服务或者应用服务,加入业务强绑定形式,聚合根内部如果需要调用service服务或者仓储时如何做。


可以这么做,但是逻辑要保证尽量内聚。


6.事件通知模式,比如是强绑定形式的,是否还是此种方式,还是与本聚合根无关的逻辑均走事件通知


强依赖形式的走逻辑编排,比如订单依赖支付结果进行聚合修改则走应用服务编排。订单支付后发送优惠券,积分等弱耦合方式走事件通知模式。


7.聚合根,PO,DTO,VO的限界


po是数据库表结构的一一对应。


dto是数据载体,贫血模型,仅对数据进行装载。


vo为dto结构不符合前端展示要求时的包装。


聚合根为一个或者多个po的聚合数据,当然不仅仅是po的组合,还有可能是值对象数据,充血模型,内聚核心业务逻辑处理。


8.查询逻辑单独开设一个repository,还是可以在聚合根的仓储中,划分的依据是什么


单独开设一个仓储。聚合根的仓储应该查询结果与save的参数均为聚合根,但是业务查询可能多样,展示给前端的数据也不一定都是聚合根的字段组成,并且查询不会对数据库造成不可逆的后果,因此单独开设查询逻辑处理,走CQRS模式。


9.返回的结果数据为多个接口组成,是否在应用服务层直接组合


不可以,需要定义一个assember类,单独对外部依赖的各种数据进行处理。


10.save方法做完delete,insert,update所有方法吗?


delete方法单独处理,可以增加一个delete方法,insert与update方法理论上是需要保持统一方法的。


11.查询逻辑如果涉及到修改聚合根怎么处理


简单查询逻辑直接走仓储,复杂逻辑走应用服务,在应用服务中进行聚合根数据修改。


12.逻辑处理的service放置在何处


如果为此种逻辑仅为某个聚合使用,则放置在对应的领域服务中,如果逻辑处理会被多个聚合使用,则将其单独定义一个service,作为一个工具类。


五.总结


本文对DDD做了一个不算深入的概念,架构的介绍。后对现在仍旧还是被最多使用的MVC三层架构迁移至DDD方案做了一个介绍,最后对可能碰到的一些细节疑问点做了问答。


当然不是所有的业务服务都合适做DDD架构,DDD合适产品化,可持续迭代,业务逻辑足够复杂的业务系统,中小规模的系统与团队还是不建议使用的,毕竟相比较与MVC架构,成本很大。


demo演示:DDD-demo


关于MVC分层的微服务架构博主在之前的文章中也给出过一些设计规范,感兴趣的大家可以去看看:


1.看完这篇,你就是架构师


2.求求你,别写祖传代码了


image.png


六.更多DDD学习资料


博客资料:


ThoughtWork DDD系列


张逸 DDD系列


欧创新 DDD系列


代码示例:


阿里COLA



github.com/citerus/ddd…




github.com/YaoLin1/ddd…




github.com/ddd-by-exam…




github.com/Sayi/ddd-ca…



七.特别鸣谢


lilpilot


image.png


作者:柏炎
来源:juejin.cn/post/7004002483601145863
收起阅读 »

Spring 缓存注解这样用,太香了!

作者最近在开发公司项目时使用到 Redis 缓存,并在翻看前人代码时,看到了一种关于 @Cacheable 注解的自定义缓存有效期的解决方案,感觉比较实用,因此作者自己拓展完善了一番后分享给各位。 Spring 缓存常规配置 Spring Cache 框架给我...
继续阅读 »

作者最近在开发公司项目时使用到 Redis 缓存,并在翻看前人代码时,看到了一种关于 @Cacheable 注解的自定义缓存有效期的解决方案,感觉比较实用,因此作者自己拓展完善了一番后分享给各位。


Spring 缓存常规配置


Spring Cache 框架给我们提供了 @Cacheable 注解用于缓存方法返回内容。但是 @Cacheable 注解不能定义缓存有效期。这样的话在一些需要自定义缓存有效期的场景就不太实用。


按照 Spring Cache 框架给我们提供的 RedisCacheManager 实现,只能在全局设置缓存有效期。这里给大家看一个常规的 CacheConfig 缓存配置类,代码如下,


@EnableCaching
@Configuration
public class CacheConfig extends CachingConfigurerSupport {
...

private RedisSerializer keySerializer() {
return new StringRedisSerializer();
}

private RedisSerializer valueSerializer() {
return new GenericFastJsonRedisSerializer();
}

public static final String CACHE_PREFIX = "crowd:";

@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 配置序列化(解决乱码的问题)
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
//设置key为String
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
//设置value为自动转Json的Object
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
.computePrefixWith(name -> CACHE_PREFIX + name + ":")
.entryTtl(Duration.ofSeconds(600));
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(Objects.requireNonNull(redisConnectionFactory));
return new RedisCacheManager(redisCacheWriter, config);
}
}

这里面简单对 RedisCacheConfiguration 缓存配置做一下说明:



  1. serializeKeysWith():设置 Redis 的 key 的序列化规则。

  2. erializeValuesWith():设置 Redis 的 value 的序列化规则。

  3. computePrefixWith():计算 Redis 的 key 前缀。

  4. entryTtl():全局设置 @Cacheable 注解缓存的有效期。


那么使用如上配置生成的 Redis 缓存 key 名称是什么样得嘞?这里用开源项目 crowd-adminConfigServiceImpl 类下 getValueByKey(String key) 方法举例,


@Cacheable(value = "configCache", key = "#root.methodName + '_' + #root.args[0]")
@Override
public String getValueByKey(String key) {
QueryWrapper wrapper = new QueryWrapper<>();
wrapper.eq("configKey", key);
Config config = getOne(wrapper);
if (config == null) {
return null;
}
return config.getConfigValue();
}

执行此方法后,Redis 中缓存 key 名称如下,



crowd:configCache:getValueByKey_sys.name




ttl 过期时间是 287,跟我们全局设置的 300 秒基本是一致的。此时假如我们想把 getValueByKey 方法的缓存有效期单独设置为 600 秒,那我们该如何操作嘞?


@Cacheable 注解默认是没有提供有关缓存有效期设置的。想要单独修改 getValueByKey 方法的缓存有效期只能修改全局的缓存有效期。那么有没有别的方法能够为 getValueByKey 方法单独设置缓存有效期嘞?当然是有的,大家请往下看。


自定义 MyRedisCacheManager 缓存


其实我们可以通过自定义 MyRedisCacheManager 类继承 Spring Cache 提供的 RedisCacheManager 类后,重写 createRedisCache(String name, RedisCacheConfiguration cacheConfig) 方法来完成自定义缓存有效期的功能,代码如下,


public class MyRedisCacheManager extends RedisCacheManager {
public MyRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
super(cacheWriter, defaultCacheConfiguration);
}

@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
String[] array = StringUtils.split(name, "#");
name = array[0];
// 解析 @Cacheable 注解的 value 属性用以单独设置有效期
if (array.length > 1) {
long ttl = Long.parseLong(array[1]);
cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));
}
return super.createRedisCache(name, cacheConfig);
}
}


MyRedisCacheManager 类逻辑如下,



  1. 继承 Spring Cache 提供的 RedisCacheManager 类。

  2. 重写 createRedisCache(String name, RedisCacheConfiguration cacheConfig) 方法。

  3. 解析 name 参数,根据 # 字符串进行分割,获取缓存 key 名称以及缓存有效期。

  4. 重新设置缓存 key 名称以及缓存有效期。

  5. 调用父类的 createRedisCache(name, cacheConfig) 方法来完成缓存写入。


接着我们修改下 CacheConfig 类的 cacheManager 方法用以使用 MyRedisCacheManager 类。代码如下,


@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return new MyRedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory), defaultCacheConfig());
}

private RedisCacheConfiguration defaultCacheConfig() {
return RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
.computePrefixWith(name -> CACHE_PREFIX + name + ":")
.entryTtl(Duration.ofSeconds(600));
}

最后在使用 @Cacheable 注解时,在原有 value 属性的 configCache 值后添加 #600,单独标识缓存有效期。代码如下,


@Cacheable(value = "configCache#600", key = "#root.methodName + '_' + #root.args[0]")
@Override
public String getValueByKey(String key) {
...
}

看下 getValueByKey 方法生成的 Redis 缓存 key 有效期是多久。如下,



OK,看到是 590 秒有效期后,我们就大功告成了。到这里我们就完成了对 @Cacheable 注解的自定义缓存有效期功能开发。


作者:waynaqua
来源:juejin.cn/post/7299353390764179506
收起阅读 »

《程序员的自我修养》读后感

书中内容涉及哪些方面? 新人如何学习编程,如何进入这一行,对职业人的建议 进来以后职场中的一些经验,这一职业经常面临的问题 全栈工程师的相关问题,面试招人环节 除了技术你应该怎样提升自己,如何建立自己的品牌 如何教育自己的孩子编程,作者的自学设计经验分...
继续阅读 »

书中内容涉及哪些方面


新人如何学习编程,如何进入这一行,对职业人的建议

进来以后职场中的一些经验,这一职业经常面临的问题

全栈工程师的相关问题,面试招人环节

除了技术你应该怎样提升自己,如何建立自己的品牌

如何教育自己的孩子编程,作者的自学设计经验分享

程序员的未来和作者的美好希冀......

可以看出来作者写了很多,对程序员各个阶段都有思考和分享。看起来都是些老生常谈的建议,也有些都是职场的潜规则。但是还是得说出来,摆到明面上,这样大家才能学习、思考、践行。这也是这本书的意义所在。


我也说说自己的一点想法,一方面是对书的内容的消化吸收、一方面也是有自己的感悟,及时记录下来。


1. 保证健康的体魄


身体是革命的本钱,最近看《去有风的地方》,主角的闺蜜第一集就死了,为什么呢?没有照顾好自己的身体,胃不行,太可惜了。前些年 github 的996 项目还有越来越多的猝死现象也让大家对行业内司空见惯的加班潜规则加以抵制,都说明大家对自己的身体越来越重视了。说到底还是得(心里)坚持锻炼。


锻炼在现实中是怎么实施的呢?


很多人说,哎呀,我办了健身卡可是都没去过几次,太浪费了,就在家锻炼吧。


退而求其次,买了哑铃甚至跑步机,用上几次一直吃灰。


再或者,买了瑜伽垫,开通健身会员,跟着练上一个月,舒展筋骨,体能恢复什么的。坚持打卡,一旦哪天没打,完了,开始断断续续直至把各类健身会员订阅关掉。


说下楼跑步,冬天嫌冷夏天怕热.......


没错,这就是我,甚至遛狗都是骑着电动车让狗跑。但是,我是真的想让身体越来越好。所做的这些努力,哪怕放弃的次数再多,我也会继续下去,比起因为放弃过就不努力,我这样算不算越挫越勇呢?到现在,我已经不怕放弃了,只会遗憾没有开始。


2.读书、旅行与思考不能停


精神与身体都需要锻炼,什么软技能、代码重构、艺术审美,你不会真指望一本书能让你学会这些吧?


《自我修养》书里说的那些道理,更多的像是旅游景点的指路牌,告诉你方向,你想往哪个方向走,你就读哪方面的书充实自己,而不是把路牌拍张照站原地不动。多读书,现在这年头,你就是读狗血的网络小说,说不定哪天还能看到同名影视剧上映。


旅行嘛,去年被辞职以后我出去旅游了两周,回来身心俱疲,开始思考旅行的意义。看惯了全国统一的网红景点,我悟了。旅行也得带点脑子去,我想从这个城市看到什么?历史、不一样的人文、自然,总要有点不同吧,要是冲着网红景点的噱头去,那指定被坑。酒吧、小吃,哪个地方没有。旅行,就是要看不一样的东西。


关于思考,之前的我,对技术思考最多的时候就是在找工作的时候。那确实是每天都很充实,被面试官各种问题轰炸,工作中呢,差不多的管理系统,差不多的业务,差不多的逻辑,有复杂的也不会超过两天,超过了就把需求打回去,或者找别人一起想,最后说一声:实现不了。现在的我,几乎每天都在思考,一个是做的业务之前没做过,还有个是观念转变,面向面试上班,主动思考。


展开说说面向面试上班,既然面试问的最多也难回答的是:你遇到的问题有哪些,怎么处理的。那我上班就开始整理问题,碰到难题就思考,解决了就复盘。没问题我想问题,(1)想之前工作的几家后台系统的难题是什么,没有?那你搞的不是核心,难题都让别人解决了或者引入一个包搞定了。你要说实在没有,这个系统就我一个人写的,那可以想下通常这种系统比较难实现的是哪部分业务,比如权限、审核、排班等等,然后记录下解决方案(2)由点及面,css遇到了一个问题,比如移动端适配问题,单位是用vw呢还是rem呢,布局是flex还是百分比呢,搜结果的过程中顺便可以看看解决方案都有哪些,对照下优缺点和适用场景,最终选择最佳方案。


3. 认真工作、诚信做人


书里有这样一段话:


那些优秀的程序员有时看上去很懒,他们会在上班时间做那些与工作毫无关系的事情,比如,在纸上随意地乱涂乱画,长时间坐在那里发呆,甚至玩手机。但一旦他们进入编程状态,你又会发现他们变得像打字员般,指尖飞舞,瞬间完成他们的工作。 —第三章第一节第5小节 很懒却又很高效


这就是我们现在说的“摸鱼”,我之前一直觉得摸鱼的时候有种羞愧感,同事路过我会马上放下手机,假装在敲代码。真正意识到这是一个正常的行为,我也就心态上轻松了。除了无所事事的时候,有一堆工作还摸鱼的情况,我内心是这样的:**,太难了,等会再看吧,然后拿起手机。刷了一会,想到活还没干完,果断放下手机,开始敲代码或者接着想。越是在忙的时候,这样的情形重复次数越多。很显然,这是给大脑休息的机会,摸鱼基本上不用动脑子,只需要眼睛浏览,因为有这样的机会,接下来才能更高效的完成任务。


特别忙的时候,手机拿起放下的速度也越来越快,这个时候会比较着急,想上厕所也憋着,憋不住了上厕所一路上也在想逻辑,从第三视角去审视自己这个行为又觉得很有意思,不知道在心理学上是怎么解释的。说这一点,不是为了摆烂,而是认真的工作,这样无愧于内心,也肯定是有产出的。在读《人人都是产品经理》的时候,也有一段话很应景:


任何情况下 我们都要做好手头的事情。 确保就算这事儿对公司来说又黄了,我也要通过做事有所收获。


那面试你拿的出手的自然也就是这些收获。


诚信做人,这一点毋庸置疑,书中说的职场经验、软技能、建立自己的品牌啦,这些都能和认真工作、诚信做人挂的上钩。工作认真完成了,你的个人品牌效应自然能提升,职场中大家对你也会尊重。打个比方,初入公司,你活干不下来,是个人都想骂你两句。


差不多了,就写这么多感想吧。


作者:宅神king
来源:juejin.cn/post/7190294025214099517
收起阅读 »

别把执着用错地方,两种思维的碰撞

概要   今天通过一个项目问题解决过程来分享,两种不同的思维方式在解决项目问题时的碰撞,中间引起的带入问题,请忽略不计。 背景   先说说前提,本身的技术路子比较野,啥东西都能摸两把,基本的思路和习惯都是以结果优先,技术作为手段,但这并不代表我没有标准化的基准...
继续阅读 »

概要


  今天通过一个项目问题解决过程来分享,两种不同的思维方式在解决项目问题时的碰撞,中间引起的带入问题,请忽略不计。


背景


  先说说前提,本身的技术路子比较野,啥东西都能摸两把,基本的思路和习惯都是以结果优先,技术作为手段,但这并不代表我没有标准化的基准和要求,只不过这个次序和容忍度有不同的理解、而我作为程序猿中的代码收容所,杂货收割机,可能是绝大部分标榜 “技术控、理论控、优雅控” 最不屑的。


  文档、PPT、事务性、管理性、运维、交付、架构、甚至是商务、咨询、售前支撑都有涉及,这两年又对工控、数采非标互联网进一步在学习,说这些不是为了标榜啥,只是想说明我对技术的态度,是只要感觉缺的就会去补充学,早先,从一直排斥文档,认为这项工作是 “不务正业”,到后来慢慢发现实际是有点儿恐惧写文档,天天被批判,再到后来反感的PPT和"跟傻子讲技术",心态上变化,看到差距,尊重别人的成就,就会自然而然的追赶,直到无所畏惧。


前因


  Java属于可能我最先了解和尝试的编程语言,但怎么说呢,在当时的环境里面,C++、C# 是我选择的结果,期间也涉及了其他乱七八糟的语言,也都是副业需要,涉及到就捡起来用,可能内心觉得Java比较 “简单”,直到工作里面不得不顶着运维和维护,说实话,没啥太大障碍,真正接受生态,也就近两年,心态的变化,感觉看到了不一样的新大陆,当然主要是各位开拓者的不断努力,不断的思维碰撞和实践努力,而我本身也不能说是专职做这块,只是有些块别人解决有问题,思维碰撞一下,以下就说说关于问题解决的剖析。


过程


   同事A属于入行就做Java,也有个8年多工作经验,技术能力,执行力都不错,就是这个行业通病,沟通有所欠缺,这个只是一个点的分享,只是从思维上去谈问题,(我本身是个大白菜),还是规则引擎的点,之前有过分享,整体是由我设计的方案,之前有分享,聊聊规则引擎的调研及实现全过程,因工期等其他影响,我把人员能力问题也考虑在内,在正式应用交互过程中,关于Mqtt处理相应速度问题和数据重复入库的问题得以复现,同事A有不少的参与度,因为某些原因(我不得不负起主责,前后端整体调整),这是问题的大背景


复现排查



  • 尝试1:这个问题同事A经过问题复现,初步确认是多线程循环引起的问题,解决的办法很粗暴,判断如果循环超过1000次就终止线程,导致的结果就是性能损耗和任务丢失,(这种情况在采集上是可以容忍的,数据是不间断的),就此就准备收手。我认为这个种解决办法是最终没办法下的妥协处置,而且问题也没分析定位清楚,做进一步尝试,此处是应该要执着的地方,

  • 尝试2:于是A经过一顿跟踪,用了各种线程跟测工具和理论检索,通过方法锁尝试去解决该问题,造成的第二种后果是结果正确了,但及时性达不到,根本发挥不了性能。进一步也发现了消息密集的情况下会产生问题,中间也进行过任务队列的分组处置,(此项尝试基本属于A的各种线程理论一顿分析,进行尝试,得不出来啥有效结果,各种理论分析)

  • 尝试3: 我经过不停得添加代码,定位异常,定位到了异常代码块,属于递归循环调用侧发生得异常,之前线程循环调用得问题也基本定位到,是因为父子线程得优先级,加上规则本身深度遍历及结果等待造成得问题,也就是Future得get等待引发得,此时,要验证该问题,把规则内部得递归任务添加调整为同步,(只涉及到一句代码),循环得问题得以解决,处理速度也跟得上,(线程放置到了外侧处理消息(消息接收本身也是多线程)),但又引起了新得问题,入库得数据发生重复和不正常了(如果问题回溯,其实很快能发现,当然有点儿马后炮了)

  • 尝试4:此时,同事A又尝试想放弃,认为在入库侧判断是否重复插入即可,并把别人得关于多线程入库重复得解决常规办法得内容做了共享,我给得理由是:“再往前尝试以下,感觉马上就能找到真相,正是学技术得好时候,不要此时妥协”,很显示A在口头上答应,行动上已经放弃了,我就消息重复产生得地方又进行了各种锁尝试,也基本定位到了产生消息重复的地方,加上方法锁,结果和执行均验证没啥问题。

  • 补上最后一药: 最后的相同broker相同主题下多个规则执行问题,其实不是啥大问题,很容易就解决了,两种方案,但实在是实践浪费的太久,我尝试了复杂的理想办法,交付中的问题都得以解决。


回溯


   其实这个问题在回溯过程来看,在尝试4的时候,应该马上就能反应过来去解决的,但实际可能是周期和脑子都有点儿混乱,还是分析了执行结果和操作才做了验证,实在是我已经不敢太过相信线程理论,还是实践出真知,所以,这个过程中,我对Java的语法半吊子,但并不妨碍我去参与分析解决问题,而同事A,虽然知道线程的理论和各种语法,各个线程分析结果,但依然妨碍了他去解决此项问题,之前有听过软件的思维最重要,经验有时候会成为负担,可能在你们看来,这个问题其实有充足的理论基础,一定很容易解决定位这个问题,但怎么说呢,记得上计算机理论的时候,老师说过一句,编程就是抽象化具体事务到编程语言的过程、


  在其后的过程中,好像绝大多数都是把问题分解成实现,排查分析问题的过程,我理解就是把具体的事务反推到程序上,很可惜这个过程,被归集到个人的能力或者习惯上,毕竟,对过程负责,是常态,项目的面试过程也就是说清过程,至于是谁解决的问题,并没有那么重要,这造成了两种分化,掌握全貌的失去了动手能力,对结果负责,不对过程负责,掌握单一的,失去了对结果负责的能力,掌握着对过程负责,新的纷争和各种千奇百怪的妥协结果也就是值得接受的。


分析


   当你在执着各种编码规范,各种优雅简洁之道的同时,是否也能思考一下,我们所专注的语法,面试理论,就具体问题而言,真就是理论精通了之后,不会成为 “赵括”,指点江山,飞扬文字。


  而这种分化性,我已遇到了不下3种典型,要不点了吹牛逼的和撕逼的技能点、要不就点了技术理论的技能点,要不就去点了文档管理技能点,当然,只是个现象,躺平是常态,没啥压力和责任,谁愿意变成受虐狂,但有没有可能,也有那么一丝,对这个职业和技术的热爱? 可能又会变成鸡汤和躺着说话不腰疼,至于外在因素、想法和处境的不同,咱姑且就图一乐,从容面对吧、总之是,事不临身,我不愁,事临身,愁也没用,那能不能,稍微有一些,哪怕只有一丝希望的掌控呢,要说起怨怼,我有一肚子,但总不可能变成情绪发泄机吧,对结果有预期,但不放弃努力的过程,也许这是我此刻能够坚持做的。


PS


  在这里申明一下,同事A的各方面能力我很认可,此处也只是精力有限下的小分享(有个小背景是,我们基本上背着2.5个以上的事在解决此项问题)、所以请各位不要基于我个人的认知有指摘。


作者:沈二到不行
来源:juejin.cn/post/7292290711734681651
收起阅读 »

从源码角度解读Java Set接口底层实现原理

  咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~ 环境说明:Windows 10 + Int...
继续阅读 »

  咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~


在这里插入图片描述


环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

前言


  Set是Java集合框架中的一个接口,它继承了Collection接口,并添加了一些独有的方法。Set可看做是没有重复元素的Collection,它的实现类包括HashSet、TreeSet等。本文将从源码的角度来解读Set接口的底层实现原理。


摘要


  本文将对Java Set接口进行详细的解读,包括Set的概述、源代码解析、应用场景案例、优缺点分析、类代码方法介绍和测试用例等方面。


Set接口


概述


  Set是一个不允许重复元素的集合。它继承了Collection接口,最基本的操作包括添加元素、检查元素是否存在、删除元素等。Set接口的实现类包括HashSet、TreeSet等。HashSet是基于哈希表的实现,TreeSet是基于红黑树的实现。


源代码解析


Set


  Set接口是Java集合框架中的一种接口,它表示一组无序且不重复的元素。Set接口继承自Collection接口,因此它具有Collection接口的所有方法,但是在Set接口中,添加重复元素是不允许的。Set接口有两个主要的实现类:HashSet和TreeSet。其中,HashSet基于哈希表实现,对于非Null元素具有O(1)的插入和查找时间复杂度;而TreeSet基于红黑树实现,对于有序集合的操作具有良好的性能表现。在使用Set接口时,可以通过迭代器遍历元素,也可以使用foreach语句遍历元素。


  如下是部分源码截图:


在这里插入图片描述


HashSet


  HashSet基于哈希表实现,它使用了一个称为“hash表”的数组来存储元素。当我们向HashSet中添加元素时,首先会对元素进行哈希,并通过哈希值来确定元素在数组中的位置。如果该位置已经有元素了,就会通过equals方法来判断是否重复,如果重复则不添加,如果不重复则添加到该位置。当然,由于哈希表中可能会存在多个元素都哈希到同一个位置的情况,因此这些元素会被存储在同一个位置上,形成一个链表。在查找元素时,先通过哈希值定位到链表的头部,然后在链表中进行搜索,直到找到匹配的元素或到达链表的末尾。


public class HashSet extends AbstractSet implements Set, Cloneable, java.io.Serializable {
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

public HashSet() {
map = new HashMap<>();
}

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

public boolean contains(Object o) {
return map.containsKey(o);
}

public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
}

  这是一个实现了 HashSet 数据结构的 Java 类。HashSet 继承了 AbstractSet 类,同时实现了 Set 接口和 Cloneable 和 Serializable 接口。


  HashSet 内部使用 HashMap 来存储元素,其中元素作为 key ,一个 static final Object 作为 value,即:


private transient HashMap map;
private static final Object PRESENT = new Object();

  在构造器中,HashSet 实例化了一个空的 HashMap 对象:


public HashSet() {
map = new HashMap<>();
}

  向 HashSet 中添加元素时,HashSet 调用 HashMap 的 put() 方法。当新元素没有在 HashMap 中存在时,put() 方法返回 null ,此时 HashSet 返回 true,表示添加成功。如果元素已经存在于 HashMap(即已经在 HashSet 中),那么 put() 方法返回已经存在的 Object,此时 HashSet 返回 false,表示添加失败。


public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

  判断 HashSet 是否包含指定元素时,HashSet 调用 HashMap 的 containsKey() 方法。如果 HashMap 中包含该元素,则 HashSet 返回 true,否则返回 false。


public boolean contains(Object o) {
return map.containsKey(o);
}

  从 HashSet 中移除指定元素时,HashSet 调用 HashMap 的 remove() 方法。如果该元素存在于 HashMap 中(即在 HashSet 中),则返回 true,否则返回 false。


public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}

  如下是部分源码截图:


在这里插入图片描述


TreeSet


  TreeSet基于红黑树实现,它是一种自平衡的二叉查找树。每个节点都有一个额外的颜色属性,只能是红色或黑色。红黑树的基本操作包括插入、删除和查找。当我们向TreeSet中添加元素时,它会根据元素的大小来将元素添加到树中的合适位置。对于每个节点,其左子树的所有元素都比该节点的元素小,右子树的所有元素都比该节点的元素大。在删除时,如果要删除的节点有两个子节点,会先在右子树中找到最小元素,然后将该节点的元素替换为最小元素。删除最小元素就是从根节点开始,一直找到最左侧的节点即可。


public class TreeSet extends AbstractSet implements NavigableSet, Cloneable, java.io.Serializable {
private transient NavigableMap m;
private static final Object PRESENT = new Object();

public TreeSet() {
this(new TreeMap());
}

public TreeSet(NavigableMap m) {
this.m = m;
}

public boolean add(E e) {
return m.put(e, PRESENT)==null;
}

public boolean contains(Object o) {
return m.containsKey(o);
}

public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}
}

  这段代码定义了一个泛型类TreeSet,继承了AbstractSet类,并实现了接口NavigableSet,Cloneablejava.io.Serializable


  类中的m变量是一个NavigableMap类型的成员变量,TreeSet内部实际上是通过使用TreeMap实现的。


  类中还定义了PRESENT静态常量,用于表示在TreeSet中已经存在的元素。


  TreeSet类中的add方法实现了向集合中添加元素的功能,使用了NavigableMap中的put方法,如果添加的元素在集合中不存在,则返回null,否则返回PRESENT


  contains方法判断集合中是否包含某个元素。使用了NavigableMap中的containsKey方法。


  remove方法实现删除某个元素的功能,使用NavigableMap中的remove方法,如果删除成功,则返回PRESENT


  如下是部分源码截图:


在这里插入图片描述


应用场景案例


  Set的一个常见应用场景就是去重。由于Set中不允许存在重复元素,因此我们可以利用Set来去除列表中的重复元素,代码如下:


代码演示


List list = new ArrayList<>(Arrays.asList(1, 2, 3, 2, 1));
Set set = new HashSet<>(list);
list = new ArrayList<>(set);
System.out.println(list); // output: [1, 2, 3]

代码分析


  根据如上代码,在此我给大家进行深入详细的解读一下测试代码,以便于更多的同学能够理解并加深印象。


  该代码创建了一个包含重复元素的整型列表list,并使用list初始化了一个整型哈希集合set。然后,通过将set转换回一个新的ArrayList对象,生成一个不带重复元素的整型列表list。最后,输出list的元素。 因此,代码输出应该是[1, 2, 3]。


优缺点分析


优点



  1. Set接口的实现类可以高效地检查元素是否存在;

  2. Set接口的实现类不允许存在重复元素,可以用来进行去重操作;

  3. HashSet的添加、删除、查找操作时间复杂度为O(1);TreeSet的添加、删除、查找操作时间复杂度为O(logN)。


缺点



  1. 如果需要有序存储元素,那么需要使用TreeSet,但是由于TreeSet是基于红黑树实现的,因此占用内存空间较大;

  2. HashSet在哈希冲突的情况下,会导致链表长度增加,从而影响查找效率;

  3. HashSet在遍历元素时,元素的顺序不能保证。


类代码方法介绍


HashSet



  1. add(E e):向集合中添加元素;

  2. clear():清空集合中所有元素;

  3. contains(Object o):判断集合中是否存在指定的元素;

  4. isEmpty():判断集合是否为空;

  5. iterator():返回一个用于遍历集合的迭代器;

  6. remove(Object o):从集合中移除指定的元素;

  7. size():返回集合中元素的数量。


TreeSet



  1. add(E e):向集合中添加元素;

  2. ceiling(E e):返回集合中大于等于指定元素的最小元素;

  3. clear():清空集合中所有元素;

  4. contains(Object o):判断集合中是否存在指定的元素;

  5. descendingIterator():返回一个逆序遍历集合的迭代器;

  6. first():返回集合中的第一个元素;

  7. headSet(E toElement, boolean inclusive):返回集合中小于指定元素的子集;

  8. isEmpty():判断集合是否为空;

  9. iterator():返回一个用于遍历集合的迭代器;

  10. last():返回集合中的最后一个元素;

  11. remove(Object o):从集合中移除指定的元素;

  12. size():返回集合中元素的数量;

  13. subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive):返回集合中大于等于fromElement且小于等于toElement的子集;

  14. tailSet(E fromElement, boolean inclusive):返回集合中大于等于指定元素的子集。


测试用例


下面是一些测试用例,展示了Set接口的一些基本操作:


package com.demo.javase.day61;

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
*
@Author bug菌
*
@Date 2023-11-06 10:33
*/

public class SetTest {

public static void main(String[] args) {

// 创建一个 HashSet 对象
Set set = new HashSet();

// 向集合中添加元素
set.add("Java");
set.add("C++");
set.add("Python");

// 打印出集合中的元素个数
System.out.println("集合中的元素个数为:" + set.size());

// 判断集合是否为空
System.out.println("集合是否为空:" + set.isEmpty());

// 判断集合中是否包含某个元素
System.out.println("集合中是否包含 Python:" + set.contains("Python"));

// 从集合中移除某个元素
set.remove("C++");
System.out.println("从集合中移除元素后,集合中的元素个数为:" + set.size());

// 使用迭代器遍历集合中的元素
Iterator iterator = set.iterator();
System.out.println("遍历集合中的元素:");
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}

// 清空集合中的所有元素
set.clear();
System.out.println("清空集合中的元素后,集合中的元素个数为:" + set.size());
}
}

该测试用例使用了HashSet作为实现Set接口的具体类,并测试了以下基本操作:



  1. 向集合中添加元素

  2. 打印出集合中的元素个数

  3. 判断集合是否为空

  4. 判断集合中是否包含某个元素

  5. 从集合中移除某个元素

  6. 使用迭代器遍历集合中的元素

  7. 清空集合中的所有元素


测试结果


  根据如上测试用例,本地测试结果如下,仅供参考,你们也可以自行修改测试用例或者添加更多的测试数据或测试方法,进行熟练学习以此加深理解。


当运行该测试用例后,我们将得到以下输出结果:


集合中的元素个数为:3
集合是否为空:false
集合中是否包含 Python:true
从集合中移除元素后,集合中的元素个数为:2
遍历集合中的元素:
Java
Python
清空集合中的元素后,集合中的元素个数为:0

具体执行截图如下:


在这里插入图片描述


测试代码分析


  根据如上测试用例,在此我给大家进行深入详细的解读一下测试代码,以便于更多的同学能够理解并加深印象。


这段代码演示了如何使用Java中的Set接口和HashSet类。具体来说,代码实现了:


1.创建一个HashSet对象。


2.向集合中添加元素。


3.打印出集合中的元素个数。


4.判断集合是否为空。


5.判断集合中是否包含某个元素。


6.从集合中移除某个元素。


7.使用迭代器遍历集合中的元素。


8.清空集合中的所有元素。


  从这段代码可以看出,Set接口和HashSet类可以帮助我们快速地实现集合的添加、删除、查找等操作,并且还支持迭代器遍历集合中的所有元素。


作者:bug菌
来源:juejin.cn/post/7298969233546067968
收起阅读 »