老刘

老刘 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

老刘 发布了文章 · 4月2日

大数运算—大数加法、减法、乘法、除法详解

前言

最近,大数加减频频登上笔试的舞台,小伙伴们也分享自己遇到面试官碰到大数运算的题目,想着这么重要而简单的知识点我还没写过,那得好好和大家一起总结一下。

image-20210331190632569

各位有过分类刷题的小伙伴,可能看到很多人分类 字符串、贪心、动态规划、bfs、dfs、大数、数论等,初听大数,你可能会差异:大数是个啥?听起来怪高大上的。

image-20210329191141360

大数,其实就是很大很大数字(可能远超32、64位,基础类型无法表示)的加减法,在Java中我们可以使用一个大数类(BigInteger等)很容易解决大数的各种运算,但如果遇到面试官他肯定会让你手写的。

这个数字一般用字符串、链表等形式表示、返回,大数运算的核心就是:模拟,模拟我们日常用纸笔算数字的加减乘除流程,然后再根据计算机、编程语言等特性适当存储计算即可,不过,大数除法运算稍微特殊一点,和我们直接模拟的思维方式稍有不同。

大数加法

大数加法是最简单的,简单模拟即可。首先,我们想一下两个数加法的流程:从右向左计算求和、进位,一直到最后。

在编程语言中同样也是模拟从右向左逐位相加的过程,不过在具体实现上需要注意一些细节。

1、枚举字符串将其转换程char[]提高效率

2、从右往左进行计算,可以将结果放到一个数组中最后组成字符串,也可以使用StringBuider拼接,拼接的时候最后要逆置一下顺序。

3、余数每次叠加过需要清零,两数相加如果大于等于10即有余数,添加到结果中该位置的数也应该是该数%10的结果。

4、计算完最后还要看看余数是否为1,如果为1需要将其添加到结果,例如 "991"+"11"算三个位置为002但还有一个余数需要添加,所以应该是1002

一个加法流程

当然在具体实现上方法较多,你可以首先就将字符串逆置然后从前往后就可以计算了。当然我这里实现的是字符串从后向前各个位对应计算,然后将结果顺序添加到StringBuilder上。

这题在力扣【415两数相加】可以检验自己代码,实现代码为:

public String addStrings(String num1, String num2) {
// 公众号:bigsai 欢迎你的关注
    int len1=num1.length()-1,len2=num2.length()-1;
    char ch1[]=num1.toCharArray();
    char ch2[]=num2.toCharArray();

    StringBuilder sb=new StringBuilder();
    int remainder =0;//计算余数
    while (len1>=0||len2>=0)
    {
        int n1=len1>=0?(ch1[len1--]-'0'):0;
        int n2=len2>=0?(ch2[len2--]-'0'):0;
        int num=n1+n2+remainder;//求和对应数字
        remainder=num/10;//是否进位
        sb.append(num%10);// 添加到结果字符串中
    }

    if(remainder>0)//是否还需要进位
    {
        sb.append(remainder);
    }
    //反装即为结果
    return sb.reverse().toString();
} 

大数减法

加法对应的就是减法,有了上面大数加法的实现思路,那么我想你在大数减法也应该有点想法,但是减法和加法不同的是减法有位置的区别,加法需要进位而减法需要借位。并且大整正数减法可能产生正负也不一定。

两个正数,如果大数减去小数,那么一切正常,结果是一个正数;但如果小数减去大数,那么结果将是一个负数,并且结果处理起来比较麻烦。 所以在这里全部转成大-小处理(大-小不存在不能借位的情况)。

减法转成大-小

1、执行计算前首先比较减数(num1)和被减数(num2)的大小,如果num1>num2,那么就模拟num1-num2的过程,如果num1<num2,那么结果就为-(num2-num1) 。当然可以为了稳定模拟时候一个大一个小,可将num1始终指向较大的那个数,少写一个if/else.

2、在比较两个数字大小的时候,因为是字符形式,首先比较两个字符串的长度,长的那个更大短的那个更小,如果两个字符串等大,那么就可以通过字典序从前往后进行比较(Java可直接使用compareTo方法)。

3、和加法不同的是,减法前面可能产生若干前缀0,这些0是需要你去掉的,例如"1100"-"1000"计算得到的结果位"0100",你就要吧前面的0去掉返回"100"

4、具体实现的时候和加法相似,如果使用StringBuilder存储,需要逆置顺序,如果是个负数,前面还要加上'-'.

5、每个位置正常进行减法运算,如果值小于0,那么就需要向上借位(+10),那么处理上一位进行减法时候还要将借位的处理一下。

一个减法大概流程

这题在力扣上没有原题,但是可以在小米OJ【大数相减】上验证自己代码的正确性,具体实现的代码为:

public static boolean compare(String num1,String num2) {
    if(num1.length()<num2.length())
        return  false;
    else if(num1.length()>num2.length())
        return true;
    else
        return num1.compareTo(num2)>0;
}
public static  String subtractString(String num1,String num2) {
    char sign='+';//正负号
    //让num1>num2 如果num1<num2 那么结果就是—(num2-num1) 
    //可以先将num1和num2交换和前面情况统一
    if(!compare(num1,num2))
    {
        sign='-';
        String team = num2;
        num2 = num1;
        num1 = team;
    }
    int len1=num1.length()-1;
    int len2=num2.length()-1;

    char ch1[] = num1.toCharArray();
    char ch2[] = num2.toCharArray();
    StringBuilder sb=new StringBuilder();
    int borrow=0;//借位
    while (len1>=0||len2>=0)
    {
        int n1=len1>=0?(ch1[len1--]-'0'):0;
        int n2=len2>=0?(ch2[len2--]-'0'):0;

        int num=n1-n2-borrow;
        borrow=0;
        if(num<0)//需要向前借位
        {
            borrow=1;
            num+=10;
        }
        sb.append(num);
    }

    sb=sb.reverse();//需要先翻转
    int index = 0;//去掉前面没用的’0‘
    while (index<sb.length()&&sb.charAt(index) == '0')
    {
        index++;
    }
    //如果两个数相同 直接返回"0"
    if(index==sb.length())
        return "0";
    if(sign=='+')//如果正数 
        return  sb.substring(index);
    else  return sign+sb.substring(index);//负数需要返回
} 

大数乘法

大数乘法乍一想可能比较复杂,因为乘法比起加法可能进位不光是1,还有两个数各种位置都需要相乘计算,这时候就需要我们化繁为简了。

多*多考虑起来可能有些麻烦,但是如果多*一考虑起来呢?如果是多位乘以一位数,那么就拿一位的分别乘以多位数的个位、十位、百位,在计算的同时考虑一下进位的情况。

但是也可以先直接用int类型数组存储各位的乘积然后从右向左进行进位,如下图所示。

先计算后进位

多*多 也是这个道理,将不同位乘积先叠加到对应位置上,然后从右向左进位,一直到不需要进位为止。

一个乘法流程

你可能会疑问,如果两个数组的长度分别为a和b这个数组到底该开多大呢

  • a+b大小就够了,怎么分析呢?其中一个a不变。另一个b变成最小b+1数字即十的倍数,那么这样在相乘的时候也不过是a+b长度,所以这里a+b长度就够了。

这题有力扣对应题可以去试试【43字符串相乘】,具体代码位:

public String multiply(String num1, String num2) {
    if("0".equals(num1)||"0".equals(num2))return "0";
    char a[]=num1.toCharArray();
    char b[]=num2.toCharArray();
    
    int value[]=new int[a.length+b.length];
    
    for(int i=a.length-1;i>=0;i--)
    {
        for(int j=b.length-1;j>=0;j--)
        {
            int index=a.length-1-i+b.length-1-j;
            value[index]+=(a[i]-'0')*(b[j]-'0');
        }
    }
    for(int i=0;i<value.length-1;i++)
    {
        value[i+1]+=value[i]/10;
        value[i]=value[i]%10;
    }
    int index=value.length-1;
    while(value[index]==0)
    {index--;}
    StringBuilder sBuilder=new StringBuilder();
    while (index>=0) {
        sBuilder.append(value[index--]);
    }
    return sBuilder.toString();
} 

大数除法

大数加减乘都搞定了,通过模拟来实现,但是大数除法也通过模拟来实现?

并不是,对于大数a/b,一般最多要求求到其整数解或者余数,即a/b=c……d(a,b,c,d均为整);也就是a里面有c个b,并且还剩下d。核心是先求c是多少,对于程序来说,可以通过枚举啊,将除法变成减法,从a中不断减d,一直到不能减为止。

除法转成减法运算

但是有个问题,如果被除数a很大很大,可能有居多个b,那么这样时间复杂度太高了,不可能执行那么多次,那么需要怎么样去优化这个方法呢?

那就要加速寻找次数,减少这个减法的次数了,减法次数减小的一个最好方案就是能不能扩大除数b。如果b后面加个'0',那么算出来的结果就乘以10,减法的次数变成原来十分之一。根据这个思想我们可以一直每次找到b的最大10的倍数(小于a)计算减的次数再换算成减b的总词数,将结果要以字符串方式保留,后面一直迭代到最后为止,这虽然是一道除法运算的题,但是也蕴涵减法和加法(次数叠加到结果中)。

优化思想

当然,也有一些人使用二分法来压缩寻找可以被减的次数也是可以的(加法可以迭代数字实现二分倍数),具体实现的话也不是很困难,但是代码量可能比较多所以一般的面试笔试不会让你现场写的,所以好好掌握前面的减法、减法、乘法的代码即可。

当然,如果你依然很想看大数除法部分的代码,可以百度搜一下或者在文末评论催更一下,如果有感兴趣的可以后面把代码补充上。

结语

到这里,大数的加减乘除基本都讲解完啦,不知道你有没有收获,因为这里的大数都是用字符串的方式存储和处理,遇到的最多,但是也可能遇到一些链表、数组等其他形式存储的需要处理,但是整体的思想都是一样的。

查看原文

赞 0 收藏 0 评论 0

老刘 发布了文章 · 3月30日

通俗地理解面向服务的架构(SOA)以及微服务之间的关系

**SOA是一种软件的应用架构方法,它基于面向对象,但又不是面向对象,整体上是面向服务的架构。SOA由精确的服务定义、松散的构件服务组成,以及业务流程调用等多个方面形成的一整套架构方法。
这话是不是听起来,让人觉得有点晕,我们就细细品读一下。**

SOA的架构思想

(一)SOA架构是面向服务的,只不过是基于面向对象

SOA继承了很多面向对象的特点,比如说面向对象的封装,经常代表很多类封装成一个模块,为其他对象调用者提供接口调用,良好的面向对象设计就是暴露接口,隐藏实现,类比到SOA的设计,SOA也需要精准明确地定义好服务接口,具体服务内部的逻辑实现都是隐藏在背后的,只不过有两个很大的区别:

  • (1)面向对象的实现都是基于同一个编程语言或平台(同构),但SOA服务彻底隐藏了实现上用何种语言平台的具体细节(异构)
  • (2)面向对象的实现其实大部分都是本地方法之间的调用,当然也具备分布式远程方法调用,但SOA是纯粹提供了独立的服务,面向分布式的远程服务调用。

(二)SOA的服务定义是精确的

这个怎么理解呢?因为SOA的服务一旦发布出来,那么就会有很多其他的异构平台服务进行调用,这时候的服务接口修改就不像一个人或者一个小团队之间协作那么容易了,可能涉及到一个大型企业多部门的信息协作,或者对构件已经形成依赖的生态链条。因此这就牵扯出了SOA另外一个特征,那就是服务接口的粒度一般要设置得比较粗。若提供过多的服务接口,服务又定义得很细粒度,那么频繁修改是在所难免的。这一点上就注定了SOA架构适合在较重量的环境下存在。

那什么是较重量的环境呢?(1)体系健全、制度稳定的重管理型企业,(2)业务逻辑复杂,服务的独立性,开放性需求又大,服务的稳定性也是刚需。例如:医院信息化系统架构。

(三)SOA是由松散的构件服务组成

为什么是松散的呢?由上述我们可以了解到SOA的服务接口是粗粒度的,而且组成服务的构件都是独立部署并具有独立的上下文环境,这种形态就是为了降低与其他构件之间的强依赖性。让每个构件尽可能一次性为客户提供足够的其领域范围的服务。

例如:通知服务,客户端只要传递过来通知内容即可,到底是通知短信、微信、站内信等等,这是通知服务与配置库、用户关系库的内部逻辑关系,也可以通过消息从其他服务中获取。因此SOA服务更倾向于前期就配置好通知渠道、通知用户组的逻辑关系,这种形式就是客户端轻,管理端重

上述这种松散的、粗粒度的构建服务例子,就非常符合SOA架构的胃口,可以让每个服务的独立性看起来很不错,提供一个简单的接口外观,而且越少的接口参数,频繁更改的接口的几率就越低,又满足了服务接口的精确要求,以及服务更偏重管理的特点。

ESB、BPM在SOA中的实现方式

SOA架构可以按业务流程调用各个构件服务,这是个什么概念?想要弄清楚这个概念,我们要站在上帝视角去俯视SOA架构了!

如上图所示:这是一种SOA架构的解决方案,与ESB和BPM的基础中间件结合,BPM作为一个业务流程管理平台,很好的将SOA服务通过流程建模的形式,与业务流程逻辑联系在一起。那么这个过程中,BPM支撑SOA架构的业务流程协作问题,ESB支撑SOA架构的数据交换问题。这个架构体系是不是看起来就比较完整了!

例如:应急指挥系统中,我们制定一个流程预案,可以由BPM工具进行建模,进行不同独立运行的SOA构件服务进行流程执行调度,并形成流程执行库。应用执行端,一般就是客户端手动或定时器自动,启动流程引擎实例,流程引擎读取流程模型库,并配合应用管理端的操作,对构件服务实现访问调度,流程引擎调度的这个过程中,SOA的服务构件始终围绕在ESB周围,交换过程数据。进行物资服务调度、医疗资源服务调度、通讯设备服务调度、对外信息披露服务调用等。

那么这种架构例子中,大家是不是看得出来非常适合复杂应用系统整合、协作,因为很有可能通讯设备服务提供了C++网络通讯包,物资服务是Java平台运行,医疗资源服务又是.Net平台运行,但是大家基于统一的服务规约,提供精确而风格一致的服务接口,那么对于BPM也好,ESB也好,就极大的减少了适配集成的复杂过程,让各种业务和通讯系统,都变成了一项服务,作为SOA整体调度与管理的一枚棋子而存在。这其实就有点SOA的精髓了。

WebService的实现方式

往往很多人不太了解SOA的情况下,就会认为Webservice就是SOA,所以这就是为什么先把上面的SOA思想以及架构实现讲讲,大家就能对SOA有个整体全面的理解。Webservice只是实现SOA构件服务的一种手段,若将其中的换成基于RestFul风格的实现,也是没有问题的。

WebService又依赖于几种具体的技术规范和协议了,具体描述我就直接引用吧:

SOAP(Simple ObjectAccess Protocol,简单对象访问协议) 定义了服务请求者和服务提供者之间的消息传输规范,SOAP 用 XML 来格式化消息,用 HTTP 来承载消息。通过 SOAP,应用程序可以在网络中进行数据交换和远程过程调用(Remote Procedure Call, RPC)。

WSDL(Web ServiceDescription Language,Web 服务描述语言) 是对服务进行描述的语言,它有一套基于 XML 的语法定义。WSDL 描述的重点是服务,它包含服务实现定义和服务接口定义。

UDDI(Universal DescriptionDiscovery and Integration,统一描述、发现和集成) 提供了一种服务发布、查找和定位的方法,是服务的信息注册规范,以便被需要该服务的用户发现和使用它。UDDI 规范描述了服务的概念,同时也定义了一种编程接口。通过 UDDI 提供的标准接口,企业可以发布自己的服务供其他企业查询和调用,也可以查询特定服务的描述信息,并动态绑定到该服务上。

如何通俗地去理解这三大件呢?

还是上个图,看起来舒服一些。如上图所示:SOA中的服务1需要调用服务2的接口,那么我们就描述一下Webservices方式。

首先虚线中,也就是开发阶段服务1要去理解服务2的WSDL描述,清楚服务2提供的服务接口是什么样子,描述语言就是XML,服务1的程序就知道需要设置什么参数,返回什么结果。

然后在运行时服务1要从UDDI服务上,也就是注册发现中心,找到服务2在哪里,由于服务2早已经在UDDI服务中注册,那么服务1就可以获得服务2的路由地址。再对需要传递的数据进行SOAP格式编码。

SOAP是HTTP层之上的一个传输协议,服务1对传递参数进行满足SOAP协议的xml编码和参数发送,形成对服务2的WebService接口调用,服务2接收到SOAP协议数据,进行xml解码,然后再进行内部实现层的逻辑处理,并最终将结果仍然以SOAP方式编码返回给服务1,由服务1再解码数据。这就完成了WebService的一次请求和响应。当然了服务1也可以是一个普通的客户端。

从上述的图示例子中,我们可以看到WebService是通过XML作为中间传递格式,这就兼容了异构平台的数据格式,SOAP协议大部分是基于HTTP协议(SOAP的设计不限于HTTP),这样就兼容了异构平台数据传输。

因此WebService的技术实现方案就非常符合SOA架构中服务的异构平台兼容性要求(SOAP),并且具备完整规范的服务接口语义描述(WSDL)和服务注册发现管理的规范定义(UDDI)。

SOA与微服务的优劣对比

往往没有对比就没有伤害,因此我们通过SOA架构与微服务架构的对比,来更深刻地认识SOA架构的优势与劣势,同时也能掌握到微服务优劣特征。

我们往往会从单体过渡的角度去寻求微服务的发展踪迹,也就是单体向微服务的过渡。但很少有人会去从SOA的变种这个角度去思考微服务。

因此我们需要定义一个问题,微服务到底和SOA有没有关系?其实,这其中就隐藏着两种关系:

  • (1)微服务简化了SOA架构思想,是SOA一个离经叛道的继任者,
  • (2)微服务进行了SOA基因改造,成了一个新的变种,

微服务是SOA一个离经叛道的继任者, 其实这是一句赞美之词!

首先我们来看看微服务和SOA比起来有多么的相似,又多么的不同。

(1)微服务专注小的个体问题,形成服务,通过松耦合的通讯机制协作起来,解决更大的问题;反之,SOA一开始就专注大的协调问题,首先关注的是服务协议、规则、表述的统一性,然后才是设计足够大的独立服务,并通过流程建模,解决整体上的问题。

(2)微服务倾向于拆分,也就是将单体应用尽量拆分到一个适当的粒度,形成个人或小团队去关注独立的服务个体;但SOA不同,服务要足够的粗粒度,服务接口只是作为异构系统调用的统一手段,甚至我们可以将一个大系统作为SOA的一个构建服务而独立存在,例如前面说到的应急指挥系统的SOA架构中通讯调度系统作为一个独立的SOA服务而存在。

(3)微服务的实施模式是自底向上型:不同的小团队分配不同的微服务进行开发、构建、部署、发布。系统整体上的把控,是在发布、测试过程中所有团队共同参与的结果,这时候开发变成了运维,运维变成了顾问,这就是Devops的思想,因此微服务更适合小型团队的持续化发布;反之SOA是自顶向下的实施模式,必须进行分层式的过程管理,要有人对流程管理负责、ESB企业数据总线负责、各个构件服务也是不同组织的项目或开发团队负责。因此SOA架构在实施过程中具备清晰的责任关系,特别适合项目跨企业、大企业跨部门的复杂应用系统建设。这和微服务的实施过程可以说是天壤之别。

(4)微服务与SOA一样,都是在分布式环境下,形成很多不同的独立服务,相对于SOA,微服务是细粒度的,SOA是粗粒度的,而且它们在技术的异构性的兼容上有着一致的风格,微服务是通过通讯机制,主要是Restful,实现不同微服务的相互协作,但微服务自身用什么技术来实现,那都不影响;同样前面的内容也说清楚了SOA的服务接口定义和Webservices实现,本身就是为了统一兼容异构平台之间的协作。

最后我们看看SOA和微服务的对比总结

从上面的对比,我们可以看到不能把任何问题都统一论之。微服务有其适合的场景,若在一个复杂的社会关系体系下建立一套复杂的应用系统,微服务的架构思想就是无源之水了。反倒是SOA架构思想就具备这种复杂体系下的生存条件,但是,例如放到很多互联网应用需要快速应对需求、敏捷迭代开发,灵活建立部署发布机制,那么SOA架构肯定就不适合了,这种环境正是微服务架构所适应的。

因此我们可以总结到微服务在形式上与SOA很类似,在分布式环境中都是进行更多独立的服务、独立的部署,我们可以理解是SOA的继任者。但是骨子里微服务又将SOA那一套沉重的前期规划、设计和分层实施的思路彻底打烂,形成了一个新的思想变种,灵活、敏捷、小巧,更适合团队密切的协作。 这就是进行了SOA基因的彻底改造,形成了更简化的一种分布式架构形态,尤其满足更为互联网化应用的需求。

查看原文

赞 0 收藏 0 评论 0

老刘 发布了文章 · 3月30日

Git基础知识之内部状态管理系统

本文主要来介绍一下 Git 的内部状态管理系统。它利用基于节点和指针的数据结构来跟踪及管理编辑操作的时间线。

对本地项目而言,任一时刻,Git 处于三种状态中的一种:工作区状态、暂存区状态和提交区状态。
下面利用新建项目来演示一下不同状态及其转换。

1. Initialize the project

$ mkdir git_tree_test && cd git_tree_test

$ git init
提示:使用 'master' 作为初始分支的名称。这个默认分支名称可能会更改。要在新仓库中
提示:配置使用初始分支名,并消除这条警告,请执行:
提示:
提示:  git config --global init.defaultBranch <名称>
提示:
提示:除了 'master' 之外,通常选定的名字有 'main'、'trunk' 和  'development'。
提示:可以通过以下命令重命名刚创建的分支:
提示:
提示:  git branch -m <name>
已初始化空的 Git 仓库于 /Users/phillee/git_tree_test/.git/

$ git status
位于分支 master
尚无提交
无文件要提交(创建/拷贝文件并使用 "git add" 建立跟踪) 

这时我们初始化了一个本地项目,默认创建 master 分支,尚无文件跟踪及提交。

2. The Working Directory

$ touch reset_lifecycle_file

$ git status
位于分支 master
尚无提交
未跟踪的文件:
  (使用 "git add <文件>..." 以包含要提交的内容)

        reset_lifecycle_file

提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪) 

现在我们为项目新增了文件 reset_lifecycle_file ,尚未提交,当前位于工作区(Working directory)。博客园代码显示系统太垃圾了,这时的 reset_lifecycle_file 应该是红色的,表示还没有被跟踪。

3. Staging Index

$ git add reset_lifecycle_file 

$ git status
位于分支 master
尚无提交
要提交的变更:
  (使用 "git rm --cached <文件>..." 以取消暂存)
        新文件:   reset_lifecycle_file 

所有变动的文件,Git 都记录在一个区域,叫做"暂存区"(Staging index)。我们通过 git add 指令将工作区中的内容保存到暂存区,这时已经实现了对文件的跟踪,但还没有请求提交。这时候的文件已经被跟踪了,reset_lifecycle_file 应该是绿色的。

4. Commit History

$ git commit -m "init commit"
[master(根提交) 88b5382] init commit
1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 reset_lifecycle_file

$ git status
位于分支 master
无文件要提交,干净的工作区 

暂存区保留变动的文件信息,等到修改结束添加到"提交历史"(Commit history)中,这就相当于当前项目的一个快照(snapshot)。
项目提交历史就是由不同时间的快照构成。Git 可以根据此提交信息将项目恢复到任意一个快照状态。

5. Reverse state switching

前面叙述并展示了三种状态之间的前向转换,现在我们反过来看一下,如何将当前状态转换成其父状态。

$ touch gitadd_test_file
$ vim gitadd_test_file 

新建一个测试文件并利用该文件进行不同状态之间转换的实验。

  • i 键进入编辑模式;
  • 键入 Hello world!
  • esc 退出编辑模式;
  • shift+;进入命令编辑模式;
  • 键入x回车即可保存到文件并退出vim
$ git status
位于分支 master
未跟踪的文件:
  (使用 "git add <文件>..." 以包含要提交的内容)
        gitadd_test_file

提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)

$ git add gitadd_test_file 
$ git commit -m "add one file for test"
[master d97ee77] add one file for test
 1 file changed, 1 insertion(+)
 create mode 100644 gitadd_test_file

$ git log --oneline
d97ee77 (HEAD -> master) add one file for test
88b5382 init commit 

按步骤2-4的方式将新创建的文件添加到提交历史中。现在我们尝试将已经提交 commit 但尚未 push 到远端仓库的状态返回到暂存区状态。此时的gitadd_test_file为绿色。

$ git reset --soft 88b538
$ git status
位于分支 master
要提交的变更:
  (使用 "git restore --staged <文件>..." 以取消暂存)
        新文件:   gitadd_test_file 

如上结果所示,这时已经处于 git commit 命令之前的状态,达到此结果使用的是 git reset --soft 指令。
该操作会保留文件的改动及索引状态,撤销完成后将回到添加改动的状态。注意与接下来要使用的 git reset --hard 之间的区别。此时的gitadd_test_file为绿色。

$ git restore --staged gitadd_test_file 
$ git status
位于分支 master
未跟踪的文件:
  (使用 "git add <文件>..." 以包含要提交的内容)
        gitadd_test_file

提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪) 

通过 git restore --staged 指令,我们得以将暂存区状态返回到工作区状态,也就是 git add 之前的状态。此时的gitadd_test_file为红色。

$ git add gitadd_test_file 
$ git commit -m "add test file for git add test"
[master d535a57] add test file for git add test
 1 file changed, 1 insertion(+)
 create mode 100644 gitadd_test_file

$ git log --oneline
d535a57 (HEAD -> master) add test file for git add test
88b5382 init commit
$ git reset --hard 88b538
HEAD 现在位于 88b5382 init commit
$ git status
位于分支 master
无文件要提交,干净的工作区 

注意这里是将 gitadd_test_file 重新添加到暂存区,然后保存到提交历史中。从提交历史中的状态直接返回到 git add 之前的状态使用的指令是 git reset --hard ,该指令强制将 HEAD 指针指向提交历史线中的前一个提交状态,会连同我们刚才新建的文件一起全部撤销。这是一个比较危险的举动,使用的时候要注意场合。当然即使这么操作了也并非就不能复原了,只是会多几步操作而已。

(全文完)

查看原文

赞 0 收藏 0 评论 0

老刘 发布了文章 · 3月30日

数据结构之复杂度

一.算法效率

  算法效率分析分为两种:

    1.时间效率

      时间效率又叫做时间复杂度,它衡量的主要是一个算法的运行速度。

    2.空间效率

  空间效率又叫做空间复杂度,它衡量的主要是一个算法所需要的额外空间。在计算机发展的早期,因为科技水平有限,往往计算机的容量很少,但如今科技急速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

二.时间复杂度

   1.概念

  在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是同一个算法在不同性能的机器上也会有不同的差异,所以才有了时间复杂度这个分析方式。

  因一个算法所花费的时间与其中语句的执行次数成正比例,所以,算法中的基本操作的执行次数,为算法的时间复杂度。

  比如所如下代码,它的执行次数为多少呢?

复制代码; "复制代码")

include <stdio.h>

void Func1(int N)
{ int count = 0; for (int i = 0; i < N ; ++ i)

{ for (int j = 0; j < N ; ++ j)
    { ++count;
    }
} for (int k = 0; k < 2 * N ; ++ k)
{ ++count;
} int M = 10; while (M--)
{ ++count;
}
printf("%dn", count);

}

复制代码; "复制代码")

  显而易见,它的执行次数为 F=NN+2N+10;

  往往我们在计算时间复杂度的时候,我们写的是一个概数,为什么呢?

  我们不妨设想一下,对于上面的执行次数函数,当N趋向于无穷大时,那个10对于它的影响微乎其微。如果是一个高阶函数,这个函数的大小往往取决于表达式中次数最高的项,所以我们的时间复杂度也采取这种思想。

  我们不妨来测试一下:

复制代码; "复制代码")

include <stdio.h> #include <time.h>

void Func1(int N)
{ int start = clock(); int count = 0; for (int i = 0; i < N ; ++ i)

{ for (int j = 0; j < N ; ++ j)
    { ++count;
    }
} for (int k = 0; k < 2 * N ; ++ k)
{ ++count;
} int M = 10; while (M--)
{ ++count;
} int end = clock();
printf("%dn", end - start);//单位为毫秒

} int main()
{

Func1(0);//0
Func1(100);//0
Func1(10000);//386
return 0;

}

复制代码; "复制代码")

  我们发现,差异主要是在最高次上面。所以接下来我们来介绍大O的渐进表示法。

   2.大O的渐进表示法

  实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。

  大O符号(Big O notation):是用于描述函数渐进行为的数学符号。

    推导大O阶方法:

      1、用常数1取代运行时间中的所有加法常数。

2、在修改后的运行次数函数中,只保留最高阶项。

3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

  所以,使用大O的渐进表示法以后,Func1的时间复杂度为:O(N^2);

  通过上面,我们可以得到现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

  另外有些算法的时间复杂度存在最好、平均和最坏情况:

    最坏情况:任意输入规模的最大运行次数(上界)

    平均情况:任意输入规模的期望运行次数

    最好情况:任意输入规模的最小运行次数(下界)

  例如:

复制代码; "复制代码")

//描述:传递一个数组,给定一个数,查找该数组中是否含有这个数
int FindNum(int* arr,int N,int search_num)
{ int i = 0; for(i=0;i<len;i++)

{ if(search_num == arr[i]) return 1;//找到则返回 1

} return 0;//找不到则返回 0
}

复制代码; "复制代码")

  它的最好情况为 1 ,即只执行一次就找到了

  平均情况:N/2

  最差情况: N ,遍历了整个数组。

  那么,对于这个算法,它的时间复杂度是多少呢?答案是: O(N)

  在计算时间复杂度时,我们往往取得是它的最差情况。

   3.示例

复制代码; "复制代码")

// 1.计算Func2的时间复杂度?
void Func2(int N)
{ int count = 0; for (int k = 0; k < 2 * N ; ++ k)

{ ++count;
} int M = 10; while (M--)
{ ++count;
}
printf("%dn", count);

}

复制代码; "复制代码")

  它的时间复杂度为多少呢? ---- O(N)

复制代码; "复制代码")

// 2.计算Func3的时间复杂度?
void Func3(int N, int M)
{ int count = 0; for (int k = 0; k < M; ++ k)
{ ++count;
} for (int k = 0; k < N ; ++ k)
{ ++count;
}
printf("%dn", count);
}

复制代码; "复制代码")

  答案是:O(N+M) ,因为在这个式子中,我们并不知道M和N的大小情况,如果N>>M,那这题的时间复杂度就是O(N)

复制代码; "复制代码")

//3. 计算Func4的时间复杂度?
void Func4(int N)
{ int count = 0; for (int k = 0; k < 100; ++ k)

{ ++count;
}
printf("%dn", count);

}

复制代码; "复制代码")

  对于它,执行次数为一个确定的常数:100,所以它的时间复杂度为 O(1)

复制代码; "复制代码")

// 4.计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n)
{

assert(a); for (size_t end = n; end > 0; --end)
{ int exchange = 0; for (size_t i = 1; i < end; ++i)
    { if (a[i-1] > a[i])
        {
            Swap(&a[i-1], &a[i]);
            exchange = 1;
        }
    } if (exchange == 0) break;
}

}

复制代码; "复制代码")

  对于实例4基本操作执行最好N次,最坏执行了(N*(N+1)/2次,通过推导大O阶方法+时间复杂度一般看最坏,所以时间复杂度为 O(N^2)

复制代码; "复制代码")

// 5.计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{

assert(a); int begin = 0; int end = n-1; while (begin < end)
{ int mid = begin + ((end-begin)>>1); if (a[mid] < x)
        begin = mid+1; else if (a[mid] > x)
        end = mid; else
        return mid;
} return -1;

}

复制代码; "复制代码")

  实例5基本操作执行最好1次,最坏O(logN)次,时间复杂度为 O(logN)(我们可以通过折纸查找的方式理解logN是怎么计算出来的)

  注:

    logN在算法分析中表示是底数为2,对数为N。有些地方会写成lgN。

// 6.计算阶乘递归Factorial的时间复杂度?
long long Factorial(size_t N)
{ return N < 2 ? N : Factorial(N-1)*N;
}

  对于递归的时间复杂度我们怎么算呢?

递归的时间复杂度  = 递归次数*每次递归中的次数

对于上题,一共递归了N次,每次递归中只有一次,所以时间复杂度为O(N)

//7. 计算斐波那契递归Fibonacci的时间复杂度?
long long Fibonacci(size_t N)
{ return N < 2 ? N : Fibonacci(N-1)+Fibonacci(N-2);
}

  在这个例中,一共递归了N次,而在递归中的次数为2^N-1,所以最后的时间复杂度为:O(N^2)

   注:

如果有一个递归有N层,且每一层中又有N次,那么它最后的时间复杂度为 O(N^2)

三.空间复杂度

   1.概念

  空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少 bytes的空间,因为这个也没太大意义,

  所以空间复杂度算的是变量的个数。

  空间复杂度计算规则基本跟实践 复杂度类似,也使用大O渐进表示法。(估算)

   2.示例

复制代码; "复制代码")

//1. 计算BubbleSort的空间复杂度?
void BubbleSort(int* a, int n)
{

assert(a); for (size_t end = n; end > 0; --end)
{ int exchange = 0; for (size_t i = 1; i < end; ++i)
    { if (a[i-1] > a[i])
        {
            Swap(&a[i-1], &a[i]);
            exchange = 1;
        }
    } if (exchange == 0) break;
}

}

复制代码; "复制代码")

  **我们要注意在计算时,我们计算的是算法需要的额外空间,不考虑数组输入的空间。并且我们要注意,空间是可以复用的,比如所某个算法需要创建一个变量并不断的为这个变量赋值,这时这个空间复杂度为O(1);
**

所以,因实例1使用了常数个额外空间,所以空间复杂度为 O(1)

复制代码; "复制代码")

//2. 计算生成的Fibonacci数组的空间复杂度?
long long* Fibonacci(size_t n)
{ if(n==0)

{ return NULL;
} long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));

fibArray[0] = 0;//斐波那契数的第一个
fibArray[1] = 1;//斐波那契数的第二个
for (int i = 2; i <= n ; ++i)//利用循环来生成斐波那契数

{

    fibArray[i ] = fibArray[ i - 1] + fibArray [i - 2];
} return fibArray ;

}

复制代码; "复制代码")

  实例 2动态开辟了N个空间来放N个斐波那契数,所以空间复杂度为 O(N)

//3. 计算阶乘递归Factorial的空间复杂度?
long long Factorial(size_t N)
{ return N < 2 ? N : Factorial(N-1)*N;
}

  递归算法的空间复杂度通常情况下为 递归的深度 (递归的次数)

  实例3递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。所以空间复杂度为O(N).

那么在最后我们再来看一张图:

  这就说明有时候  不同的时间复杂度在N小的时候都还接近,当N越来越大时差距越来越多!

查看原文

赞 0 收藏 0 评论 0

老刘 发布了文章 · 3月22日

Pycharm激活码,免费!!!2021年最新Pycharm2020.3.3版激活码~

一、Pycharm激活码免费获取方法

关注微信公众号: Python联盟 ,然后回复 “ 激活码 ”,即可获取2021年最新有效的免费Pycharm激活码,公众号的Pycharm激活码每24小时更新一次~

Pycharm激活码

二、PyCharm使用技巧

1. 神一样的调试技巧


如果您在搜寻器项目中,请使用正则表达式来匹配要搜寻的内容。一步就能做这种事情的人数。通常需要大量调试才能获得预期的匹配。您需要更改一次常规规则,然后再次运行它们,并重新获取来自网站的请求,以确保没有匹配项。然后更改版本,然后重试。您还需要启动请求。 ..结果,我们可以看到还没有匹配项。Pycharm最新激活码,对于那些不擅长正规化的人,学生可能不得不尝试数十次。

答案当然是有。

假如我在调试如下几行简单的代码。在第 3 行处打了个断点。然后点击图示位置 Show Python Prompt 按钮。

2. 指定参数执行脚本


我在Pycharm中运行我的项目,通常如何运行它?我的方法是右键单击并单击运行,或使用快捷键“ Shift + F10”。

在某些情况下,在运行/调试脚本时,需要指定一些可以直接在命令行上指定的参数。

刚接触Pycharm的学生可能不知道Pycharm可以指定参数。单击下面照片中的位置

image.png

Pycharm激活码2021年

3. 搜索时过滤测试文件


接下来,当我查看框架的源代码时,我将介绍一些技巧。这可能仅适用于少数人。我通常看到的框架是OpenStack。我不知道其他框架是什么样的,但是OpenStack有很多(确实很多)单元测试文件。使用FindinPath时,这引起了很多问题。您可以从下图中的搜索结果中感觉到它。当您搜索功能时,测试文件中的结果要比常规文件中的结果高得多。

pycharm激活码

三、Pycharm激活码免费获取渠道汇总整理

  1. Pycharm激活码密钥,2021年3月Pycharm永久注册码 - 简书
  2. Pycharm激活码无偿共享,2021年最新永久激活码!: 2021年最新Pycharm永久激活码分享,每24小时更新一次~
  3. Pycharm激活码2021年,可以永久白嫖最新专业版! - 哔哩哔哩
  4. Pycharm激活码免费使用,Pycharm专业版激活码2021年!
查看原文

赞 0 收藏 0 评论 0

老刘 发布了文章 · 3月22日

IDEA最新激活码,2021年最新IDEA安装教程与激活

一、IDEA激活码获取方式

关注微信公众号:Java团长,然后回复 “ 激活码 ” 即可获取2021年最新IDEA激活码,公众号的激活码每24小时更新一次~

IDEA激活码

二、IDEA安装教程

1、打开官网:http://www.jetbrains.com/idea/,点击页面中的“DOWNLOAD”

image.png

2、根据自己的需要选择下载的IntelliJ IDEA版本,此处我的电脑是Windows7 64 位的,我选择的是Community(社区版)
在这里插入图片描述

3、双击下载好的安装包
在这里插入图片描述
4、点击“Next”
在这里插入图片描述

5、选择安装位置,然后点击“Next”

在这里插入图片描述
6、勾选安装选型,然后点击“Next”
在这里插入图片描述

7、点击“Install”,进入安装
在这里插入图片描述
在这里插入图片描述
8、安装完成,点击“Finish”
在这里插入图片描述

三、IDEA激活码获取途径汇总

  1. IDEA最新激活码2021(IDEA2020.3永久激活方法)-Java团长-51CTO博客
  2. IDEA激活码,2021年最新IDEA永久激活码分享
  3. IDEA激活码,2021年最新IDEA永久激活码 - 简书
  4. IntelliJ IDEA 2021年最新激活码免费获取(IDEA永久激活注册码): IntelliJ IDEA 2021年最新激活码免费获取(IDEA永久激活注册码)
  5. IntelliJ IDEA激活码,绝对有效!(2021年最新永久激活码) - osc_ed2py9ot的个人空间 - OSCHINA - 中文开源技术交流社区
查看原文

赞 0 收藏 0 评论 0

老刘 发布了文章 · 3月22日

给Mybatis-Plus插上小翅膀,支持多表查询

之前一直使用Mybatis-Plus,说实话,个人还是比较喜欢Mybatis-Plus

ORM框架用的比较多的就两个,JPAMybatis。据说国内用Mybatis比较多,国外用JPA比较多。

Mybatis-Plus是在Mybatis的基础上,增加了很多牛🍺的功能。

再粘一下官网介绍的特性,又啰嗦了:

  1. 无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑
  2. 损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对象操作
  3. 强大的 CRUD 操作:内置通用 Mapper、通用 Service,仅仅通过少量配置即可实现单表大部分 CRUD 操作,更有强大的条件构造器,满足各类使用需求
  4. 支持 Lambda 形式调用:通过 Lambda 表达式,方便的编写各类查询条件,无需再担心字段写错
  5. 支持主键自动生成:支持多达 4 种主键策略(内含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解决主键问题
  6. 支持 ActiveRecord 模式:支持 ActiveRecord 形式调用,实体类只需继承 Model 类即可进行强大的 CRUD 操作
  7. 支持自定义全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  8. 内置代码生成器:采用代码或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 层代码,支持模板引擎,更有超多自定义配置等您来使用
  9. 内置分页插件:基于 MyBatis 物理分页,开发者无需关心具体操作,配置好插件之后,写分页等同于普通 List 查询
  10. 分页插件支持多种数据库:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer 等多种数据库
  11. 内置性能分析插件:可输出 Sql 语句以及其执行时间,建议开发测试时启用该功能,能快速揪出慢查询
  12. 内置全局拦截插件:提供全表 delete 、 update 操作智能分析阻断,也可自定义拦截规则,预防误操作

详细的可以去官网看:mybatis.plus/ 官网新域名也是牛🍺。反正用过的都说好。

至于JPA,虽然个人觉得有点太死板,不过也有值得学习的地方。

很早以前,用Mybatis-Plus的时候,有一个比较麻烦的问题,就是如果一组数据存在多张表中,这些表之间可能是一对一,一对多或者多对一,那我要想全部查出来就要调好几个Mapper的查询方法。代码行数一下就增加了很多。

之前也看过Mybatis-Plus的源码,想过如何使Mybatis-Plus支持多表联接查询。可是发现难度不小。因为Mybatis-Plus底层就只支持单表。

最近看到JPA@OneToOne@OneToMany@ManyToMany这些注解,忽然一个想法就在我的脑海里闪现出来,如果像JPA那样使用注解的方式,是不是简单很多呢?

事先声明,全是自己想的,没有看JPA源码, 所以实现方式可能和JPA不一样。

说干就干

  1. 添加注解
  2. 处理注解
  3. 打包发布

可能有人不知道,其实Mybatis也是支持拦截器的,既然如此,用拦截器处理注解就可以啦。

注解 One2One

@Inherited
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface One2One {

    /**
     * 本类主键列名
     */
    String self() default "id";

    /**
     * 本类主键在关联表中的列名
     */
    String as();

    /**
     * 关联的 mapper
     */
    Class<? extends BaseMapper> mapper();

}

说一下,假如有两张表,AB是一对一的关系,AidB表中是a_id,用这样的方式关联的。 在A的实体类中使用这个注解,self就是id,而as就是a_id,意思就是Aid作为a_id来查询,而mapper就是BMapper,下面是例子A就是UserAccountB就是UserAddress

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "UserAccount对象", description = "用户相关")
public class UserAccount extends Model<UserAccount> {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @ApiModelProperty(value = "昵称")
    private String nickName;

    @TableField(exist = false)
    //把id的值 作为userId  在 UserAddressMapper中 查询
    @One2One(self = "id", as = "user_id", mapper = UserAddressMapper.class)
    private UserAddress address;

    @Override
    protected Serializable pkVal() {
        return this.id;
    }
}

Mybatis拦截器 One2OneInterceptor

这里不再详细介绍拦截器了,之前也写了几篇关于Mybatis拦截器的,有兴趣的可以去看看。

Mybatis拦截器实现Geometry类型数据存储与查询

Mybatis拦截器打印完整SQL

@Intercepts({
        @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
@Slf4j
public class One2OneInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object result = invocation.proceed();
        if (result == null) {
            return null;
        }
        if (result instanceof ArrayList) {
            ArrayList list = (ArrayList) result;
            for (Object o : list) {
                handleOne2OneAnnotation(o);
            }
        } else {
            handleOne2OneAnnotation(result);
        }
        return result;
    }

    @SneakyThrows
    private void handleOne2OneAnnotation(Object o) {
        Class<?> aClass = o.getClass();
        Field[] fields = aClass.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            One2One one2One = field.getAnnotation(One2One.class);
            if (one2One != null) {
                String self = one2One.self();
                Object value = MpExtraUtil.getValue(o, self);
                String as = one2One.as();
                Class<? extends BaseMapper> mapper = one2One.mapper();
                BaseMapper baseMapper = SpringBeanFactoryUtils.getApplicationContext().getBean(mapper);
                QueryWrapper<Object> eq = Condition.create().eq(as, value);
                Object one = baseMapper.selectOne(eq);
                field.set(o, one);
            }
        }
    }

    @Override
    public Object plugin(Object o) {
        return Plugin.wrap(o, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

Mybatis拦截器可以针对不同的场景进行拦截,比如:

  1. Executor:拦截执行器的方法。
  2. ParameterHandler:拦截参数的处理。
  3. ResultHandler:拦截结果集的处理。
  4. StatementHandler:拦截Sql语法构建的处理。

这里是通过拦截结果集的方式,在返回的对象上查找这个注解,找到注解后,再根据注解的配置,自动去数据库查询,查到结果后把数据封装到返回的结果集中。这样就避免了自己去多次调Mapper的查询方法。

难点:虽然注解上标明了是什么Mapper,可是在拦截器中取到的还是BaseMapper,而用BaseMapper实在不好查询,我试了很多方法,不过还好Mybatis-Plus支持使用Condition.create().eq(as, value);拼接条件SQL,然后可以使用baseMapper.selectOne(eq);去查询。

public class MpExtraUtil {

    @SneakyThrows
    public static Object getValue(Object o, String name) {
        Class<?> aClass = o.getClass();
        Field[] fields = aClass.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            if (field.getName().equals(name)) {
                return field.get(o);
            }
        }
        throw new IllegalArgumentException("未查询到名称为:" + name + " 的字段");
    }
}

MpExtraUtil就是使用反射的方式,获取id的值。

再讲一个多对多的注解
@Inherited
@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Many2Many {

    /**
     * 本类主键列名
     */
    String self() default "id";

    /**
     * 本类主键在中间表的列名
     */
    String leftMid();

    /**
     * 另一个多方在中间表中的列名
     */
    String rightMid();

    /**
     * 另一个多方在本表中的列名
     */
    String origin();

    /**
     * 关联的 mapper
     */
    Class<? extends BaseMapper> midMapper();

    /**
     * 关联的 mapper
     */
    Class<? extends BaseMapper> mapper();

假设有AA_BB三张表,在A的实体类中使用这个注解, self就是A表主键idleftMid就是A表的id在中间表中的名字,也就是a_id,而rightMidB表主键在中间表的名字,就是b_id, origin就是B表自己主键原来的名字,即idmidMapper是中间表的Mapper,也就是A_B对应的MappermapperB表的Mapper

这个确实有点绕。

还有一个@One2Many就不说了,和@One2One一样,至于Many2One,从另一个角度看就是@One2One

使用方法

先把表创建好,然后代码生成器一键生成代码。

CREATE TABLE `user_account` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  `nick_name` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '昵称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户相关';

CREATE TABLE `user_address` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '地址id',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  `address` varchar(200) DEFAULT NULL COMMENT '详细地址',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `user_class` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '课程id',
  `class_name` varchar(20) DEFAULT NULL COMMENT '课程名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `user_hobby` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '爱好id',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  `hobby` varchar(40) DEFAULT NULL COMMENT '爱好名字',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `user_mid_class` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '中间表id',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  `class_id` bigint(20) DEFAULT NULL COMMENT '课程id',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

添加依赖,已经发布到中央仓库了,可以直接使用:

<dependency>
    <groupId>top.lww0511</groupId>
    <artifactId>mp-extra</artifactId>
    <version>1.0.1</version>
</dependency>

在启动类上添加注解

@EnableMpExtra

配置拦截器

因为一般项目都会配置自己的MybatisConfiguration,我在这里配置后,打包,然后被引入,是无法生效的。

所以就想了一种折中的方法。

以前MybatisConfiguration是通过new出来的,现在通过MybatisExtraConfig.getMPConfig();来获取,这样获取到的MybatisConfiguration就已经添加好了拦截器。

完整Mybatis-Plus配置类例子,注意第43行:

@Slf4j
@Configuration
@MapperScan(basePackages = "com.ler.demo.mapper", sqlSessionTemplateRef = "sqlSessionTemplate")
public class MybatisConfig {

    private static final String BASE_PACKAGE = "com.ler.demo.";

    @Bean("dataSource")
    public DataSource dataSource() {
        try {
            DruidDataSource dataSource = new DruidDataSource();
            dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dataSource.setUrl("jdbc:mysql://localhost/mp-extra?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai");
            dataSource.setUsername("root");
            dataSource.setPassword("adminadmin");

            dataSource.setInitialSize(1);
            dataSource.setMaxActive(20);
            dataSource.setMinIdle(1);
            dataSource.setMaxWait(60_000);
            dataSource.setPoolPreparedStatements(true);
            dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
            dataSource.setTimeBetweenEvictionRunsMillis(60_000);
            dataSource.setMinEvictableIdleTimeMillis(300_000);
            dataSource.setValidationQuery("SELECT 1");
            return dataSource;
        } catch (Throwable throwable) {
            log.error("ex caught", throwable);
            throw new RuntimeException();
        }
    }

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean();
        factoryBean.setDataSource(dataSource);
        factoryBean.setVfs(SpringBootVFS.class);
        factoryBean.setTypeAliasesPackage(BASE_PACKAGE + "entity");

        Resource[] mapperResources = new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml");
        factoryBean.setMapperLocations(mapperResources);
        // 43行 获取配置
        MybatisConfiguration configuration = MybatisExtraConfig.getMPConfig();
        configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        configuration.setJdbcTypeForNull(JdbcType.NULL);
        configuration.setMapUnderscoreToCamelCase(true);
        configuration.addInterceptor(new SqlExplainInterceptor());
        configuration.setUseGeneratedKeys(true);
        factoryBean.setConfiguration(configuration);
        return factoryBean.getObject();
    }

    @Bean(name = "sqlSessionTemplate")
    public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean(name = "transactionManager")
    public PlatformTransactionManager platformTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "transactionTemplate")
    public TransactionTemplate transactionTemplate(@Qualifier("transactionManager") PlatformTransactionManager transactionManager) {
        return new TransactionTemplate(transactionManager);
    }

}

在实体类上建立关系

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value = "UserAccount对象", description = "用户相关")
public class UserAccount extends Model<UserAccount> {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "id")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    @ApiModelProperty(value = "昵称")
    private String nickName;

    @TableField(exist = false)
    //把id的值 作为userId  在 UserAddressMapper中 查询
    @One2One(self = "id", as = "user_id", mapper = UserAddressMapper.class)
    private UserAddress address;

    @TableField(exist = false)
    @One2Many(self = "id", as = "user_id", mapper = UserHobbyMapper.class)
    private List<UserHobby> hobbies;

    @TableField(exist = false)
    @Many2Many(self = "id", leftMid = "user_id", rightMid = "class_id", origin = "id"
            , midMapper = UserMidClassMapper.class, mapper = UserClassMapper.class)
    private List<UserClass> classes;

    @Override
    protected Serializable pkVal() {
        return this.id;
    }

}

主要是那几个注解。对了还要加@TableField(exist = false),不然会报错。

源码:https://www.douban.com/note/7...

测试接口

@Slf4j
@RestController
@RequestMapping("/user")
@Api(value = "/user", description = "用户")
public class UserAccountController {

    @Resource
    private UserAccountService userAccountService;

    @Resource
    private UserAccountMapper userAccountMapper;

    @ApiOperation("查询一个")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "", value = "", required = true),
    })
    @GetMapping(value = "/one", name = "查询一个")
    public HttpResult one() {
        //service
        UserAccount account = userAccountService.getById(1L);
        //mapper
        // UserAccount account = userAccountMapper.selectById(1L);
        //AR模式
        // UserAccount account = new UserAccount();
        // account.setId(1L);
        // account = account.selectById();
        return HttpResult.success(account);
    }
}

接口非常简单,调用内置的getById,可是却查出了所有相关的数据,这都是因为配置的那些注解。

可以看到其实发送了好几条SQL。第一条是userAccountService.getById(1L),后面几条都是自动发送的。

总结

实在不想贴太多代码,其实还是挺简单的,源码地址还有示例地址都贴出来啦,有兴趣的可以去看一下。觉得好用可以点个Star。欢迎大家一起来贡献。

查看原文

赞 0 收藏 0 评论 0

老刘 发布了文章 · 3月22日

浅谈容器性能监控

写在前面

最近在研究docker集群(kubernetes)的监控,为了彻底弄清楚,简单看了一点源码。这里分享一下我学到的东西。

docker api: stats

首先是docker的api,stats的具体使用场景如:

http://$dockerip:2375/containers/$containerid/stats

可以获取docker机器上某一个容器的状态,该请求的response会持续的写响应报文回来(每秒一次)

http://$dockerip:2375/containers/$containerid/stats?stream=false

参数stream默认是true,设为false后,response只写一次。

docker中该api对应的操作,就相当于docker stats $CID 这条命令,它调用到了ContainerStats()方法(位于docker项目目录的/daemon/stats.go 第19行),函数中启动了一个收集器:

daemon.SubscribeToContainerStats(name)
复制代码

收集器定时写数据到update变量中。 然后启动一个协程读数据,获取数据的途径包括:

update := v.(*execdriver.ResourceStats)

nwStats, err := daemon.getNetworkStats(name)

可以看到网络状态是另外读的,而cpu,内存状态在哪读呢?我们一步步跳转,看到在这里: /daemon/execdriver/driver_linux.go 112行:

func Stats(containerDir string, containerMemoryLimit int64, machineMemory int64) (*ResourceStats, error)

在这个函数里我们看得到,其实所谓的获取容器状态,就是读文件而已。我们举一个已经在docker运行的容器来说:

 cat /run/docker/execdriver/native/$containerID/state.json

这里面记录了一个cgroup_paths字段,他的值是一个路径,通过cstats, err := mgr.GetStats()程序才真正拿到了监控数据,如cpu的状态信息,存储在一个如下的路径中:

cd /sys/fs/cgroup/cpu,cpuacct/system.slice/docker-9b479ceaf3bef1caee2c37dfdc982a968144700a2c42991b238b938788a8a91f.scope

又比如内存的大致信息存在:

cat /sys/fs/cgroup/memory/system.slice/docker-9b479ceaf3bef1caee2c37dfdc982a968144700a2c42991b238b938788a8a91f.scope/memory.stat

具体里面放了什么数据,大家就自己看咯。

还剩下一个数据,也是我们讨论的重点:网络IO。

我们回到/daemon/stats.go: 看看函数getNetworkStats(name string): 每个docker容器创建后都会又docker创建一个网卡,网卡名以veth开头,后面是一串随机生成的十六进制码。 我们只要找到该网卡,然后,像上文的cpu,内存状态获取的方法一样,去找文件就行了。 然而这个网卡名似乎是存在内存中,至少我没有看到docker将这个网卡名存到别的地方。 代码中: nw, err := daemon.netController.NetworkByID(c.NetworkSettings.NetworkID) 可以看出获取了网卡(network),尔后有一个Statistics()方法,我们继续跟踪会发现,docker调用了一个组件包: github.com/docker/libcontainer 其中有statistics方法,并执行了一个cat的命令(该组件包的/sandbox/interface_linux.go 第174行): data, err := exec.Command("cat", netStatsFile).Output()

是的,又是读文件。 这次读的文件在:/sys/class/net/目录下, 我们进入该目录可以看到有许多子目录,其中就包括了docker启动容器时生成的网卡。

个人猜测:docker在内存中保存了容器到网卡的映射关系。通过这个映射关系可以找到网卡的统计数据(数据由内核生成,记录在机器的文件中) 我们可以看到如下的数据: clipboard.png

将这些数据统一起来,就是docker stats 这条命令干的事。

kubernetes如何调用上述的监控功能

kubernetes的监控采用了cAdvisor组件。因为kubernetes中记录了容器的信息(但是没有记录容器-网卡的映射关系),所以运行在节点上的cAdvisor不需要通过docker stats去获取容器的cpu和内存使用数据。 而网络IO数据呢?

我们知道k8s部署运行一个容器是会先生成一个pause容器。 是的,网络IO都记录在pause容器中。这里大家可以在自己的k8s环境中验证。

所以只要我们获取某容器对应的pause容器的containerID,我们就可以用如上的方式去抓到网络IO。

因为cAdvisor并不是为k8s专门设计的,不会特地在获取网络IO时去遍历查找容器对应的pause容器。所以当前的cAdvisor没有办法获取容器的网络IO。

所以如果在使用k8s集群,想要加入网络IO监控功能的时候,可以从k8s自带的cAdvisor入手。

cAdvisor

上面讲到cAdvisor,那么cAdvisor是如何获取网络的IO的呢? 首先,在k8s(1.0.6版本)的/pkg/kubelet/cadvisor/cadvisor_linux.go中51行,New(port int)方法,这是kubelet调用cAdvisor的入口。实例化一个cAdvisorClient,并执行他的Start()方法,我们可以进入cAdvisor项目中找到该方法(github.com/google/cadvisor/manager/manager.go 195行)。

我们看到有一句

err docker.Register(self, self.fsInfo)

先mark之。继续看:

glog.Infof("Starting recovery of all containers")
err = self.detectSubcontainers("/")

这里程序程序检查所有在运行的容器,同内存中记录的容器作比较,有新的容器就新建一个相关的处理机制:一个Container的结构(有减少的,就删除),然后执行cont.Start() (github.com/google/cadvisor/manager/manager.go 766行)

持续追踪我们就可以知道每秒它收集一次监控信息,收集方法即

stats, statsErr := c.handler.GetStats()

(/cadvisor/manager/container.go 401行) handler是一个接口。我们得知道当时是如何给他赋值的。这里不赘述了,我们直接找关键的方法:

func (self *dockerContainerHandler) GetStats() (*info.ContainerStats, error)

(cadvisor/container/docker/handler.go 第305行)

它又调用了:

func GetStats(cgroupManager cgroups.Manager, networkInterfaces []string) (*info.ContainerStats, error)

(cadvisor/container/libcontainer/helpers.go 第77行)

这里就是真正获取监控信息的地方,它引入了libcontainer包中的同名方法,

最终是导入了"github.com/docker/libcontainer/cgroups"这个第三方包中的方法。是不是很熟悉?对就是上文中提到的docker stats获取网络IO用到的包。我们到这个包里找到函数: (/libcontainer/cgroups/fs/apply_raw.go 第148行)

func (m *Manager) GetStats() (*cgroups.Stats, error) {
    m.mu.Lock()
    defer m.mu.Unlock()
    stats := cgroups.NewStats()
    for name, path := range m.Paths {
        //m.Paths中包括了cpu,memory,network等字段,根据这些字段找到相应的目录
        sys, ok := subsystems[name]
        if !ok || !cgroups.PathExists(path) {
            continue
        }
        //读目录里的文件获取监控信息
        if err := sys.GetStats(path, stats); err != nil {
            return nil, err
        }
    }

    return stats, nil
}

我们可以看到libcontainer/cgroups/fs目录下有cpu.go,memory.go等文件,每一个文件中都有一个集成了GetStats()的结构,我们可以获取到任何数据。

我们再回到cadvisor项目中cadvisor/container/libcontainer/helpers.go 第77行,

往下看:

// TODO(rjnagal): Use networking stats directly from libcontainer.
    stats.Network.Interfaces = make([]info.InterfaceStats, len(networkInterfaces))
    for i := range networkInterfaces {
        interfaceStats, err := sysinfo.GetNetworkStats(networkInterfaces[i])
        if err != nil {
            return stats, err
        }
        stats.Network.Interfaces[i] = interfaceStats
    }

这里官方还用了别的手段再去找network的数据,虽然不知道是处于什么理由,也不管这里是去找哪个系统文件看状态,但是这样依然是拿不到数据的。因为根本没有找到容器对应的网卡。这里找网卡的方法在cadvisor/container/docker/handler.go 第311行:

var networkInterfaces []string
    if len(config.Networks) > 0 {
        // ContainerStats only reports stat for one network device.
        // TODO(vmarmol): Handle multiple physical network devices.
        for _, n := range config.Networks {
            // Take the first non-loopback.
            if n.Type != "loopback" {
                networkInterfaces = []string{n.HostInterfaceName}
                break
            }
        }
    }
    stats, err := containerLibcontainer.GetStats(self.cgroupManager, networkInterfaces)

很显然这里是要改进的(在k8s的环境下)。

注:随着版本更新,新版本的cAdvisor采用github.com/opencontainers包。详见:github.com/opencontainers/runc/libcontainer/container_linux.go line 151: 另外,cadvisor获取网络监控数据的正确途径应该是: 监控pause容器,在其Pid对应的/proc/$pid/net/dev文件中即可得到容器内部网络监控数据

磁盘监控

cadvisor在主函数初始化时,通过一些init方法在内存中构建了机器上的文件系统的结构(有哪些磁盘、分别挂载目录是啥、maj和min号是啥)。随后,cadvisor会在监控机器的性能时,分为磁盘使用情况的监控和磁盘读写情况的监控。

磁盘使用情况的监控

cadvisor会执行syscall.Statfs方法,通过linux系统调用,获得磁盘的总大小、可用空间、inodes总数和使用数等信息

磁盘读写情况的监控

通过读取机器的/proc/diskstats文件内容,形如:

cat /proc/diskstats 
 254       0 vda 48560 0 994278 64304 2275295 70286 18962312 6814364 0 1205480 6877464
 254       1 vda1 48177 0 991034 64008 1865714 70286 18962304 6777592 0 1170880 6840740
 254      16 vdb 700 0 4955 284 0 0 0 0 0 284 284

通过这些数据计算出各个存储设备的读写次数、读写速度。(这里文件内容释义和计算方式可以参见www.udpwork.com/item/12931.…

暴露接口

当我们调用cadvisor的接口: /api/v2.1/machinestats 时,就能从返回的json结构中找到filesystem字段,包含了磁盘读写性能和磁盘使用情况的监控数据

缺陷

上文提到,cadvisor在初始化阶段就生成了机器文件系统的抽象结构,但是这个结构不会动态检查和更新,当机器上动态挂载了一个数据盘(比如使用ceph rbd做pod的pv),cadvisor不会感知,这个新挂的盘的监控数据也无法被感知。

目前社区暂时没有对这个功能进行优化。

Prometheus

这里写Prometheus其实只是想介绍一下我们上面提到的性能数据如何更有效地被采集和使用。

kubelet启动时,会监听在10250端口,并提供一个/metrics url, 其下还有 /metrics/cadvisor url ,分别提供kubelet自身工作流程的数据监控、以及cadvisor提供的性能监控信息。

这些信息以Prometheus Metrics 的格式返回给客户端。 并且可以通过相应的Grafana组件展示、通过Prometheus分析并提供报警。

参考:

查看原文

赞 0 收藏 0 评论 0

老刘 关注了用户 · 2018-03-03

SangSir @sangsir

艺术界的一朵奇葩

关注 2520

老刘 关注了用户 · 2018-03-03

火狼 @huolang_5a14e07d2f2ff

vue,react,小程序,ts,php,node乱炖

关注 3194

认证与成就

  • 获得 0 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-03-02
个人主页被 528 人浏览