面试复盘2020.06

面试复盘

Spring事务失效的原因

  1. 外部调用事务才会生效。【同一个类中相互调用就算有加 @Transactional注解,也不会生效,应该没有经过Spring代理的类。解决:可以在该类中注入该类的实例,通过该实例进行调用。这个实例是从SpringIOC容器中拿出的,经过了Spring的代理】
  2. 只有@Transactional只有在Spring Bean中才会生效。即@Transactional注解不能使用在普通类中,不然不会生效,即必须经过Spring的代理。
  3. 被@Transactional注解的方法只能是public,其余不能生效,想要在非public方法上生效可以开启 AspectJ 代理模式。
  4. 数据源没有配置事务管理器
  5. 数据库引擎不支持事务,如myisam
  6. 异常被吃了!使用了try{}catch{}捕获了异常,但是不抛出,即事务想要回滚,但是没有抛出异常无法回滚。
  7. 异常类型错误!事务默认回滚的是RuntimeException,若是不是这个类型的不会发生回滚,除非声明,如:@Transactional(rollbackFor = Exception.class)
  8. 声明事务无效!@Transactional(propagation=Propagation.NOT_SUPPORT)

Spring事务的实现原理

Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:

  1. 获取连接 Connection con = DriverManager.getConnection()
  2. 开启事务con.setAutoCommit(true/false);
  3. 执行CRUD
  4. 提交事务/回滚事务 con.commit() / con.rollback();
  5. 关闭连接 conn.close();

Spring事务管理可以让我们不再写2、4的代码

//TODO…

Spring事务传播的属性

  1. REQUIRED, 如果当前线程已经在一个事务中,则加入该事务,否则新建一个事务。
  2. SUPPORT, 如果当前线程已经在一个事务中,则加入该事务,否则不使用事务。
  3. MANDATORY(强制的),如果当前线程已经在一个事务中,则加入该事务,否则抛出异常。
  4. REQUIRES_NEW,无论如何都会创建一个新的事务,如果当前线程已经在一个事务中,则挂起当前事务,创建一个新的事务。
  5. NOT_SUPPORTED,如果当前线程在一个事务中,则挂起事务。
  6. NEVER,如果当前线程在一个事务中则抛出异常。
  7. NESTED, 执行一个嵌套事务,有点像REQUIRED,但是有些区别,在MySQL中是采用SAVEPOINT来实现的。

Spring事务的隔离级别

Spring IOC 、AOP 技术实现

Spring的一些重要技术

Spring Bean的生命周期

Spring IOC 容器的初始化

拦截器和过滤器

Redis实现事务的原理

简介

Redis通过MULTI、EXEC、WATCH等命令来实现事务功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而去执行其他客户端的命令请求,它会将事务中所有命令都执行完毕,然后才去处理其他客户端的命令请求。 Redis实现事务的机制与关系型数据库有很大的区别,redis的事务不支持回滚。

事务的实现

一个事务从开始到结束的三个阶段:事务开始 --> 命令入队 --> 事务执行

事务开始

MULTI命令的执行标志着事务的开始【MULTI命令将客户端从非事务状态转换成事务状态—>通过在客户端状态的flags属性中打开REDIS_MULTI标识来完成】

void multiCommand(client *c) {
	if (c->flags & CLIENT_MULTI) { 
		addReplyError(c,"MULTI calls can not be nested"); 
		return; 
	}
	c->flags |= CLIENT_MULTI; //打开事务标识 
	addReply(c,shared.ok);
}

命令入队

当客户端处于非事务状态下时,这个客户端发送的命令会立即被服务器执行

当客户端处于事务状态下时,服务器会根据这个客户端发来的不同命令执行不同的操作

  1. 当客户端发送的命令是:MULTI、EXEC、DISCARD、WATCH 服务器会立即执行命令
  2. 当客户端发送的命令是除了上面四个命令以外的命令时,会将命令放入一个事务队列中,然后向客户端返回QUEUED回复

服务器判断命令是否入队还是立即执行的流程图如下:

服务器接到来自客户端的命令
这个客户端正处于事务状态?
这个命令是EXEC DISCARD WATCH MULTI?
执行这个命令
向客户端返回命令的执行结果
将命令放入事务队列
向客户端返回QUEUED
事务队列

每个客户端都有自己的事务状态,这个事务状态保存在客户端状态下的mstate属性里面

typedef struct redisClient{
    //...
     //事务状态
    multiState mstate; /* MULTI/EXEC state */
    //...
}redisClient;

事务状态包含一个事务队列,以及一个已入队命令的计数器【统计入队命令的数量–>事务队列的长度】

typedef struct multiState{
    //事务队列 FIFO顺序  multiCmd类型数组
    multiCmd *commands;
    //已入队命令计数
    int count;
}multiState;

每一个multiCmd结构中都保存了一个已入队命令的相关信息,包括指向命令实现的函数的指针、命令的参数、参数的数量

typedef struct multiCmd{
    //参数
    robj **argv;
    //参数数量
    int argc;
    //命令指针
    struct redisCommand *cmd;
}multiCmd;

执行事务

当一个处于事务状态的客户端发送EXEC命令给服务器,服务器会立即执行这个命令。

服务器会遍历这个客户端的事务队列,执行事务队列中保存的所有命令,最后将执行命令所得的结果全部返回给客户端。

void execCommand(client *c) {
    int j;
    robj **orig_argv;
    int orig_argc;
    struct redisCommand *orig_cmd;
    int must_propagate = 0; //同步持久化,同步主从节点
    //如果客户端没有开启事务标识
    if (!(c->flags & CLIENT_MULTI)) {
        addReplyError(c,"EXEC without MULTI");
        return;
    }
    //检查是否需要放弃EXEC
    //如果某些被watch的key被修改了就放弃执行
    if (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {
        addReply(c, c->flags & CLIENT_DIRTY_EXEC ? shared.execaborterr :
                                                  shared.nullmultibulk);
        discardTransaction(c);
        goto handle_monitor;
    }

    //执行事务队列里的命令
    unwatchAllKeys(c); //因为redis是单线程的所以这里,当检测watch的key没有被修改后就统一clear掉所有的watch
    orig_argv = c->argv;
    orig_argc = c->argc;
    orig_cmd = c->cmd;
    addReplyMultiBulkLen(c,c->mstate.count);
    for (j = 0; j < c->mstate.count; j++) {
        c->argc = c->mstate.commands[j].argc;
        c->argv = c->mstate.commands[j].argv;
        c->cmd = c->mstate.commands[j].cmd;

        //同步主从节点,和持久化
        if (!must_propagate && !(c->cmd->flags & CMD_READONLY)) {
            execCommandPropagateMulti(c);
            must_propagate = 1;
        }
        //执行命令
        call(c,CMD_CALL_FULL);
        c->mstate.commands[j].argc = c->argc;
        c->mstate.commands[j].argv = c->argv;
        c->mstate.commands[j].cmd = c->cmd;
	}
    c->argv = orig_argv;
    c->argc = orig_argc;
    c->cmd = orig_cmd;
    //取消客户端的事务标识
    discardTransaction(c);
    if (must_propagate) server.dirty++;

handle_monitor:
    if (listLength(server.monitors) && !server.loading)
        replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
}

伪代码:

def EXEC():
	#创建空白的回复队列
	reply_queue = []
	#遍历事务中的每一项
	#读取命令的参数,参数的个数,以及要执行的命令
	for argv,argc,cmd in client.mstate.commands:
		#执行命令,并获得命令的返回值
		reply = execute_command(cmd,argv,argc)
		reply_queue.append(reply)
	#移除REDIS_MULTI标识,让客户端回到非事务状态
	client.flags &= ~REDIS_MULTI
    
    #清除客户端的事务状态,包括:1 清零入队命令计数器 2 释放事务队列
    client.mstate.count = 0
    release_transaction_queue(client.mstate.commands)
    
    #将事务的执行结果返回给客户端
    send_reply_to_client(client,reply_queue)

WATCH/UNWATCH/DISCARD命令

**WATCH:**命令是一个乐观锁,它可以在EXEC命令执行之前,监视任意数量的数据库键,并在执行EXEC命令时判断是否至少有一个被watch的键值被修改如果被修改就放弃事务的执行,如果没有被修改就清空watch的信息,执行事务列表里的命令。

**UNWATCH:**顾名思义可以看出它的功能是与watch相反的,是取消对一个键值的“监听”的功能。

**DISCARD:**清空客户端的事务队列里的所有命令,并取消客户端的事务标记,如果客户端在执行事务的时候watch了一些键,则discard会取消所有键的watch

Redis事务的ACID特性

**原子性(Atomicity):**事务队列中的命令要么全部执行,要么一个都不执行

**一致性(Consistency):**如果数据库在执行事务之前是一致的,那么执行事务之后,无论事务是否执行成功,数据库依旧是一致的。“一致”:数据符合数据库的定义和要求,没有包含非法或者无效的错误数据。

Redis通过谨慎的错误检测和简单的设计来保证事务的一致性。

  1. 入队错误

    事务在命令入队过程中,出现了命令不存在或者命令格式不正确等情况,那么Redis将拒绝执行这个事务

  2. 执行错误

    执行过程中,命令出现了错误,服务器不会中断事务的执行,会继续执行事务中余下的命令,且已经执行了的命令不会被出错的命令影响到。出错的命令不会对数据库中的数据进行修改,不会对破坏一致性。

  3. 服务器停机

    1. 无持久化的内存模式。重启之后数据库是空白的
    2. RDB模式,重启之后根据RDB文件恢复数据,将数据还原到一个一致性的状态【可能会丢失数据,但是数据库会处于一致性状态】,找不到RDB文件,数据库是空白的,空白的数据库也是一致性的。
    3. AOF模式,重启之后根据AOF文件恢复数据,将数据还原到一个一致性的状态【可能会丢失数据,但是数据库会处于一致性状态】,找不到AOF文件,数据库是空白的,空白的数据库也是一致性的。

**隔离性(Isolation):**数据库中有多个事务并发地执行,各个事务之间不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果一样。

Redis使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事物进行中断,因此Redis事务总是以串行的方式运行,具有隔离性。

**持久性(Durability):**当一个事务执行完的时候,执行这个事务所得的结果已经被保存到永久性存储介质(如硬盘)中,即使服务器在事务执行完之后停机,事务所执行的结果不会丢失。

Redis没有为事务提供额外的持久化功能,所以Redis事务的耐久性由Redis所使用的的持久性模式决定

  1. 当服务器在无持久化的内存模式下运作时,事务不具有耐久性:一旦服务器停机,包括事务数据在内的所有服务器数据都将丢失。
  2. RDB模式下时,服务器只会在特定的保存条件被满足时,才会执行BGSAVE命令(异步执行),无法保证事务数据第一时间被保存在硬盘中。RDB模式下,redis事务不具有耐久性。
  3. AOF模式 、appendfsync = always 程序总会在执行命令之后调用同步(sync)函数,将命令数据真正地保存到硬盘中,因此具有耐久性。
  4. AOF模式、appendfsync = everysec 程序每秒同步一次命令数据到硬盘,不具有耐久性。
  5. AOF模式、appendfsync = no 程序会交由操作系统来决定何时将命令同步到硬盘,不具有耐久性。

无论什么模式下,Redis事务的最后加上SAVE命令可以保证事务的耐久性,但是效率太低下了,不具有实用性。

Redis的RDB、AOF持久化

RDB 文件的创建与载入

创建RDB文件由rdb.c/rdbSave函数完成,**SAVE[会阻塞Redis服务器进程,知道RDB文件创建完成]BGSAVE[服务器继续处理命令请求]**以不同的方式调用这个函数

def SAVE():
	rdbSave() #创建RDB文件
def BGSAVE():
	#创建子进程
	pid = fork()
    if pid == 0 :
		#子进程负责创建RDB文件
		rdbSave()
        #完成之后向父进程发送信号
        signal_parent()
    elif pid > 0 :
		#父进程继续处理命令请求,并通过轮询等待子进程的信号
		handle_request_and_wait_signal()
    else:
		#处理出错情况
		handle_fork_error()

RDB文件载入发生在服务器重启的时候。

ps:启动了AOF的话会优先使用AOF文件完成数据恢复。因为AOF文件的刷新频率更高,数据比RDB更完整。

载入RDB文件由 rdb.c/rdbLoad函数完成,载入期间服务器被阻塞

BGSAVE命令执行期间,服务器处理SAVE、BGSAVE、BGREWRITEAOF命令会与平时不同:

  1. 客户端发送SAVE命令会被拒绝,防止产生竞争
  2. 客户端发送BGSAVE命令会被拒绝,防止产生竞争
  3. BGSAVE和BGREWRITEAOF不能同时执行
    1. BGSAVE正在执行,BGREWRITEAOF会被延迟到BGSAVE执行完成
    2. BGREWRITEAOF正在执行,BGSAVE会被拒绝

自动间隔性保存

BGSAVE可以设置自动间隔性执行

Redis设置服务器配置的save选项—>服务器每隔一段时间执行BGSAVE

eg: 满足其中任意一个即会出发BGSAVE命令

#这是用户没有设置,服务器为save选项设置的默认条件
save 900 1      #服务器900秒之内,对数据库进行了至少1次修改
save 300 10		#服务器300秒内,对数据库进行了至少10次修改
save 60 10000   #服务器60秒内,对数据库进行了至少10000次修改

上面的save保存在服务器状态redisServer结构中的saveparams属性:

struct redisServer{
  //...
  //记录了保存条件的数组
  struct saveparam *saveparams;
  
  //修改计数器
    long long dirty;
  //上一次执行SAVE或BGSAVE命令的时间戳
  	tiem_t lastsave;
  //..
};

struct saveparam{
    //秒数
    time_t seconds;
    //修改数
    int changes;
}

检查保存条件是否满足

Redis服务器的周期性操作函数serverCron函数默认每100毫秒执行一次,对正在运行的服务器进行维护,包括了检查save选项所设置的保存条件是否满足,满足就执行BGSAVE

def serverCron():
	#...
	#遍历所有的保存条件
	for saveparam in server.saveparams:
		save_interval = unixtime_now() - server.lastsasve
		if server.dirty >= saveparam.changes and save_interval > saveparam.seconds:
			BGSAVE()
	#...

执行完之后会修改 dirty=0 lastsave

RDB文件结构

|REDIS|db_version|database|EOF|check_num

REDIS:5字节 快速检查载入的文件是RDB

db_version:4字节,RDB文件的版本号

database:包含0个或者多个数据库

​ 0个:|REDIS|db_version|EOF|check_num

​ 多个:|REDIS|db_version|database 0|database 3|EOF|check_num

EOF:1字节,标识着RDB文件正文的结束,所有的键值对都载入完毕

check_num:8字节。由前面四部分内容计算所得的检验和,用来检验文件是否出错或者被修改。

database部分

结构:|SELECTDB|db_number|key_value_pairs|

SELECTDB:1字节,当读入程序读到这个就知道后面要读的是数据库号码

db_number:保存着数据库号码。服务器会调用SELECT命令切换到正确的数据库中,使得之后载入的数据能够正确载入到对应的数据库中。

key_value_pairs:保存数据库中的所有键值对

key_value_pairs部分

结构:

不带过期时间的键值对:|TYPE|key|value|

带过期时间的键值对:|EXPIRETIEM_MS|ms|TYPE|key|value|

TYPE(1字节):

  1. REDIS_RDB_TYPE_STRING

  2. REDIS_RDB_TYPE_LIST

  3. REDIS_RDB_TYPE_SET

  4. REDIS_RDB_TYPE_ZSET

  5. REDIS_RDB_TYPE_HASH

  6. REDIS_RDB_TYPE_LIST_ZIPLIST

  7. REDIS_RDB_TYPE_SET_INTSET

  8. REDIS_RDB_TYPE_ZSET_ZIPLIST

  9. REDIS_RDB_TYPE_HASH_ZIPLIST

AOF

AOF持久化的实现(三部分)

命令追加(append)、文件写入、文件同步(sync)

命令追加
struct redisServer{
  //...
    //AOF缓冲区
    sds aof_buf;
  //...
};

执行命令你之后会将命令追加在aof_buf缓冲区中。

redis服务器就像一个循环,一个时间循环。

def eventLoop():
    while True:
        # 处理文件事件,接收命令请求以及发送命令回复
        # 处理命令请求时可能会有新的内容被追加到aof_buf中
        processFileEvents()
      	# 处理时间事件:比如 serverCron()函数这种定时运行的函数
        processTimeEvents()
        
        #考虑是否将aof_buf中的内容写入和保存到AOF文件中
        flushAppendOnlyFile()

flushAppendOnlyFile()函数的行为由服务器配置的appendfsync来决定。

appendfsync选项值 flushAppendOnlyFile函数的行为
always 将aof_buf缓冲区中的所有内容写入并同步到AOF文件中
everysec(默认) 将aof_buf缓冲区中的所有内容写入到AOF文件,如果上次同步AOF文件的时间距离现在超过一秒钟,那么再次对AOF文件进行同步,并且这个同步操作是由一个线程专门负责执行的
no 将aof_buf的内容写到AOF文件中,但是不同步,何时同步由操作系统来决定

AOF文件的载入与数据还原

服务器启动载入程序
创建伪客户端
从AOF文件中分析并读取出一条命令
使用伪客户端执行命令
AOF文件中所有的写命令都已经执行完毕
载入完毕

伪客户端(fake client):不带网络连接

AOF文件重写:BGREWRITEAOF

AOF文件越来越大—>重写变小,状态不变,比如将对一个列表的几次插入合并到一次插入中。

重写过程并不需要原来对AOF文件进行分析等,而是根据现在数据库的状态逆向推出AOF文件。

比如:redis中有一个列表[‘A’,‘B’,‘C’]那么即使原本中的AOF 是一个一个插入的。重写的AOF文件中推导出来的是一次性插入三个数据,只有一条命令。

def aof_rewrite(new_aof_file_name):
    #创建新AOF文件
    f = create_file(new_aof_file_name)
    #遍历数据库
    for db in redisServer.db:
        if db.is_empty():continue
        
        f.write_command("SELECT"+db.id)
        
        for key in db:
            if key.is_expired():continue
            #根据键的类型分别写命令
            if key.type == String:
                rewrite_string(key)
            elif key.type == List:
                rewrite_list(key)
            elif key.type == Hash:
                rewrite_hash(key)
            elif key.type == Set:
                rewrite_set(key)
            elif key.type == SortedSet:
                rewrite_sorted_set(key)
           	
            if key.have_expire_tiem():
                rewrite_expire_time(key)
     f.close()        

一个集合中的元素超过redis.h/REDIS_AOF_REWRITE_ITEMS_PER_CMD (64)常量就会使用多条命令来重写。怕客户端输入缓冲区溢出。

使用子进程完成后台AOF重写。

AOF重写时,父进程可以接受命令请求,这时的父进程会将命令追击到aof_bufaof重写缓冲区

当子进程重写完成之后,父进程会被阻塞,然后再将aof重写缓冲区追加到新的AOF文件中,在修改名字覆盖旧的AOF文件。

怎么解决高并发(JAVA代码层面)

复杂SQL的学习

JAVA内存泄露与内存溢出

内存泄露:被分配对象可达但无用
内存溢出:无法申请到足够的内存而产生的错误

内存泄漏场景
a)创建和应用生命周期一样的单例对象
b)创建匿名内部类的静态对象
c)未关闭资源
d)长时间存在的集合容器中创建生命周期短的对象

e)修改hashset中的值,因此改变了该对象的哈希值

内存溢出场景
a)堆内存溢出
b)方法区内存溢出(反射,静态变量)
c)线程栈溢出(递归)

JAVA IO

分为:Reader 、Writer、InputStream、OutputStream

Reader:FileReader、PipedReader、CharArrayReader 、BufferedReader、InputStreamReader

Writer :FilerWriter、PipedWriter、CharArrayWriter、BufferedWriter、OutputStreamWriter、printWriter

**InputStream:**FileInputStream、PipedInputStream、ByteArrayInputStream、BufferedInputStream、DataInputStream、ObjectInputStream、SequenceInputStream

**OutputStream:**FileOutputStream、PipedOutputStream、ByteArrayOutputStream、BufferedOutputStream、DataOutputStream、ObjectOutputStream、PrintStream

BIO\NIO\AIO

Innodb引擎的学习(体系构造实现等)

Mybatis的整体学习认识

先留着

你可能感兴趣的:(面试)