spring事务传播特性(7种)。
- 必须有事务的
- REQUIRED如果没有,就新建一个事务,如果有,就加入当前事务(事务合并成一个)
- REQUIRES_NEW:有没有都新建事务,如果原来有,就将原来的挂起
- nested(嵌套): 如果没有,就新建一个事务,如果有,就在当前事务中嵌套其他事务
- mandatory(强制性):如果没有就抛出异常,如果有,就使用当前事务
- 可有可无的
- supports:有就用,没有就算了
- 死活都不要事务的
- never:没有就非事务执行,有就抛出异常
- not_supported:没有就非事务执行,有就挂起。然后非事务执行
验证方法如下:
首先建立两张表:sell_info和sell_info1(两张表字段一模一样,就表名不一样。)
然后建立一个service
//新增数据2
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void insert1(SellerInfo sellerInfo){
sellerInfoRepository.save(sellerInfo);
}
//新增数据2
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void insert2(SellerInfo2 sellerInfo2){
sellerInfo2Repository.save(sellerInfo2);
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void insert3Throw(SellerInfo2 sellerInfo2){
sellerInfo2Repository.save(sellerInfo2);
throw new RuntimeException();
}
场景1:REQUIRED事务,总结----外层没有事务,内层的事务独立运行,外层有事务,内层事务合并为外层。最外层为主。
//新增数据2
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void insert1(SellerInfo sellerInfo){
sellerInfoRepository.save(sellerInfo);
}
//新增数据2
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void insert2(SellerInfo2 sellerInfo2){
sellerInfo2Repository.save(sellerInfo2);
}
@Test
@DisplayName("REQUIRED:最外层没有事务,内层两个事务,可看成独立的两个单元,互不影响")
public void notransaction() throws Exception {
String sellId = UUID.randomUUID().toString().replace("-","").substring(0,10);
SellerInfo sellerInfo = new SellerInfo(sellId,"username","3456","111");
sellerService.insert1(sellerInfo);
SellerInfo2 sellerInfo2 = new SellerInfo2(sellId,"username2","3456","111");
sellerService.insert2(sellerInfo2);
throw new RuntimeException();
//这insert都执行成功
}
@Test
@DisplayName("REQUIRED:最外层没有事务,内层两个事务,可看成独立的两个单元,互不影响,有一个事务失败,不影响第二个事务执行成功")
public void notransaction_insert3_throw() throws Exception {
String sellId = UUID.randomUUID().toString().replace("-","").substring(0,10);
SellerInfo sellerInfo = new SellerInfo(sellId,"username","3456","111");
sellerService.insert1(sellerInfo);
SellerInfo2 sellerInfo2 = new SellerInfo2(sellId,"username2","3456","111");
sellerService.insert3Throw(sellerInfo2);
throw new RuntimeException();
//这里insert3回滚,insert1执行成功
}
@Test
@DisplayName("REQUIRED:外层开启一个事务,内层的两个事务,直接统一归外层管理,只要有异常,不管是内层还是外层都执行失败")
@Transactional(propagation = Propagation.REQUIRED)
public void transaction() throws Exception {
String sellId = UUID.randomUUID().toString().replace("-","").substring(0,10);
SellerInfo sellerInfo = new SellerInfo(sellId,"username","3456","111");
sellerService.insert1(sellerInfo);
SellerInfo2 sellerInfo2 = new SellerInfo2(sellId,"username2","3456","111");
// sellerService.insert2(sellerInfo2);
sellerService.insert3Throw(sellerInfo2);
// throw new RuntimeException();
}
- 场景2:REQUIRES_NEW:挂起原来的事务。
- 外层没有事务,内层一个a是REQUIRED,b是REQUIRES_NEW,他们俩互不影响,各种执行各自的。
- 外层有REQUIRED且外层抛异常,内层a是REQUIRED,b是REQUIRES_NEW,那么a受到最外层的影响,会回退,b单独事务,不受影响,执行成功。
- 外层有REQUIRED,内层a普通事务,b是new,c是new抛异常,结果是b成功,a,c全部失败(原因,c失败,整个方法都失败了,导致a失败。)
@Test
@Transactional(propagation = Propagation.REQUIRED)
public void transaction() throws Exception {
String sellId1 = UUID.randomUUID().toString().replace("-","").substring(0,10);
String sellId2 = UUID.randomUUID().toString().replace("-","").substring(0,10);
String sellId3 = UUID.randomUUID().toString().replace("-","").substring(0,10);
SellerInfo sellerInfo = new SellerInfo(sellId1,"username","3456","111");
sellerService.a_required(sellerInfo);
SellerInfo2 sellerInfo2 = new SellerInfo2(sellId2,"username2","3456","111");
sellerService.b_new(sellerInfo2);
SellerInfo2 sellerInfo3 = new SellerInfo2(sellId3,"username3","3456","111");
sellerService.c_new_throw(sellerInfo3);
}
外层Required,c是new且抛出异常,b是new,a是普通事务,那么结果是c异常带动整个方法都抛出异常了,这个异常造成了a失败回滚,带动a也失败。b成功
-
场景3:nested(嵌套)—如果没有则创建一个事务,如果有就在当前事务中嵌套其他事务
-
外层没有事务,内层a和b有nested事务:外层抛出异常,也不影响a,b,a,b是独立的
-
外层有事务,内层a,b是nested事务,c是nested抛出异常,那么abc全部失败。
总结:外层有事务 nested==required,他们两个的行为一模一样。
总结:
-
-
外层没有事务:
- 如果内层方法是required,nested,requires_new,每个都是独立的事务,各不影响。
-
外层有事务:
- 如果内层方法是required,nested,则看成一个整体,有异常,全部回滚。
- 如果内存是requires_new,则以requires_new新建一个独立的事务,不影响其他人。
Q:spring是如何解决循环依赖的问题?
循环依赖的场景:a中依赖b,b中依赖a,这就是循环依赖,构造方法注入是不能解决循环依赖的
@Component
public class ServiceA {
private ServiceB serviceB;
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Component
public class ServiceB {
private ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
启动项目会报:BeanCurrentlyInCreationException异常。
<bean id="beanA" class="com.example.demo.learn.bean.ServiceA" scope="singleton">
<property name="serviceB" ref="beanB" />
</bean>
<bean id="beanB" class="com.example.demo.learn.bean.ServiceB" scope="singleton">
<property name="serviceA" ref="beanA" />
</bean>
只有set方法,且是单例的才不会爆循环依赖,如果scope="prototype" 就会抛出
Requested bean is currently in creation: Is there an unresolvable circular reference?
- 三级缓存可以解决循环依赖。
- singletonObjects:一级缓存(单例池:存放经历了完整生命周期的bean对象)
- earlySingletonObjects:二级缓存(存放早期暴露出来的bean对象,bean生命周期尚未结束)
- singletonFactories:三级缓存----单例工厂(存放可以生成bean工厂–准备生产)
DefaultSingletonBeanRegistry
一级缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
三级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
二级缓存
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
- 3个map+四个方法(解决循环依赖)
- getSingleton
- doCreateBean
- populateBean
- addSingleton
Q:springboot自动装配原理?
前置知识:自动装配是基于条件判断来配置bean的
Configuration注解:往spring里注册组件
@Configuration(proxyBeanMethods = true)//代理组件对象
public class Myconfig {
@Bean//注入一个属性,也是一个组件,都是单实例
public User getUser01(){
return new User("zhangsan","18");
}
}
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(DemoApplication.class, args);
String[] beanDefinitionNames = run.getBeanDefinitionNames();
// for (String name : beanDefinitionNames){
// System.out.println(name);
// }
Myconfig myconfig = run.getBean("myconfig", Myconfig.class);
System.out.println(myconfig.getUser01() == myconfig.getUser01());
如果myconfig是代理对象,这里是单实例,相等的,如果是false,那么就不是单实例了
}
Import注解:导入
* @Import({User.class, BasicConfigurator.class})
* 给容器中自动创建这两个类的组件(无参构造器,全类名)
必须在组件里面
*/
@Import({User.class, BasicConfigurator.class})
@Configuration
Conditional注解:条件装配注解
@Bean
@ConditionalOnBean(name = "getUser02") //有getUser02这个组件,才会注入getUser01
public User getUser01(){
return new User("zhangsan","18");
}
// @Bean
public User getUser02(){
return new User("lisi","20");
}
@ConditionalOnMissingBean(name = "getUser02"):没有getUser02才会注入它自身。
ImportResource:注解
导入xml的配置文件,变成组件,如果你写了xml,没有导入,启动springboot是找不到这个组件bean的
@ImportResource("classpath:springContext.xml")
ConfigurationProperties注解:配置绑定
mycar:
name: 奥迪
price: 100
@Component
@ConfigurationProperties(prefix = "mycar")
public class Mycar {
private String name;
private String price;
//省略get set方法
}
Mycar bean = run.getBean(Mycar.class);
System.out.println("bean.getName() = " + bean.getName()); //奥迪
System.out.println("bean.getPrice() = " + bean.getPrice()); //100
EnableConfigurationProperties注解:开启自动绑定功能
@EnableConfigurationProperties(Mycar.class)
@Configuration
public class Myconfig {}
//它就不用使用@Component注解了,也能访问成功
@ConfigurationProperties(prefix = "mycar")
public class Mycar {}
主启动类有一个核心的注解@SpringBootApplication:它是一个复合的注解
SpringBootApplication包含下面三个
@SpringBootConfiguration//这里是一个Configuration注解,也就是配置注解
@EnableAutoConfiguration//这里是一个开启自动配置注解---这个注解就是自动装配注解
@ComponentScan//包扫描注解
其中最核心的就是EnableAutoConfiguration–自动装配就是由它引起的。
EnableAutoConfiguration包含这两个注解
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
-
@AutoConfigurationPackage //将启动类同级包下所有的组件注册。
-
@Import(AutoConfigurationPackages.Registrar.class)//将Registrar导入到组件中
-
Registrar就是将启动类所在的包下面的所有组件全部注册(com.example.demo),这里也就解释了,为什么启动类main必须在默认包下。
-
这里就是将启动类同级的包下的所有组件全部注册进来 public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0])); }
-
-
@Import(AutoConfigurationImportSelector.class)将这个类导入到组件bean中。
//获取spring-boot-autoconfigure-2.6.4.jar下meta-inf下的spring.factories所有配置+自己写的bean。(137个)
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//获取spring-boot-autoconfigure-2.6.4.jar下meta-inf下的spring.factories
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
}
后面就是按需加载了,比如
@ConditionalOnBean | 当容器里有指定的bean的条件下。 |
---|---|
@ConditionalOnMissingBean | 当容器里不存在指定bean的条件下。 |
@ConditionalOnClass | 当类路径下有指定类的条件下 |
@ConditionalOnMissingClass | 当类路径下不存在指定类的条件下 |
@ConditionalOnProperty | 指定的属性是否有指定的值,比如@ConditionalOnProperties(prefix=”xxx.xxx”, value=”enable”, matchIfMissing=true),代表当xxx.xxx为enable时条件的布尔值为true,如果没有设置的情况下也为true。 |
如果你想改变字符编码,有两种方式:
如果你都没写,那么springboot的组件已经定义了一个utf-8的了。这就是自动装配原理
通过配置类
@Configuration
public class Serport {
@Bean
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding("UTF-16");
return filter;
}
}
2.通过配置文件
server:
servlet:
encoding: UTF-16
总结
总结:springboot启动的时候,会先将主配置类(启动类)所在的包以及下级的组件全部加载进来,然后会将autoconfigure.jar包里的spring.factories里的所有EnableAutoConfiguration加载进来,后面会通过条件装配注解@Conditional还有它的子注解按条件加载,如果用户在application.yml中配置了一些属性,那么就以用户的为主,如果用户在代码中配置了,那也是以用户的为主。
学以致用:自己手写一个。
自定义一个starter
模仿springboot
新建一个空的项目,里面包含一个helloservice的maven项目,一个autoconfuration的springboot项目。
helloservice:
<groupId>org.example</groupId>
<artifactId>helloservice</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.ifugle.configure</groupId>
<artifactId>autoconfuration</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
autoconfuration:删掉一些。保留一些关键的
<groupId>com.ifugle.configure</groupId>
<artifactId>autoconfuration</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>autoconfuration</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
在autoconfuration包下新建一个service实现类(默认不放入组件中)
public class HelloService {
@Autowired
private HelloProperties helloProperties;
public String sayHello(String name){
return helloProperties.getPrefix()+":"+ name + ">>>"+helloProperties.getEndfix();
}
}
@ConfigurationProperties(prefix = "hello.spring")
public class HelloProperties {
private String prefix;
private String endfix;
//get set方法省略
}
自动装配类:HelloAutoConfiguration
@EnableConfigurationProperties(HelloProperties.class) //开启自动配置
@Configuration //是一个配置类
public class HelloAutoConfiguration {
@Bean
@ConditionalOnMissingBean(HelloService.class) //如果HelloService不在组件中,那么才会开启下面的组件
public HelloService helloService(){
HelloService helloService = new HelloService();
return helloService;
}
}
然后,对这两个项目打包maven clean,install,用的话是使用helloservice的包
新建一个springboot项目
在新项目中在建一个helloService组件(用来测试,如果容器中有helloService这个组件,就不会开启自己写的)
//这个类也可以不用加,只是为了测试,到底是走springboot的,还是你自己写的starter。
@Configuration
public class HelloConfig {
@Bean
public HelloService helloService(){
HelloService helloService = new HelloService();
return helloService;
}
}
@RestController
public class TestController {
@Autowired
private HelloService helloService;
@GetMapping("/hello")
public String getHello(String name) {
return helloService.sayHello(name);
}
}
yml里:
hello:
spring:
prefix: 前缀
endfix: 后缀
访问http://127.0.0.1:8080/hello?name=zhangsan
前缀:zhangsan>>>后缀
使用的话,直接
@RestController
public class TestController {
@Autowired
private HelloService helloService;
@GetMapping("/hello")
public String getHello(String name) {
return helloService.sayHello(name);
}
}
yml里:
hello:
spring:
prefix: 前缀
endfix: 后缀
访问http://127.0.0.1:8080/hello?name=zhangsan
前缀:zhangsan>>>后缀
springboot启动原理
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
分两步,debug断点调试,进入run方法里。
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
主流程:上面分两步,第一步,创建(实例化),第二步运行(run)。
-
创建
-
关键方法:getSpringFactoriesInstances()是通过spring类加载器,加载spring.factories对应的类。
-
从配置文件中查找BootstrapRegistryInitializer相关的启动类初始化器(空)
-
从spring.factories查找ApplicationContextInitializer相关的应用上下文启动器,
-
从spring.factories中查找ApplicationListener相关的应用监听器。
-
将上面查找的所有工具类加载进来,并放入到三个它们自己对应的list集合中
-
从配置中加载三个初始化器(bootstrap初始化器,AppContext应用上下文初始化器,Applistener应用监听器),并将这三初始化器放入到list集合中,并从所有的堆栈中查找main方法所在的堆栈,也就是主启动类。
-
-
运行
-
stopWatch.start():记录应用的启动时间
-
创建应用的引导上下文。createBootstrapContext
-
获取所有的运行监听器SpringApplicationRunListeners
-
配置要启动的环境。prepareEnvironment
-
打印banner图printBanner
-
创建应该上下文(最核心的,也就是创建ioc容器)
-
准备应用上下文
-
刷新应用(refresh核心方法)refreshContext
-
stop.stop(计算启动到现在的时间)并打印启动时间。
-
所有监听器通知应用上下文listeners.started(context);
-
如果有异常,则调用监听器的failed()方法
-
结束,返回ioc容器。
-
Q:springbean的生命周期?
10.MQ队列消息
- Q:如何保证消息不会被重复消费?
- 生产数据时,mq内部针对每条生产者发送消息生成一个msg-id。作为去重和幂等性的依据。消费队列,必须要求消息体中有一个唯一id(主键,订单id)作为去重的依据。
- 如果消息是数据库的insert操作,给这个消息做一个唯一的主键,有重复数据,可以使用update操作。
- 如果是redis的话,就用set(天然去重)。
- Q:如何处理数据丢失?
- 启用事务(不建议,是同步了)
- 生产者:使用confirm模式,在生产者启用confirm模式。
- 防止数据丢失,可以开启mq的持久化。消息写入后放入磁盘。
- 消费者:关闭mq的自动ACK
spring的过滤器和拦截器有什么区别
-
过滤器:filter,主要实现了filter接口的三个接口,分别是初始化,dofilter,销毁。依赖于servlet容器。
过滤拦截的是地址栏请求。过滤器实在进入容器后知行servlet之前后知行的,针对于处理业务之前的操作。
chain.doFilter(request, response)
- 拦截器:实现的是HandlerInterceptor接口,不依赖于servlet容器,能获取到所有的类,对类里的方法进行拦截。拦截器可以注入service,也可以调用业务逻辑,是aop的一种策略,可以理解为spring的容器管理的
spring事务哪种情况下会失效
-
数据库不支持事务,比如你的存储引擎是MyISAM。则spring事务会失效
-
事务方法未被spring管理,会失效
-
也就是没有被spring加载到ioc容器中。
public class ProductService { @Autowired private ProductDao productDao; @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateProductStockCountById(Integer stockCount, Long id){ productDao.updateProductStockCountById(stockCount, id); } }
-
-
方法名没有被public修饰也会失效。
@Transactional(propagation = Propagation.REQUIRES_NEW) private void updateProductStockCountById(Integer stockCount, Long id){ productDao.updateProductStockCountById(stockCount, id);
-
同一个类有两个方法a和b,a没加,b加了,a调用b,则失效
-
未配置事务管理器
-
@Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); }
-
-
方法传播特性不支持事务
@Transactional(propagation = Propagation.REQUIRED) public void submitOrder(){ this.updateProductStockCountById(1, 1L); } @Transactional(propagation = Propagation.NOT_SUPPORTED) public void updateProductStockCountById(Integer stockCount, Long id){ productDao.updateProductStockCountById(stockCount, id); } }
-
不正确捕获异常:a事务,没有捕获异常,b事务捕获了,a调用b,b回滚了,a没有回滚
-
错误标志异常类型:这是因为Spring中对于默认回滚的事务异常类型为RuntimeException,默认情况下,Spring事务中无法捕获到Exception异常,此时可以手动指定updateProductStockCountById()方法标注的事务异常类型
@Transactional(propagation = Propagation.REQUIRED,rollbackFor = Exception.class)
@Transactional(propagation = Propagation.REQUIRED) public void updateProductStockCountById(Integer stockCount, Long id){ try{ productDao.updateProductStockCountById(stockCount, id); }catch(Exception e){ logger.error("扣减库存异常:", e.getMesaage()); throw new Exception("扣减库存异常"); } }
Q:全局异常怎么捕获?
处理办法通过@ControllerAdvice来进行统一异常处理,@ExceptionHandler({ PayException.class })来指定需要捕获的各个类型的异常。这个异常护理是全局的,所有类似的异常,都会跑到这个方法里处理。