精华内容
下载资源
问答
  • 右键你的VC程序,选择打开文件所在位置,然后找到MFC文件夹,友情提供两个文件夹的位置截图。 建议编译的C文件以cpp后缀保存。 2.编程 2.1.创建你的界面 \qquad创建一个480×360的窗口,我们需要使用initgraph()函数...

    0.引言

    \qquad 看了CSDN上很多关于C程序图形化界面的介绍,有的代码繁琐难解,不方便调试修改;有的不够详细。本文提供的代码简单、易于移植、容易理解,望急需使用C语言制作图形化界面的朋友采纳。
    \qquad 对easyx尚不熟悉的朋友不需要担心,我敢打包票它只需10分钟就可以上手,而它为你节省的时间可能是3个小时甚至更多。关于easyx的简单应用请参考一篇我以前写的关于C程序可视化的博文。
    →【C语言实现动画控制】←
    \qquad 本文的讲解是循序渐进的,读者应该重点关注每个步骤的理解,两步之间代码的变化,即可完全理解本文。

    1.素材准备

    1. easyx的下载链接如下:(本文使用的版本是2014冬至版)
      https://www.easyx.cn/downloads/
      注:使用easyx需要注意它兼容的编译器(下载的帮助文件会写),不同的easyx兼容的编译器不同,但总是和visual C++6兼容(和字符编码有关),本文以visual C++6编译器为例书写代码。
    2. easyx的最新英文帮助文档链接(下载2014冬至版会自带中文帮助文档):
      https://docs.easyx.cn/en-us/intro
    3. 如果你成功下载了easyx2014冬至版,那么解压后把头文件(easyx.h和graphic.h)和lib文件(amd64)分别放在VC文件夹默认的include文件夹和lib文件夹中。右键你的VC程序,选择打开文件所在位置,然后找到MFC文件夹,友情提供两个文件夹的位置截图。
      include
      lib
    4. 建议编译的C文件以cpp后缀保存。

    2.编程

    2.1.创建你的界面

    \qquad 创建一个480×360的窗口,我们需要使用initgraph()函数,闲言少叙,让我们直接看一段代码:

    #include <graphics.h>              // 引用图形库头文件
    #include <conio.h>
    #include <stdio.h>
    #include <windows.h>				//用到了定时函数sleep()
    #include <math.h>
    
    int main()
    {
    	int i;
    	short win_width,win_height;//定义窗口的宽度和高度
    	win_width = 480;win_height = 360;
    	initgraph(win_width,win_height);//初始化窗口(黑屏)
    	for(i=0;i<256;i+=5)
    	{
    		setbkcolor(RGB(i,i,i));//设置背景色,原来默认黑色
    		cleardevice();//清屏(取决于背景色)
    		Sleep(15);//延时15ms
    	}
    	closegraph();//关闭绘图界面
    }
    
    

    \qquad 这段代码很容易理解,运行这段程序,就会出现逐渐明亮的屏幕。因为不断刷新背景色为 R G B ( i , i , i ) RGB(i,i,i) RGB(i,i,i)。C语言中的颜色使用十六进制表示的,RGB函数可以将0~255范围内的三个整数三原色转换成这个十六进制。
    \qquad cleardevice()函数用于清屏,是界面内所有元素都被清空,一般只会在初始化出现。
    \qquad Sleep()是毫秒级延迟,当然界面变亮时间不一定是准确的15ms×255/5=0.765s,因为其他语句还需要执行时间。
    \qquad closegraph():关闭绘图界面。注意,如果初始化了绘图界面但没有在主函数结束前关闭它,可能会引发一些莫名其妙的错误!所以这个函数一定要有!

    2.2.创建按钮

    \qquad 我们尝试在界面创建几个按钮,按钮需要的操作是绘制矩形和打印文字。虽然看着简单,但是里面还是有点学问,为了方便大家理解,还是先放上代码和注释。

    #include <graphics.h>              // 引用图形库头文件
    #include <conio.h>
    #include <stdio.h>
    #include <windows.h>				//用到了定时函数sleep()
    #include <math.h>
    int r1[]={30,20,130,60};//输入按钮的矩形参数
    int r2[]={170,20,220,60};//运行按钮的矩形参数
    int r3[]={260,20,310,60};//退出按钮的矩形参数
    int main()
    {
    	int i;
    	short win_width,win_height;//定义窗口的宽度和高度
    	win_width = 480;win_height = 360;
    	initgraph(win_width,win_height);//初始化窗口(黑屏)
    	for(i=0;i<256;i+=5)
    	{
    		setbkcolor(RGB(i,i,i));//设置背景色,原来默认黑色
    		cleardevice();//清屏(取决于背景色)
    		Sleep(15);//延时15ms
    	}
    	RECT R1={r1[0],r1[1],r1[2],r1[3]};//矩形指针R1
    	RECT R2={r2[0],r2[1],r2[2],r2[3]};//矩形指针R2
    	RECT R3={r3[0],r3[1],r3[2],r3[3]};//矩形指针R3
    	LOGFONT f;//字体样式指针
    	gettextstyle(&f);					//获取字体样式
    	_tcscpy(f.lfFaceName,_T("宋体"));	//设置字体为宋体
    	f.lfQuality = ANTIALIASED_QUALITY;    // 设置输出效果为抗锯齿  
    	settextstyle(&f);                     // 设置字体样式
    	settextcolor(BLACK);				//BLACK在graphic.h头文件里面被定义为黑色的颜色常量
    	drawtext("输入参数",&R1,DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域R1内输入文字,水平居中,垂直居中,单行显示
    	drawtext("运行",&R2,DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域R2内输入文字,水平居中,垂直居中,单行显示
    	drawtext("退出",&R3,DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域R3内输入文字,水平居中,垂直居中,单行显示
    	setlinecolor(BLACK);
    	rectangle(r1[0],r1[1],r1[2],r1[3]);
    	rectangle(r2[0],r2[1],r2[2],r2[3]);
    	rectangle(r3[0],r3[1],r3[2],r3[3]);
    	system("pause");//暂停,为了显示
    	closegraph();
    	return 0;
    }
    

    在这里插入图片描述
    \qquad 这里需要特别介绍的是矩形指针 p R e c t pRect pRect,它使用句柄RECT定义,并且不可以中途再次赋值。之所以要设置矩形指针了为了打印字体的时候以矩形为边界自动填充。它的格式是RECT r={X1,Y1,X2,Y2},X1和X2是矩形的左边和右边的横坐标,Y1和Y2是矩形的上边和下边的纵坐标,这一点和rectangle()绘制空心矩形函数参数排列一致。后面的DT_CENTER | DT_VCENTER | DT_SINGLELINE就是描述填充格式的常量。使用drawtext书写文字不需要再计算文字的坐标和设置大小,会方便很多。
    \qquad LOGFONT是字体样式指针,通过gettextstyle()函数来获取当前的字体类型,再通过settextstyle()函数加以设置。这里只修改了字体的名称和显示质量,还可以修改斜体、下划线等属性,更详细的部分请参考帮助文档。

    2.3.鼠标操作

    2.3.1.单击特效

    \qquad 作为一个图形化界面的C程序,鼠标操作总不能少吧。在讲解程序前先别着急,简单为大家科普一下鼠标事件:
    \qquad 鼠标是输入设备,只要发生以下的事件,就会暂存在鼠标消息列表中,我们的操作系统就会依次响应列表中的鼠标消息事件,常用的鼠标事件如下:

    • WM_MOUSEMOVE——鼠标移动
    • WM_MOUSEWHEEL——鼠标滚轮滚动
    • WM_LBUTTONDOWN——鼠标左键按下
    • WM_LBUTTONUP——鼠标左键弹起
    • WM_LBUTTONDBLCLK——鼠标左键双击
    • WM_RBUTTONDOWN——鼠标右键按下
    • WM_RBUTTONUP——鼠标右键弹起
    • WM_RBUTTONDBLCLK——鼠标左键双击
    • WM_MBUTTONDOWN——鼠标中键按下
    • WM_MBUTTONUP——鼠标中键弹起
    • WM_MBUTTONDBLCLK——鼠标中键双击
      \qquad 我们只需要根据不断获取鼠标消息队列的消息并根据消息依次进行响应即可。

    \qquad 相信大家已经迫不及待了,那么请看下面一个简单的程序。

    #include <graphics.h>              // 引用图形库头文件
    #include <conio.h>
    #include <stdio.h>
    #include <windows.h>				//用到了定时函数sleep()
    #include <math.h>
    int r1[]={30,20,130,60};//输入按钮的矩形参数
    int r2[]={170,20,220,60};//运行按钮的矩形参数
    int r3[]={260,20,310,60};//退出按钮的矩形参数
    int main()
    {
    	int i;
    	short win_width,win_height;//定义窗口的宽度和高度
    	win_width = 480;win_height = 360;
    	initgraph(win_width,win_height);//初始化窗口(黑屏)
    	for(i=0;i<256;i+=5)
    	{
    		setbkcolor(RGB(i,i,i));//设置背景色,原来默认黑色
    		cleardevice();//清屏(取决于背景色)
    		Sleep(15);//延时15ms
    	}
    	RECT R1={r1[0],r1[1],r1[2],r1[3]};//按钮1的矩形区域
    	RECT R2={r2[0],r2[1],r2[2],r2[3]};//按钮2的矩形区域
    	RECT R3={r3[0],r3[1],r3[2],r3[3]};//按钮2的矩形区域
    	LOGFONT f;
    	gettextstyle(&f);					//获取字体样式
    	_tcscpy(f.lfFaceName,_T("宋体"));	//设置字体为宋体
    	f.lfQuality = ANTIALIASED_QUALITY;    // 设置输出效果为抗锯齿  
    	settextstyle(&f);                     // 设置字体样式
    	settextcolor(BLACK);				//BLACK在graphic.h头文件里面被定义为黑色的颜色常量
    	drawtext("输入参数",&R1,DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域R1内输入文字,水平居中,垂直居中,单行显示
    	drawtext("运行",&R2,DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域R2内输入文字,水平居中,垂直居中,单行显示
    	drawtext("退出",&R3,DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域R3内输入文字,水平居中,垂直居中,单行显示
    	setlinecolor(BLACK);
    	rectangle(r1[0],r1[1],r1[2],r1[3]);
    	rectangle(r2[0],r2[1],r2[2],r2[3]);
    	rectangle(r3[0],r3[1],r3[2],r3[3]);
    	MOUSEMSG m;//鼠标指针
    	setrop2(R2_NOTXORPEN);//二元光栅——NOT(屏幕颜色 XOR 当前颜色)
    	while(true)
    	{
    		m = GetMouseMsg();//获取一条鼠标消息
    		if(m.uMsg==WM_LBUTTONDOWN)
    		{
    			for(i=0;i<=10;i++)
    			{
    				setlinecolor(RGB(25*i,25*i,25*i));//设置圆颜色
    				circle(m.x,m.y,2*i);
    				Sleep(25);//停顿2ms
    				circle(m.x,m.y,2*i);//抹去刚刚画的圆
    			}
    			FlushMouseMsgBuff();//清空鼠标消息缓存区
    		}
    	}
    	system("pause");//暂停,为了显示
    	closegraph();
    	return 0;
    }
    
    
    

    在这里插入图片描述
    \qquad 每点击鼠标以下,应该可以看到鼠标点击处有一个逐渐扩大并淡出的圆(截图无法清晰,在画面的中右侧),当循环体内Sleep的视觉大于20ms后视觉效果很强。
    \qquad 每响应一次鼠标左键单击事件,都会调用一次清空鼠标消息缓存区的函数FlushMouseMsgBuff(),如果没有这个函数会怎么样呢?如果我们快速连续地单击鼠标左键N次,那么特效就会播放N次,如果特效播放速度比单击的速度慢,那么即使你停下来了,程序仍然会接着播放单击特效,因为你的左键单击仍然在鼠标的消息队列m.uMsg中的鼠标消息没有响应完。
    \qquad 这里需要解释的是一个二元光栅设置函数setrop2(),二元光栅是混合背景色和当前颜色的模式。我们这里采用的方式是同或(NOT XOR)的方式,若底色为白色(1),则当前颜色不变;若底色是黑色(0),则当前颜色反色。为什么需要采用这种方式呢?因为我们在第二次抹去原来的圆的时候不能采用白色,否则如果背景色原来就为黑(比如按钮和文字),就也会被抹成白色。而背景色与任意一个颜色同或两次都为其本身,即可起到还原背景色的效果。这里的背景色与cleardevice()前面那个背景色不同,这里的是指执行这一条绘画指令之前屏幕上的颜色。

    2.3.2.光标感应

    \qquad 我们希望鼠标移到按钮上时按钮会有所变化,移开按钮时又会回到原样。这里我们采用一种简单的填充颜色的方法,就是按钮变色。我们需要解决一个问题就是按钮变色了但是按钮的文字不能被覆盖,那么我们还是需要使用到二元光栅。只是我们这次的模式改成了同或。
    \qquad 为了方便起见,存放三个按钮的数组我们合并为了一个二维数组,在鼠标事件中更容易使用和分配任务。

    #include <graphics.h>              // 引用图形库头文件
    #include <conio.h>
    #include <stdio.h>
    #include <windows.h>				//用到了定时函数sleep()
    #include <math.h>
    int r[3][4]={{30,20,130,60},{170,20,220,60},{260,20,310,60}};//三个按钮的二维数组
    
    int button_judge(int x,int y)
    {
    	if(x>r[0][0] && x<r[0][2] && y>r[0][1] && y<r[0][3])return 1;
    	if(x>r[1][0] && x<r[1][2] && y>r[1][1] && y<r[1][3])return 2;
    	if(x>r[2][0] && x<r[2][2] && y>r[2][1] && y<r[2][3])return 3;
    	return 0;
    }
    int main()
    {
    	int i,event=0;
    	short win_width,win_height;//定义窗口的宽度和高度
    	win_width = 480;win_height = 360;
    	initgraph(win_width,win_height);//初始化窗口(黑屏)
    	for(i=0;i<256;i+=5)
    	{
    		setbkcolor(RGB(i,i,i));//设置背景色,原来默认黑色
    		cleardevice();//清屏(取决于背景色)
    		Sleep(15);//延时15ms
    	}
    	RECT R1={r[0][0],r[0][1],r[0][2],r[0][3]};
    	RECT R2={r[1][0],r[1][1],r[1][2],r[1][3]};
    	RECT R3={r[2][0],r[2][1],r[2][2],r[2][3]};
    	LOGFONT f;
    	gettextstyle(&f);					//获取字体样式
    	_tcscpy(f.lfFaceName,_T("宋体"));	//设置字体为宋体
    	f.lfQuality = ANTIALIASED_QUALITY;    // 设置输出效果为抗锯齿  
    	settextstyle(&f);                     // 设置字体样式
    	settextcolor(BLACK);				//BLACK在graphic.h头文件里面被定义为黑色的颜色常量
    	drawtext("输入参数",&R1,DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域R1内输入文字,水平居中,垂直居中,单行显示
    	drawtext("运行",&R2,DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域R2内输入文字,水平居中,垂直居中,单行显示
    	drawtext("退出",&R3,DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域R3内输入文字,水平居中,垂直居中,单行显示
    	setlinecolor(BLACK);
    	rectangle(r[0][0],r[0][1],r[0][2],r[0][3]);
    	rectangle(r[1][0],r[1][1],r[1][2],r[1][3]);
    	rectangle(r[2][0],r[2][1],r[2][2],r[2][3]);
    	MOUSEMSG m;//鼠标指针
    	
    	while(true)
    	{
    		m = GetMouseMsg();//获取一条鼠标消息
    
    		switch(m.uMsg)
    		{
    			case WM_MOUSEMOVE:
    				setrop2(R2_XORPEN);
    				setlinecolor(LIGHTCYAN);//线条颜色为亮青色
    				setlinestyle(PS_SOLID, 3);//设置画线样式为实现,10磅
    				setfillcolor(WHITE);//填充颜色为白色
    				if(button_judge(m.x,m.y)!=0)
    				{
    					if(event != button_judge(m.x,m.y))
    					{
    						event = button_judge(m.x,m.y);//记录这一次触发的按钮
    						fillrectangle(r[event-1][0],r[event-1][1],r[event-1][2],r[event-1][3]);//有框填充矩形(X1,Y1,X2,Y2)
    					}
    				}
    				else
    				{
    					if(event != 0)//上次触发的按钮未被修正为原来的颜色
    					{
    						fillrectangle(r[event-1][0],r[event-1][1],r[event-1][2],r[event-1][3]);//两次同或为原来颜色
    						event = 0;
    					}
    				}
    				break;
    			case WM_LBUTTONDOWN:
    				setrop2(R2_NOTXORPEN);//二元光栅——NOT(屏幕颜色 XOR 当前颜色)
    				for(i=0;i<=10;i++)
    				{
    					setlinecolor(RGB(25*i,25*i,25*i));//设置圆颜色
    					circle(m.x,m.y,2*i);
    					Sleep(30);//停顿30ms
    					circle(m.x,m.y,2*i);//抹去刚刚画的圆
    				}
    				break;
    				FlushMouseMsgBuff();//清空鼠标消息缓存区
    		}
    	}
    	system("pause");//暂停,为了显示
    	return 0;
    }
    
    

    在这里插入图片描述
    \qquad 这里我们运用了两次设置二元光栅的函数setrop2,在鼠标的移动条件内(case MOUSEMOVE),我们使用的是屏幕颜色和当前颜色异或,这是为什么呢?因为fillrectangle()函数是画一个有框填充矩形,我们让这个有框填充矩形和原按钮的一样大。由于线条的颜色为亮青色,填充颜色为白色(1),白色的填充颜色和屏幕颜色异或,取的是屏幕颜色的反色。按钮的边框是黑色(0),它与亮青色异或,则会保留原来的亮青色。
    \qquad 与同或一样,异或两次等于没有执行操作,所以可以还原到原屏幕画布的颜色。

    2.3.3.进度条

    \qquad 既然涉及到进度条了,那么就应该涉及到正式程序了,这里我不想涉及太多的专业知识影响大家理解,但又不能让程序运行得太快以至于看不出进度条的变化,所以我设计了一个简单的弹性球轨迹作图程序。
    \qquad 假设球半径为R,初始高度为 h 0 h_0 h0,初速度为0(自由落体),非弹性碰撞时能量损失率为 α \alpha α。计算部分子函数如下:

    int simulation()
    {
    	float dt = 0.01;//仿真间隔10ms
    	long int N = (long int)(sim_t/dt);//迭代次数
    	float *h=(float*)calloc(N,sizeof(float));//高度
    	float *v=(float*)calloc(N,sizeof(float));//速度(竖直方向)
    	long int i;//迭代变量
    	for(i=1;i<N;i++)
    	{
    		if(h[i-1]>R)//未发生碰撞
    		{
    			v[i]=v[i-1]-9.8*dt;//速度计算
    		}
    		else//发生碰撞,动能损失alpha,速度损失alpha的开方
    		{
    			v[i]=-sqrt(alpha)*v[i-1];	
    		}
    	}
    	free(h);
    	free(v);//释放内存
    	return 0;
    }
    

    \qquad 当然,我们还需要绘图网格,定义绘图网格的函数如下:

    void init_figure()
    {
    	int i;
    	setrop2(R2_COPYPEN);//当前颜色
    	setlinecolor(BLACK);
    	setlinestyle(PS_SOLID);//实线
    	rectangle(30,100,420,330);//外框线
    	setlinestyle(PS_DOT);//点线
    	for(i=30+39;i<420;i+=39)
    	{
    		line(i,100,i,330);//竖直辅助线
    	}
    	for(i=100+23;i<330;i+=23)
    	{
    		line(30,i,420,i);//水平辅助线
    	}
    }
    

    \qquad 注意,我们使用了rectangle()空心矩形函数绘制网格外框架,使用了line函数依次画出了辅助线。

    \qquad 我们现在的目标就是将h的坐标转换到网格上去,绘制出球心的轨迹,这样似乎并不复杂,只需要对simulation()函数稍加修改即可。

    int simulation()
    {
    	float dt = 0.01;//仿真间隔10ms
    	float dy = 230/h0;//单位纵坐标
    	long int N = (long int)(sim_t/dt);//迭代次数
    	float *h=(float*)calloc(N,sizeof(float));//高度
    	float *v=(float*)calloc(N,sizeof(float));//速度(竖直方向)
    	long int i;//迭代变量
    	float process_duty;//进度
    	init_figure();//初始化图像网格
    	setrop2(R2_COPYPEN);//当前颜色
    	//计算步骤
    	h[0]=h0;v[0]=0;
    	for(i=1;i<N;i++)
    	{
    		if(h[i-1]>R)//未发生碰撞
    		{
    			v[i]=v[i-1]-9.8*dt;//速度计算
    		}
    		else//发生碰撞,动能损失alpha,速度损失alpha的开方
    		{
    			v[i]=-sqrt(alpha)*v[i-1];	
    		}
    		h[i]=h[i-1]+v[i]*dt;//高度计算
    		process_duty = (i+1)/(float)(N);
    		putpixel(30+(int)(process_duty*390),330-(int)(h[i]*dy),RED);//画点putpixel(X,Y,color*)
    		Sleep(dt*1000);//延时
    	}
    	free(h);
    	free(v);
    	return 0;
    }
    

    \qquad 这里的新函数putpixel(X,Y,color*)是画像素点的函数,适合刻画不连续或不规则的移动轨迹。
    \qquad 现在我们只剩下了刻画进度条的函数了,进度条的刷新很明显是应该放在for循环里面的,那么我们采用什么进度条的格式呢?进度条可以有圆形、扇形、长条连续型、长条不连续型等多种,我们这里采用的是环形进度条,将进度数字显示在环中心。请看以下的对simulation()函数改进的代码:

    //仿真运行
    int simulation()
    {
    	char t[3];//百分值的字符
    	char *out_text;//带百分号的百分字符
    	float dt = 0.01;//仿真间隔10ms
    	float dy = 230/h0;//单位纵坐标
    	long int N = (long int)(sim_t/dt);//迭代次数
    	float *h=(float*)calloc(N,sizeof(float));//高度
    	float *v=(float*)calloc(N,sizeof(float));//速度(竖直方向)
    	long int i;//迭代变量
    	float process_duty;//进度
    	RECT r={370,35,400,65};//百分值显示区域的矩形指针
    	init_figure();//初始化图像网格
    	setrop2(R2_COPYPEN);//当前颜色
    	setfillcolor(WHITE);
    	setlinecolor(WHITE);
    	fillrectangle(354,19,411,81);//覆盖原进度条区域
    	setlinestyle(PS_NULL);//无线条
    	setbkmode(TRANSPARENT);//设置文字填充背景为透明
    	//计算步骤
    	h[0]=h0;v[0]=0;
    	BeginBatchDraw();//开始缓存区
    	for(i=1;i<N;i++)
    	{
    		if(h[i-1]>R)//未发生碰撞
    		{
    			v[i]=v[i-1]-9.8*dt;//速度计算
    		}
    		else//发生碰撞,动能损失alpha,速度损失alpha的开方
    		{
    			v[i]=-sqrt(alpha)*v[i-1];	
    		}
    		setfillcolor(WHITE);
    		setlinecolor(WHITE);
    		fillrectangle(354,19,416,81);//覆盖原进度条区域
    		h[i]=h[i-1]+v[i]*dt;//高度计算
    		process_duty = (i+1)/(float)(N);
    		setlinestyle(PS_SOLID);
    		putpixel(30+(int)(process_duty*390),330-(int)(h[i]*dy),RED);
    		setfillcolor(BLUE);
    		setlinestyle(PS_NULL);
    		fillpie(355,20,415,80,0,process_duty*2*PI);
    		setfillcolor(WHITE);
    		fillcircle(385,50,20);
    		sprintf(t,"%d",(int)(process_duty*100.0));//整型转换为字符串
    		out_text = strcat(t,"%");//添加一个百分号
    		drawtext(out_text,&r,DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    		Sleep(dt*1000);
    		FlushBatchDraw();//刷新缓存区
    	}
    	EndBatchDraw();//结束缓存区
    	free(h);
    	free(v);
    	return 0;
    }
    

    \qquad 这里我们需要多加载一个头文件<string.h>。
    \qquad 首先需要计算进度条的坐标,把环形进度条区域用白色矩形刷新掉,环形进度条需要一个扇形和圆形的组合,扇形的角度是0~360°。这里我们用到了fillpie(X1,Y1,X2,Y2,start_angle,end_angle),前四个参数为椭圆扇形的外接矩形坐标,后两个参数分别为起始角和终止角(弧度制)。每过一次迭代都重新计算终止角(起始角始终为0),即可起到扇形角度逐渐增长的效果,再用一个白色填充圆覆盖中心部分即可变成环形进度条。
    \qquad FlushBatchDraw()函数是刷新缓存区的函数,与BeginBatchDraw()EndBatchDraw()一起使用,如果我们绘图之后不想立即显示,而想批量绘图最后一起刷新画板,用缓存区的方法再合适不过了。

    3.完整代码及效果

    #include <graphics.h>              // 引用图形库头文件
    #include <conio.h>
    #include <stdio.h>
    #include <windows.h>				//用到了定时函数sleep()
    #include <math.h>
    #include <string.h>
    #define PI 3.1416
    int r[3][4]={{30,20,130,60},{170,20,220,60},{260,20,310,60}};//三个按钮的二维数组
    float alpha,R,h0,sim_t;//碰撞时的能量损失率,球的半径、初始高度、仿真时间
    //按钮判断函数
    int button_judge(int x,int y)
    {
    	if(x>r[0][0] && x<r[0][2] && y>r[0][1] && y<r[0][3])return 1;
    	if(x>r[1][0] && x<r[1][2] && y>r[1][1] && y<r[1][3])return 2;
    	if(x>r[2][0] && x<r[2][2] && y>r[2][1] && y<r[2][3])return 3;
    	return 0;
    }
    //初始化图像
    void init_figure()
    {
    	int i;
    	setrop2(R2_COPYPEN);//当前颜色
    	setlinecolor(BLACK);
    	setlinestyle(PS_SOLID);//实线
    	rectangle(30,100,420,330);//外框线
    	setlinestyle(PS_DOT);//点线
    	for(i=30+39;i<420;i+=39)
    	{
    		line(i,100,i,330);//竖直辅助线
    	}
    	for(i=100+23;i<330;i+=23)
    	{
    		line(30,i,420,i);//水平辅助线
    	}
    }
    //仿真运行
    int simulation()
    {
    	char t[3];//百分值的字符
    	char *out_text;
    	float dt = 0.01;//仿真间隔10ms
    	float dy = 230/h0;//单位纵坐标
    	long int N = (long int)(sim_t/dt);//迭代次数
    	float *h=(float*)calloc(N,sizeof(float));//高度
    	float *v=(float*)calloc(N,sizeof(float));//速度(竖直方向)
    	long int i;//迭代变量
    	float process_duty;//进度
    	RECT r={370,35,400,65};//百分值显示区域的矩形指针
    	init_figure();//初始化图像网格
    	setrop2(R2_COPYPEN);//当前颜色
    	setfillcolor(WHITE);
    	setlinecolor(WHITE);
    	fillrectangle(354,19,411,81);//覆盖原进度条区域
    	setlinestyle(PS_NULL);//无线条
    	setbkmode(TRANSPARENT);//设置文字填充背景为透明
    	//计算步骤
    	h[0]=h0;v[0]=0;
    	BeginBatchDraw();//开始缓存区
    	for(i=1;i<N;i++)
    	{
    		if(h[i-1]>R)//未发生碰撞
    		{
    			v[i]=v[i-1]-9.8*dt;//速度计算
    		}
    		else//发生碰撞,动能损失alpha,速度损失alpha的开方
    		{
    			v[i]=-sqrt(alpha)*v[i-1];	
    		}
    		setfillcolor(WHITE);
    		setlinecolor(WHITE);
    		fillrectangle(354,19,416,81);//覆盖原进度条区域
    		h[i]=h[i-1]+v[i]*dt;//高度计算
    		process_duty = (i+1)/(float)(N);
    		setlinestyle(PS_SOLID);
    		putpixel(30+(int)(process_duty*390),330-(int)(h[i]*dy),RED);
    		setfillcolor(BLUE);
    		setlinestyle(PS_NULL);
    		fillpie(355,20,415,80,0,process_duty*2*PI);
    		setfillcolor(WHITE);
    		fillcircle(385,50,20);
    		sprintf(t,"%d",(int)(process_duty*100.0));//整型转换为字符串
    		out_text = strcat(t,"%");//添加一个百分号
    		drawtext(out_text,&r,DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    		Sleep(dt*1000);
    		FlushBatchDraw();//刷新缓存区
    	}
    	EndBatchDraw();//结束缓存区
    	free(h);
    	free(v);
    	return 0;
    }
    
    int main()
    {
    	int i,event=0;
    	char s[30];//输入字符串变量
    	short win_width,win_height;//定义窗口的宽度和高度
    	win_width = 480;win_height = 360;
    	initgraph(win_width,win_height);//初始化窗口(黑屏)
    	for(i=0;i<256;i+=5)
    	{
    		setbkcolor(RGB(i,i,i));//设置背景色,原来默认黑色
    		cleardevice();//清屏(取决于背景色)
    		Sleep(30);//延时30ms
    	}
    	RECT R1={r[0][0],r[0][1],r[0][2],r[0][3]};
    	RECT R2={r[1][0],r[1][1],r[1][2],r[1][3]};
    	RECT R3={r[2][0],r[2][1],r[2][2],r[2][3]};
    	LOGFONT f;//字体样式指针
    	gettextstyle(&f);					//获取字体样式
    	_tcscpy(f.lfFaceName,_T("宋体"));	//设置字体为宋体
    	f.lfQuality = ANTIALIASED_QUALITY;    // 设置输出效果为抗锯齿  
    	settextstyle(&f);                     // 设置字体样式
    	settextcolor(BLACK);				//BLACK在graphic.h头文件里面被定义为黑色的颜色常量
    	drawtext("输入参数",&R1,DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域R1内输入文字,水平居中,垂直居中,单行显示
    	drawtext("运行",&R2,DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域R2内输入文字,水平居中,垂直居中,单行显示
    	drawtext("退出",&R3,DT_CENTER | DT_VCENTER | DT_SINGLELINE);//在矩形区域R3内输入文字,水平居中,垂直居中,单行显示
    	setlinecolor(BLACK);
    	rectangle(r[0][0],r[0][1],r[0][2],r[0][3]);
    	rectangle(r[1][0],r[1][1],r[1][2],r[1][3]);
    	rectangle(r[2][0],r[2][1],r[2][2],r[2][3]);
    	MOUSEMSG m;//鼠标指针
    	
    	while(true)
    	{
    		m = GetMouseMsg();//获取一条鼠标消息
    
    		switch(m.uMsg)
    		{
    			case WM_MOUSEMOVE:
    				setrop2(R2_XORPEN);
    				setlinecolor(LIGHTCYAN);//线条颜色为亮青色
    				setlinestyle(PS_SOLID, 3);//设置画线样式为实现,10磅
    				setfillcolor(WHITE);//填充颜色为白色
    				if(button_judge(m.x,m.y)!=0)
    				{
    					if(event != button_judge(m.x,m.y))
    					{
    						event = button_judge(m.x,m.y);//记录这一次触发的按钮
    						fillrectangle(r[event-1][0],r[event-1][1],r[event-1][2],r[event-1][3]);//有框填充矩形(X1,Y1,X2,Y2)
    					}
    				}
    				else
    				{
    					if(event!=0)//上次触发的按钮未被修正为原来的颜色
    					{
    						fillrectangle(r[event-1][0],r[event-1][1],r[event-1][2],r[event-1][3]);//两次同或为原来颜色
    						event = 0;
    					}
    				}
    				break;
    			case WM_LBUTTONDOWN:
    				setrop2(R2_NOTXORPEN);//二元光栅——NOT(屏幕颜色 XOR 当前颜色)
    				for(i=0;i<=10;i++)
    				{
    					setlinecolor(RGB(25*i,25*i,25*i));//设置圆颜色
    					circle(m.x,m.y,2*i);
    					Sleep(20);//停顿30ms
    					circle(m.x,m.y,2*i);//抹去刚刚画的圆
    				}
    				//按照按钮判断左键单击后的操作
    				switch(button_judge(m.x,m.y))
    				{
    					//复原按钮原型
    					case 1:
    						InputBox(s,30,"请输入碰撞时的能量损失率、球的半径、初始高度、仿真时间");
    						sscanf(s,"%f%f%f%f",&alpha,&R,&h0,&sim_t);//将输入字符串依次扫描到全局变量里面
    						FlushMouseMsgBuffer();//单击事件后清空鼠标消息
    						break;
    					case 2:
    						simulation();//仿真运行
    						FlushMouseMsgBuffer();//单击事件后清空鼠标消息
    						break;
    					case 3:
    						closegraph();//关闭绘图环境
    						exit(0);//正常退出
    					default:
    						FlushMouseMsgBuffer();//单击事件后清空鼠标消息
    						//printf("\r\n(%d,%d)",m.x,m.y);//打印鼠标坐标,方便调试时确定区域
    						break;
    				}
    				break;
    		}
    	}
    	return 0;
    }
    
    

    在这里插入图片描述
    在这里插入图片描述
    希望本文对您有帮助,谢谢阅读。

    展开全文
  • C语言

    万次阅读 多人点赞 2019-12-18 23:01:50
    冒泡法是在扫描过程中逐次比较相邻两个元素的大小。例:9+8+7+6+5+4+3+2+1=45. 9.对象间的信息传递靠消息。 10.多态性是指同一个操作可以是不同对象的行为。操作—对象。 C语言 1.源程序的扩展名为.c,目标程序的...

    公共考点
    1.算法的空间复杂度是指算法在执行过程中所需要的内存空间。
    2.算法的时间复杂度是指算法所需要的计算工作量。
    3.数据的逻辑结构与储存结构不是一一对应的。
    4.队列的修改是以先进先出的原则进行的。–与队列结构有关联的是先到先服务的作业调度。
    5.循环队列中的元素个数随队头指针和队尾指针变化而动态变化。
    6.C语言中的result只是一个自己定义的量
    7.对空和队满时,头尾指针均相等。
    8.冒泡法是在扫描过程中逐次比较相邻两个元素的大小。例:9+8+7+6+5+4+3+2+1=45.
    9.对象间的信息传递靠消息。
    10.多态性是指同一个操作可以是不同对象的行为。操作—对象。

    C语言
    1.源程序的扩展名为.c,目标程序的扩展名为.obj,可执行程序的扩展名为.exe(每个后缀为.c的C语言都可以单独进行编译)(C语言编译程序把.c编译成.obj的二进制文件)(链接形成.exe文件)
    2.循环结构、选择结构,顺序结构都是结构化程序的基本结构。
    3.N-S流程图是复杂算法的描述手段。
    4.长方形为处理框。椭圆形为连接点。
    5.一个c语言只能有一个主函数。
    6.函数的定义不可以嵌套,函数的调用可以嵌套。
    7.C语言总是以main函数开始执行。
    8.常量的类型:整型常量、实型常量、字符常量、字符串常量、符号常量。
    9.十进制整型常量:基本数字范围:0-9;(十进制小数两边必须有数字)
    八进制整型常量:以0开头,输出格式控制符为%o,基本数字范围0-7;
    十六进制整型常量:以0x开头,输出格式为%x,基本数字范围为0-15写为A-F或a-f;
    指数形式:e前必须有数字,e后必须为整数。
    10. 关键字属于标识符。(关键字不能做变量名也不能做函数名)
    11.数值型常量有整型常量、实型常量但均有正负值之分。
    12.语言的预编译处理可以可以用符号名代表一个常量定义是不必指定常量类型。
    13.实型常量又称实数或浮点数。在C语言中可以用单精度型和双精度型两种形式表示实型常量,分别用类型名float和double进行定义。实型常量在一般的微型集中占用4个字节,一般形式或者指数形式,数值范围都是-1038~1038,有效数字是7位。(不能是整形数据,如0)(常量的类型可以从字面上区分)(1为整型常量)(1.0为实型常量)(a为字符型常量)
    14.\0为八进制数,所以\09是错误的。
    15.字符常量在内存中占1个字节,字符常量可以进行关系运算。不能参与数值运算,可以参与任何整数运算。
    16.不能用字符串常量对字符数组名进行整体赋值操作。
    17.可以使用字符串常量来给一维数组进行复制。
    18.关于字节大小的问题

    16位编译器:char 1个字节  char* 2个字节 int 2个字节 float 4个字节 double 8个字节
    32位编译器:char 1个字节  char* 2个字节 int 4个字节 float 4个字节 double 8个字节
    64位编译器:char 1个字节  char* 2个字节 int 4个字节 float 4个字节 double 8个字节
    

    19.10进制转8进制,手算用 除8取余数法得
    20.十进制转十六进制为:除十六取余直到商为0,余数从后往前读。
    21.%f代表单精度浮点型数据(float),%lf代表双精度浮点型数(double)。
    单精度浮点数有效数字保证6位,部分7位,双精度浮点数有效数字保证15位,部分16位。
    22.sizeof可以看成是一个无符号整型表达式(sizeof为字节运算符)
    23.强制运算符:(类型名)(表达式) 逗号运算符:, 条件运算符::? :
    24. 赋值运算符左边必须是(一个)变量。
    25.a=bc,先运算bc,这个表达式的含义是,若b与c相等,那么得出的值为1,若不等则为0.
    26.“^” 按位异或 两数的二进制对应位相同,则为0,不同则为1.
    27.“|” 按位或 两个二进制中只要有一个为1,则结果为1。
    28.“~” 按位取反 二进制 0变1,1变0.
    29. “&”按位与 两个二进制都为1,则该位的结果为1,否则为零
    【 零的按位取反是 -1(0在数学界既不是正数也不是负数)
    所有正整数的按位取反是其本身+1的负数
    所有负整数的按位取反是其本身+1的绝对值 】
    30.位运算的对象只能是整形或字符型数据
    31.||逻辑或 前后条件只要有一个满足则为真。
    32.&&逻辑与 前后条件同时满足表达式为真。
    33.再用||的地方一般可以用|代替,但是用|的地方不能用||代替。
    34.“&”取地址运算
    35“”指针运算符
    36.p是指针变量,则&p是变量p的地址
    37.p是指针变量,则
    p是变量p所指向地址的值
    38.基类型不同的指针变量不可以相互混用
    39.函数的类型可以是指针类型
    40.函数的参数可以是整型、实型、字符型、指针类型。
    41.在这里插入图片描述

    42.C语言是一种计算机高级语言。
    43.C语言允许直接访问物理地址,能进行位操作。
    44.C语言是结构化程序设计语言
    45.c程序要通过编译,连接才能得到可执行的目标程序
    46.用c语言编写程序,可以编写出任何类型的程序
    47.C语言允许有空函数
    48.C程序书写格式,允许一行内可以写几个语句
    49.C程序的语句无行号(C语言中给源程序加行号;行号是用来定位代码的,指文件在几行)
    50.C语言的每个语句的最后必须有一个分号
    51.C语言本身没有输入输出语句(没有特定的输入输出语句)
    52.C语言可用来编写应用软件,也可用来编写系软件
    53.TurboC是在微机上广泛使用的编译程序
    54.C语言的数据结构是以数据类型形式出现的(不是常量和变量)
    55.空类型是C语言的一种数据类型
    56.C语言中数据有常量和变量之分
    57.利用指针和结构体类型可以构成表、树等复杂的数据结构
    58.在C程序中对所用到的所有数据都必须指定其数据类型
    59.c程序运行过程中,其值不能被改变的量称为常量
    60.在程序运行过程中,其值可以改变的量称为变量
    61.C语言可以用一个标识符代表一个常量,称为符号常量
    62.C语言规定标识符只能由字母、数字和下划线三种字符组成
    63.C语言整型常量可用十进制整数、八进整数和十六进制整数三种形式表示
    64.在现微机上使用的C编译系统,每一个整型变量在内存中占2个字节
    65.整型变量的基本类型符为int
    66.在微机上,一个长整型变量在内存中占4个字节(float型变量在内存中占4个字节)
    67.一个int型变量的最大允许值为32767
    68.在一个整常量后面加一个字母“L”或“1”.则认为该常量是longint 型常量
    69.C语言实型常量可用二进制小数和指数二种形式表示
    70.C语言实型变量分为:float型、double型、long double型三类
    71.C语言doule型一个变量的数值有效数字是16位
    72.C语言的字符常量是用单引号括起来的一个字符
    73.C语言的转义字符是以一个“\”开头的一种特殊形式的字符常量
    74.C语言中换行符使用’\n’,这是一个转义字符
    75.转文字符\r的含义是回车。
    76.C语言的字符型变量只能存放一个字符
    77.C语言允许字符数据与整数直接进行算术运算
    78.C语言允许在定义变量的同时使变量初始化
    79.C语言允许整型、实型、字符型数据间可以混合运算
    80.C语言规定两个整数相除的结果为整数
    81.用求余运算符“%”作运算,运算符两侧均应为整型数据
    82.用算术运算符和括号将运算对象按C语法规则组成的式子,称为C算术表达式
    83.算术运算符的结合方向为“自左至右”
    84.强制类型转换时,原来变量的类型未发生变化
    85.自增、自减运算符的结合方向为“自右至左”
    86.自增运算符只能用于变量,不能用于常量或表达式
    87指针.自增(减)运算符也可以用于指针变量,使指向下一个地址
    88.运算符“=”的作用是将一个数据赋给一个变量
    89.运算符“”的作用是将两侧数据是否相等
    90.赋运算符的结合方向是“自右向左”
    91.凡是二目运算符,都可以与赋值运算符一起组合成复合赋值运算符
    92.运算符“
    ”的作用是将一个数据赋给一个变量
    93.C语言不允许将实型数据赋给整型变量
    94.一个逗号表达式又可以与另一个表达式组成一个新的逗号表达式
    95.一个C程序可以由若干个源程序文件组成
    96.一个源文件可以由若千个函数和预处理命令以及全局变量声明部分组成
    97.空语句是C语言的一种语句
    98.复合语句中最后一个语句中最后的分号不能省略不写
    99.putchar函数的作用是向终端输出一个字符
    100.getchar函数的作用是从终端输入一个字符
    101.格式输出函数(print)一次可以输出多个数据
    102.printf函数的%ld格式参数,用来输入出长整型数据
    103.printf函数的%o格式参数,用来以8进制数形式输出整数
    104.printf函数的%f格式参数,用来以小数形式输出实数
    105.printf函数的%x格式参数,可以输出指定参数的16进制形式
    106.printf函数的%s格式参数,用来输出一个字符串
    107.C语言不是面向对象的程序设计语言
    108.printf函数的%e格式参数,以指数形式输出实数
    109.C语言单精度数的有效数一般为7位
    110.printf函数的%g格式参数
    111.%g是C语言printf()函数的一个输出格式类型,它表示以%f%e中较短的输出宽度输出单、双精度实数,在指数小于-4或者大于等于精度时使用%e格式
    112.p++是指下一个地址。
    (p)++是指将p所指的数据的值加一。
    C编译器认为
    和++是同优先级操作符,且都是从右至左结合的,所以p++中的++只作用在p上,和(p++)意思一样;在(p)++中,由于()的优先级比和++都高,所以++作用在()内的表达式*p上。比如有:
    int x,y,a[]={1,2,3,4,5},*p=a,*q=a;
    x=*p++;//执行这一句后x=a[0]=1,p=a+1
    y=(*q)++;//执行这一句后,y=a[0]+1=2,q仍然=a
    113. printf函数的附加格式说明字符“m”的含义是指输出数据的最小宽度
    114.scanf函数中的“格式控制”后面应当是变量地址(不是变量符)
    115.逻辑运算符>算术运算符>关系运算符>条件运算符>赋值运算符(罗算管调幅)
    116.条件运算符的结合方向是“自右向左"
    117.if语中又包含文可以转在电百度网点电 平句的嵌套
    118.条件运算符要求有3个操作对象,称为三目运算符
    119.条件表达式中三个表达式的类型可以不同
    120.switch语句是多分支选择语句
    121.switch语句中每一个case的常量表达式的值必须互不相同
    122.switch语句执行完一个case后面的语句后,流程控制转移到下一个case继续执行
    123.switch语句中多个case可以共用组执行语句
    124.goto语句为无条件转向语句
    125.C语句的循环语句中循环体如果包含一个以上的语句,必须以复合语句形式出现bre
    126.for循环语句中的3个表达式都可以省略
    127.C语句的一个循环体内允许又包含另一个完整的循环结构
    128.break语句不能用于循环语句和switch语句之外的任何其它语句中
    129.continue语句的作用是结束本次循环(而不是终止整个循环)
    130.C数组中的每一个元素都必须属于同一个数据类型
    131.C数组必须先定义,然后使用
    132.C语言规定只能逐个引用数组元素而不能一次引用整个数组
    133.在定义一维数组时可以只给一部分元素赋初值
    134.对二维数组初始化,可以分行给数组赋初值
    135.可以对二维数组的部分元素赋初值
    136.字符数组中的一个元素只存放一个字符
    137.如果一个字符数组中包含一个以上结束符’\0”,则遇第一个’\0’时输出就结束
    138.puts函数的作用是将一个字符串输出终端
    139.gets丽数的作用是从终端输入一个字符串到字符数组
    140.strlen 函数是测试字符串长度的函数
    141
    strcat函数是“字符串复制函数”。X
    strcpy函数是“字符串连接函数”。X
    strcmp函数是“字符串复制函数”。X
    strlwr函数是测试字符串长度的函数。X
    strupr函数是测试字符串长度的函数。X
    142.C程序一个函数可以被一个或多个函数调用多次
    143.一个C程序可由一个主函数和若干个其它函数构成
    144.C程序以源程序为单位进行编译(而不是函数)
    145.C程序由一个或多个源程序文件组成
    146.C语言在定义函数时是互相独立的,不能嵌套定义
    147.在调用有参函数时,主调函数和被调用函数之间有数据传递关系
    148.在调用一个函数的过程中又出现直接或间接地调用该函数本身称为函数的递归调用
    149.在一个函数内部定义的变量是内部变量,称为局部变量
    150.在函数之外定义的变量称为外部变量,是全局变量
    151.从变量的作用域角度来分,可以分为全局变量和局部变量(而不是静态和动态变量)
    152.静态存储方式是指在程序运行期间分配固定的存储空间的方式
    153.存储方法分为两大类:静态存储类和动态存储类
    154.C语言允许将局部变量的值放在CPU中的寄存器中,这种变量称为“寄存器变量”
    155.局部静态变量不能定义为寄存器变量
    156.如果一个函数只能被本文件中其它函数所调用,称为内部函数
    157.C源程序中的预处理命令,它不是C语言本身的组成部分
    158.宏定义不是C语句,在行末不加分号
    159.宏定又是用宏名代替一个字符串,只作简单的置换,不作正确性检查
    160.在进行宏定义时,可以引用已定义的宏名
    161.宏替换不占程序运行时间,只占编译时间
    162.文件包含处理是指个源文件可以将另一个的全部内容含进来源文件包
    163.一个include命令只能指定一个被包含文件
    164.存放变量地址的变量是指针变量
    165.C语言中变量的指针就是变量的地址
    166.函数的参数也可以是指针变量
    167.指针变量可以指向变量,也可以指向数组和数组元素
    168.引用数组元素可以用下标法,也可以用指针法
    169.用指针变量可以指向一维数组,也可以指向多维数组,用指针变量也可以指向一个函数
    170.一个函数可以带回一个整型值、字符值或实型值,也可以带回指针型的数据
    171.指针数组中的每一个元素都相当于一个指针变量
    172.指针数组中的每一个元素都相当于一个整型变量
    173.指针变量可以有空值,即该指针变量不指向任何变量
    174.若两个指针指向同一个数组的元素。则两指针变量可以进行比较
    175.用户自己定义一个结构教型后们其中并无具体数据
    176.在程序中使用的可和百网时 定义结构体类型的变量
    177.结构体类型的成员也可以是一个结构体变量
    178.结构体成员名可以与程序中的变量名相同
    179.不能将一个结构体变量作为一个整体进行输入和输出
    180.对结构体变量的成员可以像普通变量一样进行各种运算
    181.可以引用结构体变量的地址
    182.可以引用结构体变量成员的地址
    183.结构体数组的每个元素都是一个个结构体类型的数据
    184.对结构体数组可以初始化,即赋初值
    185.可以定义一个指针变量,用来指向一个结构体变量
    186.指针可以用结构体变量作链表中的结点
    187.malloc函数的返回值是一个指向分配域起始地址的指针
    188.建立动态链表是指在程序执行过程中从无到有地建立起一个链表
    189.使几个不同的变量共占同一段内存的结构,称为共用体类型的结构
    190.共用体变量所占的内存长度等于最长的成员长度
    191.定义了共用体变量,只能引用共用体变量中的成员(不能引用公用体变量)
    192.共用体变量的地址和它的各成员的地址都是同一地址
    193.共用体类型可以出现在结构体类型定义中
    194.结构体类型可以出在共用体类型定义中
    195.在C编译中,对枚举元素按常量处理
    196.一个整数不能直接赋给一个枚举变量
    枚举类型在C#或C++,java,VB等一些计算机编程语言中是一种基本数据类型而不是构造数据类型,而在C语言等计算机编程语言中是一种构造数据类型 。它用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型。
    枚举可以根据Integer、Long、Short或Byte中的任意一种数据类型来创建一种新型变量。这种变量能设置为已经定义的一组之中的一个,有效地防止用户提供无效值。该变量可使代码更加清晰,因为它可以描述特定的值。
    197.可以用typedef声明新的类型名来代替已有的类型名
    198.位运算的运算量只能是整型或字符型的数据
    200.位运算符与赋值运算符可以组成复合赋值运算符
    在 C 语言中, 一种方法是用叫做位段的构造类型来定义一个压缩信息的结构。
    201.已有定义int (*p)( );指针p可以指向函数的入口地址
    202.C语言中运算对象必须是整型的是%=
    203.int *p 表达的是p是指向int型数据的指针。
    204函数rewind的功能是将文件指针重新指向一个流的开头(即使文件指针重新返回文件的开始位置),int rewind(FILE *stream);并且无返值。
    205.如果函数值的类型与返回值类型不一致,以函数值类型为准
    206.c语言中形参和实参类型不一致时以形参的类型为准
    207.形参应该是函数声明的时候就已经定义好
    208.若有定义int t[3][2],能正确表达t数组元素地址的是–t[2]+1
    209.int[]={1,2};
    210.C语言中的循环语句有for,while,do-while和goto,,***不是if、switch、break
    211.不正确的赋值语句是—ch‘a+b’,正确的是ch=‘\0’ ch=‘7’+‘9’ ch=7+9
    212.正确的赋值语句x3=12;
    213.C语言逻辑运算时,0为假,非0为真
    214.字符串常量是以双引号扩起来的字符序列“a”(其他C语言常量‘\n’ 012)(e-2不是C语言常量----实数的指数形式中,e后面必须有一个整数)

    301.一个位段必须存储在同一存储单元中
    302.位段的长度不能大于存储单元的长度
    303.一个c程序由若干个函数构成,其中有且仅有一个主函数
    304.指针变量中存放的是它所指对象的地址
    305.在C语言中,分号是语句的必然组成部分
    306.结构体变量所占空间是各成员所占空间之和
    307.数据文件可顺序读取,也可借助文件的定位操作实现随机读取
    308.从用户的角度上讲,类型的含义是规定了该类型变量的取值范围和运算范围
    309.c语言中,变量和函数均具有类型和存贮类别两个属性
    340.顺序结构>选择结构>循环结构
    341.函数返回值的类型是由函数定义时指定的类型
    342.*与s[]相等
    343.当从键盘输入数据时,对于整型变量可以输入整型数值和字符,对于实型变量可以输入实型数和整型数值等。
    344. getchar函数没有参数
    345.静态储存方式是在程序运行期间分配固定的储存方式的方式
    356.局部静态变量不能定义为寄存器变量
    357.不能把共用体变量作为函数的参数
    358.一个整数不能直接赋给一个枚举变量
    359.int *p=a 是对指针变量p的正确定义和初始化。
    360.Char s[]=”china”;
    Char p;
    P=s;
    p与s[]相等
    有int [],*p=a
    则p+5表示元素a[]的地址
    361.C语言中,退格符是\b
    362.C语言中,变量的隐含储存类别是auto
    363.实际参数和形式参数可以同名
    364.函数调用可以作为一个函数的形参
    365.结构化程序设计的3中结构是-顺序结构、选择结构、循环结构
    366.当从键盘输入数据时整型变量可以输出整型值和字符,对于实型变量可以输入实型数和整型数值
    367.C语言中逗号运算符的优先级最低,指针最优,单目运算优于双目运算。如正负号。
    先算术运算,后移位运算,最后位运算。请特别注意:1 << 3 + 2 & 7等价于 (1 << (3 + 2))&7.
    逻辑运算最后结合。
    368.C语言区分定义变量名的大小写
    369.设有如下定义:
    struck sk
    { int a;
    float b;
    } data;
    int *p;
    若要使P指向data中的a域,正确的赋值语句是(C)A、 p=&a; B、 p=data.a; C、 p=&data.a; D、 *p=data.a;
    370.double)a是将a转换成double类型;(int)(x+y)是将x+y的值转换成整型。
    371.设有以下说明语句:
    struct stu
    {
    int a;
    float b;
    }
    stutype;
    则下面叙述不正确的是( )。
    A) struct是结构体类型的关键字
    B) structstu是用户定义的结构体类型
    C) stutype是用户定义的结构体类型名
    D) a和b都是结构体成员名
    答案解析
    定义一个结构的一般形式为:
    struct结构体名
    {
    成员列表
    }变量名列表;
    本题中的stutype是在声明结构体类型structstu的同时定义的该结构体变量,而不是用户定义的结构体类型名。类型与变量是不同的概念; 2)对结构体中的成员,可以单独使用,它的作用与地位相当于普通变量;3)成员也可以是一个结构体变量; 4)成员名可以与程序中的变量名相同,二者不代表同一对象。
    372.C语言中的数据类型是指-函数返回值的数据类型
    373.C程序设计语言的基本成分是数据成分、运算成分、控制成分、传输成分。
    374.while(t=1)循环控制表达式的值为1。
    375.printf(++x);表示地址所连接的数值加1.
    376.int[3][4]; 表示a为3行
    4列的数组,它可用的最大行下标为2,列下标最大为3;
    若是引用a[0][4],则超过了数组的范围
    377.若有如下说明和定义
    struct test
    {
    int ml; char m2; float m3;
    union uu
    {
    char ul[5]; int u2[2];
    }
    ua;
    } myaa;
    则sizeof(struct test)
    的值是A.12 B.16 C.14 D.9
    正确答案:A
    在本题中,首先定义了一个结构体。在该结构体中,定义了一个整型变量成员、一个字符型变量成员和一个浮点型变量成员,并在结构体中定义了一个联合体变量成员,联合体变量成员中又包含两个联合体成员数组。题目最后要求计算该结构体变量所占的存储空间。
    在C语言中,联合体变量中的所有成员共享存储空间,联合变量的长度等于各成员中最长的长度,因此,本题的联合体部分所占的长度为5,但是结构体与联合体不一样的是,结构体不能共享空间,一个结构体变量的总长度是各成员长度之和,因此,该结构体所需的存储空间为5+1+2+4=12。本题的正确答案选A。
    378.静态储存类别的关键词是static
    379.C语言中提供了存储说明符auto,register,extern,static说明的四种存储类别。四种存储类别说明符有两种存储期:自动存储期和静态存储期。其中auto和register对应自动存储期。具有自动存储期的变量在进入声明该变量的程序块是被建立,它在该程序块活动时存在,退出该程序块时撤销。
    380.fseek(文件指针,位移量,起始点)
    “起始点”用0,1或2代替,0代表“文件开始”,1为“当前位置”,2为“文件末尾”。“位移量”指以“起始点”为基点,向前移动的字节数。ANSIC和大多数C版本要求位移量是long型数据。这样当文件的长度大于 64k时不致出现问题。ANSI C标准规定在数字的末尾加一个字母L,就表示long型。
    381.若有定义:int (*p)[4];则标识符p ,是一个指针指向一个含有四个整形元素的一维数组。
    382.基本数据类型:整型、实型、字符型
    383.EOF是指向文本文件的结束标志,NULL是打开文件错误时的返回值。feof(fp)用来判断文件是否在文件末尾,文本文件和二进制文件均可以使用此函数,如果遇到文件结束就返回1,否则返回0。
    384.C语言的函数可以嵌套调用
    385.标准库函数fgets(s,n,f)的功能是什么–从文件f中读取长度不超过n-1的字符串存入指针s所指的内存。
    从流中读一行或指定个字符,
    原型是char *fgets(char *s, int n, FILE *stream);
    从流中读取n-1个字符,除非读完一行,参数s是来接收字符串,如果成功则返回s的指针,否则返回NULL。
    形参注释:*string结果数据的首地址;n-1:一次读入数据块的长度,其默认值为1k,即1024;stream文件指针
    说得简单一点就是从f这个文件输入流中读取n-1个字符,存到s中。
    如果一行的字符数小于n-1,那么就是一行的字符数,所以应该理解为不超过n-1,如果一行的长度大于n-1,就是n-1个字符
    386.
    1、数据计算类型不同。基本数据类型分为三类:整数型(定点型)、实数型(浮点型)和字符型。除了基本数据类型,还有构造类型(数组、结构体、共用体、枚举类型)、指针类型、空类型void。
    2、各种数据类型的关键词不同。short、long、int、float、double、char六个关键词表示C语言里六种基本数据类型。
    3、不同数据类型占用内存的大小不同。short占2byte,int占4byte,long占4byte,float占2byte,double占8byte,char占1byte(不同的平台可能占用内存大小不一样,具体的可以用sizeof 测试下)。
    387.一个可以没有变量定义和执行部分,例如空函数

    展开全文
  • 入门学习Linux常用必会60个命令实例详解doc/txt

    千次下载 热门讨论 2011-06-09 00:08:45
    文件为doc版,可自行转成txt,在手机上看挺好的。 本资源来自网络,如有纰漏还请告知,如觉得还不错,请留言告知后来人,谢谢!!!!! ...入门学习Linux常用必会60个命令实例详解 ...Linux提供了大量的命令,利用它...
  • MySQL 面试题

    万次阅读 多人点赞 2019-09-02 16:03:33
    2、varchar(50) 中 50 的涵义最多存放 50 个字符。varchar(50) 和 (200) 存储 hello 所占空间一样, 但后者在排序时会消耗更多内存,因为 ORDER BY col 采用 fixed_length 计算 col 长度(memory引擎也一样) 。所以,...

    MySQL 面试题

    MySQL 涉及的内容非常非常非常多,所以面试题也容易写的杂乱。当年,我们记着几个一定要掌握的重心:

    重点的题目添加了【重点】前缀。

    1. 索引。
    2. 锁。
    3. 事务和隔离级别。

    因为 MySQL 还会有部分内容和运维相关度比较高,所以本文我们分成两部分【开发】【运维】两部分。

    • 对于【开发】部分,我们需要掌握。
    • 对于【运维】部分,更多考验开发的知识储备情况,当然能回答出来是比较好的,特别是对于高级开发工程师、架构师等。

    开发

    为什么互联网公司一般选择 MySQL 而不是 Oracle?

    免费、流行、够用。

    ? 当然,这个回答要稍微润色下。不过一般,很少问这个问题了。

    数据库的三范式是什么?什么是反模式?

    艿艿:重点在于反模式的回答。实际开发中,不会严格遵守三范式。

    胖友直接看 《服务端指南 数据存储篇 | MySQL(07) 范式与反模式》

    MySQL 有哪些数据类型?

    MySQL 支持多种类型,大致可以分为三类:数值、日期/时间和字符串(字符)类型。具体可以看看 《MySQL 数据类型》 文档。

    • 正确的使用数据类型,对数据库的优化是非常重要的。

    ? MySQL 中 varchar 与 char 的区别?varchar(50) 中的 50 代表的涵义?

    • 1、varchar 与 char 的区别,char 是一种固定长度的类型,varchar 则是一种可变长度的类型。
    • 2、varchar(50) 中 50 的涵义最多存放 50 个字符。varchar(50) 和 (200) 存储 hello 所占空间一样,但后者在排序时会消耗更多内存,因为 ORDER BY col 采用 fixed_length 计算 col 长度(memory引擎也一样)。所以,实际场景下,选择合适的 varchar 长度还是有必要的。

    ? int(11) 中的 11 代表什么涵义?

    int(11) 中的 11 ,不影响字段存储的范围,只影响展示效果。具体可以看看 《MySQL 中 int 长度的意义》 文章。

    ? 金额(金钱)相关的数据,选择什么数据类型?

    • 方式一,使用 int 或者 bigint 类型。如果需要存储到分的维度,需要 *100 进行放大。
    • 方式二,使用 decimal 类型,避免精度丢失。如果使用 Java 语言时,需要使用 BigDecimal 进行对应。

    ? 一张表,里面有 ID 自增主键,当 insert 了 17 条记录之后,删除了第 15,16,17 条记录,再把 MySQL 重启,再 insert 一条记录,这条记录的 ID 是 18 还是 15?

    • 一般情况下,我们创建的表的类型是 InnoDB ,如果新增一条记录(不重启 MySQL 的情况下),这条记录的 ID 是18 ;但是如果重启 MySQL 的话,这条记录的 ID 是 15 。因为 InnoDB 表只把自增主键的最大 ID 记录到内存中,所以重启数据库或者对表 OPTIMIZE 操作,都会使最大 ID 丢失。
    • 但是,如果我们使用表的类型是 MyISAM ,那么这条记录的 ID 就是 18 。因为 MyISAM 表会把自增主键的最大 ID 记录到数据文件里面,重启 MYSQL 后,自增主键的最大 ID 也不会丢失。

    最后,还可以跟面试官装个 x ,生产数据,不建议进行物理删除记录。

    ? 表中有大字段 X(例如:text 类型),且字段 X 不会经常更新,以读为为主,请问您是选择拆成子表,还是继续放一起?写出您这样选择的理由

    • 拆带来的问题:连接消耗 + 存储拆分空间。

      如果能容忍拆分带来的空间问题,拆的话最好和经常要查询的表的主键在物理结构上放置在一起(分区) 顺序 IO ,减少连接消耗,最后这是一个文本列再加上一个全文索引来尽量抵消连接消耗。

    • 不拆可能带来的问题:查询性能。

      如果能容忍不拆分带来的查询性能损失的话,上面的方案在某个极致条件下肯定会出现问题,那么不拆就是最好的选择。

    实际场景下,例如说商品表数据量比较大的情况下,会将商品描述单独存储到一个表中。即,使用拆的方案。

    MySQL 有哪些存储引擎?

    MySQL 提供了多种的存储引擎:

    • InnoDB
    • MyISAM
    • MRG_MYISAM
    • MEMORY
    • CSV
    • ARCHIVE
    • BLACKHOLE
    • PERFORMANCE_SCHEMA
    • FEDERATED

    具体每种存储引擎的介绍,可以看看 《数据库存储引擎》

    ? 如何选择合适的存储引擎?

    提供几个选择标准,然后按照标准,选择对应的存储引擎即可,也可以根据 常用引擎对比 来选择你使用的存储引擎。使用哪种引擎需要根据需求灵活选择,一个数据库中多个表可以使用不同的引擎以满足各种性能和实际需求。使用合适的存储引擎,将会提高整个数据库的性能。

    1. 是否需要支持事务。

    2. 对索引和缓存的支持。

    3. 是否需要使用热备。

    4. 崩溃恢复,能否接受崩溃。

    5. 存储的限制。

    6. 是否需要外键支持。

      艿艿:目前开发已经不考虑外键,主要原因是性能。具体可以看看 《从 MySQL 物理外键开始的思考》 文章。

    目前,MySQL 默认的存储引擎是 InnoDB ,并且也是最主流的选择。主要原因如下:

    • 【最重要】支持事务。
    • 支持行级锁和表级锁,能支持更多的并发量。
    • 查询不加锁,完全不影响查询。
    • 支持崩溃后恢复。

    在 MySQL5.1 以及之前的版本,默认的存储引擎是 MyISAM ,但是目前已经不再更新,且它有几个比较关键的缺点:

    • 不支持事务。
    • 使用表级锁,如果数据量大,一个插入操作锁定表后,其他请求都将阻塞。

    艿艿:也就是说,我们不需要花太多力气在 MyISAM 的学习上。

    ? 请说明 InnoDB 和 MyISAM 的区别

    InnoDBMyISAM
    事务支持不支持
    存储限制64TB
    锁粒度行锁表锁
    崩溃后的恢复支持不支持
    外键支持不支持
    全文检索5.7 版本后支持支持

    更完整的对比,可以看看 《数据库存储引擎》「常用引擎对比」 小节。

    ? 请说说 InnoDB 的 4 大特性?

    艿艿:貌似我面试没被问过…反正,我是没弄懂过~~

    • 插入缓冲(insert buffer)
    • 二次写(double write)
    • 自适应哈希索引(ahi)
    • 预读(read ahead)

    ? 为什么 SELECT COUNT(*) FROM table 在 InnoDB 比 MyISAM 慢?

    对于 SELECT COUNT(*) FROM table 语句,在没有 WHERE 条件的情况下,InnoDB 比 MyISAM 可能会慢很多,尤其在大表的情况下。因为,InnoDB 是去实时统计结果,会全表扫描;而 MyISAM 内部维持了一个计数器,预存了结果,所以直接返回即可。

    详细的原因,胖友可以看看 《高性能 MySQL 之 Count 统计查询》 博客。

    ? 各种不同 MySQL 版本的 Innodb 的改进?

    艿艿:这是一个选择了解的问题。

    MySQL5.6 下 Innodb 引擎的主要改进:

    1. online DDL
    2. memcached NoSQL 接口
    3. transportable tablespace( alter table discard/import tablespace)
    4. MySQL 正常关闭时,可以 dump 出 buffer pool 的( space, page_no),重启时 reload,加快预热速度
    5. 索引和表的统计信息持久化到 mysql.innodb_table_stats 和 mysql.innodb_index_stats,可提供稳定的执行计划
    6. Compressed row format 支持压缩表

    MySQL5.7 下 Innodb 引擎的主要改进:

    • 1、修改 varchar 字段长度有时可以使用

      这里的“有时”,指的是也有些限制。可见 《MySQL 5.7 online ddl 的一些改进》

    • 2、Buffer pool 支持在线改变大小

    • 3、Buffer pool 支持导出部分比例

    • 4、支持新建 innodb tablespace,并可以在其中创建多张表

    • 5、磁盘临时表采用 innodb 存储,并且存储在 innodb temp tablespace 里面,以前是 MyISAM 存储

    • 6、透明表空间压缩功能

    重点】什么是索引?

    索引,类似于书籍的目录,想找到一本书的某个特定的主题,需要先找到书的目录,定位对应的页码。

    MySQL 中存储引擎使用类似的方式进行查询,先去索引中查找对应的值,然后根据匹配的索引找到对应的数据行。

    ? 索引有什么好处?

    1. 提高数据的检索速度,降低数据库IO成本:使用索引的意义就是通过缩小表中需要查询的记录的数目从而加快搜索的速度。
    2. 降低数据排序的成本,降低CPU消耗:索引之所以查的快,是因为先将数据排好序,若该字段正好需要排序,则正好降低了排序的成本。

    ? 索引有什么坏处?

    1. 占用存储空间:索引实际上也是一张表,记录了主键与索引字段,一般以索引文件的形式存储在磁盘上。
    2. 降低更新表的速度:表的数据发生了变化,对应的索引也需要一起变更,从而减低的更新速度。否则索引指向的物理数据可能不对,这也是索引失效的原因之一。

    ? 索引的使用场景?

    • 1、对非常小的表,大部分情况下全表扫描效率更高。

    • 2、对中大型表,索引非常有效。

    • 3、特大型的表,建立和使用索引的代价随着增长,可以使用分区技术来解决。

      实际场景下,MySQL 分区表很少使用,原因可以看看 《互联网公司为啥不使用 MySQL 分区表?》 文章。

      对于特大型的表,更常用的是“分库分表”,目前解决方案有 Sharding Sphere、MyCAT 等等。

    ? 索引的类型?

    索引,都是实现在存储引擎层的。主要有六种类型:

    • 1、普通索引:最基本的索引,没有任何约束。

    • 2、唯一索引:与普通索引类似,但具有唯一性约束。

    • 3、主键索引:特殊的唯一索引,不允许有空值。

    • 4、复合索引:将多个列组合在一起创建索引,可以覆盖多个列。

    • 5、外键索引:只有InnoDB类型的表才可以使用外键索引,保证数据的一致性、完整性和实现级联操作。

    • 6、全文索引:MySQL 自带的全文索引只能用于 InnoDB、MyISAM ,并且只能对英文进行全文检索,一般使用全文索引引擎。

      常用的全文索引引擎的解决方案有 Elasticsearch、Solr 等等。最为常用的是 Elasticsearch 。

    具体的使用,可以看看 《服务端指南 数据存储篇 | MySQL(03) 如何设计索引》

    ? MySQL 索引的“创建”原则?

    注意,是“创建”噢。

    • 1、最适合索引的列是出现在 WHERE 子句中的列,或连接子句中的列,而不是出现在 SELECT 关键字后的列。

    • 2、索引列的基数越大,索引效果越好。

      具体为什么,可以看看如下两篇文章:

    • 3、根据情况创建复合索引,复合索引可以提高查询效率。

      因为复合索引的基数会更大。

    • 4、避免创建过多的索引,索引会额外占用磁盘空间,降低写操作效率。

    • 5、主键尽可能选择较短的数据类型,可以有效减少索引的磁盘占用提高查询效率。

    • 6、对字符串进行索引,应该定制一个前缀长度,可以节省大量的索引空间。

    ? MySQL 索引的“使用”注意事项?

    注意,是“使用”噢。

    • 1、应尽量避免在 WHERE 子句中使用 !=<> 操作符,否则将引擎放弃使用索引而进行全表扫描。优化器将无法通过索引来确定将要命中的行数,因此需要搜索该表的所有行。

      注意,column IS NULL 也是不可以使用索引的。

    • 2、应尽量避免在 WHERE 子句中使用 OR 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:SELECT id FROM t WHERE num = 10 OR num = 20

    • 3、应尽量避免在 WHERE 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。

    • 4、应尽量避免在 WHERE 子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。

    • 5、不要在 WHERE 子句中的 = 左边进行函数、算术运算或其他表达式运算,否则系统将可能无法正确使用索引。

    • 6、复合索引遵循前缀原则。

    • 7、如果 MySQL 评估使用索引比全表扫描更慢,会放弃使用索引。如果此时想要索引,可以在语句中添加强制索引。

    • 8、列类型是字符串类型,查询时一定要给值加引号,否则索引失效。

    • 9、LIKE 查询,% 不能在前,因为无法使用索引。如果需要模糊匹配,可以使用全文索引。

    关于这块,可以看看 《服务端指南 数据存储篇 | MySQL(04) 索引使用的注意事项》 文章,写的更加细致。

    ? 以下三条 SQL 如何建索引,只建一条怎么建?

    WHERE a = 1 AND b = 1
    WHERE b = 1
    WHERE b = 1 ORDER BY time DESC
    
    
    • 以顺序 b , a, time 建立复合索引,CREATE INDEX table1_b_a_time ON index_test01(b, a, time)
    • 对于第一条 SQL ,因为最新 MySQL 版本会优化 WHERE 子句后面的列顺序,以匹配复合索引顺序。

    ? 想知道一个查询用到了哪个索引,如何查看?

    EXPLAIN 显示了 MYSQL 如何使用索引来处理 SELECT 语句以及连接表,可以帮助选择更好的索引和写出更优化的查询语句。

    使用方法,在 SELECT 语句前加上 EXPLAIN 就可以了。 《MySQL explain 执行计划详细解释》

    【重点】MySQL 索引的原理?

    解释 MySQL 索引的原理,篇幅会比较长,并且网络上已经有靠谱的资料可以看,所以艿艿这里整理了几篇,胖友可以对照着看。

    下面,艿艿对关键知识做下整理,方便胖友回顾。

    几篇好一点的文章:

    《MySQL索引背后的数据结构及算法原理》

    《MySQL 索引原理》

    《深入理解 MySQL 索引原理和实现 —— 为什么索引可以加速查询?》

    MySQL 有哪些索引方法?

    在 MySQL 中,我们可以看到两种索引方式:

    什么是 B-Tree 索引?

    B-Tree 是为磁盘等外存储设备设计的一种平衡查找树。因此在讲 B-Tree 之前先了解下磁盘的相关知识。

    • 系统从磁盘读取数据到内存时是以磁盘块(block)为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么。
    • InnoDB存储引擎中有页(Page)的概念,页是其磁盘管理的最小单位。InnoDB 存储引擎中默认每个页的大小为 16 KB,可通过参数 innodb_page_size 将页的大小设置为 4K、8K、16K ,在 MySQL 中可通过如下命令查看页的大小:
    mysql> show variables like 'innodb_page_size';
    
    • 而系统一个磁盘块的存储空间往往没有这么大,因此 InnoDB 每次申请磁盘空间时都会是若干地址连续磁盘块来达到页的大小 16KB 。InnoDB 在把磁盘数据读入到磁盘时会以页为基本单位,在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率。

    B-Tree 结构的数据可以让系统高效的找到数据所在的磁盘块。为了描述B-Tree,首先定义一条记录为一个二元组 [key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key值互不相同。

    一棵 m 阶的 B-Tree 有如下特性:

    1. 每个节点最多有 m 个孩子。
      • 除了根节点和叶子节点外,其它每个节点至少有 Ceil(m/2) 个孩子。
      • 若根节点不是叶子节点,则至少有 2 个孩子。
    2. 所有叶子节点都在同一层,且不包含其它关键字信息。
    3. 每个非叶子节点包含 n 个关键字信息(P0,P1,…Pn, k1,…kn)
      • 关键字的个数 n 满足:ceil(m/2)-1 <= n <= m-1
      • ki(i=1,…n) 为关键字,且关键字升序排序。
      • Pi(i=0,…n) 为指向子树根节点的指针。P(i-1) 指向的子树的所有节点关键字均小于 ki ,但都大于 k(i-1) 。

    B-Tree 中的每个节点根据实际情况可以包含大量的关键字信息和分支,如下图所示为一个 3 阶的 B-Tree:

    B-Tree 的结构

    • 每个节点占用一个盘块的磁盘空间,一个节点上有两个升序排序的 key 和三个指向子树根节点的 point ,point 存储的是子节点所在磁盘块的地址。两个 key 划分成的三个范围域,对应三个 point 指向的子树的数据的范围域。
    • 以根节点为例,key 为 17 和 35 ,P1 指针指向的子树的数据范围为小于 17 ,P2 指针指向的子树的数据范围为 [17~35] ,P3 指针指向的子树的数据范围为大于 35 。

    模拟查找 key 为 29 的过程:

    • 1、根据根节点找到磁盘块 1 ,读入内存。【磁盘I/O操作第1次】
    • 2、比较 key 29 在区间(17,35),找到磁盘块 1 的指针 P2 。
    • 3、根据 P2 指针找到磁盘块 3 ,读入内存。【磁盘I/O操作第2次】
    • 4、比较 key 29 在区间(26,30),找到磁盘块3的指针P2。
    • 5、根据 P2 指针找到磁盘块 8 ,读入内存。【磁盘I/O操作第3次】
    • 6、在磁盘块 8 中的 key 列表中找到 eky 29 。

    分析上面过程,发现需要 3 次磁盘 I/O 操作,和 3 次内存查找操作。由于内存中的 key 是一个有序表结构,可以利用二分法查找提高效率。而 3 次磁盘 I/O 操作是影响整个 B-Tree 查找效率的决定因素。B-Tree 相对于 AVLTree 缩减了节点个数,使每次磁盘 I/O 取到内存的数据都发挥了作用,从而提高了查询效率。

    什么是 B+Tree 索引?

    B+Tree 是在 B-Tree 基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用 B+Tree 实现其索引结构。

    从上一节中的 B-Tree 结构图中可以看到,每个节点中不仅包含数据的 key 值,还有 data 值。而每一个页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率。在 B+Tree 中,所有数据记录节点都是按照键值大小顺序存放在同一层的叶子节点上,而非叶子节点上只存储 key 值信息,这样可以大大加大每个节点存储的 key 值数量,降低 B+Tree 的高度。

    B+Tree 相对于 B-Tree 有几点不同:

    • 非叶子节点只存储键值信息。
    • 所有叶子节点之间都有一个链指针。
    • 数据记录都存放在叶子节点中。

    将上一节中的 B-Tree 优化,由于 B+Tree 的非叶子节点只存储键值信息,假设每个磁盘块能存储 4 个键值及指针信息,则变成 B+Tree 后其结构如下图所示:

    B+Tree 的结构

    • 通常在 B+Tree 上有两个头指针,一个指向根节点,另一个指向关键字最小的叶子节点,而且所有叶子节点(即数据节点)之间是一种链式环结构。因此可以对 B+Tree 进行两种查找运算:一种是对于主键的范围查找和分页查找,另一种是从根节点开始,进行随机查找。

    可能上面例子中只有 22 条数据记录,看不出 B+Tree 的优点,下面做一个推算:

    • InnoDB 存储引擎中页的大小为 16KB,一般表的主键类型为 INT(占用4个字节) 或 BIGINT(占用8个字节),指针类型也一般为 4 或 8 个字节,也就是说一个页(B+Tree 中的一个节点)中大概存储 16KB/(8B+8B)=1K 个键值(因为是估值,为方便计算,这里的 K 取值为〖10〗^3)。也就是说一个深度为 3 的 B+Tree 索引可以维护10^3 *10^3 *10^3 = 10亿 条记录。
    • 实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2~4 层。MySQL 的 InnoDB 存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作。

    B+Tree 有哪些索引类型?

    在 B+Tree 中,根据叶子节点的内容,索引类型分为主键索引非主键索引

    • 主键索引的叶子节点存的数据是整行数据( 即具体数据 )。在 InnoDB 里,主键索引也被称为聚集索引(clustered index)。
    • 非主键索引的叶子节点存的数据是整行数据的主键,键值是索引。在 InnoDB 里,非主键索引也被称为辅助索引(secondary index)。

    辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的聚集索引键,即主键。当通过辅助索引来查询数据时,需要进过两步:

    • 首先,InnoDB 存储引擎会遍历辅助索引找到主键。
    • 然后,再通过主键在聚集索引中找到完整的行记录数据。

    另外,InnoDB 通过主键聚簇数据,如果没有定义主键,会选择一个唯一的非空索引代替,如果没有这样的索引,会隐式定义个主键作为聚簇索引。

    再另外,可能有胖友有和艿艿的一样疑惑,在辅助索引如果相同的索引怎么存储?最终存储到 B+Tree 非子节点中时,它们对应的主键 ID 是不同的,所以妥妥的。如下图所示:

    相同的索引怎么存储

    聚簇索引的注意点有哪些?

    聚簇索引表最大限度地提高了 I/O 密集型应用的性能,但它也有以下几个限制:

    • 1、插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于 InnoDB 表,我们一般都会定义一个自增的 ID 列为主键。

      关于这一点,可能面试官会换一个问法。例如,为什么主键需要是自增 ID ,又或者为什么主键需要带有时间性关联。

    • 2、更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB 表,我们一般定义主键为不可更新。

      MySQL 默认情况下,主键是允许更新的。对于 MongoDB ,其 主键是不允许更新的。

    • 3、二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。

      当然,有一种情况可以无需二次查找,基于非主键索引查询,但是查询字段只有主键 ID ,那么在二级索引中就可以查找到。

    • 4、主键 ID 建议使用整型。因为,每个主键索引的 B+Tree 节点的键值可以存储更多主键 ID ,每个非主键索引的 B+Tree 节点的数据可以存储更多主键 ID 。

    什么是索引的最左匹配特性?

    当 B+Tree 的数据项是复合的数据结构,比如索引 (name, age, sex) 的时候,B+Tree 是按照从左到右的顺序来建立搜索树的。

    • 比如当 (张三, 20, F) 这样的数据来检索的时候,B+Tree 会优先比较 name 来确定下一步的所搜方向,如果 name 相同再依次比较 age 和 sex ,最后得到检索的数据。
    • 但当 (20, F) 这样的没有 name 的数据来的时候,B+Tree 就不知道下一步该查哪个节点,因为建立搜索树的时候 name 就是第一个比较因子,必须要先根据 name 来搜索才能知道下一步去哪里查询。
    • 比如当 (张三, F) 这样的数据来检索时,B+Tree 可以用 name 来指定搜索方向,但下一个字段 age 的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是 F 的数据了。

    这个是非常重要的性质,即索引的最左匹配特性。

    MyISAM 索引实现?

    MyISAM 索引的实现,和 InnoDB 索引的实现是一样使用 B+Tree ,差别在于 MyISAM 索引文件和数据文件是分离的,索引文件仅保存数据记录的地址

    MyISAM 索引与 InnoDB 索引的区别?

    • InnoDB 索引是聚簇索引,MyISAM 索引是非聚簇索引。
    • InnoDB 的主键索引的叶子节点存储着行数据,因此主键索引非常高效。
    • MyISAM 索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据。
    • InnoDB 非主键索引的叶子节点存储的是主键和其他带索引的列数据,因此查询时做到覆盖索引会非常高效。

    【重点】请说说 MySQL 的四种事务隔离级别?

    • 1、插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于 InnoDB 表,我们一般都会定义一个自增的 ID 列为主键。

      关于这一点,可能面试官会换一个问法。例如,为什么主键需要是自增 ID ,又或者为什么主键需要带有时间性关联。

    • 2、更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB 表,我们一般定义主键为不可更新。

      MySQL 默认情况下,主键是允许更新的。对于 MongoDB ,其 主键是不允许更新的。

    • 3、二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。

      当然,有一种情况可以无需二次查找,基于非主键索引查询,但是查询字段只有主键 ID ,那么在二级索引中就可以查找到。

    • 4、主键 ID 建议使用整型。因为,每个主键索引的 B+Tree 节点的键值可以存储更多主键 ID ,每个非主键索引的 B+Tree 节点的数据可以存储更多主键 ID 。

    • 1、插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于 InnoDB 表,我们一般都会定义一个自增的 ID 列为主键。

      关于这一点,可能面试官会换一个问法。例如,为什么主键需要是自增 ID ,又或者为什么主键需要带有时间性关联。

    • 2、更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB 表,我们一般定义主键为不可更新。

      MySQL 默认情况下,主键是允许更新的。对于 MongoDB ,其 主键是不允许更新的。

    • 3、二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。

      当然,有一种情况可以无需二次查找,基于非主键索引查询,但是查询字段只有主键 ID ,那么在二级索引中就可以查找到。

    • 4、主键 ID 建议使用整型。因为,每个主键索引的 B+Tree 节点的键值可以存储更多主键 ID ,每个非主键索引的 B+Tree 节点的数据可以存储更多主键 ID 。

    • 1、插入速度严重依赖于插入顺序,按照主键的顺序插入是最快的方式,否则将会出现页分裂,严重影响性能。因此,对于 InnoDB 表,我们一般都会定义一个自增的 ID 列为主键。

      关于这一点,可能面试官会换一个问法。例如,为什么主键需要是自增 ID ,又或者为什么主键需要带有时间性关联。

    • 2、更新主键的代价很高,因为将会导致被更新的行移动。因此,对于InnoDB 表,我们一般定义主键为不可更新。

      MySQL 默认情况下,主键是允许更新的。对于 MongoDB ,其 主键是不允许更新的。

    • 3、二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据。

      当然,有一种情况可以无需二次查找,基于非主键索引查询,但是查询字段只有主键 ID ,那么在二级索引中就可以查找到。

    • 4、主键 ID 建议使用整型。因为,每个主键索引的 B+Tree 节点的键值可以存储更多主键 ID ,每个非主键索引的 B+Tree 节点的数据可以存储更多主键 ID 。

    事务就是对一系列的数据库操作(比如插入多条数据)进行统一的提交或回滚操作,如果插入成功,那么一起成功,如果中间有一条出现异常,那么回滚之前的所有操作。

    这样可以防止出现脏数据,防止数据库数据出现问题。

    事务的特性指的是?

    指的是 ACID ,如下图所示:

    事务的特性

    1. 原子性 Atomicity :一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。
    2. 一致性 Consistency :在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束触发器级联回滚等。
    3. 隔离性 Isolation :数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
    4. 持久性 Durability :事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

    事务的并发问题?

    实际场景下,事务并不是串行的,所以会带来如下三个问题:

    • 1、脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据。
    • 2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果不一致。
    • 3、幻读:系统管理员 A 将数据库中所有学生的成绩从具体分数改为 ABCDE 等级,但是系统管理员 B 就在这个时候插入了一条具体分数的记录,当系统管理员 A 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

    小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。

    MySQL 事务隔离级别会产生的并发问题?

    • READ UNCOMMITTED(未提交读):事务中的修改,即使没有提交,对其他事务也都是可见的。

      会导致脏读。

    • READ COMMITTED(提交读):事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。

      会导致不可重复读。

      这个隔离级别,也可以叫做“不可重复读”。

    • REPEATABLE READ(可重复读):一个事务按相同的查询条件读取以前检索过的数据,其他事务插入了满足其查询条件的新数据。产生幻行。

      会导致幻读。

    • SERIALIZABLE(可串行化):强制事务串行执行。

    事务隔离级别脏读不可重复读幻读
    读未提交(read-uncommitted)
    读已提交(read-committed)
    可重复读(repeatable-read)是(x)
    串行化(serializable)
    • MySQL 默认的事务隔离级别为可重复读(repeatable-read) 。
    • 上图的 <X> 处,MySQL 因为其间隙锁的特性,导致其在可重复读(repeatable-read)的隔离级别下,不存在幻读问题。也就是说,上图 <X> 处,需要改成“否”!!!!
    • ? 记住这个表的方式,我们会发现它是自左上向右下是一个对角线。当然,最好是去理解。
    • 具体的实验,胖友可以看看 《MySQL 的四种事务隔离级别》

    【重点】请说说 MySQL 的锁机制?

    表锁是日常开发中的常见问题,因此也是面试当中最常见的考察点,当多个查询同一时刻进行数据修改时,就会产生并发控制的问题。MySQL 的共享锁和排他锁,就是读锁和写锁。

    • 共享锁:不堵塞,多个用户可以同时读一个资源,互不干扰。
    • 排他锁:一个写锁会阻塞其他的读锁和写锁,这样可以只允许一个用户进行写入,防止其他用户读取正在写入的资源。

    ? 锁的粒度?

    • 表锁:系统开销最小,会锁定整张表,MyIsam 使用表锁。
    • 行锁:最大程度的支持并发处理,但是也带来了最大的锁开销,InnoDB 使用行锁。

    ? 什么是悲观锁?什么是乐观锁?

    1)悲观锁

    它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

    在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。

    2)乐观锁

    相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。

    而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

    什么是死锁?

    多数情况下,可以认为如果一个资源被锁定,它总会在以后某个时间被释放。而死锁发生在当多个进程访问同一数据库时,其中每个进程拥有的锁都是其他进程所需的,由此造成每个进程都无法继续下去。简单的说,进程 A 等待进程 B 释放他的资源,B 又等待 A 释放他的资源,这样就互相等待就形成死锁。

    虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件:

    • 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
    • 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
    • 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
    • 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合 {P0,P1,P2,•••,Pn} 中的 P0 正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,……,Pn 正在等待已被 P0 占用的资源。

    下列方法有助于最大限度地降低死锁:

    • 设置获得锁的超时时间。

      通过超时,至少保证最差最差最差情况下,可以有退出的口子。

    • 按同一顺序访问对象。

      这个是最重要的方式。

    • 避免事务中的用户交互。

    • 保持事务简短并在一个批处理中。

    • 使用低隔离级别。

    • 使用绑定连接。

    ? MySQL 中 InnoDB 引擎的行锁是通过加在什么上完成(或称实现)的?为什么是这样子的??

    InnoDB 是基于索引来完成行锁。例如:SELECT * FROM tab_with_index WHERE id = 1 FOR UPDATE

    • FOR UPDATE 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么 InnoDB 将完成表锁,并发将无从谈起。

    【重要】MySQL 查询执行顺序?

    MySQL 查询执行的顺序是:

    (1)     SELECT
    (2)     DISTINCT <select_list>
    (3)     FROM <left_table>
    (4)     <join_type> JOIN <right_table>
    (5)     ON <join_condition>
    (6)     WHERE <where_condition>
    (7)     GROUP BY <group_by_list>
    (8)     HAVING <having_condition>
    (9)     ORDER BY <order_by_condition>
    (10)    LIMIT <limit_number>
    

    具体的,可以看看 《SQL 查询之执行顺序解析》 文章。

    【重要】聊聊 MySQL SQL 优化?

    可以看看如下几篇文章:

    另外,除了从 SQL 层面进行优化,也可以从服务器硬件层面,进一步优化 MySQL 。具体可以看看 《MySQL 数据库性能优化之硬件优化》

    编写 SQL 查询语句的考题合集

    MySQL 数据库 CPU 飙升到 500% 的话,怎么处理?

    当 CPU 飙升到 500% 时,先用操作系统命令 top 命令观察是不是 mysqld 占用导致的,如果不是,找出占用高的进程,并进行相关处理。

    如果此时是 IO 压力比较大,可以使用 iostat 命令,定位是哪个进程占用了磁盘 IO 。

    如果是 mysqld 造成的,使用 show processlist 命令,看看里面跑的 Session 情况,是不是有消耗资源的 SQL 在运行。找出消耗高的 SQL ,看看执行计划是否准确, index 是否缺失,或者实在是数据量太大造成。一般来说,肯定要 kill 掉这些线程(同时观察 CPU 使用率是否下降),等进行相应的调整(比如说加索引、改 SQL 、改内存参数)之后,再重新跑这些 SQL。

    也可以查看 MySQL 慢查询日志,看是否有慢 SQL 。

    也有可能是每个 SQL 消耗资源并不多,但是突然之间,有大量的 Session 连进来导致 CPU 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等。

    ? 在 MySQL 服务器运行缓慢的情况下输入什么命令能缓解服务器压力?

    1)检查系统的状态

    通过操作系统的一些工具检查系统的状态,比如 CPU、内存、交换、磁盘的利用率,根据经验或与系统正常时的状态相比对,有时系统表面上看起来看空闲,这也可能不是一个正常的状态,因为 CPU 可能正等待IO的完成。除此之外,还应观注那些占用系统资源(CPU、内存)的进程。

    • 使用 sar 来检查操作系统是否存在 IO 问题。
    • 使用 vmstat 监控内存 CPU 资源。
    • 磁盘 IO 问题,处理方式:做 raid10 提高性能 。
    • 网络问题,telnet 一下 MySQL 对外开放的端口。如果不通的话,看看防火墙是否正确设置了。另外,看看 MySQ L是不是开启了 skip-networking 的选项,如果开启请关闭。

    2)检查 MySQL 参数

    • max_connect_errors
    • connect_timeout
    • skip-name-resolve
    • slave-net-timeout=seconds
    • master-connect-retry

    3)检查 MySQL 相关状态值

    • 关注连接数
    • 关注下系统锁情况
    • 关注慢查询(slow query)日志

    Innodb 的事务与日志的实现方式

    ? 有多少种日志?

    • redo 日志
    • undo 日志

    ? 日志的存放形式?

    • redo:在页修改的时候,先写到 redo log buffer 里面, 然后写到 redo log 的文件系统缓存里面(fwrite),然后再同步到磁盘文件(fsync)。
    • undo:在 MySQL5.5 之前,undo 只能存放在 ibdata* 文件里面, 5.6 之后,可以通过设置 innodb_undo_tablespaces 参数把 undo log 存放在 ibdata* 之外。

    ? 事务是如何通过日志来实现的,说得越深入越好

    艿艿:这个流程的理解还是比较简单的,实际思考实现感觉还是蛮复杂的。

    基本流程如下:

    • 因为事务在修改页时,要先记 undo ,在记 undo 之前要记 undo 的 redo, 然后修改数据页,再记数据页修改的 redo。 redo(里面包括 undo 的修改)一定要比数据页先持久化到磁盘。
    • 当事务需要回滚时,因为有 undo,可以把数据页回滚到前镜像的状态。
    • 崩溃恢复时,如果 redo log 中事务没有对应的 commit 记录,那么需要用 undo 把该事务的修改回滚到事务开始之前。如果有 commit 记录,就用 redo 前滚到该事务完成时并提交掉。

    MySQL binlog 的几种日志录入格式以及区别

    ? 各种日志格式的涵义

    binlog 有三种格式类型,分别如下:

    1)Statement

    每一条会修改数据的 SQL 都会记录在 binlog 中。

    • 优点:不需要记录每一行的变化,减少了 binlog 日志量,节约了 IO,提高性能。(相比 row 能节约多少性能与日志量,这个取决于应用的 SQL 情况,正常同一条记录修改或者插入 row 格式所产生的日志量还小于 Statement 产生的日志量,但是考虑到如果带条件的 update 操作,以及整表删除,alter 表等操作,ROW 格式会产生大量日志,因此在考虑是否使用 ROW 格式日志时应该跟据应用的实际情况,其所产生的日志量会增加多少,以及带来的 IO 性能问题。)

    • 缺点:由于记录的只是执行语句,为了这些语句能在 slave 上正确运行,因此还必须记录每条语句在执行的时候的一些相关信息,以保证所有语句能在 slave 得到和在 master 端执行时候相同 的结果。另外 MySQL 的复制,像一些特定函数功能,slave 可与 master 上要保持一致会有很多相关问题(如 sleep() 函数,last_insert_id(),以及 user-defined functions(udf) 会出现问题)。

    • 使用以下函数的语句也无法被复制:

      • LOAD_FILE()

      • UUID()

      • USER()

      • FOUND_ROWS()

      • SYSDATE() (除非启动时启用了 --sysdate-is-now 选项)

        同时在 INSERT …SELECT 会产生比 RBR 更多的行级锁 。

    2)Row

    不记录 SQL 语句上下文相关信息,仅保存哪条记录被修改。

    • 优点:binlog 中可以不记录执行的 SQL 语句的上下文相关的信息,仅需要记录那一条记录被修改成什么了。所以 rowlevel 的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或 function ,以及 trigger 的调用和触发无法被正确复制的问题。
    • 缺点:所有的执行的语句当记录到日志中的时候,都将以每行记录的修改来记录,这样可能会产生大量的日志内容,比如一条 Update 语句,修改多条记录,则 binlog 中每一条修改都会有记录,这样造成 binlog 日志量会很大,特别是当执行 alter table 之类的语句的时候,由于表结构修改,每条记录都发生改变,那么该表每一条记录都会记录到日志中。

    3)Mixedlevel

    是以上两种 level 的混合使用。

    • 一般的语句修改使用 Statement 格式保存 binlog 。
    • 如一些函数,statement 无法完成主从复制的操作,则采用 Row 格式保存 binlog 。

    MySQL 会根据执行的每一条具体的 SQL 语句来区分对待记录的日志形式,也就是在 Statement 和 Row 之间选择 一种。

    新版本的 MySQL 中对 row level 模式也被做了优化,并不是所有的修改都会以 row level 来记录。

    • 像遇到表结构变更的时候就会以 Statement 模式来记录。
    • 至于 Update 或者 Delete 等修改数据的语句,还是会记录所有行的变更,即使用 Row 模式。

    ? 适用场景?

    在一条 SQL 操作了多行数据时, Statement 更节省空间,Row 更占用空间。但是, Row 模式更可靠。

    因为,互联网公司,使用 MySQL 的功能相对少,基本不使用存储过程、触发器、函数的功能,选择默认的语句模式,Statement Level(默认)即可。

    ? 结合第一个问题,每一种日志格式在复制中的优劣?

    • Statement 可能占用空间会相对小一些,传送到 slave 的时间可能也短,但是没有 Row 模式的可靠。
    • Row 模式在操作多行数据时更占用空间,但是可靠。

    所以,这是在占用空间和可靠之间的选择。

    如何在线正确清理 MySQL binlog?

    MySQL 中的 binlog 日志记录了数据中的数据变动,便于对数据的基于时间点和基于位置的恢复。但日志文件的大小会越来越大,占用大量的磁盘空间,因此需要定时清理一部分日志信息。

    # 首先查看主从库正在使用的binlog文件名称
    show master(slave) status
    
    # 删除之前一定要备份
    purge master logs before'2017-09-01 00:00:00'; # 删除指定时间前的日志
    purge master logs to'mysql-bin.000001'; # 删除指定的日志文件
    
    # 自动删除:通过设置binlog的过期时间让系统自动删除日志
    show variables like 'expire_logs_days'; # 查看过期时间
    set global expire_logs_days = 30; # 设置过期时间
    

    MySQL 主从复制的流程是怎么样的?

    MySQL 的主从复制是基于如下 3 个线程的交互(多线程复制里面应该是 4 类线程):

    • 1、Master 上面的 binlog dump 线程,该线程负责将 master 的 binlog event 传到 slave 。
    • 2、Slave 上面的 IO 线程,该线程负责接收 Master 传过来的 binlog,并写入 relay log 。
    • 3、Slave 上面的 SQL 线程,该线程负责读取 relay log 并执行。
    • 4、如果是多线程复制,无论是 5.6 库级别的假多线程还是 MariaDB 或者 5.7 的真正的多线程复制, SQL 线程只做 coordinator ,只负责把 relay log 中的 binlog 读出来然后交给 worker 线程, woker 线程负责具体 binlog event 的执行。

    ? MySQL 如何保证复制过程中数据一致性?

    • 1、在 MySQL5.5 以及之前, slave 的 SQL 线程执行的 relay log 的位置只能保存在文件( relay-log.info)里面,并且该文件默认每执行 10000 次事务做一次同步到磁盘, 这意味着 slave 意外 crash 重启时, SQL 线程执行到的位置和数据库的数据是不一致的,将导致复制报错,如果不重搭复制,则有可能会导致数据不一致。
      • MySQL 5.6 引入参数 relay_log_info_repository,将该参数设置为 TABLE 时, MySQL 将 SQL 线程执行到的位置存到 mysql.slave_relay_log_info 表,这样更新该表的位置和 SQL 线程执行的用户事务绑定成一个事务,这样 slave 意外宕机后,slave 通过 innodb 的崩溃恢复可以把 SQL 线程执行到的位置和用户事务恢复到一致性的状态。
    • 2、MySQL 5.6 引入 GTID 复制,每个 GTID 对应的事务在每个实例上面最多执行一次, 这极大地提高了复制的数据一致性。
    • 3、MySQL 5.5 引入半同步复制, 用户安装半同步复制插件并且开启参数后,设置超时时间,可保证在超时时间内如果 binlog 不传到 slave 上面,那么用户提交事务时不会返回,直到超时后切成异步复制,但是如果切成异步之前用户线程提交时在 master 上面等待的时候,事务已经提交,该事务对 master 上面的其他 session 是可见的,如果这时 master 宕机,那么到 slave 上面该事务又不可见了,该问题直到 5.7 才解决。
    • 4、MySQL 5.7 引入无损半同步复制,引入参 rpl_semi_sync_master_wait_point,该参数默认为 after_sync,指的是在切成半同步之前,事务不提交,而是接收到 slave 的 ACK 确认之后才提交该事务,从此,复制真正可以做到无损的了。
    • 5、可以再说一下 5.7 的无损复制情况下, master 意外宕机,重启后发现有 binlog 没传到 slave 上面,这部分 binlog 怎么办???分 2 种情况讨论, 1 宕机时已经切成异步了, 2 是宕机时还没切成异步??? 这个怎么判断宕机时有没有切成异步呢??? 分别怎么处理???

    ? MySQL 如何解决主从复制的延时性?

    5.5 是单线程复制,5.6 是多库复制(对于单库或者单表的并发操作是没用的),5.7 是真正意义的多线程复制,它的原理是基于 group commit, 只要 master 上面的事务是 group commit 的,那 slave 上面也可以通过多个 worker线程去并发执行。 和 MairaDB10.0.0.5 引入多线程复制的原理基本一样。

    ? 工作遇到的复制 bug 的解决方法?

    5.6 的多库复制有时候自己会停止,我们写了一个脚本重新 start slave 。

    ? 你是否做过主从一致性校验,如果有,怎么做的,如果没有,你打算怎么做?

    主从一致性校验有多种工具 例如 checksum、mysqldiff、pt-table-checksum 等。

    聊聊 MySQL 备份方式?备份策略是怎么样的?

    具体的,胖友可以看看 《MySQL 高级备份策略》 。主要有几个知识点:

    • 数据的备份类型

      • 【常用】完全备份

        这是大多数人常用的方式,它可以备份整个数据库,包含用户表、系统表、索引、视图和存储过程等所有数据库对象。但它需要花费更多的时间和空间,所以,一般推荐一周做一次完全备份。

      • 增量备份

        它是只备份数据库一部分的另一种方法,它不使用事务日志,相反,它使用整个数据库的一种新映象。它比最初的完全备份小,因为它只包含自上次完全备份以来所改变的数据库。它的优点是存储和恢复速度快。推荐每天做一次差异备份。

      • 【常用】事务日志备份

        事务日志是一个单独的文件,它记录数据库的改变,备份的时候只需要复制自上次备份以来对数据库所做的改变,所以只需要很少的时间。为了使数据库具有鲁棒性,推荐每小时甚至更频繁的备份事务日志。

      • 文件备份

        数据库可以由硬盘上的许多文件构成。如果这个数据库非常大,并且一个晚上也不能将它备份完,那么可以使用文件备份每晚备份数据库的一部分。由于一般情况下数据库不会大到必须使用多个文件存储,所以这种备份不是很常用。

    • 备份数据的类型

      • 热备份
      • 温备份
      • 冷备份
    • 备份工具

      • cp
      • mysqldump
      • xtrabackup
      • lvm2 快照

    MySQL 几种备份方式?

    MySQL 一般有 3 种备份方式。

    1)逻辑备份

    使用 MySQL 自带的 mysqldump 工具进行备份。备份成sql文件形式。

    • 优点:最大好处是能够与正在运行的 MySQL 自动协同工作,在运行期间可以确保备份是当时的点,它会自动将对应操作的表锁定,不允许其他用户修改(只能访问)。可能会阻止修改操作。SQL 文件通用方便移植。
    • 缺点:备份的速度比较慢。如果是数据量很多的时候,就很耗时间。如果数据库服务器处在提供给用户服务状态,在这段长时间操作过程中,意味着要锁定表(一般是读锁定,只能读不能写入数据),那么服务就会影响的。

    2)物理备份

    艿艿:因为现在主流是 InnoDB ,所以基本不再考虑这种方式。

    直接拷贝只适用于 MyISAM 类型的表。这种类型的表是与机器独立的。但实际情况是,你设计数据库的时候不可能全部使用 MyISAM 类型表。你也不可能因为 MyISAM 类型表与机器独立,方便移植,于是就选择这种表,这并不是选择它的理由。

    • 缺点:你不能去操作正在运行的 MySQL 服务器(在拷贝的过程中有用户通过应用程序访问更新数据,这样就无法备份当时的数据),可能无法移植到其他机器上去。

    3)双机热备份。

    当数据量太大的时候备份是一个很大的问题,MySQL 数据库提供了一种主从备份的机制,也就是双机热备。

    • 优点:适合数据量大的时候。现在明白了,大的互联网公司对于 MySQL 数据备份,都是采用热机备份。搭建多台数据库服务器,进行主从复制。

    数据库不能停机,请问如何备份? 如何进行全备份和增量备份?

    可以使用逻辑备份和双机热备份。

    • 完全备份:完整备份一般一段时间进行一次,且在网站访问量最小的时候,这样常借助批处理文件定时备份。主要是写一个批处理文件在里面写上处理程序的绝对路径然后把要处理的东西写在后面,即完全备份数据库。
    • 增量备份:对 ddl 和 dml 语句进行二进制备份。且 5.0 无法增量备份,5.1 后可以。如果要实现增量备份需要在 my.ini 文件中配置备份路径即可,重启 MySQL 服务器,增量备份就启动了。

    ? 你的备份工具的选择?备份计划是怎么样的?

    视库的大小来定,一般来说 100G 内的库,可以考虑使用 mysqldump 来做,因为 mysqldump 更加轻巧灵活,备份时间选在业务低峰期,可以每天进行都进行全量备份(mysqldump 备份出来的文件比较小,压缩之后更小)。

    100G 以上的库,可以考虑用 xtrabackup 来做,备份速度明显要比 mysqldump 要快。一般是选择一周一个全备,其余每天进行增量备份,备份时间为业务低峰期。

    备份恢复时间是多长?

    物理备份恢复快,逻辑备份恢复慢。

    这里跟机器,尤其是硬盘的速率有关系,以下列举几个仅供参考:

    • 20G 的 2 分钟(mysqldump)
    • 80G 的 30分钟(mysqldump)
    • 111G 的 30分钟(mysqldump)
    • 288G 的 3 小时(xtrabackup)
    • 3T 的 4 小时(xtrabackup)

    逻辑导入时间一般是备份时间的 5 倍以上。

    备份恢复失败如何处理?

    首先在恢复之前就应该做足准备工作,避免恢复的时候出错。比如说备份之后的有效性检查、权限检查、空间检查等。如果万一报错,再根据报错的提示来进行相应的调整。

    ? mysqldump 和 xtrabackup 实现原理?

    1)mysqldump

    mysqldump 是最简单的逻辑备份方式。

    • 在备份 MyISAM 表的时候,如果要得到一致的数据,就需要锁表,简单而粗暴。
    • 在备份 InnoDB 表的时候,加上 –master-data=1 –single-transaction 选项,在事务开始时刻,记录下 binlog pos 点,然后利用 MVCC 来获取一致的数据,由于是一个长事务,在写入和更新量很大的数据库上,将产生非常多的 undo ,显著影响性能,所以要慎用。
    • 优点:简单,可针对单表备份,在全量导出表结构的时候尤其有用。
    • 缺点:简单粗暴,单线程,备份慢而且恢复慢,跨 IDC 有可能遇到时区问题

    2)xtrabackup

    xtrabackup 实际上是物理备份+逻辑备份的组合。

    • 在备份 InnoDB 表的时候,它拷贝 ibd 文件,并一刻不停的监视 redo log 的变化,append 到自己的事务日志文件。在拷贝 ibd 文件过程中,ibd文件本身可能被写”花”,这都不是问题,因为在拷贝完成后的第一个 prepare 阶段,xtrabackup 采用类似于 Innodb 崩溃恢复的方法,把数据文件恢复到与日志文件一致的状态,并把未提交的事务回滚。
    • 如果同时需要备份 MyISAM 表以及 InnoDB 表结构等文件,那么就需要用 flush tables with lock 来获得全局锁,开始拷贝这些不再变化的文件,同时获得 binlog 位置,拷贝结束后释放锁,也停止对 redo log 的监视。

    如何从 mysqldump 产生的全库备份中只恢复某一个库、某一张表?

    具体可见 《MySQL 全库备份中恢复某个库和某张表以及 mysqldump 参数 –ignore-table 介绍》 文章。

    聊聊 MySQL 集群?

    艿艿:这块艿艿懂的少,主要找了一些网络上的资料。

    ? 对于简历中写有熟悉 MySQL 高可用方案?

    我一般先问他现在管理的数据库架构是什么,如果他只说出了主从,而没有说任何 HA 的方案,那么我就可以判断出他没有实际的 HA 经验。

    不过这时候也不能就是断定他不懂 MySQL 高可用,也许是没有实际机会去使用,那么我就要问 MMM 以及 MHA 以及 MM + keepalived 等的原理、实现方式以及它们之间的优势和不足了,一般这种情况下,能说出这个的基本没有。

    • MMM 那东西好像不靠谱,据说不稳定,但是有人在用的,和 mysql-router 比较像,都是指定可写的机器和只读机器。
    • MHA 的话一句话说不完,可以搜索下相关博客。

    聊聊 MySQL 安全?

    感兴趣的胖友,可以看看:

    MySQL 有哪些日志?

    • 错误日志:记录了当 mysqld 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。

    • 二进制文件:记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,不包括数据查询语句。语句以“事件”的形式保存,它描述了数据的更改过程。(定期删除日志,默认关闭)。

      就是我们上面看到的 MySQL binlog 日志。

    • 查询日志:记录了客户端的所有语句,格式为纯文本格式,可以直接进行读取。(log 日志中记录了所有数据库的操作,对于访问频繁的系统,此日志对系统性能的影响较大,建议关闭,默认关闭)。

    • 慢查询日志:慢查询日志记录了包含所有执行时间超过参数long_query_time(单位:秒)所设置值的 SQL 语句的日志。(纯文本格式)

      重要,一定要开启。

    另外,错误日志和慢查询日志的详细解释,可以看看 《MySQL 日志文件之错误日志和慢查询日志详解》 文章。

    聊聊 MySQL 监控?

    你是如何监控你们的数据库的?

    监控的工具有很多,例如 Zabbix ,Lepus ,我这里用的是 Lepus

    对一个大表做在线 DDL ,怎么进行实施的才能尽可能降低影响?

    使用 pt-online-schema-change ,具体可以看看 《MySQL 大表在线 DML 神器–pt-online-schema-change》 文章。

    另外,还有一些其它的工具,胖友可以搜索下。

    展开全文
  • 你想太多了,部署时候基本上只有webapp里的会直接输出到根目录,其他都会放入WEB-INF里面,项目内部依然可以使用classpath:XXX来访问,好像IDE里可以设置部署输出目录,这里扯远了~ - test 这里是测试分支。...

    本文发表于2016年6月,写于作者学生时期。文中使用到的技术和框架可能不是当下最佳实践,甚至很不“优雅”。但对于刚接触JavaEE和Spring的同学来说,还是能有很多收获的,大牛轻拍= =

    我们看招聘信息的时候,经常会看到这一点,需要具备SSH框架的技能;而且在大部分教学课堂中,也会把SSH作为最核心的教学内容。
    但是,我们在实际应用中发现,SpringMVC可以完全替代Struts,配合注解的方式,编程非常快捷,而且通过restful风格定义url,让地址看起来非常优雅。
    另外,MyBatis也可以替换Hibernate,正因为MyBatis的半自动特点,我们程序猿可以完全掌控SQL,这会让有数据库经验的程序猿能开发出高效率的SQL语句,而且XML配置管理起来也非常方便。
    好了,如果你也认同我的看法,那么下面我们一起来做整合吧!

    在写代码之前我们先了解一下这三个框架分别是干什么的?
    相信大以前也看过不少这些概念,我这就用大白话来讲,如果之前有了解过可以跳过这一大段,直接看代码!

    1. SpringMVC:它用于web层,相当于controller(等价于传统的servlet和struts的action),用来处理用户请求。举个例子,用户在地址栏输入http://网站域名/login,那么springmvc就会拦截到这个请求,并且调用controller层中相应的方法,(中间可能包含验证用户名和密码的业务逻辑,以及查询数据库操作,但这些都不是springmvc的职责),最终把结果返回给用户,并且返回相应的页面(当然也可以只返回json/xml等格式数据)。springmvc就是做前面和后面过程的活,与用户打交道!!

    2. Spring:太强大了,以至于我无法用一个词或一句话来概括它。但与我们平时开发接触最多的估计就是IOC容器,它可以装载bean(也就是我们java中的类,当然也包括service dao里面的),有了这个机制,我们就不用在每次使用这个类的时候为它初始化,很少看到关键字new。另外spring的aop,事务管理等等都是我们经常用到的。

    3. MyBatis:如果你问我它跟鼎鼎大名的Hibernate有什么区别?我只想说,他更符合我的需求。第一,它能自由控制sql,这会让有数据库经验的人(当然不是说我啦捂脸)编写的代码能搞提升数据库访问的效率。第二,它可以使用xml的方式来组织管理我们的sql,因为一般程序出错很多情况下是sql出错,别人接手代码后能快速找到出错地方,甚至可以优化原来写的sql。


    SSM框架整合配置

    好了,前面bb那么多,下面我们真正开始敲代码了~

    首先我们打开IED,我这里用的是eclipse(你们应该也是用的这个,对吗?),创建一个动态web项目,建立好相应的目录结构(重点!)

    项目结构图

    (打了马赛克是因为这里还用不到,你们不要那么污好不好?)

    我说一下每个目录都有什么用吧(第一次画表格,我发现markdown的表格语法很不友好呀~)
    这个目录结构同时也遵循maven的目录规范~

    文件名作用
    src根目录,没什么好说的,下面有main和test。
    - main主要目录,可以放java代码和一些资源文件。
    - - java存放我们的java代码,这个文件夹要使用Build Path -> Use as Source Folder,这样看包结构会方便很多,新建的包就相当于在这里新建文件夹咯。
    - - resources存放资源文件,譬如各种的spring,mybatis,log配置文件。
    - - - mapper存放dao中每个方法对应的sql,在这里配置,无需写daoImpl。
    - - - spring这里当然是存放spring相关的配置文件,有dao service web三层。
    - - - sql其实这个可以没有,但是为了项目完整性还是加上吧。
    - - - webapp这个貌似是最熟悉的目录了,用来存放我们前端的静态资源,如jsp js css。
    - - - - resources这里的资源是指项目的静态资源,如js css images等。
    - - - - WEB-INF很重要的一个目录,外部浏览器无法访问,只有羡慕内部才能访问,可以把jsp放在这里,另外就是web.xml了。你可能有疑问了,为什么上面java中的resources里面的配置文件不妨在这里,那么是不是会被外部窃取到?你想太多了,部署时候基本上只有webapp里的会直接输出到根目录,其他都会放入WEB-INF里面,项目内部依然可以使用classpath:XXX来访问,好像IDE里可以设置部署输出目录,这里扯远了~
    - test这里是测试分支。
    - - java测试java代码,应遵循包名相同的原则,这个文件夹同样要使用Build Path -> Use as Source Folder,这样看包结构会方便很多。
    - - resources没什么好说的,好像也很少用到,但这个是maven的规范。

    我先新建好几个必要的,并为大家讲解一下每个包的作用,顺便理清一下后台的思路~

    包结构图

    包名名称作用
    dao数据访问层(接口)与数据打交道,可以是数据库操作,也可以是文件读写操作,甚至是redis缓存操作,总之与数据操作有关的都放在这里,也有人叫做dal或者数据持久层都差不多意思。为什么没有daoImpl,因为我们用的是mybatis,所以可以直接在配置文件中实现接口的每个方法。
    entity实体类一般与数据库的表相对应,封装dao层取出来的数据为一个对象,也就是我们常说的pojo,一般只在dao层与service层之间传输。
    dto数据传输层刚学框架的人可能不明白这个有什么用,其实就是用于service层与web层之间传输,为什么不直接用entity(pojo)?其实在实际开发中发现,很多时间一个entity并不能满足我们的业务需求,可能呈现给用户的信息十分之多,这时候就有了dto,也相当于vo,记住一定不要把这个混杂在entity里面,答应我好吗?
    service业务逻辑(接口)写我们的业务逻辑,也有人叫bll,在设计业务接口时候应该站在“使用者”的角度。额,不要问我为什么这里没显示!IDE调皮我也拿它没办法~
    serviceImpl业务逻辑(实现)实现我们业务接口,一般事务控制是写在这里,没什么好说的。
    web控制器springmvc就是在这里发挥作用的,一般人叫做controller控制器,相当于struts中的action。

    还有最后一步基础工作,导入我们相应的jar包,我使用的是maven来管理我们的jar,所以只需要在pom.xml中加入相应的依赖就好了,如果不使用maven的可以自己去官网下载相应的jar,放到项目WEB-INF/lib目录下。关于maven的学习大家可以看慕课网的视频教程,这里就不展开了。我把项目用到的jar都写在下面,版本都不是最新的,大家有经验的话可以自己调整版本号。另外,所有jar都会与项目一起打包放到我的github上,喜欢的给个star吧~

    pom.xml

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    	<groupId>com.soecode.ssm</groupId>
    	<artifactId>ssm</artifactId>
    	<packaging>war</packaging>
    	<version>0.0.1-SNAPSHOT</version>
    	<name>ssm Maven Webapp</name>
    	<url>http://github.com/liyifeng1994/ssm</url>
    	<dependencies>
    		<!-- 单元测试 -->
    		<dependency>
    			<groupId>junit</groupId>
    			<artifactId>junit</artifactId>
    			<version>4.11</version>
    		</dependency>
    
    		<!-- 1.日志 -->
    		<!-- 实现slf4j接口并整合 -->
    		<dependency>
    			<groupId>ch.qos.logback</groupId>
    			<artifactId>logback-classic</artifactId>
    			<version>1.1.1</version>
    		</dependency>
    
    		<!-- 2.数据库 -->
    		<dependency>
    			<groupId>mysql</groupId>
    			<artifactId>mysql-connector-java</artifactId>
    			<version>5.1.37</version>
    			<scope>runtime</scope>
    		</dependency>
    		<dependency>
    			<groupId>c3p0</groupId>
    			<artifactId>c3p0</artifactId>
    			<version>0.9.1.2</version>
    		</dependency>
    
    		<!-- DAO: MyBatis -->
    		<dependency>
    			<groupId>org.mybatis</groupId>
    			<artifactId>mybatis</artifactId>
    			<version>3.3.0</version>
    		</dependency>
    		<dependency>
    			<groupId>org.mybatis</groupId>
    			<artifactId>mybatis-spring</artifactId>
    			<version>1.2.3</version>
    		</dependency>
    
    		<!-- 3.Servlet web -->
    		<dependency>
    			<groupId>taglibs</groupId>
    			<artifactId>standard</artifactId>
    			<version>1.1.2</version>
    		</dependency>
    		<dependency>
    			<groupId>jstl</groupId>
    			<artifactId>jstl</artifactId>
    			<version>1.2</version>
    		</dependency>
    		<dependency>
    			<groupId>com.fasterxml.jackson.core</groupId>
    			<artifactId>jackson-databind</artifactId>
    			<version>2.5.4</version>
    		</dependency>
    		<dependency>
    			<groupId>javax.servlet</groupId>
    			<artifactId>javax.servlet-api</artifactId>
    			<version>3.1.0</version>
    		</dependency>
    
    		<!-- 4.Spring -->
    		<!-- 1)Spring核心 -->
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-core</artifactId>
    			<version>4.1.7.RELEASE</version>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-beans</artifactId>
    			<version>4.1.7.RELEASE</version>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-context</artifactId>
    			<version>4.1.7.RELEASE</version>
    		</dependency>
    		<!-- 2)Spring DAO层 -->
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-jdbc</artifactId>
    			<version>4.1.7.RELEASE</version>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-tx</artifactId>
    			<version>4.1.7.RELEASE</version>
    		</dependency>
    		<!-- 3)Spring web -->
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-web</artifactId>
    			<version>4.1.7.RELEASE</version>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-webmvc</artifactId>
    			<version>4.1.7.RELEASE</version>
    		</dependency>
    		<!-- 4)Spring test -->
    		<dependency>
    			<groupId>org.springframework</groupId>
    			<artifactId>spring-test</artifactId>
    			<version>4.1.7.RELEASE</version>
    		</dependency>
    
    		<!-- redis客户端:Jedis -->
    		<dependency>
    			<groupId>redis.clients</groupId>
    			<artifactId>jedis</artifactId>
    			<version>2.7.3</version>
    		</dependency>
    		<dependency>
    			<groupId>com.dyuproject.protostuff</groupId>
    			<artifactId>protostuff-core</artifactId>
    			<version>1.0.8</version>
    		</dependency>
    		<dependency>
    			<groupId>com.dyuproject.protostuff</groupId>
    			<artifactId>protostuff-runtime</artifactId>
    			<version>1.0.8</version>
    		</dependency>
    
    		<!-- Map工具类 -->
    		<dependency>
    			<groupId>commons-collections</groupId>
    			<artifactId>commons-collections</artifactId>
    			<version>3.2</version>
    		</dependency>
    	</dependencies>
    	<build>
    		<finalName>ssm</finalName>
    	</build>
    </project>
    
    

    下面真的要开始进行编码工作了,坚持到这里辛苦大家了~

    第一步:我们先在spring文件夹里新建spring-dao.xml文件,因为spring的配置太多,我们这里分三层,分别是dao service web。

    1. 读入数据库连接相关参数(可选)
    2. 配置数据连接池
    3. 配置连接属性,可以不读配置项文件直接在这里写死
    4. 配置c3p0,只配了几个常用的
    5. 配置SqlSessionFactory对象(mybatis)
    6. 扫描dao层接口,动态实现dao接口,也就是说不需要daoImpl,sql和参数都写在xml文件上

    spring-dao.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
    	xsi:schemaLocation="http://www.springframework.org/schema/beans
    	http://www.springframework.org/schema/beans/spring-beans.xsd
    	http://www.springframework.org/schema/context
    	http://www.springframework.org/schema/context/spring-context.xsd">
    	<!-- 配置整合mybatis过程 -->
    	<!-- 1.配置数据库相关参数properties的属性:${url} -->
    	<context:property-placeholder location="classpath:jdbc.properties" />
    
    	<!-- 2.数据库连接池 -->
    	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    		<!-- 配置连接池属性 -->
    		<property name="driverClass" value="${jdbc.driver}" />
    		<property name="jdbcUrl" value="${jdbc.url}" />
    		<property name="user" value="${jdbc.username}" />
    		<property name="password" value="${jdbc.password}" />
    
    		<!-- c3p0连接池的私有属性 -->
    		<property name="maxPoolSize" value="30" />
    		<property name="minPoolSize" value="10" />
    		<!-- 关闭连接后不自动commit -->
    		<property name="autoCommitOnClose" value="false" />
    		<!-- 获取连接超时时间 -->
    		<property name="checkoutTimeout" value="10000" />
    		<!-- 当获取连接失败重试次数 -->
    		<property name="acquireRetryAttempts" value="2" />
    	</bean>
    
    	<!-- 3.配置SqlSessionFactory对象 -->
    	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    		<!-- 注入数据库连接池 -->
    		<property name="dataSource" ref="dataSource" />
    		<!-- 配置MyBaties全局配置文件:mybatis-config.xml -->
    		<property name="configLocation" value="classpath:mybatis-config.xml" />
    		<!-- 扫描entity包 使用别名 -->
    		<property name="typeAliasesPackage" value="com.soecode.lyf.entity" />
    		<!-- 扫描sql配置文件:mapper需要的xml文件 -->
    		<property name="mapperLocations" value="classpath:mapper/*.xml" />
    	</bean>
    
    	<!-- 4.配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中 -->
    	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    		<!-- 注入sqlSessionFactory -->
    		<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
    		<!-- 给出需要扫描Dao接口包 -->
    		<property name="basePackage" value="com.soecode.lyf.dao" />
    	</bean>
    </beans>
    

    因为数据库配置相关参数是读取配置文件,所以在resources文件夹里新建一个jdbc.properties文件,存放我们4个最常见的数据库连接属性,这是我本地的,大家记得修改呀~还有喜欢传到github上“大头虾们”记得删掉密码,不然别人就很容易得到你服务器的数据库配置信息,然后干一些羞羞的事情,你懂的!!

    jdbc.properties

    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3307/ssm?useUnicode=true&characterEncoding=utf8
    jdbc.username=root
    jdbc.password=
    

    友情提示:配置文件中的jdbc.username,如果写成username,可能会与系统环境中的username变量冲突,所以到时候真正连接数据库的时候,用户名就被替换成系统中的用户名(有得可能是administrator),那肯定是连接不成功的,这里有个小坑,我被坑了一晚上!!

    因为这里用到了mybatis,所以需要配置mybatis核心文件,在recources文件夹里新建mybatis-config.xml文件。

    1. 使用自增主键
    2. 使用列别名
    3. 开启驼峰命名转换 create_time -> createTime

    mybatis-config.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
      PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
      "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
    	<!-- 配置全局属性 -->
    	<settings>
    		<!-- 使用jdbc的getGeneratedKeys获取数据库自增主键值 -->
    		<setting name="useGeneratedKeys" value="true" />
    
    		<!-- 使用列别名替换列名 默认:true -->
    		<setting name="useColumnLabel" value="true" />
    
    		<!-- 开启驼峰命名转换:Table{create_time} -> Entity{createTime} -->
    		<setting name="mapUnderscoreToCamelCase" value="true" />
    	</settings>
    </configuration>
    

    第二步:刚弄好dao层,接下来到service层了。在spring文件夹里新建spring-service.xml文件。

    1. 扫描service包所有注解 @Service
    2. 配置事务管理器,把事务管理交由spring来完成
    3. 配置基于注解的声明式事务,可以直接在方法上@Transaction

    spring-service.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    	xmlns:context="http://www.springframework.org/schema/context"
    	xmlns:tx="http://www.springframework.org/schema/tx"
    	xsi:schemaLocation="http://www.springframework.org/schema/beans
    	http://www.springframework.org/schema/beans/spring-beans.xsd
    	http://www.springframework.org/schema/context
    	http://www.springframework.org/schema/context/spring-context.xsd
    	http://www.springframework.org/schema/tx
    	http://www.springframework.org/schema/tx/spring-tx.xsd">
    	<!-- 扫描service包下所有使用注解的类型 -->
    	<context:component-scan base-package="com.soecode.lyf.service" />
    
    	<!-- 配置事务管理器 -->
    	<bean id="transactionManager"
    		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    		<!-- 注入数据库连接池 -->
    		<property name="dataSource" ref="dataSource" />
    	</bean>
    
    	<!-- 配置基于注解的声明式事务 -->
    	<tx:annotation-driven transaction-manager="transactionManager" />
    </beans>
    

    第三步:配置web层,在spring文件夹里新建spring-web.xml文件。

    1. 开启SpringMVC注解模式,可以使用@RequestMapping,@PathVariable,@ResponseBody等
    2. 对静态资源处理,如js,css,jpg等
    3. 配置jsp 显示ViewResolver,例如在controller中某个方法返回一个string类型的"login",实际上会返回"/WEB-INF/login.jsp"
    4. 扫描web层 @Controller

    spring-web.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xmlns:context="http://www.springframework.org/schema/context"
    	xmlns:mvc="http://www.springframework.org/schema/mvc" 
    	xsi:schemaLocation="http://www.springframework.org/schema/beans
    	http://www.springframework.org/schema/beans/spring-beans.xsd
    	http://www.springframework.org/schema/context
    	http://www.springframework.org/schema/context/spring-context.xsd
    	http://www.springframework.org/schema/mvc
    	http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
    	<!-- 配置SpringMVC -->
    	<!-- 1.开启SpringMVC注解模式 -->
    	<!-- 简化配置: 
    		(1)自动注册DefaultAnootationHandlerMapping,AnotationMethodHandlerAdapter 
    		(2)提供一些列:数据绑定,数字和日期的format @NumberFormat, @DateTimeFormat, xml,json默认读写支持 
    	-->
    	<mvc:annotation-driven />
    	
    	<!-- 2.静态资源默认servlet配置
    		(1)加入对静态资源的处理:js,gif,png
    		(2)允许使用"/"做整体映射
    	 -->
    	 <mvc:default-servlet-handler/>
    	 
    	 <!-- 3.配置jsp 显示ViewResolver -->
    	 <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    	 	<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
    	 	<property name="prefix" value="/WEB-INF/jsp/" />
    	 	<property name="suffix" value=".jsp" />
    	 </bean>
    	 
    	 <!-- 4.扫描web相关的bean -->
    	 <context:component-scan base-package="com.soecode.lyf.web" />
    </beans>
    

    第四步:最后就是修改web.xml文件了,它在webappWEB-INF下。

    web.xml

    <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                          http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
    	version="3.1" metadata-complete="true">
    	<!-- 如果是用mvn命令生成的xml,需要修改servlet版本为3.1 -->
    	<!-- 配置DispatcherServlet -->
    	<servlet>
    		<servlet-name>seckill-dispatcher</servlet-name>
    		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    		<!-- 配置springMVC需要加载的配置文件
    			spring-dao.xml,spring-service.xml,spring-web.xml
    			Mybatis - > spring -> springmvc
    		 -->
    		<init-param>
    			<param-name>contextConfigLocation</param-name>
    			<param-value>classpath:spring/spring-*.xml</param-value>
    		</init-param>
    	</servlet>
    	<servlet-mapping>
    		<servlet-name>seckill-dispatcher</servlet-name>
    		<!-- 默认匹配所有的请求 -->
    		<url-pattern>/</url-pattern>
    	</servlet-mapping>
    </web-app>
    
    

    我们在项目中经常会使用到日志,所以这里还有配置日志xml,在resources文件夹里新建logback.xml文件,所给出的日志输出格式也是最基本的控制台s呼出,大家有兴趣查看logback官方文档

    logback.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration debug="true">
    	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    		<!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    		<encoder>
    			<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    		</encoder>
    	</appender>
    
    	<root level="debug">
    		<appender-ref ref="STDOUT" />
    	</root>
    </configuration>
    

    到目前为止,我们一共写了7个配置文件,我们一起来看下最终的配置文件结构图

    配置文件结构图


    SSM框架应用实例(图书管理系统)

    一开始想就这样结束教程,但是发现其实很多人都还不会把这个SSM框架用起来,特别是mybatis部分。那我现在就以最常见的“图书管理系统”中【查询图书】和【预约图书】业务来做一个demo吧!

    首先新建数据库名为ssm,再创建两张表:图书表book和预约图书表appointment,并且为book表初始化一些数据,sql如下。

    schema.sql

    -- 创建图书表
    CREATE TABLE `book` (
      `book_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '图书ID',
      `name` varchar(100) NOT NULL COMMENT '图书名称',
      `number` int(11) NOT NULL COMMENT '馆藏数量',
      PRIMARY KEY (`book_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='图书表'
    
    -- 初始化图书数据
    INSERT INTO `book` (`book_id`, `name`, `number`)
    VALUES
    	(1000, 'Java程序设计', 10),
    	(1001, '数据结构', 10),
    	(1002, '设计模式', 10),
    	(1003, '编译原理', 10)
    
    -- 创建预约图书表
    CREATE TABLE `appointment` (
      `book_id` bigint(20) NOT NULL COMMENT '图书ID',
      `student_id` bigint(20) NOT NULL COMMENT '学号',
      `appoint_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '预约时间' ,
      PRIMARY KEY (`book_id`, `student_id`),
      INDEX `idx_appoint_time` (`appoint_time`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='预约图书表'
    

    entity包中添加两个对应的实体,图书实体Book.java和预约图书实体Appointment.java

    Book.java

    package com.soecode.lyf.entity;
    
    public class Book {
    
    	private long bookId;// 图书ID
    
    	private String name;// 图书名称
    
    	private int number;// 馆藏数量
    
    	// 省略构造方法,getter和setter方法,toString方法
    
    }
    

    Appointment.java

    package com.soecode.lyf.entity;
    
    import java.util.Date;
    
    /**
     * 预约图书实体
     */
    public class Appointment {
    
    	private long bookId;// 图书ID
    
    	private long studentId;// 学号
    
    	private Date appointTime;// 预约时间
    
    	// 多对一的复合属性
    	private Book book;// 图书实体
    	
    	// 省略构造方法,getter和setter方法,toString方法
    
    }
    

    dao包新建接口BookDao.javaAppointment.java

    BookDao.java

    package com.soecode.lyf.dao;
    
    import java.util.List;
    
    import com.soecode.lyf.entity.Book;
    
    public interface BookDao {
    
    	/**
    	 * 通过ID查询单本图书
    	 * 
    	 * @param id
    	 * @return
    	 */
    	Book queryById(long id);
    
    	/**
    	 * 查询所有图书
    	 * 
    	 * @param offset 查询起始位置
    	 * @param limit 查询条数
    	 * @return
    	 */
    	List<Book> queryAll(@Param("offset") int offset, @Param("limit") int limit);
    
    	/**
    	 * 减少馆藏数量
    	 * 
    	 * @param bookId
    	 * @return 如果影响行数等于>1,表示更新的记录行数
    	 */
    	int reduceNumber(long bookId);
    }
    
    

    AppointmentDao.java

    package com.soecode.lyf.dao;
    
    import org.apache.ibatis.annotations.Param;
    
    import com.soecode.lyf.entity.Appointment;
    
    public interface AppointmentDao {
    
    	/**
    	 * 插入预约图书记录
    	 * 
    	 * @param bookId
    	 * @param studentId
    	 * @return 插入的行数
    	 */
    	int insertAppointment(@Param("bookId") long bookId, @Param("studentId") long studentId);
    
    	/**
    	 * 通过主键查询预约图书记录,并且携带图书实体
    	 * 
    	 * @param bookId
    	 * @param studentId
    	 * @return
    	 */
    	Appointment queryByKeyWithBook(@Param("bookId") long bookId, @Param("studentId") long studentId);
    
    }
    

    提示:这里为什么要给方法的参数添加@Param注解呢?是因为该方法有两个或以上的参数,一定要加,不然mybatis识别不了。上面的BookDao接口的queryById方法和reduceNumber方法只有一个参数book_id,所以可以不用加 @Param注解,当然加了也无所谓~


    注意,这里不需要实现dao接口不用编写daoImpl, mybatis会给我们动态实现,但是我们需要编写相应的mapper。
    mapper目录里新建两个文件BookDao.xmlAppointmentDao.xml,分别对应上面两个dao接口,代码如下。

    BookDao.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.soecode.lyf.dao.BookDao">
    	<!-- 目的:为dao接口方法提供sql语句配置 -->
    	<select id="queryById" resultType="Book" parameterType="long">
    		<!-- 具体的sql -->
    		SELECT
    			book_id,
    			name,
    			number
    		FROM
    			book
    		WHERE
    			book_id = #{bookId}
    	</select>
    	
    	<select id="queryAll" resultType="Book">
    		SELECT
    			book_id,
    			name,
    			number
    		FROM
    			book
    		ORDER BY
    			book_id
    		LIMIT #{offset}, #{limit}
    	</select>
    	
    	<update id="reduceNumber">
    		UPDATE book
    		SET number = number - 1
    		WHERE
    			book_id = #{bookId}
    		AND number > 0
    	</update>
    </mapper>
    

    AppointmentDao.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.soecode.lyf.dao.AppointmentDao">
    	<insert id="insertAppointment">
    		<!-- ignore 主键冲突,报错 -->
    		INSERT ignore INTO appointment (book_id, student_id)
    		VALUES (#{bookId}, #{studentId})
    	</insert>
    	
    	<select id="queryByKeyWithBook" resultType="Appointment">
    		<!-- 如何告诉MyBatis把结果映射到Appointment同时映射book属性 -->
    		<!-- 可以自由控制SQL -->
    		SELECT
    			a.book_id,
    			a.student_id,
    			a.appoint_time,
    			b.book_id "book.book_id",
    			b.`name` "book.name",
    			b.number "book.number"
    		FROM
    			appointment a
    		INNER JOIN book b ON a.book_id = b.book_id
    		WHERE
    			a.book_id = #{bookId}
    		AND a.student_id = #{studentId}
    	</select>
    </mapper>
    

    mapper总结namespace是该xml对应的接口全名,selectupdate中的id对应方法名,resultType是返回值类型,parameterType是参数类型(这个其实可选),最后#{...}中填写的是方法的参数,看懂了是不是很简单!!我也这么觉得~ 还有一个小技巧要交给大家,就是在返回Appointment对象包含了一个属性名为book的Book对象,那么可以使用"book.属性名"的方式来取值,看上面queryByKeyWithBook方法的sql。


    dao层写完了,接下来test对应的package写我们测试方法吧。
    因为我们之后会写很多测试方法,在测试前需要让程序读入spring-dao和mybatis等配置文件,所以我这里就抽离出来一个BaseTest类,只要是测试方法就继承它,这样那些繁琐的重复的代码就不用写那么多了~

    BaseTest.java

    package com.soecode.lyf;
    
    import org.junit.runner.RunWith;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    /**
     * 配置spring和junit整合,junit启动时加载springIOC容器 spring-test,junit
     */
    @RunWith(SpringJUnit4ClassRunner.class)
    // 告诉junit spring配置文件
    @ContextConfiguration({ "classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml" })
    public class BaseTest {
    
    }
    
    

    因为spring-serviceservice层的测试中会时候到,这里也一起引入算了!

    新建BookDaoTest.javaAppointmentDaoTest.java两个dao测试文件。

    BookDaoTest.java

    package com.soecode.lyf.dao;
    
    import java.util.List;
    
    import org.junit.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    
    import com.soecode.lyf.BaseTest;
    import com.soecode.lyf.entity.Book;
    
    public class BookDaoTest extends BaseTest {
    
    	@Autowired
    	private BookDao bookDao;
    
    	@Test
    	public void testQueryById() throws Exception {
    		long bookId = 1000;
    		Book book = bookDao.queryById(bookId);
    		System.out.println(book);
    	}
    
    	@Test
    	public void testQueryAll() throws Exception {
    		List<Book> books = bookDao.queryAll(0, 4);
    		for (Book book : books) {
    			System.out.println(book);
    		}
    	}
    
    	@Test
    	public void testReduceNumber() throws Exception {
    		long bookId = 1000;
    		int update = bookDao.reduceNumber(bookId);
    		System.out.println("update=" + update);
    	}
    
    }
    

    BookDaoTest测试结果

    testQueryById
    testQueryById

    testQueryAll
    testQueryAll

    testReduceNumber
    testReduceNumber

    AppointmentDaoTest.java

    package com.soecode.lyf.dao;
    
    import org.junit.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    
    import com.soecode.lyf.BaseTest;
    import com.soecode.lyf.entity.Appointment;
    
    public class AppointmentDaoTest extends BaseTest {
    
    	@Autowired
    	private AppointmentDao appointmentDao;
    
    	@Test
    	public void testInsertAppointment() throws Exception {
    		long bookId = 1000;
    		long studentId = 12345678910L;
    		int insert = appointmentDao.insertAppointment(bookId, studentId);
    		System.out.println("insert=" + insert);
    	}
    
    	@Test
    	public void testQueryByKeyWithBook() throws Exception {
    		long bookId = 1000;
    		long studentId = 12345678910L;
    		Appointment appointment = appointmentDao.queryByKeyWithBook(bookId, studentId);
    		System.out.println(appointment);
    		System.out.println(appointment.getBook());
    	}
    
    }
    

    AppointmentDaoTest测试结果

    testInsertAppointment
    testInsertAppointment

    testQueryByKeyWithBook
    testQueryByKeyWithBook


    嗯,到这里一切到很顺利那么我们继续service层的编码吧可能下面开始信息里比较大,大家要做好心理准备~

    首先,在写我们的控制器之前,我们先定义几个预约图书操作返回码的数据字典,也就是我们要返回给客户端的信息。我们这类使用枚举类,没听过的小伙伴要好好恶补一下了(我也是最近才学到的= =)

    预约业务操作返回码说明

    返回码说明
    1预约成功
    0库存不足
    -1重复预约
    -2系统异常

    新建一个包叫enums,在里面新建一个枚举类AppointStateEnum.java,用来定义预约业务的数据字典,没听懂没关系,我们直接看代码吧~是不是感觉有模有样了!

    AppointStateEnum.java

    package com.soecode.lyf.enums;
    
    /**
     * 使用枚举表述常量数据字典
     */
    public enum AppointStateEnum {
    
    	SUCCESS(1, "预约成功"), NO_NUMBER(0, "库存不足"), REPEAT_APPOINT(-1, "重复预约"), INNER_ERROR(-2, "系统异常");
    
    	private int state;
    
    	private String stateInfo;
    
    	private AppointStateEnum(int state, String stateInfo) {
    		this.state = state;
    		this.stateInfo = stateInfo;
    	}
    
    	public int getState() {
    		return state;
    	}
    
    	public String getStateInfo() {
    		return stateInfo;
    	}
    
    	public static AppointStateEnum stateOf(int index) {
    		for (AppointStateEnum state : values()) {
    			if (state.getState() == index) {
    				return state;
    			}
    		}
    		return null;
    	}
    
    }
    

    接下来,在dto包下新建AppointExecution.java用来存储我们执行预约操作的返回结果。

    AppointExecution.java

    package com.soecode.lyf.dto;
    
    import com.soecode.lyf.entity.Appointment;
    import com.soecode.lyf.enums.AppointStateEnum;
    
    /**
     * 封装预约执行后结果
     */
    public class AppointExecution {
    
    	// 图书ID
    	private long bookId;
    
    	// 秒杀预约结果状态
    	private int state;
    
    	// 状态标识
    	private String stateInfo;
    
    	// 预约成功对象
    	private Appointment appointment;
    
    	public AppointExecution() {
    	}
    
    	// 预约失败的构造器
    	public AppointExecution(long bookId, AppointStateEnum stateEnum) {
    		this.bookId = bookId;
    		this.state = stateEnum.getState();
    		this.stateInfo = stateEnum.getStateInfo();
    	}
    
    	// 预约成功的构造器
    	public AppointExecution(long bookId, AppointStateEnum stateEnum, Appointment appointment) {
    		this.bookId = bookId;
    		this.state = stateEnum.getState();
    		this.stateInfo = stateEnum.getStateInfo();
    		this.appointment = appointment;
    	}
    	
    	// 省略getter和setter方法,toString方法
    
    }
    
    

    接着,在exception包下新建三个文件
    NoNumberException.java
    RepeatAppointException.java
    AppointException.java
    预约业务异常类(都需要继承RuntimeException),分别是无库存异常、重复预约异常、预约未知错误异常,用于业务层非成功情况下的返回(即成功返回结果,失败抛出异常)。

    NoNumberException.java

    package com.soecode.lyf.exception;
    
    /**
     * 库存不足异常
     */
    public class NoNumberException extends RuntimeException {
    
    	public NoNumberException(String message) {
    		super(message);
    	}
    
    	public NoNumberException(String message, Throwable cause) {
    		super(message, cause);
    	}
    
    }
    
    

    RepeatAppointException.java

    package com.soecode.lyf.exception;
    
    /**
     * 重复预约异常
     */
    public class RepeatAppointException extends RuntimeException {
    
    	public RepeatAppointException(String message) {
    		super(message);
    	}
    
    	public RepeatAppointException(String message, Throwable cause) {
    		super(message, cause);
    	}
    
    }
    
    

    AppointException.java

    package com.soecode.lyf.exception;
    
    /**
     * 预约业务异常
     */
    public class AppointException extends RuntimeException {
    
    	public AppointException(String message) {
    		super(message);
    	}
    
    	public AppointException(String message, Throwable cause) {
    		super(message, cause);
    	}
    
    }
    
    

    咱们终于可以编写业务代码了,在service包下新建BookService.java图书业务接口。

    BookService.java

    package com.soecode.lyf.service;
    
    import java.util.List;
    
    import com.soecode.lyf.dto.AppointExecution;
    import com.soecode.lyf.entity.Book;
    
    /**
     * 业务接口:站在"使用者"角度设计接口 三个方面:方法定义粒度,参数,返回类型(return 类型/异常)
     */
    public interface BookService {
    
    	/**
    	 * 查询一本图书
    	 * 
    	 * @param bookId
    	 * @return
    	 */
    	Book getById(long bookId);
    
    	/**
    	 * 查询所有图书
    	 * 
    	 * @return
    	 */
    	List<Book> getList();
    
    	/**
    	 * 预约图书
    	 * 
    	 * @param bookId
    	 * @param studentId
    	 * @return
    	 */
    	AppointExecution appoint(long bookId, long studentId);
    
    }
    

    service.impl包下新建BookServiceImpl.java使用BookService接口,并实现里面的方法。

    BookServiceImpl

    package com.soecode.lyf.service.impl;
    
    import java.util.List;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import com.soecode.lyf.dao.AppointmentDao;
    import com.soecode.lyf.dao.BookDao;
    import com.soecode.lyf.dto.AppointExecution;
    import com.soecode.lyf.entity.Appointment;
    import com.soecode.lyf.entity.Book;
    import com.soecode.lyf.enums.AppointStateEnum;
    import com.soecode.lyf.exception.AppointException;
    import com.soecode.lyf.exception.NoNumberException;
    import com.soecode.lyf.exception.RepeatAppointException;
    import com.soecode.lyf.service.BookService;
    
    @Service
    public class BookServiceImpl implements BookService {
    
    	private Logger logger = LoggerFactory.getLogger(this.getClass());
    
    	// 注入Service依赖
    	@Autowired
    	private BookDao bookDao;
    
    	@Autowired
    	private AppointmentDao appointmentDao;
    
    
    	@Override
    	public Book getById(long bookId) {
    		return bookDao.queryById(bookId);
    	}
    
    	@Override
    	public List<Book> getList() {
    		return bookDao.queryAll(0, 1000);
    	}
    
    	@Override
    	@Transactional
    	/**
    	 * 使用注解控制事务方法的优点: 1.开发团队达成一致约定,明确标注事务方法的编程风格
    	 * 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作,RPC/HTTP请求或者剥离到事务方法外部
    	 * 3.不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制
    	 */
    	public AppointExecution appoint(long bookId, long studentId) {
    		try {
    			// 减库存
    			int update = bookDao.reduceNumber(bookId);
    			if (update <= 0) {// 库存不足
    				//return new AppointExecution(bookId, AppointStateEnum.NO_NUMBER);//错误写法				
    				throw new NoNumberException("no number");
    			} else {
    				// 执行预约操作
    				int insert = appointmentDao.insertAppointment(bookId, studentId);
    				if (insert <= 0) {// 重复预约
    					//return new AppointExecution(bookId, AppointStateEnum.REPEAT_APPOINT);//错误写法
    					throw new RepeatAppointException("repeat appoint");
    				} else {// 预约成功
    					Appointment appointment = appointmentDao.queryByKeyWithBook(bookId, studentId);
    					return new AppointExecution(bookId, AppointStateEnum.SUCCESS, appointment);
    				}
    			}
    		// 要先于catch Exception异常前先catch住再抛出,不然自定义的异常也会被转换为AppointException,导致控制层无法具体识别是哪个异常
    		} catch (NoNumberException e1) {
    			throw e1;
    		} catch (RepeatAppointException e2) {
    			throw e2;
    		} catch (Exception e) {
    			logger.error(e.getMessage(), e);
    			// 所有编译期异常转换为运行期异常
    			//return new AppointExecution(bookId, AppointStateEnum.INNER_ERROR);//错误写法
    			throw new AppointException("appoint inner error:" + e.getMessage());
    		}
    	}
    
    }
    
    

    下面我们来测试一下我们的业务代码吧~因为查询图书的业务不复杂,所以这里只演示我们最重要的预约图书业务!!

    BookServiceImplTest.java

    package com.soecode.lyf.service.impl;
    
    import static org.junit.Assert.fail;
    
    import org.junit.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    
    import com.soecode.lyf.BaseTest;
    import com.soecode.lyf.dto.AppointExecution;
    import com.soecode.lyf.service.BookService;
    
    public class BookServiceImplTest extends BaseTest {
    
    	@Autowired
    	private BookService bookService;
    
    	@Test
    	public void testAppoint() throws Exception {
    		long bookId = 1001;
    		long studentId = 12345678910L;
    		AppointExecution execution = bookService.appoint(bookId, studentId);
    		System.out.println(execution);
    	}
    
    }
    
    

    BookServiceImplTest测试结果

    testAppointtestAppoint

    首次执行是“预约成功”,如果再次执行的话,应该会出现“重复预约”,哈哈,我们所有的后台代码都通过单元测试啦~~是不是很开心~


    咱们还需要在dto包里新建一个封装json返回结果的类Result.java,设计成泛型。

    Result.java

    package com.soecode.lyf.dto;
    
    /**
     * 封装json对象,所有返回结果都使用它
     */
    public class Result<T> {
    
    	private boolean success;// 是否成功标志
    
    	private T data;// 成功时返回的数据
    
    	private String error;// 错误信息
    
    	public Result() {
    	}
    
    	// 成功时的构造器
    	public Result(boolean success, T data) {
    		this.success = success;
    		this.data = data;
    	}
    
    	// 错误时的构造器
    	public Result(boolean success, String error) {
    		this.success = success;
    		this.error = error;
    	}
    
    	// 省略getter和setter方法
    }
    
    

    最后,我们写web层,也就是controller,我们在web包下新建BookController.java文件。

    BookController.java

    package com.soecode.lyf.web;
    
    import java.util.List;
    
    import org.apache.ibatis.annotations.Param;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import com.soecode.lyf.dto.AppointExecution;
    import com.soecode.lyf.dto.Result;
    import com.soecode.lyf.entity.Book;
    import com.soecode.lyf.enums.AppointStateEnum;
    import com.soecode.lyf.exception.NoNumberException;
    import com.soecode.lyf.exception.RepeatAppointException;
    import com.soecode.lyf.service.BookService;
    
    @Controller
    @RequestMapping("/book") // url:/模块/资源/{id}/细分 /seckill/list
    public class BookController {
    
    	private Logger logger = LoggerFactory.getLogger(this.getClass());
    
    	@Autowired
    	private BookService bookService;
    
    	@RequestMapping(value = "/list", method = RequestMethod.GET)
    	private String list(Model model) {
    		List<Book> list = bookService.getList();
    		model.addAttribute("list", list);
    		// list.jsp + model = ModelAndView
    		return "list";// WEB-INF/jsp/"list".jsp
    	}
    
    	@RequestMapping(value = "/{bookId}/detail", method = RequestMethod.GET)
    	private String detail(@PathVariable("bookId") Long bookId, Model model) {
    		if (bookId == null) {
    			return "redirect:/book/list";
    		}
    		Book book = bookService.getById(bookId);
    		if (book == null) {
    			return "forward:/book/list";
    		}
    		model.addAttribute("book", book);
    		return "detail";
    	}
    
    	//ajax json
    	@RequestMapping(value = "/{bookId}/appoint", method = RequestMethod.POST, produces = {
    			"application/json; charset=utf-8" })
    	@ResponseBody
    	private Result<AppointExecution> appoint(@PathVariable("bookId") Long bookId, @RequestParam("studentId") Long studentId) {
    		if (studentId == null || studentId.equals("")) {
    			return new Result<>(false, "学号不能为空");
    		}
    		//AppointExecution execution = bookService.appoint(bookId, studentId);//错误写法,不能统一返回,要处理异常(失败)情况
    		AppointExecution execution = null;
    		try {
    			execution = bookService.appoint(bookId, studentId);
    		} catch (NoNumberException e1) {
    			execution = new AppointExecution(bookId, AppointStateEnum.NO_NUMBER);
    		} catch (RepeatAppointException e2) {
    			execution = new AppointExecution(bookId, AppointStateEnum.REPEAT_APPOINT);
    		} catch (Exception e) {
    			execution = new AppointExecution(bookId, AppointStateEnum.INNER_ERROR);
    		}
    		return new Result<AppointExecution>(true, execution);
    	}
    
    }
    
    

    因为我比较懒,所以我们就不测试controller了,好讨厌写前端,呜呜呜~

    到此,我们的SSM框架整合配置,与应用实例部分已经结束了,我把所有源码和jar包一起打包放在了我的GitHub上,需要的可以去下载,喜欢就给个star吧,这篇东西写了两个晚上也不容易啊。

    完整代码下载地址:https://github.com/liyifeng1994/ssm


    2017-02-28更新(感谢网友EchoXml发现):
    修改预约业务代码,失败时抛异常,成功时才返回结果,控制层根据捕获的异常返回相应信息给客户端,而不是业务层直接返回错误结果。上面的代码已经作了修改,而且错误示范也注释保留着,之前误人子弟了,还好有位网友前几天提出质疑,我也及时做了修改。

    2017-03-30更新(感谢网友ergeerge1建议):
    修改BookController几处错误
    1.detail方法不是返回json的,故不用加@ResponseBody注解
    2.appoint方法应该加上@ResponseBody注解
    3.另外studentId参数注解应该是@RequestParam
    4.至于controller测试,测试appoint方法可不必写jsp,用curl就行,比如
    curl -H “Accept: application/json; charset=utf-8” -d “studentId=1234567890” localhost:8080/book/1003/appoint

    展开全文
  • JAVA上百实例源码以及开源项目

    千次下载 热门讨论 2016-01-03 17:37:40
     util实现Java图片水印添加功能,有添加图片水印和文字水印,可以设置水印位置,透明度、设置对线段锯齿状边缘处理、水印图片的路径,水印一般格式是gif,png,这种图片可以设置透明度、水印旋转等,可以参考代码...
  • 冯诺依曼结构概述

    千次阅读 多人点赞 2019-10-10 17:08:40
    程序指令存储地址和数据存储地址指向同一个存储器的不同物理位置,因此程序指令和数据的宽度相同,数学家冯·诺依曼提出了计算机制造的三个基本原则,即采用二进制逻辑、程序存储执行以及计算机...
  • 史上最管用的C盘深度清理秘籍

    万次阅读 多人点赞 2019-03-09 15:37:43
    像休眠文件、系统页面文件这都是动辄GB级的大文件,还有系统还原文件、虚拟内存、安装软件时的临时解压文件、系统更新和游戏更新补丁存放位置都在C盘上。 别看刚刚安装完的Windows才十几个GB,...
  • 《单片机原理及应用》复习提纲

    万次阅读 多人点赞 2015-12-14 10:42:02
    在定点表示法中,小数点的位置是固定不变的,它是事先约   定好的,不必用符号表示。通常,将小数点固定在数值部分的最高位之前或最低值之后, 前者将数表示为纯小数,后者将数表示为纯整数 。   3...
  • 我们在SpringBoot项目整合Mybatis时或多或少遇到过这个问题,怎么他...带着这些疑问,我们来了解下不同位置下XXXMapper.xml文件到底该怎么处理。 1 SpringBoot整合Mybatis 1.1 pom依赖 <!-- mybatis依赖 --> &...
  • 接上文 计算机图形学 学习笔记(一):概述,光栅图形学算法:直线扫描算法(DDA,中点画线算法,Bresenham算法)光栅图形学算法2.4 多边形扫描转换-X扫描线算法多边形的扫描转换和区域填充这个问题是怎么样在离散的...
  • Kali扫描工具Nmap

    千次阅读 2019-06-19 21:38:25
    本片介绍的是Namp的Linux Kali操作系统下的使用,Nmap在Windows操作...一、工具介绍 功能概述:Nmap是主动扫描工具,用于对指定的主机进行扫描 历史背景:Nmap是由Gordon Lyon设计并实现的,与1997开始发布,最...
  • 【原理详解】X-扫描线算法

    千次阅读 2020-04-13 17:03:39
    基本思想:按照扫描线顺序,计算扫描线与多边形的相交区间,再用要求的颜色显示这些区间里的像素
  • SpringBoot包扫描机制

    2018-09-10 06:27:00
    我将应用分成了parent+core+server这种模式,parent是一个单纯的pom文件,存放项目的一些公共依赖;core则是一个没有启动类的SpringBoot项目,存放项目的核心代码,例如model、filter、listener及配置等;server则是...
  • 1.3扫描线种子填充算法  1.1和1.2节介绍的两种种子填充算法的优点是非常简单,缺点是使用了递归算法,这不但需要大量栈空间来存储相邻的点,而且效率不高。为了减少算法中的递归调用,节省栈空间的使用,人们提出了...
  • :SpringBoot把类路径下的/static,/public,/resources和META-INF/resources文件下的静态文件映射为了 “/”,可以直接访问,我们也可以根据下面的方法自定义静态文件的存放位置。 三、自定义静态文件存放 位置,...
  • 第五章 自动化扫描 作者:Gilberto Najera-Gutierrez 译者:飞龙 协议:CC BY-NC-SA 4.0 简介几乎每个渗透测试项目都需要遵循严格的日程,多数由客户的需求或开发交谈日期决定。对于渗透测试者,拥有一种工具...
  • 开启JPA存储库扫描 1.创建JPA配置类 /** * @author Created by 谭健 on 2018/4/4 0004. Wednesday. 9:55. * © All Rights Reserved. */ @EnableJpaRepositories( // basePackages 支持多包扫描,用...
  • activex上传图片并预览 页面HTML代码 显示扫描设置 连续扫描 页面js调用Activex var tScaner = new ActiveXObject("TScan.Scaner"); function selscan() {
  • 1 背景介绍 在上一篇博文 LLVM 里的寄存器分配 - 准备工作(一...虽然我学习的第一个寄存器分配算法是图着色算法,但由于目前的 LLVM 版本里使用的寄存器分配器均是线性扫描算法的变种(事实上 LLVM3.0 版本以前的寄存
  • CE指针扫描

    千次阅读 2018-08-26 09:37:40
    指针扫描出现结果为0解决办法: 1,CT表存放路径不能有中文,不是安装路径,因为很多CE版本是免安装版依然会出现这个问题 2,要点击打开进程而不是调试器附加到进程 常见问题: 1,最大允许的偏移值是十进制,CE...
  • Nmap命令扫描详解

    千次阅读 2018-10-29 15:37:50
    设置这个选项,让nmap使用真正的ping(ICMP echo请求)来扫描目标主机是否正在运行。使用这个选项让nmap发现正在运行的主机的同时,nmap也会对你的直接子网广播地址进行观察。直接子网广播地址一些外部可达的IP地址,...
  • 扫描线算法是扫描转换多边形的常用算法,它充分利用了相邻像素之间的连贯性,避免了逐点判断和反复求交计算,达到了减少计算量和提高算法效率的目的。 处理对象:非自交多边形 (边与边之间除了顶点外无其它交点)...
  • 前段时间自己在研究行人检测方面的东西,用的是OpenCV,在网上查询相关资料的时候看到一些文章介绍如何使用OpenCV实现类似“全能扫描王”的功能。最近正好有空闲时间,于是花了几天时间研究了下,现在记录下在研究...
  • 常用操作系统扫描工具介绍(转)

    千次阅读 2020-09-23 12:09:23
    常用操作系统扫描工具介绍 1 常用操作系统扫描工具介绍 1.1 CIS-CAT 【功能】 可以根据不同的操作系统,选择不同的基准进行系统漏洞扫描。 【适用对象】 Unix/Linux,MS Windows,并且这些系统上装了java 5或...
  • 原理:调用媒体摄像头,应用的是navigator.mediaDevices.getUserMedia功能(API网站)调用摄像头拍摄视频流,将录像流放到video中,再绘制成canvas图像,将此刻的canvas图像生成base64位图片返回并存放到image中...
  • //设置窗口的顶部和左边位置 glutInitWindowSize( 400 , 300 ); //设置窗口的高度和宽度 glutCreateWindow( "An Example OpenGL Program" ); //创建显示窗口 Init(); //调用初始化过程 ...
  • U盘安装Win10系统教程

    千次阅读 2018-09-21 22:09:52
    U盘安装Win10系统教程前言电脑配置安装步骤Step1 将Win10安装包写入U盘Step2 设置BIOS,引导从U盘启动,重新分区 前言 写这篇文章的主要目的纯粹是为了防止自己忘了用命令行进行系统分区,方便日后用到能够快速的...
  • 我们来先来看下该阶段的整体流程概括: // 【 PackageManagerService.java 】 //设置扫描的参数 final int scanFlags = SCAN_NO_PATHS | SCAN_DEFER_DEX | SCAN_BOOTING | SCAN_INITIAL; ... //开始处理非系统应用...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 113,706
精华内容 45,482
关键字:

如何设置扫描存放位置