8、java常用知识面试题

常用知识

一、linux系列

1.linux常用命令

ifconfig:查看网络接口详情

ping:查看与某主机是否能联通

ps -ef|grep 进程名称:查看进程号

lost -i 端口 :查看端口占用情况

top:查看系统负载情况,包括系统时间、系统所有进程状态、cpu情况

free:查看内存占用情况

kill:正常杀死进程,发出的信号可能会被阻塞

kill -9:强制杀死进程,发送的是exit命令,不会被阻塞

2.linux的io模型

IO是对磁盘或网络数据的读写,用户进程读取一次IO请求分为两个阶段:等待数据到达内核缓冲区和将内核缓冲区数据拷贝到用户空间,当用户去内核中拷贝数据时,要从用户态转为核心态

5中io模型:

(1)同步阻塞IO模型

用户进程发起io调用后会被阻塞,等待内核缓冲区数据准备完毕时就被唤醒,将内核数据复制到用户进程。这两个阶段都是阻塞的

(2)同步非阻塞IO模型

用户进程发起IO调用后,若内核缓冲区数据还未准备好,进程会继续干别的事,每隔一段时间就去看看内核数据是否准备好。不过将内核数据复制到用户进程这个阶段依旧是阻塞的

(3)IO多路复用模型

linux中把一切都看成文件,每个文件都有一个文件描述符(FD)来关联, IO多路复用模型就是复用单个进程同时监测多个文件描述符,当某个文件描述符可读或可写,就去通知用户进程。

(4)信号IO模型

用户进程发起IO调用后,会向内核注册一个信号处理函数然后继续干别的事,当内核数据准备就绪时就通知用户进程来进行拷贝。

(5)异步非阻塞模型

前面四种全是同步的。进程在发起IO调用后,会直接返回结果。待内核数据准备好时,由内核将数据复制给用户进程。两个阶段都是非阻塞的

3、IO多路复用详解

linux中把一切都看成文件,每个文件都有一个文件描述符(FD)来关联, IO多路复用模型就是复用单个进程同时监测多个文件描述符,当某个文件描述符可读或可写,就去通知用户进程。IO多路复用有三种方式

(1)select:采用数组结构,监测的fd有限,默认为1024;当有文件描述符就绪时,需要遍历整个FD数组来查看是哪个文件描述符就绪了,效率较低;每次调用select时都需要把整个文件描述符数组从用户态拷贝到内核态中来回拷贝,当fd很多时开销会很大;

(2)poll:采用链表结构,监测的文件描述符没有上限,其它的根select差不多

(3)epoll:采用红黑树结构,监测的fd没有上限,它有三个方法,epoll_create() 用于创建一个epoll实例,epoll实例中有一颗红黑树记录监测的fd,一个链表记录就绪的fd;epoll_ctl() 用于往epoll实例中增删要监测的文件描述符,并设置回调函数,当文件描述符就绪时触发回调函数将文件描述符添加到就绪链表当中;epoll_wait() 用于见擦汗就绪列表并返回就绪列表的长度,然后将就绪列表的拷贝到用户空间缓冲区中。

所以epoll的优点是当有文件描述符就绪时,只把已就绪的文件描述符写给用户空间,不需要每次都遍历FD集合;每个FD只有在调新增的时候和就绪的时候才会在用户空间和内核空间之间拷贝一次。

4、epoll的LT和ET模式

LT(默认):水平触发,当FD有数据可读的时候,那么每次 epoll_wait都会去通知用户来操作直到读完

ET:边缘触发,当FD有数据可读的时候,它只会通知用户一次,直到下次再有数据流入才会再通知,所以在ET模式下一定要把缓冲区的数据一次读完

二、场景题

Java如何实现统计在线人数的功能?

分析:

首先,我遇见问题喜欢先分析下思路。

  • 用什么技术,可以监听用户访问服务器? (监听器)
  • 用那些技术,可以实时的存储每次登陆服务器的用户数量? (java四大域对象)
  • 用那些技术,可以让用户的数量显示到客户端页面? (el表达式)
package com.cyl.count;
 
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
/*
    初始化:
         只有服务器的启动,才会创建servletContext对象。
        用于监听servletContext创建,一旦创建servletContext创建,则设置servletContext中的count值为0;
*/
@WebListener
/*
    这个注解的作用是启动监听,相当于在web.xml配置(
    <listener>
        <listener-class>com.cyl.count.InitServletContexListener</listener-class>
    </listener>
*/
public class InitServletContexListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        //获取ServletContext域对象
        ServletContext servletContext = servletContextEvent.getServletContext();
        //给ServletContext域对象,设置count=0
        servletContext.setAttribute("count",0);
    }
 
    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
 
    }
}
package com.cyl.count;
 
import javax.servlet.ServletContext;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
 
/**
 * @监听在线人数,监听session的创建和销毁
 *     如果session创建 获取ServletContext中的count++,重新设置
 *     如果session销毁 获取ServletContext中的count--,重新设置
 */
@WebListener
public class OnlineNumberHttpSessionListener implements HttpSessionListener {
    @Override
    public void sessionCreated(HttpSessionEvent httpSessionEvent) {
        //1.获取session
        HttpSession session = httpSessionEvent.getSession();
        ServletContext servletContext = session.getServletContext();
        //2.获取counnt值,加1
        int count = (int) servletContext.getAttribute("count");
        count++;
        //3.把servlet存储到servletContext对象中
        servletContext.setAttribute("count",count);
    }
 
    @Override
    public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
 
        //1.获取session
        HttpSession session = httpSessionEvent.getSession();
        ServletContext servletContext = session.getServletContext();
        //2.获取counnt值,减1
        int count = (int) servletContext.getAttribute("count");
        count--;
        //3.把servlet存储到servletContext对象中
        servletContext.setAttribute("count",count);
    }
}

三、Java IO概念

1、Java IO概念(阻塞与非阻塞、同步与异步、BIO、NIO、AIO剖析)

1、阻塞和非阻塞、同步和异步

阻塞和非阻塞是进程在访问数据的时候,数据是否准备就绪的一种处理方式,当数据没有准备的时候。

  • 阻塞:往往需要等待缓冲区中的数据准备好过后才处理其他的事情,否则一直等待在那里。

  • 非阻塞:当我们的进程访问我们的数据缓冲区的时候,如果数据没有准备好则直接返回,不会等待。如果数据已经准备好,也直接返回。

2、同步(Synchronization)和异步(Asynchronous)

同步和异步都是基于应用程序和操作系统处理 IO 事件所采用的方式。比如同步:是应用程序要直接参与 IO 读写的操作。异步:所有的 IO 读写交给操作系统去处理,应用程序只需要等待通知。

同步方式在处理 IO 事件的时候,必须阻塞在某个方法上面等待我们的 IO 事件完成(阻塞 IO 事件或者通过轮询 IO事件的方式),对于异步来说,所有的 IO 读写都交给了操作系统。这个时候,我们可以去做其他的事情,并不需要去完成真正的 IO 操作,当操作完成 IO 后,会给我们的应用程序一个通知。

同步 : 阻塞到 IO 事件,阻塞到 read 或则 write。这个时候我们就完全不能做自己的事情。让读写方法加入到线程里面,然后阻塞线程来实现,对线程的性能开销比较大。

2、BIO与 NIO 对比

Java BIO(Block IO)和 NIO(Non-Block IO)之间的主要差别异:

1、面向流与面向缓冲

Java NIO 和 BIO 之间第一个最大的区别是,BIO 是面向流的,NIO 是面向缓冲区的。 Java BIO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO 的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有你需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

2、阻塞与非阻塞

Java BIO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO 的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞, 所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞 IO 的空闲时间用于在其它通道上执行 IO 操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

3、选择器

Java NIO 的选择器(Selector)允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。

4、NIO 和BIO 如何影响应用程序的设计

无论选择 BIO 或 NIO 工具箱,可能会影响应用程序设计的以下几个方面:

  • 对 NIO 或 BIO 类的 API 调用。
  • 数据处理逻辑。
  • 用来处理数据的线程数。
1、API 调用
	当然,使用 NIO 的 API 调用时看起来与使用 BIO 时有所不同,但这并不意外,因为并不是仅从一个 InputStream 逐字节读取,而是数据必须先读入缓冲区再处理。
2、数据处理
	使用纯粹的 NIO 设计相较 BIO 设计,数据处理也受到影响。

在 BIO 设计中,我们从 InputStream 或 Reader 逐字节读取数据。假设你正在处理一基于行的文本数据流,例如: 有如下一段文本:

Name:Ckw
Age:18
Email: ckw@qq.com
Phone:13888888888

该文本行的流可以这样处理:

FileInputStream input = new FileInputStream("d://info.txt"); 
BufferedReader reader = new BufferedReader(new InputStreamReader(input)); 	
String nameLine = reader.readLine();
String ageLine = reader.readLine();
String emailLine = reader.readLine();
String phoneLine = reader.readLine();

处理状态由程序执行多久决定。换句话说,一旦 reader.readLine()方法返回,你就知道肯定文本行就已读完, readline()阻塞直到整行读完,这就是原因。你也知道此行包含名称;同样,第二个 readline()调用返回的时候,你知道这行包含年龄等。 正如你可以看到,该处理程序仅在有新数据读入时运行,并知道每步的数据是什么。一旦正在运行的线程已处理过读入的某些数据,该线程不会再回退数据(大多如此)。下图也说明了这条原则:

(Java BIO: 从一个阻塞的流中读数据) 而一个 NIO 的实现会有所不同,下面是一个简单的例子

ByteBuffer buffer = ByteBuffer.allocate(48); 
int bytesRead = inChannel.read(buffer);

注意第二行,从通道读取字节到 ByteBuffer。当这个方法调用返回时,你不知道你所需的所有数据是否在缓冲区内。你所知道的是,该缓冲区包含一些字节,这使得处理有点困难。

所以,你怎么知道是否该缓冲区包含足够的数据可以处理呢?你不知道。发现的方法只能查看缓冲区中的数据。其结果是,在你知道所有数据都在缓冲区里之前,你必须检查几次缓冲区的数据。这不仅效率低下,而且可以使程序设计方案杂乱不堪。例如:

ByteBuffer buffer = ByteBuffer.allocate(48); 
int bytesRead = inChannel.read(buffer); 
while(!bufferFull(bytesRead)) {
	bytesRead = inChannel.read(buffer);
}

bufferFull()方法必须跟踪有多少数据读入缓冲区,并返回真或假,这取决于缓冲区是否已满。换句话说,如果缓冲区准备好被处理,那么表示缓冲区满了。

如果缓冲区已满,它可以被处理。如果它不满,并且在你的实际案例中有意义,你或许能处理其中的部分数据。但是许多情况下并非如此。下图展示了“缓冲区数据循环就绪”:

3、设置处理线程数

NIO 可只使用一个(或几个)单线程管理多个通道(网络连接或文件),但付出的代价是解析数据可能会比从一个阻塞流中读取数据更复杂。
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现 NIO 的服务器可能是一个优势。同样,如果你需要维持许多打开的连接到其他计算机上,如 P2P 网络中,使用一个单独的线程来管理你所有出站连接,可能是一个优势。一个线程多个连接的设计方案如:

Java NIO: 单线程管理多个连接

如果你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的 IO 服务器实现可能非常契合。下图说明了一个典型的 IO 服务器设计

Java BIO: 一个典型的 IO 服务器设计- 一个连接通过一个线程处理

3、Java AIO 详解

jdk1.7 (NIO2)才是实现真正的异步 AIO、把 IO 读写操作完全交给操作系统,学习了 linux epoll 模式

1、AIO(Asynchronous IO)基本原理

服务端:AsynchronousServerSocketChannel 客服端:AsynchronousSocketChannel
用户处理器:CompletionHandler 接口,这个接口实现应用程序向操作系统发起 IO 请求,当完成后处理具体逻辑,否则做自己该做的事情,

“真正”的异步IO需要操作系统更强的支持。在IO多路复用模型中,事件循环将文件句柄的状态事件通知给用户线程, 由用户线程自行读取数据、处理数据。而在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户线程指定的缓冲区内,内核在IO完成后通知用户线程直接使用即可。异步IO模型使用了Proactor设计模式实现了这一机制,如下图所示:

2、AIO 代码实现

服务端代码:

/**
 * AIO服务端
 */
public class AIOServer {

    private final int port;

    public static void main(String args[]) {
        int port = 8000;
        new AIOServer(port);
    }

    public AIOServer(int port) {
        this.port = port;
        listen();
    }

    private void listen() {
        try {
            ExecutorService executorService = Executors.newCachedThreadPool();
            AsynchronousChannelGroup threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
            //工作线程,用来侦听回调的,事件响应的时候需要回调
            final AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open(threadGroup);
            server.bind(new InetSocketAddress(port));
            System.out.println("服务已启动,监听端口" + port);

            //准备接受数据
            server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>(){
                final ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
                //实现completed方法来回调
                //由操作系统来触发
                //回调有两个状态,成功
                public void completed(AsynchronousSocketChannel result, Object attachment){
                    System.out.println("IO操作成功,开始获取数据");
                    try {
                        buffer.clear();
                        result.read(buffer).get();
                        buffer.flip();
                        result.write(buffer);
                        buffer.flip();
                    } catch (Exception e) {
                        System.out.println(e.toString());
                    } finally {
                        try {
                            result.close();
                            server.accept(null, this);
                        } catch (Exception e) {
                            System.out.println(e.toString());
                        }
                    }

                    System.out.println("操作完成");
                }

                @Override
                //回调有两个状态,失败
                public void failed(Throwable exc, Object attachment) {
                    System.out.println("IO操作是失败: " + exc);
                }
            });

            try {
                Thread.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException ex) {
                System.out.println(ex);
            }
        } catch (IOException e) {
            System.out.println(e);
        }
    }
}

客户端代码:

/**
 * AIO客户端
 */
public class AIOClient {
    private final AsynchronousSocketChannel client;

    public AIOClient() throws Exception{
        client = AsynchronousSocketChannel.open();
    }

    public void connect(String host,int port)throws Exception{
        client.connect(new InetSocketAddress(host,port),null,new CompletionHandler<Void,Void>() {
            @Override
            public void completed(Void result, Void attachment) {
                try {
                    client.write(ByteBuffer.wrap("这是一条测试数据".getBytes())).get();
                    System.out.println("已发送至服务器");
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }

            @Override
            public void failed(Throwable exc, Void attachment) {
                exc.printStackTrace();
            }
        });
        final ByteBuffer bb = ByteBuffer.allocate(1024);
        client.read(bb, null, new CompletionHandler<Integer,Object>(){

                    @Override
                    public void completed(Integer result, Object attachment) {
                        System.out.println("IO操作完成" + result);
                        System.out.println("获取反馈结果" + new String(bb.array()));
                    }

                    @Override
                    public void failed(Throwable exc, Object attachment) {
                        exc.printStackTrace();
                    }
                }
        );

        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException ex) {
            System.out.println(ex);
        }
    }

4、各 IO 模型对比与总结

四、分布式事物

分布式事务是指事务的参与者,支持事务的服务器,资源服务器以及事务管理器分别位于分布式系统的不同节点之上。通常一个分布式事务中会涉及对多个数据源或业务系统的操作。分布式事务也可以被定义为一种嵌套型的事务,同时也就具有了ACID事务的特性。

强一致性

任何一次读都能读到某个数据的最近一次写的数据。系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。简言之,在任意时刻,所有节点中的数据是一样的。

弱一致性

数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。

最终一致性

不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。简单说,就是在一段时间后,节点间的数据会最终达到一致状态。

由于分布式事务方案,无法做到完全的ACID的保证,没有一种完美的方案,能够解决掉所有业务问题。因此在实际应用中,会根据业务的不同特性,选择最适合的分布式事务方案。

分布式事务的基础

CAP理论
  • Consistency(一致性):数据一致更新,所有数据变动都是同步的(强一致性)。
  • Availability(可用性):好的响应性能。
  • Partition tolerance(分区容错性) :可靠性。

定理:任何分布式系统只可同时满足二点,没法三者兼顾。

CA系统(放弃P):指将所有数据(或者仅仅是那些与事务相关的数据)都放在一个分布式节点上,就不会存在网络分区。所以强一致性以及可用性得到满足。

CP系统(放弃A):如果要求数据在各个服务器上是强一致的,然而网络分区会导致同步时间无限延长,那么如此一来可用性就得不到保障了。坚持事务ACID(原子性、一致性、隔离性和持久性)的传统数据库以及对结果一致性非常敏感的应用通常会做出这样的选择。

AP系统(放弃C):这里所说的放弃一致性,并不是完全放弃数据一致性,而是放弃数据的强一致性,而保留数据的最终一致性。如果即要求系统高可用又要求分区容错,那么就要放弃一致性了。因为一旦发生网络分区,节点之间将无法通信,为了满足高可用,每个节点只能用本地数据提供服务,这样就会导致数据不一致。一些遵守BASE原则数据库,(如:Cassandra、CouchDB等)往往会放宽对一致性的要求(满足最终一致性即可),一次来获取基本的可用性。

BASE理论

BASE 是 Basically Available(基本可用)、Soft state(软状态)和 Eventually consistent (最终一致性)三个短语的缩写。是对CAP中AP的一个扩展。

  1. 基本可用:分布式系统在出现故障时,允许损失部分可用功能,保证核心功能可用。
  2. 软状态:允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是CAP中的不一致。
  3. 最终一致:最终一致是指经过一段时间后,所有节点数据都将会达到一致。

BASE解决了CAP中理论没有网络延迟,在BASE中用软状态和最终一致,保证了延迟后的一致性。BASE和 ACID 是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。

分布式事务解决方案

分布式事务的实现主要有以下 6 种方案:

  • 2PC 方案
  • TCC 方案
  • 本地消息表
  • MQ事务
  • Saga事务
  • 最大努力通知方案
2PC方案

2PC方案分为两阶段:

第一阶段:事务管理器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交.

第二阶段:事务协调器要求每个数据库提交数据,或者回滚数据。

优点:

尽量保证了数据的强一致,实现成本较低,在各大主流数据库都有自己实现,对于MySQL是从5.5开始支持。

缺点:

  • 单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
  • 同步阻塞:在准备就绪之后,资源管理器中的资源一直处于阻塞,直到提交完成,释放资源。
  • 数据不一致:两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,比如在第二阶段中,假设协调者发出了事务commit的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了commit操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

总的来说,2PC方案比较简单,成本较低,但是其单点问题,以及不能支持高并发(由于同步阻塞)依然是其最大的弱点。

TCC

TCC 的全称是:TryConfirmCancel

  • Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行 锁定或者预留
  • Confirm 阶段:这个阶段说的是在各个服务中执行实际的操作。
  • Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要 进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)

举个简单的例子如果你用100元买了一瓶水, Try阶段:你需要向你的钱包检查是否够100元并锁住这100元,水也是一样的。

如果有一个失败,则进行cancel(释放这100元和这一瓶水),如果cancel失败不论什么失败都进行重试cancel,所以需要保持幂等。

如果都成功,则进行confirm,确认这100元扣,和这一瓶水被卖,如果confirm失败无论什么失败则重试(会依靠活动日志进行重试)。

这种方案说实话几乎很少人使用,但是也有使用的场景。因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大。

本地消息表

本地消息表的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。

对于本地消息队列来说核心是把大事务转变为小事务。还是举上面用100元去买一瓶水的例子。

1.当你扣钱的时候,你需要在你扣钱的服务器上新增加一个本地消息表,你需要把你扣钱和写入减去水的库存到本地消息表放入同一个事务(依靠数据库本地事务保证一致性。

2.这个时候有个定时任务去轮询这个本地事务表,把没有发送的消息,扔给商品库存服务器,叫他减去水的库存,到达商品服务器之后这个时候得先写入这个服务器的事务表,然后进行扣减,扣减成功后,更新事务表中的状态。

3.商品服务器通过定时任务扫描消息表或者直接通知扣钱服务器,扣钱服务器本地消息表进行状态更新。

4.针对一些异常情况,定时扫描未成功处理的消息,进行重新发送,在商品服务器接到消息之后,首先判断是否是重复的,如果已经接收,在判断是否执行,如果执行在马上又进行通知事务,如果未执行,需要重新执行需要由业务保证幂等,也就是不会多扣一瓶水。

本地消息队列是BASE理论,是最终一致模型,适用于对一致性要求不高的。实现这个模型时需要注意重试的幂等。

MQ事务

基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。

MQ事务方案整体流程和本地消息表的流程很相似,如下图:

从上图可以看出和本地消息表方案唯一不同就是将本地消息表存在了MQ内部,而不是业务数据库中。

那么MQ内部的处理尤为重要,下面主要基于 RocketMQ 4.3 之后的版本介绍 MQ 的分布式事务方案。

在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ提供了 2PC 的提交接口,方案如下:

正常情况:事务主动方发消息

这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:

  • 发送方向 MQ 服务端(MQ Server)发送 half 消息。
  • MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功。
  • 发送方开始执行本地事务逻辑。
  • 发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
  • MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。

异常情况:事务主动方消息恢复

在断网或者应用重启等异常情况下,图中 4 提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:

  • MQ Server 对该消息发起消息回查。
  • 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
  • 发送方根据检查得到的本地事务的最终状态再次提交二次确认。
  • MQ Server基于 commit/rollback 对消息进行投递或者删除。

优点

相比本地消息表方案,MQ 事务方案优点是:

  • 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。
  • 吞吐量大于使用本地消息表方案。

缺点

  • 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) 。
  • 业务处理服务需要实现消息状态回查接口。
Saga事务

Saga是由一系列的本地事务构成。每一个本地事务在更新完数据库之后,会发布一条消息或者一个事件来触发Saga中的下一个本地事务的执行。如果一个本地事务因为某些业务规则无法满足而失败,Saga会执行在这个失败的事务之前成功提交的所有事务的补偿操作。

Saga的实现有很多种方式,其中最流行的两种方式是:

  • 基于事件的方式。这种方式没有协调中心,整个模式的工作方式就像舞蹈一样,各个舞蹈演员按照预先编排的动作和走位各自表演,最终形成一只舞蹈。处于当前Saga下的各个服务,会产生某类事件,或者监听其它服务产生的事件并决定是否需要针对监听到的事件做出响应。
  • 基于命令的方式。这种方式的工作形式就像一只乐队,由一个指挥家(协调中心)来协调大家的工作。协调中心来告诉Saga的参与方应该执行哪一个本地事务。

五、设计模式

1、设计模式六大原则

(1)单一职责原则:一个类或者一个方法只负责一项职责,尽量做到类只有一个行为引起变化;
(2)里氏替换原则:子类可以扩展父类的功能,但不能改变原有父类的功能
(3)依赖倒置原则:高层模块不应该依赖底层模块,两者都应该依赖接口或抽象类
(4)接口隔离原则:建立单一接口,尽量细化接口
(5)迪米特原则:只关心其它对象能提供哪些方法,不关心过多内部细节
(6)开闭原则:对于拓展是开放,对于修改是封闭的

2、设计模式分类

创建型模式:主要是描述对象的创建,代表有单例、原型模式、工厂方法、抽象工厂、建造者模式

结构型模式:主要描述如何将类或对象按某种布局构成更大的结构,代表有代理、适配器、装饰

行为型模式:描述类或对象之间如何相互协作共同完成单个对象无法完成的任务,代表有模板方法模式、策略模式、观察者模式、备忘录模式

3、各个模式应用场景

23种经典设计模式共分为3种类型,分别是创建型、结构型和行为型。今天,我们把这3种类型分成3个对应的小模块,逐一带你看一下每一种设计模式的原理、实现、设计意图和应用场景。

Creational Patterns 创建型模式

1 、Prototype Pattern 原型模式

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。

应用场景:不是同一个对象,但是属于同类的复制,例如复印技术。

2 Singleton Pattern 单例模式

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

应用场景:控制游戏中声音播放;UI窗口显示;资源加载;计时器等。

3 Abstract Factory Pattern 抽象工厂模式

提供一个接口,用于创建相关或者依赖对象的家族,而不需要指定具体的实现类。

应用场景:同一种类的使用,游戏难度、游戏阵营:不同级别难度下游戏可以产生不同类型的建筑、英雄以及技能;AI 战术选择:实现确定好几种可选战术作为产品族,每种战术按照类似海军、空军、陆军作为产品等级结构选择具体兵种进行生产建造;国际化:用户改变语言环境时,会改变响应的文字显示,音效,图片显示,操作习惯适配等;皮肤更换或者资源管理:用户选择不同主题的皮肤将会影响图片显示以及动画效果;屏幕适配:对高、中、低分辨率的移动设备使用不同的显示资源。

4 Builder Pattern 建造者模式

将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。

应用场景:需要生成的产品对象复杂内部结构,这些产品具备共性;隔离复杂的对象的创建和使用,并使的相同的创建的不同的产品。

5 Factory Method Pattern 工厂方法模式

定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。

应用场景:游戏场景转换;角色AI状态管理。

Structural Patterns 结构型模式

1 Adapter Pattern 适配器模式

将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

应用场景:转接,复用已有的功能,但已有的功能接口不是客户端想要的。

2 Bridge Pattern 桥接模式

将抽象和实现解耦,使得两者可以独立地变化。

应用场景:一个类存在两个独立变化的维度,且这两个维度都需要进行扩展;需要从继承或多重继承中简化结构,且应对频繁变动的需求。

3 Composite Pattern 组合模式

将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

应用场景:不同的界面的逻辑和UI通过一个管理器统一控制。

4 Decorator Pattern 装饰模式

动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。

应用场景:写好一个相应的战斗模块,但是在进入之前或者进入之后添加一些,输出日志、数据监测、数据收集埋点触发之类的功能

5 Facade Pattern 外观模式

要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。外观模式提供一个高层次的接口,使得子系统更易于使用。

应用场景:汽车的内部运作机制复杂,但是它给我们提供了方向盘、仪表盘、刹车、油门这些高级接口,我们便不需要了解引擎系统、动力传输系统等复杂系统,所以外观模式的重点在于,隐藏系统内部的互动细节,并提供简单方便的接口。之后让客户端只需要通过这个接口,就可以操作一个复杂的系统,并让它们顺利运行。

6 Flyweight Pattern 享元模式

使用共享对象可有效地支持大量的细粒度的对象。

应用场景:五子棋创建;游戏中某些不断创建销毁的物体,子弹对象池。

7 Proxy Pattern 代理模式

使用共享对象可有效地支持大量的细粒度的对象。

应用场景:代理Text, Button, Image等组件的功能;登陆设置;委托;代理下载资源等。

Behavioral Patterns 行为型模式

1 Command Pattern 命令模式

命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象,同时支持可撤消的操作。

应用场景:用户的客户端请求,实现请求的队列操作和日志光里,并且支持对操作进行撤销回退。

2 State Pattern 状态模式

当一个对象内在状态改变时允许其改变行为,这个对象看起来像改变了其类。

应用场景:角色状态;AI状态;账号登录状态;场景状态;动画机状态等。

3 Observer Pattern 观察者模式

定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。

应用场景:发布;订阅消息;角色血条值下降被改变(防御力、攻击力等)。

4 Chain of Responsibility Pattern 责任链模式

使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。

应用场景:如游戏中敌方角色的设置、通关条件、下一关纪录。

5 Mediator Pattern 中介者模式

用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

应用场景:感知系统,里面涉及视觉感应器、听觉感应器、视觉触发器、听觉触发器等交互可通过中介者完成。

6 Interpreter Pattern 解释器模式

给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。

应用场景:编译器;这些程序代码经过一般文本编辑器编写完成后放入指定的位置,例如Lua。

7 Iterator Pattern 迭代器模式

提供一种方法访问一个容器对象中各个元素,而又不需暴露该对象的内部细节。

应用场景:C#自带的IEnumerator;需要按指定顺序迭代查询集合中的元素。

8 Memento Pattern 备忘录模式

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

应用场景:资源备份;历史记录;还原操作。

9 Strategy Pattern 策略模式

定义一组算法,将每个算法都封装起来,并且使它们之间可以互换。

应用场景:算法与环境都独立开来,算法的增减、修改不会对环境和客户端影响.例如:外挂[同步图片、名言、笑话,各自的售卖算法];技能效果类和方法的构建。

10 Template Method Pattern 模板方法模式

定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

应用场景:技能效果类和方法的构建。

11 Visitor Pattern 访问者模式

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。

应用场景:例如利用一个类,表示敌人和士兵都可以访问在做一样的事情,可以封装一个父类,然后子类继承。

六、docker容器

七、跨越问题

1.跨域的概念

1.“源”由协议、域名、端口号组成
2.同源策略是浏览器的一种保护机制。同源顾名思义,指两个源相同(即,两个源的协议、域名、端口号都相同)
3.违反了同源策略的请求就是通常说的跨域请求

2、跨域的解决方案-JSONP

JSONP方案和ajax没有任何关系,是通过script标签的src属性实现,因此JSONP方案只支持get请求,并且兼容性好,几乎所有浏览器都支持。
实现原理:在全局定义一个函数,将函数名以get传参的方式写入到script标签的src属性中(如下图所示),后端返回函数名以及参数,全局定义的函数就会自动调用,形参会接收后端传过来的参数。

3、跨域的解决方案-CORS

CORS(Cross-Origin Resource Sharing,跨域资源共享)方案,就是通过服务器设置一系列响应头来实现跨域。而客户端不需要做什么额外的事情。

4、跨域的解决方案-代理转发

代理转发的原理:在前端服务和后端接口服务之间架设一个中间代理服务,它的地址保持和前端服务一致,那么:

  • 代理服务和前端服务之间由于协议域名端口三者统一不存在跨域问题,可以直接发送请求

  • 代理服务和后端服务之间由于并不经过浏览器没有同源策略的限制,可以直接发送请求

这样,我们就可以通过中间这台服务器做接口转发,在开发环境下解决跨域问题(只能解决开发环境下跨域的问题)。

实现方法:vue-cli为我们内置了该技术,我们只需要按照要求配置一下即可。
在vue.config.js配置文件中,有一项是devServer,它就是我们下边要操作的主角。

module.exports = {
  devServer: {
    // ... 省略
    // 代理配置
    proxy: {
      // 如果请求地址以/api打头,就出触发代理机制
      // http://localhost:8080/api/login -> http://localhost:3000/api/login
      '/api': {
        target: 'http://localhost:3000' // 我们要代理的真实接口地址
      }
    }
  }
}

5、nginx反向代理

反向代理和代理很像,都是在客户端与服务端中间架设一个中间代理服务器,不同点在于代理是代表客户端向服务端发送请求,反向代理是代表服务端接收客户端发送的请求。
实现方式是配置Nginx,修改nginx.conf配置文件,如下图所示:

八、nginx

1、请解释一下什么是Nginx?

Nginx是一个web服务器和方向代理服务器,用于HTTP、HTTPS、SMTP、POP3和IMAP协议。

2、请列举Nginx的一些特性。

Nginx服务器的特性包括:

反向代理/L7负载均衡器

嵌入式Perl解释器

动态二进制升级

可用于重新编写URL,具有非常好的PCRE支持

3、Nginx应用场景?

http服务器。Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。
虚拟主机。可以实现在一台服务器虚拟出多个网站,例如个人网站使用的虚拟机。
反向代理,负载均衡。当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会应为某台服务器负载高宕机而某台服务器闲置的情况。
nginz 中也可以配置安全管理、比如可以使用Nginx搭建API接口网关,对每个接口服务进行拦截。

4、Nginx怎么处理请求的?

server {         # 第一个Server区块开始,表示一个独立的虚拟主机站点
   listen       80# 提供服务的端口,默认80
   server_name  localhost; # 提供服务的域名主机名
   location / { # 第一个location区块开始
     root   html; # 站点的根目录,相当于Nginx的安装目录
     index  index.html index.html;  # 默认的首页文件,多个用空格分开
} # 第一个location区块结果
  • Nginx 在启动时,会解析配置文件,得到需要监听的端口与 IP 地址,然后在 Nginx 的 Master
    进程里面先初始化好这个监控的Socket(创建 S ocket,设置 addr、reuse 等选项,绑定到指定的 ip 地址端口,再
    listen 监听)。
  • 再 fork(一个现有进程可以调用 fork 函数创建一个新进程。由 fork 创建的新进程被称为子进程 )出多个子进程出来。
  • 子进程会竞争 accept 新的连接。此时,客户端就可以向 nginx 发起连接了。当客户端与nginx进行三次握手,与
    nginx 建立好一个连接后。此时,某一个子进程会 accept 成功,得到这个建立好的连接的 Socket ,然后创建 nginx
    对连接的封装,即 ngx_connection_t 结构体。
  • 设置读写事件处理函数,并添加读写事件来与客户端进行数据的交换。
  • 最后,Nginx 或客户端来主动关掉连接,到此,一个连接就寿终正寝了。

5、Nginx 是如何实现高并发的?

如果一个 server 采用一个进程(或者线程)负责一个request的方式,那么进程数就是并发数。那么显而易见的,就是会有很多进程在等待中。等什么?最多的应该是等待网络传输。

而 Nginx 的异步非阻塞工作方式正是利用了这点等待的时间。在需要等待的时候,这些进程就空闲出来待命了。因此表现为少数几个进程就解决了大量的并发问题。

Nginx是如何利用的呢,简单来说:同样的 4 个进程,如果采用一个进程负责一个 request 的方式,那么,同时进来 4 个 request 之后,每个进程就负责其中一个,直至会话关闭。期间,如果有第 5 个request进来了。就无法及时反应了,因为 4 个进程都没干完活呢,因此,一般有个调度进程,每当新进来了一个 request ,就新开个进程来处理。
Nginx 不这样,每进来一个 request ,会有一个 worker 进程去处理。但不是全程的处理,处理到什么程度呢?处理到可能发生阻塞的地方,比如向上游(后端)服务器转发 request ,并等待请求返回。那么,这个处理的 worker 不会这么傻等着,他会在发送完请求后,注册一个事件:“如果 upstream 返回了,告诉我一声,我再接着干”。于是他就休息去了。此时,如果再有 request 进来,他就可以很快再按这种方式处理。而一旦上游服务器返回了,就会触发这个事件,worker 才会来接手,这个 request 才会接着往下走。
这就是为什么说,Nginx 基于事件模型。
由于 web server 的工作性质决定了每个 request 的大部份生命都是在网络传输中,实际上花费在 server 机器上的时间片不多。这是几个进程就解决高并发的秘密所在。即:
webserver 刚好属于网络 IO 密集型应用,不算是计算密集型。
异步,非阻塞,使用 epoll ,和大量细节处的优化。也正是 Nginx 之所以然的技术基石。

6、什么是正向代理?

一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。

客户端才能使用正向代理。
正向代理总结就一句话:代理端代理的是客户端。
例如说:我们使用的OpenVPN 等等。

7、什么是反向代理?

反向代理(Reverse Proxy)方式,是指以代理服务器来接受 Internet上的连接请求,然后将请求,发给内部网络上的服务器并将从服务器上得到的结果返回给 Internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。
反向代理总结就一句话:代理端代理的是服务端。

透明代理

透明代理的意思是客户端根本不需要知道有代理服务器的存在,它改编你的request fields(报文),并会传送真实IP。注意,加密的透明代理则是属于匿名代理,意思是不用设置使用代理了。 透明代理实践的例子就是时下很多公司使用的行为管理软件。如下图所示:

8、反向代理服务器的优点是什么?

反向代理服务器可以隐藏源服务器的存在和特征。它充当互联网云和web服务器之间的中间层。这对于安全方面来说是很好的,特别是当您使用web托管服务时。

9、Nginx负载均衡的算法怎么实现的?策略有哪些?

为了避免服务器崩溃,大家会通过负载均衡的方式来分担服务器压力。将对台服务器组成一个集群,当用户访问时,先访问到一个转发服务器,再由转发服务器将访问分发到压力更小的服务器。

Nginx负载均衡实现的策略有以下五种:

1 .轮询(默认)

每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某个服务器宕机,能自动剔除故障系统。

upstream backserver { 
 server 192.168.31.12; 
 server 192.168.31.13; 
} 
2. 权重 weight

weight的值越大,分配到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。其次是为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。

# 权重越高,在被访问的概率越大,如上例,分别是20%,80%。
upstream backserver { 
 server 192.168.0.12 weight=2; 
 server 192.168.0.13 weight=8; 
}
3. ip_hash( IP绑定)

每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,并且可以有效解决动态网页存在的session共享问题

upstream backserver { 
 ip_hash; 
 server 192.168.0.12:88; 
 server 192.168.0.13:80; 
}
4、fair(第三方插件)

必须安装upstream_fair模块。

对比 weight、ip_hash更加智能的负载均衡算法,fair算法可以根据页面大小和加载时间长短智能地进行负载均衡,响应时间短的优先分配。

# 哪个服务器的响应速度快,就将请求分配到那个服务器上。
upstream backserver { 
 server server1; 
 server server2; 
 fair; 
}
5、url_hash(第三方插件)

必须安装Nginx的hash软件包

按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。

upstream backserver { 
 server squid1:3128; 
 server squid2:3128; 
 hash $request_uri; 
 hash_method crc32; 
}

10 、Nginx配置高可用性怎么配置?

当上游服务器(真实访问服务器),一旦出现故障或者是没有及时相应的话,应该直接轮训到下一台服务器,保证服务器的高可用

server {
        listen       80;
        server_name  www.lijie.com;
        location / {
            ### 指定上游服务器负载均衡服务器
            proxy_pass http://backServer;
            ###nginx与上游服务器(真实访问的服务器)超时时间 后端服务器连接的超时时间_发起握手等候响应超时时间
            proxy_connect_timeout 1s;
            ###nginx发送给上游服务器(真实访问的服务器)超时时间
            proxy_send_timeout 1s;
            ### nginx接受上游服务器(真实访问的服务器)超时时间
            proxy_read_timeout 1s;
            index  index.html index.htm;
        }
    }

编译器;这些程序代码经过一般文本编辑器编写完成后放入指定的位置,例如Lua。

九、Restful风格

1、什么是Restful风格?

  • Restful风格指的是网络应用中就是资源定位和资源操作的风格。不是标准也不是协议。
  • Rest即Representational State Transfer的缩写,可译为"表现层状态转化”。Restful风格最大的特点为:资源、统一接口、URI和无状态。
  • 这种风格设计的软件,可以更简洁,更有层次,更易于实现缓存等机制。

2、Restful的特点

资源:互联网所有的事务都可以被抽象为资源,例如:.txt .html .jpg .mp3 .mp4等

RESTful 架构风格是围绕资源展开的,资源操作都是统一接口的:

  • GET(SELECT):从服务器取出资源(一项或多项)。

  • POST(CREATE):在服务器新建一个资源。

  • PUT(UPDATE):在服务器更新资源(客户端提供完整资源数据)。

  • PATCH(UPDATE):在服务器更新资源(客户端提供需要修改的资源数据)。

  • DELETE(DELETE):从服务器删除资源。

  • URI:每一个URI(统一资源定位符)指向一个特定的资源。通过URI来访问资源。最典型的URI就是URL。@RequestMapping的path/value属性表示的就是URL的一部分。

无状态:

所有的资源,都可以通过URI定位,而且这个定位与其他资源无关。例如无需登录就可以通过URL查看,就是无状态。需要登录才能查看,是有状态。

3、实现

1、使用RESTful风格url

传统url:http://localhost:8080/h1?a=1&b=11

RESTful:http://localhost:8080/h2/1/11

@PathVariable 用于修饰方法传入参数,表示该参数值从请求路径中获取

@RequestMapping(“h2/{a}/{b}”) /{a}/{b}:表示要传入的参数值,在请求url中用斜线/分隔多个参数。

@Controller
public class RESTfulController {

    //传统方式:http://localhost:8080/h1?a=1&b=11
    @RequestMapping("h1")
    public String test1(int a, int b , Model model){
        int rslt=a+b;
        model.addAttribute("msg", "结果为:"+rslt);
        return "hello";
    }

    //RESTful:http://localhost:8080/h2/1/11
    @RequestMapping("h2/{a}/{b}")
    public String test2(@PathVariable int a, @PathVariable int b , Model model){
        int rslt=a+b;
        model.addAttribute("msg", "结果为:"+rslt);
        return "hello";
    }
}

2、url不变,使用method属性区分

@RequestMapping的请求路径用value或path属性表示
@RequestMapping的method属性表示请求方式,例如:POST、GET等。

@RequestMapping(value = "h3/{a}/{b}",method = RequestMethod.GET)
public String test3(@PathVariable int a, @PathVariable int b , Model model){
    int rslt=a+b;
    model.addAttribute("msg", "get结果为:"+rslt);
    return "hello";
}

@RequestMapping(value = "h3/{a}/{b}",method = RequestMethod.POST)
public String test4(@PathVariable int a, @PathVariable int b , Model model){
    int rslt=a+b;
    model.addAttribute("msg", "post结果为:"+rslt);
    return "hello";
}

请求路径都为http://localhost:8080/h3/1/11

默认为GET方式请求,走入method为get的分支

使用表单提交POST请求

点击提交后,走入method为post的分支

3、直接使用使用@GetMapping、@PostMapping(等价于使用指定形式的@RequestMapping)

@GetMapping:@RequestMapping(method = RequestMethod.GET)的快捷方式
@PostMapping:@RequestMapping(method = RequestMethod.POST)的快捷方式

@GetMapping("h3/{a}/{b}")
public String test5(@PathVariable int a, @PathVariable int b , Model model){
    int rslt=a+b;
    model.addAttribute("msg", "get结果为:"+rslt);
    return "hello";
}

@PostMapping( "h3/{a}/{b}")
public String test6(@PathVariable int a, @PathVariable int b , Model model){
    int rslt=a+b;
    model.addAttribute("msg", "post结果为:"+rslt);
    return "hello";
}

Restful风格的优点总结

  • 使请求路径变得更加简洁
  • 传递、获取参数值更加方便,框架会自动进行类型转换
  • 通过路径变量@PathVariable的类型,可以约束访问参数。
  • 若参数值与定义类型不匹配,则访问不到对应的方法,报错400错误的请求。
    安全,请求路径中直接传递参数值,并用斜线/分隔,不会暴露传递给方法的参数变量名。
  • 高效,更易于缓存的实现,让响应更加高效。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值