首页 文章 精选 留言 我的

精选列表

搜索[面试],共4912篇文章
优秀的个人博客,低调大师

面试官上来就问MySQL事务,瑟瑟发抖...

关于学习这件事情宁可花点时间系统学习,也不要东一榔头西一棒槌,都说学习最好的方式就是系统的学习,希望看完本文会让你对事务有一定的理解。数据库版本为8.0 系列文章 1. 揭开MySQL索引神秘面纱 2. MySQL查询优化必备 3. 上来就问MySQL事务,瑟瑟发抖... 一、什么是事务 事务是独立的工作单元,在这个独立工作单元中所有操作要么全部成功,要么全部失败。 也就是说如果有任何一条语句因为崩溃或者其它原因导致执行失败,那么未执行的语句都不会再执行,已经执行的语句会进行回滚操作,这个过程被称之为事务。 例: 最近在写一个论坛系统,当发布的主题被其它用户举报后,后台会对举报内容进行审核。 一经审核为违规主题,则进行删除主题的操作,但不仅仅要删除主题还要删除主题下的帖子、浏览量,关于这个主题的一切信息都需要进行清理。 删除流程如下,用上边概念来说,以下执行的四个流程,每个流程都必须成功否则事务回滚返回删除失败。 假设执行到了第三步后SQL执行失败了,那么第一二步都会进行回滚,第四步则不会在执行。 二、事务四大特征 事务的四大特征,原子性、一致性、隔离性、持久性。 1. 原子性 事务中所有操作要么全部成功,要么全部失败,不会存在一部分成功,一部分失败。 这个概念也是事务最核心的特性,事务概念本身就是使用原子性进行定义的。 原子性的实现是基于回滚日志实现(undo log),当事务需要回滚时就会调用回滚日志进行SQL语句回滚操作,实现数据还原。 2. 一致性 一致性,字面意思就是前后一致呗!在数据库中不管进行任何操作,都是从一个一致性转移到另一个一致性。 当事务结束后,数据库的完整性约束不被破坏。 当你了解完事务的四大特征之后就会发现,都是保证数据一致性为最终目标存在的。 在学习事务的过程中大家看到最多的案例就是转账,假设用户A与用户B余额共计1000,那么不管怎么转俩人的余额自始至终也就只有1000。 3. 隔离性 保证事务执行尽可能的不受其它事务影响,这个是隔离级别可以自行设置,在innodb中默认的隔离级别为可重复读(Repeatable Read)。 这种隔离级别有可能造成的问题就是出现幻读,但是使用间隙锁可以解决幻读问题。 学习了隔离性你需要知道原子性和持久性是针对单个事务,而隔离性是针对事务与事务之间的关系。 4. 持久性 持久性是指当事务提交之后,数据的状态就是永久的,不会因为系统崩溃而丢失。 事务持久性是基于重做日志(redo log)实现的。 三、事务并发会出现的问题 1. 脏读 读取了另一个事务没有提交的数据。 事务A 事务B 执行事务 执行事务 主题访问量从100修改到150 查询主题访问量为150 提交事务 以上表为例,事务A读取主题访问量时读取到了事务B没有提交的数据150。 如果事务B失败进行回滚,那么修改后的值还是会回到100。 然而事务A获取的数据是修改后的数据,这就有问题了。 2. 不可重复读 事务读取同一个数据,返回结果先后不一致问题。 事务A 事务B 执行事务 执行事务 查询主题访问量为100 修改主题访问量为200 提交事务 查询主题访问量为200 上表格中,事务A在先后获取主题访问量时,返回的数据不一致。 也就是说在事务A执行的过程中,访问量被其它事务修改,那么事务A查询到的结果就是不可靠的。 **脏读与不可重复读的区别** 脏读读取的是另一个事务没有提交的数据,而不可重复读读取的是另一个事务已经提交的数据。 3. 幻读 事务按照范围查询,俩次返回结果不同。 事务A 事务B 开始事务 开始事务 查询访问量100-200的主题个数为100 此时有一篇新的文章访问量达到了150 提交事务 再次查询访问量100-200的主题个数为101 以上表为例,当对100-200访问量的主题做统计时,第一次找到了100个,第二次找到了101个。 4. 区别 脏读读取的是另一个事务没有提交的数据,而不可重复读读取的是另一个事务已经提交的数据。 幻读和不可重复读都是读取了另一条已经提交的事务(这点与脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。 针对以上的三个问题,产生了四种隔离级别。 在第二节中对隔离性进行了简单的概念解释,实际上的隔离性是很复杂的。 在MySQL中定义了四种隔离级别,分别为未提交读 (Read Uncommitted)、提交读 (Read committed)、可重复读取 (Repeatable Read)、可串行化 (Serializable)。 未提交读 (Read Uncommitted):俩个事务同时运行,有一个事务修改了数据,但未提交,另一个事务是可以读取到没有提交的数据。这种情况被称之为脏读。 提交读(Read committed):一个事务在未提交之前,所做的任何操作其它事务不可见。这种隔离级别也被称之为不可重复读。因为会存在俩次同样的查询,返回的数据可能会得到不一样的结果。 可重复读(Repeatable Read):这种隔离级别解决了脏读问题,但是还是存在幻读问题,这种隔离界别在MySQL的innodb引擎中是默认级别。MySQL在解决幻读问题使用间隙锁来解决幻读问题。 可串行化 (Serializable):这种级别是最高的,强制事务进行串行执行,解决了可重复读的幻读问题。 隔离级别 脏读 不可重读读 幻读 未提交读 (Read Uncommitted) 可能发生 可能发生 可能发生 提交读(Read committed) 不可能发生 可能发生 可能发生 可重复读(Repeatable Read) 不可能发生 不可能发生 可能发生 可串行化 (Serializable) 不可能发生 不可能发生 不可能发生 对于隔离级别,级别越高并发就越低,而级别越低会引发脏读、不可重复读、幻读的问题。 因此在MySQL中使用可重复读(Repeatable Read)作为默认级别。 作为默认级别是如何解决并处理相应问题的呢! 那么针对这一问题,是一个难啃的骨头,咔咔将在下一期MVCC文章专门来介绍这块。 四、事务日志以及事务异常如何应对 MySQL的版本号为8.0 在Innodb中事务的日志分为俩种,回滚日志、重做日志。 先来看一下俩个日志的存放位置吧! 在Linux下的MySQL事务日志存放在/var/lib/mysql这个位置中。 从上图中可以看到分别为ib_logfile、undo_俩个文件。 ib_logfile文件为重做日志 undo_文件为回滚日志 在这里估计有点小伙伴会有点迷糊这个回滚日志。 那是因为在MySQL5.6默认回滚日志没有进行独立表空间存储,而是存放到了ibdata文件中。 独立表空间存储从MySQL5.6后就已经支持了,但是需要自行配置。 在MySQL8.0是由innodb_undo_tablespaces 这个参数来设置回滚日志独立空间个数,这个参数的范围为0-128。 默认值为0表示不开启独立的回滚日志,且回滚日志存储在ibdata文件中。 这个参数是在初始化数据库时指定的,实例一旦创建这个参数是不能改动的。 如果设置的innodb_undo_tablespaces 值大于实例创建时的个数,则会启动失败。 1. 重做日志(redo log)(持久性实现原理) 事务的持久性就是通过重做日志来实现的。 当提交事务之后,并不是直接修改数据库的数据的,而是先保证将相关的操作记录到redo日志中。 数据库会根据相应的机制将内存的中的脏页数据刷新到磁盘中。 上图是一个简单的重做日志写入流程。 在上图中提到俩个陌生概念,Buffer pool、redo log buffer,这个俩个都是Innodb存储引擎的内存区域的一部分。 而redo log file是位于磁盘位置。 也就说当有DML(insert、update、delete)操作时,数据会先写入Buffer pool,然后在写到重做日志缓冲区。 重做日志缓冲区会根据刷盘机制来进行写入重做日志中。 这个机制的设置参数为innodb_flush_log_at_trx_commit ,参数分别为0,1,2 上图即为重做日志的写入策略。 当这个参数的值为0的时,提交事务之后,会把数据存放到redo log buffer中,然后每秒将数据写进磁盘文件 当这个参数的值为1的时,提交事务之后,就必须把redo log buffer从内存刷入到磁盘文件里去,只要事务提交成功,那么redo log就必然在磁盘里了。 当这个参数的值为2的情况,提交事务之后,把redo log buffer日志写入磁盘文件对应的os cache缓存里去,而不是直接进入磁盘文件,1秒后才会把os cache里的数据写入到磁盘文件里去。 2. 服务器异常停止对事务如何应对(事务写入过程) 当参数为0时,前一秒的日志都保存在日志缓冲区,也就是内存上,如果机器宕掉,可能丢失1秒的事务数据。 当参数为1时,数据库对IO的要求就非常高了,如果底层的硬件提供的IOPS比较差,那么MySQL数据库的并发很快就会由于硬件IO的问题而无法提升。 当参数为2时,数据是直接写进了os cache缓存,这部分属于操作系统部分,如果操作系统部分损坏或者断电的情况会丢失1秒内的事务数据,这种策略相对于第一种就安全了很多,并且对IO要求也没有那么高。 小结 关于性能:0>2>1 关于安全:1>2>0 根据以上结论,所以说在MySQL数据库中,刷盘策略默认值为1,保证事务提交之后,数据绝对不会丢失。 3. 回滚日志(undo log)(原子性实现原理) 回滚日志保证了事务的原子性。 回滚日志相对重做日志来说没有那么复杂的流程。 当事务对数据库进行修改时,Innodb引擎不仅会记录redo log日志,还会记录undo log日志。 如果事务失败,或者执行了rollback,为了保证事务的原子性,就必须利用undo log日志来进行回滚操作。 回滚日志的存储形式如下。 在undo log日志文件,事务中使用的每条insert都对应了一条delete,每条update也都对应一条相反的update语句 注意: 系统发生宕机或者数据库进程直接被杀死。 当用户再次启动数据库进程时,还能够立刻通过查询回滚日志将之前未完成的事务进程回滚。 这也就需要回滚日志必须先于数据持久化到磁盘上,是需要先写日志后写数据库的主要原因。 回滚日志不仅仅可以保证事务的原子性,还是实现mvcc的重要因素。 以上就是关于事务的俩大日志,重做日志、回滚日志的理解。 五、锁机制 锁在MySQL中是是非常重要的一部分,锁对MySQL数据访问并发有着举足轻重的作用。 所以说锁的内容以及细节是十分繁琐的,本节只是对Innodb锁的一个大概整理。 MySQL中有三类锁,分别为行锁、表锁、页锁。 首先需要明确的是这三类锁是是归属于那种存储引擎的。 行锁:Innodb存储引擎 表锁:Myisam、MEMORY存储引擎 页锁:BDB存储引擎 1. 行锁 行锁又分为共享锁、排它锁,也被称之为读锁、写锁,Innodb存储引擎的默认锁。 共享锁(S): 假设一个事务对数据A加了共享锁(S),则这个事务只能读A的数据。 其它事务只能再对数据A添加共享锁(S),而不能添加排它锁(X),直到这个事务释放了数据A的共享锁(S)。 这就保证了其它事务也可以读取A的数据,但是在这个事务没有释放在A数据上的共享锁(S)之前不能对A做任何修改。 排它锁(X) 假设一个事务对数据A添加了排它锁(X),则只允许这个事务读取和修改数据A。 其它任何事务都不能在对数据A添加任何类型的锁,直至这个事务释放了数据A上的锁。 排它锁阻止其它事务获取相同数据的共享锁(S)、排它锁(X),直至释放排它锁(X)。 特点 只针对单一数据进行加锁 开销大 加锁慢 会出现死锁 锁粒度最小,发生锁冲突的概率越低,并发越高。 还记得在上文中提到的事务并发带来的问题、脏读、不可重读读、幻读。 学习到了这里,应该就明白可重复读(Repeatable Read)如何解决脏读、不可重读读了。 脏读、和不可重复读的解决方案很简单,写前加排它锁(X),事务结束才释放,读前加共享锁(S),事务结束就释放 2. 表锁 表锁又分为表共享读锁、表独占写锁,也被称之为读锁、写锁,Myisa存储引擎的默认锁。 表共享读锁 : 针对同一个份数据,可以同时读取互不影响,但不允许写操作。 表独占写锁 :当写操作没有结束时,会阻塞所有读和写。 特点 对整张表加锁 开销小 加锁快 无死锁 锁粒度最大,发生锁冲突的概率越大,并发越小。 本文主要说明Innodb和Myisam的锁,页锁不就不做详细说明了。 3. 如何加锁 表锁 隐式加锁:默认自动加锁释放锁,select加读锁、update、insert、delete加写锁。 手动加锁:lock table tableName read;(添加读锁)、lock table tableName write(添加写锁)。 手动解锁:unlock table tableName(释放单表)、unlock table(释放所有表) 行锁 隐式加锁:默认自动加锁释放锁,只有select不会加锁,update、insert、delete加排它锁。 手动加共享锁:select id name from user lock in share mode; 手动加排它锁:select id name form user for update; 解锁:正常提交事务(commit)、事务回滚(rollback)、kill进程。 六、总结 本文主要对事务的重点知识点进行解读,内容总结。 事务四大特征实现原理 原子性:使用事务日志的回滚日志(undo log)实现 隔离性:使用mvcc实现(幻读问题除外) 持久性:使用事务日志的重做日志(redo log)实现 一致性:是事务追求的最终目标,原子性、隔离性、持久性都是为了保证数据库一致性而存在 事务并发出现问题的区别 脏读与不可重复读的区别:脏读是读取没有提交事务的数据、不可重复读读取的是已提交事务的数据。 幻读与不可重复读的区别:都是读取的已提交事务的数据(与脏读不同),幻读针对的是一批数据,例如个数。不可重复读针对的是单一数据。 事务日志 重做日志(redo log):实现了事务的持久性,提交事务后不是直接修改数据库,而是保证每次事务操作读写入redo log中。并且落盘会有三种策略(详细看四-1节)。 回滚日志(undo log):实现了事务的原子性,针对DML的操作,都会有记录相反的DML操作。 坚持学习、坚持写博、坚持分享是咔咔从业以来一直所秉持的信念。希望在偌大互联网中咔咔的文章能带给你一丝丝帮助。我是咔咔,下期见。

优秀的个人博客,低调大师

面试官:说说你对序列化的理解

本文主要内容 背景 在Java语言中,程序运行的时候,会产生很多对象,而对象信息也只是在程序运行的时候才在内存中保持其状态,一旦程序停止,内存释放,对象也就不存在了。 怎么能让对象永久的保存下来呢?--------对象序列化 。 何为序列化和反序列化? 序列化:对象到IO数据流 反序列化:IO数据流到对象 有哪些使用场景? Java平台允许我们在内存中创建可复用的Java对象,但一般情况下,只有当JVM处于运行时,这些对象才可能存在,即,这些对象的生命周期不会比JVM的生命周期更长。但在现实应用中,就可能要求在JVM停止运行之后能够保存(持久化)指定的对象,并在将来重新读取被保存的对象。Java对象序列化就能够帮助我们实现该功能。 使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的"状态",即它的成员变量。由此可知,对象序列化不会关注类中的静态变量。 除了在持久化对象时会用到对象序列化之外,当使用RMI(远程方法调用),或在网络中传递对象时,都会用到对象序列化。 Java序列化API为处理对象序列化提供了一个标准机制,该API简单易用。 很多框架中都有用到,比如典型的dubbo框架中使用了序列化。 序列化有什么作用? 序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。 序列化实现方式 Java语言中,常见实现序列化的方式有两种: 实现Serializable接口 实现Externalizable接口 下面我们就来详细的说说这两种实现方式。 实现Serializable接口 创建一个User类实现Serializable接口 ,实现序列化,大致步骤为: 对象实体类实现Serializable 标记接口。 创建序列化输出流对象ObjectOutputStream,该对象的创建依赖于其它输出流对象,通常我们将对象序列化为文件存储,所以这里用文件相关的输出流对象 FileOutputStream。 通过ObjectOutputStream 的 writeObject()方法将对象序列化为文件。 关闭流。 以下就是code: packagecom.tian.my_code.test.clone;importjava.io.FileOutputStream;importjava.io.IOException;importjava.io.ObjectOutputStream;importjava.io.Serializable;publicclassUserimplementsSerializable{privateintage;privateStringname;publicUser(){}publicUser(intage,Stringname){this.age=age;this.name=name;} //set get省略publicstaticvoidmain(String[]args){try{ObjectOutputStreamobjectOutputStream=newObjectOutputStream(newFileOutputStream("user.txt"));Useruser=newUser(22,"老田");objectOutputStream.writeObject(user);}catch(IOExceptione){e.printStackTrace();}}} 创建一个User对象,然后把User对象保存的user.txt中了。 反序列化 大致有以下三个步骤: 创建输入流对象ObjectOutputStream。同样依赖于其它输入流对象,这里是文件输入流 FileInputStream。 通过 ObjectInputStream 的 readObject()方法,将文件中的对象读取到内存。 关闭流。 下面我们再进行反序列化code: packagecom.tian.my_code.test.clone;importjava.io.*;publicclassSeriTest{publicstaticvoidmain(String[]args){try{ObjectInputStreamois=newObjectInputStream(newFileInputStream("user.txt"));Useruser=(User)ois.readObject();System.out.println(user.getName());}catch(Exceptione){e.printStackTrace();}}} 运行这段代码,输出结果: 使用IDEA打开user.tst文件: 使用编辑器16机制查看 关于文件内容咱们就不用太关心了,继续说我们的重点。 序列化是把User对象存放到文件里了,然后反序列化就是读取文件内容并创建对象。 A端把对象User保存到文件user.txt中,B端就可以通过网络或者其他方式读取到这个文件,再进行反序列化,获得A端创建的User对象。 拓展 如果B端拿到的User属性如果有变化呢?比如说:增加一个字段 privateStringaddress; 再次进行反序列化就会报错 添加serialVersionUID packagecom.tian.my_code.test.clone;importjava.io.FileOutputStream;importjava.io.IOException;importjava.io.ObjectOutputStream;importjava.io.Serializable;publicclassUserimplementsSerializable{privatestaticfinallongserialVersionUID=2012965743695714769L;privateintage;privateStringname;publicUser(){}publicUser(intage,Stringname){this.age=age;this.name=name;}// set get省略publicstaticvoidmain(String[]args){try{ObjectOutputStreamobjectOutputStream=newObjectOutputStream(newFileOutputStream("user.txt"));Useruser=newUser(22,"老田");objectOutputStream.writeObject(user);}catch(IOExceptione){e.printStackTrace();}}} 再次执行反序列化,运行结果正常 然后我们再次加上字段和对应的get/set方法 privateStringaddress; 再次执行反序列化 反序列化成功。 如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,如“Java(TM) 对象序列化规范”中所述。 不过,强烈建议 所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID对类的详细信息具有较高的敏感性,根据编译器实现的不同可能千差万别,这样在反序列化过程中可能会导致意外的 InvalidClassException。 因此,为保证 serialVersionUID值跨不同 Java 编译器实现的一致性,序列化类必须声明一个明确的 serialVersionUID值。 强烈建议使用 private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类 -- serialVersionUID字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID值的要求。 所以,尽量显示的声明,这样序列化的类即使有字段的修改,因为 serialVersionUID的存在,也能保证反序列化成功。保证了更好的兼容性。 IDEA中如何快捷添加serialVersionUID? 我们的类实现Serializable接口,鼠标放在类上,Alt+Enter键就可以添加了。 实现Externalizable接口 通过实现Externalizable接口,必须实现writeExternal、readExternal方法。 @OverridepublicvoidwriteExternal(ObjectOutputout)throwsIOException{}@OverridepublicvoidreadExternal(ObjectInputin)throwsIOException,ClassNotFoundException{} Externalizable是Serializable的子接口。 public interface Externalizable extends java.io.Serializable { 继续使用前面的User,代码进行改造: packagecom.tian.my_code.test.clone;importjava.io.*;publicclassUserimplementsExternalizable{privateintage;privateStringname;publicUser(){}publicUser(intage,Stringname){this.age=age;this.name=name;}//setgetpublicstaticvoidmain(String[]args){try{ObjectOutputStreamobjectOutputStream=newObjectOutputStream(newFileOutputStream("user.txt"));Useruser=newUser(22,"老田");objectOutputStream.writeObject(user);}catch(IOExceptione){e.printStackTrace();}}@OverridepublicvoidwriteExternal(ObjectOutputout)throwsIOException{//将name反转后写入二进制流StringBufferreverse=newStringBuffer(name).reverse();out.writeObject(reverse);out.writeInt(age);}@OverridepublicvoidreadExternal(ObjectInputin)throwsIOException,ClassNotFoundException{//将读取的字符串反转后赋值给name实例变量this.name=((StringBuffer)in.readObject()).reverse().toString();//将读取到的int类型值付给agethis.age=in.readInt();}} 执行序列化,然后再次执行反序列化,输出: 注意 Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供public的无参构造器,因为在反序列化的时候需要反射创建对象。 两种方式对比 下图为两种实现方式的对比: 序列化只有两种方式吗? 当然不是。根据序列化的定义,不管通过什么方式,只要你能把内存中的对象转换成能存储或传输的方式,又能反过来恢复它,其实都可以称为序列化。因此,我们常用的Fastjson、Jackson等第三方类库将对象转成Json格式文件,也可以算是一种序列化,用JAXB实现XML格式文件输出,也可以算是序列化。所以,千万不要被思维局限,其实现实当中我们进行了很多序列化和反序列化的操作,涉及不同的形态、数据格式等。 序列化算法 所有保存到磁盘的对象都有一个序列化编码号。 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出。 如果此对象已经序列化过,则直接输出编号即可。 自定义序列化 有些时候,我们有这样的需求,某些属性不需要序列化。使用transient关键字选择不需要序列化的字段。 继续使用前面的代码进行改造,在age字段上添加transient修饰: packagecom.tian.my_code.test.clone;importjava.io.FileOutputStream;importjava.io.IOException;importjava.io.ObjectOutputStream;importjava.io.Serializable;publicclassUserimplementsSerializable{privatetransientintage;privateStringname;publicUser(){}publicUser(intage,Stringname){this.age=age;this.name=name;}publicintgetAge(){returnage;}publicvoidsetAge(intage){this.age=age;}publicStringgetName(){returnname;}publicvoidsetName(Stringname){this.name=name;}publicstaticvoidmain(String[]args){try{ObjectOutputStreamobjectOutputStream=newObjectOutputStream(newFileOutputStream("user.txt"));Useruser=newUser(22,"老田");objectOutputStream.writeObject(user);}catch(IOExceptione){e.printStackTrace();}}}```序列化,然后进行反序列化:```javapackagecom.tian.my_code.test.clone;importjava.io.*;publicclassSeriTest{publicstaticvoidmain(String[]args){try{ObjectInputStreamois=newObjectInputStream(newFileInputStream("user.txt"));Useruser=(User)ois.readObject();System.out.println(user.getName());System.out.println(user.getAge());}catch(Exceptione){e.printStackTrace();}}} 运行输出: 从输出我们看到,使用transient修饰的属性,Java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。 对于引用类型,值是null;基本类型,值是0;boolean类型,值是false。 探索 到此序列化内容算讲完了,但是,如果只停留在这个层面,是无法应对实际工作中的问题的。 比如模型对象持有其它对象的引用怎么处理,引用类型如果是复杂些的集合类型怎么处理? 上面的User中持有String引用类型的,照样序列化没问题,那么如果是我们自定义的引用类呢? 比如下面的场景: packagecom.tian.my_code.test.clone;publicclassUserAddress{privateintprovinceCode;privateintcityCode;publicUserAddress(){}publicUserAddress(intprovinceCode,intcityCode){this.provinceCode=provinceCode;this.cityCode=cityCode;}publicintgetProvinceCode(){returnprovinceCode;}publicvoidsetProvinceCode(intprovinceCode){this.provinceCode=provinceCode;}publicintgetCityCode(){returncityCode;}publicvoidsetCityCode(intcityCode){this.cityCode=cityCode;}} 然后在User中添加一个UserAddress的属性: packagecom.tian.my_code.test.clone;importjava.io.FileOutputStream;importjava.io.IOException;importjava.io.ObjectOutputStream;importjava.io.Serializable;publicclassUserimplementsSerializable{privatestaticfinallongserialVersionUID=-2445226500651941044L;privateintage;privateStringname;privateUserAddressuserAddress;publicUser(){}publicUser(intage,Stringname){this.age=age;this.name=name;}//getsetpublicstaticvoidmain(String[]args){try{ObjectOutputStreamobjectOutputStream=newObjectOutputStream(newFileOutputStream("user.txt"));Useruser=newUser(22,"老田");UserAddressuserAddress=newUserAddress(10001,10001001);user.setUserAddress(userAddress);objectOutputStream.writeObject(user);}catch(IOExceptione){e.printStackTrace();}}} 运行上面代码: 抛出了 java.io.NotSerializableException 异常。很明显在告诉我们,UserAddress没有实现序列化接口。待UserAddress类实现序列化接口后: packagecom.tian.my_code.test.clone;importjava.io.Serializable;publicclassUserAddressimplementsSerializable{privatestaticfinallongserialVersionUID=5128703296815173156L;privateintprovinceCode;privateintcityCode;publicUserAddress(){}publicUserAddress(intprovinceCode,intcityCode){this.provinceCode=provinceCode;this.cityCode=cityCode;}//getset} 再次运行,正常不报错了。 反序列化代码: packagecom.tian.my_code.test.clone;importjava.io.*;publicclassSeriTest{publicstaticvoidmain(String[]args){try{ObjectInputStreamois=newObjectInputStream(newFileInputStream("user.txt"));Useruser=(User)ois.readObject();System.out.println(user.getName());System.out.println(user.getAge());System.out.println(user.getUserAddress().getProvinceCode());System.out.println(user.getUserAddress().getCityCode());}catch(Exceptione){e.printStackTrace();}}} 运行结果: 典型运用场景 publicfinalclassStringimplementsjava.io.Serializable,Comparable<String>,CharSequence{privatestaticfinallongserialVersionUID=-6849794470754667710L;}publicclassHashMap<K,V>extendsAbstractMap<K,V>implementsMap<K,V>,Cloneable,Serializable{privatestaticfinallongserialVersionUID=362498820763181265L;}publicclassArrayList<E>extendsAbstractList<E>implementsList<E>,RandomAccess,Cloneable,java.io.Serializable{privatestaticfinallongserialVersionUID=8683452581122892189L;}..... 很多常用类都实现了序列化接口。 再次拓展 上面说的transient 反序列化的时候是默认值,但是你会发现,几种常用集合类ArrayList、HashMap、LinkedList等数据存储字段,竟然都被 transient 修饰了,然而在实际操作中我们用集合类型存储的数据却可以被正常的序列化和反序列化? 真相当然还是在源码里。实际上,各个集合类型对于序列化和反序列化是有单独的实现的,并没有采用虚拟机默认的方式。这里以 ArrayList中的序列化和反序列化源码部分为例分析: privatevoidwriteObject(java.io.ObjectOutputStreams)throwsjava.io.IOException{intexpectedModCount=modCount;//序列化当前ArrayList中非transient以及非静态字段s.defaultWriteObject();//序列化数组实际个数s.writeInt(size);//逐个取出数组中的值进行序列化for(inti=0;i<size;i++){s.writeObject(elementData[i]);}//防止在并发的情况下对元素的修改if(modCount!=expectedModCount){thrownewConcurrentModificationException();}}privatevoidreadObject(java.io.ObjectInputStreams)throwsjava.io.IOException,ClassNotFoundException{elementData=EMPTY_ELEMENTDATA;//反序列化非transient以及非静态修饰的字段,其中包含序列化时的数组大小sizes.defaultReadObject();//忽略的操作s.readInt();//ignoredif(size>0){//容量计算intcapacity=calculateCapacity(elementData,size);SharedSecrets.getJavaOISAccess().checkArray(s,Object[].class,capacity);//检测是否需要对数组扩容操作ensureCapacityInternal(size);Object[]a=elementData;//按顺序反序列化数组中的值for(inti=0;i<size;i++){a[i]=s.readObject();}}} 读源码可以知道,ArrayList的序列化和反序列化主要思路就是根据集合中实际存储的元素个数来进行操作,这样做估计是为了避免不必要的空间浪费(因为ArrayList的扩容机制决定了,集合中实际存储的元素个数肯定比集合的可容量要小)。为了验证,我们可以在单元测试序列化和返序列化的时候,在ArrayLIst的两个方法中打上断点,以确认这两个方法在序列化和返序列化的执行流程中(截图为反序列化过程): 原来,我们之前自以为集合能成功序列化也只是简单的实现了标记接口都只是表象,表象背后有各个集合类有不同的深意。所以,同样的思路,读者朋友可以自己去分析下 HashMap以及其它集合类中自行控制序列化和反序列化的个中门道了,感兴趣的小伙伴可以自行去查看一番。 序列化注意事项 1、序列化时,只对对象的状态进行保存,而不管对象的方法; 2、当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口; 3、当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化; 4、并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如: 安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行RMI传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的; 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现; 5、声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对象的临时数据。 6、序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途: 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID; 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。 7、Java有很多基础类已经实现了serializable接口,比如String,Vector等。但是也有一些没有实现serializable接口的; 8、如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因; 总结 什么是序列化?序列化Java中常用实现方式有哪些?两种实现序列化方式的对比,序列化算法?如何自定义序列化?Java集合框架中序列化是如何实现的? 这几个点如果没有get到,麻烦请再次阅读,或者加我微信进群里大家一起聊。

优秀的个人博客,低调大师

面试题:spring事务失效的9大原因

欢迎关注公众号【sharedCode】致力于主流中间件的源码分析, 个人网站:https://www.shared-code.com/ 1.spring事务实现方式及原理 Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring 是无法提供事务功能的。真正的数据库层的事务提交和回滚是在binlog提交之后进行提交的 通过 redo log 来重做, undo log来回滚。 一般我们在程序里面使用的都是在方法上面加@Transactional 注解,这种属于声明式事务。 声明式事务本质是通过 AOP 功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。 2.数据库本身不支持事务 这里以 MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB 3.当前类的调用 @Service public class UserServiceImpl implements UserService { public void update(User user) { updateUser(user); } @Transactional(rollbackFor = Exception.class) public void updateUser(User user) { // update user } } 上面的这种情况下是不会有事务管理操作的。 通过看声明式事务的原理可知,spring使用的是AOP切面的方式,本质上使用的是动态代理来达到事务管理的目的,当前类调用的方法上面加@Transactional 这个是没有任何作用的,因为调用这个方法的是this. OK, 我们在看下面的一种例子。 @Service public class UserServiceImpl implements UserService { @Transactional(rollbackFor = Exception.class) public void update(User user) { updateUser(user); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateUser(User user) { // update user } } 这次在 update 方法上加了 @Transactional,updateUser 加了 REQUIRES_NEW 新开启一个事务,那么新开的事务管用么? 答案是:不管用! 因为它们发生了自身调用,就调该类自己的方法,而没有经过 Spring 的代理类,默认只有在外部调用事务才会生效,这也是老生常谈的经典问题了。 4.方法不是public的 @Service public class UserServiceImpl implements UserService { @Transactional(rollbackFor = Exception.class) private void updateUser(User user) { // update user } } private 方法是不会被spring代理的,因此是不会有事务产生的,这种做法是无效的。 5.没有被spring管理 //@Service public class UserServiceImpl implements UserService { @Transactional(rollbackFor = Exception.class) public void updateUser(User user) { // update user } } 没有被spring管理的bean, spring连代理对象都无法生成,当然无效咯。 6.配置的事务传播性有问题 @Service public class UserServiceImpl implements UserService { @Transactional(propagation = Propagation.NOT_SUPPORTED) public void update(User user) { // update user } } 回顾一下spring的事务传播行为 Spring 事务的传播行为说的是,当多个事务同时存在的时候, Spring 如何处理这些事务的行为。 PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。 PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行 PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。 PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。 PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 PROPAGATION_NEVER: 以非事务方式执行,如果当前存在事务,则抛出异常。 PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按 REQUIRED 属性执行 当传播行为设置了PROPAGATION_NOT_SUPPORTED,PROPAGATION_NEVER,PROPAGATION_SUPPORTS这三种时,就有可能存在事务不生效 7.异常被你 "抓住"了 @Service public class UserServiceImpl implements UserService { @Transactional(rollbackFor = Exception.class) public void update(User user) { try{ // update user }catch(Execption e){ log.error("异常",e) } } } 异常被抓了,这样子代理类就没办法知道你到底有没有错误,需不需要回滚,所以这种情况也是没办法回滚的哦。 8.接口层声明式事务使用cglib代理 public interface UserService { @Transactional(rollbackFor = Exception.class) public void update(User user) } @Service public class UserServiceImpl implements UserService { public void update(User user) { // update user } } 通过元素的 "proxy-target-class" 属性值来控制是基于接口的还是基于类的代理被创建。如果 "proxy-target-class" 属值被设置为 "true",那么基于类的代理将起作用(这时需要CGLIB库cglib.jar在CLASSPATH中)。如果 "proxy-target-class" 属值被设置为 "false" 或者这个属性被省略,那么标准的JDK基于接口的代理将起作用 注解@Transactional cglib与java动态代理最大区别是代理目标对象不用实现接口,那么注解要是写到接口方法上,要是使用cglib代理,这是注解事务就失效了,为了保持兼容注解最好都写到实现类方法上。 9.rollbackFor异常指定错误 @Service public class UserServiceImpl implements UserService { @Transactional public void update(User user) { // update user } } 上面这种没有指定回滚异常,这个时候默认的回滚异常是RuntimeException ,如果出现其他异常那么就不会回滚事务

优秀的个人博客,低调大师

那些年面试官问你的红黑树----->(浅谈)

在了解红黑树之前,先要了解二叉树,又叫二叉查找树、二叉搜索树、二叉排序树。二叉树顾名思义: 是一种每个节点最多有两个子节点的树,同时遵循 左节点的值<父节点的值<右节点的值 这样的规律。 二叉树有如下几个特点: 节点的左子节点小于节点本身 节点的右子节点大于节点本身 左右节点同样为二叉搜索树 下图就是一棵典型的二叉树: 它是一种查找次数小于等于树高的数据结构。如图中树有4层,即树高为4,当我们需要查找8时,经过的路线是这样的: 1、8<9,往左查找 2、8>5,往右查找 3、8>7,往右查找 4、8=8,找到结果 总共查找4次,等于树高。这棵树不管怎么找,查找次数总是小于等于树高。 二叉树的插入同样遵循上述规则,会一步一步对比,从而找到插入的位置,可以想象上图中的8不存在,而是需要插入一个8,结果与上述路线一致。 二叉树的删除会涉及到无子节点和有子节点两种情况,无子节点直接删除即可,有子节点会需要用左边最大值或右边最小值替换当前删除节点,具体不细聊。 当二叉树插入数值不均衡时,会出现树结构的变形与查找性能的损耗,比如现在二叉树有8,9,12三个值,然后需要插入7,6,5,4,3这五个值时,产生的结构如下图所示: 树高会不合理的增高,查找效率也无法得到保证。红黑树就是为了解决这种情况而诞生的。 红黑树又称为自平衡二叉树,它符合二叉树的规则同时比它的规则更加的复杂,具体规则如下: 1、所有节点都是红色或黑色 2、根节点为黑色 3、所有叶子都是黑色(NIL节点) 4、每个红色节点必须有两个黑色的子节点。(不能有两个连续的红色节点。) 5、从任一节点到其每个叶子的所有简单路径(不要回退)都包含相同数目的黑色节点。 具体样子如下图所示: 解释一下几个规则的含义: 1和2很好理解,节点都是红和黑,根节点是黑色的。 3所有叶子都是黑色的,叶子与叶子节点是两个概念,叶子不是一个节点,可以理解为没有数值的空节点,也就是图中的NIL。 4也很好理解,红色节点的子节点都是黑色的,这就保证了没有两个连续的红色节点。 5稍微解释一下,简单路径就是说一次到底,不要回退,叶子就是NIL,比如从8这个节点出发,不管是去1下面的叶子,11下面的叶子,还是6下面的叶子,都是经过一个黑色节点,从任何节点出发都是一样的规则,经过相同数量的黑色节点。 以上5个规则,加上二叉树的规则,就组成了红黑树这一极具特点的树型数据结构。

资源下载

更多资源
优质分享App

优质分享App

近一个月的开发和优化,本站点的第一个app全新上线。该app采用极致压缩,本体才4.36MB。系统里面做了大量数据访问、缓存优化。方便用户在手机上查看文章。后续会推出HarmonyOS的适配版本。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。