/**
* Spring事务有四种特性(ACID):
* 1、原子性(Atomicity): 事务是不可分割的一组逻辑操作
* 2、一致性(Consistency):事务执行前后,数据的完整性保持一致
* 3、隔离性(Isolation): 事务执行中,不应该受到其它事务打扰
* 4、持久性(Durability): 事务结束后,数据就永久记录到数据库
*/
/**
* Spring事务有四种隔离级别:
* 1、READ_UNCOMMITTED(read_uncommitted/读未提交):事务可以读取到-其它事务未提交的数据修改
* 对应并发一致性问题:脏读、不可重复读、幻[影]读
* 2、READ_COMMITTED(read_committed/读已提交): 事务只能读取到-其它事务提交后的数据修改
* 对应并发一致性问题:不可重复读、幻[影]读
* 3、REPEATABLE_READ(repeatable_read/可重复读): 同一事务可以重复读取数据,其结果一致(默认隔离级别)
* 对应并发一致性问题:幻[影]读
* 原理是在事务启动时生成快照,所以可以多次读取结果一致,对要Update的行加写锁(排它锁)
* 4、SERIALIZABLE(serializable/序列化读): 事务串行化执行,不会出现以上并发一致性问题
* 对应并发一致性问题:无
* 原理是对要读取的行加读锁(共享锁),对要Update/Insert/Delete的表加写锁(排它锁),所以很容易出现死锁(读写锁互斥)
*
* 数据库三个并发数据一致性问题:
* 1、脏读: 读取到的数据不是真实数据库持久化后的数据(其它事务未提交的修改)
* 2、不可重复读:同一事务中重复读取到的数据不一致(并发的写事务在中间修改了数据)
* 针对的是Update操作,基于已提交的事务
* 3、幻[影]读: 本质和不可重复读一样(解释同上2、)
* 针对的是Insert/Delete操作(如用户注册),基于已提交的事务
*
* 其中,
* 1、事务的隔离级别越高(封锁粒度越大),对应系统吞吐量越低(系统并发性能越低)
* 2、若数据库和Spring配置的隔离级别不同,则按Spring配置的执行
* 3、MySQL的默认InnoDB存储引擎隔离级别是基于MVCC(Multi-Version Concurrency Control/多版本并发控制)实现的
*/
/**
* Spring事务有七种传播机制:
* 1、REQUIRED(required/需要): 当前方法-|有事务则加入|没事务则新建|-(默认传播机制)
* 常用于写业务[Update/Insert/Delete]中
* 2、SUPPORTS(supports/支持): 当前方法-|有事务则加入|没事务则不要|-
* 常用于读业务[Select]中
* 3、MANDATORY(mandatory/强制): 当前方法-|有事务则加入|没事务则异常|-
* 4、REQUIRED_NEW(required_new/需要新建): 当前方法-|新建事务 |有事务则挂起|-
* 5、NOT_SUPPORTED(not_supports/不要支持):当前方法-|不要事务 |有事务则挂起|-
* 6、NEVER(never/从不): 当前方法-|不要事务 |有事务则异常|-
* 7、NESTED(nested/嵌入): 当前方法-|有事务则嵌入|没事务则新建|-
*
* 使用场景:
* 业务层Service中,方法A和方法B本身都已经被添加了事务控制,在A调用B时需要处理的问题
* 传播机制是相较于方法B来理解的操作
*/
/**
* Spring事务失效的五种情况:
* 1、发生自调用this.xxx()/xxx(),此时this对象不是代理类,而是对象本身
* -因为Spring事务是通过AOP切面增强生成代理对象实现的
* -解决方法:通过Autowired注入AOP增强后的代理对象调用/获取本代理类调用(Xxx)AopContext.currentProxy().xxx()
* -另外:可通过AopUtils.isAopProxy(xxx)判断该对象是否代理类
* 2、事务声明的方法不是public的
* -解决方法:方法修饰符改为public/不用Spring事务默认的代理对象实现,改为开启AspectJ代理模式
* 3、数据库不支持事务,如:MySQL使用的是MyISAM存储引擎
* -因为Spring事务是基于数据库事务实现的
* -解决方法:MySQL的存储引擎改回默认的InnoDB
* 4、声明事务方法的对象没有被Spring管理,即对象没有放到Spring的IOC容器中
* 5、异常被捕获处理了,本来发生异常Spring事务会回滚,但是捕获后就不回滚失效了
*/
/**
* InnoDB和MyISM存储索引的区别:
* 1、InnoDB支持事务,MyISM不支持
* 2、InnoDB一定要有主键,MyISM不一定要有
* InnoDB主键设置:自定义主键ID-no->唯一索引-no->默认6B字节的ROW_ID
* 3、InnoDB支持外键,MyISM不支持
* 4、InnoDB既有聚簇索引也有非聚簇索引,MyISM仅有非聚簇索引
* InnoDB为了减少数据冗余,多个索引时,其它索引存储的是主键索引的Key值,主键索引存储的是数据
* MyISM所有索引均指向数据地址
* 5、InnoDB不存储行数,MyISM存储行数
* InnoDB中同一时刻,不同事务中有不同行数
* 6、InnoDB支持行锁和表锁,MyISM仅支持表锁
* InnoDB默认行锁(锁该行数据),MyISM默认表锁(锁该表数据)
* 7、InnoDB存储文件为xxx.frm(form/表结构)+xxx.ibd(index+data/索引+数据),
* MyISM存储文件为xxx.frm(form/表结构)+xxx.myi(index/索引)+xxx.myd(data/数据)
*
* 聚簇索引和非聚簇索引的区别:
* 1、聚簇索引的索引和数据是一起存储的
* 2、非聚簇索引的索引和数据是分开存储的
*
* 行锁和表锁的区别:
* 1、行锁加锁麻烦,表锁加锁容易
* 2、行锁颗粒小,表锁颗粒大
* 3、行锁不容易冲突,表锁容易冲突
* 4、行锁并发高,表锁并发低
*
* 乐观锁和悲观锁的区别:
* 1、乐观锁(Optimistic Lock)是在操作数据前判断有无其它线程更新这个数据,无则不加锁操作数据
* 适用于读的情况,类似共享锁(S锁/Shared Lock/读锁/Read Lock)
* 例:数据版本/快照记录机制、CAS(Compare And Swap)锁(修改前比较相等则交换否则自旋等待即循环)、
* JUC(java.util.concurrent)并发工具包下的AtomicXxx类(底层基于CAS实现)
* 2、悲观锁(Pessimistic Lock)是每次操作数据时都会加锁
* 适用于写的情况,类似排他锁(X锁/Exclusive Lock/写锁/Write Lock)
* 例:synchronized关键字、JUC(java.util.concurrent)并发工具包下的Lock接口实现类(ReentrantLock类等)
*
* Synchronized锁的膨胀升级过程:
* 无锁状态->偏向锁->轻量级锁->重量级锁
* -原理是根据当前线程竞争激烈程度升级从而调优性能而不是自始至终都是重量级锁
* -锁对象被创建出来时是无锁状态
* -当有一个线程获得该锁对象时升级为偏向锁(即偏向该对象重复获取该锁)
* -当有线程获取该锁而不是已获得该锁(说明请求锁线程数大于一个)时升级为轻量级锁(即CAS乐观锁:等待线程进行自旋,默认阈值是10次)
* -当等待线程自旋次数超过阈值(说明线程竞争激烈程度严重)时升级为重量级锁(即Synchronized悲观锁:挂起该线程以减轻竞争激烈程度)
*/
/**
* B树和B+树的相同:
* 1、一个节点(页)可以存储多个索引或数据
* 因此B树和B+树的树高(深度)不会太高
* 2、每个节点中多个索引是有序的(升序)
* 因此每个节点匹配索引时使用二分查找
*
* B树和B+树的区别:
* 1、B树所有节点既存储索引又存储数据,B+树非叶子节点存储索引叶子节点存储数据
* 因此B+树比B树访问数据效率更为稳定
* 2、B树叶子节点间没有关系,B+树叶子节点间有引用链路
* 因此B+数树比B树访问数据效率更高,可以更好支持全表扫描,范围查询等SQL
*
* 存储记录数计算公式:节点指针数[分支数/索引数]^(层数-1)*(页大小/每条数据大小)[每个节点数据条数]
* 节点指针数[分支数/索引数]=页大小/索引大小[主键ID大小+默认指针大小]
* 其中:页大小=16KB(千字节)=16*1024=16384B(字节),默认指针大小6B(字节)
* 常规例子:用雪花算法做主键ID(64Bit=8Byte)的每条1KB数据,三层可以存储的数据量
* count=16384/(8+6)^(3-1)*(16/1)=21902400(约可以存储2.2千万条该数据)
*
* 说明:
* 1、B树/B+树每个节点就是一个页(最小单位)
* 2、对于三层B+树,查询每条数据只需查询三个节点,也就是到磁盘进行三次IO(加载三页到内存)
* 3、把每条数据所在页加载到内存后再匹配目标索引/结果
* 4、因此在B+树中,树高一致的话,千万级和十万级数据量,查询效率相差不大
*/
package transaction;
import autowired.SubGenericityApplication;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import transaction.mapper.RollbackMapper;
import transaction.pojo.Rollback;
import java.util.List;
/**
* 事务测试
* 1、该事务类要被Spring管理(需启动Web容器)
*
* @author Chenghe Xu
* @date 2022/12/11 16:12
*/
@Component
public class RollbackTest01 {
private final RollbackMapper rollbackMapper;
@Autowired
public RollbackTest01(RollbackMapper rollbackMapper) {
this.rollbackMapper = rollbackMapper;
}
/**
* 启动mysql测试
*/
public void test() {
System.out.println(rollbackMapper);
Integer res = rollbackMapper.baseInsertRollback(new Rollback(88, "小欧", 21));
System.out.println("插入数据执行结果" + res);
List<Rollback> rollbackList = rollbackMapper.baseSelectRollback(null);
System.out.println("查询数据执行结果" + rollbackList);
}
/**
* 不加@Transactional事务注解,不会回滚
* 不处理异常,会终止程序,只插入5条
*/
public void test1() {
for (int i = 1; i <= 10; i++) {
Integer res1 = rollbackMapper.baseInsertRollback(new Rollback(null, "1小滚" + i, 21));
System.out.println("插入数据-" + i + "-执行结果" + res1);
if (i == 5) {
int x = i / 0;
}
}
System.out.println("==执行结束==");
}
/**
* 不加@Transactional事务注解,不会回滚
* 外部处理异常,会终止循环,只插入5条
*/
public void test2() {
try {
for (int i = 1; i <= 10; i++) {
Integer res1 = rollbackMapper.baseInsertRollback(new Rollback(null, "2小滚" + i, 21));
System.out.println("插入数据-" + i + "-执行结果" + res1);
if (i == 5) {
int x = i / 0;
}
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("==执行结束==");
}
/**
* 不加@Transactional事务注解,不会回滚
* 内部处理异常,不会终止循环,会插入10条
*/
public void test3() {
for (int i = 1; i <= 10; i++) {
Integer res1 = rollbackMapper.baseInsertRollback(new Rollback(null, "3小滚" + i, 21));
System.out.println("插入数据-" + i + "-执行结果" + res1);
if (i == 5) {
try {
int x = i / 0;
} catch (Exception e) {
e.printStackTrace();
}
}
}
System.out.println("==执行结束==");
}
/**
* 加@Transactional事务注解
* 默认是运行时异常回滚,非运行时异常不回滚,不插入
*/
@Transactional
public void test4() {
for (int i = 1; i <= 10; i++) {
Integer res1 = rollbackMapper.baseInsertRollback(new Rollback(null, "4小滚" + i, 21));
System.out.println("插入数据-" + i + "-执行结果" + res1);
if (i == 5) {
int x = i / 0;
}
}
System.out.println("==执行结束==");
}
/**
* 加@Transactional事务注解
* 设置rollbackFor = Exception.class,所有异常都回滚,不插入
*/
@Transactional(rollbackFor = Exception.class)
public void test5() {
for (int i = 1; i <= 10; i++) {
Integer res1 = rollbackMapper.baseInsertRollback(new Rollback(null, "5小滚" + i, 21));
System.out.println("插入数据-" + i + "-执行结果" + res1);
if (i == 5) {
int x = i / 0;
}
}
System.out.println("==执行结束==");
}
/**
* 加@Transactional事务注解
* 设置noRollbackFor = Exception.class,所有异常都不回滚,插入五条
*/
@Transactional(noRollbackFor = Exception.class)
public void test6() {
for (int i = 1; i <= 10; i++) {
Integer res1 = rollbackMapper.baseInsertRollback(new Rollback(null, "6小滚" + i, 21));
System.out.println("插入数据-" + i + "-执行结果" + res1);
if (i == 5) {
int x = i / 0;
}
}
System.out.println("==执行结束==");
}
/**
* 加@Transactional事务注解
* 捕获异常,不回滚,插入五条
*/
@Transactional
public void test7() {
try {
for (int i = 1; i <= 10; i++) {
Integer res1 = rollbackMapper.baseInsertRollback(new Rollback(null, "2小滚" + i, 21));
System.out.println("插入数据-" + i + "-执行结果" + res1);
if (i == 5) {
int x = i / 0;
}
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("==执行结束==");
}
/**
* 加@Transactional事务注解
* 捕获异常,手动回滚,不插入
*/
@Transactional
public void test8() {
try {
for (int i = 1; i <= 10; i++) {
Integer res1 = rollbackMapper.baseInsertRollback(new Rollback(null, "2小滚" + i, 21));
System.out.println("插入数据-" + i + "-执行结果" + res1);
if (i == 5) {
int x = i / 0;
}
}
} catch (Exception e) {
e.printStackTrace();
//手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
System.out.println("==执行结束==");
}
}
@SpringBootTest(classes = SubGenericityApplication.class)
class Test01 {
private final RollbackTest01 rollbackTest01;
@Autowired
public Test01(RollbackTest01 rollbackTest01) {
this.rollbackTest01 = rollbackTest01;
}
/**
* 注:是org.junit.jupiter.api.Test的@Test
*/
@Test
public void test() {
System.out.println(rollbackTest01);
// rollbackTest01.test();
// rollbackTest01.test1();
// rollbackTest01.test2();
// rollbackTest01.test3();
// rollbackTest01.test4();
// rollbackTest01.test5();
// rollbackTest01.test6();
// rollbackTest01.test7();
// rollbackTest01.test8();
}
}
package com.xch.mvc;
import cn.hutool.core.date.DateUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 并发模拟测试(发现问题并修复)
* -更新num 不可重复读的并发一致性问题
*
* @author XuChenghe
* @date 2023/3/14 9:34
*/
@RestController
public class ConcurrenceSimulationTest {
@Autowired
private ConcurrenceSimulationServiceImpl concurrenceSimulationService;
@RequestMapping("/increase")
public String incrementController() {
for (int threadId = 1; threadId <= 10; threadId++) {
new Thread(concurrenceSimulationService::incrementTest7).start();
}
return "[SUCCESS] " + DateUtil.format(DateUtil.date(), "yyyy-MM-dd HH:mm:ss.SSS");
}
}
@Service
class ConcurrenceSimulationServiceImpl {
@Autowired
private ConcurrenceSimulationRepository concurrenceSimulationRepository;
/**
* 不加事务,不加同步锁
* 会出现并发一致性的问题
* 原因:多个线程同一时刻执行修改操作
*
* |Thread-3 |0->1 |
* |Thread-11 |0->1 |
* |Thread-10 |0->1 |
* |Thread-6 |0->1 |
* |Thread-5 |0->1 |
* |Thread-2 |0->1 |
* |Thread-4 |0->1 |
* |Thread-9 |0->1 |
* |Thread-7 |0->1 |
* |Thread-8 |0->1 |
*/
public void incrementTest1() {
Integer num = concurrenceSimulationRepository.selectNum(123);
ConcurrenceSimulation concurrenceSimulation = new ConcurrenceSimulation(123, num + 1);
concurrenceSimulationRepository.updateNum(concurrenceSimulation);
System.out.println("|" + Thread.currentThread().getName() + "\t|" + num + "->" + (num + 1) + "\t|");
}
/**
* 加默认事务,不加同步锁
* 会出现并发一致性的问题,并发吞吐量有所下降
* 原因:默认可重复读的隔离性,仅是在Update数据时锁定行
* 所以只能确保有序写入,但是没法确保写入的正确性
*
* |Thread-7 |0->1 |
* |Thread-4 |0->1 |
* |Thread-11 |0->1 |
* |Thread-9 |0->1 |
* |Thread-6 |0->1 |
* |Thread-3 |0->1 |
* |Thread-2 |1->2 |
* |Thread-8 |1->2 |
* |Thread-5 |1->2 |
* |Thread-10 |1->2 |
*/
@Transactional
public void incrementTest2() {
Integer num = concurrenceSimulationRepository.selectNum(123);
ConcurrenceSimulation concurrenceSimulation = new ConcurrenceSimulation(123, num + 1);
concurrenceSimulationRepository.updateNum(concurrenceSimulation);
System.out.println("|" + Thread.currentThread().getName() + "\t|" + num + "->" + (num + 1) + "\t|");
}
/**
* 加序列化事务,不加同步锁
* 直接形成死锁异常org.springframework.dao.DeadlockLoserDataAccessException
* 原因:序列化事务添加读写锁竞争导致,Select时多个事务添加了读锁,Update时无法再添加写锁
* 均进入阻塞等待,从而后续线程均无法Update写入,形成死锁
*
* 其中,读锁是共享锁,写锁是排他锁;
* 所以读锁可以同时存在,写锁不能同时存在,读写锁也不能同时存在
*/
@Transactional(isolation = Isolation.SERIALIZABLE)
public void incrementTest3() {
Integer num = concurrenceSimulationRepository.selectNum(123);
ConcurrenceSimulation concurrenceSimulation = new ConcurrenceSimulation(123, num + 1);
concurrenceSimulationRepository.updateNum(concurrenceSimulation);
System.out.println("|" + Thread.currentThread().getName() + "\t|" + num + "->" + (num + 1) + "\t|");
}
/**
* 不加事务,加同步锁
* 不会出现并发一致性问题
* 原因:非静态方法加同步锁synchronized,锁定的是方法的调用者,所以不会并发执行
*
* |Thread-10 |0->1 |
* |Thread-7 |1->2 |
* |Thread-9 |2->3 |
* |Thread-11 |3->4 |
* |Thread-6 |4->5 |
* |Thread-2 |5->6 |
* |Thread-3 |6->7 |
* |Thread-8 |7->8 |
* |Thread-4 |8->9 |
* |Thread-5 |9->10 |
*/
public synchronized void incrementTest4() {
Integer num = concurrenceSimulationRepository.selectNum(123);
ConcurrenceSimulation concurrenceSimulation = new ConcurrenceSimulation(123, num + 1);
concurrenceSimulationRepository.updateNum(concurrenceSimulation);
System.out.println("|" + Thread.currentThread().getName() + "\t|" + num + "->" + (num + 1) + "\t|");
}
/**
* 先加默认事务,后加同步锁
* 会出现并发一致性的问题
* 原因:事务的底层是通过动态代理后反射调用本方法的,由于事务在方法的同步锁外面
* 所以方法执行完了同步锁,释放资源了,事务还没提交,下一个线程可以继续进入同步锁的方法
* 读取到的数据就不是最新的数据
*
* |Thread-8 |0->1 |
* |Thread-4 |0->1 |
* |Thread-6 |1->2 |
* |Thread-11 |1->2 |
* |Thread-7 |2->3 |
* |Thread-10 |2->3 |
* |Thread-5 |3->4 |
* |Thread-9 |3->4 |
* |Thread-3 |4->5 |
* |Thread-2 |4->5 |
*/
@Transactional
public synchronized void incrementTest5() {
Integer num = concurrenceSimulationRepository.selectNum(123);
ConcurrenceSimulation concurrenceSimulation = new ConcurrenceSimulation(123, num + 1);
concurrenceSimulationRepository.updateNum(concurrenceSimulation);
System.out.println("|" + Thread.currentThread().getName() + "\t|" + num + "->" + (num + 1) + "\t|");
}
/**
* 先加同步锁,后加默认事务
* 不会出现并发一致性问题
* 原因:由于同步锁在事务外面,所以同步锁的资源释放,需要等待事务提交
*
* |Thread-6 |0->1 |
* |Thread-2 |1->2 |
* |Thread-3 |2->3 |
* |Thread-8 |3->4 |
* |Thread-11 |4->5 |
* |Thread-4 |5->6 |
* |Thread-5 |6->7 |
* |Thread-7 |7->8 |
* |Thread-10 |8->9 |
* |Thread-9 |9->10 |
*/
public synchronized void incrementTest6() {
transactionalIncrementMethod();
}
@Transactional
public void transactionalIncrementMethod() {
Integer num = concurrenceSimulationRepository.selectNum(123);
ConcurrenceSimulation concurrenceSimulation = new ConcurrenceSimulation(123, num + 1);
concurrenceSimulationRepository.updateNum(concurrenceSimulation);
System.out.println("|" + Thread.currentThread().getName() + "\t|" + num + "->" + (num + 1) + "\t|");
}
/**
* 先加读未提交事务,后加同步锁
* 不会出现并发一致性问题
* 原因:由于默认事务+同步锁的一致性问题是在没有提交事务就读取原数据导致的
* 那么可以直接用读未提交事务,对未提交的事务进行读取即为最新的数据
*
* |Thread-10 |0->1 |
* |Thread-6 |1->2 |
* |Thread-9 |2->3 |
* |Thread-2 |3->4 |
* |Thread-4 |4->5 |
* |Thread-3 |5->6 |
* |Thread-11 |6->7 |
* |Thread-7 |7->8 |
* |Thread-5 |8->9 |
* |Thread-8 |9->10 |
*/
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public synchronized void incrementTest7() {
Integer num = concurrenceSimulationRepository.selectNum(123);
ConcurrenceSimulation concurrenceSimulation = new ConcurrenceSimulation(123, num + 1);
concurrenceSimulationRepository.updateNum(concurrenceSimulation);
System.out.println("|" + Thread.currentThread().getName() + "\t|" + num + "->" + (num + 1) + "\t|");
}
}
@Repository
interface ConcurrenceSimulationRepository {
/**
* 查询num值
*/
Integer selectNum(Integer id);
/**
* 更新num值
*/
Integer updateNum(ConcurrenceSimulation concurrenceSimulation);
}
@Data
@AllArgsConstructor
class ConcurrenceSimulation {
private Integer id;
private Integer num;
private static final String TABLE_NAME = "test_concurrence";
}
<?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.xch.mvc.ConcurrenceSimulationRepository">
<select id="selectNum" resultType="java.lang.Integer">
select num
from test_concurrence
where id = #{id,jdbcType=INTEGER}
</select>
<update id="updateNum" parameterType="com.xch.mvc.ConcurrenceSimulation">
update ${TABLE_NAME}
set num = #{num,jdbcType=INTEGER}
where id = #{id,jdbcType=INTEGER}
</update>
</mapper>