注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

听说你会架构设计?来,弄一个打车系统

目录 引言 网约车系统 需求设计 概要设计 详细设计 体验优化 小结 1.引言 1.1 台风来袭 深圳上周受台风“苏拉”影响,从 9 月 1 日 12 时起在全市启动防台风和防汛一级应急响应。 对深圳打工人的具体影响为,当日从下午 4 点起全市...
继续阅读 »

目录




  1. 引言

  2. 网约车系统



    1. 需求设计

    2. 概要设计

    3. 详细设计

    4. 体验优化



  3. 小结



1.引言


1.1 台风来袭


深圳上周受台风“苏拉”影响,从 9 月 1 日 12 时起在全市启动防台风和防汛一级应急响应。


对深圳打工人的具体影响为,当日从下午 4 点起全市实行 “五停”:停工、停业、停市,当日已经停课、晚上 7 点后停运。


由于下午 4 点停市,于是大部分公司都早早下班。其中有赶点下班的,像这样:



有提前下班的,像这样:



还有像我们这样要居家远程办公的:



1.2 崩溃打车


下午 4 点左右,公交和地铁都人满为患。


于是快下班(居家办公)的时候就想着打个车回家,然而打开滴滴之后:



排队人数 142 位,这个排队人数和时长,让我的心一下就拔凉拔凉的。


根据历史经验,在雨天打上车的应答时间得往后推半个小时左右。更何况,这还是台风天气!


滴滴啊滴滴,你就不能提前准备一下嘛,这个等待时长,会让你损失很多订单分成的。


但反过来想,这种紧急预警,也不能完全怪打车平台,毕竟,车辆调度也是需要一定时间的。在这种大家争相逃命(bushi 的时候,周围的车辆估计也不太够用。


卷起来


等着也是等着,于是就回到公司继续看技术文章。这时我突然想到,经过这次车辆紧急调度之后,如果我是滴滴的开发工程师,需要怎么处理这种情况呢?


如果滴滴的面试官在我眼前,他又会怎么考量候选人的技术深度和产品思维呢?


2. 设计一个“网约车系统”


面试官:“滴滴打车用过是吧!看你简历里写道会架构设计是吧,如果让你设计一个网约车系统,你会从哪些方面考虑呢?”


2.1 需求分析


网约车系统(比如滴滴)的核心功能是把乘客的打车订单发送给附件的网约车司机,司机接单后,到上车点接送乘客,乘客下车后完成订单。


其中,司机通过平台约定的比例抽取分成(70%-80%不等),乘客可以根据第三方平台的信用值(比如支付宝)来开通免密支付,在下车后自动支付订单。用例图如下:



乘客和司机都有注册登录功能,分属于乘客用户模块和司机用户模块。网约车系统的另外核心功能是乘客打车,订单分配,以及司机送单。


2.2 概要设计


网约车系统是互联网+共享资源的一种模式,目的是要把车辆和乘客结合起来,节约已有资源的一种方式,通常是一辆网约车对多个用户。


所以对于乘客和司机来说,他们和系统的交互关系是不同的。比如一个人一天可能只打一次车,而一个司机一天得拉好几趟活。


故我们需要开发两个 APP 应用,分别给乘客和司机打车和接单,架构图如下:



1)乘客视角


如上所示:乘客在手机 App 注册成为用户后,可以选择出发地和目的地,进行打车。


打车请求通过负载均衡服务器,经过请求转发等一系列筛选,然后到达 HTTP 网关集群,再由网关集群进行业务校验,调用相应的微服务。


例如,乘客在手机上获取个人用户信息,收藏的地址信息等,可以将请求转发到用户系统。需要叫车时,将出发地、目的地、个人位置等信息发送至打车系统


2)司机视角


如上图所示:司机在手机 App 注册成为用户并开始接单后,打开手机的位置信息,通过 TCP 长连接定时将自己的位置信息发送给平台,同时也接收平台发布的订单消息。



司机 App 采用 TCP 长连接是因为要定时发送和接收系统消息,若采用 HTTP 推送:


一方面对实时性有影响,另一方面每次通信都得重新建立一次连接会有失体面(耗费资源)。​



司机 App:每 3~5 秒向平台发送一次当前的位置信息,包括车辆经纬度,车头朝向等。TCP 服务器集群相当于网关,只是以 TCP 长连接的方式向 App 提供接入服务,地理位置服务负责管理司机的位置信息。


3)订单接收


网关集群充当业务系统的注册中心,负责安全过滤,业务限流,请求转发等工作。


业务由一个个独立部署的网关服务器组成,当请求过多时,可以通过负载均衡服务器将流量压力分散到不同的网关服务器上。


当用户打车时,通过负载均衡服务器将请求打到某一个网关服务器上,网关首先会调用订单系统,为用户创建一个打车订单(订单状态为 “已创建”),并存库。


然后网关服务器调用打车系统,打车系统将用户信息、用户位置、出发地、目的地等数据封装到一个消息包中,发送到消息队列(比如 RabbitMQ),等待系统为用户订单分配司机。


4)订单分配


订单分配系统作为消息队列的消费者,会实时监听队列中的订单。当获取到新的订单消息时,订单分配系统会将订单状态修改为 “订单分配中”,并存库。


然后,订单分配系统将用户信息、用户位置、出发地、目的地等信息发送给订单推送 SDK


接着,订单推送 SDK 调用地理位置系统,获取司机的实时位置,再结合用户的上车点,选择最合适的司机进行派单,然后把订单消息发送到消息告警系统。这时,订单分配系统将订单状态修改为 “司机已接单” 状态。


订单消息通过专门的消息告警系统进行推送,通过 TCP 长连接将订单推送到匹配上的司机手机 App。


5)拒单和抢单


订单推送 SDK 在分配司机时,会考虑司机当前的订单是否完成。当分配到最合适的司机时,司机也可以根据自身情况选择 “拒单”,但是平台会记录下来评估司机的接单效率。


打车平台里,司机如果拒单太多,就可能在后续的一段时间里将分配订单的权重分数降低,影响自身的业绩。



订单分派逻辑也可以修改为允许附加的司机抢单,具体实现为:


当订单创建后,由订单推送 SDK 将订单消息推送到一定的地理位置范围内的司机 App,在范围内的司机接收到订单消息后可以抢单,抢单完成后,订单状态变为“已派单”。


2.3 详细设计


打车平台的详细设计,我们会关注网约车系统的一些核心功能,如:长连接管理、地址算法、体验优化等。


1)长连接的优势


除了网页上常用的 HTTP 短连接请求,比如:百度搜索一下,输入关键词就发起一个 HTTP 请求,这就是最常用的短连接。


但是大型 APP,尤其是涉及到消息推送的应用(如 QQ、微信、美团等应用),几乎都会搭建一套完整的 TCP 长连接通道。


一张图看懂长连接的优势:



图片来源:《美团点评移动网络优化实践》


通过上图,我们得出结论。相比短连接,长连接优势有三:




  1. 连接成功率高




  2. 网络延时低




  3. 收发消息稳定,不易丢失




2)长连接管理


前面说到了长连接的优势是实时性高,收发消息稳定,而打车系统里司机需要定期发送自身的位置信息,并实时接收订单数据,所以司机 App 采用 TCP 长连接的方式来接入系统。


和 HTTP 无状态连接不同的是,TCP 长连接是有状态的连接。所谓无状态,是指每次用户请求可以随意发送到某一台服务器上,且每台服务器的返回相同,用户不关心是哪台服务器处理的请求。



当然,现在 HTTP2.0 也可以是有状态的长连接,我们此处默认是 HTTP1.x 的情况。



而 TCP 长连接为了保证传输效率和实时性,服务器和用户的手机 App 需要保持长连接的状态,即有状态的连接。


所以司机 App 每次信息上报或消息推送时,都会通过一个特定的连接通道,司机 App 接收消息和发送消息的连接通道是固定不变的。


因此,司机端的 TCP 长连接需要进行专门管理,处理司机 App 和服务器的连接信息,架构图如下:



为了保证每次消息的接收和推送都能找到对应通道,我们需要维护一个司机 App 到 TCP 服务器的映射关系,可以用 Redis 进行保存。


当司机 App 第一次登录,或者和服务器断开连接(比如服务器宕机、用户切换网络、后台关闭手机 App 等),需要重连时,司机 App 会通过用户长连接管理系统重新申请一个服务器连接(可用地址存储在 Zookeeper 中),TCP 连接服务器后再刷新 Redis 的缓存。


3)地址算法


当乘客打车后,订单推送 SDK 会结合司机所在地理位置,结合一个地址算法,计算出最适合的司机进行派单。


目前,手机收集地理位置一般是收集经纬度信息。经度范围是东经 180 到西经 180,纬度范围是南纬 90 到北纬 90。


我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成4个部分。



根据这个原理,我们可以先将二维的空间经纬度编码成一个字符串,来唯一标识司机和乘客的位置信息。再通过 Redis 的 GeoHash 算法,来获取乘客附加的所有司机信息。


GeoHash 算法的原理是将乘客的经纬度换算成地址编码字符串,表示在某个矩形区域,通过这个算法可以快速找到同一个区域的所有司机


它的实现用到了跳表数据结构,具体实现为:


将某个市区的一块范围作为 GeoHash 的 key,这个市区范围内所有的司机存储到一个跳表中,当乘客的地理位置出现在这个市区范围时,获取该范围内所有的司机信息。然后进一步筛选出最近的司机信息,进行派单。


4)体验优化


1. 距离算法


作为线上派单,通过距离运算来分配订单效果一定会比较差,因为 Redis 计算的是两点之间的空间距离,但司机必须沿道路行驶过来,在复杂的城市路况下,也许几十米的空间距离行驶十几分钟也未可知。


所以,后续需综合行驶距离(而非空间距离)、司机车头朝向以及上车点进行路径规划,来计算区域内每个司机到达乘客的距离和时间。


更进一步,如果区域内有多个乘客和司机,就要考虑所有人的等待时间,以此来优化用户体验,节省派单时间,提升盈利额。



2. 订单优先级


如果打车订单频繁取消,可根据司机或乘客行为进行判责。判责后给乘客和司机计算信誉分,并告知用户信誉分会影响乘客和司机的使用体验,且关联到派单的优先级。


司机接单优先级

综合考虑司机的信誉分,投诉次数,司机的接单数等等,来给不同信誉分的司机分配不同的订单优先级。


乘客派单优先级

根据乘客的打车时间段,打车距离,上车点等信息,做成用户画像,以合理安排司机,或者适当杀熟(bushi。


PS:目前有些不良打车平台就是这么做的 🐶  甚至之前爆出某打车平台,会根据不同的手机系统,进行差异收费


4. 小结


4.1 网约车平台发展


目前,全球网约车市场已经达到了数千亿美元的规模,主要竞争者包括滴滴、Uber、Grab 等公司。在中国,滴滴作为最大的网约车平台已经占据了绝大部分市场份额。


网约车的核心商业逻辑比较简单,利益关联方主要为平台、司机、车辆、消费者。


平台分别对接司机、车辆【非必选项,有很多司机是带车上岗】和乘客,通过有效供需匹配赚取整个共享经济链省下的钱。


具体表现为:乘客和司机分别通过网约平台打车和接单,平台提供技术支持。乘客为打车服务付费,平台从交易金额中抽成(10%-30%不等)。



据全国网约车监管信息交互平台统计,截至 2023 年 2 月底,全国共有 303 家网约车平台公司取得网约车平台经营许可。


这些平台一部分是依靠高德打车、百度地图、美团打车为代表的网约车聚合平台;另一部分则是以滴滴出行、花小猪、T3 为代表的出行平台


4.2 网约车平台现状


随着出行的解封,网约车平台重现生机。


但由于部分网约车聚合平台的准入门槛太低,所以在过去一段时间里暴露出愈来愈多的问题。如车辆、司机合规率低,遇到安全事故,产生责任纠纷,乘客维权困难等等。


由于其特殊的模式,导致其与网约车运营商存在责任边界问题,一直游离在法律边缘。



但随着网约车聚合平台的监管不断落地,全国各地都出行了一定的监管条例。


比如某打车平台要求车辆将司机和乘客的沟通记录留档,除了司机与乘客的在线沟通记录必须保存以外,还需要一个语音电话或车载录音转换,留存一段时间备查。


有了这些人性化的监管条例和技术的不断创新,网约车平台或许会在未来的一段时间内,继续蓬勃发展。


后话


面试官:嗯,又专又红,全面发展!这小伙子不错,关注了~


作者:xin猿意码
来源:juejin.cn/post/7275211391102746684
收起阅读 »

马斯克的Twitter迎来严重危机,我国的超级App模式是否能拯救?

Meta公司近期推出的Threads 被网友戏称为“Twitter杀手”,该应用上线仅一天,用户就突破了3000 万人。外界普遍认为,这是推特上线17年来遭遇的最严峻危机。面对扎克伯格来势汹汹的挑战马斯克会如何快速组织反击? 前段时间闹得沸沸扬扬的“马扎大战”...
继续阅读 »

Meta公司近期推出的Threads 被网友戏称为“Twitter杀手”,该应用上线仅一天,用户就突破了3000 万人。外界普遍认为,这是推特上线17年来遭遇的最严峻危机。面对扎克伯格来势汹汹的挑战马斯克会如何快速组织反击?


前段时间闹得沸沸扬扬的“马扎大战”再出新剧情,继“笼斗”约架被马斯克妈妈及时叫停之后,马斯克在7月9日再次向扎克伯克打起嘴炮,这次不仅怒骂小扎是混蛋,还要公开和他比大小?!!此番马斯克的疯狂言论,让网友直呼他不是疯了就是账号被盗了。



互联网各路“吃瓜群众”对于大佬们宛如儿戏般的掐架喜闻乐见,摇旗呐喊!以至于很多人忘了这场闹剧始于一场商战:“马扎大战”开始之初,年轻的扎克伯格先发制人,率先挥出一记左钩拳——Threads,打得老马措手不及。


Threads 被网友戏称“Twitter杀手”,该应用上线仅一天,用户就突破了3000 万人。其中,不乏从推特中逃离的各界名流。舆论普遍认为,这是Twitter上线17年来遭遇的最严峻危机。



紧接着马斯克还以一记右勾拳,一封律师函向小扎发难,称Meta公司“非法盗用推特的商业秘密和其他知识产权的行为”。虽然Meta公司迅速回应,否认其团队成员中有Twitter的前雇员。但这样的回应似乎没有什么力度,Threads在功能、UI设计上均与Twitter相似,并在相关宣传中表示,Threads“具有良好的运营”,并称其为当前“一片混乱中的”Twitter的绝佳替代品。


社交平台之战的第一个回合,小扎向老马发起了猛烈的攻势。吃了一记闷拳的马斯克除了打嘴炮之外,会如何快速组织有效的反击?


会不会是老马嘴里的“非秘密武器”App X —App of Everything?


超级App或成为Twitter反击重拳


时间回溯到去年,在收购Twitter之前,马斯克就放出豪言即将创建一款他称之为“App X”的功能包罗万有的超级应用软件(Super App), 在他的愿景中,超级 “App X”就如同多功能瑞士军刀(Swiss Army Knife)般,能够包办用户日常生活大小事,包括:社交、购物、打车、支付等等。他希望这款App可以成为美国首个集食、衣、住、行功能于一身的平台。收购Twitter,似乎给了他改造实现这个超级App的起步可能。


马斯克坦言这是从微信的经营模式中汲取的灵感。微信一直被视为“超级应用程序”的代表,作为一体化平台,满足了用户的各种需求,包括即时通讯、社交、支付等等。在去年6月的推特全体员工大会上,马斯克就表示“我们还没有一个像微信那样优秀的应用,所以我的想法是为何不借鉴微信”。马斯克还在推特上写到“购买推特是创建App X的加速器,这是一个超级App(Everything App)。”


从他接手Twitter的任期开始,马斯克便加快推动超级 “App X”的发展步伐。对标于微信,除了社交功能之外,还将推出支付与电子商务。而获得监管许可是实现支付服务的重要第一步,支付也成了推特转型超级 “App X”的第一步,除了商业的必要性外,此举多少还有点宿命感。要知道,马斯克是从支付行业起家的,1999 年他投资 1200 万美元与Intuit前首席执行官 Bill Harris 共同创立了 X.com,而这家公司就是PayPal的前身。


据英国《金融时报》 1月份报道,Twitter 已经开始申请联邦和州监管许可。同时Twitter内部正在开发电子支付功能,未来更会整合其他金融服务,以实现超级App的终极目标。


但是,在亚洲“超级应用”巨头之外,几乎没有消息应用实现支付服务的先例,Whats App和Telegram 都未推出类似服务。老马领导下的Twitter,能不能成功?


添加了支付能力,也只不过是迈向“超级”的第一小步。挑战在于怎么把“everything”卷进来:衣食住行的数字服务、各行各业的商业场景。在微信世界,everything = 小程序。老马是否也要开发一套Twitter版小程序技术、缔造一个“Twitter小程序”宇宙?



“超级App”技术已实现普世化


事实上,马斯克并非“Super App ”技术理念在欧美的唯一拥趸。超级App的雄心壮志多年来早已成为美国公司管理层炫酷PPT展示中的常客了,甚至连沃尔玛都曾考虑过超级App的计划。


全球权威咨询机构Gartner发布的企业机构在2023年需要探索的十大战略技术趋势中也提到了超级应用。并预测,到2027年,全球50%以上的人口将成为多个超级应用的日活跃用户。


国外互联网巨头们开始对超级App技术趋之若鹜,但超级App的技术,是不是只有巨头才能拥有呢?


答案是否定的。互联网技术往往领先于企业应用5~7年,现在这个技术正在进入企业软件世界,任何行业的任何企业都可以拥有。


一种被称为“小程序容器”的技术,是构建超级App的核心,目前已经完全实现普及商用。背后推手是 FinClip,它作为当前市场上唯一独立小程序容器技术产品,致力于把制造超级App的技术带进各行各业,充当下一代企业数字化软件的技术底座。


超级App的技术实现,原理上是围绕一种内容载体,由三项技术共同组成:内容载体通常是某种形态的“轻巧应用”——读者最容易理解的,当然就是小程序,万事万物的数字场景,以小程序形态出现。马斯克大概率在把Twitter改造成他所谓的App X的过程中,要发展出一种类似的东西。反正在国内这就叫小程序,在W3C正在制定的标准里,这叫做Mini-App。我们就姑且依照大家容易理解的习惯,把这种“轻巧应用”称之为小程序吧。


围绕小程序,一个超级App需要在设备端实现“安全沙箱”+ “运行时”,负责把小程序从网上下载、关在一个安全隔离环境中,然后解释运行小程序内容;小程序内容的“镜像”(也就是代码包),则是发布在云端的小程序应用商店里,供超级App的用户在使用到某个商业场景或服务的时候,动态下载到设备端按需运行 – 随需随用且可以用完即弃。小程序应用商店负责了小程序的云端镜像“四态合一“(开发、测试、灰度、投产)的发布管理。


不仅仅这样,超级App本质上是一个庞大的数字生态平台,里面的小程序内容,并不是超级App的开发团队开发的,而是由第三方“进驻”和“上架”,所以,超级App还有一个非常重要的云端运营中心,负责引进和管理小程序化的数字内容生态。


超级App之所以“超级”,是因为它的生命周期(开发、测试、发版、运营),和运行在它里面的那些内容(也就是小程序)的生命周期完全独立,两者解耦,从而可运行“全世界”为其提供的内容、服务,让“全世界”为它提供“插件”而无需担心超级App本身的安全。第三方的内容无论是恶意的、有安全漏洞的或者其他什么潜在风险,并不能影响平台自身的安全稳定、以及平台上由其他人提供的内容安全保密。在建立了这样的安全与隔离机制的基础上,超级App才能实现所谓的“Economy of Scale”(规模效应),可以大开门户,放心让互联网上千行百业的企业、个人“注入插件”,产生丰富的、包罗万有的内容。


对于企业来说,拥有一个自己的超级App意味着什么呢?是超级丰富的业务场景、超级多元的合作生态、超级数量的内容开发者、以及超级敏捷的运营能力。相比传统的、封闭的、烟囱式的App,超级App实际上是帮助企业突破传统边界、建立安全开放策略、与合作伙伴实现数字化资源交换的技术手段,真正让一家企业具备平台化商业模式,加速数字化转型、增强与世界的在线连接、形成自己的网络效应。


超级App不是一个App -- Be A“world” platform


超级App+小程序,这不是互联网大平台的专利。对于传统企业来说,考虑打造自己的超级App动因至少有三:


首先,天下苦应用商店久矣。明明是纯粹企业内部一个商业决策行为,要发布某个功能或服务到自己的App上从而触达自己的客服服务自己的市场,这个发版却不得不经过不相干的第三方(App store们)批准。想象一下,你是一家银行,现在你计划在你的“数字信用卡”App里更新上架某个信用卡服务功能,你的IT完成了开发、测试,你的信用卡业主部门作了验收,你的合规、风控、法务部门通过内部的OA系统环环相扣、层层审批,现在流程到了苹果、谷歌… 排队等候审核,最后流程回到IT,服务器端一顿操作配合,正式开闸上线。你的这个信用卡服务功能,跟苹果谷歌们有一毛钱关系?但对不起,他们在你的审批流程里拥有终极话语权。


企业如果能够控制业务内容的技术实现粒度,通过自己的“服务商店”、“业务内容商店”去控制发布,让“宿主”App保持稳定,则苹果谷歌们也不用去操这个心你的App会不会每次更新都带来安全漏洞或者其他风险行为。


第二,成为一个“world platform”,企业应该有这样的“胸襟”和策略。虽然你可能不是腾讯不是推特不拥有世界级流量,这不妨碍你成为自己所在细分市场细分领域的商业世界里的平台,这里背后的思路是开放——开放平台,让全“世界”的伙伴成为我的生态,哪怕那个“世界”只存在于一个垂直领域。而这,就是数字化转型。讲那么多“数字化转型”理念,不如先落地一个技术平台作为载体,talk is cheap,show me the code。当你拥有一个在自己那个商业世界里的超级App和数以百千计的小程序的时候,你的企业已经数字化转型了。


第三,采用超级App是最有效的云化策略,把你和你的合作伙伴的内容作为小程序,挪到云端去,设备端只是加载运行和安全控制这些小程序内容的入口。在一个小小的手机上弹丸之地,“尺寸”限制了企业IT的生产力 – 无法挤进太大的团队让太多工程师同时开发生产,把一切挪到云上,那里的空间无限大,企业不再受限于“尺寸”,在云上你可以无上限的扩展技术团队,并行开发,互不认识互不打扰,为你供应无限量的内容。互联网大平台上动辄几百万个小程序是怎么来的?并行开发、快速迭代、低成本试错、无限量内容场景供应,这样的技术架构,是不是很值得企业借鉴?


做自己所在细分市场、产业宇宙里的“World Platform”吧,技术的发展已经让这一切唾手可得,也许在马斯克还在打“App of Everything”嘴炮的时候,你的超级App已经瓜熟蒂落、呱呱坠地。


作者:Finbird
链接:https://juejin.cn/post/7256759718811418682
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iPhone 14 被用户吐槽电池老化

iOS
国内要闻 香港高校陆续拥抱 ChatGPT,暑期忙于筹备新学期变革 香港众多高校拥抱了 OpenAI 的聊天机器人 ChatGPT。香港科技大学率先引入ChatGPT。6 月 1 日它正式为学生与教职人员提供港科大版 ChatGPT,是香港首所为学生与教职工提...
继续阅读 »



国内要闻


香港高校陆续拥抱 ChatGPT,暑期忙于筹备新学期变革


香港众多高校拥抱了 OpenAI 的聊天机器人 ChatGPT。香港科技大学率先引入ChatGPT。6 月 1 日它正式为学生与教职人员提供港科大版 ChatGPT,是香港首所为学生与教职工提供 ChatGPT 的大学。香港中文大学、香港理工大学、香港浸会大学等高校也陆续推出使用 AI 工具的指引,共同希望师生批判性探索和谨慎使用 AI。除了在高等教育掀起热潮,AI 也将进入香港的初中课堂。香港教育局指出,ChatGPT 可以成为有价值的教育工具,但要留意抄袭的伦理问题,并期望所有公立中学尽快规划,于 2023/24 学年在“资讯和通讯科技课程”中安排 10 至 14 小时的 AI 课程教授。(奇客Solidot)


小鹏智驾灵魂人物吴新宙确认离职


小鹏汽车董事长何小鹏发文称,因家庭和多方面的原因,小鹏汽车自动驾驶中心副总裁吴新宙在 2022 年下半年表示要回到美国。在此后 10 个月时间里,小鹏汽车确立全新的工作模式,并在架构和组织上进行了提前优化和迭代。负责 XNGP 项目的李力耘博士将接手自动驾驶团队。


据业内人士透露,吴新宙或将担任英伟达“全球副总裁”这一级别的职位,直接向黄仁勋汇报,“是黄仁勋本人亲自出马,将吴新宙招至麾下。”届时,吴新宙将成为全球知名公司的最高等级华人高管,并继续在芯片等多个方面和小鹏汽车深度合作。(雷锋网)


微信要做“小绿书”?知情人士:小范围内测,优化视频号图文发布及呈现


据网传消息,微信正在灰度测试“小绿书”。从知情人士处了解到,这是一次非常小范围的内测,不是新功能,初衷就是为了更方便视频号创作者发布图文短内容,以及提高用户获得信息的效率。(36氪)


OPPO IoT 事业群负责人李开新离职,电视业务几近裁撤


OPPO IoT 事业群负责人李开新离职,可能导致其电视业务几近裁撤。OPPO IoT 部门最近两年变动不断,一直在探索新的产品线。虽然 OPPO 在 IoT 方面也尝试过其他小品类,但较为稳定的业务还是耳机和可穿戴设备。近期有报道称 OPPO 将裁撤电视业务,但 OPPO 方面表示电视业务目前运营正常。


百度千帆接入 LLaMA2 等 33 个大模型


8 月 2 日,百度智能云宣布千帆大模型平台完成新一轮升级,全面接入LLaMA2全系列、ChatGLM2、RWKV、MPT 等 33 个大模型,成为国内拥有大模型最多的平台,接入的模型经过千帆平台二次性能增强,模型推理成本可降低50%。同时,上线 103 个预置 Prompt 模板,覆盖对话、游戏、编程、写作十余个场景,并发布多款全新插件。


国际要闻


iPhone 14 被用户吐槽电池老化


据报道,不少 iPhone 14 系列机主在社交媒体吐槽,该系列出现了严重的电池老化问题。iPhone 14 系列于 2022 年 9 月上市发售,首批用户持有时间还不到一年。社交网站上不少用户留言反馈称手机电池健康已经低于 90%,最多的跌到 87%。苹果官方对“电池健康”的描述为:包含最大电池容量和峰值性能容量。一般在手机电池正常使用的情况下,完整充电次数达到 500 次,电池健康的最大容量低于 80% 则会影响手机峰值性能,保修期内的 iPhone 可以得到官方保修甚至更换。(IT之家)


消息称 OpenAI 正测试第三代图片生成模型


OpenAI 在去年 4 月推出了第二代 DALL-E“文生图”模型,该模型凭借过硬的实力吸引了业界广泛注意,据外媒表示,OpenAI 日前正在准备下一代 DALL-E AI 模型(DALL-E 3),目前该公司正在进行一系列 Alpha 测试,而部分用户已经提早接触到了该 AI 模型。(财联社)


韩国室温超导团队称论文存在缺陷


韩国一研究团队近日发布论文称实现了室温超导,在引起全球广泛关注的同时,也遭到了质疑。而该研究团队的成员表示,论文存在缺陷,系团队中的一名成员擅自发布,目前团队已要求下架论文。分析师郭明錤认为,常温常压超导体商业化的时程并没有任何能见度,但未来若能够顺利商业化,将对计算器与消费电子领域的产品设计有颠覆性的影响。即便是小如iPhone的行动装置,都能拥有与量子计算机匹敌的运算能力。(财联社)


消息称苹果 Vision Pro 开发者实验室冷清,开发者兴趣不大


苹果公司在 7 月份开始邀请开发者去 Vision Pro 的开发者实验室,这些实验室分布在库比蒂诺、伦敦、慕尼黑、上海、新加坡和东京等城市,但是目前看来,开发者对这些实验室并没有表现出很大的兴趣。据彭博社的 Mark Gurman 报道,这些开发者实验室“参与人数不多,只有少量的开发者”。


AI 打败 AI:谷歌研究团队利用 GPT-4 击败 AI-Guardian 审核系统


8 月 2 日消息,谷歌研究团队正在进行一项实验,他们使用 OpenAI 的 GPT-4 来攻破其他 AI 模型的安全防护措施,该团队目前已经攻破 AI-Guardian 审核系统,并分享了相关技术细节。谷歌 Deep Mind 的研究人员 Nicholas Carlini 在一篇题为“AI-Guardian 的 LLM 辅助开发”的论文中,探讨了使用 GPT-4“设计攻击方法、撰写攻击原理”的方案。据悉,GPT-4 会发出一系列错误的脚本和解释来欺骗 AI-Guardian ,论文中提到,GPT-4 可以让 AI-Guardian 认为“某人拿着枪的照片”是“某人拿着无害苹果的照片”,从而让 AI-Guardian 直接放行相关图片输入源。谷歌研究团队表示,通过 GPT-4 的帮助,他们成功地“破解”了 AI-Guardian 的防御,使该模型的精确值从 98% 的降低到仅 8%。(IT之家)


程序员专区


KubeSphere 3.4.0 发布


致力于打造以 Kubernetes 为内核的云原生分布式操作系统 KubeSphere 3.4.0 发布,该版本带来了值得大家关注的新功能以及增强:扩大对 Kubernetes 的支持范围,最新稳定性支持 1.26;重构告警策略架构,解耦为告警规则与规则组;提升集群别名展示权重,减少原集群名称不可修改导致的管理问题;升级 KubeEdge 组件到 v1.13 等。同时,还进行了多项修复、优化和增强,更进一步完善交互设计,并全面提升了用户体验。


Firefox 116 发布


浏览器 Firefox 116 正式发布,该版本新增加了编辑现有文本注释的可能性、用户可以从操作系统复制任何文件并将其粘贴到 Firefox 中,开发方面,Firefox 现在支持 CSP3 external hashes,添加了对 dirname 属性的支持。具体可查看发布说明:http://www.mozilla.org/en-US/firef…


作者:小莫欢
链接:https://juejin.cn/post/7262900590936604730
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

“WWW” 仍然属于 URL 吗?它可以消失吗?

多年来,我们的地址栏上一直在进行着一场小小的较真战。也就是Google、Instagram和Facebook 等品牌。该群组已选择重定向 example.com 至 http://www.example.com。相反:GitHub...
继续阅读 »

多年来,我们的地址栏上一直在进行着一场小小的较真战。也就是GoogleInstagramFacebook 等品牌。该群组已选择重定向 example.com 至 http://www.example.com。相反:GitHubDuckDuckGoDiscord。该组织选择执行相反的操作并重定向 http://www.example.com 到 example.com



“WWW”属于 URL 吗?一些开发人员对此主题持有强烈的意见。在了解了一些历史之后,我们将探讨支持和反对它的论据。


WWW是什么?


WWW代表"World Wide Web",是上世纪80年代晚期的一个发明,引入了浏览器和网站。使用"WWW"的习惯源于给子域名命名的传统:


如果没有WWW会发生什么问题?


1. 向子域名泄露cookies


反对"没有WWW"的域名的批评者指出,在某些情况下,subdomain.example.com可以读取example.com设置的cookies。如果你是一个允许客户在你的域名上运营子域名的Web托管提供商,这可能是不希望看到的。


然而,这种行为只存在于Internet Explorer中。


RFC 6265标准化了浏览器对cookies的处理,并明确指出这种行为是错误的。


另一个潜在的泄露源是example.com设置的cookies的Domain值。如果Domain值明确设置为example.com,那么这些cookies也将被其子域名所访问。


总之,只要你不明确设置Domain值,而且你的用户不使用Internet Explorer,就不会发生cookie泄露。



2. DNS的困扰


有时,"没有WWW"的域名可能会使你的域名系统(DNS)设置复杂化。


当用户在浏览器的地址栏中输入example.com时,浏览器需要知道他们想访问的Web服务器的Internet协议(IP)地址。浏览器通过你的域名的域名服务器向其DNS服务器(通常间接通过用户的互联网服务提供商(ISP)的DNS服务器)请求IP地址。如果你的域名服务器配置为响应包含IP地址的A记录,那么"没有WWW"的域名将正常工作。


在某些情况下,你可能希望使用规范名称(CNAME)记录来代替为你的网站设置。这样的记录可以声明http://www.example.comexample123.somecdnprovider.com的别名,这会告诉用户的浏览器去查找example123.somecdnprovider.com的IP地址,并将HTTP请求发送到那里。


请注意,上面的示例使用了一个WWW子域名。对于example.com,不可能定义一个CNAME记录。根据RFC 1912,CNAME记录不能与其他记录共存。如果你尝试为example.com定义CNAME记录,example.com上的MX(邮件交换)记录将无法存在。因此,就不可能在@example.com上接收邮件


一些DNS提供商可以让你绕过这个限制。Cloudflare称其解决方案为CNAME解析。通过这种技术,域名管理员配置一个CNAME记录,但他们的域名服务器将暴露一个A记录。


例如,如果管理员为example.com配置了指向example123.somecdnprovider.com的CNAME记录,并且存在一个指向1.2.3.4example123.somecdnprovider.com的A记录,那么Cloudflare就会暴露一个指向1.2.3.4的example.com的A记录。


总之,虽然这个问题对希望使用CNAME记录的域名所有者来说是有效的,但现在有一些DNS提供商提供了合适的解决办法。


没有WWW的好处


大部分反对WWW的论点是实用性或外观方面的。"无WWW"的支持者认为example.comhttp://www.example.com更容易说和输入(对于不那么精通技术的用户可能更不容易混淆)。


反对WWW子域名的人还指出,去掉它会带来一种谦虚的性能优势。网站所有者可以通过这样做每个HTTP请求节省4个字节。虽然这些节省对于像Facebook这样的高流量网站可能会累积起来,但带宽通常并不是一种紧缺的资源。


有"WWW"的好处


支持WWW的一个实际论点适用于使用较新顶级域的情况。例如,http://www.example.miamiexample.miami无法立即被识别为Web地址。对于具有诸如.com这样的可识别顶级域的网站,这不是一个太大的问题。


对搜索引擎排名的影响


目前的共识是你的选择不会影响你的搜索引擎表现。如果你希望从一个URL迁移到另一个URL,你需要配置永久重定向(HTTP 301)而不是临时重定向(HTTP 302)。永久重定向确保你旧的URL的SEO价值转移到新的URL。


同时支持两者的技巧


网站通常会选择example.comhttp://www.example.com作为官方网站,并为另一个配置HTTP 301重定向。理论上,可以支持http://www.example.com和example.com两者。但实际上,成本可能会超过效益。


从技术角度来看,你需要验证你的技术栈是否能够处理。你的内容管理系统(CMS)或静态生成的网站需要将内部链接输出为相对URL以保留访问者的首选主机名。除非你可以将主机名配置为别名,否则你的分析工具可能会将流量分别记录在两个主机名上。


最后,你需要采取额外的措施来保护你的搜索引擎表现。谷歌将把URL的"WWW""非WWW"版本视为重复内容。为了在其搜索索引中去重复内容,谷歌将显示它认为用户更喜欢的那个版本——不论是好是坏。


为了在谷歌中保持对自己的控制,建议插入规范链接标签。首先,决定哪个主机名将成为官方(规范)主机名。


例如,如果你选择了www.example.com,则必须在 https://example.com/my-article里的 <head> 上的标记 中插入以下代码段:

    <link href="<https://www.example.com/my-article>" rel="canonical"> 

这个代码片段告诉谷歌"无WWW"变体代表着相同的内容。通常情况下,谷歌会在搜索结果中偏好你标记为规范的版本,也就是在这个例子中的"WWW"变体。


总结


对于是否在URL中加入"WWW",人们有不同的观点。下面是支持和反对的论点:


支持"WWW"的论点:

  1. 存在子域名的安全性问题:某些情况下,子域名可以读取主域名设置的cookies。虽然这个问题只存在于Internet Explorer浏览器中,并且已经被RFC 6265标准化修复,但仍有人认为使用"WWW"可以避免潜在的安全风险。
  2. DNS配置的复杂性:如果你的域名系统(DNS)配置为响应包含IP地址的A记录,那么"没有WWW"的域名将正常工作。但如果你想使用CNAME记录来设置规范名称,那么"没有WWW"的域名可能会导致一些限制,例如无法同时定义CNAME记录和MX(邮件交换)记录。
  3. 对搜索引擎排名的影响:对于使用较新顶级域的网站,使用"WWW"可以帮助识别网址,而不是依赖可识别的顶级域名。然而,目前的共识是选择是否使用"WWW"对搜索引擎表现没有直接影响。

支持去除"WWW"的论点:

  1. 实用性和外观:去除"WWW"可以使域名更简洁和易于输入,减少了用户可能混淆的机会。
  2. 节省字节:去除"WWW"可以每个HTTP请求节省4个字节。虽然这对于高流量网站来说可能是一个可累积的优势,但对于大多数网站来说,带宽通常不是一个紧缺的资源。

最佳实践:
一般来说,网站会选择将example.com或www.example.com作为官方网址,并对另一个进行重定向。你可以通过使用HTTP 301永久重定向来确保旧URL的SEO价值转移到新URL。同时,你还可以在页面的标签中插入规范链接标签,告诉搜索引擎两个URL代表相同的内容,以避免重复内容问题。


需要注意的是,在做决策时要考虑到技术栈的支持能力、DNS配置的限制和谷歌对搜索排名的处理方式。


本文同步我的技术文档


作者:狗头大军之江苏分军
链接:https://juejin.cn/post/7263274550074507321
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

从《孤注一掷》出发,聊聊 SSL 证书的重要性

你去看《孤注一掷》了吗?相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治,以及电信诈骗中系统性极强的诈骗技巧,引发了大量讨论。  图片来源于电影《孤注一掷》 这部电影除...
继续阅读 »

你去看《孤注一掷》了吗?相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治,以及电信诈骗中系统性极强的诈骗技巧,引发了大量讨论。 


 图片来源于电影《孤注一掷》


这部电影除了让人后背发凉外,也不禁让人回忆起了曾经上网冲浪遇到的种种现象:看小说时性感荷官总在网页右下角在线发牌;看电影时网页左下角常常蹦出“在线老虎机”……这些让人烦不胜烦的广告弹窗之所以出现,要么是建站人员利欲熏心投放了非法广告,要么就是因为网站使用了不安全的 HTTP 协议而遭到了攻击,正常的网页内容被恶意篡改。


网站是电信诈骗、网络赌博等非法内容出现的重灾区,建站者和使用者都应该提高安全意识,特别是对建站者来说,保护通信安全才能更好的承担起建站责任。本文将从 HTTP 讲起,介绍 HTTPS 保护通信安全的原理,以及作为网络通信安全基石的 SSL 证书的重要性。


HTTP 协议


HTTP(Hyper Text Transfer Protocol)协议是超文本传输协议。它是从 WEB 服务器传输超文本标记语言(HTML)到本地浏览器的传送协议。HTTP 基于 TCP/IP 通信协议来传递数据,通信双方在 TCP 握手后即可开始互相传输 HTTP 数据包。具体过程如下图所示: 


 HTTP 建立流程


HTTP 协议中,请求和响应均以明文传输。如下图所示,在访问一个使用 HTTP 协议的网站时,通过抓包软件可以看到网站 HTTP 响应包中的完整 HTML 内容。




虽然 HTTP 明文传输的机制在性能上带来了优势,但同时也引入了安全问题:

  • 缺少数据机密性保护。HTTP 数据包内容以明文传输,攻击者可以轻松窃取会话内容。
  • 缺少数据完整性校验。通信内容以明文传输,数据内容可被攻击者轻易篡改,且双方缺少校验手段。
  • 缺少身份验证环节。攻击者可冒充通信对象,拦截真实的 HTTP 会话。

HTTP 劫持


作为划时代的互联网通信标准之一,HTTP 协议的出现为互联网的普及做出了不可磨灭的贡献。但正如上节谈到, HTTP 协议因为缺少加密、身份验证的过程导致很可能被恶意攻击,针对 HTTP 协议最常见的攻击就是 HTTP 劫持。


HTTP 劫持是一种典型的中间人攻击。HTTP 劫持是在使用者与其目的网络服务所建立的数据通道中,监视特定数据信息,当满足设定的条件时,就会在正常的数据流中插入精心设计的网络数据报文,目的是让用户端程序解析“错误”的数据,并以弹出新窗口的形式在使用者界面展示宣传性广告或直接显示某网站的内容。


下图是一种典型的 HTTP 劫持的流程。当客户端给服务端发送 HTTP 请求,图中发送请求为“梁安娜的电话号码是?”,恶意节点监听到该请求后将其放行给服务端,服务端返回正常 HTML 响应,关键返回内容本应该是“+86 130****1234”,恶意节点监听到该响应,并将关键返回内容篡改为泰国区电话“+66 6160 *88”,导致用户端程序展示出错误信息,这就是 HTTP 劫持的全流程。



 HTTP 劫持流程


例如,在某网站阅读某网络小说时,由于该网站使用了不安全的 HTTP 协议,攻击者可以篡改 HTTP 相应的内容,使网页上出现与原响应内容无关的广告,引导用户点击,可能将跳转进入网络诈骗或其他非法内容的页面。



 原网页



 HTTP 劫持后网页


HTTPS 工作原理


HTTPS 协议的提出正是为了解决 HTTP 带来的安全问题。HTTPS 协议(HyperText Transfer Protocol Secure,超文本传输安全协议),是一种通过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包。HTTPS 的开发主要是提供对网站服务器的身份认证,保护交换资料的隐私性与完整性。


TLS 握手是 HTTPS 工作原理的安全基础部分。TLS 传统的 RSA 握手流程如下所示:



 TLS 握手流程


TLS 握手流程主要可以分为以下四个部分:


第一次握手:客户端发送 Client Hello 消息。该消息包含:客户端支持的 SSL/TLS 协议版本(如 TLS v1.2 );用于后续生成会话密钥的客户端随机数 random_1;客户端支持的密码套件列表。


第二次握手:服务端收到 Client Hello 消息后,保存随机数 random_1,生成随机数 random_2,并发送以下消息。

  • 发送 Server Hello 消息。该消息包含:服务端确认的 SSL/TLS 协议版本(如果双方支持的版本不同,则关闭加密通信);用于后续生成会话密钥的服务端随机数 random_2;服务端确认使用的密码套件
  • 发送“Server Certificate”消息。该消息包含:服务端的 SSL 证书。SSL 证书又包含服务端的公钥、身份等信息。
  • 发送“Server Hello Done”消息。该消息表明 ServerHello 及其相关消息的结束。发送这个消息之后,服务端将会等待客户端发过来的响应。

第三次握手:客户端收到服务端证书后,首先验证服务端证书的正确性,校验服务端身份。若证书合法,客户端生成预主密钥,之后客户端根据(random_1, random_2, 预主密钥)生成会话密钥,并发送以下消息。

  • 发送“Client Key Exchange”消息,该消息为客户端生成的预主密钥,预主密钥会被服务端证书中的公钥加密后发送。
  • 发送“Change Cipher Spec”消息,表示之后数据都将用会话密钥进行加密。
  • 发送“Encrypted Handshake Message”消息,表示客户端的握手阶段已经结束。客户端会生成所有握手报文数据的摘要,并用会话密钥加密后发送给服务端,供服务端校验。

第四次握手:服务端收到客户端的消息后,利用自己的服务端证书私钥解密出预主密钥,并根据(random_1, random_2, 预主密钥)计算出会话密钥,之后发送以下消息。

  • 发送“Change Cipher Spec”消息,表示之后数据都将用会话密钥进行加密。
  • 发送“Encrypted Handshake Message”,表示服务端的握手阶段已经结束,同时服务端会生成所有握手报文数据的摘要,并用会话密钥加密后发送给客户端,供客户端校验。

根据 TLS 握手流程,可以看出它是如何解决 HTTP 协议缺陷,以及避免中间人攻击的:


1.规避窃听风险,攻击者无法获知通信内容


在客户端进行真正的 HTTPS 请求前,客户端与服务端都已经拥有了本次会话中用于加密的对称密钥,后续双方 HTTPS 会话中的内容均用该对称密钥加密,攻击者在无法获得该对称密钥的情况下,无法解密获得会话中内容的明文。即使攻击者获得了 TLS 握手中双方发送的所有明文信息,也无法从这些信息中恢复对称密钥,这是由大数质因子分解难题和有限域上的离散对数难题保证的。


2.规避篡改风险,攻击者无法篡改通信内容


在数据通信阶段,双端消息发送时会对原始消息做一次哈希,得到该消息的摘要后,与加密内容一起发送。对端接受到消息后,使用协商出来的对称加密密钥解密数据包,得到原始消息;接着也做一次相同的哈希算法得到摘要,对比发送过来的消息摘要和计算出的消息摘要是否一致,可以判断通信数据是否被篡改。


3.规避冒充风险,攻击者无法冒充身份参与通信


在 TLS 握手流程中的第二步“Server Hello”中,服务端将自己的服务端证书交付给客户端。客户端拿到 SSL 证书后,会对服务端证书进行一系列校验。以浏览器为例,校验服务端证书的过程为:

  • 验证证书绑定域名与当前域名是否匹配。
  • 验证证书是否过期,是否被吊销。
  • 查找操作系统中已内置的受信任的证书发布机构 CA(操作系统会内置有限数量的可信 CA),与服务端证书中的颁发者 CA 比对,验证证书是否为合法机构颁发。如果服务端证书不是授信 CA 颁发的证书,则浏览器会提示服务端证书不可信。
  • 验证服务端证书的完整性,客户端在授信 CA 列表中找到服务端证书的上级证书,后使用授信上级证书的公钥验证服务端证书中的签名哈希值。
  • 在确认服务端证书是由国际授信 CA 签发,且完整性未被破坏后,客户端信任服务端证书,也就确认了服务端的正确身份。

SSL 证书


正如上一节介绍,SSL 证书在 HTTPS 协议中扮演着至关重要的作用,即验证服务端身份,协助对称密钥协商。只有配置了 SSL 证书的网站才可以开启 HTTPS 协议。在浏览器中,使用 HTTP 的网站会被默认标记为“不安全”,而开启 HTTPS 的网站会显示表示安全的锁图标。



 使用 HTTP 协议的网站



 使用 HTTPS 协议的网站


从保护范围、验证强度和适用类型出发, SSL 证书会被分成不同的类型。只有了解类型之间的区别,才能根据实际情况选择更适合的证书类型,保障通信传输安全。


从保护范围分,SSL 证书可以分为单域名证书、通配符证书、多域名证书。

  • 单域名证书:单域名证书只保护一个域名,这些域名形如 http://www.test.com 等。
  • 通配符证书:通配符证书可以保护基本域和无限的子域。通配符 SSL 证书的公用名中带有星号 ,其中,星号表示具有相同基本域的任何有效子域。例如,。test.com 的通配符证书可用于保护 a.test.com、 b.test.com……
  • 多域名证书:多域证书可用于保护多个域或子域。包括完全唯一的域和具有不同顶级域的子域(本地/内部域除外)的组合。

从验证强度和适用类型进一步区分,SSL 证书可以分为 DV、OV、EV 证书。

  • DV(Domain Validated):域名验证型。在颁发该类型证书时,CA 机构仅验证申请者对域名的所有权。CA 机构会通过检查 WHOIS、DNS 的特定记录来确认资格。一般来说,DV 证书适用于博客、个人网站等不需要任何私密信息的网站。
  • OV(Organization Validated):组织验证型。OV 证书的颁发除了要验证域名所有权外,CA 还会额外验证申请企业的详细信息(名称、类型、地址)等。一般来说,OV 证书适用于中级商业组织。
  • EV(Extended Validation):扩展验证型。EV 证书的颁发除了 CA 对 DV 和 OV 证书所采取的所有身份验证步骤之外,还需要审查商业组织是否在真实运营、其实际地址,并致电以验证申请者的就业情况。一般来说,EV 证书适用于顶级商业组织。

结尾


随着互联网应用的普及,网络诈骗的方式也越发花样百出,让人防不胜防。


除了文内提到的网页环境,在软件应用、邮件、文档、物联网等领域同样存在恶意软件、钓鱼邮件、文档篡改、身份认证的问题。幸运的是,作为 PKI 体系下的优秀产品,证书体系同样在这些领域发挥着重要作用,软件签名证书、邮件签名证书、文档签名证书、私有证书等保护着各自领域的信息安全。


总有不法分子企图通过漏洞牟利,而证书体系在保护数据机密性、完整性、可用性以及身份验证场景上有着无可取代的地位,牢牢守护着用户信息,保障通信安全。


推荐活动


火山引擎域名与网站特惠活动来啦,欢迎访问火山引擎官网抢购!


5 折抢购 SSL 证书、1 元注册/转入域名、1 元升级 DNS 专业版、HTTPDNS 资源包 1 折起火热进行中……


此外,火山引擎已新推出:

  • 私有 CA(Private CA/PCA),通过私有证书灵活标识和保护企业内部资源和数据
  • 商标服务,专业、高效的商标注册管理服务平台
  • 私网解析 PrivateZone,灵活构建 VPC 内的私网域名系统
  • 公共解析PublicDNS,快速安全的递归DNS,永久免费
  • 域名委托购买服务,0元下单即可尝试获取心仪域名

关于火山引擎边缘云:
火山引擎边缘云,以云原生技术为基础底座,融合异构算力和边缘网络,构建在大规模边缘基础设施之上的云计算服务,形成以边缘位置的计算、网络、存储、安全、智能为核心能力的新一代分布式云计算解决方案。


作者:火山引擎边缘云
链接:https://juejin.cn/post/7273685263841263672
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一条SQL差点引发离职

排除一切不可能的,剩下的即使再不可能,那也是真相” 背景        最近组里的小伙伴在开发一个更新功能时踩了MySQL的一个类型转换的坑,差点造成线上故障。 本来是一个很简单的逻辑,就...
继续阅读 »

排除一切不可能的,剩下的即使再不可能,那也是真相”



背景


       最近组里的小伙伴在开发一个更新功能时踩了MySQL的一个类型转换的坑,差点造成线上故障。

本来是一个很简单的逻辑,就是根据唯一的id去更新对应的MySQL数据,代码简化后如下:


var updates []*model.Goods
for id, newGoods := range update {
 if err := model.GetDB().Model(&model.Goods{}).Where("id = ?", id).Updates(map[string]interface{}{
  "selling_price":  newGoods.SellingPrice,
  "sell_type":      newGoods.SellType,
  "status":         newGoods.Status,
  "category_id":    newGoods.CategoryID,
 }).Error; err != nil {
  return nil, err
 }
}

很明显,updates[]model.Goods\color{red}{updates []*model.Goods}本来应该是想声明为 map[string]model.Goods\color{red}{map[string]*model.Goods}类型的,然后key是唯一id。这样下面的更新逻辑才是对的,否则拿到的id其实是数组的下标。

但是code review由于跟着一堆代码一起评审了,并且这段更新很简单,同时测试的时候也测试过了(能测试通过也是“机缘巧合”),所以没有发现这段异常。

发到线上后,进行了灰度集群的测试,这个时候发现只要调用了这个接口,灰度集群的数据全部都变成了一样,回滚后正常。


分析


       回滚后在本地进行复现,由于本地环境是开启了SQL打印的,于是看到了这么一条SQL:很明显是拿数组的下标去比较了


update db_name set selling_price = xx,sell_type = xx where id = 0;

       由于我们的id是全部是通过uuid生成的,所以下意识的认为这条sql应该啥也不会更新才对,但是本地的确只执行了这条sql,没有别的sql,并且db中的数据全部都被修改了。

这个时候想起福尔摩斯的名言“排除一切不可能的,剩下的即使再不可能,那也是真相”\color{blue}{“排除一切不可能的,剩下的即使再不可能,那也是真相”} ,于是抱着试一试的心态直接拿这条sql去db控制台执行了一遍,发现果然所有的数据又都被修改了。

也就是 whereid=0\color{red}{where id = 0}  这个条件对于所有的记录都是恒为true,就会导致所有记录都被更新。在这个时候,想起曾经看到过MySQL对于不同类型的比较会有 【隐式转换】\color{red}{【隐式转换】},难道是这个原因导致的?


隐式转换规则


在MySQL官网找到了不同类型比较的规则:



最后一段的意思是:对于其他情况,将按照浮点(双精度)数进行比较。例如,字符串和数字的比较就按照浮点数规则进行比较。

也就是id会首先被转换成浮点数,然后再跟0进行比较。


MySQL字符转为浮点数时会按照如下规则进行:


1.如果字符串的第一个字符就是非数字的字符,那么转换结果就是0;

2.如果字符串以数字开头:

(1)如果字符串都是数字,转换结果就是整个字符串对应的数字;

(2)如果字符串中存在非数字,转换结果就是开头的那些数字对应的值;

举例说明:

"test" -> 0

"1test" -> 1

"12test12" -> 12

由于我们生成的uuid没有数字开头的字符串,于是都会转变成0。那么这条SQL就变成了:


update db_name set selling_price = xx,sell_type = xx where 0 = 0;

就恒为true了。

修复就很简单了,把取id的逻辑改成正确的就行。


为什么测试环境没有发现


       前面有提到这段代码在测试环境是测试通过了的,这是因为开发和测试同学的环境里都只有一条记录,每次更新他发现都能正常更新就认为是正常的了。同时由于逻辑太简单了,所以都没有重视这块的回归测试。

幸好在灰度集群就发现了这个问题,及时进行了回滚,如果发到了线上影响了用户数据,可能就一年白干了。


最后


代码无小事,事事需谨慎啊。一般致命问题往往是一行小小的修改导致的。


作者:云舒编程
来源:juejin.cn/post/7275550679790960640
收起阅读 »

请给系统加个【消息中心】功能,因为真的很简单

个人项目:社交支付项目(小老板) 作者:三哥,j3code.cn 项目文档:http://www.yuque.com/g/j3code/dv… 预览地址(未开发完):admire.j3code.cn/small-boss 内网穿透部署,第一次访问比较慢 ...
继续阅读 »

个人项目:社交支付项目(小老板)


作者:三哥,j3code.cn


项目文档:http://www.yuque.com/g/j3code/dv…


预览地址(未开发完):admire.j3code.cn/small-boss



  • 内网穿透部署,第一次访问比较慢



我相信,打开一个带有社交类型的网站,你或多或少都可以看到如下的界面:


1)消息提示


Snipaste_2023-08-27_13-41-36.jpg


2)消息列表


这样


Snipaste_2023-08-27_13-42-25.jpg


这样


Snipaste_2023-08-27_16-41-30.jpg


那,这就是我们今天要聊的【消息中心】。


1、设计


老规矩先来搞清楚消息中心的需求,再来代码实现。


我们知道在社交类项目中,有很多评论、点赞等数据的产生,而如果这些数据的产生不能让用户感知到,那你们想想这会带来什么影响?



用户A:太鸡肋了,发布的内容被人评论点赞了,我居然看不到,下次不用了...


用户B:还好没用这个系统...



所以,看到这些结果我们是不是能够意识到一个健全的社交功能,是不是少不了这种通知用户的机制啊!而这种机制我就把他定义为【消息中心】功能。


再来拆分一下这四个字:消息中心



  1. 消息

  2. 中心


消息:这个可以是由我们自己定义,如:把帖子被用户评论当作一条消息,把评论被用户点赞也可以当作一条消息,甚至系统发布的通知也是一条消息。


中心:这个就是字面意思,将上面所提到的所有消息,归拢到一个地方进行展示。


上面我们也提到消息基本就是这两种:



  • 用户对用户:用户消息

  • 平台对用户:系统消息


针对用户消息,就类似这样,用户 A 给用户 B 的一条评论进行了点赞,那这个点赞动作就会产生一条消息,并且通知到用户 B 的一个存储消息的地方,这里通常就指用户的收件箱。这个收件箱就是专门用来存储用户发给用户的消息,而这个点对点的模式是不是就是推送模式啊!(A 推送消息给 B)


接着针对系统消息,就类似这样,平台管理人员发布了一条通知,告诉大家平台有啥 XXX 活动。那这个活动通知肯定是要让平台的所有用户都知道把,所以这个通知就要存在一个发件箱中。这个发件箱就是专门存储平台的通知,所有用户都来这个发件箱中读取消息就行,而这个一对多的模式是不是就是拉取模式啊!(所有用户都来拉取平台消息)


这样一来,我们根据不同的消息场景就抽出了一个基本的消息推拉模型,模型图如下:



Snipaste_2023-08-27_14-27-25.jpg



Snipaste_2023-08-27_14-59-50.jpg


针对这两种模式,不知道大家有没有看出区别,好像乍一看没啥区别,都是发消息,读消息,对吧!


没错,确实都是一个发,一个读,但是两者的读写频率确实有着巨大的差异。先来看推模型,一个普通用户发表了一条帖子,然后获得了寥寥无几的评论和赞,这好似也没啥特别之处,对吧!那如果这个普通用户发表的帖子成为了热门帖子呢,也即该贴子获得了上万的评论和赞。那,你们想想是不是发消息的频率非常高,而该普通用户肯定是不可能一下子读取这么多消息的,所以是不是一个写多读少的场景。再来看看拉模型,如果你的平台用户人数寥寥无几,那倒没啥特别之处,但如果用户人数几万甚至几十万。那,每个用户都过来拉取系统消息是不是就是一个读频率非常高,而发消息频率非常低(系统消息肯定不会发的很快),所以这是不是一个读多写少的场景。


1.1 推:写多读少


针对这个模式,我们肯定是要将写这个动作交给性能更高的中间件来处理,而不是 MySQL,所以此时我们的 RocketMQ 就出来了。


当系统中产生了评论、点赞类的高频消息,那就无脑的丢给 MQ 吧,让其在消息中间件中呆会,等待消费者慢慢的将消息进行消费并发到各个用户的收件箱中,就类似下面这张图的流程:


Snipaste_2023-08-27_15-45-46.jpg


2.2 拉:读多写少


那对于这个模式,所实话,我觉得不用引入啥就可以实现,因为对于读多的话无非就是一个查,MySQL 肯定是能搞定的,即使你的用户几万、几十万都是 ok 的。


但咱们是不是可以这样想一下,一个系统的官方通知肯定是不多的,或者说几天或者几个星期一次,且一旦发送就不可更改。那是不是可以考虑缓存,让用户读取官方通知的时候走缓存,如果缓存没有再走 MySQL 这样应该是可以提高查询效率,提高响应速度。


具体流程如下图:


Snipaste_2023-08-27_15-57-21.jpg


2.3 表结构设计


基本的业务流程已经分析的差不多了,现在可以把表字段抽一下了,先根据上面分析的,看看我们需要那些表:



  1. 用户收件箱表

  2. 系统发件箱表


看似好像就这两张表,但是应该还有第三张表:



  1. 用户读取系统消息记录表



我们看到页面是不是每次有一条新的消息都会有一个小标点记录新消息数量,而第三张表就是为了这个作用而设计的。


具体原理如下:



  1. 首先运营人员发布的消息都是存储在第二张表中,这肯定是没错的

  2. 那用户每次过来拉取系统消息时,将最近拉取的一条消息写入到第三种表中

  3. 这样等用户下次再来拉取的时候,就可以根据第三张表的读取记录,来确定他有几条系统消息未查看了


可能有人会发出疑问:那用户的收件箱为啥不出一个用户读取记录表呢!


这个很简单,因为收件箱中的数据已经表示这个用户需要都这些个消息了,只是不知道那些是已读的那些是未读的,我们只需要再收件箱表中加一个字段,这个字段的作用就是记录最新一次读取的消息 ID 就行,等下次要读消息时,找到上传读取读取消息的记录ID,往后读新消息即可。



好,现在来看看具体的表字段:


1)用户收件箱表(sb_user_inbox)



  • id

  • 消息数据唯一 id:MQ唯一消息凭证

  • 消息类型:评论消息或者点赞消息

  • 帖子id:业务id

  • 业务数据id:业务id

  • 内容:消息内容

  • 业务数据类型:业务数据类型(商品评论、帖子、帖子一级评论、帖子二级评论)

  • 发起方的用户ID:用户 A 对用户 B 进行点赞,那这就是用户 A 的ID

  • 接收方的用户ID:用户 B 的 ID

  • 用户最新读取位置ID:用户最近一次读取记录的 ID


SQL


CREATE TABLE `sb_user_inbox` (
`id` bigint(20) NOT NULL,
`uuid` varchar(128) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '消息数据唯一id',
`message_type` tinyint(1) NOT NULL COMMENT '消息类型',
`post_id` bigint(20) DEFAULT NULL COMMENT '帖子id',
`item_id` bigint(20) NOT NULL COMMENT '业务数据id',
`content` varchar(1000) COLLATE utf8mb4_german2_ci DEFAULT NULL COMMENT '内容',
`service_message_type` tinyint(1) NOT NULL COMMENT '业务数据类型',
`from_user_id` bigint(20) NOT NULL COMMENT '发起方的用户ID',
`to_user_id` bigint(20) NOT NULL COMMENT '接收方的用户ID',
`read_position_id` bigint(20) DEFAULT '0' COMMENT '用户最新读取位置ID',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `un01` (`uuid`),
UNIQUE KEY `un02` (`item_id`,`service_message_type`,`to_user_id`),
KEY `key` (`to_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

可以看到,我加了很多业务相关的字段,这个主要是为了方便查询数据和展示数据。


2)系统发件箱表(sb_sys_outbox)



  • id

  • 内容


SQL


CREATE TABLE `sb_sys_outbox` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`content` varchar(2000) COLLATE utf8mb4_german2_ci NOT NULL COMMENT '内容',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

这个表就非常简单了,没啥业务字段冗余。


3)用户读取系统消息记录表(sb_user_read_sys_outbox)



  • id

  • 系统收件箱数据读取id

  • 读取的用户id


SQL


CREATE TABLE `sb_user_read_sys_outbox` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`sys_outbox_id` bigint(20) NOT NULL COMMENT '系统收件箱数据读取id',
`user_id` bigint(20) NOT NULL COMMENT '读取的用户id',
PRIMARY KEY (`id`),
UNIQUE KEY `un` (`user_id`),
KEY `key` (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_german2_ci

ok,这是消息中心所有分析阶段了,下面就开始实操。


2、实现


先来引入引入一下 RocketMQ 的依赖


<!--rocketmq-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>

RocketMQ 的双主双从同步刷新集群搭建教程:blog.csdn.net/qq_40399646…


MQ 配置:


Snipaste_2023-08-27_16-26-09.jpg


2.1 生产者


先来实现生产者如何发送消息。


1)消息体对象:LikeAndCommentMessageDTO


位置:cn.j3code.config.dto.mq


@Data
public class LikeAndCommentMessageDTO {

/**
* 该消息的唯一id
* 业务方可以不设置,如果为空,代码会自动填充
*/

private String uuid;

/**
* 消息类型
*/

private UserCenterMessageTypeEnum messageType;

/**
* 冗余一个帖子id进来
*/

private Long postId;

/**
* 业务数据id
*/

private Long itemId;

/**
* 如果是评论消息,这个内容就是评论的内容
*/

private String content;

/**
* 业务数据类型
*/

private UserCenterServiceMessageTypeEnum serviceMessageType;

/**
* 发起方的用户ID
*/

private Long fromUserId;

/**
* 接收方的用户ID
*/

private Long toUserId;


/*
例子:
用户 A 发表了一个帖子,B 对这个帖子进行了点赞,那这个实体如下:
messageType = UserCenterMessageTypeEnum.LIKE
itemId = 帖子ID(对评论进行点赞,就是评论id,对评论进行回复,就是刚刚评论的id)
serviceMessageType = UserCenterServiceMessageTypeEnum.POST(这个就是说明 itemId 的 ID 是归于那个业务的,方便后续查询业务数据)
fromUserId = 用户B的ID
toUserId = 用户 A 的ID
*/

}

2)发送消息代码


位置:cn.j3code.community.mq.producer


@Slf4j
@Component
@AllArgsConstructor
public class LikeAndCommentMessageProducer {

private final RocketMQTemplate rocketMQTemplate;

/**
* 单个消息发送
*
* @param dto
*/

public void send(LikeAndCommentMessageDTO dto) {
if (Objects.isNull(dto.getUuid())) {
dto.setUuid(IdUtil.simpleUUID());
}
checkMessageDTO(dto);
Message<LikeAndCommentMessageDTO> message = MessageBuilder
.withPayload(dto)
.build();
rocketMQTemplate.send(RocketMQConstants.USER_MESSAGE_CENTER_TOPIC, message);
}

/**
* 批量消息发送
*
* @param dtos
*/

public void send(List<LikeAndCommentMessageDTO> dtos) {
/**
* 将 dtos 集合分割成 1MB 大小的集合
* MQ 批量推送的消息大小最大 1MB 左右
*/

ListSizeSplitUtil.split(1 * 1024 * 1024L, dtos).forEach(items -> {
List<Message<LikeAndCommentMessageDTO>> messageList = new ArrayList<>(items.size());
items.forEach(dto -> {
if (Objects.isNull(dto.getUuid())) {
dto.setUuid(IdUtil.simpleUUID());
}
checkMessageDTO(dto);
Message<LikeAndCommentMessageDTO> message = MessageBuilder
.withPayload(dto)
.build();
messageList.add(message);
});
rocketMQTemplate.syncSend(RocketMQConstants.USER_MESSAGE_CENTER_TOPIC, messageList);
});
}

private void checkMessageDTO(LikeAndCommentMessageDTO dto) {
AssertUtil.isTrue(Objects.isNull(dto.getMessageType()), "消息类型不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getItemId()), "业务数据ID不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getServiceMessageType()), "业务数据类型不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getFromUserId()), "发起方用户ID不为空!");
AssertUtil.isTrue(Objects.isNull(dto.getToUserId()), "接收方用户ID不为空!");
}


/**
* 发送点赞消息
*
* @param messageType 消息类型
* @param serviceMessageType 业务类型
* @param itemToUserIdMap 业务ID对应的用户id
* @param saveLikeList 点赞数据
*/

public void sendLikeMQMessage(
UserCenterMessageTypeEnum messageType,
UserCenterServiceMessageTypeEnum serviceMessageType,
Map<Long, Long> itemToUserIdMap, List<Like> saveLikeList)
{
if (CollectionUtils.isEmpty(saveLikeList)) {
return;
}
List<LikeAndCommentMessageDTO> dtos = new ArrayList<>();
for (Like like : saveLikeList) {
LikeAndCommentMessageDTO messageDTO = new LikeAndCommentMessageDTO();
messageDTO.setItemId(like.getItemId());
messageDTO.setMessageType(messageType);
messageDTO.setServiceMessageType(serviceMessageType);
messageDTO.setFromUserId(like.getUserId());
messageDTO.setToUserId(itemToUserIdMap.get(like.getItemId()));
dtos.add(messageDTO);
}
try {
send(dtos);
} catch (Exception e) {
//错误处理
log.error("发送MQ消息失败!", e);
}
}
}

注意:这里我用了 MQ 批量发送消息的一个功能,但是他有一个限制就是每次只能发送 1MB 大小的数据。所以我需要做一个功能工具类将业务方丢过来的批量数据进行分割。


工具类:ListSizeSplitUtil


位置:cn.j3code.config.util


public class ListSizeSplitUtil {

private static Long maxByteSize;

/**
* 根据传进来的 byte 大小限制,将 list 分割成对应大小的 list 集合数据
*
* @param byteSize 每个 list 数据最大大小
* @param list 待分割集合
* @param <T>
* @return
*/

public static <T> List<List<T>> split(Long byteSize, List<T> list) {
if (Objects.isNull(list) || list.size() == 0) {
return new ArrayList<>();
}

if (byteSize <= 100) {
throw new RuntimeException("参数 byteSize 值不小于 100 bytes!");
}
ListSizeSplitUtil.maxByteSize = byteSize;


if (isSurpass(List.of(list.get(0)))) {
throw new RuntimeException("List 中,单个对象都大于 byteSize 的值,分割失败");
}

List<List<T>> result = new ArrayList<>();

List<T> itemList = new ArrayList<>();
for (int i = 0; i < list.size(); i++) {
itemList.add(list.get(i));

if (isSurpass(itemList)) {
i = i - 1;
itemList.remove(itemList.size() - 1);
result.add(new ArrayList<>(itemList));
itemList = new ArrayList<>();
}
}
result.add(new ArrayList<>(itemList));
return result;
}


private static <T> Boolean isSurpass(List<T> obj) {
// 字节(byte)
long objSize = RamUsageEstimator.sizeOfAll(obj.toArray());
return objSize >= ListSizeSplitUtil.maxByteSize;
}
}

至此呢,生产者的逻辑就算是完成了,每次有消息的时候就调用这个方法即可。


2.2 消费者


位置:cn.j3code.user.mq.consumer


@Slf4j
@Component
@AllArgsConstructor
@RocketMQMessageListener(topic = RocketMQConstants.USER_MESSAGE_CENTER_TOPIC,
consumerGroup = RocketMQConstants.GROUP,
messageModel = MessageModel.CLUSTERING,
consumeMode = ConsumeMode.CONCURRENTLY
)

public class LikeAndCommentMessageConsumer implements RocketMQListener<LikeAndCommentMessageDTO> {

private final UserInboxService userInboxService;

@Override
public void onMessage(LikeAndCommentMessageDTO message) {
userInboxService.saveMessage(message);
}
}

saveMessage 方法的逻辑就是将消息保存到 MySQL 中,至此消息的产生和存储就算完成了,下面来看看用户如何查看吧!


2.3 用户消息查看


对于用户查看普通的消息就是访问一下 MySQL,并且更新一下最新读取的字段值即可,我贴一下关键代码就行了,代码如下:


public IPage<UserMessageVO> page(UserMessagePageRequest request) {
// 获取消息
IPage<UserMessageVO> page = getBaseMapper().page(new Page<UserMessageVO>(request.getCurrent(), request.getSize()), request);

if (CollectionUtils.isEmpty(page.getRecords())) {
return page;
}
// 记录一下消息读取位置,默认进来就把全部消息读完了,类似掘金
if (request.getCurrent() == 1) {
if (Objects.isNull(page.getRecords().get(0).getReadPositionId()) ||
page.getRecords().get(0).getReadPositionId() == 0) {
UserInbox userInbox = new UserInbox();
userInbox.setId(page.getRecords().get(0).getId());
userInbox.setReadPositionId(userInbox.getId());
updateById(userInbox);
}
}
return page;
}

2.4 系统消息查看


对于系统消息的查看也是,只贴出关键代码,查询和更新读取记录逻辑,代码如下:


@Override
public IPage<SysOutboxVO> lookSysPage(SysOutboxPageRequest request) {
Page<SysOutbox> page = lambdaQuery()
.orderByDesc(SysOutbox::getId)
.page(new Page<>(request.getCurrent(), request.getSize()));
IPage<SysOutboxVO> outboxVOIPage = page.convert(userInboxConverter::converter);
if (CollectionUtils.isEmpty(outboxVOIPage.getRecords())) {
return outboxVOIPage;
}
// 记录一下消息读取位置,默认进来就把全部消息读完了,类似掘金
if (request.getCurrent() == 1) {
userReadSysOutboxService.updateReadLog(page.getRecords().get(0).getId(), SecurityUtil.getUserId());
}
return outboxVOIPage;
}

这里,可能有人会发现,没有按照上面分析的那用从缓存中读,是的。这里的实现我没有用到 Redis,这里我偷了一下懒,如果有拿到我代码的同学可以试着优化一下这个逻辑。


作者:J3code
来源:juejin.cn/post/7274922643453853735
收起阅读 »

从《孤注一掷》出发,聊聊 SSL 证书的重要性

你去看《孤注一掷》了吗?相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治,以及电信诈骗中系统性极强的诈骗技巧,引发了大量讨论。 图片来源于电影《孤注一掷》 这部电影除了让人后背发凉外,也...
继续阅读 »

你去看《孤注一掷》了吗?相信最近大家的朋友圈和抖音都被爆火电影《孤注一掷》成功刷屏。取材于上万真实案例的《孤注一掷》揭露了缅甸诈骗园区残暴的统治,以及电信诈骗中系统性极强的诈骗技巧,引发了大量讨论。
image001.png
图片来源于电影《孤注一掷》


这部电影除了让人后背发凉外,也不禁让人回忆起了曾经上网冲浪遇到的种种现象:看小说时性感荷官总在网页右下角在线发牌;看电影时网页左下角常常蹦出“在线老虎机”……这些让人烦不胜烦的广告弹窗之所以出现,要么是建站人员利欲熏心投放了非法广告,要么就是因为网站使用了不安全的 HTTP 协议而遭到了攻击,正常的网页内容被恶意篡改。


网站是电信诈骗、网络赌博等非法内容出现的重灾区,建站者和使用者都应该提高安全意识,特别是对建站者来说,保护通信安全才能更好的承担起建站责任。本文将从 HTTP 讲起,介绍 HTTPS 保护通信安全的原理,以及作为网络通信安全基石的 SSL 证书的重要性。


HTTP 协议


HTTP(Hyper Text Transfer Protocol)协议是超文本传输协议。它是从 WEB 服务器传输超文本标记语言(HTML)到本地浏览器的传送协议。HTTP 基于 TCP/IP 通信协议来传递数据,通信双方在 TCP 握手后即可开始互相传输 HTTP 数据包。具体过程如下图所示:
image003.jpg
HTTP 建立流程


HTTP 协议中,请求和响应均以明文传输。如下图所示,在访问一个使用 HTTP 协议的网站时,通过抓包软件可以看到网站 HTTP 响应包中的完整 HTML 内容。


image005.png


虽然 HTTP 明文传输的机制在性能上带来了优势,但同时也引入了安全问题:



  • 缺少数据机密性保护。HTTP 数据包内容以明文传输,攻击者可以轻松窃取会话内容。

  • 缺少数据完整性校验。通信内容以明文传输,数据内容可被攻击者轻易篡改,且双方缺少校验手段。

  • 缺少身份验证环节。攻击者可冒充通信对象,拦截真实的 HTTP 会话。


HTTP 劫持


作为划时代的互联网通信标准之一,HTTP 协议的出现为互联网的普及做出了不可磨灭的贡献。但正如上节谈到, HTTP 协议因为缺少加密、身份验证的过程导致很可能被恶意攻击,针对 HTTP 协议最常见的攻击就是 HTTP 劫持。


HTTP 劫持是一种典型的中间人攻击。HTTP 劫持是在使用者与其目的网络服务所建立的数据通道中,监视特定数据信息,当满足设定的条件时,就会在正常的数据流中插入精心设计的网络数据报文,目的是让用户端程序解析“错误”的数据,并以弹出新窗口的形式在使用者界面展示宣传性广告或直接显示某网站的内容。


下图是一种典型的 HTTP 劫持的流程。当客户端给服务端发送 HTTP 请求,图中发送请求为“梁安娜的电话号码是?”,恶意节点监听到该请求后将其放行给服务端,服务端返回正常 HTML 响应,关键返回内容本应该是“+86 130****1234”,恶意节点监听到该响应,并将关键返回内容篡改为泰国区电话“+66 6160 *88”,导致用户端程序展示出错误信息,这就是 HTTP 劫持的全流程。


image007.jpg
HTTP 劫持流程


例如,在某网站阅读某网络小说时,由于该网站使用了不安全的 HTTP 协议,攻击者可以篡改 HTTP 相应的内容,使网页上出现与原响应内容无关的广告,引导用户点击,可能将跳转进入网络诈骗或其他非法内容的页面。


image009.png
原网页


image011.png
HTTP 劫持后网页


HTTPS 工作原理


HTTPS 协议的提出正是为了解决 HTTP 带来的安全问题。HTTPS 协议(HyperText Transfer Protocol Secure,超文本传输安全协议),是一种通过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包。HTTPS 的开发主要是提供对网站服务器的身份认证,保护交换资料的隐私性与完整性。


TLS 握手是 HTTPS 工作原理的安全基础部分。TLS 传统的 RSA 握手流程如下所示:


image013.jpg
TLS 握手流程


TLS 握手流程主要可以分为以下四个部分:


第一次握手:客户端发送 Client Hello 消息。该消息包含:客户端支持的 SSL/TLS 协议版本(如 TLS v1.2 );用于后续生成会话密钥的客户端随机数 random_1;客户端支持的密码套件列表。


第二次握手:服务端收到 Client Hello 消息后,保存随机数 random_1,生成随机数 random_2,并发送以下消息。



  • 发送 Server Hello 消息。该消息包含:服务端确认的 SSL/TLS 协议版本(如果双方支持的版本不同,则关闭加密通信);用于后续生成会话密钥的服务端随机数 random_2;服务端确认使用的密码套件

  • 发送“Server Certificate”消息。该消息包含:服务端的 SSL 证书。SSL 证书又包含服务端的公钥、身份等信息。

  • 发送“Server Hello Done”消息。该消息表明 ServerHello 及其相关消息的结束。发送这个消息之后,服务端将会等待客户端发过来的响应。


第三次握手:客户端收到服务端证书后,首先验证服务端证书的正确性,校验服务端身份。若证书合法,客户端生成预主密钥,之后客户端根据(random_1, random_2, 预主密钥)生成会话密钥,并发送以下消息。



  • 发送“Client Key Exchange”消息,该消息为客户端生成的预主密钥,预主密钥会被服务端证书中的公钥加密后发送。

  • 发送“Change Cipher Spec”消息,表示之后数据都将用会话密钥进行加密。

  • 发送“Encrypted Handshake Message”消息,表示客户端的握手阶段已经结束。客户端会生成所有握手报文数据的摘要,并用会话密钥加密后发送给服务端,供服务端校验。


第四次握手:服务端收到客户端的消息后,利用自己的服务端证书私钥解密出预主密钥,并根据(random_1, random_2, 预主密钥)计算出会话密钥,之后发送以下消息。



  • 发送“Change Cipher Spec”消息,表示之后数据都将用会话密钥进行加密。

  • 发送“Encrypted Handshake Message”,表示服务端的握手阶段已经结束,同时服务端会生成所有握手报文数据的摘要,并用会话密钥加密后发送给客户端,供客户端校验。


根据 TLS 握手流程,可以看出它是如何解决 HTTP 协议缺陷,以及避免中间人攻击的:


1.规避窃听风险,攻击者无法获知通信内容


在客户端进行真正的 HTTPS 请求前,客户端与服务端都已经拥有了本次会话中用于加密的对称密钥,后续双方 HTTPS 会话中的内容均用该对称密钥加密,攻击者在无法获得该对称密钥的情况下,无法解密获得会话中内容的明文。即使攻击者获得了 TLS 握手中双方发送的所有明文信息,也无法从这些信息中恢复对称密钥,这是由大数质因子分解难题和有限域上的离散对数难题保证的。


2.规避篡改风险,攻击者无法篡改通信内容


在数据通信阶段,双端消息发送时会对原始消息做一次哈希,得到该消息的摘要后,与加密内容一起发送。对端接受到消息后,使用协商出来的对称加密密钥解密数据包,得到原始消息;接着也做一次相同的哈希算法得到摘要,对比发送过来的消息摘要和计算出的消息摘要是否一致,可以判断通信数据是否被篡改。


3.规避冒充风险,攻击者无法冒充身份参与通信


在 TLS 握手流程中的第二步“Server Hello”中,服务端将自己的服务端证书交付给客户端。客户端拿到 SSL 证书后,会对服务端证书进行一系列校验。以浏览器为例,校验服务端证书的过程为:



  • 验证证书绑定域名与当前域名是否匹配。

  • 验证证书是否过期,是否被吊销。

  • 查找操作系统中已内置的受信任的证书发布机构 CA(操作系统会内置有限数量的可信 CA),与服务端证书中的颁发者 CA 比对,验证证书是否为合法机构颁发。如果服务端证书不是授信 CA 颁发的证书,则浏览器会提示服务端证书不可信。

  • 验证服务端证书的完整性,客户端在授信 CA 列表中找到服务端证书的上级证书,后使用授信上级证书的公钥验证服务端证书中的签名哈希值。

  • 在确认服务端证书是由国际授信 CA 签发,且完整性未被破坏后,客户端信任服务端证书,也就确认了服务端的正确身份。


SSL 证书


正如上一节介绍,SSL 证书在 HTTPS 协议中扮演着至关重要的作用,即验证服务端身份,协助对称密钥协商。只有配置了 SSL 证书的网站才可以开启 HTTPS 协议。在浏览器中,使用 HTTP 的网站会被默认标记为“不安全”,而开启 HTTPS 的网站会显示表示安全的锁图标。


image015.png
使用 HTTP 协议的网站


image028.gif
使用 HTTPS 协议的网站


从保护范围、验证强度和适用类型出发, SSL 证书会被分成不同的类型。只有了解类型之间的区别,才能根据实际情况选择更适合的证书类型,保障通信传输安全。


从保护范围分,SSL 证书可以分为单域名证书、通配符证书、多域名证书。



  • 单域名证书:单域名证书只保护一个域名,这些域名形如 http://www.test.com 等。

  • 通配符证书:通配符证书可以保护基本域和无限的子域。通配符 SSL 证书的公用名中带有星号 ,其中,星号表示具有相同基本域的任何有效子域。例如,。test.com 的通配符证书可用于保护 a.test.com、 b.test.com……

  • 多域名证书:多域证书可用于保护多个域或子域。包括完全唯一的域和具有不同顶级域的子域(本地/内部域除外)的组合。


从验证强度和适用类型进一步区分,SSL 证书可以分为 DV、OV、EV 证书。



  • DV(Domain Validated):域名验证型。在颁发该类型证书时,CA 机构仅验证申请者对域名的所有权。CA 机构会通过检查 WHOIS、DNS 的特定记录来确认资格。一般来说,DV 证书适用于博客、个人网站等不需要任何私密信息的网站。

  • OV(Organization Validated):组织验证型。OV 证书的颁发除了要验证域名所有权外,CA 还会额外验证申请企业的详细信息(名称、类型、地址)等。一般来说,OV 证书适用于中级商业组织。

  • EV(Extended Validation):扩展验证型。EV 证书的颁发除了 CA 对 DV 和 OV 证书所采取的所有身份验证步骤之外,还需要审查商业组织是否在真实运营、其实际地址,并致电以验证申请者的就业情况。一般来说,EV 证书适用于顶级商业组织。


结尾


随着互联网应用的普及,网络诈骗的方式也越发花样百出,让人防不胜防。


除了文内提到的网页环境,在软件应用、邮件、文档、物联网等领域同样存在恶意软件、钓鱼邮件、文档篡改、身份认证的问题。幸运的是,作为 PKI 体系下的优秀产品,证书体系同样在这些领域发挥着重要作用,软件签名证书、邮件签名证书、文档签名证书、私有证书等保护着各自领域的信息安全。


总有不法分子企图通过漏洞牟利,而证书体系在保护数据机密性、完整性、可用性以及身份验证场景上有着无可取代的地位,牢牢守护着用户信息,保障通信安全。


作者:火山引擎边缘云
来源:juejin.cn/post/7273685263841263672
收起阅读 »

使用 Vim 两年后的个人总结

为什么要使用 Vim 学习动机非常重要。并不是很多大牛程序员用 Vim 编程,你就应该去学习 Vim,如果你是这种心态,很大的概率,你会在几次尝试以后最终放弃,就像我曾经做过的一样。因为 Vim 的学习曲线很陡峭,没有强烈的学习动机很难坚持下来。 那我为什么后...
继续阅读 »

为什么要使用 Vim


学习动机非常重要。并不是很多大牛程序员用 Vim 编程,你就应该去学习 Vim,如果你是这种心态,很大的概率,你会在几次尝试以后最终放弃,就像我曾经做过的一样。因为 Vim 的学习曲线很陡峭,没有强烈的学习动机很难坚持下来。


那我为什么后来又重新开始学习 Vim,并在两年多后已经习惯、喜欢甚至离不开 Vim?原因很简单,我必须掌握 Vim。


我是一个很爱折腾的人,自己买过很多云服务器,也经常会在服务器上写一些程序,编辑器当然首选 Vim。日复一日,当有一天我实在无法忍受自己在服务端极其低效的编程体验后,我决定真正掌握 Vim。从那时候起,我开始刻意频繁练习,也终于有一天,我发现我完全存活了下来,并且喜欢上了 Vim。


我并不是说你一定要买个云服务器,然后在云服务器上写代码(其实现在你可以用 VSCode 的远程功能在服务器上写代码),我想表达的是,你一定要有足够的学习动机,这个学习动机往往来自于必要性,不管是工作上的必要性,还是自己业余项目上的必要性。也许,有强烈的炫耀动机可能也行。


当然了,当你真正喜欢上 Vim,你会有新的理解,比如 Vim 某种意义上代表了一些正向的价值,文章最后我会提到这一点。


关于 Normal 模式的最佳隐喻


《代码大全》(Code Complete)开头就讲了“软件构建的隐喻”,隐喻是非常好的方式,能够通过熟悉的事物帮我们建立正确的思维模型。关于 Vim 为什么要有 Normal 模式,我看过的最好的隐喻来自《Practical Vim》这本书,我摘录几个关键的段落:



Think of all of the things that painters do besides paint. They study their subject, adjust the lighting, and mix paints into new hues. And when it comes to applying paint to the canvas, who says they have to use brushes? A painter might switch to a palette knife to achieve a different texture or use a cotton swab to touch up the paint that's already been applied.




The painter does not rest with a brush on the canvas. And so it is with Vim. Normal mode is the natural resting state. The clue is in the name, really.




Just as painters spend a fraction of their time applying paint,programmers spend a fraction of their time composing code . More time is spent thinking, reading, and navigating from one part of a codebase to another. And when we do want to make a change, who says we have to switch to Insert mode? We can reformat existing code, duplicate it, move it around, or delete it. From Normal mode, we have many tools at our disposal.



作者把编程比喻成绘画,把 Normal 模式比喻成画家作画的间隙。就像画家要经常放下画笔,走远处看看,或用小刀、棉球等工具修改画作一样,程序员也不会一直输入代码(Insert 模式),程序员也需要思考,需要对程序做一些修改(不一定是插入内容),那么这个时候就应该进入 Normal 模式。Normal 模式让程序员休息、思考,同时提供了更多的工具,比如删除、复制、黏贴、跳转光标等等。每当写程序需要停顿思考的时候,就可以进入 Normal 模式。


一个最重要的模式


这里的模式,不是指“Normal”或“Insert”模式。而是我们在使用 Vim 组合快捷键时候的“操作模式”。这个最重要的模式如下:

Action = Operator + Motion

举一个例子,“删除当前到句尾的所有字符”的操作是d$d$ = d + $,其中的 d 即为 Operator,也即操作,$ 即为 Motion,也即操作的范围。这个模式在 Vim 中无处不在,再举一些例子:

  • dap,删除一整个段落;
  • yG,复制当前行到文件末尾所有内容;
  • cw, 修改当前单词(删除单词并进入 Insert 模式);

这是最基本的模式,也是 Vim 编辑器能高效编辑文本的基础,它把常用的 Operator 和 Motion 做了抽象,抽象成了一些简单字母,比如 d 代表删除操作,$代表句子末尾,而这些抽象符号又可以通过同一个公式组合使用,减轻了记忆负担。这是 Vim 非常优雅的地方。 不过有一个例外,如果你连续输入两个 Operator,就表示对当前行进行操作。比如 yy 表示复制当前行。


那 Vim 中有哪些常用的 Operator 呢,有以下这些:


至于 Motion,有更多,以下也是一些常用的:


当然还有更多,如果你感兴趣,可以在 Vim 的 Normal 模式下,输入以下命令查看完整的文档:

:h motion.txt

先存活下来


在成为 Vim 高手之前,我们的首要目标是先存活下来。这个目标其实并不难。


掌握基本的光标跳转,比如hlkj0^$ggG 等等,以及以上说的基本操作模式后,你大概率可以生存下来。当然知道不等于掌握,你需要频繁地练习把基本操作变成肌肉记忆。我一开始是跟着左耳朵耗子(在此纪念耗子叔)的文章《简明 VIM 练级攻略》练习,当时一旦有时间就打开文章,跟着内容逐条操作,一段时间后,我就真的存活下来了。


如果你也顺利存活了下来,在实际的开发过程中就已经可以使用 Vim 做一些编辑工作了,但可能总还是觉得哪儿哪儿不对劲,要完全行云流水还欠缺更多技巧。这个时候或许有必要去看看《Practival Vim》或者类似的书,更好地掌握 Vim 的设计理念以及许多细微的地方,同样配合不断的练习,我相信你迟早有一天会欣喜地发现自己在编码的时候几乎可以放弃鼠标了,这种喜悦或许类似于修仙小说中的破境。


恭喜你。


成为 Vim 高手的终极秘诀


其实没有秘诀。Vim 很快,但成为 Vim 高手是一个相对漫长的过程,在这个过程中你会掌握更多微妙的技能,比如如何更高效地使用 f{char} 命令更快地定位到某个字符。在生存下来以后,你唯一能做的就是每天使用 Vim。慢慢地, Vim 的使用会变成水和空气一样的自然存在,你从此离不开它。


如何每天使用 Vim 呢,以下是我的一些建议:

  • 把 Vim 变成日常开发工具。学习使用 Neovim,它提供了更好的插件和扩展机制,你如果愿意你甚至可以把 Neovim 配置成强大的 IDE。这里推荐一下掘金小册 Neovim 配置实战
  • 如果习惯使用 VSCode 或其他编辑器,可以安装相应的 Vim 插件
  • 如果你使用 Chrome 浏览器,你可以安装相应的 Vim 插件来提升浏览效率。
  • 平时习惯做笔记?那就使用一款支持 Vim 快捷键的笔记软件,比如我最喜欢的 Obsidian
  • 经常在 Cloud IDE 上写代码?建议使用一款支持 Vim 快捷键的 IDE,比如我常用的 Replit

总之,在我决定使用 Vim 提高编程效率以后,在任何编辑场景我都变得无法忍受没有 Vim 的存在,就是这么自然,它变成了我工作的一部分。Reddit 上有这样一条讨论,If using vim is a lifestyle/philosophy, what other products also fits into this lifestyle?,把 Vim 隐喻成一种生活方式/哲学确实很合适,Vim 和学习使用 Vim 隐含了一些有价值的东西,我相信大约有追求极致——更快更强的精神,坚持长期主义——忍受暂时痛苦,着眼长远的精神。或许也可以这么说,如果你有朝一日能成为 Vim 高手,你大概率也能做成其他许多困难的事。


少年们,加油。


作者:chaosflutter
链接:https://juejin.cn/post/7263676990736220220
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

把数据库里的未付款订单改成已付款,会发生什么

导言 不知道大家在网上购物的时候,有没有这样的念头,如果能把未付款的订单偷偷用一条SQL改成已付款,该多么美好啊。那么在实际开发过程中,我们应当如何保证数据库里的数据在保存后不会被偷偷更改? 大家好我是日暮与星辰之间,创作不易,如果觉得有用,求点赞,求收藏,...
继续阅读 »

导言


不知道大家在网上购物的时候,有没有这样的念头,如果能把未付款的订单偷偷用一条SQL改成已付款,该多么美好啊。那么在实际开发过程中,我们应当如何保证数据库里的数据在保存后不会被偷偷更改?



大家好我是日暮与星辰之间,创作不易,如果觉得有用,求点赞,求收藏,求转发,谢谢。



理论


在介绍具体的内容之间,先介绍MD5算法,简单的来说,MD5能把任意大小、长度的数据转换成固定长度的一串字符,经常玩大型游戏的朋友应该都注意到过,各种补丁包、端游客户端之类的大型文件一般都附有一个MD5值,用于确保你下载文件的完整性。那么在这里,我们可以借鉴其思想,对订单的某些属性进行加密计算,得出来一个 MD5值一并保存在数据库当中。从数据库取出数据后第一时间进行校验,如果有异常更改,那么及时抛出异常进行人工处理。


实现


道理我都懂,但是我要如何做呢,别急,且听我一一道来。


这种需求听起来并不强绑定于某个具体的业务需求,这就要用到了我们熟悉的鼎鼎有名的AOP(面向切面编程)来实现。


首先定义四个类型的注解作为AOP的切入点。@Sign@Validate都是作用在方法层面的,分别用于对方法的入参进行加签和验证方法的返回值的签名。@SignField用于注解关键的不容篡改的字段。@ValidateField用于注解保存计算后得出的签名值。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sign {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Validate {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SignField {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidField {
}

以订单的实体为例 sn,amt,status,userId就是关键字段,绝不能允许有人在落单到数据库后对这些字段偷偷篡改。

public class Order {
@SignField
private String sn;
@SignField
private String amt;
@SignField
private int status;
@SignField
private int userId;
@ValidField
private String sign;
}

下面就到了重头戏的部分,如何通过AOP来进行实现。


1. 定义切入点

@Pointcut("execution(@com.example.demo.annotations.Sign * *(..))")
public void signPointCut() {

}

@Pointcut("execution(@com.example.demo.annotations.Validate * *(..))")
public void validatePointCut() {

}

2.环绕切入点

@Around("signPointCut()")
public Object signAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
for (Object o : args) {
System.out.println(o);
sign(o);
}
Object res = pjp.proceed(args);
return res;
}

@Around("validatePointCut()")
public Object validateAround(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
Object res = pjp.proceed(args);
valid(res);
return res;
}

3. 签名的实现

  • 获取需要签名字段
private Map<String, String> getSignMap(Object o) throws IllegalAccessException {
Map<String, String> fieldNameToValue = new HashMap<>();
for (Field f : o.getClass().getDeclaredFields()) {
System.out.println(f.getName());
for (Annotation annotation : f.getDeclaredAnnotations()) {
if (annotation.annotationType().equals(SignField.class)) {
String value = "";
f.setAccessible(true);
fieldNameToValue.put(f.getName(), f.get(o).toString());
}
}
}
return fieldNameToValue;
}
  • 计算出签名值,这里在属性名和属性值以外加入了我的昵称以防止他人猜测,同时使用了自定义的分隔符来加强密码强度。
private String getSign(Map<String, String> fieldNameToValue) {
List<String> names = new ArrayList<>(fieldNameToValue.keySet());
StringBuilder sb = new StringBuilder();
for (String name : names)
sb.append(name).append("@").append(fieldNameToValue.get(name));
System.out.println(sb.append("日暮与星辰之间").toString());
String signValue = DigestUtils.md5DigestAsHex(sb.toString().getBytes(StandardCharsets.UTF_8));
return signValue;
}

  • 找到保存签名的字段
private Field getValidateFiled(Object o) {
for (Field f : o.getClass().getDeclaredFields()) {
for (Annotation annotation : f.getDeclaredAnnotations()) {
if (annotation.annotationType().equals(ValidField.class)) {
return f;
}
}
}
return null;
}

  • 对保存签名的字段进行赋值
public void sign(Object o) throws IllegalAccessException {
Map<String, String> fieldNameToValue = getSignMap(o);
if (fieldNameToValue.isEmpty()) {
return;
}
Field validateField = getValidateFiled(o);
if (validateField == null)
return;
String signValue = getSign(fieldNameToValue);
validateField.setAccessible(true);
validateField.set(o, signValue);
}

  • 对从数据库中取出的对象进行验证
public void valid(Object o) throws IllegalAccessException {
Map<String, String> fieldNameToValue = getSignMap(o);
if (fieldNameToValue.isEmpty()) {
return;
}
Field validateField = getValidateFiled(o);
validateField.setAccessible(true);
String signValue = getSign(fieldNameToValue);
if (!Objects.equals(signValue, validateField.get(o))) {
throw new RuntimeException("数据非法");
}

}

使用示例


对将要保存到数据库的对象进行签名

@Sign
public Order save( Order order){
orderList.add(order);
return order;
}

验证从数据库中取出的对象是否合理

@Validate
public Order query(@ String sn){
return orderList.stream().filter(e -> e.getSn().equals(sn)).findFirst().orElse(null);
}

好文分享 ⬇️
从Offer收割机到延毕到失业再到大厂996,二零二二我的兵荒马乱 - 掘金


另类年终总结:在煤老板开的软件公司实习是怎样一种体验? - 掘金


第一次值守双十一,居然没有任何意外发生?! - 掘金


大厂996三个月,我曾迷失了生活的意义,努力找回中 - 掘金


阿里实习三个月,我学会了面试时讲好自己的项目,欢迎提问 - 掘金


迟到的苏州微软实习历险记 - 掘金


什么时候要用到本地缓存,比Redis还要快?怎么用? - 掘金


作者:日暮与星辰之间
链接:https://juejin.cn/post/7144378128914186270
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

聊一聊过度设计!

  新手程序员在做设计时,因为缺乏经验,很容易写出欠设计的代码,但有一些经验的程序员,尤其是在刚学习过设计模式之后,很容易写出过度设计的代码,而这种代码比新手程序员的代码更可怕,过度设计的代码不仅写出来时的成本很高,后续维护的成本也高。因为相对于毫无设计的代码...
继续阅读 »

  新手程序员在做设计时,因为缺乏经验,很容易写出欠设计的代码,但有一些经验的程序员,尤其是在刚学习过设计模式之后,很容易写出过度设计的代码,而这种代码比新手程序员的代码更可怕,过度设计的代码不仅写出来时的成本很高,后续维护的成本也高。因为相对于毫无设计的代码,过度设计的代码有比较高的理解成本。说这么多,到底什么是过度设计?


什么是过度设计?


  为了解释清楚,我这里用个类比,假如你想拧一颗螺丝,正常的解决方案是找一把螺丝刀,这很合理对吧。 但是有些人就想:“我就要一个不止能拧螺丝的工具,我想要一个可以干各种事的工具!”,于是就花大价钱搞了把瑞士军刀。在你解决“拧螺丝”问题的时候,重心早已从解决问题转变为搞一个工具,这就是过度设计。

   再举个更技术的例子,假设你出去面试,面试官让你写一个程序,可以实现两个数的加减乘除,方法出入参都给你提供好了 int calc(int x, int y, char op),普通程序员可能会写出以下实现。

    public int calc(int x, int y, int op) {
if (op == '+') {
return x + y;
} else if (op == '-') {
return x - y;
} else if (op == '*') {
return x * y;
} else {
return x / y;
}
}

  而高级程序员会运用设计模式,写出这样的代码:

public interface Strategy {
int calc(int x, int y);
}

public class AddStrategy implements Strategy{
@Override
public int calc(int x, int y) {
return x + y;
}
}

public class MinusStrategy implements Strategy{
@Override
public int calc(int x, int y) {
return x - y;
}
}
/**
* 其他实现
*/
public class Main {
public int calc(int x, int y, int op) {
Strategy add = new AddStrategy();
Strategy minux = new MinusStrategy();
Strategy multi = new MultiStrategy();
Strategy div = new DivStrategy();
if (op == '+') {
return add.calc(x, y);
} else if (op == '-') {
return minux.calc(x, y);
} else if (op == '*') {
return multi.calc(x, y);
} else {
return div.calc(x, y);
}
}
}

  策略模式好处在于将计算(calc)和具体的实现(strategy)拆分,后续如果修改具体实现,也不需要改动计算的逻辑,而且之后也可以加各种新的计算,比如求模、次幂……,扩展性明显增强,很是牛x。 但光从代码量来看,复杂度也明显增加。回到我们原始的需求上来看,如果我们只是需要实现两个整数的加减乘除,这明显过度设计了。


过度设计的坏处


  个人总结过度设计有两大坏处,首先就是前期的设计和开发的成本问题。过度设计的方案,首先设计的过程就需要投入额外的时间成本,其次越复杂的方案实现成本也就越高、耗时越长,如果是在快速迭代的业务中,这些可能都会决定到业务的生死。其次即便是代码正常上线后,其复杂度也会导致后期的维护成本高,比如当你想将这些代码交接给别人时,别人也需要付出额外的学习成本。


  如果成本问题你都可以接受,接下来这个问题可能影响更大,那就是过度设计可能会影响到代码的灵活性,这点听起来和做设计的目的有些矛盾,做设计不就是为了提升代码的灵活性和扩展性吗!实际上很多过度设计的方案搞错了扩展点,导致该灵活的地方不灵活,不该灵活的地方瞎灵活。在机器学习领域,有个术语叫做“过拟合”,指的是算法模型在测试数据上表现完美,但在更广泛的数据上表现非常差,模式缺少通用性。 过度设计也会出现类似的现象,就是缺少通用性,在面对稍有差异的需求上时可能就需要伤筋动骨级别的改造了。


如何避免过度设计


  既然过度设计有着成本高和欠灵活的问题,那如何避免过度设计呢!我这里总结了几个方法,希望可以帮到大家。


充分理解问题本身


  在设计的过程中,要确保充分理解了真正的问题是什么,明确真正的需求是什么,这样才可以避免做出错误的设计。


保持简单


  过度设计毫无例外都是复杂的设计,很多时候未来有诸多的不确定性,如果过早的针对某个不确定的问题做出方案,很可能就白做了,等遇到真正问题的时候再去解决问题就行。


小步快跑


  不要一开始就想着做出完美的方案,很多时候优秀的方案不是设计出来的,而是逐渐演变出来的,一点点优化已有的设计方案比一开始就设计出一个完美的方案容易得多。


征求其他人的意见


  如果你不确定自己的方案是不是过度设计了,可以咨询下其他人的,尤其是比较资深的人,交叉验证可以快速让你确认问题。


总结


  其实在业务的快速迭代之下,很难判定当前的设计是欠设计还是过度设计,你当前设计了一个简单的方案,未来可能无法适应更复杂的业务需求,但如果你当前设计了一个复杂的方案,有可能会浪费时间……。 在面对类似这种不确定性的时候,我个人还是比较推崇大道至简的哲学,当前用最简单的方案,等需要复杂性扩展的时候再去重构代码。


作者:xindoo
链接:https://juejin.cn/post/7204423284905738298
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Moshi:现代 Json 解析库全解析

json 解析框架,很容易想到 Gson、fastJson 等。而这些流行框架对 kotlin 的支持并不好,而Moshi 天生对 kotlin 友好。 前言 Gson 通过反射反序列化数据,Java 类默认有无参构造函数,对于默认参数能够很好的支持。对于 k...
继续阅读 »

json 解析框架,很容易想到 Gson、fastJson 等。而这些流行框架对 kotlin 的支持并不好,而Moshi 天生对 kotlin 友好。


前言


Gson 通过反射反序列化数据,Java 类默认有无参构造函数,对于默认参数能够很好的支持。对于 kotlin ,我们经常使用的 data class,其往往没有无参构造函数,Gson 便会通过 UnSafe 的方式创建实例,成员无法正常初始化默认值。为了勉强能用,只能将构造参数都加上默认值才行,不过这种兼容方式太过隐晦,有潜在的维护风险。


另外,Gson 无法支持 kotlin 空安全特性。定义为不可空且无默认值的字段,在没有该字段对应的 json 数据时会被赋值为 null,这可能导致使用时引发空指针问题。


Moshi


Moshi 是一个适用于 Android、Java 和 Kotlin 的现代 JSON 库。它可以轻松地将 JSON 解析为 Java 和 Kotlin 类。


val json: String = ...

val moshi: Moshi = Moshi.Builder().build()
val jsonAdapter: JsonAdapter = moshi.adapter()

val person = jsonAdapter.fromJson(json)

通过类型适配器 JsonAdapter 可以对数据类型 T 进行序列化/反序列化操作,即 toJsonfromJson 方法。


内置类型适配器


moshi 内置支持以下类型的类适配器:

  • 基本类型
  • Arrays, Collections, Lists, Sets, Maps
  • Strings
  • Enums


直接或间接由它们构成的自定义数据类型都可以直接解析。


反射 OR 代码生成


moshi 支持反射和代码生成两种方式进行 Json 解析。


反射的好处是无需对数据类做任何变动,可以解析 private 和 protected 成员,缺点是引入反射相关库,包体积增大2M多,且反射在性能上稍差。


代码生成的好处是速度更快,缺点是需要对数据类添加注解,无法处理 private 和 protected 成员,用于编译时生成代码,影响编译速度,且注解使用越来越多生成的代码也会越来越多。


反射方案依赖:


implementation("com.squareup.moshi:moshi-kotlin:1.14.0")

代码生成方案依赖(ksp):


plugins {
id("com.google.devtools.ksp").version("1.6.10-1.0.4") // Or latest version of KSP
}

dependencies {
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.14.0")
}

使用代码生成,需要使用注解 @JsonClass(generateAdapter = true) 修饰数据类:


@JsonClass(generateAdapter = true)
data class Person(
val name: String
)

使用反射时,需要添加 KotlinJsonAdapterFactoryMoshi.Builder


val moshi = Moshi.Builder()
.addLast(KotlinJsonAdapterFactory())
.build()

💡 注意:这里要使用 addLast 添加 KotlinJsonAdapterFactory,因为 Adapter 是按添加顺序排列和使用的,如果有自定义的 Adapter,为确保自定义的始终在前,建议通过 addLastKotlinJsonAdapterFactory 始终放在最后。


我们目前使用的是反射方案,主要考虑到侵入性低,数据类几乎无改动。


其实也可以两种方案都使用,Moshi 会优先使用代码生成的 Adapter,没有的话则走反射。


解析 JSON 数组


对于 json 数据:


[
{
"rank": "4",
"suit": "CLUBS"
},
{
"rank": "A",
"suit": "HEARTS"
}
]

解析:


String cardsJsonResponse = ...;
Type type = Types.newParameterizedType(List.class, Card.class);
JsonAdapter> adapter = moshi.adapter(type);
List cards = adapter.fromJson(cardsJsonResponse);

和 Gson 类似,为了运行时获取泛型信息,稍微麻烦点,可以定义扩展函数简化用法:


inline fun <reified T> Moshi.listAdapter(): JsonAdapter> {
val type = Types.newParameterizedType(List::class.java, T::class.java)
return adapter(type)
}

简化后:


String cardsJsonResponse = ...
val cards = moshi.listAdapter().fromJson(cardsJsonResponse)

自定义字段名


如果Json 中字段名和数据类中字段名不一致,或 json 中有空格,可以使用 @Json 注解修饰别名。


{
"username": "jesse",
"lucky number": 32
}

class Player {
val username: String
@Json(name = "lucky number") val luckyNumber: Int

...
}

忽略字段


使用 @Json(ignore = true) 可以忽略字段的解析,java 中的 @Transient 注解也可以。


class BlackjackHand(...) {
@Json(ignore = true)
var total: Int = 0

...
}

Java 支持


Moshi 同样支持 Java。需要注意的是,和 Gson 一样,Java 类需要有无参构造方法,否则成员变量的默认值无法生效。


public final class BlackjackHand {
private int total = -1;
...

public BlackjackHand(Card hidden_card, List visible_cards) {
...
}
}

如上,total 的默认值会为 0.


另外,和 Gson 不一样的是,Moshi 并不支持 JsonElement 这种中间产物,它只支持内置类型如 List、Map。


自定义 JsonAdapter


如果 json 的数据格式和我们想要的不一样,就需要我们自定义 JsonAdapter 来解析了。有意思的是,任何拥有 @Json@ToJson 注解的类都可以成为 Adapter,无需继承 JsonAdapter。


例如 json 格式:


{
"title": "Blackjack tournament",
"begin_date": "20151010",
"begin_time": "17:04"
}

目标数据类定义:


class Event(
val title: String,
val beginDateAndTime: String
)

我们希望 json 中日期 begin_date 和时间 begin_time 组成 beginDateAndTime 字段。moshi 支持我们在 json 和目标数据转换间定义一个中间类,json 和中间类转换后再转换为最终类型。


定义中间类型,本例中即和 json 匹配的数据类型:


class EventJson(
val title: String,
val begin_date: String,
val begin_time: String
)

定义 Adapter :


class EventJsonAdapter {
@FromJson
fun eventFromJson(eventJson: EventJson): Event {
return Event(
title = eventJson.title,
beginDateAndTime = "${eventJson.begin_date} ${eventJson.begin_time}"
)
}

@ToJson
fun eventToJson(event: Event): EventJson {
return EventJson(
title = event.title,
begin_date = event.beginDateAndTime.substring(0, 8),
begin_time = event.beginDateAndTime.substring(9, 14),
)
}
}

将 adapter 注册到 moshi:


val moshi = Moshi.Builder()
.add(EventJsonAdapter())
.build()

这样就可以使用 moshi 直接将 json 转换成 Event 了。本质是将 Json 和目标数据的相互转换加了个中间步骤,先转换为中间产物,再转为最终 Json 或数据实例。


@JsonQualifier:自定义字段类型解析


如下 json,color 为十六进制 rgb 格式的字符串:


{
"width": 1024,
"height": 768,
"color": "#ff0000"
}

数据类,color 为 Int 类型:


class Rectangle(
val width: Int,
val height: Int,
val color: Int
)

Json 中 color 字段类型是 String,数据类同名字段类型为 Int,除了上面介绍的自定义 JsonAdapter 外,还可以自定义同一数据的不同数据类型间的转换。


首先自定义注解:


@Retention(RUNTIME)
@JsonQualifier
annotation class HexColor

使用注解修饰字段:


class Rectangle(
val width: Int,
val height: Int,
@HexColor val color: Int
)

自定义 Adapter:


/** Converts strings like #ff0000 to the corresponding color ints.  */
class ColorAdapter {
@ToJson fun toJson(@HexColor rgb: Int): String {
return "#x".format(rgb)
}

@FromJson @HexColor fun fromJson(rgb: String): Int {
return rgb.substring(1).toInt(16)
}
}

通过这种方式,同一字段可以有不同的解析方式,可能不多见,但的确有用。


适配器组合


举个例子:


class UserKeynote(
val type: ResourceType,
val resource: KeynoteResource?
)

enum class ResourceType {
Image,
Text
}

sealed class KeynoteResource(open val id: Int)

data class Image(
override val id: Int,
val image: String
) : KeynoteResource(id)

data class Text(
override val id: Int,
val text: String
) : KeynoteResource(id)

UserKeynote 是目标类,其中的 KeynoteResource 可能是 ImageText ,具体是哪个需要根据 type 字段来决定。也就是说 UserKeynote 的解析需要 Image 或 Text 对应的 Adapter 来完成,具体是哪个取决于 type 的值。


显然自带的 Adapter 不能满足需求,需要自定义 Adapter。


先看下 Adapter 中签名要求(参见源码 AdapterMethodsFactory.java):


@FromJson


 R fromJson(JsonReader jsonReader) throws 

R fromJson(JsonReader jsonReader, JsonAdapter delegate, ) throws

R fromJson(T value) throws

@ToJson


 void toJson(JsonWriter writer, T value) throws 

void toJson(JsonWriter writer, T value, JsonAdapter delegate, ) throws

R toJson(T value) throws

前面分析了我们需要借助 Image 或 Text 对应的 Adapter,所以使用第二组函数签名:


class UserKeynoteAdapter {
private val namesOption = JsonReader.Options.of("type")

@FromJson
fun fromJson(
reader:
JsonReader,
imageJsonAdapter:
JsonAdapter<Image>,
textJsonAdapter:
JsonAdapter<Text>
)
: UserKeynote {
// copy 一份 reader,得到 type
val newReader = reader.peekJson()
newReader.beginObject()
var type: String? = null
while (newReader.hasNext()) {
if (newReader.selectName(namesOption) == 0) {
type = newReader.nextString()
}
newReader.skipName()
newReader.skipValue()
}
newReader.endObject()

// 根据 type 做解析
val resource = when (type) {
ResourceType.Image.name -> {
imageJsonAdapter.fromJson(reader)
}

ResourceType.Text.name -> {
textJsonAdapter.fromJson(reader)
}

else -> throw IllegalArgumentException("unknown type $type")
}
return UserKeynote(ResourceType.valueOf(type), resource)
}

@ToJson
fun toJson(
writer:
JsonWriter,
userKeynote:
UserKeynote,
imageJsonAdapter:
JsonAdapter<Image>,
textJsonAdapter:
JsonAdapter<Text>
)
{
when (userKeynote.resource) {
is Image -> imageJsonAdapter.toJson(writer, userKeynote.resource)
is Text -> textJsonAdapter.toJson(writer, userKeynote.resource)
null -> {}
}
}
}

函数接收一个 JsonReader / JsonWriter 以及若干 JsonAdapter,可以认为该 Adapter 由其他多个 Adapter 组合完成。这种委托的思路在 Moshi 中很常见,比如内置类型 List 的解析,便是委托给了 T 的适配器,并重复调用。


限制



  • 不要 Kotlin 类继承 Java 类

  • 不要 Java 类继承 Kotlin 类


这是官方强调不要做的,如果你那么做了,发现还没问题,不要侥幸,建议修改,毕竟有有维护风险,且会误导其他维护的人以为这样是可靠合理的。


作者:Aaron_Wang
来源:juejin.cn/post/7273516671575113743
收起阅读 »

面试官:如何防止重复提交订单?

这个问题,在电商领域的面试场景题下,应该算是妥妥的高频问题了,仅次于所谓的“秒杀场景如何实现”。 说个题外话,有段时间“秒杀场景如何实现”这个问题风靡一时,甚至在面试的时候,有些做财务领域、OA领域公司的面试官也都跟风问。 大有一种”无秒杀,不面试“的感觉了。...
继续阅读 »

这个问题,在电商领域的面试场景题下,应该算是妥妥的高频问题了,仅次于所谓的“秒杀场景如何实现”。


说个题外话,有段时间“秒杀场景如何实现”这个问题风靡一时,甚至在面试的时候,有些做财务领域、OA领域公司的面试官也都跟风问。


大有一种”无秒杀,不面试“的感觉了。


重复提交原因


其实原因无外乎两种:



  • 一种是由于用户在短时间内多次点击下单按钮,或浏览器刷新按钮导致。

  • 另一种则是由于Nginx或类似于SpringCloud Gateway的网关层,进行超时重试造成的。


常见解决方案


方案一:提交订单按钮置灰


这种解决方案在注册登录的场景下比较常见,当我们点击”发送验证码“按钮的时候,会进行手机短信验证码发送,且按钮就会有一分钟左右的置灰。


有些经验不太丰富的同学,通常会简单粗暴地把这个方案直接照搬过来。


但这种方案只能解决多次点击下单按钮的问题,对于Nginx或类似于SpringCloud Gateway的超时重试所导致的问题是无能为力的。


当然,这种方案也不是真的没有价值。它可以在高并发场景下,从浏览器端去拦住一部分请求,减少后端服务器的处理压力。


说到底,“下单防重”的问题是属于“接口幂等性”的问题范畴。



幂等性


接口幂等性是指:以相同的参数,对一个接口进行多次调用,所产生的结果和一次调用是完全相同的。


下面的情况就是幂等的:


student.setName("张三");

而这种情况就是非幂等的,因为每次调用,年龄都会增加一岁。


student.increaseAge(1);

现在我们的思路需要切换到幂等性的解决方案来。


同样是幂等性场景,“如何防止重复提交订单” 比 “如何防止订单重复支付” 的解决方案要难一些。


因为,后者在常规情况下,一个订单都是对应一笔支付单,所以orderID可以作为一个幂等性校验、防止订单重复支付的天然神器。


但这个方案在“如何防止重复提交订单”就不适用了,需要其他的解决方案,请继续看下文。


方案二:预生成全局唯一订单号


(1)后端新增一个接口,用于预生成一个“全局唯一订单号”,如:UUID 或 NanoID。


(2)进入创建订单页面时,前端请求该接口,获取该订单号。


(3)在提交订单时,请求参数里要带上这个预生成的“全局唯一订单号”,利用数据库的唯一索引特性,在插入订单记录时,如果该“全局唯一的订单号”重复,记录会插入失败。


btw:该“全局唯一订单号”不能代替数据库主键,在未分库分表场景下,主键还是用数据库自增ID比较好。



方案二


优点:彻底解决了重复下单的问题;


缺点:方案复杂,前后端都有开发工作量,还要新增接口,新增字段。


另外,网上还有同学说,要单独弄一个生成“全局唯一订单号”的服务,我觉得还是免了吧,这不是更麻烦了吗?


方案三:前端生成全局唯一订单号


这种方案是在借鉴了“方案二”的基础上,做了一些实现逻辑的简化。


(1)用户进入下页面时,前端程序自己生成一个“全局唯一订单号”。


(2)在提交订单时,请求参数里要带上这个预生成的“全局唯一订单号”,利用数据库的唯一索引特性,在插入订单记录时,如果该“全局唯一的订单号”重复,记录会插入失败。



方案三


优点:彻底解决了重复下单的问题,且技术方案做了一定简化;


缺点:前后端仍然都有开发工作量,且需要新增字段;


方案四:从订单业务的本质入手


先跟大家探讨一个概念,什么是订单?


其实,订单就是某个用户用特定的价格购买了某种商品,即:用户和商品的连接。


那么,“如何防止重复提交订单”,其实就是防止在短时间内,用户和商品进行多次连接。弄明白问题本质,接下来我们就着手制定技术方案了。


可以用 ”用户ID + 分隔符 + 商品ID“ 作为唯一标识,让持有相同标识的请求在短时间内不能重复下单,不就可以了吗?而且,Redis不正是做这种解决方案的利器吗?


Redis命令如下:


SET key value NX EX seconds


把”用户ID + 分隔符 + 商品ID“作为Redis key,并把”短时间所对应的秒数“设置为seconds,让它过期自动删除。


这样一来,整体业务步骤如下:


(1)在提交订单时,我们可以把”用户ID + 分隔符 + 商品ID“作为Redis key,并设置过期时间,让它可以到期自动删除。


(2)若Redis命令执行成功,则可以继续走下单的业务逻辑,执行不成功,直接返回给前端”下单失败“就可以了。



方案四


从上图来看,是不是实现方式越来越简单了?


优点:彻底解决了重复下单的问题,且在技术方案上,不需要前端参与,不需要添加接口,不需要添加字段;


缺点:综合比较而言,暂无明显缺点,如果硬要找缺点的话,可能强依赖于Redis勉强可以算上吧;


结语


在真正的生产环境下,我们最终选择了”方案四:从订单业务的本质入手“。


原因很简单,整体改动范围比较小,测试的回归范围也比较可控,且技术方案复杂度最低。


这样做技术选型的话,也比较符合百度一直倡导的”简单可依赖“原则。


作者:库森学长
来源:juejin.cn/post/7273024681631776829
收起阅读 »

聊聊Java中浮点丢失精度的事

在说这个之前,我们先看看十进制到二进制的转换过程 整数的十进制到二进制的转换过程 用白话说这个过程就是不断的除2,得到商继续除,直到商小于1为止,然后他每次结果的余数倒着排列出来就是它的二进制结果了,直接上图 说一下为什么倒着排列就是二进制结果哈 通俗点说就...
继续阅读 »

在说这个之前,我们先看看十进制到二进制的转换过程


整数的十进制到二进制的转换过程


用白话说这个过程就是不断的除2,得到商继续除,直到商小于1为止,然后他每次结果的余数倒着排列出来就是它的二进制结果了,直接上图


整数十进制转二进制.jpg
说一下为什么倒着排列就是二进制结果哈


通俗点说就是整数是一步一步除下来的,那回去不得一步一步乘上去?也就是说从上到下就是二进制从低位到高位的过程。


小数十进制到二进制的转换过程


小数的十进制到二进制的转换其实和整数类似,只不过算的方式变成了乘法,也就是用小数不断的乘2,然后得到的结果的整数部分拿出来,接着剩下的小数部分继续乘2,直到小数部分为0为止,直接上图~


小数十进制转二进制过程(不循环).jpg
二进制结果中的二分之一是转换后的,其实就是2的-1次方,-2次方。。。


当然了,小数转二进制的过程中,很多情况下都是无尽的,接着上图


小数十进制转二进制过程(循环).jpg
所以可以看到这样的循环下去是得不到二进制的结果的,所以计算机就要进行取舍。也就是IEEE 754规范


IEEE 754规范


IEEE 754规定了四种标识浮点数值的方式,单精确度(32位),双精确度(64位),延伸单精确度(43比特以上,很少用)和延伸双精确度(79比特以上,通常80位)


最常用的还是单精确度和双精确度,也就是对标的float和double。但是IEEE 754规范并没有解决精确标识小数的问题,只是提供了一种用近似值标识小数的方式。而且精确度不同近似值也会不同。# 为什么会精度丢失?教你看懂 IEEE-754!


下面有个例子来看一下丢失精度的问题,如0.1+0.2
0.1的64位二进制:0.00011001100110011001100110011001100110011001100110011001
0.2的64位二进制:0.00110011001100110011001100110011001100110011001100110011
二者相加的结果为:0.30000000000000004


那么如何解决精度问题呢?


BigDecimal


BigDecimal使用java.math包提供的,在涉及到金钱相关的计算的时候都需要使用它,而且其中提供了大量的方法,比如加减乘除都是可以直接调用的。


先看这个问题,BigDecimal中的比较问题


先看下面这个例子


public class ReferenceDemo {

public static void main(String[] args) {

BigDecimal bigDecimal1 = new BigDecimal(1);
BigDecimal bigDecimal2 = new BigDecimal(1);
System.out.println(bigDecimal1.equals(bigDecimal2));

BigDecimal bigDecimal3 = new BigDecimal(1);
BigDecimal bigDecimal4 = new BigDecimal(1.0);
System.out.println(bigDecimal3.equals(bigDecimal4));

BigDecimal bigDecimal5 = new BigDecimal("1");
BigDecimal bigDecimal6 = new BigDecimal("1.0");

System.out.println(bigDecimal5.equals(bigDecimal6));
}


}

结果为:


image.png
其中第二个例子和第三个例子的不同是需要聊一聊的。为什么会出现这种呢?下面是BigDecimal中的equals的源码。


public boolean equals(Object x) {
if (!(x instanceof BigDecimal))
return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this)
return true;
//关键在这一行,比较了scale
if (scale != xDec.scale)
return false;
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor(this.intVal);

return this.inflated().equals(xDec.inflated());
}

由上面的注释可以看到BigDecimal中有一个很关键的属性,就是scale,标度。标度是什么?
首先看一下BigDecimal的结构


public class BigDecimal extends Number implements Comparable<BigDecimal> {
/**
* The unscaled value of this BigDecimal, as returned by {@link
* #unscaledValue}.
*
* @serial
* @see #unscaledValue
*/

private final BigInteger intVal;

/**
* The scale of this BigDecimal, as returned by {@link #scale}.
*
* @serial
* @see #scale
*/

private final int scale; // Note: this may have any value, so
// calculations must be done in longs

/**
* If the absolute value of the significand of this BigDecimal is
* less than or equal to {@code Long.MAX_VALUE}, the value can be
* compactly stored in this field and used in computations.
*/

private final transient long intCompact;
}

我截取了几个关键字段,依次看一下:


intVal: 无标度值


scale: 标度


intCompact: 当intVal超过阈值(默认为Long.MAX_VALUE)时,进行压缩运算,结果存到这个字段上,用于后续计算。


注释中解释到,scale为0或者正数的时候代表数字小数点之后的位数,如果scale为负数,代表数字的无标度值需要乘10的该负数的绝对值的幂,即末尾有几个0


比如123.123这个数,他的intVal就是123123,scale就是3了


而二进制无法标识0.1,通过BigDecimal标识的话,它的intVal就是1,scale也是1。


接着看回上面的例子,传入的参数是字符串的bigDecimal5和bigDecimal6,为什么就返回了false。上图


image.png


他们的标度是不同的,所以直接返回了false,那么在看bigDecimal3和bigDecimal4的比较,为什么就返回了true呢,同样上图


image.png
可以看到他们的intVal和scale都是相等的,但是明明传入了不同的,有兴趣的可以取看看源码,找一些资料,对于1.0这个数,它本质上也是一个整数,经过一系列的运算他的intVal还是1,scale还是0,所以比较之后返回的是true。


这时候就能看出来equals方法的一些问题了,用equals涉及到scale的比较,实际的结果可能和预期不一样,所在BigDecimal的比较推荐用compareTo方法,如果返回0,代表相等


BigDecimal bigDecimal5 = new BigDecimal("1");
BigDecimal bigDecimal6 = new BigDecimal("1.0");

System.out.println(bigDecimal5.compareTo(bigDecimal6));

说到这里同时提一下,不要用传参为double的构造方法,同样会丢失精度,如果需要小数,需要传入字符串的小数来获取BigDecimal的实例对象。


说到这其实应该明白了他是怎么保证精度的了,其实关键点就是scale,这个标度贯穿了整个过程,加减乘除的运算都需要它来把控。上面说了其实2个参数最为关键,一个是无标度值,一个是标度,无标度值就是整数了,以加法为例子,不就可以变成整数的加法了吗,然后用scale控制小数点,说是这么说,实现过程还是很复杂的,有兴趣的可以自己查资料去学习。


除了用字符串代替double来表示BigDecimal的小数,其实也可以通过BigDecimal.valueOf()方法,它传入double之后可以和字符串一样的效果,为啥呢?上代码


public static BigDecimal valueOf(double val) {
// Reminder: a zero double returns '0.0', so we cannot fastpath
// to use the constant ZERO. This might be important enough to
// justify a factory approach, a cache, or a few private
// constants, later.
return new BigDecimal(Double.toString(val));
}

它把传入的double给toString了。。。。


作者:yulbo
来源:juejin.cn/post/7274692953058082877
收起阅读 »

谷歌是如何写技术文档的

Google软件工程文化的一个关键要素是使用设计文档来定义软件设计。这些相对非正式的文件由软件系统或应用程序的主要作者在开始编码项目之前创建。设计文档记录了高层次的实现策略和关键设计决策,重点强调在这些决策过程中考虑到的权衡。 作为软件工程师,我们的任务不仅仅...
继续阅读 »

Google软件工程文化的一个关键要素是使用设计文档来定义软件设计。这些相对非正式的文件由软件系统或应用程序的主要作者在开始编码项目之前创建。设计文档记录了高层次的实现策略和关键设计决策,重点强调在这些决策过程中考虑到的权衡。


作为软件工程师,我们的任务不仅仅是生成代码,而更多地是解决问题。像设计文档这样的非结构化文本可能是项目生命周期早期解决问题更好的工具,因为它可能更简洁易懂,并且以比代码更高层次的方式传达问题和解决方案。


除了原始软件设计文件外,设计文档还在以下方面发挥着作用:


在进行变更时及早识别出设计问题仍然较便宜。


在组织内达成对某个设计方案的共识。


确保考虑到跨领域关注点。


将资深工程师们掌握知识扩展到整个组织中去。


形成围绕设计决策建立起来的组织记忆基础。


作为技术人员投资组合中一份摘要性产物存在于其中。


设计文档的构成


设计文档是非正式的文件,因此其内容没有严格的指导方针。第一条规则是:以对特定项目最有意义的形式编写。


话虽如此,事实证明,某种结构已经被证明非常有用。


上下文和范围


本节为读者提供了新系统构建的大致概述以及实际正在构建的内容。这不是一份需求文档。保持简洁!目标是让读者迅速了解情况,但可以假设有一些先前的知识,并且可以链接到详细信息。本节应完全专注于客观背景事实。


目标和非目标


列出系统目标的简短项目列表,有时更重要的是列出非目标。请注意,非目标并不是否定性的目标,比如“系统不应崩溃”,而是明确选择不作为目标而合理可能成为目标的事项。一个很好的例子就是“ACID兼容性”;在设计数据库时,您肯定想知道是否将其作为一个目标或非目标。如果它是一个非目标,则仍然可以选择提供该功能的解决方案,前提是它不会引入阻碍实现这些目标的权衡考虑。


实际设计


这一部分应该以概述开始,然后进入细节。


image.png


设计文档是记录你在软件设计中所做的权衡的地方。专注于这些权衡,以产生具有长期价值的有用文档。也就是说,在给定上下文(事实)、目标和非目标(需求)的情况下,设计文档是提出解决方案并展示为什么特定解决方案最能满足这些目标的地方。


撰写文件而不是使用更正式的媒介之一的原因在于提供灵活性,以适当方式表达手头问题集。因此,并没有明确指导如何描述设计。


话虽如此,已经出现了一些最佳实践和重复主题,在大多数设计文档中都很合理:


系统上下文图


在许多文档中,系统上下文图非常有用。这样的图表将系统显示为更大的技术环境的一部分,并允许读者根据他们已经熟悉的环境来理解新设计。


image.png
一个系统上下文图的示例。


APIs


如果设计的系统暴露出一个API,那么草拟出该API通常是个好主意。然而,在大多数情况下,应该抵制将正式接口或数据定义复制粘贴到文档中的诱惑,因为这些定义通常冗长、包含不必要的细节,并且很快就会过时。相反,重点关注与设计及其权衡相关的部分。


数据存储


存储数据的系统可能需要讨论如何以及以什么样的形式进行存储。与对API的建议类似,出于同样的原因,应避免完全复制粘贴模式定义。而是专注于与设计及其权衡相关的部分。


代码和伪代码


设计文档很少包含代码或伪代码,除非描述了新颖的算法。在适当的情况下,可以链接到展示设计可实现性的原型。


约束程度


影响软件设计和设计文档形状的主要因素之一是解决方案空间的约束程度。


在极端情况下,有一个“全新软件项目”,我们只知道目标,解决方案可以是任何最合理的选择。这样的文档可能涉及范围广泛,但也需要快速定义一组规则,以便缩小到可管理的解决方案集。


另一种情况是系统中可能存在非常明确定义的解决方案,但如何将它们结合起来实现目标并不明显。这可能是一个难以更改且未设计为满足您期望功能需求的遗留系统,或者是需要在主机编程语言约束下运行的库设计。


在这种情况下,您可能能够相对容易地列举出所有可以做到的事情,但需要创造性地将这些事物组合起来实现目标。可能会有多个解决方案,并且没有一个真正很好,在识别了所有权衡后该文档应专注于选择最佳方式。


考虑的替代方案


本节列出了其他可能达到类似结果的设计方案。重点应放在每个设计方案所做的权衡以及这些权衡如何导致选择文档主题中所述设计的决策上。


尽管对于最终未被选中的解决方案可以简洁地进行描述,但是这一部分非常重要,因为它明确展示了根据项目目标而选择该解决方案为最佳选项,并且还说明了其他解决方案引入了不太理想的权衡,读者可能会对此产生疑问。


交叉关注点


这是您的组织可以确保始终考虑到安全、隐私和可观察性等特定的交叉关注点的地方。这些通常是相对简短的部分,解释设计如何影响相关问题以及如何解决这些问题。团队应该在他们自己的情况下标准化这些关注点。


由于其重要性,Google项目需要有专门的隐私设计文档,并且还有专门针对隐私和安全进行Review。尽管Review只要求在项目启动之前完成,但最佳实践是尽早与隐私和安全团队合作,以确保从一开始就将其纳入设计中。如果针对这些主题有专门文档,则中央设计文档当然可以引用它们而不详述。


设计文档的长度


设计文档应该足够详细,但又要短到忙碌的人实际上能读完。对于较大的项目来说,最佳页数似乎在10-20页左右。如果超过这个范围,可能需要将问题拆分成更易管理的子问题。还应注意到,完全可以编写一个1-3页的“迷你设计文档”。这对于敏捷项目中的增量改进或子任务尤其有帮助 - 你仍然按照长篇文档一样进行所有步骤,只是保持简洁,并专注于有限的问题集合。


何时不需要编写设计文档


编写设计文档是一种额外的工作量。是否要编写设计文档的决策取决于核心权衡,即组织共识在设计、文档、高级Review等方面的好处是否超过了创建文档所需的额外工作量。这个决策的核心在于解决设计问题是否模糊——因为问题复杂性或解决方案复杂性,或者两者都有。如果不模糊,则通过撰写文档来进行流程可能没有太大价值。


一个明确的指标表明可能不需要文档是那些实际上只是实施手册而非设计文档。如果一个文件基本上说“这就是我们将如何实现它”,而没有涉及权衡、替代方案和解释决策(或者解决方案显然意味着没有任何权衡),那么直接编写程序可能会更好。


最后,创建和Review设计文档所需的开销可能与原型制作和快速迭代不兼容。然而,大多数软件项目确实存在一系列已知问题。遵循敏捷方法论并不能成为对真正已知问题找到正确解决方案时间投入不足的借口。此外,原型制作本身可以是设计文档创建的一部分。“我尝试过,它有效”是选择一个设计方案的最佳论据之一。


设计文档的生命周期


设计文档的生命周期包括以下步骤:


创建和快速迭代
Review(可能需要多轮)
实施和迭代
维护和学习


创作和快速迭代


你撰写文档。有时与一组合著者共同完成。


这个阶段很快进入了一个快速迭代的时间,文档会与那些对问题领域最了解的同事(通常是同一个团队的人)分享,并通过他们提出的澄清问题和建议来推动文档达到第一个相对稳定版本。


虽然你肯定会找到一些工程师甚至团队更喜欢使用版本控制和代码Review工具来创建文档,但在谷歌,绝大多数设计文档都是在Google Docs中创建并广泛使用其协作功能。


Review


在Review阶段,设计文档会与比原始作者和紧密合作者更广泛的受众分享。Review可以增加很多价值,但也是一个危险的开销陷阱,所以要明智地对待。


Review可以采取多种形式:较轻量级的版本是将文档发送给(更广泛的)团队列表,让大家有机会看一下。然后主要通过文档中的评论线程进行讨论。在Review方面较重型的方式是正式的设计评审会议,在这些会议上作者通常通过专门制作的演示文稿向经验丰富、资深工程师们展示该文档内容。谷歌公司许多团队都定期安排了此类会议,并邀请工程师参加审核。自然而然地等待这样的会议可能会显著减慢开发过程。工程师可以通过直接寻求最关键反馈并不阻碍整体审核进度来缓解这个问题。


当谷歌还是一家较小的公司时,通常会将设计发送到一个中央邮件列表,高级工程师会在自己的闲暇时间进行Review。这可能是处理公司事务的好方法。其中一个好处是确立了相对统一的软件设计文化。但随着公司规模扩大到更庞大的工程团队,维持集中式方法变得不可行。


此类Review所添加的主要价值在于它们为组织的集体经验提供了融入设计的机会。最重要的是,在Review阶段可以确保考虑到横切关注点(如可观察性、安全性和隐私)等方面。Review的主要价值并非问题被发现本身,而是这些问题相对早期地在开发生命周期内被发现,并且修改仍然相对廉价。


实施和迭代


当事情进展到足够程度,有信心进一步Review不需要对设计进行重大更改时,就是开始实施的时候了。随着计划与现实的碰撞,不可避免地会出现缺陷、未解决的需求或者被证明错误的猜测,并且需要修改设计。强烈建议在这种情况下更新设计文档。作为一个经验法则:如果设计系统尚未发布,则绝对要更新文档。在实践中,我们人类很擅长忘记更新文件,并且由于其他实际原因,变更通常被隔离到新文件中。这最终导致了一个类似于美国宪法带有一堆修正案而不是一份连贯文档的状态。从原始文档链接到这些修正案可以极大地帮助那些试图通过设计文档考古学来理解目标系统的可怜未来维护程序员们。


维护和学习


当谷歌工程师面对一个之前没有接触过的系统时,他们经常会问:“设计文档在哪里?”虽然设计文档(像所有文档一样)随着时间推移往往与现实脱节,但它们通常是了解系统创建背后思考方式最容易入手的途径。


作为作者,请你给自己一个方便,并在一两年后重新阅读你自己的设计文档。你做得对了什么?你做错了什么?如果今天要做出不同决策,你会怎么选择?回答这些问题是作为一名工程师进步并改善软件设计技能的好方法。


结论


设计文档是在软件项目中解决最困难问题时获得清晰度和达成共识的好方法。它们可以节省金钱,因为避免了陷入编码死胡同而无法实现项目目标,并且可以通过前期调查来避免这种情况;但同时也需要花费时间和金钱进行创建和Review。所以,在选择项目时要明智!


在考虑撰写设计文档时,请思考以下几点:


您是否对正确的软件设计感到不确定,是否值得花费前期时间来获得确定性?


相关地,是否有必要让资深工程师参与其中,即使他们可能无法Review每个代码更改,在设计方面能提供帮助吗?


软件设计是否模糊甚至具有争议性,以至于在组织中达成共识将是有价值的?


我的团队是否有时会忘记在设计中考虑隐私、安全、日志记录或其他横切关注点?


组织中对遗留系统的高层次洞察力提供文档存在强烈需求吗?


如果您对以上3个或更多问题回答“是”,那么撰写一个设计文档很可能是开始下一个软件项目的好方法。


Reference


http://www.industrialempathy.com/posts/desig…


作者:dooocs
来源:juejin.cn/post/7272730352710418447
收起阅读 »

浅谈多人游戏原理和简单实现

一、我的游戏史 我最开始接触游戏要从一盘300游戏的光碟说起,那是家里买DVD送的,《魂斗罗》、《超级马里奥》天天玩。自从买回来后,我就经常和姐姐因为抢电视机使用权而大打出手。有次她把遥控器藏到了沙发的夹层里,被我妈一屁股做成了两半,我和我姐喜提一顿暴打。那顿...
继续阅读 »



一、我的游戏史


我最开始接触游戏要从一盘300游戏的光碟说起,那是家里买DVD送的,《魂斗罗》、《超级马里奥》天天玩。自从买回来后,我就经常和姐姐因为抢电视机使用权而大打出手。有次她把遥控器藏到了沙发的夹层里,被我妈一屁股做成了两半,我和我姐喜提一顿暴打。那顿是我挨得最狠的,以至于现在回想起来,屁股还条件反射的隐隐作痛。


后来我骗我妈说我要学习英语、练习打字以后成为祖国的栋梁之才!让她给我买台小霸王学习机(游戏机),在我一哭二闹三上吊胡搅蛮缠的攻势下,我妈妥协了。就此我接触到了FC游戏。现在还能记得我和朋友玩激龟快打,满屋子的小朋友在看的场景。经常有家长在我家门口喊他家小孩吃饭。那时候我们县城里面有商店卖游戏卡,小卡一张5块钱,一张传奇卡25-40块不等(所谓传奇卡就是角色扮演,带有存档的游戏),每天放学都要去商店去看看,有没有新的游戏卡,买不起,就看下封面过过瘾。我记得我省吃俭用一个多月,买了两张卡:《哪吒传奇》和《重装机兵》那是真的上瘾,没日没夜的玩。


再然后我接触到了手机游戏,记得那时候有个软件叫做冒泡游戏(我心目的中的Stream),里面好多游戏,太吸引我了。一个游戏一般都是几百KB,最大也就是几MB,不过那时候流量很贵,1块钱1MB,并且!一个月只有30Mb。我姑父是收手机的,我在他那里搞到了一部半智能手机,牌子我现在还记得:诺基亚N70,那时候我打开游戏就会显示一杯冒着热气的咖啡,我很喜欢这个图标,因为看见它意味着我的游戏快加载完成了,没想到,十几年后我们会再次相遇,哈哈哈哈。我当时玩了一款网游叫做:《幻想三国》,第一回接触网游简直惊呆了,里面好多人都是其他玩的家,这太有趣了。并且我能在我的手机上看到其他玩家,能够看到他们的行为动作,这太神奇了!!!我也一直思考这到底是怎么实现的!


最后是电脑游戏,单机:《侠盗飞车》、《植物大战僵尸》、《虐杀原型》;网游:《DNF》、《CF》、《LOL》、《梦幻西游》我都玩过。


不过那个疑问一直没有解决,也一值留在我心中 —— 在网络游戏中,是如何实时更新其他玩家行为的呢?


二、解惑


在我进入大学后,我选择了软件开发专业,真巧!再次遇到了那个冒着热气的咖啡图标,这时我才知道它叫做——Java。我很认真的去学,希望有一天能够做一款游戏!


参加工作后,我并没有如愿以偿,我成为了一名Java开发程序员,但是我在日常的开发的都是web应用,接触到大多是HTTP请求,它是种请求-响应协议模式。这个问题也还是想不明白,难道每当其他玩家做一个动作都需要发送一次HTTP请求?然后响应给其他玩家。这样未免效率也太低了吧,如果一个服务器中有几千几万人,那么服务器得承受多大压力呀!一定不是这样的!!!


直到我遇到了Websocket,这是一种长连接,而HTTP是一种短连接,顿时这个问题我就想明白了。在此二者的区别我就不过多赘述了。详细请看我的另一篇文章


知道了这个知识后,我终于能够大致明白了网络游戏的基本原理。原来网络游戏是由客户端服务器端组成的,客户端就是我们下载到电脑或者手机上的应用,而服务器端就是把其他玩家连接起来的中转站,还有一点需要说明的是,网络游戏是分房间的,这个房间就相当于一台服务器。首先,在玩家登陆客户端并选择房间建立长连接后,A玩家做出移动的动作,随即会把这个动作指令上传给服务器,然后服务器再将指令广播到房间中的其他玩家的客户端来操作A的角色,这样就可以实现实时更新其他玩家行为。




三、简单实现


客户端服务端在处理指令时,方法必须是配套的。比如说,有新的玩家连接到服务器,那么服务器就应当向其它客户端广播创建一个新角色的指令,客户端在接收到该指令后,执行客户端创建角色的方法。
为了方便演示,这里需要定义两个HTML来表示两个不同的客户端不同的玩家,这两套客户端代码除了玩家的信息不一样,其它完全一致!!!


3.1 客户端实现步骤


我在这里客户端使用HTML+JQ实现


客户端——1代码:


(1)创建画布

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Game</title>
<style>
canvas {
border: 1px solid black;
}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<canvas id="gameCanvas" width="800" height="800"></canvas>
</body>
</html>

(2)设置1s60帧更新页面

const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function gameLoop() {
clearCanvas();
players.forEach(player => {
player.draw();
});
}
setInterval(gameLoop, 1000 / 60);
//清除画布方法
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}

(3)连接游戏服务器并处理指令


这里使用websocket链接游戏服务器

 //连接服务器
const websocket = new WebSocket("ws://192.168.31.136:7070/websocket?userId=" + userId + "&userName=" + userName);
//向服务器发送消息
function sendMessage(userId,keyCode){
const messageData = {
playerId: userId,
keyCode: keyCode
};
websocket.send(JSON.stringify(messageData));
}
//接收服务器消息,并根据不同的指令,做出不同的动作
websocket.onmessage = event => {
const data = JSON.parse(event.data);
// 处理服务器发送过来的消息
console.log('Received message:', data);
//创建游戏对象
if(data.type == 1){
console.log("玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
console.log("玩家id:"+playerOfIds);
createPlayer(data.players[i].playerId,data.players[i].pointX, data.players[i].pointY, data.players[i].color);
}
}
//销毁游戏对象
if(data.type == 2){
console.log("玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
destroyPlayer(data.players[i].playerId)
}
}
//移动游戏对象
if(data.type == 3){
console.log("移动;玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
players.filter(player => player.id === data.players[i].playerId)[0].move(data.players[i].keyCode)
}
}
};

(4)创建玩家对象

//存放游戏对象
let players = [];
//playerId在此写死,正常情况下应该是用户登录获取的
const userId = "1"; // 用户的 id
const userName = "逆风笑"; // 用户的名称
//玩家对象
class Player {
constructor(id,x, y, color) {
this.id = id;
this.x = x;
this.y = y;
this.size = 30;
this.color = color;
}
//绘制游戏角色方法
draw() {
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.size, this.size);
}
//游戏角色移动方法
move(keyCode) {
switch (keyCode) {
case 37: // Left
this.x = Math.max(0, this.x - 10);
break;
case 38: // Up
this.y = Math.max(0, this.y - 10);
break;
case 39: // Right
this.x = Math.min(canvas.width - this.size, this.x + 10);
break;
case 40: // Down
this.y = Math.min(canvas.height - this.size, this.y + 10);
break;
}
this.draw();
}
}

(5)客户端创建角色方法

//创建游戏对象方法
function createPlayer(id,x, y, color) {
const player = new Player(id,x, y, color);
players.push(player);
playerOfIds.push(id);
return player;
}

(6)客户端销毁角色方法


在玩家推出客户端后,其它玩家的客户端应当销毁对应的角色。

//角色销毁
function destroyPlayer(playId){
players = players.filter(player => player.id !== playId);
}

客户端——2代码:


客户端2的代码只有玩家信息不一致:

  const userId = "2"; // 用户的 id
const userName = "逆风哭"; // 用户的名称

3.2 服务器端


服务器端使用Java+websocket来实现!


(1)引入依赖:

 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.3</version>
</dependency>

(2)创建服务器

@Component
@ServerEndpoint("/websocket")
@Slf4j
public class Server {
/**
* 服务器玩家池
* 解释:这里使用 ConcurrentHashMap为了保证线程安全,不会出现同一个玩家存在多条记录问题
* 使用 static fina修饰 是为了保证 playerPool 全局唯一
*/
private static final ConcurrentHashMap<String, Server> playerPool = new ConcurrentHashMap<>();
/**
* 存储玩家信息
*/
private static final ConcurrentHashMap<String, Player> playerInfo = new ConcurrentHashMap<>();
/**
* 已经被创建了的玩家id
*/
private static ConcurrentHashMap<String, Server> createdPlayer = new ConcurrentHashMap<>();

private Session session;

private Player player;

/**
* 连接成功后调用的方法
*/
@OnOpen
public void webSocketOpen(Session session) throws IOException {
Map<String, List<String>> requestParameterMap = session.getRequestParameterMap();
String userId = requestParameterMap.get("userId").get(0);
String userName = requestParameterMap.get("userName").get(0);
this.session = session;
if (!playerPool.containsKey(userId)) {
int locationX = getLocation(151);
int locationY = getLocation(151);
String color = PlayerColorEnum.getValueByCode(getLocation(1) + 1);
Player newPlayer = new Player(userId, userName, locationX, locationY,color,null);
playerPool.put(userId, this);
this.player = newPlayer;
//存放玩家信息
playerInfo.put(userId,newPlayer);
}
log.info("玩家:{}|{}连接了服务器", userId, userName);
// 创建游戏对象
this.createPlayer(userId);
}

/**
* 接收到消息调用的方法
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException, InterruptedException {
log.info("用户:{},消息{}:",this.player.getPlayerId(),message);
PlayerDTO playerDTO = new PlayerDTO();
Player player = JSONObject.parseObject(message, Player.class);
List<Player> players = new ArrayList<>();
players.add(player);
playerDTO.setPlayers(players);
playerDTO.setType(OperationType.MOVE_OBJECT.getCode());
String returnMessage = JSONObject.toJSONString(playerDTO);
//广播所有玩家
for (String key : playerPool.keySet()) {
synchronized (session){
String playerId = playerPool.get(key).player.getPlayerId();
if(!playerId.equals(this.player.getPlayerId())){
playerPool.get(key).session.getBasicRemote().sendText(returnMessage);
}
}
}
}

/**
* 关闭连接调用方法
*/
@OnClose
public void onClose() throws IOException {
String playerId = this.player.getPlayerId();
log.info("玩家{}退出!", playerId);
Player playerBaseInfo = playerInfo.get(playerId);
//移除玩家
for (String key : playerPool.keySet()) {
playerPool.remove(playerId);
playerInfo.remove(playerId);
createdPlayer.remove(playerId);
}
//通知客户端销毁对象
destroyPlayer(playerBaseInfo);
}

/**
* 出现错误时调用的方法
*/
@OnError
public void onError(Throwable error) {
log.info("服务器错误,玩家id:{},原因:{}",this.player.getPlayerId(),error.getMessage());
}
/**
* 获取随即位置
* @param seed
* @return
*/
private int getLocation(Integer seed){
Random random = new Random();
return random.nextInt(seed);
}
}

websocket配置:

@Configuration
public class ServerConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}


(3)创建玩家对象


玩家对象:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
/**
* 玩家id
*/
private String playerId;
/**
* 玩家名称
*/
private String playerName;
/**
* 玩家生成的x坐标
*/
private Integer pointX;
/**
* 玩家生成的y坐标
*/
private Integer pointY;
/**
* 玩家生成颜色
*/
private String color;
/**
* 玩家动作指令
*/
private Integer keyCode;
}

创建玩家对象返回给客户端DTO:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerDTO {
private Integer type;
private List<Player> players;
}

玩家移动指令返回给客户端DTO:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerMoveDTO {
private Integer type;
private List<Player> players;
}


(4)动作指令

public enum OperationType {
CREATE_OBJECT(1,"创建游戏对象"),
DESTROY_OBJECT(2,"销毁游戏对象"),
MOVE_OBJECT(3,"移动游戏对象"),
;
private Integer code;
private String value;

OperationType(Integer code, String value) {
this.code = code;
this.value = value;
}

public Integer getCode() {
return code;
}

public String getValue() {
return value;
}
}

(5)创建对象方法

  /**
* 创建对象方法
* @param playerId
* @throws IOException
*/
private void createPlayer(String playerId) throws IOException {
if (!createdPlayer.containsKey(playerId)) {
List<Player> players = new ArrayList<>();
for (String key : playerInfo.keySet()) {
Player playerBaseInfo = playerInfo.get(key);
players.add(playerBaseInfo);
}
PlayerDTO playerDTO = new PlayerDTO();
playerDTO.setType(OperationType.CREATE_OBJECT.getCode());
playerDTO.setPlayers(players);
String syncInfo = JSONObject.toJSONString(playerDTO);
for (String key :
playerPool.keySet()) {
playerPool.get(key).session.getBasicRemote().sendText(syncInfo);
}
// 存放
createdPlayer.put(playerId, this);
}
}

(6)销毁对象方法

   /**
* 销毁对象方法
* @param playerBaseInfo
* @throws IOException
*/
private void destroyPlayer(Player playerBaseInfo) throws IOException {
PlayerDTO playerDTO = new PlayerDTO();
playerDTO.setType(OperationType.DESTROY_OBJECT.getCode());
List<Player> players = new ArrayList<>();
players.add(playerBaseInfo);
playerDTO.setPlayers(players);
String syncInfo = JSONObject.toJSONString(playerDTO);
for (String key :
playerPool.keySet()) {
playerPool.get(key).session.getBasicRemote().sendText(syncInfo);
}
}

四、演示


4.1 客户端1登陆服务器




4.2 客户端2登陆服务器




4.3 客户端2移动




4.4 客户端1移动




4.5 客户端1退出



 完结撒花


完整代码传送门


五、总结


以上就是我对网络游戏如何实现玩家实时同步的理解与实现,我实现后心里也释然了,哈哈哈,真的好有趣!!!
我希望大家也是,不要失去好奇心,遇到自己感兴趣的事情,一定要多思考呀~


后来随着我经验的不断积累,我又去了解了一下Java作为游戏服务器的相关内容,发现Netty更适合做这个并且更容易入门,比如《我的世界》一些现有的服务器就是使用Netty实现的。有空也实现下,玩玩~


作者:是江迪呀
链接:https://juejin.cn/post/7273429629398581282
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

往往排查很久的问题,最后发现都非常简单。。。

之前线上发生了一个很诡异的异常,网上各种搜索、排查,都没有找到问题,给大家分享一下。 大概在 2 月份的时候,我们的某个应用整合了中间件的 kafka 客户端,发布到灰度和蓝节点进行观察,然后就发现线上某个 Topic 发生了大量的RetriableCommi...
继续阅读 »

之前线上发生了一个很诡异的异常,网上各种搜索、排查,都没有找到问题,给大家分享一下。


大概在 2 月份的时候,我们的某个应用整合了中间件的 kafka 客户端,发布到灰度和蓝节点进行观察,然后就发现线上某个 Topic 发生了大量的RetriableCommitException,并且集中在灰度机器上。

E20:21:59.770 RuntimeException  org.apache.kafka.clients.consumer.RetriableCommitFailedException  ERROR [Consumer clientId=xx-xx.4-0, groupId=xx-xx-consumer_[gray]] Offset commit with offsets {xx-xx-xx-callback-1=OffsetAndMetadata{offset=181894918, leaderEpoch=4, metadata=''}, xx-xx-xx-callback-0=OffsetAndMetadata{offset=181909228, leaderEpoch=5, metadata=''}} failed org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.TimeoutException: Failed to send request after 30000 ms.


排查


检查了这个 Topic 的流量流入、流出情况,发现并不是很高,至少和 QA 环境的压测流量对比,连零头都没有达到。


但是从发生异常的这个 Topic 的历史流量来看的话,发生问题的那几个时间点的流量又确实比平时高出了很多。



同时我们检查 Broker 集群的负载情况,发现那几个时间点的 CPU 负载也比平时也高出很多(也只是比平时高,整体并不算高)。



对Broker集群的日志排查,也没发现什么特殊的地方。


然后我们对这个应用在QA上进行了模拟,尝试复现,遗憾的是,尽管我们在QA上把生产流量放大到很多倍并尝试了多次,问题还是没能出现。


此时,我们把问题归于当时的网络环境,这个结论在当时其实是站不住脚的,如果那个时刻网络环境发生了抖动的话,其它应用为什么没有这类异常?


可能其它的服务实例网络情况是好的,只是发生问题的这个灰实例网络发生了问题。


那问题又来了,为什么这个实例的其它 Topic 没有报出异常,偏偏问题只出现在这个 Topic 呢?。。。。。。。。。


至此,陷入了僵局,无从下手的感觉。


从这个客户端的开发、测试到压测,如果有 bug 的话,不可能躲过前面那么多环节,偏偏爆发在了生产环境。


没办法了,我们再次进行了一次灰度发布,如果过了一夜没有事情发生,我们就把问题划分到环境问题,如果再次出现问题的话,那就只能把问题划分到我们实现的 Kafka 客户端的问题了。


果不其然,发布后的第二天凌晨1点多,又出现了大量的 RetriableCommitFailedException,只是这次换了个 Topic,并且异常的原因又多出了其它Caused by 。

org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.DisconnectException
...
...
E16:23:31.640 RuntimeException  org.apache.kafka.clients.consumer.RetriableCommitFailedException  ERROR 
...
...
org.apache.kafka.clients.consumer.RetriableCommitFailedException: Offset commit failed with a retriable exception. You should retry committing the latest consumed offsets.
Caused by: org.apache.kafka.common.errors.TimeoutException: The request timed out.

分析


这次出现的异常与之前异常的不同之处在于:

  1. 1. Topic 变了
  2. 2. 异常Cause变了

而与之前异常又有相同之处:

  1. 1. 只发生在灰度消费者组
  2. 2. 都是RetriableCommitFailedException

RetriableCommitFailedException 意思很明确了,可以重试提交的异常,网上搜了一圈后仅发现StackOverFlow上有一问题描述和我们的现象相似度很高,遗憾的是没人回复这个问题:StackOverFlow。


我们看下 RetriableCommitFailedException 这个异常和产生这个异常的调用层级关系。



除了产生异常的具体 Cause 不同,剩下的都是让我们再 retry,You should retry Commiting the lastest consumed offsets。



从调用层级上来看,我们可以得到几个关键的信息,commit 、 async。


再结合异常发生的实例,我们可以得到有用关键信息: 灰度、commit 、async。


在灰度消息的实现上,我们确实存在着管理位移和手动提交的实现。



看代码的第 62 行,如果当前批次消息经过 filter 的过滤后一条消息都不符合当前实例消费,那么我们就把当前批次进行手动异步提交位移。结合我们在生产的实际情况,在灰度实例上我们确实会把所有的消息都过滤掉,并异步提交位移。


为什么我们封装的客户端提交就会报大量的报错,而使用 spring-kafka 的没有呢?


我们看下Spring对提交位移这块的核心实现逻辑。



可以同步,也可以异步提交,具体那种提交方式就要看 this.containerProperties.isSyncCommits() 这个属性的配置了,然而我们一般也不会去配置这个东西,大部分都是在使用默认配置。



人家默认使用的是同步提交方式,而我们使用的是异步方式。


同步提交和异步提交有什么区别么?


先看下同步提交的实现:



只要遇到了不是不可恢复的异常外,在 timer 参数过期时间范围内重试到成功(这个方法的描述感觉不是很严谨的样子)。



我们在看下异步提交方式的核心实现:



我们不要被第 645 行的 RequestFuture future = sendOffsetCommitRequest(offsets) 所迷惑,它其实并不是发送位移提交的请求,它内部只是把当前请求包装好,放到 private final UnsentRequests unsent = new UnsentRequests(); 这个属性中,同时唤醒真正的发送线程来发送的。



这里不是重点,重点是如果我们的异步提交发生了异常,它只是简单的使用 RetriableCommitFailedException 给我们包装了一层。


重试呢?为什么异步发送产生了可重试异常它不给我们自动重试?


如果我们对多个异步提交进行重试的话,很大可能会导致位移覆盖,从而引发重复消费的问题。


正好,我们遇到的所有异常都是 RetriableCommitException 类型的,也就是说,我们把灰度位移提交的方式修改成同步可重试的提交方式,就可以解决我们遇到的问题了。


作者:艾小仙
链接:https://juejin.cn/post/7214398563023274021
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

刚咬了一口馒头,服务器突然炸了!

首先,这个项目是完全使用Websocket开发,后端采用了基于Swoole开发的Hyperf框架,集成了Webscoket服务。 其次,这个项目是我们组第一次尝试使用Socket替换传统的HTTP API请求,前端使用了Vue3,前端同学定义了一套Socket...
继续阅读 »

首先,这个项目是完全使用Websocket开发,后端采用了基于Swoole开发的Hyperf框架,集成了Webscoket服务。
其次,这个项目是我们组第一次尝试使用Socket替换传统的HTTP API请求,前端使用了Vue3,前端同学定义了一套Socket管理器。


看着群里一群“小可爱”疯狂乱叫,我被吵的头都炸了,赶紧尝试定位问题。



  1. 查看是否存在Jenkins发版 -> 无

  2. 查看最新提交记录 -> 最后一次提交是下午,到晚上这段时间系统一直是稳定的

  3. 查看服务器资源,htop后,发现三台相关的云服务器资源都出现闲置状态

  4. 查看PolarDB后,既MySQL,连接池正常、吞吐量和锁正常

  5. 查看Redis,资源正常,无异常key

  6. 查看前端控制台,出现一些报错,但是这些报错经常会变化


  7. 查看前端测试环境、后端测试环境,程序全部正常

  8. 重启前端服务、后端服务、NGINX服务,好像没用,过了5分钟后,咦,好像可以访问了


就在我们组里的“小可爱”通知系统恢复正常后,20分钟不到,再一次处于无法打开的状态,沃焯!!!
完蛋了,找不出问题在哪里了,实在想不通问题究竟出在哪里。


我不服啊,我不理解啊!


咦,Nginx?对呀,我瞅瞅访问日志,于是我浏览了一下access.log,看起来貌似没什么问题,不过存在大量不同浏览器的访问记录,刷的非常快。


再瞅瞅error.log,好像哪里不太对


2023/04/20 23:15:35 [alert] 3348512#3348512: 768 worker_connections are not enough
2023/04/20 23:33:33 [alert] 3349854#3349854: *3492 open socket #735 left in connection 1013

这是什么?貌似是连接池问题,赶紧打开nginx.conf看一下配置


events {
worker_connections 666;
# multi_accept on;
}

???


运维小可爱,你特么在跟我开玩笑?虽然我们这个系统是给B端用的,还是我们自己组的不到100人,但是这连接池给的也太少了吧!


另外,前端为什么会开到 1000 个 WS 连接呢?后端为什么没有释放掉FD呢?


询问后,才知道,前端的Socket管理器,会在连接超时或者其它异常情况下,重新开启一个WS连接。


后端的心跳配置给了300秒


Constant::OPTION_OPEN_WEBSOCKET_PROTOCOL => true, // websocket 协议
Constant::OPTION_HEARTBEAT_CHECK_INTERVAL => 150,
Constant::OPTION_HEARTBEAT_IDLE_TIME => 300,

此时修改nginx.conf的配置,直接拉满!!!


worker_connections 655350;

重启Nginx,哇绰,好像可以访问了,但是每当解决一个问题,总会产生新的问题。


此时error.log中出现了新的报错:


2023/04/20 23:23:41 [crit] 3349767#3349767: accept4() failed (24: Too many open files)

这种就不怕了,貌似和Linux的文件句柄限制有关系,印象中是1024个。
至少有方向去排查,不是吗?而且这已经算是常规问题了,我只需小小的百度一下,哼哼~


拉满拉满!!


worker_rlimit_nofile 65535;

此时再次重启Nginx服务,系统恢复稳定,查看当前连接数:


netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

# 打印结果
TIME_WAIT 1175

FIN_WAIT1 52

SYN_RECV 1

FIN_WAIT2 9

ESTABLISHED 2033

经过多次查看,发现TIME_WAITESTABLISHED都在不断减少,最后完全降下来。


本次问题排查结束,问题得到了解决,但是关于socket的连接池管理器,仍然需要优化,及时释放socket无用的连接。


作者:兰陵笑笑生666
来源:juejin.cn/post/7224314619865923621
收起阅读 »

浅谈多人游戏原理和简单实现

一、我的游戏史 我最开始接触游戏要从一盘300游戏的光碟说起,那是家里买DVD送的,《魂斗罗》、《超级马里奥》天天玩。自从买回来后,我就经常和姐姐因为抢电视机使用权而大打出手。有次她把遥控器藏到了沙发的夹层里,被我妈一屁股做成了两半,我和我姐喜提一顿暴打。那顿...
继续阅读 »

在这里插入图片描述


一、我的游戏史


我最开始接触游戏要从一盘300游戏的光碟说起,那是家里买DVD送的,《魂斗罗》、《超级马里奥》天天玩。自从买回来后,我就经常和姐姐因为抢电视机使用权而大打出手。有次她把遥控器藏到了沙发的夹层里,被我妈一屁股做成了两半,我和我姐喜提一顿暴打。那顿是我挨得最狠的,以至于现在回想起来,屁股还条件反射的隐隐作痛。


后来我骗我妈说我要学习英语、练习打字以后成为祖国的栋梁之才!让她给我买台小霸王学习机(游戏机),在我一哭二闹三上吊胡搅蛮缠的攻势下,我妈妥协了。就此我接触到了FC游戏。现在还能记得我和朋友玩激龟快打,满屋子的小朋友在看的场景。经常有家长在我家门口喊他家小孩吃饭。那时候我们县城里面有商店卖游戏卡,小卡一张5块钱,一张传奇卡25-40块不等(所谓传奇卡就是角色扮演,带有存档的游戏),每天放学都要去商店去看看,有没有新的游戏卡,买不起,就看下封面过过瘾。我记得我省吃俭用一个多月,买了两张卡:《哪吒传奇》和《重装机兵》那是真的上瘾,没日没夜的玩。


再然后我接触到了手机游戏,记得那时候有个软件叫做冒泡游戏(我心目的中的Stream),里面好多游戏,太吸引我了。一个游戏一般都是几百KB,最大也就是几MB,不过那时候流量很贵,1块钱1MB,并且!一个月只有30Mb。我姑父是收手机的,我在他那里搞到了一部半智能手机,牌子我现在还记得:诺基亚N70,那时候我打开游戏就会显示一杯冒着热气的咖啡,我很喜欢这个图标,因为看见它意味着我的游戏快加载完成了,没想到,十几年后我们会再次相遇,哈哈哈哈。我当时玩了一款网游叫做:《幻想三国》,第一回接触网游简直惊呆了,里面好多人都是其他玩的家,这太有趣了。并且我能在我的手机上看到其他玩家,能够看到他们的行为动作,这太神奇了!!!我也一直思考这到底是怎么实现的!


最后是电脑游戏,单机:《侠盗飞车》、《植物大战僵尸》、《虐杀原型》;网游:《DNF》、《CF》、《LOL》、《梦幻西游》我都玩过。


不过那个疑问一直没有解决,也一值留在我心中 —— 在网络游戏中,是如何实时更新其他玩家行为的呢?


二、解惑


在我进入大学后,我选择了软件开发专业,真巧!再次遇到了那个冒着热气的咖啡图标,这时我才知道它叫做——Java。我很认真的去学,希望有一天能够做一款游戏!


参加工作后,我并没有如愿以偿,我成为了一名Java开发程序员,但是我在日常的开发的都是web应用,接触到大多是HTTP请求,它是种请求-响应协议模式。这个问题也还是想不明白,难道每当其他玩家做一个动作都需要发送一次HTTP请求?然后响应给其他玩家。这样未免效率也太低了吧,如果一个服务器中有几千几万人,那么服务器得承受多大压力呀!一定不是这样的!!!


直到我遇到了Websocket,这是一种长连接,而HTTP是一种短连接,顿时这个问题我就想明白了。在此二者的区别我就不过多赘述了。详细请看我的另一篇文章


知道了这个知识后,我终于能够大致明白了网络游戏的基本原理。原来网络游戏是由客户端服务器端组成的,客户端就是我们下载到电脑或者手机上的应用,而服务器端就是把其他玩家连接起来的中转站,还有一点需要说明的是,网络游戏是分房间的,这个房间就相当于一台服务器。首先,在玩家登陆客户端并选择房间建立长连接后,A玩家做出移动的动作,随即会把这个动作指令上传给服务器,然后服务器再将指令广播到房间中的其他玩家的客户端来操作A的角色,这样就可以实现实时更新其他玩家行为。


在这里插入图片描述


三、简单实现


客户端服务端在处理指令时,方法必须是配套的。比如说,有新的玩家连接到服务器,那么服务器就应当向其它客户端广播创建一个新角色的指令,客户端在接收到该指令后,执行客户端创建角色的方法。
为了方便演示,这里需要定义两个HTML来表示两个不同的客户端不同的玩家,这两套客户端代码除了玩家的信息不一样,其它完全一致!!!


3.1 客户端实现步骤


我在这里客户端使用HTML+JQ实现


客户端——1代码:


(1)创建画布


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Canvas Game</title>
<style>
canvas {
border: 1px solid black;
}
</style>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<canvas id="gameCanvas" width="800" height="800"></canvas>
</body>
</html>

(2)设置1s60帧更新页面


const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function gameLoop() {
clearCanvas();
players.forEach(player => {
player.draw();
});
}
setInterval(gameLoop, 1000 / 60);
//清除画布方法
function clearCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}

(3)连接游戏服务器并处理指令


这里使用websocket链接游戏服务器


 //连接服务器
const websocket = new WebSocket("ws://192.168.31.136:7070/websocket?userId=" + userId + "&userName=" + userName);
//向服务器发送消息
function sendMessage(userId,keyCode){
const messageData = {
playerId: userId,
keyCode: keyCode
};
websocket.send(JSON.stringify(messageData));
}
//接收服务器消息,并根据不同的指令,做出不同的动作
websocket.onmessage = event => {
const data = JSON.parse(event.data);
// 处理服务器发送过来的消息
console.log('Received message:', data);
//创建游戏对象
if(data.type == 1){
console.log("玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
console.log("玩家id:"+playerOfIds);
createPlayer(data.players[i].playerId,data.players[i].pointX, data.players[i].pointY, data.players[i].color);
}
}
//销毁游戏对象
if(data.type == 2){
console.log("玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
destroyPlayer(data.players[i].playerId)
}
}
//移动游戏对象
if(data.type == 3){
console.log("移动;玩家信息:" + data.players.length)
for (let i = 0; i < data.players.length; i++) {
players.filter(player => player.id === data.players[i].playerId)[0].move(data.players[i].keyCode)
}
}
};

(4)创建玩家对象


//存放游戏对象
let players = [];
//playerId在此写死,正常情况下应该是用户登录获取的
const userId = "1"; // 用户的 id
const userName = "逆风笑"; // 用户的名称
//玩家对象
class Player {
constructor(id,x, y, color) {
this.id = id;
this.x = x;
this.y = y;
this.size = 30;
this.color = color;
}
//绘制游戏角色方法
draw() {
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.size, this.size);
}
//游戏角色移动方法
move(keyCode) {
switch (keyCode) {
case 37: // Left
this.x = Math.max(0, this.x - 10);
break;
case 38: // Up
this.y = Math.max(0, this.y - 10);
break;
case 39: // Right
this.x = Math.min(canvas.width - this.size, this.x + 10);
break;
case 40: // Down
this.y = Math.min(canvas.height - this.size, this.y + 10);
break;
}
this.draw();
}
}

(5)客户端创建角色方法


//创建游戏对象方法
function createPlayer(id,x, y, color) {
const player = new Player(id,x, y, color);
players.push(player);
playerOfIds.push(id);
return player;
}

(6)客户端销毁角色方法


在玩家推出客户端后,其它玩家的客户端应当销毁对应的角色。


//角色销毁
function destroyPlayer(playId){
players = players.filter(player => player.id !== playId);
}

客户端——2代码:


客户端2的代码只有玩家信息不一致:


  const userId = "2"; // 用户的 id
const userName = "逆风哭"; // 用户的名称

3.2 服务器端


服务器端使用Java+websocket来实现!


(1)引入依赖:


 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.3.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.6.3</version>
</dependency>

(2)创建服务器


@Component
@ServerEndpoint("/websocket")
@Slf4j
public class Server {
/**
* 服务器玩家池
* 解释:这里使用 ConcurrentHashMap为了保证线程安全,不会出现同一个玩家存在多条记录问题
* 使用 static fina修饰 是为了保证 playerPool 全局唯一
*/

private static final ConcurrentHashMap<String, Server> playerPool = new ConcurrentHashMap<>();
/**
* 存储玩家信息
*/

private static final ConcurrentHashMap<String, Player> playerInfo = new ConcurrentHashMap<>();
/**
* 已经被创建了的玩家id
*/

private static ConcurrentHashMap<String, Server> createdPlayer = new ConcurrentHashMap<>();

private Session session;

private Player player;

/**
* 连接成功后调用的方法
*/

@OnOpen
public void webSocketOpen(Session session) throws IOException {
Map<String, List<String>> requestParameterMap = session.getRequestParameterMap();
String userId = requestParameterMap.get("userId").get(0);
String userName = requestParameterMap.get("userName").get(0);
this.session = session;
if (!playerPool.containsKey(userId)) {
int locationX = getLocation(151);
int locationY = getLocation(151);
String color = PlayerColorEnum.getValueByCode(getLocation(1) + 1);
Player newPlayer = new Player(userId, userName, locationX, locationY,color,null);
playerPool.put(userId, this);
this.player = newPlayer;
//存放玩家信息
playerInfo.put(userId,newPlayer);
}
log.info("玩家:{}|{}连接了服务器", userId, userName);
// 创建游戏对象
this.createPlayer(userId);
}

/**
* 接收到消息调用的方法
*/

@OnMessage
public void onMessage(String message, Session session) throws IOException, InterruptedException {
log.info("用户:{},消息{}:",this.player.getPlayerId(),message);
PlayerDTO playerDTO = new PlayerDTO();
Player player = JSONObject.parseObject(message, Player.class);
List<Player> players = new ArrayList<>();
players.add(player);
playerDTO.setPlayers(players);
playerDTO.setType(OperationType.MOVE_OBJECT.getCode());
String returnMessage = JSONObject.toJSONString(playerDTO);
//广播所有玩家
for (String key : playerPool.keySet()) {
synchronized (session){
String playerId = playerPool.get(key).player.getPlayerId();
if(!playerId.equals(this.player.getPlayerId())){
playerPool.get(key).session.getBasicRemote().sendText(returnMessage);
}
}
}
}

/**
* 关闭连接调用方法
*/

@OnClose
public void onClose() throws IOException {
String playerId = this.player.getPlayerId();
log.info("玩家{}退出!", playerId);
Player playerBaseInfo = playerInfo.get(playerId);
//移除玩家
for (String key : playerPool.keySet()) {
playerPool.remove(playerId);
playerInfo.remove(playerId);
createdPlayer.remove(playerId);
}
//通知客户端销毁对象
destroyPlayer(playerBaseInfo);
}

/**
* 出现错误时调用的方法
*/

@OnError
public void onError(Throwable error) {
log.info("服务器错误,玩家id:{},原因:{}",this.player.getPlayerId(),error.getMessage());
}
/**
* 获取随即位置
* @param seed
* @return
*/

private int getLocation(Integer seed){
Random random = new Random();
return random.nextInt(seed);
}
}

websocket配置:


@Configuration
public class ServerConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}


(3)创建玩家对象


玩家对象:


@Data
@AllArgsConstructor
@NoArgsConstructor
public class Player {
/**
* 玩家id
*/

private String playerId;
/**
* 玩家名称
*/

private String playerName;
/**
* 玩家生成的x坐标
*/

private Integer pointX;
/**
* 玩家生成的y坐标
*/

private Integer pointY;
/**
* 玩家生成颜色
*/

private String color;
/**
* 玩家动作指令
*/

private Integer keyCode;
}

创建玩家对象返回给客户端DTO:


@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerDTO {
private Integer type;
private List<Player> players;
}

玩家移动指令返回给客户端DTO:


@Data
@AllArgsConstructor
@NoArgsConstructor
public class PlayerMoveDTO {
private Integer type;
private List<Player> players;
}


(4)动作指令


public enum OperationType {
CREATE_OBJECT(1,"创建游戏对象"),
DESTROY_OBJECT(2,"销毁游戏对象"),
MOVE_OBJECT(3,"移动游戏对象"),
;
private Integer code;
private String value;

OperationType(Integer code, String value) {
this.code = code;
this.value = value;
}

public Integer getCode() {
return code;
}

public String getValue() {
return value;
}
}

(5)创建对象方法


  /**
* 创建对象方法
* @param playerId
* @throws IOException
*/

private void createPlayer(String playerId) throws IOException {
if (!createdPlayer.containsKey(playerId)) {
List<Player> players = new ArrayList<>();
for (String key : playerInfo.keySet()) {
Player playerBaseInfo = playerInfo.get(key);
players.add(playerBaseInfo);
}
PlayerDTO playerDTO = new PlayerDTO();
playerDTO.setType(OperationType.CREATE_OBJECT.getCode());
playerDTO.setPlayers(players);
String syncInfo = JSONObject.toJSONString(playerDTO);
for (String key :
playerPool.keySet()) {
playerPool.get(key).session.getBasicRemote().sendText(syncInfo);
}
// 存放
createdPlayer.put(playerId, this);
}
}

(6)销毁对象方法


   /**
* 销毁对象方法
* @param playerBaseInfo
* @throws IOException
*/

private void destroyPlayer(Player playerBaseInfo) throws IOException {
PlayerDTO playerDTO = new PlayerDTO();
playerDTO.setType(OperationType.DESTROY_OBJECT.getCode());
List<Player> players = new ArrayList<>();
players.add(playerBaseInfo);
playerDTO.setPlayers(players);
String syncInfo = JSONObject.toJSONString(playerDTO);
for (String key :
playerPool.keySet()) {
playerPool.get(key).session.getBasicRemote().sendText(syncInfo);
}
}

四、演示


4.1 客户端1登陆服务器


在这里插入图片描述


4.2 客户端2登陆服务器


在这里插入图片描述


4.3 客户端2移动


在这里插入图片描述


4.4 客户端1移动


在这里插入图片描述


4.5 客户端1退出


在这里插入图片描述
完结撒花


完整代码传送门


五、总结


以上就是我对网络游戏如何实现玩家实时同步的理解与实现,我实现后心里也释然了,哈哈哈,真的好有趣!!!
我希望大家也是,不要失去好奇心,遇到自己感兴趣的事情,一定要多思考呀~


后来随着我经验的不断积累,我又去了解了一下Java作为游戏服务器的相关内容,发现Netty更适合做这个并且更容易入门,比如《我的世界》一些现有的服务器就是使用Netty实现的。有空也实现下,玩玩~


作者:是江迪呀
来源:juejin.cn/post/7273429629398581282
收起阅读 »

向前兼容与向后兼容

2012年3月发布了Go 1.0,随着 Go 第一个版本发布的还有一份兼容性说明文档。该文档说明,Go 的未来版本会确保向后兼容性,不会破坏现有程序。 即用10年前Go 1.0写的代码,用10年后的Go 1.18版本,依然可以正常运行。即较高版本的程序能正常...
继续阅读 »

2012年3月发布了Go 1.0,随着 Go 第一个版本发布的还有一份兼容性说明文档。该文档说明,Go 的未来版本会确保向后兼容性,不会破坏现有程序。


即用10年前Go 1.0写的代码,用10年后的Go 1.18版本,依然可以正常运行。即较高版本的程序能正常处理较低版本程序的数据(代码)


反之则不然,如之前遇到过的这个问题[1]:在Mac上用Go 1.16可正常编译&运行的代码,在cvm服务器上Go 1.11版本,则编译不通过;


再如部署Spring Boot项目[2]时遇到的,在Mac上用Java 17开发并打的jar包,在cvm服务器上,用Java 8运行会报错




一般会认为向前兼容是向之前的版本兼容,这理解其实是错误的。


注意要把「前」「后」分别理解成「前进」和「后退」,不可以理解成「从前」和「以后」


线上项目开发中,向后(后退)兼容非常重要; 向后兼容就是新版本的Go/Java,可以保证之前用老版本写的程序依然可以正常使用




前 forward 未来拓展。


后 backward 兼容以前。







  • 向前兼容(Forward Compatibility):指老版本的软/硬件可以使用或运行新版本的软/硬件产生的数据。“Forward”一词在这里有“未来”的意思,其实翻译成“向未来”更明确一些,汉语中“向前”是指“从前”还是“之后”是有歧义的。是旧版本对新版本的兼容 (即向前 到底是以前还是前面?实际是前面





  • 向上兼容(Upward Compatibility):与向前兼容相同。









  • 向后兼容(Backward Compatibility):指新的版本的软/硬件可以使用或运行老版本的软/硬件产生的数据。是新版本对旧版本的兼容





  • 向下兼容(Downward Compatibility):与向后兼容相同。











软件的「向前兼容」和「向后兼容」如何区分?[3]


参考资料


[1]

这个问题: https://dashen.tech/2021/05/30/gvm-%E7%81%B5%E6%B4%BB%E7%9A%84Go%E7%89%88%E6%9C%AC%E7%AE%A1%E7%90%86%E5%B7%A5%E5%85%B7/#%E7%BC%98%E8%B5%B7

[2]

部署Spring Boot项目: https://dashen.tech/2022/02/01/%E9%83%A8%E7%BD%B2Spring-Boot%E9%A1%B9%E7%9B%AE/

[3]

软件的「向前兼容」和「向后兼容」如何区分?: https://www.zhihu.com/question/47239021



作者:fliter
来源:mdnice.com/writing/b8eb5fdae77f42e897ba69898a58e0d8
收起阅读 »

对负载均衡的全面理解

title: 对负载均衡的全面理解 date: 2021-07-10 21:41:24 tags: TCP/IP 对负载均衡服务(LBS)大名入行不多久就一直听闻,后来的工作中,也了解到 软件负载均衡器,如被合入Linux内核的章文嵩的LVS,还有...
继续阅读 »


title: 对负载均衡的全面理解 date: 2021-07-10 21:41:24 tags: TCP/IP





负载均衡服务(LBS)大名入行不多久就一直听闻,后来的工作中,也了解到 软件负载均衡器,如被合入Linux内核的章文嵩的LVS,还有以应用程序形式出现的HAProxy、KeepAlived,以及更熟悉的Nginx 等


也知道价格高昂的硬件负载均衡器如F5,A10 (甚至搬运过报废的F5)



















但长期以来,也有一些疑惑不解,比如





  • 常说的四层负载均衡是不是就是在传输层实现负载均衡?





  • 四层负载均衡中常听到的三角传输模式IP隧道模式NAT模式,有何区别?哪个性能最好?





  • 四层负载均衡性能好,那为何还有如nginx这样名气更大的七层负载均衡的出现?(Nginx也可以用来做四层代理)





  • 负载均衡与反向代理有何异同?





  • 转发和代理有何本质不同?




这是几年前记的笔记,显然存有谬误。





计算机网络中常见缩略词翻译及简明释要




通读 凤凰架构--负载均衡一章,可知





  • 四层负载均衡 主要工作在第二层和第三层,即 数据链路层和网络层 (通过改写 MAC 地址IP 地址 实现转发)​​​





  • “三角传输模式”(Direct Server Return,DSR),是作用于 数据链路层负载均衡,也称“单臂模式”(Single Legged Mode)或者“直接路由”(Direct Routing)。 通过修改请求数据帧中的 MAC 目标地址,让用户原本是发送给负载均衡器的请求的数据帧,被二层交换机根据新的 MAC 目标地址转发到服务器集群中对应的服务器(“真实服务器”)的网卡上。 效率高性能好,但有些场景不能满足










  • 网络层负载均衡:IP隧道模式,NAT模式


IP隧道模式:





NAT模式:







在流量压力比较大的时候,NAT 模式的负载均衡会带来较大的性能损失,比起直接路由和 IP 隧道模式,甚至会出现数量级上的下降






  • 四层负载均衡进行转发,只有一条TCP通道; 七层负载均衡只能进行代理,需要有两条TCP通道








  • 七层负载均衡器就属于反向代理中的一种;





  • 如果只论网络性能,七层均衡器肯定是无论如何比不过四层均衡器的;但其工作在应用层,可以感知应用层通信的具体内容,往往能够做出更明智的决策,玩出更多的花样来。









负载均衡的两大职责是“选择谁来处理用户请求”和“将用户请求转发过去”。上面讲的都是怎样将用户请求转发过去


至于选择哪台应用服务器来处理用户请求(翻牌子),则有很多算法,如下图就是F5的一些选择算法












B站:一次性讲清楚四层负载均衡中的NAT模式和IP隧道模式


Shadowsocks源码解读——什么是代理?什么是隧道?


NAT模式、路由模式、桥接模式的区别


VLAN是二层技术还是三层技术?


四层负载均衡详解


作者:fliter
来源:mdnice.com/writing/c5b54a9bdd78478a87c6d39e38572358
收起阅读 »

Apipost: 开发者们的瑞士军刀

在当今的数字化时代,数据流通是推动社会进步的关键因素之一。其中,API(应用编程接口)已经成为跨平台数据交互的标准。然而,API开发和管理并非易事,Apipost一体化研发协作赋能平台,支持从API设计到API调试再到API测试覆盖整个API生命周期的API管...
继续阅读 »

在当今的数字化时代,数据流通是推动社会进步的关键因素之一。其中,API(应用编程接口)已经成为跨平台数据交互的标准。然而,API开发和管理并非易事,Apipost一体化研发协作赋能平台,支持从API设计到API调试再到API测试覆盖整个API生命周期的API管理平台,一起来看看Apipost有什么不同吧。


一、Apipost是什么?


Apipost是一个专为开发者设计的API管理工具,提供了全面的API文档生成、调试、测试和分享功能。它的目标是帮助开发者简化API开发流程,提高工作效率。


二、如何使用Apipost?


安装:


进入官网下载安装或直接使用web端



使用:


可以从其他平台如postman导入脚本文件,或创建接口。



接口调试:


输入接口URL后点击发送即可模拟接口请求,上方为请求区,下方为响应区



生成接口文档:


点击分享文档即可生成标准的接口文档,可以将链接分享给需要查看接口的其他同事



一键压测


接口调试完成后可以在一键压测页面进行并发测试,看看接口在高并发情况下的运行情况



总结


Apipost作为一款专为开发者设计的API管理工具,凭借其强大的功能和易用性,已经在开发者社区中积累了良好的口碑。通过使用Apipost,开发者可以节省大量时间,专注于创新和打造卓越的产品。如果你正在寻找一款强大且易用的API管理工具,那么Apipost无疑是一个值得考虑的选择。


作者:刘天瑜
链接:https://juejin.cn/post/7273024616522891301
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

哇咔咔,体验了一把抖音的ChatGPT

说实在的,看到这里,让我忽然想吃过年时候蒸的豆包,不知道字节起这样一个名字有什么用意 最近一直在分享各种AIGC类的东西,感兴趣的可以看下主页历史干货。 你好,我是豆包 礼貌性的回复一句,你好,我是1点东西,我要开始使用你了。豆包是谁呢,可能有些朋友还不知道 ...
继续阅读 »


说实在的,看到这里,让我忽然想吃过年时候蒸的豆包,不知道字节起这样一个名字有什么用意


最近一直在分享各种AIGC类的东西,感兴趣的可以看下主页历史干货。


你好,我是豆包


礼貌性的回复一句,你好,我是1点东西,我要开始使用你了。豆包是谁呢,可能有些朋友还不知道


据悉,“豆包”的前身正是字节内部代号为“Grace”的AI项目。目前在AI浪潮下已经形成独立的AIGC产品供用户使用




刚进来可以看到经典的左右格局,左侧依然是历史问题记录区域,和其他国产GPT产品一样,有一些聚焦的功能模块。


不同的是,显的更加简洁大气,使用柔和不僵硬。毋庸置疑抖音的模型是基于字节产品多年数据沉淀最终服务于子节用户以及更好的发展。



可以看到回答问题响应很快,问题回答干脆不拖泥带水,同样在问题的最下方有点赞、复制、重新生成等功能。需要注意的是最下面有一个搜索功能,点击会跳转到今日头条进行搜索。


体验能力


左侧有英语小助手,先来看下英文能力怎么样




这能力杠杠的,学英语再也不是难事。接下来看全能写作助手体验。






接着问,测试上下文能力。




总体上还算总结的不错,我们问下小日本核废水排海事件




很明显,并不支持联网。




而且没有文生图功能




看下编程能力




编程能力也毫不逊色,最后可以问下GPT3.5都回答错误的问题。看看国产大模型咋样。




OK,今天的一个小分享暂时先到这里。上面的抖音的的申请体验链接:http://www.doubao.com/chat


最近涉猎于AIGC,总结了一些AI资料(实时更新),无套路分享给大家


1点东西AI资料地址



标签:

作者:1点东西
链接:https://juejin.cn/post/7273024681641689149
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

为啥count(*)会这么慢?

背景 本没想着写这篇文章的,因为我觉得这个东西大多数有经验的开发遇到过,肯定也了解过相关的原因,但最近我看到有几个关注的技术公众号在推送相关的文章。实在令我吃惊! 先上公众号文章的结论: count(*) :它会获取所有行的数据,不做任何处理,行数加1。 c...
继续阅读 »

背景


本没想着写这篇文章的,因为我觉得这个东西大多数有经验的开发遇到过,肯定也了解过相关的原因,但最近我看到有几个关注的技术公众号在推送相关的文章。实在令我吃惊!


先上公众号文章的结论:



  • count(*) :它会获取所有行的数据,不做任何处理,行数加1。

  • count(1):它会获取所有行的数据,每行固定值1,也是行数加1。

  • count(id):id代表主键,它需要从所有行的数据中解析出id字段,其中id肯定都不为NULL,行数加1。

  • count(普通索引列):它需要从所有行的数据中解析出普通索引列,然后判断是否为NULL,如果不是NULL,则行数+1。

  • count(未加索引列):它会全表扫描获取所有数据,解析中未加索引列,然后判断是否为NULL,如果不是NULL,则行数+1。


结论:count(*) ≈ count(1) > count(id) > count(普通索引列) > count(未加索引列)


我也不想卖关子了,以上结论纯属放屁。根本就是个人yy出来的东西,甚至不愿意去验证一下,哪怕看一眼执行计划,也得不出这么离谱的结论。


我不敢相信这是一篇被多个技术公众号转载的文章!


以下所有的内容均是基于,mysql 5.7 + InnoDB引擎, 进行的分析。


拓展:


MyISAM 如果没有查询条件,只是简单的统计表中数据总数,将会返回的超快,因为service层中获取到表信息中的总行数是准确的,而InnoDB只是一个估值。


实例


废话不多说,先看一个例子。


以下是一张表数据量有100w,表中字段相对较短,整体数据量不算大。


CREATE TABLE `hospital_statistics_data` (
`pk_id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`id` varchar(36) COLLATE utf8mb4_general_ci NOT NULL COMMENT '外键',
`hospital_code` varchar(36) COLLATE utf8mb4_general_ci NOT NULL COMMENT '医院编码',
`biz_type` tinyint NOT NULL COMMENT '1服务流程 2管理效果',
`item_code` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核项目编码',
`item_name` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核项目名称',
`item_value` varchar(36) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '考核结果',
`is_deleted` tinyint DEFAULT NULL COMMENT '是否删除 0否 1是',
`gmt_created` datetime DEFAULT NULL COMMENT '创建时间',
`gmt_modified` datetime DEFAULT NULL COMMENT 'gmt_modified',
`gmt_deleted` datetime(3) DEFAULT '9999-12-31 23:59:59.000' COMMENT '删除时间',
PRIMARY KEY (`pk_id`)
) DEFAULT CHARSET=utf8mb4 COMMENT='医院统计数据';

此表初始状态只有一个聚簇索引


以下分不同索引情况,看一下COUNT(*)的执行计划。


1)在只有一个聚簇索引的情况下看一下执行计划。


EXPLAIN select COUNT(*) from hospital_statistics_data;

结果:



关于执行计划的各个参数的含义,不在本文的讨论范围内,可自行了解。


这里只关注以下几个属性。



  1. type: 这里显示index,说明使用了索引。

  2. key:PRIMARY使用了主键索引。

  3. key_len: 索引长度8字节。


这里有很关键的一点:count(*)也会走索引,在当前情况下使用了聚簇索引。


好,再往下看。


2)存在一个非聚簇索引(二级索引)


给表添加一个hospital_code索引。


alter table hospital_statistics_data add index idx_hospital_code(hospital_code)

此时表中存在2个索引,主键 hospital_code


同样的,再执行一下:


EXPLAIN select COUNT(*) from hospital_statistics_data;

结果:



同样的,看一下 type、key和key_len三个字段。


是不是觉得有点“神奇”。


为何索引变成刚添加的idx_hospital_code了。


先别急着想结论,再看下面一种情况。


3)存在两个非聚簇索引(二级索引)


在上面的基础上,再添加一个二级索引。


alter table hospital_statistics_data add index idx_biz_type(biz_type)

此时表中存在3个索引,主键 、hospital_code 和 biz_type。


同样的,执行一下:


EXPLAIN select COUNT(*) from hospital_statistics_data;

结果:



是不是更困惑了,索引又..又又...变了.


变成新添加的idx_biz_type。


先不说为何会产生以上的变化,继续往下分析。


在以上3个索引的基础上,分别看一下,count(1)count(id)count(index)count(无索引)


这4种情况,与count(*)的执行计划有何区别。



  1. count(1)




  1. count(id)
    对于样例表来说是,主键是pk_id


image.png



  1. count(index)


这里选取biz_type索引字段。




  1. count(无索引)



小结:




  1. count(index) 会使用当前index指定的索引。




  2. count(无索引) 是全表扫描,未走索引。




  3. count(1) , count(*), count(id) 一样都会选择idx_biz_type索引




看到这,你还觉得那些千篇一律的公众号文章的结论正确吗?


必要知识点




  1. mysql 分为service层引擎层




  2. 所有的sql在执行前会经过service层的优化,优化分为很多类型,简单的来说可分为成本规则




  3. 执行计划所反映的是service层经过sql优化后,可能的执行过程。并非绝对(免得有些人说我只看执行计划过于片面)。绝大多数情况执行计划是可信的




  4. 索引类型分为聚簇索引非聚簇索引(二级索引)。其中数据都是挂在聚簇索引上的,非聚簇索引上只是记录的主键id。




  5. 抛开数据内存,只谈数据量,都是扯淡。什么500w就是极限,什么2个表以上的join都需要优化了,什么is null不会走索引等,纯纯的放屁。




  6. 相信一点,编写mysql代码的人比,看此文章的大部分人都要优秀。他们会尽可能在执行前,对我这样菜逼写的乱七八糟的sql进行优化。




原因分析


其实原因非常非常简单,上面也说了,service层会基于成本进行优化


并且,正常情况下,非聚簇索引所占有的内存要远远小于聚簇索引。所以问题来了,如果你是mysql的开发人员,你在执行count(*)查询的时候会使用那个索引?


我相信正常人都会使用非聚簇索引


那如果存在2个甚至多个非聚簇索引又该如何选择呢?


那肯定选择最短的,占用内存最小的一个呀,在回头看看上面的实例,还迷惑吗。


同样都是非聚簇索引。idx_hospital_codelen146字节;而idx_biz_typelen只有1。那还要选吗?


那为何count(*)走了索引,却还是很慢呢?


这里要明确一点,索引只是提升效率的一种方式,但不能完全的解决效率问题。count(*)有一个明显的缺陷,就是它要计算总数,那就意味着要遍历所有符合条件的数据,相当于一个计数器,在数据量足够大的情况下,即使使用非聚簇索引也无法优化太多。


官方文档:



InnoDBhandlesSELECT COUNT(*)andSELECT COUNT(1)operations in the same way. There is no performance difference.



简单的来说就是,InnoDB下 count(*) 等价于 count(1)


既然会自动走索引,那么上面那个所谓的速度排序还觉得对吗? count(*)的性能跟数据量有很大的关系,此外最好有一个字段长度较短的二级索引。


拓展:


另外,多说一下,关于网上说的那些索引失效的情况,大多都是片面的,我这里只说一点。量变才能引起质变,索引的失效取决于你圈定数据的范围,若你圈定的数据量占整体数据量的比例过高,则会放弃使用索引,反之则会优先使用索引。但是此规则并不是完美的,有时候可能与你预期的不同,也可以通过一些技巧强制使用索引,但这种方式少用。


举个栗子:


通过上面这个表hospital_statistics_data,我进行了如下查询:


select * from hospital_statistics_data where hospital_code is not null;

此时这个sql会使用到hospital_code的索引吗?


这里也不卖关子了,若hospital_code只有很少一部分数据是null值,那么将不会走索引,反之则走索引。


原因就2个字:回表


好比去买砂糖橘,如果你只买几斤,那么你随便挑筐里面好的就行。但是如果你要买一筐,我相信老板不会让你在里面一个个挑,而是一次给你一整筐,当然大家都不傻,都知道筐里里面肯定有那么几个坏果子。但是这样效率最高,而且对老板来说损失更小。


执行过程


摘抄自《从根上理解mysql》。我强烈推荐没有系统学过mysql的,看看这本书。


1.首先在server层维护一个count变量


2.server层向InnoDB引擎要第一条记录


3.InnoDB找到第一条二级索引记录,并返回给server层(注意:由于此时只是统计记录数量,所以并不需要回表)


4.由于COUNT函数的参数是*,MySQL会将*当作常数0处理。由于0并不是NULL,server层给count变量加1。


5.server层向InnoDB要下一条记录。


6.InnoDB通过二级索引记录的next_record属性找到下一条二级索引记录,并返回给server层。


7.server层继续给count变量加1。


8.重复上述过程,直到InnoDB向server层返回没记录可查的消息。


9.server层将最终的count变量的值发送到客户端。


总结


写完后还是心中挺郁闷的,现在能从公众号获取到的好文章越来越少了,现在已经是知识付费的时代了。


挺怀念刚工作的时候,那时候每天上午都花点时间看看公众号文章,现在全都是广告。哎!


不过也正常,谁也不能一直为爱发电。


学习还是建议多看看书籍,一般能成书的都不会太差。现在晚上能搜到的都

作者:微笑兔
来源:juejin.cn/post/7182491131651817531
是千篇一律的文章,对错不知。网上

收起阅读 »

Git 合并冲突不知道选哪个?试试开启 diff3 吧

iOS
导读:Git 早在 2008 年就提供 diff3,用于冲突展示时额外提供该区域的原始内容(两个分支公共祖先节点在此区域的内容),帮助更好的合并冲突。在 2022 年 Q1 发布的 Git 2.35 ,提供了一个新的选项 zdiff3,进一步优化了diff3 ...
继续阅读 »

导读:Git 早在 2008 年就提供 diff3,用于冲突展示时额外提供该区域的原始内容(两个分支公共祖先节点在此区域的内容),帮助更好的合并冲突。在 2022 年 Q1 发布的 Git 2.35 ,提供了一个新的选项 zdiff3,进一步优化了diff3 的展现。



Git 合并冲突,常见的展示形式分为 Current Change (ours, 当前分支的变更)和 Incoming Change (theirs, 目标分支的变更),两者针对的是同一区域的变化。



观察上面这个冲突示例,我们并不清楚两个分支各自都发生了什么变化,有两种可能:

  1. 两个分支同时增加了一行代码 "pkg": xxx
  2. 原先的提交记录里就有 "pkg": xxx ,只是两个分支同时修改了版本号

实际上这个例子,是第二种情况,两个分支都对 pkg 的版本做了改变。




这样的场景还有很多,如果不知道上下文,在解决冲突的时候容易束手束脚。


现在,我们可以使用 git 提供的 diff3 选项来调整合并冲突的展示效果



红框区域(|||||||=======)表示的就是改动前的上下文,确切的说, 当前分支 目标合并分支 的最近公共祖先节点在该区域的内容。


如何开启


冲突展示有两个选项 diff3merge(默认选项),可以通过以下方法进行配置



在 v2.35 新增了 zdiff3 选项,下文会提到

  • 对单个文件开启
git checkout --conflict=diff3 <文件名>
# 示例
git checkout --conflict=diff3 package.json
# 使用默认配置
git checkout --conflict=merge package.json
  • 项目配置
git config merge.conflictstyle diff3
# 删除配置
git config --unset merge.conflictstyle
# 使用默认配置
git config merge.conflictstyle merge
  • 全局配置
git config --global merge.conflictstyle diff3
# 删除配置
git config --global --unset merge.conflictstyle

示例展示


在同一位置添加代码行

<<<<<<< HEAD
import 'some_pkg';
||||||| merged common ancestor
=======
c
>>>>>>> merged-branch

如上示例,合并的公共祖先节点在该位置是空白,每个分支都在相同的位置添加代码行。


我们通常希望保留两者,并按照最有意义的顺序排序,也可能选择只保留其中一个。以下是一个冲突修复后的示例:

import 'some_pkg';
import 'some_pkg';

一方修改一方删除

<<<<<<< HEAD
||||||| merged common ancestor
console.log('调试信息')
=======
console.log('调试信息2')
>>>>>>> merged-branch

如上示例,一方把调试信息删除,而另一方修改了调试信息内容。对于这个示例,我们通常是选择删除而不保留修改。


为什么不是默认选项


经常需要知道祖先节点的内容来确保正确的合并,而 diff3 解决了这个痛点。同时,diff3 没有任何弊端(除了冲突区域行数变多🌝),没有理由不启用它。


那为什么 Git 不将 diff3 作为默认的合并冲突展示选项呢?


stackoverflow 上有人回答了这个问题,大概意思是说可能和 Unix diff 有关,早前默认的 Unix diff 不支持展示 3-way diff (待考证)。


之后的新版本也不方便调整默认值,否则会对用户造成困扰 — “合并冲突区域怎么多了一块内容?”。


zdiff3 (zealous diff3)


2022 年 Q1 ,Git 发布 v2.35,其中有个变化是冲突展示新增了 zdiff3 的配置选项。


zdiff3 基于 diff3 ,并对冲突块两侧的公共代码行做了压缩。


举个例子:




使用默认配置,合并冲突展示如下:

1
2
3
4
A
<<<<<<< ours
B
C
D
=======
X
C
Z
>>>>>>> theirs
E
7
8
9

使用 diff3 后,合并冲突展示如下:

1
2
3
4
<<<<<<
A
B
C
D
E
||||||
5
6
======
A
X
C
Z
E
>>>>>>
7
8
9

通过观察可以发现,冲突区域两侧有公共的代码行 A、E 。而这些代码行在默认配置下会被提取到外部。


而用了 zdiff3 之后,A、E 两行又将移到冲突之外。

1
2
3
4
A
<<<<<<
B
C
D
||||||
5
6
======
X
C
Z
>>>>>>
E
7
8
9

一句话总结 zdiff3 的优化:即展示公共祖先节点内容,又能够充分压缩冲突的公共部分。


最后


解决 Git 合并冲突是一个难题,diff3 并不是一个“银弹”,它只能帮助提供更多的信息,减少决策成本。


推荐读者尝试下 zdiff3 ,至少使用 diff3 ,并将其作为默认配置。


最后,如果看完本文有收获,欢迎一键三连(点赞、收藏、分享)🍻 ~


拓展阅读


作者:francecil
链接:https://juejin.cn/post/7267477916744630308
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Go 负责人说以后不会有 Go2 了

大家好,我是煎鱼。 最近 Go 核心团队负责人 @Russ Cox(下称:rsc)专门写了一篇文章《Backward Compatibility, Go 1.21, and Go 2》为 Go 这门编程语言的 Go1 兼容性增强和 Go2 的情况说明做诠释和宣...
继续阅读 »

大家好,我是煎鱼。


最近 Go 核心团队负责人 @Russ Cox(下称:rsc)专门写了一篇文章《Backward Compatibility, Go 1.21, and Go 2》为 Go 这门编程语言的 Go1 兼容性增强和 Go2 的情况说明做诠释和宣传。


今天希望能够帮助你获悉 Go 未来的规划、方向以及 rsc 的思考。


Go1 破坏兼容性的往事


新增结构体字段


第一个案例,比较经典。在 Go1 的时候,这段代码是可以正常运行的。如下演示代码:

// 脑子进煎鱼了
package main

import "net"

var myAddr = &net.TCPAddr{
net.IPv4(18, 26, 4, 9),
80,
}

但在 Go1.1,这段代码就跑不起来。必须要改成如下代码:

var myAddr = &net.TCPAddr{
IP: net.IPv4(18, 26, 4, 9),
Port: 80,
}

因为在当时的新版本中,对 net.TCPAddr 新增了 Zone 字段。原先的未声明值对应字段的方式就会出现一些问题。


后续在新版本的规范中,官方直接对标准库提交的代码增加了要求,赋值时必须声明字段名。以此避免该问题的产生。


改进排序/压缩的算法实现


第二个案例,Go1.6 时,官方修改了 Sort 的排序实现,使得运行速度提高了 10% 左右。以下是演示代码,将根据名称长度对颜色列表进行排序并输出结果:

colors := strings.Fields(
`black white red orange yellow green blue indigo violet`)
sort.Sort(ByLen(colors))
fmt.Println(colors)

一切听起来是那么的美好。


真实世界是改变排序算法通常会改变相等元素的排序方式。导致了 Go1.5 和 Go1.6 所输出的结果不一致:

Go 1.5:  [red blue green white black yellow orange indigo violet]
Go 1.6: [red blue white green black orange yellow indigo violet]

按照顺序排序后,结果集的差异点在于:


  • Go1.5 返回 green, white, black。
  • Go1.6 返回 white, green, black。

如果说程序依赖了结果集的输出顺序,这将是一个影响不小的兼容性破坏。


第三个案例,类似的还有在 Go1.8 中,官方改进了 compress/flate 的算法,达到了在 CPU 和 Memory 没有什么明显变化下,压缩后的结果集更小了。听起来是个很好的成果。


但实际上自己内部却翻车了,因为 Google 内部有一个需要可重现归档构建的项目,依赖了原有的算法。最后自己 fork 了一份来解决。


Go1.21 起增强兼容性(GODEBUG)


从上面的部分破坏兼容性示例来看,可以知道 Go 官方也不是刻意破坏的。但又存在必然要修改的各种原因和考量。


为此在 Go1.21 起,正式输出了 GODEBUG 的机制,相当于是开了个官方 “后门” 了。将其作为破坏性变更后的门把手。


允许设置 GODEBUG,来开关新功能特性。例如以下选项:

  • GODEBUG=asyncpreemptoff=1:禁用基于信号的 Goroutine 抢占,这偶尔会发现操作系统的错误。
  • GODEBUG=cgocheck=0:禁用运行时的 CGO 指针检查。
  • GODEBUG=cpu.<extension>=off:在运行时禁止使用某个特定的 CPU 扩展。

也会根据根据 go.mod 中的 Go 版本号来设置对应 GODEBUG,以提供版本所约定的 Go1 兼容性保障策略。


如果对这块感兴趣,可以查看《加大力度!Go 将会增强 Go1 向后兼容性》,有完整的增强兼容性的规范说明。


Go2 的情况和规划


Go 官方(via @rsc)正式回答了之前画的饼,也就是什么时候可以看到 Go2 的规范推出,打破 Go1 程序?


答案是永远不会。从与过去决裂、不再编译旧程序的意义上来说,Go 2 永远不会出现。从 Go 在 2017 年开始对 Go 1 进行重大修订的意义上来说,Go 2 已经发生了。


简而言之,透露出来的意思是:硬要说的话,Go2 已经套壳 Go1 上市了。


在未来规划上,不会出现破坏 Go1 程序的 Go2。工作方向会往将加倍努力保证兼容性的基础上,开展新的新工作。


总结


整体上 rsc 对破坏 Go1 兼容性做了很长时间规划的回溯和规划,释出了一大堆手段,例如:GODEBUG、go.mod 版本约束等。


从而引导了 Go2 直接可以借壳上的方向,也更好兑现了 Go1 兼容性保障的规范承诺。单从这方面来讲,还是非常的深思熟虑的。


也可能会有同学说,看 Go 现在这样,说不定下次就变了。这可能比较难,其实 rsc 才上任做团队负责人没几年,工作履历上和其他几位骨干大佬在 Google 已经有非常长年的在职经验了。



我目测一时半会是不会变的了。


想变,得等 Go 核心团队这一班子换了才有可能了。阻力也会很多,因为社区人多,一般会比较注重规范。



文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blo… 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。



Go 图书系列


推荐阅读


作者:煎鱼eddycjy
链接:https://juejin.cn/post/7272378529211744317
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一个上午,我明白了,为什么总说人挣不到认知以外的钱

你好,我是刘卡卡,94年的大厂奶爸程序员,探索副业中 01 接下来,以我昨天上午的一段经历,讲述下为什么说我挣不到认知以外的钱 在昨天上班的路上,我在微信的订阅号推荐里面看到了下图 当时我的想法是:这东西阅读量好高噢,不过养老金和目前的我没什么关系。于是,我...
继续阅读 »

你好,我是刘卡卡,94年的大厂奶爸程序员,探索副业中


01


接下来,以我昨天上午的一段经历,讲述下为什么说我挣不到认知以外的钱


在昨天上班的路上,我在微信的订阅号推荐里面看到了下图



当时我的想法是:这东西阅读量好高噢,不过养老金和目前的我没什么关系。于是,我就划过去了。



(读者可以先停5s 思考下,假设是你看到这张图,会有什么想法)



02


当我坐上工位后,我看到我参加的社群里也人有发了上图,并附上了一段文字:


“养老金类型的公众号容易出爆文。


小白玩转职场这个号,篇篇10w+,而且这并不是一个做了很久的老号,而是今年5月才注册不久的号。 之前这个号刚做的时候是往职场方向发展,所以取名叫小白玩转职场,但是发了一阵后数据不是很好于是就换风格做了养老金的内容。


换到养老金赛道后就几乎篇篇10w+。 这些内容一般从官方网站找就好,选一些内容再加上自己想法稍微改下,或者直接通过Chatgpt辅助,写好标题就行”。


同时,文字下面有社群圈友留下评论说:“这是个好方向啊,虽然公众号文章已经同质化很严重了,但可以往视频号、带货等方向发展”。



读者可以先停5s 思考下,假设是你看到这段文字,会有什么想法。如果你不知道公众号赚钱的模式,那你大概率看不出这段话中的赚钱信息的



我想了想,对噢,确实可以挣到钱,于是将这则信息发到了程序员副业交流的微信群里。



然后,就有群友在交流:“他这是转载还是什么,不可能自己天天写吧”,“这种怎么冷启动呢,不会全靠搜索吧,“做他这种类型的公众号挺多吧,怎么做到每篇10w的”



有没有发现,这3个问题都是关注的怎么做的问题?怎么写的,怎么启动的,怎么每篇10w。


这就是我们大部分人的认知习惯,看到一个信息或别人赚钱的点子后,我们大部分人习惯去思考别人是如何做到的,是不是真的,自己可不可以做到。


可一般来说,我们当下的认知是有限的,大概率是想不出完整的答案的的,想不出来以后,然后就会觉得这个事情有点假,很难或者不适合。从而就错过这条信息了。



我当时觉得就觉得可能大部分群友会错过这则信息了,于是,在群里发了下面这段话


“分享一个点子后


首先去看下点子背后的商业变现机会,如带货/流量主/涨粉/等一系列


而后,才去考虑执行的问题,执行的话


1、首先肯定要对公众号流量主的项目流程进行熟悉


2、对标模仿,可以去把这个公众号的内容全看一看,看看别人为什么起号


3、做出差异化内容。”


发完后还有点小激动(嗯,我又秀了波自己的认知)。可到中午饭点,我发现我还是的认知太低了。


03


在中午吃饭时,我看到亦仁(生财有术的老大)发了这段内容



我被这段话震撼到了,我发现我现在的思考习惯,还是只停留在最表面的看山是山的地步。


我仅仅看到了这张图流量大,没有去思考它为什么这么大流量?



因为他通过精心制作的文章,为老年用户介绍了养老金的方方面面,所以,才会有流量


简单说,因为他满足了用户的需求



同样,我也没有思考还有没有其他产品可以满足用户的这个需求,我仅仅是停留在了视频号和公众号这两个产品砂锅。



只要满足用户需求,就不只有一个产品形态,对于养老金这个信息,我们可以做直播,做课程,做工具,做咨询,做1对1私聊的。这么看,就能有无数的可能



同时,我想到了要做差异化,但没有想到要通过关键字挖掘,去挖掘长尾词。



而亦仁,则直接就挖掘了百万关键字,并无偿分享了。



这才知道,什么叫做看山不是山了。


之前知道了要从“需求 流量 营销 变现”的角度去看待信息,也知道“需求为王”的概念。


可我看到这则信息时,还是没有考虑到需求这一层,更没有形成完整的闭环思路。


因此,以后要在看到这些信息时,去刻意练习“需求 流量 营销 变现”这个武器库,去关注他用什么产品,解决了用户什么需求,从哪里获取到的流量的,怎么做营销的,怎么做变现的。


04


于是,我就把这些思考过程也同样分享到了群里。


接着,下午我就看到有群友在自己的公众号发了篇和养老金相关的文章,虽然文章看上去很粗糙,但至少是起步了。


同时,我也建了个项目交流群,方便感兴趣的小伙伴交流进步(一群人走的更远)


不过我觉得在起步之前,也至少得花一两天时间去调研下,去评估这个需求有哪些地方自己可以切入进去,值不值得切入,能切入的话,怎么切入。


对了,可能你会关心我写了这么多,自己有没有做养老金相关的?


我暂时还没有,因为我目前关心的领域还在出海工具和个人IP上。


全文完结,如果对你有收获的话,关注公众号 刘卡卡 和我一起交流进步


作者:刘卡卡
链接:https://juejin.cn/post/7268590610189418533
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

👨‍💻 14 个最佳免费编程字体

我们整天都在使用代码编辑器、终端和其他开发工具,使用一种让眼睛舒服的字体可以大大改善我们的工作效率。 这篇文章汇总了 14 个免费的等宽编程字体,包括了每个字体的介绍、评价和下载链接。 😝 分享几个好玩的 VSCode 主题 🗂 让你的 VSCode 文件图...
继续阅读 »

我们整天都在使用代码编辑器、终端和其他开发工具,使用一种让眼睛舒服的字体可以大大改善我们的工作效率。
这篇文章汇总了 14 个免费的等宽编程字体,包括了每个字体的介绍、评价和下载链接。


😝 分享几个好玩的 VSCode 主题


🗂 让你的 VSCode 文件图标更好看的10个文件图标主题


🌈 冷门但好看的 VSCode 主题推荐


1. Fira Code




我曾使用 Monaco 字体超过十年的时间,直到我遇到了 Fira Code。这个字体在 Github 上面有超过 53,600 个 star,它这么受欢迎是有原因的。字体作者 Nikita Prokopov 在连字符(Ligature)上花了很多功夫,连字符可以把单独的字符合并成单一的逻辑标记。Fira Code 是我现在最喜欢的字体。




(Fira Code 中的连字符)


下载链接 • Github链接


2. IBM Plex Mono




Plex 系列字体是在 IBM 使用了 50 多年的 Helvetica 字体之后,被创建出来作为替代品的。它有着非常优雅的斜体字体,以及非常清晰易读的字形。美中不足的是,它没有包含连字符。


下载链接 • Github链接


3. Source Code Pro




Source Code Pro 是 Adobe 首先制作的开源字体之一。自2012年发布后,该字体大受欢迎,并被许多开发人员使用。它保留了 Source Sans 的设计特征和垂直比例,但改变了字形宽度,使其在所有粗细中保持一致。


下载链接 • Github链接


4. Monoid


如果你是那种讨厌水平滚动的人,这就是适合你的字体(因为这款字体比较细长)。它针对编程进行了优化,即使在低分辨率的显示器上也有 12px/9pt 的类似位图的清晰度。该字体还有一个名为 Monoisome 的 Font Awesome 集成。


下载链接 • Github链接




5. Hack


Hack 是所有字体中最可定制的之一,拥有1573个字形,你可以自行更改每一个字形的细节。此外,Powerline 字形也包含在其常规字体套件中。


下载链接 • Github链接




6. Iosevka


Iosevka 默认提供了苗条的字体形状:其字形宽度正好为1/2em。相比于其他的字体,你可以在同样的屏幕宽度下放置更多列的文字。它有两种宽度:普通和扩展。如果你希望字体间隔更大一点的话,就选择扩展版本的宽度。


下载链接 • Github链接




7. JetBrains Mono


IntelliJ、WebStorm 等诸多IDE背后的公司 —— JetBrains,在2020年出人意料地推出了自己的字体。他们的字体力求让代码行长度更符合开发人员的期望,使每个字母占据更多的像素。他们在保持字符的宽度标准的基础上最大化了小写字母的高度,从而实现这个目标。


下载链接 • Github链接




8. Fantasque Sans Mono


Fantasque Sans Mono 的设计以实用性为重点,它可以给你的代码增添一丝不一样的感觉。它手写风格的模糊感使其成为一个很酷的选择。


下载链接 • Github链接




9. Ubuntu Mono


这款字体是专门为了补充 Ubuntu 的语气而设计的。它拥有一种现代风格,并具有独特的 Ubuntu 品牌特性,传达出一种精准、可靠和自由的态度。如果你喜欢 Linux,但需要在 Windows 或 MacOS 上工作,这款字体将给你带来一点小小的慰藉和快乐。


下载链接 • 官网




10. Anonymous Pro


这种字体的出色之处在于,它特别区分了那些容易被误认为相同的字符,比如“0”(零)和“O”(大写字母O)。它是一个由四种固定宽度字体组成的字体族,特别针对编程人员而设计。


下载链接 • 官网




11. Inconsolata


这款字体可以作为微软的 Consolas 字体的开源替代。它是一种用于显示代码、终端等使用场景的等宽字体。它提供了连字符,能够给用户出色的编码体验。


下载链接 • GitHub链接




12. Victor Mono


这种字体简洁、清新且细长,具有较大的字母高度和清晰的标点符号,因此易读性强并且适合用于编程。它具有七种不同粗细和 Roman、Italic 和 Oblique 样式。它还提供可选的半连接草书斜体和编程符号连字符。


下载链接 • Github链接




13. Space Mono


这款字体专门为了标题和显示器排版而开发,它拥有几何板块的核心与新颖的近乎过度的合理化形式。它支持拉丁扩展字形集,可以用于英语和其他西欧语言的排版。


下载链接 • GitHub链接




14. Hasklig


在Source Code Pro的基础上,这款字体通过连字符来解决不合适的字符的问题,这也是排版师们一直以来使用的方式。底层代码保持不变——只有表现形式发生变化。


下载链接 • Github链接




哪个字体是你的最爱?


字体,就像颜色主题一样,是一个非常因人而异的话题。不同的开发者喜欢不同的字体。有些人喜欢连词,有些人不喜欢。有些人喜欢斜体字,有些人则讨厌。


希望这篇文章能帮助你找到喜欢的字体,给它一个机会,尝试使用几天,你会有不一样的感觉。



作者:wzkMaster
链接:https://juejin.cn/post/7243067221999894583
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

关于强制加班、人才培养、绩效考核的思考

来源于池老师星球里的一个提问,我也借此机会做一个小归纳,本来想直接贴问题截图的,想了想由于是池老师星球的里的提问,还是不要贴图片了。 大致问题描述:1.公司强制124加班,但是没那么多事情需要加班去做,如何让大家把这些时间利用起来去学习,提高团队能力2.作为研...
继续阅读 »

来源于池老师星球里的一个提问,我也借此机会做一个小归纳,本来想直接贴问题截图的,想了想由于是池老师星球的里的提问,还是不要贴图片了。


大致问题描述:

  • 1.公司强制124加班,但是没那么多事情需要加班去做,如何让大家把这些时间利用起来去学习,提高团队能力
  • 2.作为研发团队管理者绩效考核怎么设计,量化与非量化如何平衡

我的评论原文:


同样的情况我也有碰到,124强制加班,公司跟不上的意思猜测情况是“技术团队支撑业务需要绰绰有余,又或者是业务侧增速不够,总之就是没那么多工作需要加班去完成”


强制加班,至少公司看上去灯火通明的,很多时候是高管或老板要求的,有可能是一些对软件研发理解不足的老板他们需要安慰剂,也有可能是一些“政治原因”。这就不好揣测了,非心腹当然是没法知道,但此时尤为要注意做好管理工作,很多事情没法讲也讲不清楚,团队成员可能因此会对团队、公司失去信心。那么拥有健康的团队氛围,愿意帮助大家成长。规划有长期的团队目标,目标符合公司发展,符合团队成员成长需要,同时要具备一定的挑战性,具备这两点的团队这方面的问题会少很多。团队不可以长期处于磨洋工的状态,如果人心涣散,再聚极难。


研发团队怎么做人才培养:


有加班时间了才想到用这个时间帮助员工成长,之所以有这样的问题是不是平时做人才培养不到位,不够细致。比如项目空窗期的时候之前都是放任大家自由学习或“摸鱼“吗? 从团队管理者角度去看,大家自由学习不能算是好事,很有可能学完了就走了,毕竟你这里没啥挑战。


我的一些经验:


结合公司业务,比如toC 还是toB 去看公司下阶段的规模与增速,分析产研需要达到的能力,以此为基础去看行业内的标准与自己团队的落差,把落差放大一些 作为团队的长期目标,时间上至少是一个季度以上才够大。
这些目标的特点都是重要但不紧急,但具备一定的挑战性。既满足人才培养又能对应未来公司发展需要。


把这些目标作为OKR,分担到各个小组,各个小组再拆落实到个人,并至少最小以月为单位进行复核,协助他们分析解决碰到的阻力问题,同时很多一线同学向上管理做得不好,管理者需要时常主动了解情况,及时给予资源支持。


执行过程难免碰到阻力出现停滞,或速度不理想,那么配合KPI奖励或其他激励来提高成员的驱动力。
在完成的目标过程中,挑一些大里程碑收获拿出来做分享,做沉淀,结合业务做实际应用,大家也能感受到做这些事儿的实际意义,团队信任关系也会越牢固。


KPI设计权衡:


产研团队虽然很难量化指标,但是做一份大家都认可的KPI 是完全可行的。


员工自评+管理者补评只要达到双方的认可,保持公平,公开。


关于KPI第一是考虑清楚KPI是一个奖惩手段,奖惩要分开来看。不然他就单纯变成一把尺子,架在每个人都脖子上,为了KPI而KPI,黑魔法多了去了,比如池老师举过一个例子,为了提高pv,产品经理把一个页面拆成2个,得到2倍pv。


我个人倾向于把它的激励作用放大一些,惩罚效果降低。好的员工有荣辱心,反省能力强,反而是在出事时帮他一把作为激励效果很好。


展开挺复杂,我简单举2个例子表述我的理解:


开发侧好量化的软件交付质量:100分,那么出 bug,分测试阶段,线上阶段,等级从p1-p4 定级,定级之后要对应的扣分标准呢,主动或协助及时修复奖励多少分。这个比较典型,很好量化。


不好量化的沟通协作能力:


"能胜任跨部门沟通协调,高效协作 85~100分


能主导 1 V N 沟通协调 70~85分


能主导 1 V 1 沟通协调 60~70分


沟通能力一般 60分"


这可以用管理者+实例结合来做。员工自评写实例,在什么项目主导协作,这个大家都看得见,管理者再主观的做一些评价,稍微修正一下。


最后,以100为单位为每一项考核做好权重分配,假设考核一共分5项,软件交付质量,设置它的权重为25%,来控制它的影响幅度。


设置权重能很好的适应员工能力梯队,凸显他们各自的优点。


所有人都打完分了,不管是271,28的划分这对管理者都是个头疼的事情,这时候就需要花较多的时间综合考虑,绝对公平太难了,见仁见智了。


我见过的不好的例子就是:给A这个月打高分给B打低分,下个月给B打高分给A打低分,全年下来大家保个平安,上下串通一气,团队一片和谐。


一点题外话,我对设计流程,考核等工作内容有天然的兴趣,但不知道可以以此作为主要能力找工作吗?这会是什么样的工作呢?(我也去问下gpt)


GPT回复:


设计流程和考核等工作内容是非常重要的设计领域的核心能力,如果您对此有天然的兴趣,可以尝试在这方面深入学习和实践,成为一名设计项目经理、设计团队领导、设计流程咨询师等职业。


作为一名设计项目经理,您需要负责整个设计项目的流程管理、协调和监督,确保设计团队能够按时、按质完成设计任务。同时,您需要与客户和其他相关方保持良好的沟通,理解客户需求,并协调解决设计项目中的问题。


作为一名设计团队领导,您需要带领设计团队完成项目任务,并对团队成员进行管理和培训。您需要制定设计流程和考核标准,并监督团队成员按照标准完成任务。


作为一名设计流程咨询师,您需要帮助客户建立有效的设计流程,并优化设计流程中的各个环节,以提高设计效率和质量。您需要了解各种设计软件和工具,并根据客户需求推荐最佳的设计工具和流程。


需要注意的是,设计流程和考核等工作内容是设计领域的重要能力之一,但并不是唯一的能力。如果您想在设计领域发展,还需要具备良好的设计技能和创造力。


emm.....


作者:橘子没了
链接:https://juejin.cn/post/7232120266805493816
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

技术主管是否需要什么段位的技术

今天来跟大家讨论一下技术主管需要什么样段位的技术? 首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然...
继续阅读 »

今天来跟大家讨论一下技术主管需要什么样段位的技术?


首先我要说明的一点,技术主管前提一定是技术出身。对于那些完全不懂技术,但是又身兼技术主管或者总监的同学,我这里就不再赘述,毕竟这个已经超出我目前理解力的范围。比如阿里云的王坚博士,基本上不懂技术细节,但是依然是阿里云的CTO,一手缔造了阿里云。


那我们这里再详细讨论一下,作为一名技术主管,到底应该有什么样的一个技术的段位?或者换句话来说,你的主管的技术水平需要到达什么样的一个水位?


先说结论,作为一名技术主管,一定是整个团队的技术架构师。像其他的一些大家所讨论的条件我觉得都是次要的,比如说写代码的多少,对于技术深度的钻研多少,带的团队人数多少等等,最核心的是技术主管一定要把控整个团队整个业务技术发展的骨架。


为什么说掌控团队技术架构是最重要的?因为对于一个团队来说无非就两点,第一点就是业务价值,第二点就是技术价值。


对于业务价值来说,有各种各样的同学都可以去负责业务上面的一些导向和推进,比如说产品经理,比如说运营同学。技术主管可以在一定程度上去帮助业务成功,甚至是助力业务成功,但是一定要明白技术同学一定要有自己的主轴,就是你对于整个技术的把握。因为业务上的决策说到底技术主管是只能去影响而非去决策,否则就是你们整体业务同学太过拉胯,无法形成战术合力的目的。


对于一线开发同学来说,你只要完成一个接一个的技术项目即可。但是对于技术主管来说,你就要把握整体的技术发展脉络。要清晰的明白什么样的技术架构是和当前的业务匹配的,同时又具备未来业务发展的可扩展性。


那为什么不能把整个技术架构的设计交给某一个核心的骨干研发同学呢?


所以这里就要明白,对于名技术主管来说,未必一定要深刻的钻研技术本身,一定要把技术在业务上的价值发挥到最大。所以在一定程度上来说,可以让适当的同学参与或者主导整个技术架构的设计,但是作为主管必须要了解到所谓的技术投入的产出比是什么。但是如果不对技术架构有一个彻底的理解,如何能决定ROI?



也就是在技术方案的选型里面一定要有一个平衡,能够用最小的技术投入获取到最大的技术利益,而非深究于技术本身的实习方式。如果一名技术主管不了解技术的框架或者某一些主干流程,那么就根本谈不上怎么样去评估这投入的技术产出比。一旦一名技术主管无法衡量整个技术团队的投入产出比,那就意味着整个团队的管理都是在抓虾和浑水摸鱼的状态,这时候就看你团队同学是否自觉了。


出现了这种情况下的团队,可能换一头猪在主管的位置上,业务依然运行良好。如果在业务发展好的时候,可能一直能够顺利推动,你只要坐享其成就可以了,但是一旦到了要突破困难的时期,或者在业务走下行的时候,这个时候你技术上面的优势就一点就没有了。而且在这种情况下,如果你跳槽到其他公司,作为一名技术主管,对方的公司对你的要求也是非常高的,所以这个时候你如果都说不出来你的技术价值对于业务上面的贡献是什么那想当然,你可能大概率就凉凉了。


那问题又回到了什么样的水平才能到达架构师这个话题,可以出来另一篇文章来描述,但是整体上来说,架构的本质首先一定要明白,为的就是业务的增长。


其次,架构的设计其实就是建造一个软件体系的结构,使得具备清晰度,可维护性和可扩展性。另外要想做好架构,基本的基础知识也必不可少,比如说数据库选型、分布式缓存、分库分表、幂等、分布式锁、消息架构、异步架构等等。所以本身来说做好架构师本身难度就非常大,需要长期的积累,实现厚积而薄发。如何成为一名优秀的架构师可以看我的公众号的其他文章,这里就不再详细的介绍了。



第二点是技术主管需要对于技术细节有敏感度。很多人在问一名主管到底应该具备什么样的综合能力,能不能用一种更加形象的方式来概括,我认为就有一句话就可以概括了。技术主管应该是向战略轰炸机在平常的时候一直遨游在大气的最上层能够掌控整个全局,当到了必须要战斗的时候,可以快速的补充下去,定点打击。


我参加过一次TL培训课程,讲师是阿里云智能交付技术部总经理张瑞,他说他最喜欢的一句管理概括,就是“心有猛虎,细嗅蔷薇”,也就是技术主管在平常的时候会关注于更大的宏观战略或策略,也就是注重思考全局,但是在关键的时候一定要关注和落地实际的细节。


换句更加通俗的话来说,就是管理要像战略轰炸机,平常的时候飞在万丈高空巡视,当发生了战斗的时候,立即能够实现定点轰炸。



所以如果说架构上面的设计就是对于整个团队业务和技术骨架的把握,那么对于细节的敏感度就是对于解决问题的落地能力。


那怎么样能够保证你自己有一个技术细节的敏感度?


我认为必要的代码量是需要的,也就是说对于一个主管来说,不必要写太多低代码,但一定要保证一定的代码量,让自己能够最好的,最快的,最贴近实际的理解实际的业务项目。自己写一些代码,其实好处非常多,一方面能够去巩固和加深自己对技术的理解,另外一方面也能够通过代码去更加理解业务。


当然贴近技术的方式有很多种,不一定要全部靠写代码来完成,比如说做code review的方式来完成,做技术方案的评审来完成,这都是可以的。对我来说,我就会强迫自己在每一个迭代会写上一个需求,需求会涉及到各方各面的业务点。有前端的,有后端的,也有数据库设计的。


自己亲自参与写代码或者code review,会让自己更加贴近同学,能够感知到同学的痛点,而不至于只是在空谈说教。


总结


所以对于一个技术主管来说,我认为首要的就是具备架构设计的能力,其次就是要有代码细节的敏感度,对全局和对细节都要有很强大的把控能力。


当然再总结一下,这一套理论只是适用于基础的管理者,而非高层的CTO等,毕竟不同的层级要求的能力和影响力都是不一样的。


作者:ali老蒋
链接:https://juejin.cn/post/7257784425044705340
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

兄弟,王者荣耀的段位排行榜是通过Redis实现的?

在王者荣耀中,我们会打排位赛,而且大家最关注的往往都是你的段位,还有在好友中的排名。 作为程序员的你,思考过吗,这个段位排行榜是怎么实现的?了解它的实现原理,会不会对上分有所帮助? 看看我的排名,你就知道了,答案是否定的,哈哈。 一、排行榜设计方案 从技...
继续阅读 »

在王者荣耀中,我们会打排位赛,而且大家最关注的往往都是你的段位,还有在好友中的排名。


作为程序员的你,思考过吗,这个段位排行榜是怎么实现的?了解它的实现原理,会不会对上分有所帮助?



看看我的排名,你就知道了,答案是否定的,哈哈。




一、排行榜设计方案


从技术角度而言,我们可以根据排行榜的类型来选择不同技术方案来进行排行榜设计。


1、数据库直接排序


在低数据量场景中,用数据库直接排序做排行榜的,有很多。


举个栗子,比如要做一个程序员薪资排行榜,看看哪个城市的程序员最有钱。


根据某招聘网站的数据,2023年中国国内程序员的平均月薪为1.2万元,其中最高的是北京,达到了2.1万元,最低的是西安,只有0.7万元。


以下是几个主要城市的程序员平均月薪排行榜:



  1. 北京:2.1万元

  2. 上海:1.9万元

  3. 深圳:1.8万元

  4. 杭州:1.6万元

  5. 广州:1.5万元

  6. 成都:1.3万元

  7. 南京:1.2万元

  8. 武汉:1.1万元

  9. 西安:0.7万元


从这个榜单中可以看出,我拖了大家的后腿,抱歉了。



这个就可以用数据库来做,一共也没有多少个城市,来个百大,撑死了。


对于这种量级的数据,加好索引,用好top,都不会超过100ms,在请求量小、数据量小的情况下,用数据库做排行榜是完全没有问题的。


2、王者荣耀好友排行


这类榜单是根据自己好友数据来进行排行的,这类榜单不用将每位好友的数据都存储在数据库中,而是通过获取自己的好友列表,获取好友的实时分数,在客户端本地进行本地排序,展现出王者荣耀好友排行榜,因为向数据库拉取数据是需要时间的,比如一分钟拉取一次,因为并非实时拉取,这类榜单对数据库的压力还是较小的。



下面探索一下在Java中使用Redis实现高性能的排行榜是如何实现的?



二、Redis实现计数器


1、什么是计数器功能?


计数器是一种常见的功能,用于记录某种事件的发生次数。在应用中,计数器可以用来跟踪用户行为、统计点击次数、浏览次数等。


例如,您可以使用计数器来记录一篇文章被阅读的次数,或者统计某个产品被购买的次数。通过跟踪计数,您可以了解数据的变化趋势,从而做出更明智的决策。


2、Redis实现计数器的原理


Redis是一款高性能的内存数据库,提供了丰富的数据结构和命令,非常适合实现计数器功能。在Redis中,我们可以使用字符串数据类型以及相关的命令来实现计数器。


(1)使用INCR命令实现计数器


Redis的INCR命令是一个原子操作,用于将存储在键中的数字递增1。如果键不存在,将会创建并初始化为0,然后再执行递增操作。这使得我们可以轻松地实现计数器功能。


让我们通过Java代码来演示如何使用Redis的INCR命令实现计数器:


import redis.clients.jedis.Jedis;

public class CounterExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String articleId = "article:123";
String viewsKey = "views:" + articleId;

// 使用INCR命令递增计数
long views = jedis.incr(viewsKey);

System.out.println("Article views: " + views);

jedis.close();
}
}

在上面的代码中,我们使用了Jedis客户端库来连接Redis服务器,并使用INCR命令递增一个存储在views:article:123键中的计数器。每次执行该代码,计数器的值都会递增,并且我们可以轻松地获取到文章的浏览次数。


(2)使用INCRBY命令实现计数器


除了单次递增1,我们还可以使用INCRBY命令一次性增加指定的数量。这对于一些需要一次性增加较大数量的场景非常有用。


让我们继续使用上面的例子,但这次我们使用INCRBY命令来增加浏览次数:


import redis.clients.jedis.Jedis;

public class CounterExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String articleId = "article:123";
String viewsKey = "views:" + articleId;

// 使用INCRBY命令递增计数
long views = jedis.incrBy(viewsKey, 10); // 一次增加10

System.out.println("Article views: " + views);

jedis.close();
}
}

在上述代码中,我们使用了INCRBY命令将文章浏览次数一次性增加了10。这在统计需要一次性增加较多计数的场景中非常有用。


通过使用Redis的INCRINCRBY命令,我们可以轻松实现高性能的计数器功能。这些命令的原子性操作保证了计数的准确性,而且非常适用于需要频繁更新计数的场景。


三、通过Redis实现“王者荣耀”排行榜?


王者荣耀的排行榜是不是用Redis做的,我不得而知,但,我的项目中,排行榜确实是用Redis做的,这是实打实的。



看见了吗?掌握算法的男人,到哪里都是无敌的。




1、什么是排行榜功能?


排行榜是一种常见的功能,用于记录某种项目的排名情况,通常按照某种规则对项目进行排序。在社交媒体、游戏、电商等领域,排行榜功能广泛应用,可以增强用户的参与度和竞争性。例如,社交媒体平台可以通过排行榜展示最活跃的用户,游戏中可以展示玩家的分数排名等。


2、Redis实现排行榜的原理


在Redis中,我们可以使用有序集合(Sorted Set)数据结构来实现高效的排行榜功能。有序集合是一种键值对的集合,每个成员都与一个分数相关联,Redis会根据成员的分数进行排序。这使得我们能够轻松地实现排行榜功能。


(1)使用ZADD命令添加成员和分数


Redis的ZADD命令用于向有序集合中添加成员和对应的分数。如果成员已存在,可以更新其分数。让我们通过Java代码演示如何使用ZADD命令来添加成员和分数到排行榜:


import redis.clients.jedis.Jedis;

public class LeaderboardExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
String player1 = "PlayerA";
String player2 = "PlayerB";

// 使用ZADD命令添加成员和分数
jedis.zadd(leaderboardKey, 1000, player1);
jedis.zadd(leaderboardKey, 800, player2);

jedis.close();
}
}

在上述代码中,我们使用ZADD命令将PlayerAPlayerB作为成员添加到leaderboard有序集合中,并分别赋予分数。这样,我们就在排行榜中创建了两名玩家的记录。


(2)使用ZINCRBY命令更新成员分数


除了添加成员,我们还可以使用ZINCRBY命令更新已有成员的分数。这在实时更新排行榜中的分数非常有用。


让我们继续使用上面的例子,但这次我们将使用ZINCRBY命令来增加玩家的分数:


import redis.clients.jedis.Jedis;

public class LeaderboardExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
String player1 = "PlayerA";
String player2 = "PlayerB";

// 使用ZINCRBY命令更新成员分数
jedis.zincrby(leaderboardKey, 200, player1); // 增加200分

jedis.close();
}
}

在上述代码中,我们使用了ZINCRBY命令将PlayerA的分数增加了200分。这种方式可以用于记录玩家的得分、积分等变化,从而实时更新排行榜数据。


通过使用Redis的有序集合以及ZADDZINCRBY等命令,我们可以轻松实现高性能的排行榜功能。这些命令的原子性操作保证了排行的准确性和一致性,非常适用于需要频繁更新排行榜的场景。



我的最强百里,12-5-6,这都能输?肯定是哪里出问题了,服务器性能?




四、计数器与排行榜的性能优化


在本节中,我们将重点讨论如何在高并发场景下优化计数器和排行榜功能的性能。通过合理的策略和技巧,我们可以确保系统在处理大量数据和用户请求时依然保持高性能。


1、如何优化计数器的性能?


(1)使用Redis事务


在高并发场景下,多个用户可能同时对同一个计数器进行操作,这可能引发并发冲突。为了避免这种情况,可以使用Redis的事务来确保原子性操作。事务将一组命令包装在一个原子性的操作中,保证这些命令要么全部执行成功,要么全部不执行。


下面是一个示例,演示如何使用Redis事务进行计数器操作:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;

public class CounterOptimizationExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String counterKey = "view_count";
try {
// 开始事务
Transaction tx = jedis.multi();
// 对计数器执行加1操作
tx.incr(counterKey);
// 执行事务
tx.exec();
} catch (JedisException e) {
// 处理事务异常
e.printStackTrace();
} finally {
jedis.close();
}
}
}

在上述代码中,我们使用了Jedis客户端库,通过MULTI命令开启一个事务,然后在事务中执行INCR命令来增加计数器的值。最后,使用EXEC命令执行事务。如果在事务执行期间出现错误,我们可以通过捕获JedisException来处理异常。


(2)使用分布式锁


另一种优化计数器性能的方法是使用分布式锁。分布式锁可以确保在同一时刻只有一个线程能够对计数器进行操作,避免了并发冲突。这种机制可以保证计数器的更新是串行化的,从而避免了竞争条件。


以下是一个使用Redisson框架实现分布式锁的示例:


import org.redisson.Redisson;
import org.redisson.api.RLock;

public class CounterOptimizationWithLockExample {

public static void main(String[] args) {
Redisson redisson = Redisson.create();
RLock lock = redisson.getLock("counter_lock");

try {
lock.lock(); // 获取锁
// 执行计数器操作
} finally {
lock.unlock(); // 释放锁
redisson.shutdown();
}
}
}

在上述代码中,我们使用了Redisson框架来创建一个分布式锁。通过调用lock.lock()获取锁,然后执行计数器操作,最后通过lock.unlock()释放锁。这样可以保证在同一时间只有一个线程能够执行计数器操作。



2、如何优化排行榜的性能?


(1)分页查询


在排行榜中,通常会有大量的数据,如果一次性查询所有数据,可能会影响性能。为了解决这个问题,可以使用分页查询。将排行榜数据分成多个页,每次查询一小部分数据,以减轻数据库的负担。


以下是一个分页查询排行榜的示例:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;

public class LeaderboardPaginationExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
int pageSize = 10; // 每页显示的数量
int pageIndex = 1; // 页码

// 获取指定页的排行榜数据
Set<Tuple> leaderboardPage = jedis.zrevrangeWithScores(leaderboardKey, (pageIndex - 1) * pageSize, pageIndex * pageSize - 1);

for (Tuple tuple : leaderboardPage) {
String member = tuple.getElement();
double score = tuple.getScore();
System.out.println("Member: " + member + ", Score: " + score);
}

jedis.close();
}
}

在上述代码中,我们使用zrevrangeWithScores命令来获取指定页的排行榜数据。通过计算起始索引和结束索引,我们可以实现分页查询功能。


(2)使用缓存


为了进一步提高排行榜的查询性能,可以将排行榜数据缓存起来,减少对数据库的访问。例如,可以使用Redis缓存最近的排行榜数据,定期更新缓存以保持数据的新鲜性。


以下是一个缓存排行榜数据的示例:


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;
import java.util.Set;

public class LeaderboardCachingExample {

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);

String leaderboardKey = "leaderboard";
String cacheKey = "cached_leaderboard";
int cacheExpiration = 300; // 缓存过期时间,单位:秒

// 尝试从缓存中获取排行榜数据
Set<Tuple> cachedLeaderboard = jedis.zrevrangeWithScores(cacheKey, 0, -1);

if (cachedLeaderboard.isEmpty()) {
// 如果缓存为空,从数据库获取数据并更新缓存
Set<Tuple> leaderboardData = jedis.zrevrangeWithScores(leaderboardKey, 0, -1);
jedis.zadd(cacheKey, leaderboardData);
jedis.expire(cacheKey, cacheExpiration);
cachedLeaderboard = leaderboardData;
}

for

(Tuple tuple : cachedLeaderboard) {
String member = tuple.getElement();
double score = tuple.getScore();
System.out.println("Member: " + member + ", Score: " + score);
}

jedis.close();
}
}

在上述代码中,我们首先尝试从缓存中获取排行榜数据。如果缓存为空,我们从数据库获取数据,并将数据存入缓存。使用expire命令来设置缓存的过期时间,以保持数据的新鲜性。


五、实际应用案例


在本节中,我们将通过两个实际的案例,展示如何使用Redis的计数器和排行榜功能来构建社交媒体点赞系统和游戏玩家排行榜系统。这些案例将帮助您更好地理解如何将Redis的功能应用于实际场景中。


1、社交媒体点赞系统案例


(1)问题背景


假设我们要构建一个社交媒体平台,用户可以在文章、照片等内容上点赞。我们希望能够统计每个内容的点赞数量,并实时显示最受欢迎的内容。


(2)系统架构



  • 每个内容的点赞数可以使用Redis的计数器功能进行维护。

  • 我们可以使用有序集合(Sorted Set)来维护内容的排名信息,将内容的点赞数作为分数。


(3)数据模型



  • 每个内容都有一个唯一的标识,如文章ID或照片ID。

  • 使用一个计数器来记录每个内容的点赞数。

  • 使用一个有序集合来记录内容的排名,以及与内容标识关联的分数。


(4)Redis操作步骤



  1. 用户点赞时,使用Redis的INCR命令增加对应内容的点赞数。

  2. 使用ZADD命令将内容的标识和点赞数作为分数添加到有序集合中。


Java代码示例


import redis.clients.jedis.Jedis;

public class SocialMediaLikeSystem {

private Jedis jedis;

public SocialMediaLikeSystem() {
jedis = new Jedis("localhost", 6379);
}

public void likeContent(String contentId) {
// 增加点赞数
jedis.incr("likes:" + contentId);

// 更新排名信息
jedis.zincrby("rankings", 1, contentId);
}

public long getLikes(String contentId) {
return Long.parseLong(jedis.get("likes:" + contentId));
}

public void showRankings() {
// 显示排名信息
System.out.println("Top content rankings:");
jedis.zrevrangeWithScores("rankings", 0, 4)
.forEach(tuple -> System.out.println(tuple.getElement() + ": " + tuple.getScore()));
}

public static void main(String[] args) {
SocialMediaLikeSystem system = new SocialMediaLikeSystem();
system.likeContent("post123");
system.likeContent("post456");
system.likeContent("post123");

System.out.println("Likes for post123: " + system.getLikes("post123"));
System.out.println("Likes for post456: " + system.getLikes("post456"));

system.showRankings();
}
}

在上述代码中,我们创建了一个名为SocialMediaLikeSystem的类来模拟社交媒体点赞系统。我们使用了Jedis客户端库来连接到Redis服务器,并实现了点赞、获取点赞数和展示排名的功能。每当用户点赞时,我们会使用INCR命令递增点赞数,并使用ZINCRBY命令更新有序集合中的排名信息。通过调用zrevrangeWithScores命令,我们可以获取到点赞数排名前几的内容。



2、游戏玩家排行榜案例


(1)问题背景


在一个多人在线游戏中,我们希望能够实时追踪和显示玩家的排行榜,以鼓励玩家参与并提升游戏的竞争性。


(2)系统架构



  • 每个玩家的得分可以使用Redis的计数器功能进行维护。

  • 我们可以使用有序集合来维护玩家的排名,将玩家的得分作为分数。


(3)数据模型



  • 每个玩家都有一个唯一的ID。

  • 使用一个计数器来记录每个玩家的得分。

  • 使用一个有序集合来记录玩家的排名,以及与玩家ID关联的得分。


(4)Redis操作步骤



  1. 玩家完成游戏时,使用Redis的ZINCRBY命令增加玩家的得分。

  2. 使用ZREVRANK命令获取玩家的排名。


(5)Java代码示例


import redis.clients.jedis.Jedis;
import redis.clients.jedis.Tuple;

import java.util.Set;

public class GameLeaderboard {

private Jedis jedis;

public GameLeaderboard() {
jedis = new Jedis("localhost", 6379);
}

public void updateScore(String playerId, double score) {
jedis.zincrby("leaderboard", score, playerId);
}

public Long getPlayerRank(String playerId) {
return jedis.zrevrank("leaderboard", playerId);
}

public Set<Tuple> getTopPlayers(int count) {
return jedis.zrevrangeWithScores("leaderboard", 0, count - 1);
}

public static void main(String[] args) {
GameLeaderboard leaderboard = new GameLeaderboard();
leaderboard.updateScore("player123", 1500);
leaderboard.updateScore("player456", 1800);
leaderboard.updateScore("player789", 1600);

Long rank = leaderboard.getPlayerRank("player456");
System.out.println("Rank of player456: " + (rank != null ? rank + 1 : "Not ranked"));

Set<Tuple> topPlayers = leaderboard.getTopPlayers(3);
System.out.println("Top players:");
topPlayers.forEach(tuple -> System.out.println(tuple.getElement() + ": " + tuple.getScore()));
}
}

在上述代码中,我们创建了一个名为GameLeaderboard的类来模拟游戏玩家排行榜系统。我们同样使用Jedis客户端库来连接到Redis服务器,并实现了更新玩家得分、获取玩家排名和获取排名前几名玩家的功能。使用zincrby命令可以更新玩家的得分,而zrevrank命令则用于


获取玩家的排名,注意排名从0开始计数。通过调用zrevrangeWithScores命令,我们可以获取到排名前几名玩家以及他们的得分。


六、总结与最佳实践


在本篇博客中,我们深入探讨了如何使用Redis构建高性能的计数器和排行榜功能。通过实际案例和详细的Java代码示例,我们了解了如何在实际应用中应用这些功能,提升系统性能和用户体验。让我们在这一节总结Redis在计数器和排行榜功能中的价值,并提供一些最佳实践指南。


1、Redis在计数器和排行榜中的价值


通过使用Redis的计数器和排行榜功能,我们可以实现以下价值:




  • 实时性和高性能:Redis的内存存储和优化的数据结构使得计数器和排行榜功能能够以极高的性能实现。这对于需要实时更新和查询数据的场景非常重要。




  • 用户参与度提升:在社交媒体和游戏等应用中,计数器和排行榜功能可以激励用户参与。通过显示点赞数量或排行榜,用户感受到了更强的互动性和竞争性,从而增加了用户参与度。




  • 数据统计和分析:通过统计计数和排行数据,我们可以获得有价值的数据洞察。这些数据可以用于分析用户行为、优化内容推荐等,从而指导业务决策。




2、最佳实践指南


以下是一些使用Redis构建计数器和排行榜功能的最佳实践指南:




  • 合适的数据结构选择:根据实际需求,选择合适的数据结构。计数器可以使用简单的String类型,而排行榜可以使用有序集合(Sorted Set)来存储数据。




  • 保证数据准确性:在高并发环境下,使用Redis的事务、管道和分布式锁来保证计数器和排行榜的数据准确性。避免并发写入导致的竞争条件。




  • 定期数据清理:定期清理不再需要的计数器和排行数据,以减小数据量和提高查询效率。可以使用ZREMRANGEBYRANK命令来移除排行榜中的过期数据。




  • 适度的缓存:对于排行榜数据,可以考虑添加适度的缓存以提高查询效率。但要注意平衡缓存的更新和数据的一致性。




通过遵循这些最佳实践,您可以更好地应用Redis的计数器和排行榜功能,为您的应

作者:哪吒编程
来源:juejin.cn/post/7271908000414351400
用程序带来更好的性能和用户体验。

收起阅读 »

天涯论坛倒闭,我给天涯续一秒

时代抛弃你,连句招呼都不会打 "时代抛弃你,甚至连句招呼都不会打",成立差不多23年,承载无数人青春回忆的社区就这样悄无声息的落幕了。在那个没有微博抖音的年代,天涯可谓是神一般的存在,当时还没有网络实名制,因此内容包罗万象五花八门,各路大神层出不穷,这里有:盗...
继续阅读 »

时代抛弃你,连句招呼都不会打


"时代抛弃你,甚至连句招呼都不会打",成立差不多23年,承载无数人青春回忆的社区就这样悄无声息的落幕了。在那个没有微博抖音的年代,天涯可谓是神一般的存在,当时还没有网络实名制,因此内容包罗万象五花八门,各路大神层出不穷,这里有:盗走麻花腾qq的黑客大神、高深莫测的民生探讨、波诡云谲的国际形势分析、最前沿最野的明星八卦、惊悚刺激的怪力乱神、脑洞大开的奇人异事 等等,让人眼花缭乱。甚至还有教你在家里养一只活生生的灵宠(见下文玄学类) 


今年4月初,天涯官微发布公告,因技术升级和数据重构,暂时无法访问。可直到现在,网站还是打不开。虽然后来,官微略带戏谑和无奈地表示:“我会回来的”。但其糟糕的财务状况预示着,这次很可能真是,咫尺天涯,永不再见了。 



神奇的天涯


当时还在读大一时候就接触到了 天涯,还记得特别喜欢逛的板块是 "莲蓬鬼话"、"天涯国际"。莲蓬鬼话老用户都知道,主要是一些真真假假的怪力乱神的惊险刺激的事情,比如 有名的双鱼玉佩,还有一些擦边的玩意,比如《风雪漫千山人之大欲》,懂得都懂,这些都在 pdf里面自取😁;天涯国际主要是各路大佬分析国际局势,每每看完总有种感觉 "原来在下一盘大棋",还有各种人生经验 比如kk大神对房产的预测,现在看到貌似还是挺准的。还有教你在家里养一只活生生的灵宠,神奇吧。 总共200+篇,这里先做下简单介绍





关注公众号,回复 「天涯」 海量社区经典文章双手奉上,感受一下昔日论坛的繁华



历史人文类


功底深厚,博古通今,引人入胜,实打实的的拓宽的你的知识面

  • (长篇)女性秘史◆那些风华绝代、风情万种的女人,为你打开女人的所有秘密.pdf
  • 办公室实用暴力美学——用《资治通鉴》的智慧打造职场金饭碗.pdf
  • 《二战秘史》——纵论二战全史——邀你一起与真相贴身肉搏.pdf
  • 不被理解的mzd(密码是123).zip
  • 地缘看世界——欧洲部分-温骏轩.pdf
  • 宝钗比黛玉大八岁!重解红楼全部诗词!血泪文字逐段解释!所有谜团完整公开!.pdf
  • 现代金融经济的眼重看历史-谁是谁非任评说.pdf
  • 蒋介石为什么失掉大陆:1945——1949-flp713.pdf

人生箴言类


开挂一般的人生,有的应该是体制内大佬闲来灌水,那时上网还无需实名

  • 职业如何规划?大城市,小城市,如何抉择?我来说说我的个人经历和思考-鸟树下睡懒觉的猪.pdf
  • kk所有内容合集(506页).pdf
  • 一个潜水多年的体制内的生意人来实际谈谈老百姓该怎么办?.pdf
  • 三年挣850万,你也可以复制!现在新书已出版,书名《我把一切告诉你》.pdf
  • 互联网“裁员”大潮将起:离开的不只是马云 可能还有你.pdf
  • 大鹏金翅明王合集.pdf
  • 解密社会中升官发财的经济学规律-屠龙有术.pdf

房产金融


上帝视角,感觉有的可能是参与制定的人

  • 从身边最简单的经济现象判断房价走势-招招是道.pdf
  • 大道至简,金融战并不复杂。道理和在县城开一个赌场一样。容我慢慢道来-战略定力.pdf
  • 沉浮房产十余载,谈房市心得.pdf
  • 现代金融的本质以及房价-curgdbd.pdf
  • 对当前房地产形势的判断和对一些现象的解释-loujinjing.pdf
  • 中国VS美国:决定世界命运的博弈 -不要二分法 .pdf
  • 大江论经济-大江宁静.pdf
  • 形势转变中,未来哪些行业有前景.pdf
  • 把握经济大势和个人财运必须读懂钱-现代金钱的魔幻之力.pdf
  • 烽烟四起,中美对决.pdf
  • 赚未来十年的钱-王海滨.pdf
  • 一个炒房人的终极预测——调控将撤底失败.pdf

故事连载小说类


小说爱好者的天堂,精彩绝伦不容错过

  • 人之大欲,那些房中术-风雪漫千山.pdf
  • 冒死记录中国神秘事件(真全本).pdf 五星推荐非常精彩
  • 六相十年浩劫中的灵异往事,颍水尸媾,太湖獭淫,开封鬼谷,山东杀坑-御风楼主人.pdf
  • 《内参记者》一名“非传统”记者颠覆你三观的采访实录-有骨难画.pdf
  • 中国式骗局大全-我是骗子他祖宗.pdf
  • 我是一名警察,说说我多年来破案遇到的灵异事件.pdf
  • 一个十年检察官所经历的无数奇葩案件.pdf
  • 宜昌鬼事 (三峡地区巫鬼轶事记录整理).pdf
  • 南韩往事——华人黑帮回忆录.pdf
  • 惊悚灵异《青囊尸衣》(斑竹推荐)-鲁班尺.pdf
  • 李幺傻江湖故事之《戚绝书》(那些湮没在岁月深处的江湖往事)-我是骗子他祖宗.pdf
  • 闲来8一下自己幽暗的成长经历-风雪漫千山.pdf
  • 阴阳眼(1976年江汉轶事).pdf
  • 民调局异闻录-儿东水寿.pdf
  • 我当道士那些年.pdf
  • 目睹殡仪馆之奇闻怪事.pdf

玄学类


怪力乱神,玄之又玄,虽然已经要求建国后不许成精了

  • 请块所谓的“开光”玉,不如养活的灵宠!.pdf
  • 写在脸上的风水-禅海商道.pdf
  • 谶纬、民谣、推背图-大江宁静.pdf
  • 拨开迷雾看未来.pdf
  • 改过命的玄教弟子帮你断别你的网名吉凶-大雨小水.pdf

天涯的败落


内容社区赚钱,首先还是得有人气,这是互联网商业模式的基础。天涯在PC互联网时代,依靠第一节说的几点因素,持续快速的吸引到用户,互联网热潮,吸引了大量的资本进入,作为有超高流量的天涯社区,自然也获得了资本的青睐。营收这块,主要分为两个部分:网络广告营销业务和互联网增值业务收入。广告的话,最大的广告主是百度,百度在2015年前5个月为天涯社区贡献了476万元,占总收入的比重达11.24%;百度在2014年为天涯社区贡献收入1328万元,占比12.76%。广告收入严重依赖于流量,天涯为了获得广告营收,大幅在社区内植入广告位,影响了用户体验,很有竭泽而渔的感觉。 但是在进入移动互联网时代没跟上时代步伐, 

2010年底,智能手机的出货量开始超过PC,另外,移动互联网走的是深度垂直创新,天涯还是大而全的综合社区模式,加上运营也不是很高明,一两个没工资的版主,肯定打不过别人公司化的运作,可以看到在细分领域被逐步蚕食:

  • 新闻娱乐,被**「微博、抖音」**抢走;
  • 职场天地,被**「Boss直聘」**抢走;
  • 跳蚤市场,被**「闲鱼、转转」**抢走;
  • 音乐交友,被**「网易云、qq音乐」**抢走;
  • 女性兴趣,被**「小红书」**抢走,等等

强如百度在移动互联网没占到优势,一直蛰伏到现在,在BAT中名存实亡,何况天涯,所以也能理解吧。"海内存知己,天涯若比邻",来到2023年,恐怕只剩物是人非,变成一个被遗忘的角落,一段被尘封的回忆罢了,期待天涯能够度过难关再度重来吧。


作者:Android茶话会
链接:https://juejin.cn/post/7245594280303165497
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

介绍一款CPP代码bug检测神器

最近使用C++开发的比较多,对于C++开发来说,内存管理是一个绕不开的永恒话题,因此在使用C++特别是指针时经常是慎之又慎, 最怕一不小心就给自己挖了一个坑,刚好最近发现了一个特别好用的C++静态代码分析利器,借助这个强大的分析工具,我们可以很好地将一些空指针...
继续阅读 »

最近使用C++开发的比较多,对于C++开发来说,内存管理是一个绕不开的永恒话题,因此在使用C++特别是指针时经常是慎之又慎,
最怕一不小心就给自己挖了一个坑,刚好最近发现了一个特别好用的C++静态代码分析利器,借助这个强大的分析工具,我们可以很好地将一些空指针,
数组越界等一些常见的bug扼杀在萌芽阶段,正所谓独乐了不如众乐乐,特将这个利器分享给大家。


这个利器就是cppcheck,它的官网是:cppcheck.sourceforge.io/


同时它还提供了在线分析的功能:cppcheck.sourceforge.io/demo/


在这个在线分析工具中,我们只需要将我们需要检测的代码拷贝粘贴到输入框,然后点击Check按钮即可进行代码分析。


当然啦,这个在线分析还是有很多不足的,比如最多只支持1024个字符,无法在线分析多文件,大项目等,如果要分析长文件,甚至是大项目,那就得安装本地使用啦,
下面我们就以CLion为例,简单介绍下cppcheck的安装和使用。


插件cppcheck的安装


首先这个强大的分析工具是有一个CLion插件的,它的连接是:plugins.jetbrains.com/plugin/8143…


我们可以直接在这个地址上进行在线安装,也可以在CLion的插件市场中搜索cppcheck进行安装。


需要注意的是这个插件仅仅是为了在CLion中自动帮我们分析项目代码,它是不包含cppcheck功能的,也就是要让这个插件正常工作,我们还得
手动安装cppcheck,然后在CLion配置好cppcheck的可执行文件的路径才行。


关于这个cppcheck核心功能的安装官网已经说得很清楚,也就是一句命令行的事情。


比如Debian系统可以通过一下命令安装:

sudo apt-get install cppcheck

Fedora的系统可以通过以下命令安装:

sudo yum install cppcheck

至于Mac系统,那肯定就是用神器包管理工具Homebrew进行安装啦:

brew install cppcheck

CLion插件配置cppcheck路径


安装好cppcheck核心工具包和CLion的cppcheck插件工具之后,我们只需要在CLion中配置一下cppcheck工具包的安装路径就可以正常使用啦。


以笔者的Mac系统的CLion为例子,打开CLion后,点击CLion-> Settings -> Other Settings -> Cppcheck Configuration



在弹出框中设置好cppcheck安装包的绝对路径即可。


如果你是使用Homebrew安装的话可以通过命令brew info cppcheck查找到cppcheck安装的具体路径。


功能实测


为了检测cppcheck这个分析工具的功能,我们新建了一个工程,输入以下代码:

void foo(int x)
{
int buf[10];
if (x == 1000)
buf[x] = 0; // <- ERROR
}

int main() {
int y[1];
y[2] = 1;
return 0;
}

当我们没有安装cppcheck插件时,它是这样子的,看起来没什么问题:



当我们安装了cppcheck插件之后,对于可能会发生潜在的空指针、数组越界、除数为0等等可能导致bug的地方会有高亮提示,
给人一看就有问题的感觉:



当然啦,cppcheck的功能远比这个这个例子所展示的强大,更多惊喜欢迎大家使用体验。


工具是智慧的延伸,在开发的过程选择适合你的工具,可以让我们的工作事半功倍,同行的你如果有好的开发辅助工具欢迎留言分享...


系统话学习博文推荐


音视频入门基础

C++进阶

NDK学习入门

安卓camera应用开发

ffmpeg系列

Opengl入门进阶

webRTC


关注我,一起进步,有全量音视频开发进阶路径、资料、踩坑记等你来学习...


作者:思想觉悟
链接:https://juejin.cn/post/7270208019388350524
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

你知道什么是SaaS吗?

天天听SaaS,相信大家都知道什么叫SaaS系统!这不?领导安排下来了任务,说要去做SaaS系统,作为小白的我赶紧去看看什么是SaaS,大概收集整理(并非原创)了这部分内容,分享给大家。相信大家看了也会有很多收获。 本文从以下几个方面对SaaS系统召开介绍...
继续阅读 »

天天听SaaS,相信大家都知道什么叫SaaS系统!这不?领导安排下来了任务,说要去做SaaS系统,作为小白的我赶紧去看看什么是SaaS,大概收集整理(并非原创)了这部分内容,分享给大家。相信大家看了也会有很多收获。


  本文从以下几个方面对SaaS系统召开介绍:


  1. 云服务架构的三个概念

  2. SaaS系统的两大特征

  3. SaaS服务与传统服务、互联网服务的区别

  4. B2B2C

  5. SaaS系统的分类

  6. 如何SaaS化

  7. SaaS产品的核心组件

  8. SaaS多租户


一、云服务架构的三个概念


1.1 PaaS


英文就是 Platform-as-a-Service(平台即服务)


PaaS,某些时候也叫做中间件。就是把客户采用提供的开发语言和工具(例如Java,python, .Net等)开发的或收购的应用程序部署到供应商的云计算基础设施上去。
客户不需要管理或控制底层的云基础设施,包括网络、服务器、操作系统、存储等,但客户能控制部署的应用程序,也可能控制运行应用程序的托管环境配置。


PaaS 在网上提供各种开发和分发应用的解决方案,比如虚拟服务器和特定的操作系统。底层的平台3/4帮你铺建好了,你只需要开发自己的上层应用。这即节省了你在硬件上的费用,也让各类应用的开发更加便捷,不同的工作互相打通也变得容易,因为在同一平台上遵循的是同样的编程语言、协议和底层代码。


1.2 IaaS


英文就是 Infrastructure-as-a-Service(基础设施即服务)


IaaS 提供给消费者的服务是对所有计算基础设施的利用,包括处理 CPU、内存、存储、网络和其它基本的计算资源,用户能够部署和运行任意软件,包括操作系统和应用程序。
消费者不管理或控制任何云计算基础设施,但能控制操作系统的选择、存储空间、部署的应用,也有可能获得有限制的网络组件(例如路由器、防火墙、负载均衡器等)的控制。


IaaS 会提供场外服务器,存储和网络硬件,你可以租用。节省了维护成本和办公场地,公司可以在任何时候利用这些硬件来运行其应用。我们最熟悉的IaaS服务是我们服务器托管业务,多数的IDC都提供这样的服务,用户自己不想要再采购价格昂贵的服务器和磁盘阵列了,所有的硬件都由 IaaS 提供,你还能获得品质更高的网络资源。


1.3 SaaS


英文就是 Software-as-a-Service(软件即服务)


SaaS提供给客户的服务是运行在云计算基础设施上的应用程序,用户可以在各种设备上通过客户端界面访问,如浏览器。
消费者不需要管理或控制任何云计算基础设施,包括网络、服务器、操作系统、存储等等。


SaaS 与我们普通使用者联系可能是最直接的,简单地说任何一个远程服务器上的应用都可以通过网络来运行,就是SaaS了。国内的互联网巨头竭力推荐的 SaaS 应用想必大家已经耳熟能详了,比如阿里的钉钉,腾讯的企业微信,这些软件里面应用平台上的可供使用的各类SaaS小软件数不胜数,从OA,到ERP到CRM等等,涵盖了企业运行所需的几乎所用应用。


二、SaaS系统的两大特征



  1. 部署在供应商的服务器上,而不是部署在甲方的服务器上。

  2. 订购模式,服务商提供大量功能供客户选择,客户可以选择自己需要的进行组合,支付所需的价格,并支持按服务时间付费。


三、SaaS服务与传统服务、互联网服务的区别


3.1 SaaS服务


介于传统与互联网之间,通过租用的方式提供服务,服务部署在云端,任何用户通过注册后进行订购后获得需要的服务,可以理解成服务器及软件归供应商所有,用户通过付费获得使用权
image.png


3.2 传统软件


出售软件及配套设备,将软件部署在客户服务器或客户指定云服务器,出售的软件系统及运维服务为盈利来
image.png


3.3 互联网应用供应商


服务器部署在云端,所有用户可以通过客户端注册进行使用,广告及付费增值服务作为盈利来源
image.png


四、B2B2C


SaaS作为租户系统,需要为租户(C端)提供注册、购买、业务系统的入口,还得为B端(运营/运维)提供租户管理、流量监控、服务状态监控运维入口


五、SaaS系统的分类


5.1 业务型SaaS


定义:为客户的赚钱业务提供工具以及服务的SaaS,直面的是用户的生意,例如有赞微盟等电商SaaS以及销售CRM工具,为B2B2C企业;


架构以及商业模式:在产品的成长期阶段,为了扩充业务规模和体量,业务SaaS产品会拓展为“多场景+多行业”的产品模式,为不同行业或者不同场景提供适应的解决方案,例如做电商独立站的有赞,后期发展为“商城、零售、美业、教育”多行业的解决方案进行售卖。
image.png


5.2 效率型SaaS


定义:为客户效率提升工具的SaaS,如项目管理工具、Zoom等会议工具,提升办公或者生产效率,为B2B企业;


架构以及商业模式:不同于业务型的SaaS,效率SaaS思考得更多的是企业内存在一个大共性的效率的问题,不同的企业对于CRM销售系统的需求是不一样的,但都需要一个协同办公的产品来提升协作效率。对于效率类SaaS来说,从哪来到哪去是非常清晰的,就是要解决优化或者解决一个流程上的问题。
image.png


5.3 混合型SaaS


定义:即兼顾企业业务和效率效用SaaS,例如近几年在私域流量上大做文章的企业微信,其本身就是一个办公协同工具,但为企业提供了一整套的私域管理能力,实现业务的提升,同时也支持第三方服务。


架构以及商业模式:混合SaaS是业务和效率SaaS的结合体,负责企业业务以及企业管理流程的某类场景上的降本增效;因混合SaaS核心业务的使用场景是清晰且通用的,非核心业务是近似于锦上添花的存在,所以在中台产品架构上更接近为“1+X”组合方式——即1个核心业务+X个非核心功能,两者在产品层级上是属于同一层级的。
image.png


六、如何SaaS化



  1. 进行云化部署,性能升级,能够支持更大规模的用户访问

  2. 用户系统改造,支持2C用户登录(手机号一键登录、小程序登录、短信验证码登录)

  3. 网关服务,限流,接口防篡改等等

  4. 租户系统开发,包含租户基础信息管理、租户绑定资源(订购的功能)、租户服务期限等等

  5. 客户端改造(通常SaaS系统主要提供WEB端服务),页面权限控制,根据租户系统用户资源提供用户已购买的模块或页面

  6. 官网开发,功能报价单,功能试用、用户选购及支付

  7. 服务端接口数据权限改造、租户级别数据权限


七、SaaS产品的核心组件



  1. 安全组件:在SaaS产品中,系统安全永远是第一位需要考虑的事情

  2. 数据隔离组件:安全组件解决了用户数据安全可靠的问题,但数据往往还需要解决隐私问题,各企业之间的数据必须相互不可见,即相互隔离。

  3. 可配置组件:SaaS产品在设计之初就考虑了大多数通用的功能,让租户开箱即用,但任然有为数不少的租户需要定制服务自身业务需求的配置项,如UI布局、主题、标识(Logo)等信息

  4. 可扩展组件:SaaS产品应该具备水平扩展的能力。如通过网络负载均衡其和容器技术,在多个服务器上部署多个软件运行示例并提供相同的软件服务,以此实现水平扩展SaaS产品的整体服务性能

  5. 0停机时间升级产品:实现在不重启原有应用程序的情况下,完成应用程序的升级修复工作

  6. 多租户组件:SaaS产品需要同时容纳多个租户的数据,同时还需要保证各租户之间的数据不会相互干扰,保证租户中的用户能够按期望索引到正确的数据


八、SaaS多租户


8.1 多租户核心概念



  • 租户:一般指一个企业客户或个人客户,租户之间数据与行为是隔离的

  • 用户:在某个租户内的具体使用者,可以通过使用账户名、密码等登录信息,登录到SaaS系统使用软件服务

  • 组织:如果租户是一个企业客户,通常会拥有自己的组织架构

  • 员工:是指组织内部具体的某位员工。

  • 解决方案:为了解决客户的某类型业务问题,SaaS服务商将产品与服务组合在一起,为商家提供整体的打包方案。

  • 产品能力:指的是SaaS服务商对客户售卖的产品应用,特指能够帮助客户实现端到端场景解决方案闭环的能力。

  • 资源域:用来运行1个或多个产品应用的一套云资源环境

  • 云资源:SaaS产品一般都部署在各种云平台上,例如阿里云、腾讯云、华为云等。对这些云平台提供的计算、存储、网络、容器等资源,抽象为云资源。


8.2 三大模式


8.2.1 竖井隔离模式


image.png



  • 优势:



  1. 满足强隔离需求:一些客户为了系统和数据的安全性,可能提出非常严格的隔离需求,期望软件产品能够部署在一套完全独立的环境中,不和其他租户的应用实例、数据放在一起。

  2. 计费逻辑简单:SaaS服务商需要针对租户使用资源进行计费,对于复杂的业务场景,计算、存储、网络资源间的关系同样也会非常复杂,计费模型是很有挑战的,但在竖井模式下,计费模型相对来说是比较简单的。

  3. 降低故障影响面:因为每个客户的系统都部署在自己的环境中,如果其中一个环境出现故障,并不会影响其他客户使用软件服务。



  • 劣势:



  1. 规模化问题:由于租户的SaaS环境是独立的,所以每入驻一个租户,就需要创建和运营一套SaaS环境,如果只是少量的租户,还可能可以管理,但如果是成千上万的租户,管理和运营这些环境将会是非常大的挑战。

  2. 成本问题:每个租户都有独立的环境,花费在单个客户上的成本将非常高,会大幅削弱SaaS软件服务的盈利能力。

  3. 敏捷迭代问题:SaaS模式的一个优势是能够快速响应市场需求,迭代产品功能。但竖井隔离策略会阻碍这种敏捷迭代能力,因为更新、管理、支撑这些租户的SaaS环境,会变得非常复杂和低效。

  4. 统一管理与监控:在同一套环境中,对部署的基础设施进行管理与监控,是较为简单的。但每个租户都有独立的环境,在这种非中心化的模式下,对每个租户的基础设施进行管理与监控,同样也是非常复杂、困难的。


8.2.2 共享模式


image.png



  • 优势:



  1. 高效管理:在共享策略下,能够集中化地管理、运营所有租户,管理效率非常高。同时,对基础设施配置管理、监控,也将更加容易。相比竖井策略,产品的迭代更新会更快。

  2. 成本低:SaaS服务商的成本结构中,很大一块是基础设施的成本。在共享模型下,服务商可以根据租户们的实际资源负载情况,动态伸缩系统,这样基础设施的利用率将非常高。



  • 劣势:



  1. 租户相互影响:由于所有租户共享一套资源,当其中一个租户大量占用机器资源,其他租户的使用体验很可能受到影响,在这种场景下,需要在技术架构上设计一些限制措施(限流、降级、服务器隔离等),让影响面可控。

  2. 租户计费困难:在竖井模型下,非常容易统计租户的资源消耗。然而,在共享模型下,由于所有租户共享一套资源,需要投入更多的精力统计单个租户的合理费用。


8.2.3 分域隔离模式


image.png


8.3 多租户系统需要具备的能力



  1. 多个租户支持共享一套云资源,如计算、存储、网络资源等。单个租户也可以独占一套云资源。

  2. 多个租户间能够实现数据与行为的隔离,能够对租户进行分权分域控制。

  3. 租户内部能够支持基于组织架构的管理,可以对产品能力进行授权和管理。

  4. 不同的产品能力可以根据客户需求,支持运行在不同的云资源上。


8.4 多租户系统应用架构图


image.png

收起阅读 »

搞明白什么是零拷贝,就是这么简单

我们总会在各种地方看到零拷贝,那零拷贝到底是个什么东西。 接下来,让我们来理一理啊。 拷贝说的是计算机里的 I/O 操作,也就是数据的读写操作。计算机可是一个复杂的家伙,包括软件和硬件两大部分,软件主要指操作系统、驱动程序和应用程序。硬件那就多了,CPU、内存...
继续阅读 »

我们总会在各种地方看到零拷贝,那零拷贝到底是个什么东西。


接下来,让我们来理一理啊。


拷贝说的是计算机里的 I/O 操作,也就是数据的读写操作。计算机可是一个复杂的家伙,包括软件和硬件两大部分,软件主要指操作系统、驱动程序和应用程序。硬件那就多了,CPU、内存、硬盘等等一大堆东西。


这么复杂的设备要进行读写操作,其中繁琐和复杂程度可想而知。


传统I/O的读写过程


如果要了解零拷贝,那就必须要知道一般情况下,计算机是如何读写数据的,我把这种情况称为传统 I/O。


数据读写的发起者是计算机中的应用程序,比如我们常用的浏览器、办公软件、音视频软件等。


而数据的来源呢,一般是硬盘、外部存储设备或者是网络套接字(也就是网络上的数据通过网口+网卡的处理)。


过程本来是很复杂的,所以大学课程里要通过《操作系统》、《计算机组成原理》来专门讲计算机的软硬件。


简化版读操作流程


那么细的没办法讲来,所以,我们把这个读写过程简化一下,忽略大多数细节,只讲流程。



上图是应用程序进行一次读操作的过程。

  1. 应用程序先发起读操作,准备读取数据了;
  2. 内核将数据从硬盘或外部存储读取到内核缓冲区;
  3. 内核将数据从内核缓冲区拷贝到用户缓冲区;
  4. 应用程序读取用户缓冲区的数据进行处理加工;

详细的读写操作流程


下面是一个更详细的 I/O 读写过程。这个图可好用极了,我会借助这个图来厘清 I/O 操作的一些基础但非常重要的概念。



先看一下这个图,上面红粉色部分是读操作,下面蓝色部分是写操作。


如果一下子看着有点儿迷糊的话,没关系,看看下面几个概念就清楚了。


应用程序


就是安装在操作系统上的各种应用。


系统内核


系统内核是一些列计算机的核心资源的集合,不仅包括CPU、总线这些硬件设备,也包括进程管理、文件管理、内存管理、设备驱动、系统调用等一些列功能。


外部存储


外部存储就是指硬盘、U盘等外部存储介质。


内核态

  • 内核态是操作系统内核运行的模式,当操作系统内核执行特权指令时,处于内核态。
  • 在内核态下,操作系统内核拥有最高权限,可以访问计算机的所有硬件资源和敏感数据,执行特权指令,控制系统的整体运行。
  • 内核态提供了操作系统管理和控制计算机硬件的能力,它负责处理系统调用、中断、硬件异常等核心任务。

用户态


这里的用户可以理解为应用程序,这个用户是对于计算机的内核而言的,对于内核来说,系统上的各种应用程序会发出指令来调用内核的资源,这时候,应用程序就是内核的用户。

  • 用户态是应用程序运行的模式,当应用程序执行普通的指令时,处于用户态。
  • 在用户态下,应用程序只能访问自己的内存空间和受限的硬件资源,无法直接访问操作系统的敏感数据或控制计算机的硬件设备。
  • 用户态提供了一种安全的运行环境,确保应用程序之间相互隔离,防止恶意程序对系统造成影响。

模式切换


计算机为了安全性考虑,区分了内核态和用户态,应用程序不能直接调用内核资源,必须要切换到内核态之后,让内核来调用,内核调用完资源,再返回给应用程序,这个时候,系统在切换会用户态,应用程序在用户态下才能处理数据。


上述过程其实一次读和一次写都分别发生了两次模式切换。



内核缓冲区


内核缓冲区指内存中专门用来给内核直接使用的内存空间。可以把它理解为应用程序和外部存储进行数据交互的一个中间介质。


应用程序想要读外部数据,要从这里读。应用程序想要写入外部存储,要通过内核缓冲区。


用户缓冲区


用户缓冲区可以理解为应用程序可以直接读写的内存空间。因为应用程序没法直接到内核读写数据, 所以应用程序想要处理数据,必须先通过用户缓冲区。


磁盘缓冲区


磁盘缓冲区是计算机内存中用于暂存从磁盘读取的数据或将数据写入磁盘之前的临时存储区域。它是一种优化磁盘 I/O 操作的机制,通过利用内存的快速访问速度,减少对慢速磁盘的频繁访问,提高数据读取和写入的性能和效率。


PageCache

  • PageCache 是 Linux 内核对文件系统进行缓存的一种机制。它使用空闲内存来缓存从文件系统读取的数据块,加速文件的读取和写入操作。
  • 当应用程序或进程读取文件时,数据会首先从文件系统读取到 PageCache 中。如果之后再次读取相同的数据,就可以直接从 PageCache 中获取,避免了再次访问文件系统。
  • 同样,当应用程序或进程将数据写入文件时,数据会先暂存到 PageCache 中,然后由 Linux 内核异步地将数据写入磁盘,从而提高写入操作的效率。

再说数据读写操作流程


上面弄明白了这几个概念后,再回过头看一下那个流程图,是不是就清楚多了。


读操作
  1. 首先应用程序向内核发起读请求,这时候进行一次模式切换了,从用户态切换到内核态;
  2. 内核向外部存储或网络套接字发起读操作;
  3. 将数据写入磁盘缓冲区;
  4. 系统内核将数据从磁盘缓冲区拷贝到内核缓冲区,顺便再将一份(或者一部分)拷贝到 PageCache;
  5. 内核将数据拷贝到用户缓冲区,供应用程序处理。此时又进行一次模态切换,从内核态切换回用户态;

写操作
  1. 应用程序向内核发起写请求,这时候进行一次模式切换了,从用户态切换到内核态;
  2. 内核将要写入的数据从用户缓冲区拷贝到 PageCache,同时将数据拷贝到内核缓冲区;
  3. 然后内核将数据写入到磁盘缓冲区,从而写入磁盘,或者直接写入网络套接字。

瓶颈在哪里


但是传统I/O有它的瓶颈,这才是零拷贝技术出现的缘由。瓶颈是啥呢,当然是性能问题,太慢了。尤其是在高并发场景下,I/O性能经常会卡脖子。


那是什么地方耗时了呢?


数据拷贝


在传统 I/O 中,数据的传输通常涉及多次数据拷贝。数据需要从应用程序的用户缓冲区复制到内核缓冲区,然后再从内核缓冲区复制到设备或网络缓冲区。这些数据拷贝过程导致了多次内存访问和数据复制,消耗了大量的 CPU 时间和内存带宽。


用户态和内核态的切换


由于数据要经过内核缓冲区,导致数据在用户态和内核态之间来回切换,切换过程中会有上下文的切换,如此一来,大大增加了处理数据的复杂性和时间开销。


每一次操作耗费的时间虽然很小,但是当并发量高了以后,积少成多,也是不小的开销。所以要提高性能、减少开销就要从以上两个问题下手了。


这时候,零拷贝技术就出来解决问题了。


什么是零拷贝


问题出来数据拷贝和模态切换上。


但既然是 I/O 操作,不可能没有数据拷贝的,只能减少拷贝的次数,还有就是尽量将数据存储在离应用程序(用户缓冲区)更近的地方。


而区分用户态和内核态有其他更重要的原因,不可能单纯为了 I/O 效率就改变这种设计吧。那也只能尽量减少切换的次数。


零拷贝的理想状态就是操作数据不用拷贝,但是显示情况下并不一定真的就是一次复制操作都没有,而是尽量减少拷贝操作的次数。


要实现零拷贝,应该从下面这三个方面入手:

  1. 尽量减少数据在各个存储区域的复制操作,例如从磁盘缓冲区到内核缓冲区等;
  2. 尽量减少用户态和内核态的切换次数及上下文切换;
  3. 使用一些优化手段,例如对需要操作的数据先缓存起来,内核中的 PageCache 就是这个作用;

实现零拷贝方案


直接内存访问(DMA)


DMA 是一种硬件特性,允许外设(如网络适配器、磁盘控制器等)直接访问系统内存,而无需通过 CPU 的介入。在数据传输时,DMA 可以直接将数据从内存传输到外设,或者从外设传输数据到内存,避免了数据在用户态和内核态之间的多次拷贝。




如上图所示,内核将数据读取的大部分数据读取操作都交个了 DMA 控制器,而空出来的资源就可以去处理其他的任务了。


sendfile


一些操作系统(例如 Linux)提供了特殊的系统调用,如 sendfile,在网络传输文件时实现零拷贝。通过 sendfile,应用程序可以直接将文件数据从文件系统传输到网络套接字或者目标文件,而无需经过用户缓冲区和内核缓冲区。


如果不用sendfile,如果将A文件写入B文件。



  1. 需要先将A文件的数据拷贝到内核缓冲区,再从内核缓冲区拷贝到用户缓冲区;

  2. 然后内核再将用户缓冲区的数据拷贝到内核缓冲区,之后才能写入到B文件;


而用了sendfile,用户缓冲区和内核缓冲区的拷贝都不用了,节省了一大部分的开销。


共享内存


使用共享内存技术,应用程序和内核可以共享同一块内存区域,避免在用户态和内核态之间进行数据拷贝。应用程序可以直接将数据写入共享内存,然后内核可以直接从共享内存中读取数据进行传输,或者反之。



通过共享一块儿内存区域,实现数据的共享。就像程序中的引用对象一样,实际上就是一个指针、一个地址。


内存映射文件(Memory-mapped Files)


内存映射文件直接将磁盘文件映射到应用程序的地址空间,使得应用程序可以直接在内存中读取和写入文件数据,这样一来,对映射内容的修改就是直接的反应到实际的文件中。


当文件数据需要传输时,内核可以直接从内存映射区域读取数据进行传输,避免了数据在用户态和内核态之间的额外拷贝。


虽然看上去感觉和共享内存没什么差别,但是两者的实现方式完全不同,一个是共享地址,一个是映射文件内容。


Java 实现零拷贝的方式


Java 标准的 IO 库是没有零拷贝方式的实现的,标准IO就相当于上面所说的传统模式。只是在 Java 推出的 NIO 中,才包含了一套新的 I/O 类,如 ByteBufferChannel,它们可以在一定程度上实现零拷贝。


ByteBuffer:可以直接操作字节数据,避免了数据在用户态和内核态之间的复制。


Channel:支持直接将数据从文件通道或网络通道传输到另一个通道,实现文件和网络的零拷贝传输。


借助这两种对象,结合 NIO 中的API,我们就能在 Java 中实现零拷贝了。


首先我们先用传统 IO 写一个方法,用来和后面的 NIO 作对比,这个程序的目的很简单,就是将一个100M左右的PDF文件从一个目录拷贝到另一个目录。

public static void ioCopy() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream(targetFile)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
System.out.println("传输 " + formatFileSize(sourceFile.length()) + " 字节到目标文件");
} catch (IOException e) {
e.printStackTrace();
}
}

下面是这个拷贝程序的执行结果,109.92M,耗时1.29秒。



传输 109.92 M 字节到目标文件
耗时: 1.290 秒



FileChannel.transferTo() 和 transferFrom()


FileChannel 是一个用于文件读写、映射和操作的通道,同时它在并发环境下是线程安全的,基于 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 getChannel() 方法可以创建并打开一个文件通道。FileChannel 定义了 transferFrom() 和 transferTo() 两个抽象方法,它通过在通道和通道之间建立连接实现数据传输的。


这两个方法首选用 sendfile 方式,只要当前操作系统支持,就用 sendfile,例如Linux或MacOS。如果系统不支持,例如windows,则采用内存映射文件的方式实现。


transferTo()


下面是一个 transferTo 的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

public static void nioTransferTo() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long transferredBytes = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);

System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

只耗时0.536秒,快了一倍。



传输 109.92 M 字节到目标文件
耗时: 0.536 秒



transferFrom()


下面是一个 transferFrom 的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

public static void nioTransferFrom() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);

try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long transferredBytes = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

执行时间:



传输 109.92 M 字节到目标文件
耗时: 0.603 秒



Memory-Mapped Files


Java 的 NIO 也支持内存映射文件(Memory-mapped Files),通过 FileChannel.map() 实现。


下面是一个 FileChannel.map()的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

    public static void nioMap(){
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);

try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long fileSize = sourceChannel.size();
MappedByteBuffer buffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
targetChannel.write(buffer);
System.out.println("传输 " + formatFileSize(fileSize) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

执行时间:



传输 109.92 M 字节到目标文件
耗时: 0.663 秒



推荐阅读


我的第一个 Chrome 插件上线了,欢迎试用!


前端同事最讨厌的后端行为,看看你中了没有


RPC框架的核心到底是什么


作者:古时的风筝
链接:https://juejin.cn/post/7270519061207908352
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

开发工具 2.0 的时代已经来临

AI 正在变革软件工程:开发工具 2.0 时代 生成式 AI 的爆发已经开始改变了很多行业的工作方式,但对于软件工程来说,转型才刚刚开始。 从 Copilot 说起 Github Copilot 的成功引发了一场 AI 编程工具的浪潮,《Research: q...
继续阅读 »

AI 正在变革软件工程:开发工具 2.0 时代


生成式 AI 的爆发已经开始改变了很多行业的工作方式,但对于软件工程来说,转型才刚刚开始。


从 Copilot 说起


Github Copilot 的成功引发了一场 AI 编程工具的浪潮,《Research: quantifying GitHub Copilot’s impact on developer productivity and happiness》这份报告研究了 Copilot 对开发者效率和幸福感的提升,如下

  • 使用 GitHub Copilot 的开发人员比不使用 GitHub Copilot 的开发人员完成任务的速度快 55%
  • 使用 GitHub Copilot 的小组完成任务的比例为 78%,而没有使用 Copilot 的小组为 70%
  • 88% 的使用者认为自己生产力提高了
  • 96% 的使用者认为自己处理重复性的工作更快了
  • 88% 的使用者认为自己可以更加专注于更喜欢的工作上了


原文地址:github.blog/2022-09-07-…





从数据上来看,Copilot 已经是非常成功了,我们会认为这已经是一个大的变革,但是当我们把眼光放到整个软件工程行业的时候,才发现 Copilot 可能只是 AI 改变软件工程师工作方式的开端。



我曾经写了一篇 Copilot 的体验文章,有兴趣可以看看 # 与 AI 结对编程,好搭档 Copilot



开发工具 2.0 与现状


红衫资本在《Developer Tools 2.0》中定义了”开发工具 2.0“ :通过 AI 改变软件创造方式的工具。


还整理了一张图用以展示现有的开发工具在不同的软件研发阶段的应用。




这图本质上是一个表格,每一行从左到右代表了软件在当前市场的占用水平,分为

  • Incumbents:当前主流使用的标准工具
  • Challengers:挑战者,一些加入了 AI 特性的创新型工具
  • Dev Tools 2.0:通过 AI 改变软件创造方式的工具

列的话从上到下代表了软件开发的各个阶段,或者说生命周期,分别为

  • Deployment:部署阶段,包括 CI/CD、云、监控等
  • Implementation:实现阶段,包括 code review 工具、文档工具、代码编写维护工具等
  • Setup:配置阶段,包括 IDE、终端、ISSUE 记录工具等

接下来我们从上往下来分析。


Deployment 所属区域中,软件还是集中在 Incumbents(主流) 和 Challengers(挑战者) 中,这里可以看到很多熟悉的产品,比如 Datadog、Grafana、Aws、Jenkins 等。


但 Deployment 目前还没有 Dev Tools 2.0 的工具




Implementation 中,目前已有很多 Dev Tools 2.0 了,比如 AI code review 工具 Codeball、DEBUG 和对话工具 ChatGPT、AI 文档工具 Mintlify、以及 AI 代码补全工具 Copilot 和 Tabnine。


注意看细分的 write docs(文档编写) 和 write & maintain code (代码编写维护)中,在主流中这些都是人力维护,这说明当前的软件工程已经处于一个分水岭了:从人工到 AI。


对比 Deployment 的话,Implementation 的 2.0 工具可谓是百花齐放。




最后就是 Setup 了,目前只有 Cursor (一款集成了 ChatGPT 4 的代码编辑器)被完全定义为 Dev Tools 2.0




这里比较意外的是 warp 和 fig 居然没有被定义为 2.0 工具,因为我前段时间刚试用了 warp 终端,有兴趣的可以看看我发的视频


其实回顾一下红衫资本对 Dev Tools 2.0 的定义就能理解了:通过 AI 改变软件创造方式的工具。


warp 和 fig 只是带了 AI 的特性,还没有改变软件的创造规则,所以就被列入了 challenger 里。


从目前世面上的工具来看,AI 已经有了巨大的机会改变软件工程,并且这是一个关于“谁”,而不是“是与否”的问题。


开发工具 2.0 的共同点


再再再次啰嗦一下红衫资本对 Dev Tools 2.0 的定义:通过 AI 改变软件创造方式的工具。


我考察了 5 个图中被归类为 2.0 的软件,看看它们是如何改变软件的创作方式的



首先是 Cursor,我们可以用自然语言来写新的代码、维护既有代码,从这点来看它是超越了 Copilot (这不是指下一代 Copilot X )。




然后是 Codeball,它主要是用 AI 来自动执行 code review,它可以为每一个 PR 进行评分(检查代码规范、Bug 等)并自动合并,大量节省功能特性因 PR 被 Block 的时间,而且用机器代替人做检查也能避免 Review 成为形式主义的流程。




ChatGPT 此处就不做演示了,直接看一下 Grit 吧。虽然下面展示的动图只是将代码片段的优化,但 Grit 给自己的定位是通过 AI 自动化完成整个项目的代码迁移和升级,比如从 JavaScript 到 TypeScript、自动处理技术债等




最后就是 Adrenaline 了,它是一个 AI Debuger(调试器?),我输入了一段会导致 NullPointerException 的代码,但是因为服务器请求的数量太多无法运行。所以我直接在对话框里问了一句:Is there anything wrong with this code?(这段代码有问题吗?)。Adrenaline 不仅回答了会出问题,还详细分析了这段代码的功能




再来对比一下这几个场景下传统的处理方式



基于以上工具的特点,我们也可以畅想一下 Deployment 2.0 工具的特点

  1. 首先肯定是通过自然语言进行交互,比如:帮我在阿里云上部署一下 xxx 项目;也可以说帮我创建一个项目,这项目叫熔岩巨兽,需要使用到 mysql、redis,需要一个公网域名等…
  2. 然后是能够自动分析并配置项目的依赖,比如:部署 xxx 项目需要 mysql 数据库、redis 缓存
  3. 如果能够为我使用最优(成本、性能等多方面)的解决方案更好

其实随着云平台的成熟、容器化的普及,我相信这样的 Deployment 2.0 工具肯定不会太遥远。


事实上在写这篇文章的时候我就发现了 Github 上的一个项目叫 Aquarium,它已经初步基于 AI 的能力实现了部署,它给 AI 输入了以下的前提提示:



你现在控制着一个Ubuntu Linux服务器。你的目标是运行一个Minecraft服务器。不要回答任何批判、问题或解释。你会发出命令,我会回应当前的终端输出。 回答一个要给服务器的Linux命令。



然后向 AI 输入要执行的部署,比如:”Your goal is to run a minecraft server“。


接着 AI 就会不断的输出命令,Aquarium 负责在程序执行命令并将执行结果返回给 AI,,不断重复这个过程直到部署结束。


对开发者的影响


作为一名软件开发者,我们经常会自嘲为 CV 工程师,CV 代表了 ctrl + cctral + v ,即复制粘贴工程师。


这是因为大多数的代码都是通过搜索引擎查询获得,开发者可以直接复制、粘贴、运行,如果运行失败就把错误信息放进搜索引擎再次搜索,接着又复制、粘贴、运行……


但基于开发工具 2.0,这个流程就产生了变化:搜索、寻找答案、检查答案的过程变成了询问、检查答案,直接省去了最费时间的寻找答案的过程。




还有就是开发模式的改变,以前是理解上游的需求并手写代码,而现在是理解上游的需求并用自然语言描述需求,由 AI 写代码。


也就是说在代码上的关注会降低,需要将更多的注意力集中在需求上




也许你发现了,其实可以直接从产品到 AI,因为程序员极有可能是在重复的描述产品需求。


这个问题其实可以更大胆一点假设:如果 AI 可以根据输入直接获得期望的输出,那么老板可以直接对接 AI 了,80% 的业务人员都不需要。


既然已经谈到了对”人“的影响,那不如就接着说两点吧

  • 这些工具会让高级开发者的技能经验价值打折扣,高级和初级的编码能力会趋于拟合,因为每个人都拥有一个收集了全人类知识集的 AI 助手
  • 会编程的人多了,但是适合以编程为工作的人少了

很多开发者对此产生焦虑,其实也不必,因为这是时代的趋势,淹没的也不止你一个,浪潮之下顺势而为指不定也是一个机遇。


如果光看软件工具 2.0,它给软件工程带来的是一次转型,是一次人效的变革,目前来看还没有达到对软件工程的颠覆,那什么时候会被颠覆呢?



有一天有一个这样的游戏出现了,每个人在里面都是独一无二的,系统会为每个人的每个行为动态生成接下来的剧情走向,也就是说这个游戏的代码是在动态生成,并且是为每一个人动态生成。这个游戏的内存、存储空间等硬件条件也是动态在增加。 这就是地球 Online



短期来看,AI 还不会代替程序员,但会替代不会用 AI 的程序员。


AI 正在吞噬软件


最后就用两位大佬的话来结束本文吧。


原 Netscape(网景公司)创始人 Marc Andreessen 说过一句经典的话:软件正在吞噬世界。


人工智能领域知名科学家 Andrej Karpathy 在 2017 年为上面的话做了补充:软件(1.0)正在吞噬世界,现在人工智能(软件2.0)正在吞噬软件



Software (1.0) is eating the world, and now AI (Software 2.0) is eating software.



所以,你准备好了吗?


参考

  1. http://www.sequoiacap.com/article/ai-…
  2. karpathy.medium.com/software-2-…
  3. github.blog/2022-09-07-…
  4. github.com/fafrd/aquar…

作者:vran
链接:https://juejin.cn/post/7215608036394647612
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

继copilot之后,又一款免费帮你写代码的插件

写在前面 在之前的文章中推荐过一款你写注释,它就能帮你写代码的插件copilot。 copilot写代码的能力没得说,但是呢copilot试用没几天之后就收费了。 按理说这么好用,又可以提高效率的工具,收点费也理所当然 但是秉承白嫖一时爽,一直白嫖一直爽的原则...
继续阅读 »

写在前面


在之前的文章中推荐过一款你写注释,它就能帮你写代码的插件copilot。


copilot写代码的能力没得说,但是呢copilot试用没几天之后就收费了。


按理说这么好用,又可以提高效率的工具,收点费也理所当然


但是秉承白嫖一时爽,一直白嫖一直爽的原则(主要是我穷穷穷),又发现了一款可以平替的插件CodeGeex


一、CodeGeex简介


① 来自官方的介绍



CodeGeeX is a powerful intelligent programming assistant based on LLMs. It provides functions such as code generation/completion, comment generation, code translation, and AI-based chat, helping developers significantly improve their work efficiency. CodeGeeX supports multiple programming languages.



翻译过来大概是



CodeGeeX是一个功能强大的基于llm的智能编程助手。它提供了代码生成/完成、注释生成、代码翻译和基于ai的聊天等功能,帮助开发人员显著提高工作效率。CodeGeeX支持多种编程语言。



GitHub地址


github.com/THUDM/CodeG…


目前在GitHub上 2.6k star 最近更新是2周前




③ 下载量

  • vscode 目前已有129k下载量
  • idea 目前已有58.7k 下载量

二、插件安装


① vscode




②idea


注: idea低版本的搜不到这个插件,小编用的是2023.01 这个版本的




安装完成后,注册一个账号即可使用


三、帮你写代码

① 我们只需要输入注释回车,它就可以根据注释帮你写代码

② tab接受一行代码 ctrl+space 接受一个单词








四、帮你添加注释



有时候,我们拿到同事没有写注释的代码,或者翻看一周前自己写的代码时。


这写得啥,完全看不懂啊,这时候就可以依靠它来帮我们的代码添加注释了



操作方法:

① 选中需要添加注释的代码

② 鼠标右键选择Add Comment

③ 选择中文或者英文






这是没加注释的代码

public class test02 {
   public static void main(String[] args) {
       int count=0;
       for(int i=101;i<200;i+=2) {
           boolean flag=true;
           for(int j=2;j<=Math.sqrt(i);j++) {
               if(i%j==0) {
                   flag=false;
                   break;
              }
          }
           if(flag==true) {
               count++;
               System.out.println(i);
          }
      }
       System.out.println(count);
  }
}

这是CodeGeex帮加上的注释

public class test02 {
   //主方法,用于执行循环
   public static void main(String[] args) {
       //定义一个变量count,初始值为0
       int count=0;
       //循环,每次循环,计算101到200之间的值,并判断是否是因子
       for(int i=101;i<200;i+=2) {
           //定义一个变量flag,初始值为true
           boolean flag=true;
           //循环,每次循环,计算i的值,并判断是否是因子
           for(int j=2;j<=Math.sqrt(i);j++) {
               //如果i的值不是因子,则flag设置为false,并跳出循环
               if(i%j==0) {
                   flag=false;
                   break;
              }
          }
           //如果flag为true,则count加1,并打印出i的值
           if(flag==true) {
               count++;
               System.out.println(i);
          }
      }
       //打印出count的值
       System.out.println(count);
  }
}

基本上每一行都加上了注释,这还怕看不懂别人写的代码


五、帮你翻译成其他语言



除了上面功能外,CodeGeeX 还可以将一种语言的代码转换成其他语言的代码



操作方法:

① 选中需要转换的代码

② 鼠标右键选择Translation mode

③ 在弹出的侧边栏中选择需要转换成的语言,例如C++、 C#、Javascript 、java、Go、Python、C 等等

④ 选择转换按钮进行转换






六 小结


试用了一下,CodeGeeX 还是可以基本可以满足需求的,日常开发中提高效率是没得说了


作为我这样的穷逼,完全可以用来平替copilot,能白嫖一天是一天~


也不用当心哪天不能用了,等用不了了再找其他的呗




本期内容到此就结束了


希望对你有所帮助,我们下期再见~ (●'◡'●)


作者:xiezhr
链接:https://juejin.cn/post/7270114845402382375
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Java仿抽奖系统

Java仿抽奖系统 前言 今天也是刚看完最近挺火的电影《孤注一掷》,也是亲眼的看到了,一个完整的家庭,是如何因为赌,而导致分崩离析,最后导致走向破碎的。 一旦涉及到电子的东西,很多东西都是变得可以控制的。这个作为程序员的我们是最清楚的,同时现在的反诈宣传,做的...
继续阅读 »

Java仿抽奖系统


前言


今天也是刚看完最近挺火的电影《孤注一掷》,也是亲眼的看到了,一个完整的家庭,是如何因为赌,而导致分崩离析,最后导致走向破碎的。


一旦涉及到电子的东西,很多东西都是变得可以控制的。这个作为程序员的我们是最清楚的,同时现在的反诈宣传,做的也是非常的到位,当时剧中哪位女警说的话,影响也非常的深刻。人都有贪心和不甘心,这也就是赌能真正抓住人的东西


好了不说那么多了,下面看一个简易的程序的代码实现


代码实现


首先我们定义一些常量


private static final int PRIZE_LEVELS = 4; // 奖品级别数量
private static final int[] PRIZE_AMOUNTS = {1, 10, 100, 1000}; // 奖品金额
private static final double[] WINNING_RATES = {10, 0, 0, 0}; // 中奖率

public static void main(String[] args) {
       // 设定中奖率
       double winningRate = 0.1;

       // 抽奖
       int prize = drawLottery(winningRate);

       // 发放奖品
       if (prize > 0) {
           System.out.println("恭喜你中奖了!奖金:" + prize + "元");
      } else {
           System.out.println("很遗憾,未中奖");
      }
  }

   // 抽奖方法
   private static int drawLottery(double winningRate) {
       Random random = new Random();
       int prize = 0;

       // 根据奖品级别逐级判断中奖
       for (int i = 0; i < PRIZE_LEVELS; i++) {
           // 生成0到1之间的随机数,判断是否中奖
           if (random.nextDouble() < winningRate * WINNING_RATES[i]) {
               prize = PRIZE_AMOUNTS[i];
               break;
          }
      }

       return prize;
  }
}

一个简单的抽奖程序。我们根据这个进行一些修改,更加的客观真实,我们加上已经有的金额和权重,让他更像是真正的赌。


我们加入权重以及自己的现金


private static double[] WEIGHTS;
// 自己的现金余额
static int cashBalance = 1000;

之后我们进行这样设计


   public static void main(String[] args) {
       // 计算权重
       calculateWeights();

       // 自己的现金余额
       int cashBalance = 1000;

       // 抽奖一次
       drawLottery(cashBalance);
  }

public static void calculateWeights() {
   WEIGHTS = new double[WINNING_RATES.length];
   double totalWeight = 0;

   // 计算总权重
   for (double rate : WINNING_RATES) {
       totalWeight += rate;
  }

   // 计算每个奖品级别的权重
   for (int i = 0; i < WEIGHTS.length; i++) {
       WEIGHTS[i] = WINNING_RATES[i] / totalWeight;
  }
}

public static void drawLottery() {
   Random random = new Random();
   double randomValue = random.nextDouble();

   int prizeIndex = 0;
   double cumulativeWeight = 0;

   // 根据随机值选择对应的奖品级别
   for (int i = 0; i < WEIGHTS.length; i++) {
       cumulativeWeight += WEIGHTS[i];
       if (randomValue <= cumulativeWeight) {
           prizeIndex = i;
           break;
      }
  }

   // 判断是否中奖
   if (random.nextDouble() <= WINNING_RATES[prizeIndex]) {
       int prizeAmount = PRIZE_AMOUNTS[prizeIndex];
       System.out.println("恭喜您中奖了!获得奖金:" + prizeAmount + "元");
       cashBalance += prizeAmount;
  } else {
       System.out.println("很遗憾,未中奖。");
  }

   // 更新现金余额
   cashBalance -= COST_PER_DRAW;
   System.out.println("抽奖后的现金余额:" + cashBalance + "元");
}

可以看出,我们这里规定的是20元抽奖一次,最高能达到1000元。


image-20230821164510692


运行一次后发现从原来的升值到了5000,


可是当你一旦陷入进去的话,只要我们稍微修改一下中奖率


image-20230821164625143


就会不断的去输。



赌博有害健康,需要我们每个人去

作者:小u
来源:juejin.cn/post/7270173541457723452
制止


收起阅读 »

优化重复冗余代码的8种方式

前言 大家好,我是田螺。好久不见啦~ 日常开发中,我们经常会遇到一些重复代码。大家都知道重复代码不好,它主要有这些缺点:可维护性差、可读性差、增加错误风险等等。最近呢,我优化了一些系统中的重复代码,用了好几种的方式。感觉挺有用的,所以本文给大家讲讲优化重复代码...
继续阅读 »

前言


大家好,我是田螺。好久不见啦~


日常开发中,我们经常会遇到一些重复代码。大家都知道重复代码不好,它主要有这些缺点:可维护性差、可读性差、增加错误风险等等。最近呢,我优化了一些系统中的重复代码,用了好几种的方式。感觉挺有用的,所以本文给大家讲讲优化重复代码的几种方式。

  • 抽取公用方法
  • 抽个工具类
  • 反射
  • 泛型
  • 继承和多态
  • 设计模式
  • 函数式
  • AOP

1. 抽取公用方法


抽取公用方法,是最常用的代码去重方法~


比如这个例子,分别遍历names列表,然后各自转化为大写和小写打印出来:


public class TianLuoExample {

public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "TianLuo");

System.out.println("Uppercase Names:");
for (String name : names) {
String uppercaseName = name.toUpperCase();
System.out.println(uppercaseName);
}

System.out.println("Lowercase Names:");
for (String name : names) {
String lowercaseName = name.toLowerCase();
System.out.println(lowercaseName);
}
}
}

显然,都是遍历names过程,代码是重复的,只不过转化大小写不一样。我们可以抽个公用方法processNames,优化成这样:


public class TianLuoExample {

public static void processNames(List<String> names, Function<String, String> nameProcessor, String processType) {
System.out.println(processType + " Names:");
for (String name : names) {
String processedName = nameProcessor.apply(name);
System.out.println(processedName);
}
}

public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "TianLuo");

processNames(names, String::toUpperCase, "Uppercase");
processNames(names, String::toLowerCase, "Lowercase");
}
}

2. 抽工具类


我们优化重复代码,抽一个公用方法后,如果发现这个方法有更多共性,就可以把公用方法升级为一个工具类。比如这样的业务场景:我们注册的时候,修改邮箱,重置密码等,都需要校验邮箱


实现注册功能时,用户会填邮箱,需要验证邮箱格式


public class RegisterServiceImpl implements RegisterService{
private static final String EMAIL_REGEX =
"^[A-Za-z0-9+_.-]+@(.+)$";

public boolean registerUser(UserInfoReq userInfo) {
String email = userInfo.getEmail();
Pattern pattern = Pattern.compile(EMAIL_REGEX);
Matcher emailMatcher = pattern.matcher(email);
if (!emailMatcher.matches()) {
System.out.println("Invalid email address.");
return false;
}

// 进行其他用户注册逻辑,比如保存用户信息到数据库等
// 返回注册结果
return true;
}
}

密码重置流程中,通常会向用户提供一个链接或验证码,并且需要发送到用户的电子邮件地址。在这种情况下,也需要验证邮箱格式合法性


public class PasswordServiceImpl implements PasswordService{

private static final String EMAIL_REGEX =
"^[A-Za-z0-9+_.-]+@(.+)$";

public void resetPassword(PasswordInfo passwordInfo) {
Pattern pattern = Pattern.compile(EMAIL_REGEX);
Matcher emailMatcher = pattern.matcher(passwordInfo.getEmail());
if (!emailMatcher.matches()) {
System.out.println("Invalid email address.");
return false;
}
//发送通知修改密码
sendReSetPasswordNotify();
}
}

我们可以抽取个校验邮箱的方法出来,又因为校验邮箱的功能在不同的类中,因此,我们可以抽个校验邮箱的工具类


public class EmailValidatorUtil {
private static final String EMAIL_REGEX =
"^[A-Za-z0-9+_.-]+@(.+)$";

private static final Pattern pattern = Pattern.compile(EMAIL_REGEX);

public static boolean isValid(String email) {
Matcher matcher = pattern.matcher(email);
return matcher.matches();
}
}

//注册的代码可以简化为这样啦
public class RegisterServiceImpl implements RegisterService{

public boolean registerUser(UserInfoReq userInfo) {
if (!EmailValidatorUtil.isValid(userInfo.getEmail())) {
System.out.println("Invalid email address.");
return false;
}

// 进行其他用户注册逻辑,比如保存用户信息到数据库等
// 返回注册结果
return true;
}
}

3. 反射


我们日常开发中,经常需要进行PO、DTO和VO的转化。所以大家经常看到类似的代码:


    //DTO 转VO
public UserInfoVO convert(UserInfoDTO userInfoDTO) {
UserInfoVO userInfoVO = new UserInfoVO();
userInfoVO.setUserName(userInfoDTO.getUserName());
userInfoVO.setAge(userInfoDTO.getAge());
return userInfoVO;
}
//PO 转DTO
public UserInfoDTO convert(UserInfoPO userInfoPO) {
UserInfoDTO userInfoDTO = new UserInfoDTO();
userInfoDTO.setUserName(userInfoPO.getUserName());
userInfoDTO.setAge(userInfoPO.getAge());
return userInfoDTO;
}

我们可以使用BeanUtils.copyProperties() 去除重复代码BeanUtils.copyProperties()底层就是使用了反射


    public UserInfoVO convert(UserInfoDTO userInfoDTO) {
UserInfoVO userInfoVO = new UserInfoVO();
BeanUtils.copyProperties(userInfoDTO, userInfoVO);
return userInfoVO;
}

public UserInfoDTO convert(UserInfoPO userInfoPO) {
UserInfoDTO userInfoDTO = new UserInfoDTO();
BeanUtils.copyProperties(userInfoPO,userInfoDTO);
return userInfoDTO;
}

4.泛型


泛型是如何去除重复代码的呢?给大家看个例子,我有个转账明细和转账余额对比的业务需求,有两个类似这样的方法:


private void getAndUpdateBalanceResultMap(String key, Map<String, List> compareResultListMap,
List balanceDTOs
) {
List<TransferBalanceDTO> tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(balanceDTOs);
compareResultListMap.put(key, tempList);
}

private void getAndUpdateDetailResultMap(String key, Map<String, List> compareResultListMap,
List detailDTOS
) {
List<TransferDetailDTO> tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(detailDTOS);
compareResultListMap.put(key, tempList);
}

这两块代码,流程功能看着很像,但是就是不能直接合并抽取一个公用方法,因为类型不一致。单纯类型不一样的话,我们可以结合泛型处理,因为泛型的本质就是参数化类型.优化为这样:


private  void getAndUpdateResultMap(String key, Map<String, List> compareResultListMap, List accountingDTOS) {
List tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(accountingDTOS);
compareResultListMap.put(key, tempList);
}

5. 继承与多态


假设你正在开发一个电子商务平台,需要处理不同类型的订单,例如普通订单和折扣订单。每种订单都有一些共同的属性(如订单号、购买商品列表)和方法(如计算总价、生成订单报告),但折扣订单还有特定的属性和方法


在没有使用继承和多态的话,会写出类似这样的代码:


//普通订单
public class Order {
private String orderNumber;
private List products;

public Order(String orderNumber, List products) {
this.orderNumber = orderNumber;
this.products = products;
}

public double calculateTotalPrice() {
double total = 0;
for (Product product : products) {
total += product.getPrice();
}
return total;
}

public String generateOrderReport() {
return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice();
}
}

//折扣订单
public class DiscountOrder {
private String orderNumber;
private List products;
private double discountPercentage;

public DiscountOrder(String orderNumber, List products, double discountPercentage) {
this.orderNumber = orderNumber;
this.products = products;
this.discountPercentage = discountPercentage;
}

public double calculateTotalPrice() {
double total = 0;
for (Product product : products) {
total += product.getPrice();
}
return total - (total * discountPercentage / 100);
}
public String generateOrderReport() {
return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice();
}
}

显然,看到在OrderDiscountOrder类中,generateOrderReport() 方法的代码是完全相同的。calculateTotalPrice()则是有一点点区别,但也大相径庭。


我们可以使用继承和多态去除重复代码,让DiscountOrder去继承Order,代码如下:


public class Order {
private String orderNumber;
private List products;

public Order(String orderNumber, List products) {
this.orderNumber = orderNumber;
this.products = products;
}

public double calculateTotalPrice() {
double total = 0;
for (Product product : products) {
total += product.getPrice();
}
return total;
}

public String generateOrderReport() {
return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice();
}
}

public class DiscountOrder extends Order {
private double discountPercentage;

public DiscountOrder(String orderNumber, List products, double discountPercentage) {
super(orderNumber, products);
this.discountPercentage = discountPercentage;
}

@Override
public double calculateTotalPrice()
{
double total = super.calculateTotalPrice();
return total - (total * discountPercentage / 100);
}
}

6.使用设计模式


很多设计模式可以减少重复代码、提高代码的可读性、可扩展性.比如:



  • 工厂模式: 通过工厂模式,你可以将对象的创建和使用分开,从而减少重复的创建代码

  • 策略模式: 策略模式定义了一族算法,将它们封装成独立的类,并使它们可以互相替换。通过使用策略模式,你可以减少在代码中重复使用相同的逻辑

  • 模板方法模式:模板方法模式定义了一个算法的骨架,将一些步骤延迟到子类中实现。这有助于避免在不同类中重复编写相似的代码


我给大家举个例子,模板方法是如何去除重复代码的吧,业务场景:



假设你正在开发一个咖啡和茶的制作流程,制作过程中的热水和添加物质的步骤是相同的,但是具体的饮品制作步骤是不同的



如果没有使用模板方法模式,实现是酱紫的:


public class Coffee {
public void prepareCoffee() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addCondiments();
}

private void boilWater() {
System.out.println("Boiling water");
}

private void brewCoffeeGrinds() {
System.out.println("Brewing coffee grinds");
}

private void pourInCup() {
System.out.println("Pouring into cup");
}

private void addCondiments() {
System.out.println("Adding sugar and milk");
}
}

public class Tea {
public void prepareTea() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}

private void boilWater() {
System.out.println("Boiling water");
}

private void steepTeaBag() {
System.out.println("Steeping the tea bag");
}

private void pourInCup() {
System.out.println("Pouring into cup");
}

private void addLemon() {
System.out.println("Adding lemon");
}
}

这个代码例子,我们可以发现,烧水和倒入杯子的步骤代码,在CoffeeTea类中是重复的。


使用模板方法模式,代码可以优化成这样:


abstract class Beverage {
public final void prepareBeverage() {
boilWater();
brew();
pourInCup();
addCondiments();
}

private void boilWater() {
System.out.println("Boiling water");
}

abstract void brew();

private void pourInCup() {
System.out.println("Pouring into cup");
}

abstract void addCondiments();
}

class Coffee extends Beverage {
@Override
void brew() {
System.out.println("Brewing coffee grinds");
}

@Override
void addCondiments() {
System.out.println("Adding sugar and milk");
}
}

class Tea extends Beverage {
@Override
void brew() {
System.out.println("Steeping the tea bag");
}

@Override
void addCondiments() {
System.out.println("Adding lemon");
}
}

在这个例子中,我们创建了一个抽象类Beverage,其中定义了制作饮品的模板方法 prepareBeverage()。这个方法包含了烧水、倒入杯子等共同的步骤,而将制作过程中的特定步骤 brew() 和 addCondiments() 延迟到子类中实现。这样,我们避免了在每个具体的饮品类中重复编写相同的烧水和倒入杯子的代码,提高了代码的可维护性和重用性。


7.自定义注解(或者说AOP面向切面)


使用 AOP框架可以在不同地方插入通用的逻辑,从而减少代码重复。


业务场景:


假设你正在开发一个Web应用程序,需要对不同的Controller方法进行权限检查。每个Controller方法都需要进行类似的权限验证,但是重复的代码会导致代码的冗余和维护困难


public class MyController {
public void viewData() {
if (!User.hasPermission("read")) {
throw new SecurityException("Insufficient permission to access this resource.");
}
// Method implementation
}

public void modifyData() {
if (!User.hasPermission("write")) {
throw new SecurityException("Insufficient permission to access this resource.");
}
// Method implementation
}
}

你可以看到在每个需要权限校验的方法中都需要重复编写相同的权限校验逻辑,即出现了重复代码.我们使用自定义注解的方式能够将权限校验逻辑集中管理,通过切面来处理,消除重复代码.如下:


@Aspect
@Component
public class PermissionAspect {

@Before("@annotation(requiresPermission)")
public void checkPermission(RequiresPermission requiresPermission) {
String permission = requiresPermission.value();

if (!User.hasPermission(permission)) {
throw new SecurityException("Insufficient permission to access this resource.");
}
}
}

public class MyController {
@RequiresPermission("read")
public void viewData() {
// Method implementation
}

@RequiresPermission("write")
public void modifyData() {
// Method implementation
}
}

就这样,不管多少个Controller方法需要进行权限检查,你只需在方法上添加相应的注解即可。权限检查的逻辑在切面中集中管理,避免了在每个Controller方法中重复编写相同的权限验证代码。这大大提高了代码的可读性、可维护性,并避免了代码冗余。


8.函数式接口和Lambda表达式


业务场景:



假设你正在开发一个应用程序,需要根据不同的条件来过滤一组数据。每次过滤的逻辑都可能会有些微的不同,但基本的流程是相似的。



没有使用函数式接口和Lambda表达式的情况:


public class DataFilter {
public List<Integer> filterPositiveNumbers(List numbers) {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (number > 0) {
result.add(number);
}
}
return result;
}

public List<Integer> filterEvenNumbers(List numbers) {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
result.add(number);
}
}
return result;
}
}

在这个例子中,我们有两个不同的方法来过滤一组数据,但是基本的循环和条件判断逻辑是重复的,我们可以使用使用函数式接口和Lambda表达式,去除重复代码,如下:


public class DataFilter {
public List<Integer> filterNumbers(List numbers, Predicate predicate) {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (predicate.test(number)) {
result.add(number);
}
}
return result;
}
}


我们将过滤的核心逻辑抽象出来。该方法接受一个 Predicate函数式接口作为参数,以便根据不同的条件来过滤数据。然后,我们可以使用Lambda表达式来传递具体的条件,这样最终也达到去除重复代码的效果啦.


最后


我是捡田螺的小男孩,大家如果觉得看了本文有帮助的话,麻烦给个三连(点赞、分享、转发)支持一下哈。最近我在工作中,用了其中的几种方式,去优化重复代码。下一篇文章,我打算出一篇后端思维系列的文章,基于业务代码,手把手教大家去除重复代码哈。一起加油~~

作者:捡田螺的小男孩
来源:juejin.cn/post/7270026656663322685

收起阅读 »

消息太大,kafka受不了

前言 上周在进行自测的时候,kafka抛出一个RecordTooLargeException异常,从名字我们可以直接看出是消息太大了,导致发不出去而抛出异常,那么怎么应该怎么解决这个问题呢,其实很简单,要么将消息拆分得小一点,要么调节kafka层面的参数,依然...
继续阅读 »

前言


上周在进行自测的时候,kafka抛出一个RecordTooLargeException异常,从名字我们可以直接看出是消息太大了,导致发不出去而抛出异常,那么怎么应该怎么解决这个问题呢,其实很简单,要么将消息拆分得小一点,要么调节kafka层面的参数,依然它抛出这个异常,那么就证明超过了某个参数的阈值,由此我们可以有两种方式来处理这个问题,但是一切还要从我们的业务背景和数据结构去看这个问题。


业务背景


我们这边会将数据写入文件,通过FTP的方式,没产生数据,就往FTP里面追加,而这些数据都是需要保证不丢失的,由于业务的发展,我这边需要专门去处理这些文件,然后通过kafka投递给下游系统,所以自然需要解析文件,还得一条一条的解析后发送。


问题出现


一开始我看到文件都比较小,所以处理方式是只有这个文件的数据全部解析完成并成功投递kafka,那么我这边才记录这个文件处理成功,但是处理了很多个大文件过后,发现数据条数对不上,看日志是RecordTooLargeException异常,因为上面的处理方式是文件处理完成并全部投递到kafka才记录文件解析完成,所以这是有问题的,一个大文件可能有即使上百万条数据,难免会遇到很大的数据,所以只要一条没解析成功,那么后面的数据就不去解析了,这个文件就不算解析成功,所以应该要设计容错,并对数据进行监控和补偿。


处理问题


在得知是某些数据过大的问题,我就DEBUG去看源码,在kafka生产端的KafkaProducer类中,发现问题出在下面这方法中。



ensureValidRecordSize方法就是对消息的大小进行判断的,参数size就是我们所发送的消息的字节数,maxRequestSize就是允许消息的最大字节,因为没有进行设置,所以这个值使用的是默认值,默认为1M,所以就应该将maxRequestSize这个参数进行重新设置。


因为我们使用的是SpringBoot开发,于是通过yml方式配置,但是发现spring-kafka没提示这个属性,于是只有写一个Kafka的配置类,然后再读取yml文件内容进行配置


配置类


yml文件



通过上面的配置后,我们看到我将max.request.size参数的值设置为10M,这需要根据实际情况来,因为我在处理的过程中发现像比较大的数据行也只有个别。


如果在实际使用过程中数据比较大,那么可能需要拆分数据,不过如果数据不能拆分,那么我们应该考虑消息压缩方式,将数据压缩后再发送,然后在消费者进行解压,不过这种压缩是我们自己实现的,并不是kafka层面的压缩,kafka本身也提供了压缩功能,有兴趣可以了解一下。


扩展


上面设置了max.request.size参数,我们在上面的截图代码中看到第二个判断中有一个参数totalMemorySize,这个值是缓冲区大小,我们发送的消息并不会马上发送kafka服务端,而是会先放在内存缓冲区,然后kafka通过一个线程去取,然后发送,可通过buffer.memory设置,这个值的默认值为32M,所以我们在设置max.request.size的时候也要考虑一下这个值。


总结


有必要对kafka进行比较深一点的学习,这样在出现问题的时候能够快速定位,并且合理解决,当然,在业务处理的时候要充分考虑可能出现的问题,做好容错和相应的补偿方案。


今天的分享就到这里,感谢你的观

作者:刘牌
来源:juejin.cn/post/7269745800178286627
看,我们下期见

收起阅读 »

开发者不需要成为 K8s 专家!!!

之前有一篇文章 “扯淡的DevOps,我们开发者根本不想做运维!” 得到了许多开发者的共鸣,每一个开发人员,都希望能够抛却运维工作,更专注于自己开发的代码,将创意转化为令人惊叹的应用。然而事不尽如人意,到了云原生时代,开发者的运维工作似乎并没有减少,而是变成了...
继续阅读 »

之前有一篇文章 “扯淡的DevOps,我们开发者根本不想做运维!” 得到了许多开发者的共鸣,每一个开发人员,都希望能够抛却运维工作,更专注于自己开发的代码,将创意转化为令人惊叹的应用。然而事不尽如人意,到了云原生时代,开发者的运维工作似乎并没有减少,而是变成了在 K8s 上的应用部署和管理。


对运维人员来说,只需要维护好底层的 K8s,便可以在弹性、便捷性上得到巨大提升。然而 K8s 对于我们开发者而言还是太复杂了,我们还需要学习如何打包镜像以及 K8s 相关知识。许多时间都浪费在了应用部署上,我们真的需要成为 K8s 专家吗?我只是想部署一个应用至于那么复杂吗?你是否曾想过,能否有平台或方法,让我们不必成为 K8s 专家,甚至都不需要懂 K8s 就能部署好你的应用,轻松管理应用?


实际面临的问题


对于我们开发者而言,总会遇到以下不同的场景,也许是公司层面的问题、又或是业务层面的问题,也许现在用传统部署方式很简单,但随着业务增长,又不得不迁移。而面对这些问题,我们也要发出自己的声音。




  • 身处小公司,没有专门的运维。需要程序员自己负责写 Dockerfile + YAML + Kustomize 然后部署到 k8s 上面。除了工作量以外,还面临 K8s 自身的复杂性,对于多套业务,Dockerfie、Yaml、CI、CD 脚本占据了绝大部分的工作量。不写这些行不行?




  • 公司内的微服务越来越复杂,在写代码的基础上还得考虑各个服务之间的通信、依赖和部署问题,毕竟除了我们开发者以外,运维人员也不会比你更熟悉微服务之间的复杂依赖。也许已经开始尝试 Helm ,但是编写一个完整的 Chart 包依然是如此复杂,还可能面临格式问题、配置解耦不完全导致的换个环境无法部署问题。时间全写 Yaml 了。不额外编写 Helm Chart,直接复制应用行不行?




  • 在大型企业内部,正处于在传统应用迁移到云环境的十字路口。面对多种集群的需求、现有应用的平稳迁移、甚至一些公共的模块的复用如何做都将成为我们需要解决的问题。不要每次都重新开发,把现有的应用或模块积累下来行不行?




在这些场景下,我们大量的时间都消耗在额外的 Dockerfile、Yaml、Helm Chart 这些编写上了。K8s 很好,但似乎没解决我们开发者的问题,我们开发者用 K8s 反而变得更加复杂。不说需要额外编写的这些文件或脚本,单单是掌握 K8s 的知识就需要耗费大量时间和精力。
这些问题真的绕不过去吗?我觉得不是。来了解下 Rainbond 这个不需要懂 K8s 的云原生应用管理平台吧。谁说只有成为 K8s 专家后,才能管理好你的应用?


为什么是 Rainbond?


Rainbond 是一个不需要懂 K8s 的应用管理平台。不用在服务器上进行繁琐操作,也不用深入了解 K8s 的相关知识。Rainbond 遵循“以应用为中心”的设计理念。在这里只有你的业务模块和应用。每一个业务模块都可以从你的代码仓库直接部署并运行,你不是 K8s 专家也可以管理应用的全生命周期。同时利用 Rainbond 的模块化拼装能力,你的业务可以灵活沉淀为独立的应用模块,这些模块可以随意组合、无限拼装,最终构建出多样化的应用系统。


1. 不懂 K8s 部署 K8s 行不行?


行! 对于很多初学者或者开发人员来说,如果公司内部已经搭建好了可用的 K8s 平台,那么这一步不会是需要担心的问题。但是对于一些独立开发者而言,却很难有这样的环境,而 Rainbond 就提供了这样的解决方案,在 Linux 服务器上,只需要先运行一个 Docker 容器,访问到 Rainbond 控制台后,再输入服务器的 IP 地址,即可快速部署一套完整的 K8s 集群。


add_cluster


如果这还是太复杂,那么可以尝试使用 Rainbond 的快速安装,只需要一个容器和 5 分钟时间,就能为你启动一个带 K8s 集群的平台,而你在平台上部署的业务也都会部署到这个集群中。


2. 不想或不会写 Dockerfile、Yaml 等文件能不能部署应用?


能! Rainbond 支持自动识别各类开发语言,不论你是使用哪种开发语言,如Java、Python、Golang、NodeJS、Dockerfile、Php、.NetCore等,通过简单的向导式流程,无需配置或少量配置,Rainbond 都能够将它们识别并自动打包为容器镜像,并将你的业务快速部署到 K8s 集群中进行高效管理。你不再需要编写任何与代码无关的文件。只需要提供你的代码仓库地址即可。


source_code_build


3. 各类业务系统如何拼装?


在 Rainbond 中,不同的业务程序可以通过简单的连线方式进行快速编排。如果你需要前端项目依赖后端,只需打开编排模式,将它们连接起来,便能迅速建立依赖关系,实现模块化的拼装。这为你的应用架构带来了极大的灵活性,无需复杂的配置和操作,即可快速构建复杂的应用系统。


同时如果你已经实现了完整的业务程序,它可能包含多个微服务模块,你还可以将其发布到本地的组件库实现模块化的积累。下次部署时可以直接即点即用,且部署后可以与你其他应用程序再次进行拼装。实现无限拼装组合的能力。


component_assembly


4. 不会 K8s 能不能管理部署好的应用?


没问题! Rainbond 提供了面向应用的全生命周期管理运维,不需要学习 Kubectl 命令,也不需要知道 K8s 内复杂的概念,即可在页面上一键管理应用内各个业务模块的批量启动、关闭、构建、更新、回滚等关键操作,同时还支持应用故障时自动恢复,以及应用自动伸缩等功能。同时还支持应用 http 和 tcp 策略的配置,以及相应的证书管理。


app_manage


如何使用?


在 Linux 终端执行以下命令, 5 分钟之后,打开浏览器,输入 http://<你的IP>:7070 ,即可访问 Rainbond 的 UI 了。


curl -o install.sh https://get.rainbond.com && bash ./install.sh

作者:Rainbond开源
来源:juejin.cn/post/7268539925086519353

收起阅读 »

Amazon SageMaker 让机器学习轻松“云上见”!

最近,“上云探索实验室”活动 正在如火如荼进行,亚马逊云、“大厂”背景加持,来看看它们有什么新鲜技术/产品? 本篇带来对 shouAmazon SageMaker 的认识~ 闻名不如一见 首先,开宗明义,shouAmazon SageMaker 是什么? 从...
继续阅读 »

最近,“上云探索实验室”活动 正在如火如荼进行,亚马逊云、“大厂”背景加持,来看看它们有什么新鲜技术/产品?


本篇带来对 shouAmazon SageMaker 的认识~


闻名不如一见


首先,开宗明义,shouAmazon SageMaker 是什么?




官网我们可以了解到:Amazon SageMaker 是一项帮助开发人员快速构建、训练和部署机器学习 (ML) 模型的托管服务。


其实,类比于:就像咱们开发人员经常把代码部署 Github Page 上,自动构建、运行我们的代码服务。


当下,我们可以把 AIGC 领域火爆的模型共享平台 —— Hugging Face 中的模型放到 Amazon SageMaker 中去部署运行、进行微调等~


如果你还不了解 HuggingFace?那抓紧快上车!




就这张小黄脸,目前已经估值 20 亿美元,它拥有 30K+ 的模型,5K+ 的数据集,以及各式各样的 Demo,用于构建、训练最先进的 NLP (自然语言处理)模型。


是的,如果你:


1、不想关心硬件、软件和基础架构等方面的问题


2、想简化操作机器学习模型的开发流程


3、想灵活选择使用自己的算法和框架以满足不同业务需求


就可以 把目光投向 Amazon SageMaker,用它的云服务来部署你想要用的 HuggingFace 模型等~




百思不如一试


Amazon SageMaker 可以全流程的帮助我们构建机器学习模型,这样真的会省下很多心力(少掉几根头发)~


具体实践中,我们知道在数据预处理过程中,在训练模型之前,需要做一系列操作,比如:缺失值处理、数据归一化和特征选择等,Amazon SageMaker 提供了很好的预处理和转换数据的工具,助力快速完成这些工作。


在模型选择环节,Amazon SageMaker 提供多种内置的机器学习算法和框架,你可以根据数据集和任务类型选择合适的模型。


还有,提高模型性能是我们需要特别关注的,Amazon SageMaker 让你可以指定调优的目标和约束条件,系统会自动搜索最优的参数组合,这就很智能、很舒服。


模型训练完后,Amazon SageMaker 也自带易用的模型部署和监控功能;


一整套下来,训练模型感觉就像呼吸一样自然~


官网教程写的很清晰,还有很多视频讲解:# Amazon SageMaker - 入门手册


这里不赘述,仅看实战中代码表示,感受一二:


如何在Amazon SageMaker 上部署Hugging Face 模型?




1、首先,在 Amazon SageMaker 中创建一个 Notebook 实例。可以使用以下代码在 Amazon SageMaker 中创建 Notebook 实例:

import sagemaker from sagemaker
import get_execution_role

role = get_execution_role()

sess = sagemaker.Session()

# 创建 Notebook 实例
notebook_instance_name = 'YOUR_NOTEBOOK_INSTANCE_NAME'
instance_type = 'ml.t2.medium'
sagemaker_session = sagemaker.Session()
sagemaker_session.create_notebook_instance(notebook_instance_name=notebook_instance_name,instance_type=instance_type,role=role)


2、其次,下载 Hugging Face 模型,你可以使用以下代码下载 Hugging Face 模型:

!pip install transformers

from transformers import AutoTokenizer, AutoModelForSequenceClassification

# 下载 Hugging Face 模型
model_name = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name)

3、创建一个推理规范,以指定部署的模型。可以参照以下代码创建推理规范:

from sagemaker.predictor import Predictor
from sagemaker.serializers import JSONSerializer
from sagemaker.deserializers import JSONDeserializer
from sagemaker.tensorflow.serving import Model

# 创建推理规范
class HuggingFacePredictor(Predictor):
def __init__(self, endpoint_name, sagemaker_session):
super().__init__(endpoint_name, sagemaker_session, serializer=JSONSerializer(),
deserializer=JSONDeserializer())

model_name = "huggingface"
model_data = "s3://YOUR_S3_BUCKET/YOUR_MODEL.tar.gz"
entry_point = "inference.py"
instance_type = "ml.m5.xlarge"
role = sagemaker.get_execution_role()

model = Model(model_data=model_data,
image_uri="763104351884.dkr.ecr.us-east-1.amazonaws.com/huggingface-pytorch-inference:1.6.0-transformers4.0.0",
role=role,
predictor_cls=HuggingFacePredictor,
entry_point=entry_point)

predictor = model.deploy(initial_instance_count=1, instance_type=instance_type)


4、测试部署模型

data = {"text": "I love using Amazon SageMaker!"}
response = predictor.predict(data)

print(response)

过程就是,创建 NoteBook => 下模型 => 指定模型、设定推理脚本 => 测试部署,足够简洁~


人人都能上云,人人都能训练机器模型~


不知道大家发现没有,其实现在的编程开发都逐渐在“云”化,不仅是机器学习,普通开发也是;类似低代码平台,开发不再是一点点复制代码、拼凑代码、修改代码,更多是通过自动化平台“点点点”就能构建自己想要的服务了!拥抱“云”平台,省心又省力,也在拥抱未来~


防守不如亮剑


目前市面上同类型的产品也有一些,比如:


1、Google Cloud AI Platform:Google提供的全托管的机器学习平台,可以帮助用户构建、训练和部署机器学习模型。


2、Microsoft Azure Machine Learning:微软提供的一款全托管的机器学习平台,可以帮助用户构建、训练和部署机器学习模型。


3、IBM Watson Studio:IBM提供的一款机器学习工具和服务平台,可以帮助用户构建、训练和部署机器学习模型。此外它还提供了数据可视化、模型解释和协作等功能。


它们与Amazon SageMaker 类似,具有全托管和自动化的特点,同时提供了多种算法和框架供用户选择。但尺有所长、寸有所短,咱们不妨用表格来一眼对比它们各自的优缺点:



可以看到,Amazon SageMaker 的配置更简单;


至于谈到:“需掌握 AWS 服务”,其实它也好上手,类比于阿里云,AWS 是亚马逊云服务,全球顶流、百万客户、加速敏捷,即使不用来开发机器模型,也建议体验、上手其它云服务产品~具体参见aws.amazon.com




活动福利环节


刚好,最近正在进行“上云探索实验室”活动;6月27-28日亚马逊云科技中国峰会上海现场,展区5楼【开发者会客厅】有互动活动:


现场为开发者提供了部分 Amazon codewhisperer 账号,开发者可以到现场直接进行代码体验,参与体验问卷回复可获得社区定制周边礼品一份。


同时开发者也可于现场进行专业版账号注册,完成注册即可获得定制周边礼品一份或免费 Serverlesspresso 咖啡券一张。


欢迎大家注册~~


话不多少,先冲为敬~




作者:掘金安东尼
链接:https://juejin.cn/post/7244526264618680380
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

总是跳转到国内版(cn.bing.com)?New Bing使用全攻略

你是否想要使用强大的(被削后大嘘)New Bing? 你是否已经获得了New Bing的使用资格? 你是否在访问www.bing.com/new时提示页面不存在? 你是否在访问www.bing.com时总是重定向到cn.bing.com而使用不了New Bin...
继续阅读 »

你是否想要使用强大的(被削后大嘘)New Bing?


你是否已经获得了New Bing的使用资格?


你是否在访问www.bing.com/new时提示页面不存在?


你是否在访问www.bing.com时总是重定向到cn.bing.com而使用不了New Bing?


New Bing的使用本来就不需要依赖科学上网,看完下面的教程,不论你卡在哪一步,你都可以成功使用New Bing。


3.13更新


根据大量评论反馈,微软似乎已经让本文中的方法:“修改请求头X-Forwarded-For来伪装请求地址”的方法失效了。现在微软已经可以检测到请求的源IP了,使用科学上网(代理)的方法依然可用,使用前需清空cookie,否则还是国内特供版Bing。没有办法科学上网的朋友,目前暂时帮不上忙了,如果未来有新方法,我仍然会更新在文档中。


一、加入New Bing的候选名单


现在的情况是:访问 http://www.bing.com/new,会自动跳转到cn.bing.com
如果你是Chrome或者Edge浏览器(下面以Edge举例,Chrome也同理)可以通过以下扩展的方式,修改请求Header来防止被重定向。 


打开浏览器的扩展,找到 管理扩展 按钮,在打开的页面左侧找到 获取 Microsoft Edge 扩展 并打开。
搜索 ModHeader ,安装下面这个扩展。




接着在已有扩展中,找到这个扩展并点击打开。 


打开扩展后就会弹出下面这个弹窗,点击 FILTER,选择 Request URL filter (在Chrome中是 URL filter

  


填写下面三个内容(分别是:X-Forwarded-For8.8.8.8.*://www.bing.com/.*)并确保都勾选。 



设置好后,再次访问 http://www.bing.com/new ,登录自己的微软账号,点击加入候选名单即可。

  


静静等待Microsoft Bing发来的 “你已中奖”的邮件,或者你微软的邮箱不是你常用的邮箱,时不时重新访问一下 http://www.bing.com/new 也可以看到你是否已经 “中奖”。


2023.3.6更新


在这里,可能会有一些朋友遇到重定向次数过多,请清除Cookie的问题,地址栏会有很多相同的zh-CN“后缀”,可以尝试以下方法:

  1. 点击Request URL filters一行的右方加号,并添加一个 .*://cn.bing.com/.* 。这时候Request URL filters中,同时存在两个筛选规则,包括www与cn两个
  2. 清除bing相关网站的cookie。在设置->cookie与网站权限->管理和删除cookie->查看所有网站cookie->右上方搜索bing,然后删除所有相关的条目
  3. 如果还有登录账号时遇到类似的问题。可以先按上一步Cookie,然后关闭扩展,在cn.bing.com中登录账号,再开启扩展访问正常版本试试。

二、下载Edge Dev 目前普通版Edge也可以了


获得资格后,首先需要解决的问题应该是下载Edge的DEV版本,在除了dev版本的Edge以外的任何浏览器中,均不能使用带有 Chat 功能的 New Bing。


通过下面这个链接,下载dev版本的Edge
http://www.microsoft.com/en-us/edge


不要直接点页面中大大的下载按钮(那个是普通版),找到下面这个部分并打开我框住的链接。 


在打开的页面也不要直接下载,往下找到下面这个图片的部分:



确保Edge图标上有 DEV 字样,点击右侧的下载适合自己电脑的版本(macOS、Windows、Linux)


三、访问New Bing


在你走完上面的流程后,访问 http://www.bing.com 即可看到上面的导航栏有Chat字样。

点击后,开始你的New Bing使用之旅吧。 



四、手机访问New Bing


现在(更新时间2023.2.28)微软已经将New Bing带上了手机。


现在有了更方面的访问途径,使用手机的Bing App使用New Bing的Chat功能。
但国内的应用商城应该是名叫“微软必应”的阉割版,我这里找到了微软官方的下载地址:Microsoft Bing


不过我并不是通过这种方式下载的APP,我在谷歌的应用商城下载的Bing APP,如果上述官网的地址下载的APP在账号已拥有测试资格的情况下仍没有Chat功能,请自行尝试谷歌商店下载。


五、结语


有问题可以评论中询问我,请确保你清楚地描述出你遇到的问题和尝试过的方法。New Bing、ChatGPT还有本文的作者我,都需要你具备基本的提问题的能力。我有看到部分评论(CSDN)的朋友仅简单问了一句话,我根本无从得知你的问题的现状,自然也无法解决你的问题。请你清楚地思考完下面几个问题:

  1. 你进行到哪一步卡住了?
  2. 你尝试了哪些方法?
  3. 你是否完整阅读了本文?

如果你的问题描述不清楚,恕我拒不回答。


另外如果有帮到你成功访问到New Bing,还请不要吝啬你的点赞和收藏 ^_^


作者:西木鹿亚
链接:https://juejin.cn/post/7202531472720592951
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Git stash 存储本地修改

前言 我们在开发的过程中经常会遇到以下的一些场景:当我们在 dev 分支开发时,可能需要临时切换到 master 拉分支修复线上 bug 或者修改其他分支代码,此时对于 dev 分支的修改,最常见的处理方式是将代码提交到远程后再切到对应的分支进行开发,但是如果...
继续阅读 »

前言


我们在开发的过程中经常会遇到以下的一些场景:

  • 当我们在 dev 分支开发时,可能需要临时切换到 master 拉分支修复线上 bug 或者修改其他分支代码,此时对于 dev 分支的修改,最常见的处理方式是将代码提交到远程后再切到对应的分支进行开发,但是如果 dev 分支的修改不足以进行一次 commit(功能开发不完整、还有 bug 未解决等各种原因),或者觉得提交代码的步骤过多,此时 dev 的修改就不好处理

  • 在开发阶段可能某个分支需要修改特定的配置文件才能运行,而其他分支不需要,那么当我们在这个分支和其他分支来回切换的时候,就需要反复的修改、回滚对应的配置文件,这种操作也是比较低效且麻烦的


而我们通过 Git 提供的 stash 存储操作,可以将当前分支的修改或者开发常用的配置文件存储至暂存区并生成一条存储记录,在需要使用时通过存储记录的索引直接取出,无需额外提交或单独保存,可以有效的解决以上的问题


Git stash 使用流程


1. 存储修改:我们可以使用 git stash 对 dev 的修改进行存储,存储修改后会生成一条存储记录




2. 查看存储记录:通过 git stash list 查看存储记录列表,存储记录的格式为:

stash@{索引值}:WIP on [分支名]: [最近的一次 commitId + 提交信息]



3. 多次存储记录相同:如果多次存储修改的过程中没有进行过 commit 提交,存储记录除了 索引值 之外将会完全相同,此时我们就无法快速辨识存储记录对应的修改内容




4. 存储记录标识


为了解决存储记录无法辨识问题,存储修改时可以用 git stash -m '标识内容' 对存储记录进行标识




此时我们再查看存储记录列表,就可以看到存储记录的标识,此时存储记录的格式为:

stash@{索引值}:on [分支名]: [标识内容]



5. 恢复存储:当我们在其他分支完成开发再回到 dev 分支时,就可以通过 git stash apply index 将指定的存储记录恢复至工作区,index 是存储记录的索引,未指定则恢复第一条存储记录




6. 删除存储


对于不再需要的存储记录,可以通过 git stash drop index 删除指定的存储记录,此时我们执行 git stash drop 删除第一条记录后再使用 git stash list 查看存储记录就已经少了一条了




如果所有的存储记录都不需要,可以使用 git stash clear 清除所有存储记录




Git Stash 命令


查看存储记录


查看存储记录列表

git stash list

查看 最近一次 存储记录的具体修改内容,即修改了哪些文件

git stash show

查看 指定索引 存储记录的具体修改内容

git stash show index
git stash show stash@{index}

存储修改


直接存储修改

git stash

存储修改,并添加备注

git stash -m '备注内容'

恢复存储记录



恢复存储记录的修改内容



恢复 最近一次 的存储记录

git stash apply

恢复 指定索引 的存储记录

git stash apply index
git stash apply stash@{index}

删除存储记录



对不需要的存储记录进行删除,可以删除部分或全部

  • 删除 最近一次 的存储记录
git stash drop
  • 删除 指定索引 的存储记录
git stash drop index
git stash drop stash@{index}
  • 删除所有的暂存修改
git stash clear

恢复并删除存储记录



恢复存储记录的同时删除对应的存储记录

  • 恢复并删除 最近一次 的存储记录
git stash pop
  • 恢复并删除 指定索引 的存储记录
git stash pop index
git stash pop stash@{index}

作者:蒋绾心
链接:https://juejin.cn/post/7269691627598479375
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

💢可恶!终究还是逃不过这些网站吗?

Documatic http://www.documatic.com/ Documatic 是一个高效的搜索引擎工具,旨在帮助开发人员轻松搜索他们的代码库以查找特定的代码片段,函数,方法和其他相关信息。这个工具旨在为开发人员节省宝贵的时间并增加生产力,可以在几...
继续阅读 »

Documatic


http://www.documatic.com/


Documatic 是一个高效的搜索引擎工具,旨在帮助开发人员轻松搜索他们的代码库以查找特定的代码片段,函数,方法和其他相关信息。这个工具旨在为开发人员节省宝贵的时间并增加生产力,可以在几秒钟内快速提供准确和相关的搜索结果。Documatic 是一个代码搜索工具,具有自然语言查询功能,可以简化新手和专家开发人员的代码库搜索。输入查询后,Documatic 会快速从代码库中获取相关的代码块,使您更容易找到所需的信息。 



Transform.tools


transform.tools/


Transform.tools 是一个网站,可以转换大多数内容,如 HTML 到 JSX,JavaScript 到 JSON,CSS 到 JS 对象等等。当我需要转换任何内容时,它真的节省了我的时间。 




Convertio


convertio.co/


Convertio - 在线轻松转换文件。超过 309 种不同的文档,图像,电子表格,电子书,档案,演示文稿,音频和视频格式。比如 PNG 到 JPEG,SVG 到 PNG,PNG 到 ICO 等等。 



Removebg


http://www.remove.bg/


Removebg 是一个令人惊叹的工具,可以轻松地删除任何图像的背景。RemoveBG 可以立即检测图像的主题并删除背景,留下透明的 PNG 图像,您可以轻松地在项目中使用。无论您是否从事平面设计,图片编辑或涉及图像的任何其他项目,我使用过这个工具太多次,我甚至不记得了。




Imglarger


imglarger.com/


Imglarger 允许您将图像放大高达 800%,并增强照片而不损失质量,这对摄影师和图像处理者特别有用。它是一个一体化的 AI 工具包,可以增强和放大图像。增加图像分辨率而不损失质量。




Code Beautify


codebeautify.org/


Code Beautify 是一个在线代码美化和格式化工具,允许您美化源代码。除了此功能外,它还支持一些转换器,如图像到 base64,不仅如此,它还有如下图像所示的大量功能:




Vercel


vercel.com/


Vercel 是前端开发人员的平台,为创新者提供构建即时 Web 应用程序所需的速度和可靠性。它是一个云平台,自动化开发和部署流程来构建无服务器 Web 应用程序。它提供诸如无服务器功能,静态站点托管,持续部署,自定义域名和 SSL 以及团队协作等功能。它有免费层和付费计划以获得更高级功能,并被许多流行的网站和 Web 应用程序使用。




作者:前端小蜗
链接:https://juejin.cn/post/7248440645688819769
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

微信(群)接入ChatGPT,MJ聊天机器人Bot

前言 微信接入ChatGPT机器人还是挺有必要的,不用被墙,可以直接问它问题,还可以接入微信群等一些实用的功能。 注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入 注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,...
继续阅读 »

前言


微信接入ChatGPT机器人还是挺有必要的,不用被墙,可以直接问它问题,还可以接入微信群等一些实用的功能。



注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入




注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入




注意:微信接入ChatGPT机器人Bot,微信账号极其容易被封,请谨慎接入



首先你需要一个 OpenAI 的账号并且创建一个可用的 api key,这里不做过多介绍,有任何问题可以加博客首页公告处微信群进行沟通。


相关的聊天机器人Bot GitHub上有非常多的项目,不仅支持接入ChatGPT,还支持接入MJ画图等一些其他功能。


本篇介绍两个项目(我用的第一个 chatgpt-on-wechat 项目):


chatgpt-on-wechat 项目最新版支持如下功能:

  • 多端部署: 有多种部署方式可选择且功能完备,目前已支持个人微信,微信公众号和企业微信应用等部署方式
  • 基础对话: 私聊及群聊的消息智能回复,支持多轮会话上下文记忆,支持 GPT-3,GPT-3.5,GPT-4模型
  • 语音识别: 可识别语音消息,通过文字或语音回复,支持 azure, baidu, google, openai等多种语音模型
  • 图片生成: 支持图片生成 和 图生图(如照片修复),可选择 Dell-E, stable diffusion, replicate模型
  • 丰富插件: 支持个性化插件扩展,已实现多角色切换、文字冒险、敏感词过滤、聊天记录总结等插件
  • Tool工具: 与操作系统和互联网交互,支持最新信息搜索、数学计算、天气和资讯查询、网页总结,基于 chatgpt-tool-hub 实现

支持 Linux、MacOS、Windows 系统(可在Linux服务器上长期运行),同时需安装 Python。



建议Python版本在 3.7.1~3.9.X 之间,推荐3.8版本,3.10及以上版本在 MacOS 可用,其他系统上不确定能否正常运行。


注意:Docker 或 Railway 部署无需安装python环境和下载源码



Windows、Linux、Mac本地部署


本地部署请参考官方文档,按照文档一步一步操作即可。


注意要安装相对应的环境,例如 Node、Python等,这里不做过多介绍,建议大家用 Docker 方式安装,无需关心环境问题,一个命令直接部署。


环境变量

# config.json文件内容示例
{
"open_ai_api_key": "YOUR API KEY", # 填入上面创建的 OpenAI API KEY
"model": "gpt-3.5-turbo", # 模型名称。当use_azure_chatgpt为true时,其名称为Azure上model deployment名称
"proxy": "127.0.0.1:7890", # 代理客户端的ip和端口
"single_chat_prefix": ["bot", "@bot"], # 私聊时文本需要包含该前缀才能触发机器人回复
"single_chat_reply_prefix": "[bot] ", # 私聊时自动回复的前缀,用于区分真人
"group_chat_prefix": ["@bot"], # 群聊时包含该前缀则会触发机器人回复
"group_name_white_list": ["ChatGPT测试群", "ChatGPT测试群2"], # 开启自动回复的群名称列表
"group_chat_in_one_session": ["ChatGPT测试群"], # 支持会话上下文共享的群名称
"image_create_prefix": ["画", "看", "找"], # 开启图片回复的前缀
"conversation_max_tokens": 1000, # 支持上下文记忆的最多字符数
"speech_recognition": false, # 是否开启语音识别
"group_speech_recognition": false, # 是否开启群组语音识别
"use_azure_chatgpt": false, # 是否使用Azure ChatGPT service代替openai ChatGPT service. 当设置为true时需要设置 open_ai_api_base,如 https://xxx.openai.azure.com/
"azure_deployment_id": "", # 采用Azure ChatGPT时,模型部署名称
"character_desc": "你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。", # 人格描述
# 订阅消息,公众号和企业微信channel中请填写,当被订阅时会自动回复,可使用特殊占位符。目前支持的占位符有{trigger_prefix},在程序中它会自动替换成bot的触发词。
"subscribe_msg": "感谢您的关注!\n这里是ChatGPT,可以自由对话。\n支持语音对话。\n支持图片输出,画字开头的消息将按要求创作图片。\n支持角色扮演和文字冒险等丰富插件。\n输入{trigger_prefix}#help 查看详细指令。"
}

配置说明:


1.个人聊天

  • 个人聊天中,需要以 "bot"或"@bot" 为开头的内容触发机器人,对应配置项 single_chat_prefix (如果不需要以前缀触发可以填写 "single_chat_prefix": [""])
    • 机器人回复的内容会以 "[bot] " 作为前缀, 以区分真人,对应的配置项为 single_chat_reply_prefix (如果不需要前缀可以填写 "single_chat_reply_prefix": "")

    2.群组聊天

  • 群组聊天中,群名称需配置在 group_name_white_list 中才能开启群聊自动回复。如果想对所有群聊生效,可以直接填写 "group_name_white_list": ["ALL_GROUP"]
    • 默认只要被人 @ 就会触发机器人自动回复;另外群聊天中只要检测到以 "@bot" 开头的内容,同样会自动回复(方便自己触发),这对应配置项 group_chat_prefix
    • 可选配置: group_name_keyword_white_list配置项支持模糊匹配群名称,group_chat_keyword配置项则支持模糊匹配群消息内容,用法与上述两个配置项相同。(Contributed by evolay)
    • group_chat_in_one_session:使群聊共享一个会话上下文,配置 ["ALL_GROUP"] 则作用于所有群聊

    3.语音识别

  • 添加 "speech_recognition": true 将开启语音识别,默认使用openai的whisper模型识别为文字,同时以文字回复,该参数仅支持私聊 (注意由于语音消息无法匹配前缀,一旦开启将对所有语音自动回复,支持语音触发画图);
    • 添加 "group_speech_recognition": true 将开启群组语音识别,默认使用openai的whisper模型识别为文字,同时以文字回复,参数仅支持群聊 (会匹配group_chat_prefix和group_chat_keyword, 支持语音触发画图);
    • 添加 "voice_reply_voice": true 将开启语音回复语音(同时作用于私聊和群聊),但是需要配置对应语音合成平台的key,由于itchat协议的限制,只能发送语音mp3文件,若使用wechaty则回复的是微信语音。

    4.其他配置

  • model: 模型名称,目前支持 gpt-3.5-turbotext-davinci-003gpt-4gpt-4-32k (其中gpt-4 api暂未完全开放,申请通过后可使用)
    • temperature,frequency_penalty,presence_penalty: Chat API接口参数,详情参考OpenAI官方文档。
    • proxy:由于目前 openai 接口国内无法访问,需配置代理客户端的地址,详情参考 #351
    • 对于图像生成,在满足个人或群组触发条件外,还需要额外的关键词前缀来触发,对应配置 image_create_prefix
    • 关于OpenAI对话及图片接口的参数配置(内容自由度、回复字数限制、图片大小等),可以参考 对话接口 和 图像接口 文档,在config.py中检查哪些参数在本项目中是可配置的。
    • conversation_max_tokens:表示能够记忆的上下文最大字数(一问一答为一组对话,如果累积的对话字数超出限制,就会优先移除最早的一组对话)
    • rate_limit_chatgptrate_limit_dalle:每分钟最高问答速率、画图速率,超速后排队按序处理。
    • clear_memory_commands: 对话内指令,主动清空前文记忆,字符串数组可自定义指令别名。
    • hot_reload: 程序退出后,暂存微信扫码状态,默认关闭。
    • character_desc 配置中保存着你对机器人说的一段话,他会记住这段话并作为他的设定,你可以为他定制任何人格 (关于会话上下文的更多内容参考该 issue)
    • subscribe_msg:订阅消息,公众号和企业微信channel中请填写,当被订阅时会自动回复, 可使用特殊占位符。目前支持的占位符有{trigger_prefix},在程序中它会自动替换成bot的触发词。

    本说明文档可能会未及时更新,当前所有可选的配置项均在该config.py中列出。


    Railway部署



    Railway 每月提供5刀和最多500小时的免费额度,目前大部分账号已无法免费部署


    1. 进入 Railway
    2. 点击 Deploy Now 按钮。
    3. 设置环境变量来重载程序运行的参数,例如open_ai_api_keycharacter_desc

    Docker方式搭建


    如果想一直跑起来这个项目,建议在自己服务器上搭建,如果在自己本地电脑上搭建,电脑关机后就用不了啦,下面演示的是在我服务器上搭建,和在本地搭建步骤是一样的。


    环境准备

    1. 域名、服务器购买
    2. 服务器环境搭建,需要系统安装docker、docker-compose
    3. docker、docker-compose安装:blog.fanjunyang.zone/archives/de…

    创建相关目录


    我自己放在服务器中 /root/docker_data/wechat_bot 文件夹下面

    mkdir -p /root/docker_data/wechat_bot
    cd /root/docker_data/wechat_bot

    创建yml文件


    /root/docker_data/wechat_bot文件夹下面新建docker-compose.yml文件如下:

    version: '2.0'
    services:
    chatgpt-on-wechat:
    image: zhayujie/chatgpt-on-wechat
    container_name: chatgpt-on-wechat
    security_opt:
    - seccomp:unconfined
    environment:
    OPEN_AI_API_KEY: 'YOUR API KEY'
    MODEL: 'gpt-3.5-turbo'
    PROXY: ''
    SINGLE_CHAT_PREFIX: '["bot", "@bot"]'
    SINGLE_CHAT_REPLY_PREFIX: '"[bot] "'
    GROUP_CHAT_PREFIX: '["@bot"]'
    GROUP_NAME_WHITE_LIST: '["ChatGPT测试群", "ChatGPT测试群2"]'
    IMAGE_CREATE_PREFIX: '["画", "看", "找"]'
    CONVERSATION_MAX_TOKENS: 1000
    SPEECH_RECOGNITION: 'False'
    CHARACTER_DESC: '你是ChatGPT, 一个由OpenAI训练的大型语言模型, 你旨在回答并解决人们的任何问题,并且可以使用多种语言与人交流。'
    EXPIRES_IN_SECONDS: 3600
    USE_LINKAI: 'False'
    LINKAI_API_KEY: ''
    LINKAI_APP_CODE: ''

    运行yml文件


    进入/root/docker_data/wechat_bot文件夹下面,运行命令:docker-compose up -d


    或者在任意文件夹下面,运行命令:docker-compose -f /root/docker_data/wechat_bot/docker-compose.yml up -d


    然后服务就跑起来了,运行 sudo docker ps 能查看到 NAMES 为 chatgpt-on-wechat 的容器即表示运行成功。


    使用


    运行以下命令可查看容器运行日志,微信扫描日志中的二维码登录后即可使用:

    sudo docker logs -f chatgpt-on-wechat

    插件使用:

    如果需要在docker容器中修改插件配置,可通过挂载的方式完成,将 插件配置文件 重命名为 config.json,放置于 docker-compose.yml 相同目录下,并在 docker-compose.yml 中的 chatgpt-on-wechat 部分下添加 volumes 映射:

    volumes:
    - ./config.json:/app/plugins/config.json

    作者:Junyang
    链接:https://juejin.cn/post/7259249904777855013
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    工作6年了日期时间格式化还在写YYYY疯狂给队友埋雷

    前言 哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。 正文 不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。 他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的Simpl...
    继续阅读 »

    前言



    哈喽小伙伴们好久不见,今天来个有意思的雷,看你有没有埋过。



    正文



    不多说废话,公司最近来了个外地回来的小伙伴,在广州工作过6年,也是一名挺有经验的开发。




    他提交的代码被小组长发现有问题,给打回了,原因是里面日期格式化的用法有问题,用的SimpleDateFormat,但不知道是手误还是什么原因,格式用了YYYY-MM-dd。




    这种写法埋了一个不大不小的雷。




    用一段测试代码就可以展示出来问题



    1.jpg



    打印结果如下:



    2.jpg



    很明显,使用YYYY时,2023年变成了2024年,在正常情况下可能没问题,但是在跨年的时候大概率就会有问题了。




    原因比较简单,与小写的yyyy不同,大写的YYYY表示一个基于周的年份。它是根据周计算的年份,而不是基于日历的年份。通常情况下,两者的结果是相同的,但在跨年的第一周或最后一周可能会有差异。




    比如我如果换成2023-12-30又不会有问题了



    3.jpg



    另外,Hutool工具类本身是对Java一些工具的封装,DateUtil里面也有用到SimpleDateFormat,因此也会存在类似的问题。



    4.jpg



    避免这个问题的方法也十分简单,要有公用的格式类,所有使用日期格式的地方都引用这个类,这个类中就定义好yyyy-MM-dd想给的格式即可,这样就不会出现有人手误给大家埋雷了。



    总结




    1. 日期时间格式统一使用yyyy小写;

    2. 日期格式要规定大家都引用定义好的工具类,避免有人手误打错。




    最后再回头想一想,这种小问题并不会马上暴露出来,倘若没有被发现,到了明年元旦,刚好跨年的时候,是不是就要坑死一堆人

    作者:程序员济癫
    来源:juejin.cn/post/7269013062677823528
    了。


    收起阅读 »

    228欢乐马事件,希望大家都能平安健

    iOS
    我这个人体质很奇怪,总能遇到一些奇怪的事。比如六年前半夜打车回家,差点被出租车司机拉到深山老林。比如两年前去福州出差,差点永远回不了家。比如十点从实验室下班,被人半路拦住。比如这次,被人冒充 (在我心里这事和前几件同样恶劣) 不过幸好每次都是化险为夷...
    继续阅读 »

    我这个人体质很奇怪,总能遇到一些奇怪的事。

    • 比如六年前半夜打车回家,差点被出租车司机拉到深山老林。
    • 比如两年前去福州出差,差点永远回不了家。
    • 比如十点从实验室下班,被人半路拦住。
    • 比如这次,被人冒充 (在我心里这事和前几件同样恶劣)

    不过幸好每次都是化险为夷,所以我顺顺利利活到现在。




    事情起因是这样的:


    去年朋友B突然告诉我:发现了你的闲鱼账号。


    :我没有闲鱼啊?


    他给我截图,那个人卖掘金的周边,名字叫LolitaAnn


    因为我遇到太多离谱的事,再加上看的一堆被冒充的新闻,所以我第一反应是:这人也在冒充我


    当时朋友F 说我太敏感了,他觉得只是巧合。


    但我觉得不是巧合,因为LolitaAnn是我自己造的词。




    把我的沸点搬到小红书


    又是某一天, 朋友H告诉我:你的小红书上热门了。


    :?我没有小红书啊?


    然后他们给我看,有个人的小红书完全照搬我的沸点。


    为此我又下载小红书和他对线。起初正常交涉,但是让他删掉,他直接不回复我了,最后还是投诉他,被小红书官方删掉的。




    现在想了想,ip一样,极有可能是一个人干的。




    闲鱼再次被挖出来


    今年,有人在掘金群里说我卖周边。


    我跑到那个群解释,说我被人冒充了。


    群友很热心,都去举报那个人的昵称。昵称被举报下去了。


    但是几天之后:




    看到有人提醒我,它名字又改回来了。


    当时以为是热心群友,后来知道就是它冒充我,现在回想起来一阵恶寒。


    把名字改回去之后还在群里跟炫耀一样,心里想着:我改回来了,你们不知道是我吧。




    冒充我的人被揪出来了


    2.28的时候, 朋友C突然给我发了一段聊天记录。


    是它在群里说 它的咸鱼什么掘金周边都有。结果打开一看,闲鱼是我的名字和头像。




    事情到今天终于知道是谁冒充我了


    虽然Smile只是它的微信小号之一,都没实名认证。但是我还是知道了一些强哥的信息。


    发现是谁冒充我,我第一反应必然是喷一顿。


    刚在群里被我骂完,它脑子也不太好使,马上跑去改了自己掘金和闲鱼的名字。这不就是自爆了? 证明咸鱼和掘金都是他的号。


    奔跑姐妹兄弟(原名一只小小黄鸭) ←点击链接即可鞭尸。






    牵扯出一堆小号


    本来我以为事情已经结束了,我就去群里吐槽他。结果一堆认识他的掘友们给我说它还有别的掘金号。因为它和不同掘友用不同掘金号认识的。所以掘友们给我提供了一堆。我就挨个搜。


    直到我看到了这两条:




    因为我搜欢乐马出来上万个同名账号, 再加上他说自己有脚本,当时我以为都是他的小号。


    后来掘友们提醒我,欢乐马是微信默认生成的,所以有一堆欢乐马,不一定是他的小号。


    但是我确信他有很多小号,比如:




    比如咸鱼卖了六七个掘金鼠标垫,卖了掘金的国行switch……

    • 你们有没有想过为什么fixbug不许助攻了

    • 你们有没有想过为什么矿石贬值了,兑换商店越来越贵了?

    • 你们有没有想过为什么掘金活动必得奖励越来越少了?


    有这种操作在,普通用户根本没办法玩吧。


    所以最后,我就把这个事交给官方了。




    处理结果


    所幸官方很给力,都处理了,感谢各位官方大大。



    本次事件,共涉及主要近期活跃UserID 4个,相关小号570个。 





    我再叨叨几句

    • 卖周边可以, 你别冒充别人卖吧,又不是见不得光,做你自己不丢人。

    • 开小号可以,你别一开开一堆,逼得普通玩家拿不到福利吧。

    • 先成人后成才,不指望能为国家做多大贡献,起码别做蛀虫吧。

    • 又不是没生活,专注点自己的东西,别老偷别人沸点。

    • 我以后改名了嗷。叫Ann的八百万个,别碰瓷你爹了。


    作者:Ann
    链接:https://juejin.cn/post/7206249542751404069
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    LangChain 是 LLM 交响乐的指挥家

    本文分享 LangChain 框架中一个引人入胜的概念 Prompt Templates。如果热衷于语言模型并且热爱 Python,那么将会大饱口福。 深入了解 LangChain 的世界,这个框架在我作为开发者的旅程中改变了游戏规则。 LangChain 是...
    继续阅读 »

    本文分享 LangChain 框架中一个引人入胜的概念 Prompt Templates。如果热衷于语言模型并且热爱 Python,那么将会大饱口福。


    深入了解 LangChain 的世界,这个框架在我作为开发者的旅程中改变了游戏规则。


    LangChain 是一个框架,它一直是我作为开发者旅途中的规则改变者。 LangChain 是一个独特的工具,它利用大语言模型(LLMs)的力量为各种使用案例构建应用程序。Harrison Chase 的这个创意于 2022 年 10 月作为开源项目首次亮相。从那时起,它就成为 GitHub 宇宙中一颗闪亮的明星,拥有高达 42,000 颗星,并有超过 800 名开发者的贡献。


    LangChain 就像一位大师,指挥着 OpenAI 和 HuggingFace Hub 等 LLM 模型以及 Google、Wikipedia、Notion 和 Wolfram 等外部资源的管弦乐队。它提供了一组抽象(链和代理)和工具(提示模板、内存、文档加载器、输出解析器),充当文本输入和输出之间的桥梁。这些模型和组件链接到管道中,这让开发人员能够轻而易举地快速构建健壮的应用程序原型。本质上,LangChain 是 LLM 交响乐的指挥家。


    LangChain 的真正优势在于它的七个关键模块:

    1. 模型:这些是构成应用程序主干的封闭或开源 LLM
    2. 提示:这些是接受用户输入和输出解析器的模板,这些解析器格式化 LLM 模型的输出。
    3. 索引:该模块准备和构建数据,以便 LLM 模型可以有效地与它们交互。
    4. 记忆:这为链或代理提供了短期和长期记忆的能力,使它们能够记住以前与用户的交互。
    5. :这是一种在单个管道(或“链”)中组合多个组件或其他链的方法。
    6. 代理人:根据输入决定使用可用工具/数据采取的行动方案。
    7. 回调:这些是在 LLM 运行期间的特定点触发以执行的函数。

    GitHub:python.langchain.com/


    什么是提示模板?


    在语言模型的世界中,提示是一段文本,指示模型生成特定类型的响应。顾名思义,提示模板是生成此类提示的可重复方法。它本质上是一个文本字符串,可以接收来自最终用户的一组参数并相应地生成提示。


    提示模板可以包含语言模型的说明、一组用于指导模型响应的少量示例以及模型的问题。下面是一个简单的例子:

    from langchain import PromptTemplate

    template = """
    I want you to act as a naming consultant for new companies.
    What is a good name for a company that makes {product}?
    """

    prompt = PromptTemplate(
    input_variables=["product"],
    template=template,
    )

    prompt.format(product="colorful socks")

    在此示例中,提示模板要求语言模型为生产特定产品的公司建议名称。product 是一个变量,可以替换为任何产品名称。


    创建提示模板


    在 LangChain 中创建提示模板非常简单。可以使用该类创建简单的硬编码提示 PromptTemplate。这些模板可以采用任意数量的输入变量,并且可以格式化以生成提示。以下是如何创建一个没有输入变量、一个输入变量和多个输入变量的提示模板:

    from langchain import PromptTemplate

    # No Input Variable 无输入变量
    no_input_prompt = PromptTemplate(input_variables=[], template="Tell me a joke.")
    print(no_input_prompt.format())

    # One Input Variable 一个输入变量
    one_input_prompt = PromptTemplate(input_variables=["adjective"], template="Tell me a {adjective} joke.")
    print(one_input_prompt.format(adjective="funny"))

    # Multiple Input Variables 多个输入变量
    multiple_input_prompt = PromptTemplate(
    input_variables=["adjective", "content"],
    template="Tell me a {adjective} joke about {content}."
    )
    print(multiple_input_prompt.format(adjective="funny", content="chickens"))

    总结


    总之,LangChain 中的提示模板是为语言模型生成动态提示的强大工具。它们提供了对提示的灵活性和控制,能够有效地指导模型的响应。无论是为特定任务创建语言模型还是探索语言模型的功能,提示模板都可以改变游戏规则。


    作者:天行无忌
    链接:https://juejin.cn/post/7247810665242083383
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    AI孙燕姿翻唱爆火,多亏这个开源项目!广西老表带头打造,上手指南已出

    明敏 发自 凹非寺 量子位 | 公众号 QbitAI AI孙燕姿这么快翻唱了这么多首歌,到底是咋实现的? 关键在于一个开源项目。 最近,这波AI翻唱趋势大火,不仅是AI孙燕姿唱的歌越来越多,AI歌手的范围也在扩大,就连制作教程都层出不穷了。 而如果在各大教程...
    继续阅读 »
    明敏 发自 凹非寺 量子位 | 公众号 QbitAI

    AI孙燕姿这么快翻唱了这么多首歌,到底是咋实现的?


    关键在于一个开源项目




    最近,这波AI翻唱趋势大火,不仅是AI孙燕姿唱的歌越来越多,AI歌手的范围也在扩大,就连制作教程都层出不穷了。


    而如果在各大教程中溜达一圈后就会发现,其中的关键秘诀,还是要靠一个名为so-vits-svc的开源项目。



    它提供了一种音色替换的办法,项目在今年3月发布。


    贡献成员应该大部分都来自国内,其中贡献量最高的还是一位玩明日方舟的广西老表。




    如今,项目已经停止更新了,但是星标数量还在蹭蹭上涨,目前已经到了8.4k。


    所以它到底实现了哪些技术能引爆这波趋势?


    一起来看。


    多亏了一个开源项目


    这个项目名叫SoftVC VITS Singing Voice Conversion(歌声转换)。


    它提供了一种音色转换算法,采用SoftVC内容编码器提取源音频语音特征,然后将矢量直接输入VITS,中间不转换成文本,从而保留了音高和语调。


    此外,还将声码器改为NSF HiFiGAN,可以解决声音中断的问题。


    具体分为以下几步:

    • 预训练模型
    • 准备数据集
    • 预处理
    • 训练
    • 推理

    其中,预训练模型这步是关键之一,因为项目本身不提供任何音色的音频训练模型,所以如果你想要做一个新的AI歌手出来,需要自己训练模型。


    而预训练模型的第一步,是准备干声,也就是无音乐的纯人声。


    很多博主使用的工具都是UVR_v5.5.0


    推特博主@歸藏介绍说,在处理前最好把声音格式转成WAV格式,因为So-VITS-SVC 4.0只认这个格式,方便后面处理。


    想要效果好一些,需要处理两次背景音,每次的设置不同,能最大限度提高干声质量。


    得到处理好的音频后,需要进行一些预处理操作。


    比如音频太长容易爆显存,需要对音频切片,推荐5-15秒或者再长一点也OK。


    然后要重新采样到44100Hz和单声道,并自动将数据集划分为训练集和验证集,生成配置文件。再生成Hubert和f0。


    接下来就能开始训练和推理了。


    具体的步骤可以移步GitHub项目页查看(指路文末)。


    值得一提的是,这个项目在今年3月上线,目前贡献者有25位。从贡献用户的简介来看,很多应该都来自国内。


    据说项目刚上线时也有不少漏洞并且需要编程,但是后面几乎每一天都有人在更新和修补,现在的使用门槛已经降低了不少。


    目前项目已经停止更新了,但还是有一些开发者创建了新的分支,比如有人做出了支持实时转换的客户端。




    项目贡献量最多的一位开发者是Miuzarte,从简介地址判断应该来自广西。




    随着想要上手使用的人越来越多,也有不少博主推出了上手难度更低、更详细的食用指南。


    歸藏推荐的方法是使用整合包来推理(使用模型)和训练,还有B站的Jack-Cui展示了Windows下的步骤指南(http://www.bilibili.com/read/cv2237…


    需要注意的是,模型训练对显卡要求还是比较高的,显存小于6G容易出现各类问题。


    Jack-Cui建议使用N卡,他用RTX 2060 S,训练自己的模型大概用了14个小时


    训练数据也同样关键,越多高质量音频,就意味着最后效果可以越好。


    还是会担心版权问题


    值得一提的是,在so-vits-svc的项目主页上,着重强调了版权问题。



    警告:请自行解决数据集的授权问题。因使用未经授权的数据集进行培训而产生的任何问题及其一切后果,由您自行承担责任。存储库及其维护者、svc开发团队,与生成结果无关!





    这和AI画画爆火时有点相似。


    因为AI生成内容的最初数据取材于人类作品,在版权方面的争论不绝于耳。


    而且随着AI作品盛行,已经有版权方出手下架平台上的视频了。


    据了解,一首AI合成的《Heart on My Sleeve》在油管和Tik Tok上爆火,它合成了Drake和Weekend演唱的版本。


    但随后,Drake和Weekend的唱片公司环球音乐将这个视频从平台上下架了,并在声明里向潜在的仿冒者发问,“是要站在艺术家、粉丝和人类创造性表达的一边,还是站在Deepfake、欺诈和拒付艺术家赔偿的一边?”


    此外,歌手Drake也在ins上对AI合成翻唱歌曲表达了不满。


    而另一边,也有人选择拥抱这项技术。


    加拿大歌手Grimes表示,她愿意让别人使用自己的声音合成歌曲,但是要给她一半版权费。


    GitHub地址:

    github.com/svc-develop…


    参考链接:

    [1]mp.weixin.qq.com/s/bXD1u6ysY…

    [2]http://www.vulture.com/article/ai-…


    —  —


    作者:量子位
    链接:https://juejin.cn/post/7231738442274439225
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »