spring并发读写数据库

spring并发读写数据库

多线程写入数据库,事物通过AOP控制,无需影响业务代码

  1. 存在几个小问题还不太理解有大佬看懂了麻烦点拨一下
  2. 有好的优化代码思路欢迎交流
  3. 代码当前仓库不能公开,代码下载

待解决问题

   // 如果executor出现问题,导致子线程不执行,sonLatch.await() 会一直阻塞
  	CompletableFuture.runAsync(()->task(manLatch,sonLatch,rollback,finalI),getExecutor());
 private static AtomicBoolean  initFlag = new AtomicBoolean(true);
    private static ThreadPoolExecutor executor = null;

    // TODO CompletableFuture中使用此线程池有时候会导致某个线程阻塞,有人知道原因吗
    public static ThreadPoolExecutor getExecutor(){
        if(initFlag.getAndSet(false)){
            // 阻塞队列一定要设置大小,默认 Integer.Max
            // 线程工厂设置名称于业务名称方便排查问题
            executor=new ThreadPoolExecutor(2,5,60, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<>(512),
                    r->{
                        int i=0;
                        Thread thread = new Thread(r);
                        thread.setName("myTask-NO-" + i++);
                        return thread;
                    },
                    new ThreadPoolExecutor.CallerRunsPolicy()
            );
        }
        return executor;
    }

问题:使用CompletableFuture执行子任务时,使用自己的线程池就总会出现某个线程阻塞的情况。

猜测原因

  1. 自己的线程池有问题
  2. 线程A在执行任务子任务1的时候其实已经在ThreadLoacl中有的对应事务信息,当阻塞失去当前任务进度时,又获取到子任务2需要生成新的ThreadLoacl中的信息产生冲突

具体原因我找不到谁懂原理啊

基础实现原理

  1. spring事物时以当Thread.getName()作为事物信息的key,事物提交或回滚之前,需啊哟所有任务线程都存活
  2. 通过二段提交协议来统一事物的提交或回滚
// 先看一段代码,确保自己控制线程的阻塞和销毁时间
public class CountDo {
    public static void main(String[] args) throws InterruptedException {
        t1();
    }

    public static void t1() throws InterruptedException {
        // 主子线程计数 和回滚标志
        Integer sonCount=3;
        CountDownLatch manLatch = new CountDownLatch(1);
        CountDownLatch sonLatch = new CountDownLatch(sonCount);
        AtomicBoolean rollbackFlag = new AtomicBoolean(false);

        String str = "主线程任务";
        // 先走子线程
        for (int i = 0; i < sonCount; i++) {
            int finalI = i;// 这里非原子并发数据不准确
            CompletableFuture.runAsync(()->task(finalI,manLatch, sonLatch,rollbackFlag));
        }
        try {
            System.out.println( str+"执行");
            sonLatch.await(5, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 所有的线程都执行完成
        if(rollbackFlag.get()){
            str+="回滚";
        }else{
            str+="执行";
        }
        System.out.println(str);
        // 等待子线程结束 实用spring时不需要这里
        Thread.sleep(4000);
    }

    /**
     * 任务
     * @param i 任务次数
     * @param manLatch 主线程计数
     * @param sonLatch 子线程计数
     * @param rollbackFlag 回滚标志
     */
    public static void task(Integer i,
                            CountDownLatch manLatch,
                            CountDownLatch sonLatch,
                            AtomicBoolean rollbackFlag){
        String str = i + " 子线程任务" + Thread.currentThread().getName();
        try{
            // 这里这些主要任务 save 等
            System.out.println(str);
            if(i == 2){
                System.out.println("异常出现" + str);
                throw  new RuntimeException("tt");
            }
        }catch (RuntimeException e){
            // 出现问题设置标识
            rollbackFlag.set(true);
            System.out.println( str + " 异常:" + e);
        }finally {
            // 任务执行完成减1 通知主线程可以继续
            sonLatch.countDown();
        }
        try {
            // 等地所有线程结束 在继续完成任务
            manLatch.await(3,TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(rollbackFlag.get()){
            str = "回滚 " + str;
        }else{
            str = "成果" + str;
        }
        System.out.println(str);
    }
}
// 运行结果
主线程任务执行
0 子线程任务ForkJoinPool.commonPool-worker-1
2 子线程任务ForkJoinPool.commonPool-worker-3
异常出现2 子线程任务ForkJoinPool.commonPool-worker-3
1 子线程任务ForkJoinPool.commonPool-worker-2
2 子线程任务ForkJoinPool.commonPool-worker-3 异常:java.lang.RuntimeException: tt
主线程任务回滚
回滚 2 子线程任务ForkJoinPool.commonPool-worker-3
回滚 0 子线程任务ForkJoinPool.commonPool-worker-1
回滚 1 子线程任务ForkJoinPool.commonPool-worker-2

单独任务实现

 /**
     * 主子线程并发插入
     */
    @GetMapping("syncInsert")
    public void syncInsert(){
        Integer taskCount = 3;
        // 确保主线程子线程之间的阻塞,事物本身存储在ThreadLocal中线程销毁会session.Close()
        CountDownLatch manLatch = new CountDownLatch(1);
        CountDownLatch sonLatch = new CountDownLatch(taskCount);
        AtomicBoolean rollback = new AtomicBoolean(false);
        // 创建事物之后需要关闭或回滚事物,如果异常结束却未处理事物,数据库会出现死锁
        TransactionStatus transaction = manager.getTransaction(definition);
        // 子线程循环执行
        for (int i = 0; i < taskCount; i++) {
            int finalI = i;
            // 如果executor出现问题,导致子线程不执行,sonLatch.await() 会一直阻塞
            CompletableFuture.runAsync(()->task(manLatch,sonLatch,rollback, finalI));
        }
        // 主线程任务
        try {
            TestTable v = newVO("主线程任务");
            service.getBaseMapper().insert(v);
            log.info(Thread.currentThread().getName() + " 主线程任务完成");
        }catch (RuntimeException e){
            // 设置回滚标识
            rollback.set(true);
            log.warning(Thread.currentThread().getName() + " 主线程err" );
            e.printStackTrace();
        }
        try {
             // 等待所有子线程完成 设置超时时间
            // 这里设置时间后,即使没有归0也会正常执行
            sonLatch.await();
        } catch (RuntimeException | InterruptedException e){
            rollback.set(true);
            log.warning(Thread.currentThread().getName()+ " 子线程RunTime异常" );
            e.printStackTrace();
        }
        // 不处理线程会一直阻塞,最后线程池耗尽
        manLatch.countDown();

        if(rollback.get()){
            manager.rollback(transaction);
            log.warning("主线程回滚事物");
        }else{
            manager.commit(transaction);
        }
    }

    /**
     * 子线程任务
     */
    private void task(CountDownLatch manLatch, CountDownLatch sonLatch, AtomicBoolean rollback, int finalI){
        TransactionStatus transaction = manager.getTransaction(definition);
        try{
            TestTable v = newVO("子线程任务 " + finalI);
            service.getBaseMapper().insert(v);
            log.info(Thread.currentThread().getName() + " -任务"+finalI +"子线程任务完成" );
        }catch (RuntimeException e){
            rollback.set(true);
            log.warning(Thread.currentThread().getName() + " 子线程err" );
            e.printStackTrace();
        }finally {
            sonLatch.countDown();
        }
        // 等待主线程确认所有子线程任务执行结束
        // 这里等待时间要比主线程长一点,主线程确认完成后在通知子线程
        try {
            manLatch.await();
        } catch (RuntimeException | InterruptedException e){
            rollback.set(true);
            log.warning(Thread.currentThread().getName() + " 主线程RunTime异常" );
            e.printStackTrace();
        }
        if(rollback.get()){
            manager.rollback(transaction);
            log.warning(Thread.currentThread().getName() + " 子线程R回滚事物" );
        }else{
            manager.commit(transaction);
        }
    }

这段代码已经可以完成主子线程的事物提交或回滚

需要注意使用过程中manLatch.await(); 不设置时间的情况下需要处理好所有可能出现err的地方,不然线程进入阻塞之后,就无法在唤醒

通过AOP抽离事物控制的代码

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ManTransaction {
   // 子任务的数量这里可以设置 
    int taskCount() default 1;
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SonTransaction {
}

先定义2个注解来区分主线程任务和子线程任务

@Data
public class MyTransactionInfo {
    private CountDownLatch manLatch;
    private CountDownLatch sonLatch;
    private AtomicBoolean rollback;

    /**
     * @param taskNumber 子任务数量
     */
    public MyTransactionInfo(Integer taskNumber) {
        this.manLatch = new CountDownLatch(1);
        this.sonLatch = new CountDownLatch(taskNumber);
        this.rollback = new AtomicBoolean(false);
    }
}

主要参数作为对象,这样可以通过一个public static Map来全局的读取,主要是不知道怎么让2个注解标记的方法能传递数据。(有更好的思路欢迎交流)

@Component
@Aspect
public class MyTransaction {

    @Autowired
    private DataSourceTransactionManager manager;
    @Autowired
    private TransactionDefinition definition;
    /**
     * 方法唯一事物标识,需要通过形参列表传递
     */
    public static final String TASK_NAME="taskName";
 		// 这里提供了get方法,方便设置子任务数量时,可以动态的设置
    private static Map<String, MyTransactionInfo> taskMap = new ConcurrentHashMap<>();
    private static final AtomicBoolean AT_TRUE = new AtomicBoolean(true);

    /**
     * 主任务
     */
    @Around("@annotation(com.example.mybaitsplus.aop.ManTransaction)")
    public Object man(ProceedingJoinPoint joinPoint) throws Throwable {
        // 设置事务相关数据
        String key = verificationArgs(joinPoint);
        MyTransactionInfo myTransactionInfo;
        if(taskMap.get(key) == null){
            int taskCount = getTaskCount(joinPoint);
            myTransactionInfo = new MyTransactionInfo(taskCount);
            taskMap.put(key,myTransactionInfo);
        }else{
            myTransactionInfo = taskMap.get(key);
        }
        TransactionStatus transaction = manager.getTransaction(definition);

        // 当前方法主体
        Object proceed = null;
        try {
            proceed = joinPoint.proceed();
        }catch (RuntimeException e){
            myTransactionInfo.setRollback(AT_TRUE);
            e.printStackTrace();
        }

        try {
            // 设置10秒超时,避免线程假死
            myTransactionInfo.getSonLatch().await(10, TimeUnit.SECONDS);
            if(myTransactionInfo.getSonLatch().getCount() != 0){
                myTransactionInfo.setRollback(AT_TRUE);
            }
        }catch (RuntimeException e ){
            myTransactionInfo.setRollback(AT_TRUE);
            e.printStackTrace();
        }
        myTransactionInfo.getManLatch().countDown();
        if(myTransactionInfo.getRollback().get()){
            manager.rollback(transaction);
        }else{
            manager.commit(transaction);
        }
        return  proceed;
    }

    /**
     * 子任务
     */
    @Around("@annotation(com.example.mybaitsplus.aop.SonTransaction)")
    public Object sou(ProceedingJoinPoint joinPoint) throws Throwable {
        String key = verificationArgs(joinPoint);
        MyTransactionInfo myTransactionInfo = taskMap.get(key);
        if(myTransactionInfo == null){
            throw new RuntimeException("AOP子线程任务异常需要排查问题");
        }
        TransactionStatus transaction = manager.getTransaction(definition);
        // 任务主体
        Object proceed= null;
        try{
            proceed = joinPoint.proceed();
        }catch (RuntimeException e){
            myTransactionInfo.setRollback(AT_TRUE);
            e.printStackTrace();
        }finally {
            myTransactionInfo.getSonLatch().countDown();
        }
        myTransactionInfo.getManLatch().await();
        if(myTransactionInfo.getRollback().get()){
            manager.rollback(transaction);
        }else{
            manager.commit(transaction);
        }
        return  proceed;
    }

    /**
     * 校验方法参数取到 key
     */
    private String verificationArgs(ProceedingJoinPoint joinPoint){
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            if(args[i].toString().startsWith(TASK_NAME)){
                return args[i].toString();
            }
        }
        throw new RuntimeException("需要传入正确的事务key");
    }

    /**
     * 获取注解中的任务数量
     */
    private int getTaskCount(ProceedingJoinPoint joinPoint){
        MethodSignature sign =  (MethodSignature)joinPoint.getSignature();
        Method method = sign.getMethod();
        ManTransaction manAnnotation = method.getAnnotation(ManTransaction.class);
        return manAnnotation.taskCount();
    }

    public static Map<String, MyTransactionInfo> getTaskMap() {
        return taskMap;
    }
}

上面已经完成了事物控制的抽离。

在来看看使用注解控制需啊哟注意的点

    @GetMapping("syncAnnotation")
    public void syncAnnotation(){
        // 设置唯一标识 
        String taskKey = TASK_NAME + UUID.randomUUID().toString().replace("-", "");
        service.manTaskAnnotation(taskKey);
    }

这里是作为taskMap的key来存储 主任务和子任务都是同意个key 就作为同一个任务来处理,其实我也挺想把这个taskKey 换一种方式加到AOP里面 注解不好像不能动态的去设置属性也不知道怎么让这个属性从一个注解传递到另一个注解,最后还是感觉只能用形参

public interface TestTableService extends IService<TestTable> {
  // 这里用注解是没有用的,切记
    //@ManTransaction 
    public void manTaskAnnotation(String keyName);

    public void souTaskAnnotation(String keyName,Integer i);
}
 		@Override
    @ManTransaction(taskCount = 3)
    public void manTaskAnnotation(String keyName) {
        /*
            此处是内部调用,方法manTaskAnnotation调用方法souTaskAnnotation时,this已经被AOP代理
            所以直接调用就是非理类manTaskAnnotation调用了代类的souTaskAnnotation,导致souTaskAnnotation的AOP会失效
            解决处理 @EnableAspectJAutoProxy(exposeProxy = true) // 开启获取代理对象
            后获取代理对象来调用 TestTableService tableService = (TestTableService) AopContext.currentProxy();
         */
        TestTableService tableService = (TestTableService) AopContext.currentProxy();
        for (int i = 0; i < 3; i++) {
            int finalI = i;
            CompletableFuture.runAsync(()-> tableService.souTaskAnnotation(keyName,finalI));
        }
        TestTable vo = newVO("主线程插入,注解");
        mapper.insert(vo);
    }

    @Override
    @SonTransaction
    public void souTaskAnnotation(String keyName,Integer i) {
        TestTable vo = newVO(i + " 子线程任务,注解");
        mapper.insert(vo);
    }
@SpringBootApplication
@MapperScan(basePackages = "com.example.mybaitsplus.mapper")
@EnableAspectJAutoProxy(exposeProxy = true) // 开启获取代理对象
public class MybaitsplusApplication {
    public static void main(String[] args) {
        SpringApplication.run(MybaitsplusApplication.class, args);
    }
}

以上就是二段提交的事物管理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
读写分离是指将数据库的读和写操作分别放到不同的数据库服务器上,以提高数据库读写性能和并发能力。SpringBoot可以通过集成多个数据源来实现读写分离。 首先,在SpringBoot中配置多个数据源,在application.yml文件中配置: ```yaml spring: datasource: master: url: jdbc:mysql://localhost:3306/db_master?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver slave: url: jdbc:mysql://localhost:3307/db_slave?useUnicode=true&characterEncoding=utf-8&useSSL=false username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver ``` 其中,配置了一个主数据源(master)和一个从数据源(slave),两个数据源连接的是不同的数据库服务器。 接着,在SpringBoot中配置读写分离,可以使用MyBatis-Plus提供的DynamicDataSource类来实现,具体配置如下: ```java @Configuration public class DataSourceConfig { @Bean public DynamicDataSource dataSource() { Map<String, DataSource> targetDataSources = new HashMap<>(); DataSource master = DataSourceBuilder.create() .url("jdbc:mysql://localhost:3306/db_master?useUnicode=true&characterEncoding=utf-8&useSSL=false") .username("root") .password("root") .driverClassName("com.mysql.cj.jdbc.Driver") .build(); DataSource slave = DataSourceBuilder.create() .url("jdbc:mysql://localhost:3307/db_slave?useUnicode=true&characterEncoding=utf-8&useSSL=false") .username("root") .password("root") .driverClassName("com.mysql.cj.jdbc.Driver") .build(); targetDataSources.put("master", master); targetDataSources.put("slave", slave); DynamicDataSource dataSource = new DynamicDataSource(); dataSource.setTargetDataSources(targetDataSources); dataSource.setDefaultTargetDataSource(master); return dataSource; } @Bean public SqlSessionFactory sqlSessionFactory(DynamicDataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource); sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml")); return sessionFactory.getObject(); } @Bean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { return new SqlSessionTemplate(sqlSessionFactory); } } ``` 在这个配置类中,首先创建了一个DynamicDataSource类的实例,并将主数据源和从数据源都添加到了targetDataSources中,并将主数据源设置为默认数据源。接着配置了SqlSessionFactory和SqlSessionTemplate,这里使用了Mybatis-Plus提供的MybatisSqlSessionFactoryBean类来创建SqlSessionFactory。 最后,在需要访问数据库的地方,使用注解@DS指定数据源即可,例如: ```java @Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override @DS("master") public void addUser(User user) { userMapper.insert(user); } @Override @DS("slave") public User getUserById(int id) { return userMapper.selectById(id); } } ``` 在这个例子中,addUser方法使用了主数据源,getUserById方法使用了从数据源。通过@DS注解,可以让Spring动态切换数据源,从而实现读写分离。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值