Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:
Spring事务管理可以让我们不再写2、4的代码
//TODO…
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);
}
当客户端处于非事务状态下时,这个客户端发送的命令会立即被服务器执行
当客户端处于事务状态下时,服务器会根据这个客户端发来的不同命令执行不同的操作
服务器判断命令是否入队还是立即执行的流程图如下:
每个客户端都有自己的事务状态,这个事务状态保存在客户端状态下的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:**命令是一个乐观锁,它可以在EXEC命令执行之前,监视任意数量的数据库键,并在执行EXEC命令时判断是否至少有一个被watch的键值被修改如果被修改就放弃事务的执行,如果没有被修改就清空watch的信息,执行事务列表里的命令。
**UNWATCH:**顾名思义可以看出它的功能是与watch相反的,是取消对一个键值的“监听”的功能。
**DISCARD:**清空客户端的事务队列里的所有命令,并取消客户端的事务标记,如果客户端在执行事务的时候watch了一些键,则discard会取消所有键的watch
**原子性(Atomicity):**事务队列中的命令要么全部执行,要么一个都不执行
**一致性(Consistency):**如果数据库在执行事务之前是一致的,那么执行事务之后,无论事务是否执行成功,数据库依旧是一致的。“一致”:数据符合数据库的定义和要求,没有包含非法或者无效的错误数据。
Redis通过谨慎的错误检测和简单的设计来保证事务的一致性。
入队错误
事务在命令入队过程中,出现了命令不存在或者命令格式不正确等情况,那么Redis将拒绝执行这个事务
执行错误
执行过程中,命令出现了错误,服务器不会中断事务的执行,会继续执行事务中余下的命令,且已经执行了的命令不会被出错的命令影响到。出错的命令不会对数据库中的数据进行修改,不会对破坏一致性。
服务器停机
**隔离性(Isolation):**数据库中有多个事务并发地执行,各个事务之间不会互相影响,并且在并发状态下执行的事务和串行执行的事务产生的结果一样。
Redis使用单线程的方式来执行事务(以及事务队列中的命令),并且服务器保证,在执行事务期间不会对事物进行中断,因此Redis事务总是以串行的方式运行,具有隔离性。
**持久性(Durability):**当一个事务执行完的时候,执行这个事务所得的结果已经被保存到永久性存储介质(如硬盘)中,即使服务器在事务执行完之后停机,事务所执行的结果不会丢失。
Redis没有为事务提供额外的持久化功能,所以Redis事务的耐久性由Redis所使用的的持久性模式决定
无论什么模式下,Redis事务的最后加上SAVE命令可以保证事务的耐久性,但是效率太低下了,不具有实用性。
创建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命令会与平时不同:
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
|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字节):
REDIS_RDB_TYPE_STRING
REDIS_RDB_TYPE_LIST
REDIS_RDB_TYPE_SET
REDIS_RDB_TYPE_ZSET
REDIS_RDB_TYPE_HASH
REDIS_RDB_TYPE_LIST_ZIPLIST
REDIS_RDB_TYPE_SET_INTSET
REDIS_RDB_TYPE_ZSET_ZIPLIST
REDIS_RDB_TYPE_HASH_ZIPLIST
命令追加(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文件中,但是不同步,何时同步由操作系统来决定 |
伪客户端(fake client):不带网络连接
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_buf和aof重写缓冲区
当子进程重写完成之后,父进程会被阻塞,然后再将aof重写缓冲区追加到新的AOF文件中,在修改名字覆盖旧的AOF文件。
内存泄露:被分配对象可达但无用
内存溢出:无法申请到足够的内存而产生的错误
内存泄漏场景
a)创建和应用生命周期一样的单例对象
b)创建匿名内部类的静态对象
c)未关闭资源
d)长时间存在的集合容器中创建生命周期短的对象
e)修改hashset中的值,因此改变了该对象的哈希值
内存溢出场景
a)堆内存溢出
b)方法区内存溢出(反射,静态变量)
c)线程栈溢出(递归)
分为: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